/* * Copyright (c) 2011, 2016, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javafx.scene.chart; import java.util.*; import javafx.animation.*; import javafx.application.Platform; import javafx.beans.NamedArg; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.scene.AccessibleRole; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.layout.StackPane; import javafx.scene.shape.*; import javafx.util.Duration; import com.sun.javafx.charts.Legend.LegendItem; import javafx.css.converter.BooleanConverter; import javafx.beans.property.BooleanProperty; import javafx.beans.value.WritableValue; import javafx.css.CssMetaData; import javafx.css.Styleable; import javafx.css.StyleableBooleanProperty; import javafx.css.StyleableProperty; /** * StackedAreaChart is a variation of {@link AreaChart} that displays trends of the * contribution of each value. (over time e.g.) The areas are stacked so that each * series adjoins but does not overlap the preceding series. This contrasts with * the Area chart where each series overlays the preceding series. * * The cumulative nature of the StackedAreaChart gives an idea of the total Y data * value at any given point along the X axis. * * Since data points across multiple series may not be common, StackedAreaChart * interpolates values along the line joining the data points whenever necessary. * * @since JavaFX 2.1 */ public class StackedAreaChart extends XYChart { // -------------- PRIVATE FIELDS ------------------------------------------ /** A multiplier for teh Y values that we store for each series, it is used to animate in a new series */ private Map, DoubleProperty> seriesYMultiplierMap = new HashMap<>(); // -------------- PUBLIC PROPERTIES ---------------------------------------- /** * When true, CSS styleable symbols are created for any data items that * don't have a symbol node specified. * @since JavaFX 8.0 */ private BooleanProperty createSymbols = new StyleableBooleanProperty(true) { @Override protected void invalidated() { for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) { Series series = getData().get(seriesIndex); for (int itemIndex = 0; itemIndex < series.getData().size(); itemIndex++) { Data item = series.getData().get(itemIndex); Node symbol = item.getNode(); if (get() && symbol == null) { // create any symbols symbol = createSymbol(series, getData().indexOf(series), item, itemIndex); if (null != symbol) { getPlotChildren().add(symbol); } } else if (!get() && symbol != null) { // remove symbols getPlotChildren().remove(symbol); symbol = null; item.setNode(null); } } } requestChartLayout(); } public Object getBean() { return this; } public String getName() { return "createSymbols"; } public CssMetaData,Boolean> getCssMetaData() { return StyleableProperties.CREATE_SYMBOLS; } }; /** * Indicates whether symbols for data points will be created or not. * * @return true if symbols for data points will be created and false otherwise. * @since JavaFX 8.0 */ public final boolean getCreateSymbols() { return createSymbols.getValue(); } public final void setCreateSymbols(boolean value) { createSymbols.setValue(value); } public final BooleanProperty createSymbolsProperty() { return createSymbols; } // -------------- CONSTRUCTORS ---------------------------------------------- /** * Construct a new Area Chart with the given axis * * @param xAxis The x axis to use * @param yAxis The y axis to use */ public StackedAreaChart(@NamedArg("xAxis") Axis xAxis, @NamedArg("yAxis") Axis yAxis) { this(xAxis,yAxis, FXCollections.>observableArrayList()); } /** * Construct a new Area Chart with the given axis and data. *

* Note: yAxis must be a ValueAxis, otherwise {@code IllegalArgumentException} is thrown. * * @param xAxis The x axis to use * @param yAxis The y axis to use * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart * * @throws java.lang.IllegalArgumentException if yAxis is not a ValueAxis */ public StackedAreaChart(@NamedArg("xAxis") Axis xAxis, @NamedArg("yAxis") Axis yAxis, @NamedArg("data") ObservableList> data) { super(xAxis,yAxis); if (!(yAxis instanceof ValueAxis)) { throw new IllegalArgumentException("Axis type incorrect, yAxis must be of ValueAxis type."); } setData(data); } // -------------- METHODS ------------------------------------------------------------------------------------------ private static double doubleValue(Number number) { return doubleValue(number, 0); } private static double doubleValue(Number number, double nullDefault) { return (number == null) ? nullDefault : number.doubleValue(); } @Override protected void dataItemAdded(Series series, int itemIndex, Data item) { final Node symbol = createSymbol(series, getData().indexOf(series), item, itemIndex); if (shouldAnimate()) { boolean animate = false; if (itemIndex > 0 && itemIndex < (series.getData().size()-1)) { animate = true; Data p1 = series.getData().get(itemIndex - 1); Data p2 = series.getData().get(itemIndex + 1); double x1 = getXAxis().toNumericValue(p1.getXValue()); double y1 = getYAxis().toNumericValue(p1.getYValue()); double x3 = getXAxis().toNumericValue(p2.getXValue()); double y3 = getYAxis().toNumericValue(p2.getYValue()); double x2 = getXAxis().toNumericValue(item.getXValue()); double y2 = getYAxis().toNumericValue(item.getYValue()); // //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1) double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1); item.setCurrentY(getYAxis().toRealValue(y)); item.setCurrentX(getXAxis().toRealValue(x2)); //2. we can simply use the midpoint on the line as well.. // double x = (x3 + x1)/2; // double y = (y3 + y1)/2; // item.setCurrentX(x); // item.setCurrentY(y); } else if (itemIndex == 0 && series.getData().size() > 1) { animate = true; item.setCurrentX(series.getData().get(1).getXValue()); item.setCurrentY(series.getData().get(1).getYValue()); } else if (itemIndex == (series.getData().size() - 1) && series.getData().size() > 1) { animate = true; int last = series.getData().size() - 2; item.setCurrentX(series.getData().get(last).getXValue()); item.setCurrentY(series.getData().get(last).getYValue()); } else if (symbol != null) { // fade in new symbol symbol.setOpacity(0); getPlotChildren().add(symbol); FadeTransition ft = new FadeTransition(Duration.millis(500),symbol); ft.setToValue(1); ft.play(); } if (animate) { animate( new KeyFrame(Duration.ZERO, (e) -> { if (symbol != null && !getPlotChildren().contains(symbol)) { getPlotChildren().add(symbol); } }, new KeyValue(item.currentYProperty(), item.getCurrentY()), new KeyValue(item.currentXProperty(), item.getCurrentX()) ), new KeyFrame(Duration.millis(800), new KeyValue(item.currentYProperty(), item.getYValue(), Interpolator.EASE_BOTH), new KeyValue(item.currentXProperty(), item.getXValue(), Interpolator.EASE_BOTH)) ); } } else if (symbol != null) { getPlotChildren().add(symbol); } } @Override protected void dataItemRemoved(final Data item, final Series series) { final Node symbol = item.getNode(); if (symbol != null) { symbol.focusTraversableProperty().unbind(); } // remove item from sorted list int itemIndex = series.getItemIndex(item); if (shouldAnimate()) { boolean animate = false; // dataSize represents size of currently visible data. After this operation, the number will decrement by 1 final int dataSize = series.getDataSize(); // This is the size of current data list in Series. Note that it might be totaly different from dataSize as // some big operation might have happened on the list. final int dataListSize = series.getData().size(); if (itemIndex > 0 && itemIndex < dataSize - 1) { animate = true; Data p1 = series.getItem(itemIndex - 1); Data p2 = series.getItem(itemIndex + 1); double x1 = getXAxis().toNumericValue(p1.getXValue()); double y1 = getYAxis().toNumericValue(p1.getYValue()); double x3 = getXAxis().toNumericValue(p2.getXValue()); double y3 = getYAxis().toNumericValue(p2.getYValue()); double x2 = getXAxis().toNumericValue(item.getXValue()); double y2 = getYAxis().toNumericValue(item.getYValue()); // //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1) double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1); item.setCurrentX(getXAxis().toRealValue(x2)); item.setCurrentY(getYAxis().toRealValue(y2)); item.setXValue(getXAxis().toRealValue(x2)); item.setYValue(getYAxis().toRealValue(y)); //2. we can simply use the midpoint on the line as well.. // double x = (x3 + x1)/2; // double y = (y3 + y1)/2; // item.setCurrentX(x); // item.setCurrentY(y); } else if (itemIndex == 0 && dataListSize > 1) { animate = true; item.setXValue(series.getData().get(0).getXValue()); item.setYValue(series.getData().get(0).getYValue()); } else if (itemIndex == (dataSize - 1) && dataListSize > 1) { animate = true; int last = dataListSize - 1; item.setXValue(series.getData().get(last).getXValue()); item.setYValue(series.getData().get(last).getYValue()); } else if (symbol != null) { // fade out symbol symbol.setOpacity(0); FadeTransition ft = new FadeTransition(Duration.millis(500),symbol); ft.setToValue(0); ft.setOnFinished(actionEvent -> { getPlotChildren().remove(symbol); removeDataItemFromDisplay(series, item); symbol.setOpacity(1.0); }); ft.play(); } else { item.setSeries(null); removeDataItemFromDisplay(series, item); } if (animate) { animate( new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(), item.getCurrentY()), new KeyValue(item.currentXProperty(), item.getCurrentX())), new KeyFrame(Duration.millis(800), actionEvent -> { getPlotChildren().remove(symbol); removeDataItemFromDisplay(series, item); }, new KeyValue(item.currentYProperty(), item.getYValue(), Interpolator.EASE_BOTH), new KeyValue(item.currentXProperty(), item.getXValue(), Interpolator.EASE_BOTH)) ); } } else { getPlotChildren().remove(symbol); removeDataItemFromDisplay(series, item); } //Note: better animation here, point should move from old position to new position at center point between prev and next symbols } /** {@inheritDoc} */ @Override protected void dataItemChanged(Data item) { } @Override protected void seriesChanged(ListChangeListener.Change c) { // Update style classes for all series lines and symbols for (int i = 0; i < getDataSize(); i++) { final Series s = getData().get(i); Path seriesLine = (Path)((Group)s.getNode()).getChildren().get(1); Path fillPath = (Path)((Group)s.getNode()).getChildren().get(0); seriesLine.getStyleClass().setAll("chart-series-area-line", "series" + i, s.defaultColorStyleClass); fillPath.getStyleClass().setAll("chart-series-area-fill", "series" + i, s.defaultColorStyleClass); for (int j=0; j < s.getData().size(); j++) { final Data item = s.getData().get(j); final Node node = item.getNode(); if(node!=null) node.getStyleClass().setAll("chart-area-symbol", "series" + i, "data" + j, s.defaultColorStyleClass); } } } @Override protected void seriesAdded(Series series, int seriesIndex) { // create new paths for series Path seriesLine = new Path(); Path fillPath = new Path(); seriesLine.setStrokeLineJoin(StrokeLineJoin.BEVEL); fillPath.setStrokeLineJoin(StrokeLineJoin.BEVEL); Group areaGroup = new Group(fillPath,seriesLine); series.setNode(areaGroup); // create series Y multiplier DoubleProperty seriesYAnimMultiplier = new SimpleDoubleProperty(this, "seriesYMultiplier"); seriesYMultiplierMap.put(series, seriesYAnimMultiplier); // handle any data already in series if (shouldAnimate()) { seriesYAnimMultiplier.setValue(0d); } else { seriesYAnimMultiplier.setValue(1d); } getPlotChildren().add(areaGroup); List keyFrames = new ArrayList(); if (shouldAnimate()) { // animate in new series keyFrames.add(new KeyFrame(Duration.ZERO, new KeyValue(areaGroup.opacityProperty(), 0), new KeyValue(seriesYAnimMultiplier, 0) )); keyFrames.add(new KeyFrame(Duration.millis(200), new KeyValue(areaGroup.opacityProperty(), 1) )); keyFrames.add(new KeyFrame(Duration.millis(500), new KeyValue(seriesYAnimMultiplier, 1) )); } for (int j=0; j item = series.getData().get(j); final Node symbol = createSymbol(series, seriesIndex, item, j); if (symbol != null) { if (shouldAnimate()) symbol.setOpacity(0); getPlotChildren().add(symbol); if (shouldAnimate()) { // fade in new symbol keyFrames.add(new KeyFrame(Duration.ZERO, new KeyValue(symbol.opacityProperty(), 0))); keyFrames.add(new KeyFrame(Duration.millis(200), new KeyValue(symbol.opacityProperty(), 1))); } } } if (shouldAnimate()) animate(keyFrames.toArray(new KeyFrame[keyFrames.size()])); } @Override protected void seriesRemoved(final Series series) { // remove series Y multiplier seriesYMultiplierMap.remove(series); // remove all symbol nodes if (shouldAnimate()) { Timeline tl = new Timeline(createSeriesRemoveTimeLine(series, 400)); tl.play(); } else { getPlotChildren().remove(series.getNode()); for (Data d:series.getData()) getPlotChildren().remove(d.getNode()); removeSeriesFromDisplay(series); } } /** {@inheritDoc} */ @Override protected void updateAxisRange() { // This override is necessary to update axis range based on cumulative Y value for the // Y axis instead of the normal way where max value in the data range is used. final Axis xa = getXAxis(); final Axis ya = getYAxis(); if (xa.isAutoRanging()) { List xData = new ArrayList(); for(Series series : getData()) { for(Data data: series.getData()) { xData.add(data.getXValue()); } } xa.invalidateRange(xData); } if (ya.isAutoRanging()) { double totalMinY = Double.MAX_VALUE; Iterator> seriesIterator = getDisplayedSeriesIterator(); boolean first = true; NavigableMap accum = new TreeMap<>(); NavigableMap prevAccum = new TreeMap<>(); NavigableMap currentValues = new TreeMap<>(); while (seriesIterator.hasNext()) { currentValues.clear(); Series series = seriesIterator.next(); for(Data item : series.getData()) { if(item != null) { final double xv = xa.toNumericValue(item.getXValue()); final double yv = ya.toNumericValue(item.getYValue()); currentValues.put(xv, yv); if (first) { // On the first pass, just fill the map accum.put(xv, yv); // minimum is applicable only in the first series totalMinY = Math.min(totalMinY, yv); } else { if (prevAccum.containsKey(xv)) { accum.put(xv, prevAccum.get(xv) + yv); } else { // If the point wasn't yet in the previous (accumulated) series Map.Entry he = prevAccum.higherEntry(xv); Map.Entry le = prevAccum.lowerEntry(xv); if (he != null && le != null) { // If there's both point above and below this point, interpolate accum.put(xv, ((xv - le.getKey()) / (he.getKey() - le.getKey())) * (le.getValue() + he.getValue()) + yv); } else if (he != null) { // The point is before the first point in the previously accumulated series accum.put(xv, he.getValue() + yv); } else if (le != null) { // The point is after the last point in the previously accumulated series accum.put(xv, le.getValue() + yv); } else { // The previously accumulated series is empty accum.put(xv, yv); } } } } } // Now update all the keys that were in the previous series, but not in the new one for (Map.Entry e : prevAccum.entrySet()) { if (accum.keySet().contains(e.getKey())) { continue; } Double k = e.getKey(); final Double v = e.getValue(); // Look at the values of the current series Map.Entry he = currentValues.higherEntry(k); Map.Entry le = currentValues.lowerEntry(k); if (he != null && le != null) { // Interpolate the for the point from current series and add the accumulated value accum.put(k, ((k - le.getKey()) / (he.getKey() - le.getKey())) * (le.getValue() + he.getValue()) + v); } else if (he != null) { // There accumulated value is before the first value in the current series accum.put(k, he.getValue() + v); } else if (le != null) { // There accumulated value is after the last value in the current series accum.put(k, le.getValue() + v); } else { // The current series are empty accum.put(k, v); } } prevAccum.clear(); prevAccum.putAll(accum); accum.clear(); first = (totalMinY == Double.MAX_VALUE); // If there was already some value in the series, we can consider as // being past the first series } if(totalMinY != Double.MAX_VALUE) ya.invalidateRange(Arrays.asList(ya.toRealValue(totalMinY), ya.toRealValue(Collections.max(prevAccum.values())))); } } /** {@inheritDoc} */ @Override protected void layoutPlotChildren() { ArrayList> currentSeriesData = new ArrayList<>(); // AggregateData hold the data points of both the current and the previous series. // The goal is to collect all the data, sort it and iterate. ArrayList> aggregateData = new ArrayList<>(); for (int seriesIndex=0; seriesIndex < getDataSize(); seriesIndex++) { // for every series Series series = getData().get(seriesIndex); aggregateData.clear(); // copy currentSeriesData accumulated in the previous iteration to aggregate. for(DataPointInfo data : currentSeriesData) { data.partOf = PartOf.PREVIOUS; aggregateData.add(data); } currentSeriesData.clear(); // now copy actual data of the current series. for (Iterator> it = getDisplayedDataIterator(series); it.hasNext(); ) { Data item = it.next(); DataPointInfo itemInfo = new DataPointInfo<>(item, item.getXValue(), item.getYValue(), PartOf.CURRENT); aggregateData.add(itemInfo); } DoubleProperty seriesYAnimMultiplier = seriesYMultiplierMap.get(series); Path seriesLine = (Path)((Group)series.getNode()).getChildren().get(1); Path fillPath = (Path)((Group)series.getNode()).getChildren().get(0); seriesLine.getElements().clear(); fillPath.getElements().clear(); int dataIndex = 0; // Sort data points from prev and current series sortAggregateList(aggregateData); Axis yAxis = getYAxis(); Axis xAxis = getXAxis(); boolean firstCurrent = false; boolean lastCurrent = false; int firstCurrentIndex = findNextCurrent(aggregateData, -1); int lastCurrentIndex = findPreviousCurrent(aggregateData, aggregateData.size()); double basePosition = yAxis.getZeroPosition(); if (Double.isNaN(basePosition)) { ValueAxis valueYAxis = (ValueAxis) yAxis; if (valueYAxis.getLowerBound() > 0) { basePosition = valueYAxis.getDisplayPosition(valueYAxis.getLowerBound()); } else { basePosition = valueYAxis.getDisplayPosition(valueYAxis.getUpperBound()); } } // Iterate over the aggregate data : this process accumulates data points // cumulatively from the bottom to top of stack for (DataPointInfo dataInfo : aggregateData) { if (dataIndex == lastCurrentIndex) lastCurrent = true; if (dataIndex == firstCurrentIndex) firstCurrent = true; final Data item = dataInfo.dataItem; if (dataInfo.partOf.equals(PartOf.CURRENT)) { // handle data from current series int pIndex = findPreviousPrevious(aggregateData, dataIndex); int nIndex = findNextPrevious(aggregateData, dataIndex); DataPointInfo prevPoint; DataPointInfo nextPoint; if (pIndex == -1 || (nIndex == -1 && !(aggregateData.get(pIndex).x.equals(dataInfo.x)))) { if (firstCurrent) { // Need to add the drop down point. Data ddItem = new Data(dataInfo.x, 0); addDropDown(currentSeriesData, ddItem, ddItem.getXValue(), ddItem.getYValue(), xAxis.getDisplayPosition(ddItem.getCurrentX()), basePosition); } double x = xAxis.getDisplayPosition(item.getCurrentX()); double y = yAxis.getDisplayPosition( yAxis.toRealValue(yAxis.toNumericValue(item.getCurrentY()) * seriesYAnimMultiplier.getValue())); addPoint(currentSeriesData, item, item.getXValue(), item.getYValue(), x, y, PartOf.CURRENT, false, (firstCurrent) ? false : true); if (dataIndex == lastCurrentIndex) { // need to add drop down point Data ddItem = new Data(dataInfo.x, 0); addDropDown(currentSeriesData, ddItem, ddItem.getXValue(), ddItem.getYValue(), xAxis.getDisplayPosition(ddItem.getCurrentX()), basePosition); } } else { prevPoint = aggregateData.get(pIndex); if (prevPoint.x.equals(dataInfo.x)) { // Need to add Y values // Check if prevPoint is a dropdown - as the stable sort preserves the order. // If so, find the non dropdown previous point on previous series. if (prevPoint.dropDown) { pIndex = findPreviousPrevious(aggregateData, pIndex); prevPoint = aggregateData.get(pIndex); // If lastCurrent - add this drop down } if (prevPoint.x.equals(dataInfo.x)) { // simply add double x = xAxis.getDisplayPosition(item.getCurrentX()); final double yv = yAxis.toNumericValue(item.getCurrentY()) + yAxis.toNumericValue(prevPoint.y); double y = yAxis.getDisplayPosition( yAxis.toRealValue(yv * seriesYAnimMultiplier.getValue())); addPoint(currentSeriesData, item, dataInfo.x, yAxis.toRealValue(yv), x, y, PartOf.CURRENT, false, (firstCurrent) ? false : true); } if (lastCurrent) { addDropDown(currentSeriesData, item, prevPoint.x, prevPoint.y, prevPoint.displayX, prevPoint.displayY); } } else { // interpolate nextPoint = (nIndex == -1) ? null : aggregateData.get(nIndex); prevPoint = (pIndex == -1) ? null : aggregateData.get(pIndex); final double yValue = yAxis.toNumericValue(item.getCurrentY()); if (prevPoint != null && nextPoint != null) { double x = xAxis.getDisplayPosition(item.getCurrentX()); double displayY = interpolate(prevPoint.displayX, prevPoint.displayY, nextPoint.displayX, nextPoint.displayY, x); double dataY = interpolate(xAxis.toNumericValue(prevPoint.x), yAxis.toNumericValue(prevPoint.y), xAxis.toNumericValue(nextPoint.x), yAxis.toNumericValue(nextPoint.y), xAxis.toNumericValue(dataInfo.x)); if (firstCurrent) { // now create the drop down point Data ddItem = new Data(dataInfo.x, dataY); addDropDown(currentSeriesData, ddItem, dataInfo.x, yAxis.toRealValue(dataY), x, displayY); } double y = yAxis.getDisplayPosition(yAxis.toRealValue((yValue + dataY) * seriesYAnimMultiplier.getValue())); // Add the current point addPoint(currentSeriesData, item, dataInfo.x, yAxis.toRealValue(yValue + dataY), x, y, PartOf.CURRENT, false, (firstCurrent) ? false : true); if (dataIndex == lastCurrentIndex) { // add drop down point Data ddItem = new Data(dataInfo.x, dataY); addDropDown(currentSeriesData, ddItem, dataInfo.x, yAxis.toRealValue(dataY), x, displayY); } // Note: add drop down if last current } else { // we do not need to take care of this as it is // already handled above with check of if(pIndex == -1 or nIndex == -1) } } } } else { // handle data from Previous series. int pIndex = findPreviousCurrent(aggregateData, dataIndex); int nIndex = findNextCurrent(aggregateData, dataIndex); DataPointInfo prevPoint; DataPointInfo nextPoint; if (dataInfo.dropDown) { if (xAxis.toNumericValue(dataInfo.x) <= xAxis.toNumericValue(aggregateData.get(firstCurrentIndex).x) || xAxis.toNumericValue(dataInfo.x) > xAxis.toNumericValue(aggregateData.get(lastCurrentIndex).x)) { addDropDown(currentSeriesData, item, dataInfo.x, dataInfo.y, dataInfo.displayX, dataInfo.displayY); } } else { if (pIndex == -1 || nIndex == -1) { addPoint(currentSeriesData, item, dataInfo.x, dataInfo.y, dataInfo.displayX, dataInfo.displayY, PartOf.CURRENT, true, false); } else { nextPoint = aggregateData.get(nIndex); if (nextPoint.x.equals(dataInfo.x)) { // do nothing as the current point is already there. } else { // interpolate on the current series. prevPoint = aggregateData.get(pIndex); double x = xAxis.getDisplayPosition(item.getCurrentX()); double dataY = interpolate(xAxis.toNumericValue(prevPoint.x), yAxis.toNumericValue(prevPoint.y), xAxis.toNumericValue(nextPoint.x), yAxis.toNumericValue(nextPoint.y), xAxis.toNumericValue(dataInfo.x)); final double yv = yAxis.toNumericValue(dataInfo.y) + dataY; double y = yAxis.getDisplayPosition( yAxis.toRealValue(yv * seriesYAnimMultiplier.getValue())); addPoint(currentSeriesData, new Data(dataInfo.x, dataY), dataInfo.x, yAxis.toRealValue(yv), x, y, PartOf.CURRENT, true, true); } } } } dataIndex++; if (firstCurrent) firstCurrent = false; if (lastCurrent) lastCurrent = false; } // end of inner for loop // Draw the SeriesLine and Series fill if (!currentSeriesData.isEmpty()) { seriesLine.getElements().add(new MoveTo(currentSeriesData.get(0).displayX, currentSeriesData.get(0).displayY)); fillPath.getElements().add(new MoveTo(currentSeriesData.get(0).displayX, currentSeriesData.get(0).displayY)); } for (DataPointInfo point : currentSeriesData) { if (point.lineTo) { seriesLine.getElements().add(new LineTo(point.displayX, point.displayY)); } else { seriesLine.getElements().add(new MoveTo(point.displayX, point.displayY)); } fillPath.getElements().add(new LineTo(point.displayX, point.displayY)); // draw symbols only for actual data points and skip for interpolated points. if (!point.skipSymbol) { Node symbol = point.dataItem.getNode(); if (symbol != null) { final double w = symbol.prefWidth(-1); final double h = symbol.prefHeight(-1); symbol.resizeRelocate(point.displayX-(w/2), point.displayY-(h/2),w,h); } } } for(int i = aggregateData.size()-1; i > 0; i--) { DataPointInfo point = aggregateData.get(i); if (PartOf.PREVIOUS.equals(point.partOf)) { fillPath.getElements().add(new LineTo(point.displayX, point.displayY)); } } if (!fillPath.getElements().isEmpty()) { fillPath.getElements().add(new ClosePath()); } } // end of out for loop } private void addDropDown(ArrayList> currentSeriesData, Data item, X xValue, Y yValue, double x, double y) { DataPointInfo dropDownDataPoint = new DataPointInfo<>(true); dropDownDataPoint.setValues(item, xValue, yValue, x, y, PartOf.CURRENT, true, false); currentSeriesData.add(dropDownDataPoint); } private void addPoint(ArrayList> currentSeriesData, Data item, X xValue, Y yValue, double x, double y, PartOf partof, boolean symbol, boolean lineTo) { DataPointInfo currentDataPoint = new DataPointInfo<>(); currentDataPoint.setValues(item, xValue, yValue, x, y, partof, symbol, lineTo); currentSeriesData.add(currentDataPoint); } //-------------------- helper methods to retrieve data points from the previous // or current data series. private int findNextCurrent(ArrayList> points, int index) { for(int i = index+1; i < points.size(); i++) { if (points.get(i).partOf.equals(PartOf.CURRENT)) { return i; } } return -1; } private int findPreviousCurrent(ArrayList> points, int index) { for(int i = index-1; i >= 0; i--) { if (points.get(i).partOf.equals(PartOf.CURRENT)) { return i; } } return -1; } private int findPreviousPrevious(ArrayList> points, int index) { for(int i = index-1; i >= 0; i--) { if (points.get(i).partOf.equals(PartOf.PREVIOUS)) { return i; } } return -1; } private int findNextPrevious(ArrayList> points, int index) { for(int i = index+1; i < points.size(); i++) { if (points.get(i).partOf.equals(PartOf.PREVIOUS)) { return i; } } return -1; } private void sortAggregateList(ArrayList> aggregateList) { Collections.sort(aggregateList, (o1, o2) -> { Data d1 = o1.dataItem; Data d2 = o2.dataItem; double val1 = getXAxis().toNumericValue(d1.getXValue()); double val2 = getXAxis().toNumericValue(d2.getXValue()); return (val1 < val2 ? -1 : ( val1 == val2) ? 0 : 1); }); } private double interpolate(double lowX, double lowY, double highX, double highY, double x) { // using y = mx+c find the y for the given x. return (((highY - lowY)/(highX - lowX))*(x - lowX))+lowY; } private Node createSymbol(Series series, int seriesIndex, final Data item, int itemIndex) { Node symbol = item.getNode(); // check if symbol has already been created if (symbol == null && getCreateSymbols()) { symbol = new StackPane(); symbol.setAccessibleRole(AccessibleRole.TEXT); symbol.setAccessibleRoleDescription("Point"); symbol.focusTraversableProperty().bind(Platform.accessibilityActiveProperty()); item.setNode(symbol); } // set symbol styles // Note not sure if we want to add or check, ie be more careful and efficient here if (symbol != null) symbol.getStyleClass().setAll("chart-area-symbol", "series" + seriesIndex, "data" + itemIndex, series.defaultColorStyleClass); return symbol; } @Override LegendItem createLegendItemForSeries(Series series, int seriesIndex) { LegendItem legendItem = new LegendItem(series.getName()); legendItem.getSymbol().getStyleClass().addAll("chart-area-symbol", "series" + seriesIndex, "area-legend-symbol", series.defaultColorStyleClass); return legendItem; } // -------------- INNER CLASSES -------------------------------------------- /* * Helper class to hold data and display and other information for each * data point */ final static class DataPointInfo { X x; Y y; double displayX; double displayY; Data dataItem; PartOf partOf; boolean skipSymbol = false; // interpolated point - skip drawing symbol boolean lineTo = false; // should there be a lineTo to this point on SeriesLine. boolean dropDown = false; // Is this a drop down point ( non data point). //----- Constructors -------------------- DataPointInfo() {} DataPointInfo(Data item, X x, Y y, PartOf partOf) { this.dataItem = item; this.x = x; this.y = y; this.partOf = partOf; } DataPointInfo(boolean dropDown) { this.dropDown = dropDown; } void setValues(Data item, X x, Y y, double dx, double dy, PartOf partOf, boolean skipSymbol, boolean lineTo) { this.dataItem = item; this.x = x; this.y = y; this.displayX = dx; this.displayY = dy; this.partOf = partOf; this.skipSymbol = skipSymbol; this.lineTo = lineTo; } public final X getX() { return x; } public final Y getY() { return y; } } // To indicate if the data point belongs to the current or the previous series. private static enum PartOf { CURRENT, PREVIOUS } // -------------- STYLESHEET HANDLING -------------------------------------- private static class StyleableProperties { private static final CssMetaData, Boolean> CREATE_SYMBOLS = new CssMetaData, Boolean>("-fx-create-symbols", BooleanConverter.getInstance(), Boolean.TRUE) { @Override public boolean isSettable(StackedAreaChart node) { return node.createSymbols == null || !node.createSymbols.isBound(); } @Override public StyleableProperty getStyleableProperty(StackedAreaChart node) { return (StyleableProperty)(WritableValue)node.createSymbolsProperty(); } }; private static final List> STYLEABLES; static { final List> styleables = new ArrayList>(XYChart.getClassCssMetaData()); styleables.add(CREATE_SYMBOLS); STYLEABLES = Collections.unmodifiableList(styleables); } } /** * @return The CssMetaData associated with this class, which may include the * CssMetaData of its superclasses. * @since JavaFX 8.0 */ public static List> getClassCssMetaData() { return StyleableProperties.STYLEABLES; } /** * {@inheritDoc} * @since JavaFX 8.0 */ @Override public List> getCssMetaData() { return getClassCssMetaData(); } }