Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.scene.chart;
027
028
029import java.util.*;
030import javafx.animation.*;
031import javafx.beans.property.DoubleProperty;
032import javafx.beans.property.SimpleDoubleProperty;
033import javafx.collections.FXCollections;
034import javafx.collections.ListChangeListener;
035import javafx.collections.ObservableList;
036import javafx.event.ActionEvent;
037import javafx.event.EventHandler;
038import javafx.scene.Group;
039import javafx.scene.Node;
040import javafx.scene.layout.StackPane;
041import javafx.scene.shape.*;
042import javafx.util.Duration;
043import com.sun.javafx.charts.Legend;
044import com.sun.javafx.charts.Legend.LegendItem;
045import com.sun.javafx.css.converters.BooleanConverter;
046import javafx.beans.property.BooleanProperty;
047import javafx.css.CssMetaData;
048import javafx.css.Styleable;
049import javafx.css.StyleableBooleanProperty;
050import javafx.css.StyleableProperty;
051import javafx.scene.paint.Color;
052
053/**
054 * StackedAreaChart is a variation of {@link AreaChart} that displays trends of the 
055 * contribution of each value. (over time e.g.) The areas are stacked so that each 
056 * series adjoins but does not overlap the preceding series. This contrasts with 
057 * the Area chart where each series overlays the preceding series. 
058 * 
059 * The cumulative nature of the StackedAreaChart gives an idea of the total Y data 
060 * value at any given point along the X axis.
061 * 
062 * Since data points across multiple series may not be common, StackedAreaChart
063 * interpolates values along the line joining the data points whenever necessary.
064 * 
065 */
066public class StackedAreaChart<X,Y> extends XYChart<X,Y> {
067
068    // -------------- PRIVATE FIELDS ------------------------------------------
069
070    /** A multiplier for teh Y values that we store for each series, it is used to animate in a new series */
071    private Map<Series, DoubleProperty> seriesYMultiplierMap = new HashMap<Series, DoubleProperty>();
072    private Legend legend = new Legend();
073
074    // -------------- PUBLIC PROPERTIES ----------------------------------------
075    /**
076     * When true, CSS styleable symbols are created for any data items that
077     * don't have a symbol node specified.
078     */
079    private BooleanProperty createSymbols = new StyleableBooleanProperty(true) {
080        @Override
081        protected void invalidated() {
082            for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) {
083                Series<X, Y> series = getData().get(seriesIndex);
084                for (int itemIndex = 0; itemIndex < series.getData().size(); itemIndex++) {
085                    Data<X, Y> item = series.getData().get(itemIndex);
086                    Node symbol = item.getNode();
087                    if (get() && symbol == null) { // create any symbols
088                        symbol = createSymbol(series, getData().indexOf(series), item, itemIndex);
089                        if (null != symbol) {
090                            getPlotChildren().add(symbol);
091                        }
092                    } else if (!get() && symbol != null) { // remove symbols
093                        getPlotChildren().remove(symbol);
094                        symbol = null;
095                        item.setNode(null);
096                    }
097                }
098            }
099            requestChartLayout();
100        }
101
102        public Object getBean() {
103            return this;
104        }
105
106        public String getName() {
107            return "createSymbols";
108        }
109
110        public CssMetaData getCssMetaData() {
111            return StyleableProperties.CREATE_SYMBOLS;
112        }
113    };
114
115    /**
116     * Indicates whether symbols for data points will be created or not.
117     *
118     * @return true if symbols for data points will be created and false otherwise.
119     */
120    public final boolean getCreateSymbols() { return createSymbols.getValue(); }
121    public final void setCreateSymbols(boolean value) { createSymbols.setValue(value); }
122    public final BooleanProperty createSymbolsProperty() { return createSymbols; }
123    
124    // -------------- CONSTRUCTORS ----------------------------------------------
125
126    /**
127     * Construct a new Area Chart with the given axis
128     *
129     * @param xAxis The x axis to use
130     * @param yAxis The y axis to use
131     */
132    public StackedAreaChart(Axis<X> xAxis, Axis<Y> yAxis) {
133        this(xAxis,yAxis, FXCollections.<Series<X,Y>>observableArrayList());
134    }
135
136    /**
137     * Construct a new Area Chart with the given axis and data
138     *
139     * @param xAxis The x axis to use
140     * @param yAxis The y axis to use
141     * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart
142     */
143    public StackedAreaChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<Series<X,Y>> data) {
144        super(xAxis,yAxis);
145        setLegend(legend);
146        setData(data);
147    }
148
149    // -------------- METHODS ------------------------------------------------------------------------------------------
150
151    private static double doubleValue(Number number) { return doubleValue(number, 0); }
152    private static double doubleValue(Number number, double nullDefault) {
153        return (number == null) ? nullDefault : number.doubleValue();
154    }
155
156    @Override protected void dataItemAdded(Series<X,Y> series, int itemIndex, Data<X,Y> item) {
157        final Node symbol = createSymbol(series, getData().indexOf(series), item, itemIndex);
158        if (shouldAnimate()) {
159            boolean animate = false;
160            if (itemIndex > 0 && itemIndex < (series.getData().size()-1)) {
161                animate = true;
162                Data<X,Y> p1 = series.getData().get(itemIndex - 1);
163                Data<X,Y> p2 = series.getData().get(itemIndex + 1);
164                double x1 = getXAxis().toNumericValue(p1.getXValue());
165                double y1 = getYAxis().toNumericValue(p1.getYValue());
166                double x3 = getXAxis().toNumericValue(p2.getXValue());
167                double y3 = getYAxis().toNumericValue(p2.getYValue());
168                
169                double x2 = getXAxis().toNumericValue(item.getXValue());
170                double y2 = getYAxis().toNumericValue(item.getYValue());
171      
172//                //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1)
173                double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1);
174                item.setCurrentY(getYAxis().toRealValue(y));
175                item.setCurrentX(getXAxis().toRealValue(x2));
176                //2. we can simply use the midpoint on the line as well..
177//                double x = (x3 + x1)/2;
178//                double y = (y3 + y1)/2;
179//                item.setCurrentX(x);
180//                item.setCurrentY(y);
181            } else if (itemIndex == 0 && series.getData().size() > 1) {
182                animate = true;
183                item.setCurrentX(series.getData().get(1).getXValue());
184                item.setCurrentY(series.getData().get(1).getYValue());
185            } else if (itemIndex == (series.getData().size() - 1) && series.getData().size() > 1) {
186                animate = true;
187                int last = series.getData().size() - 2;
188                item.setCurrentX(series.getData().get(last).getXValue());
189                item.setCurrentY(series.getData().get(last).getYValue());
190            } else if (symbol != null) {
191                // fade in new symbol
192                FadeTransition ft = new FadeTransition(Duration.millis(500),symbol);
193                ft.setToValue(1);
194                ft.play();
195            }
196            if (animate) {
197                animate(
198                    new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(),
199                                        item.getCurrentY()),
200                                        new KeyValue(item.currentXProperty(),
201                                        item.getCurrentX())),
202                    new KeyFrame(Duration.millis(800), new KeyValue(item.currentYProperty(),
203                                        item.getYValue(), Interpolator.EASE_BOTH),
204                                        new KeyValue(item.currentXProperty(),
205                                        item.getXValue(), Interpolator.EASE_BOTH))
206                );
207            }
208            
209        } else if (symbol != null) {
210            getPlotChildren().add(symbol);
211        }
212    }
213
214    @Override protected  void dataItemRemoved(final Data<X,Y> item, final Series<X,Y> series) {
215        final Node symbol = item.getNode();
216        // remove item from sorted list
217        int itemIndex = series.getItemIndex(item);
218        if (shouldAnimate()) {
219            boolean animate = false;
220            if (itemIndex > 0 && itemIndex < series.getDataSize()) {
221                animate = true;
222                int index=0; Data<X,Y> d;
223                for (d = series.begin; d != null && index != itemIndex - 1; d=d.next) index++;
224                Data<X,Y> p1 = d;
225                Data<X,Y> p2 = (d.next).next;
226                double x1 = getXAxis().toNumericValue(p1.getXValue());
227                double y1 = getYAxis().toNumericValue(p1.getYValue());
228                double x3 = getXAxis().toNumericValue(p2.getXValue());
229                double y3 = getYAxis().toNumericValue(p2.getYValue());
230
231                double x2 = getXAxis().toNumericValue(item.getXValue());
232                double y2 = getYAxis().toNumericValue(item.getYValue());
233
234//                //1.  y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1)
235                double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1);
236                item.setCurrentX(getXAxis().toRealValue(x2));
237                item.setCurrentY(getYAxis().toRealValue(y2));
238                item.setXValue(getXAxis().toRealValue(x2));
239                item.setYValue(getYAxis().toRealValue(y));
240                //2.  we can simply use the midpoint on the line as well..
241//                double x = (x3 + x1)/2;
242//                double y = (y3 + y1)/2;
243//                item.setCurrentX(x);
244//                item.setCurrentY(y);
245            } else if (itemIndex == 0 && series.getDataSize() > 1) {
246                animate = true;
247                item.setXValue(series.getData().get(0).getXValue());
248                item.setYValue(series.getData().get(0).getYValue());
249            } else if (itemIndex == (series.getDataSize() - 1) && series.getDataSize() > 1) {
250                animate = true;
251                int last = series.getData().size() - 1;
252                item.setXValue(series.getData().get(last).getXValue());
253                item.setYValue(series.getData().get(last).getYValue());
254            } else {
255                // fade out symbol
256                symbol.setOpacity(0);
257                FadeTransition ft = new FadeTransition(Duration.millis(500),symbol);
258                ft.setToValue(0);
259                ft.setOnFinished(new EventHandler<ActionEvent>() {
260                    @Override public void handle(ActionEvent actionEvent) {
261                        getPlotChildren().remove(symbol);
262                        removeDataItemFromDisplay(series, item);
263                    }
264                });
265                ft.play();
266            }
267            if (animate) {
268                animate( new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(),
269                            item.getCurrentY()), new KeyValue(item.currentXProperty(),
270                            item.getCurrentX())),
271                            new KeyFrame(Duration.millis(800), new EventHandler<ActionEvent>() {
272                                @Override public void handle(ActionEvent actionEvent) {
273                                    getPlotChildren().remove(symbol);
274                                    removeDataItemFromDisplay(series, item);
275                                }
276                            },
277                            new KeyValue(item.currentYProperty(),
278                            item.getYValue(), Interpolator.EASE_BOTH),
279                            new KeyValue(item.currentXProperty(),
280                            item.getXValue(), Interpolator.EASE_BOTH))
281                );
282            }
283        } else {
284            getPlotChildren().remove(symbol);
285            removeDataItemFromDisplay(series, item);
286        }
287        //Note: better animation here, point should move from old position to new position at center point between prev and next symbols
288    }
289
290    /** @inheritDoc */
291    @Override protected void dataItemChanged(Data<X, Y> item) {
292    }
293
294    @Override protected void seriesChanged(ListChangeListener.Change<? extends Series> c) {
295        // Update style classes for all series lines and symbols
296        for (int i = 0; i < getDataSize(); i++) {
297            final Series<X,Y> s = getData().get(i);
298            Path seriesLine = (Path)((Group)s.getNode()).getChildren().get(1);
299            Path fillPath = (Path)((Group)s.getNode()).getChildren().get(0);
300            seriesLine.getStyleClass().setAll("chart-series-area-line", "series" + i, s.defaultColorStyleClass);
301            fillPath.getStyleClass().setAll("chart-series-area-fill", "series" + i, s.defaultColorStyleClass);
302            for (int j=0; j < s.getData().size(); j++) {
303                final Data item = s.getData().get(j);
304                final Node node = item.getNode();
305                if(node!=null) node.getStyleClass().setAll("chart-area-symbol", "series" + i, "data" + j, s.defaultColorStyleClass);
306            }
307        }
308    }
309
310    @Override protected  void seriesAdded(Series<X,Y> series, int seriesIndex) {
311        // create new paths for series
312        Path seriesLine = new Path();
313        Path fillPath = new Path();
314        seriesLine.setStrokeLineJoin(StrokeLineJoin.BEVEL);
315        fillPath.setStrokeLineJoin(StrokeLineJoin.BEVEL);
316        Group areaGroup = new Group(fillPath,seriesLine);
317        series.setNode(areaGroup);
318        // create series Y multiplier
319        DoubleProperty seriesYAnimMultiplier = new SimpleDoubleProperty(this, "seriesYMultiplier");
320        seriesYMultiplierMap.put(series, seriesYAnimMultiplier);
321        // handle any data already in series
322        if (shouldAnimate()) {
323            seriesYAnimMultiplier.setValue(0d);
324        } else {
325            seriesYAnimMultiplier.setValue(1d);
326        }
327        getPlotChildren().add(areaGroup);
328        List<KeyFrame> keyFrames = new ArrayList<KeyFrame>();
329        if (shouldAnimate()) {
330            // animate in new series
331            keyFrames.add(new KeyFrame(Duration.ZERO,
332                new KeyValue(areaGroup.opacityProperty(), 0),
333                new KeyValue(seriesYAnimMultiplier, 0)
334            ));
335            keyFrames.add(new KeyFrame(Duration.millis(200),
336                new KeyValue(areaGroup.opacityProperty(), 1)
337            ));
338            keyFrames.add(new KeyFrame(Duration.millis(500),
339                new KeyValue(seriesYAnimMultiplier, 1)
340            ));
341        }
342        for (int j=0; j<series.getData().size(); j++) {
343            Data item = series.getData().get(j);
344            final Node symbol = createSymbol(series, seriesIndex, item, j);
345            if (symbol != null) {
346                if (shouldAnimate()) symbol.setOpacity(0);
347                getPlotChildren().add(symbol);
348                if (shouldAnimate()) {
349                    // fade in new symbol
350                    keyFrames.add(new KeyFrame(Duration.ZERO, new KeyValue(symbol.opacityProperty(), 0)));
351                    keyFrames.add(new KeyFrame(Duration.millis(200), new KeyValue(symbol.opacityProperty(), 1)));
352                }
353            }
354        }
355        if (shouldAnimate()) animate(keyFrames.toArray(new KeyFrame[keyFrames.size()]));
356    }
357
358    @Override protected  void seriesRemoved(final Series<X,Y> series) {
359        // remove series Y multiplier
360        seriesYMultiplierMap.remove(series);
361        // remove all symbol nodes
362        if (shouldAnimate()) {
363            // create list of all nodes we need to fade out
364            final List<Node> nodes = new ArrayList<Node>();
365            nodes.add(series.getNode());
366            for (Data d: series.getData()) nodes.add(d.getNode());
367            // fade out old and symbols
368            if (getCreateSymbols()) {
369                KeyValue[] startValues = new KeyValue[nodes.size()];
370                KeyValue[] endValues = new KeyValue[nodes.size()];
371                for (int j=0; j < nodes.size(); j++) {
372                    startValues[j]   = new KeyValue(nodes.get(j).opacityProperty(),1);
373                    endValues[j]       = new KeyValue(nodes.get(j).opacityProperty(),0);
374                }
375                Timeline tl = new Timeline();
376                tl.getKeyFrames().addAll(
377                    new KeyFrame(Duration.ZERO,startValues),
378                    new KeyFrame(Duration.millis(400), new EventHandler<ActionEvent>() {
379                        @Override public void handle(ActionEvent actionEvent) {
380                            getPlotChildren().removeAll(nodes);
381                            removeSeriesFromDisplay(series);
382                        }
383                    },endValues)
384                );
385                tl.play();
386            } else {
387                Timeline tl = new Timeline();
388                tl.getKeyFrames().addAll(
389                    new KeyFrame(Duration.millis(400), new EventHandler<ActionEvent>() {
390                        @Override public void handle(ActionEvent actionEvent) {
391                            getPlotChildren().removeAll(nodes);
392                            removeSeriesFromDisplay(series);
393                        }
394                    })
395                );
396                tl.play();                
397            }
398        } else {
399            getPlotChildren().remove(series.getNode());
400            for (Data d:series.getData()) getPlotChildren().remove(d.getNode());
401            removeSeriesFromDisplay(series);
402        }
403    }
404    
405    /** @inheritDoc */
406    @Override protected void updateAxisRange() {
407        // This override is necessary to update axis range based on cumulative Y value for the
408        // Y axis instead of the normal way where max value in the data range is used.
409        final Axis<X> xa = getXAxis();
410        final Axis<Y> ya = getYAxis();
411        if (xa.isAutoRanging()) {
412            List xData = new ArrayList<Number>();
413            if(xData != null) {
414                for(Series<X,Y> series : getData()) {
415                    for(Data<X,Y> data: series.getData()) {
416                        if(xData != null) xData.add(data.getXValue());
417                    }
418                }
419                if(xData != null) xa.invalidateRange(xData);
420            }
421        }
422        if (ya.isAutoRanging()) {
423            List yData = new ArrayList<Number>();
424            if(yData != null) {
425                double totalY = 0;
426                Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator();
427                while (seriesIterator.hasNext()) {
428                    double maxY = 0;
429                    Series<X, Y> series = seriesIterator.next();
430                    for(Data<X,Y> item : series.getData()) {
431                        if(item != null) maxY = Math.max(maxY, ya.toNumericValue(item.getYValue()));
432                    }
433                    totalY += maxY;
434
435                }
436                if(totalY > 0) yData.add(totalY);
437                ya.invalidateRange(yData);
438            }
439        }
440    }
441
442     
443    /** @inheritDoc */
444    @Override protected void layoutPlotChildren() {
445        ArrayList<DataPointInfo> currentSeriesData = 
446                                new ArrayList<DataPointInfo>();
447        // AggregateData hold the data points of both the current and the previous series.
448            // The goal is to collect all the data, sort it and iterate.
449        ArrayList<DataPointInfo> aggregateData = 
450                                new ArrayList<DataPointInfo>();
451        for (int seriesIndex=0; seriesIndex < getDataSize(); seriesIndex++) { // for every series
452            Series<X, Y> series = getData().get(seriesIndex);
453            aggregateData.clear();
454            // copy currentSeriesData accumulated in the previous iteration to aggregate.
455            for(DataPointInfo<X,Y> data : currentSeriesData) {
456                data.partOf = PartOf.PREVIOUS;
457                aggregateData.add(data);
458            }
459            currentSeriesData.clear(); 
460            // now copy actual data of the current series. 
461            for(Data<X, Y> item = series.begin; item != null; item = item.next) {
462                DataPointInfo<X,Y> itemInfo = new DataPointInfo(item, item.getXValue(), 
463                        item.getYValue(), PartOf.CURRENT);
464                aggregateData.add(itemInfo);
465            }
466            DoubleProperty seriesYAnimMultiplier = seriesYMultiplierMap.get(series);
467            Path seriesLine = (Path)((Group)series.getNode()).getChildren().get(1);
468            Path fillPath = (Path)((Group)series.getNode()).getChildren().get(0);
469            seriesLine.getElements().clear();
470            fillPath.getElements().clear();
471            int dataIndex = 0;
472            // Sort data points from prev and current series
473            sortAggregateList(aggregateData);
474           
475            boolean firstCurrent = false;
476            boolean lastCurrent = false;
477            int firstCurrentIndex = findNextCurrent(aggregateData, -1);
478            int lastCurrentIndex = findPreviousCurrent(aggregateData, aggregateData.size());
479            // Iterate over the aggregate data : this process accumulates data points
480            // cumulatively from the bottom to top of stack
481            for (DataPointInfo<X,Y> dataInfo : aggregateData) {
482                if (dataIndex == lastCurrentIndex) lastCurrent = true;
483                if (dataIndex == firstCurrentIndex) firstCurrent = true;
484                double x = 0;
485                double y = 0;
486                DataPointInfo<X,Y> currentDataPoint = new DataPointInfo();
487                DataPointInfo<X,Y> dropDownDataPoint = new DataPointInfo(true);
488                Data<X,Y> item = dataInfo.dataItem;
489                if (dataInfo.partOf.equals(PartOf.CURRENT)) { // handle data from current series
490                    int pIndex = findPreviousPrevious(aggregateData, dataIndex); 
491                    int nIndex = findNextPrevious(aggregateData, dataIndex);
492                    DataPointInfo<X,Y> prevPoint;
493                    DataPointInfo<X,Y> nextPoint;
494                    if (pIndex == -1 || (nIndex == -1 && !(aggregateData.get(pIndex).x.equals(dataInfo.x)))) {
495                        if (firstCurrent) {
496                            // Need to add the drop down point.
497                            item = new Data(dataInfo.x, 0);
498                            x = getXAxis().getDisplayPosition(item.getCurrentX());
499                            y = getYAxis().getZeroPosition();
500                            dropDownDataPoint.setValues(item, item.getXValue(), item.getYValue(), x, y, PartOf.CURRENT, true, false);
501                            currentSeriesData.add(dropDownDataPoint);
502                        } 
503                        // And add current point.
504                        item = dataInfo.dataItem;
505                        x = getXAxis().getDisplayPosition(item.getCurrentX());
506                        y = getYAxis().getDisplayPosition(
507                                getYAxis().toRealValue(getYAxis().toNumericValue(item.getCurrentY()) * seriesYAnimMultiplier.getValue()));
508                        currentDataPoint.setValues(item, item.getXValue(), item.getYValue(), x, y, PartOf.CURRENT, false, (firstCurrent) ? false : true);
509                        currentSeriesData.add(currentDataPoint);
510                        if (dataIndex == lastCurrentIndex) {
511                            // need to add drop down point
512                            item = new Data(dataInfo.x, 0);
513                            x = getXAxis().getDisplayPosition(item.getCurrentX());
514                            y = getYAxis().getZeroPosition();
515                            dropDownDataPoint.setValues(item, item.getXValue(), item.getYValue(), x, y, PartOf.CURRENT, true, false);
516                            currentSeriesData.add(dropDownDataPoint);
517                        }
518                    } else {
519                        prevPoint = aggregateData.get(pIndex);
520                        if (prevPoint.x.equals(dataInfo.x)) { // Need to add Y values
521                            // Check if prevPoint is a dropdown - as the stable sort preserves the order.
522                            // If so, find the non dropdown previous point on previous series.
523                            DataPointInfo<X,Y> ddPoint = prevPoint;
524                            if (prevPoint.dropDown) {
525                                pIndex = findPreviousPrevious(aggregateData, pIndex);
526                                prevPoint = (DataPointInfo<X,Y>)aggregateData.get(pIndex);
527                                // If lastCurrent - add this drop down
528                            } 
529                            if (prevPoint.x.equals(dataInfo.x)) { // simply add
530                                x = getXAxis().getDisplayPosition(item.getCurrentX());
531                                y = getYAxis().getDisplayPosition(
532                                    getYAxis().toRealValue(getYAxis().toNumericValue(item.getCurrentY()) * seriesYAnimMultiplier.getValue()));
533                                y += -(getYAxis().getZeroPosition() - prevPoint.displayY);
534                                currentDataPoint.setValues(item, dataInfo.x, dataInfo.y, x, y, PartOf.CURRENT, false, 
535                                                            (firstCurrent) ? false : true);
536                                currentSeriesData.add(currentDataPoint);
537                            }
538                            if (lastCurrent) {
539                                    dropDownDataPoint.setValues(ddPoint.dataItem, ddPoint.x, ddPoint.y, 
540                                            ddPoint.displayX, ddPoint.displayY, PartOf.CURRENT, true, false);
541                                    currentSeriesData.add(dropDownDataPoint);
542                            }
543                        } else {
544                            // interpolate 
545                            nextPoint = (nIndex == -1) ? null : (DataPointInfo<X,Y>)aggregateData.get(nIndex);
546                            prevPoint = (pIndex == -1) ? null : (DataPointInfo<X,Y>)aggregateData.get(pIndex);
547                            x = getXAxis().getDisplayPosition(item.getCurrentX());
548                            y = getYAxis().getDisplayPosition(
549                                getYAxis().toRealValue(getYAxis().toNumericValue(item.getCurrentY()) * seriesYAnimMultiplier.getValue()));
550                            if (prevPoint != null && nextPoint != null) {
551                                 double displayY = interpolate(prevPoint.displayX, 
552                                        prevPoint.displayY, nextPoint.displayX, nextPoint.displayY, x);
553                                 y+= -(getYAxis().getZeroPosition() - displayY);
554                                 double dataY = interpolate(getXAxis().toNumericValue(prevPoint.x), 
555                                         getYAxis().toNumericValue(prevPoint.y), 
556                                         getXAxis().toNumericValue(nextPoint.x), 
557                                         getYAxis().toNumericValue(nextPoint.y), 
558                                         getXAxis().toNumericValue(dataInfo.x)); 
559                                 if (firstCurrent) {
560                                     // now create the drop down point
561                                     item = new Data(dataInfo.x, dataY);
562                                     dropDownDataPoint.setValues(item, dataInfo.x, getYAxis().toRealValue(dataY), x, displayY, PartOf.CURRENT, true, false);
563                                     currentSeriesData.add(dropDownDataPoint);
564                                 }
565                                 // Add the current point
566                                 currentDataPoint.setValues(item, dataInfo.x, dataInfo.y, x, y, PartOf.CURRENT, false, 
567                                                                        (firstCurrent) ? false : true);
568                                 currentSeriesData.add(currentDataPoint);
569                                 if (dataIndex == lastCurrentIndex) {
570                                     // add drop down point
571                                     item = new Data(dataInfo.x, dataY);
572                                     dropDownDataPoint.setValues(item, dataInfo.x, getYAxis().toRealValue(dataY), x, displayY, PartOf.CURRENT, true, false);
573                                     currentSeriesData.add(dropDownDataPoint);
574                                 }
575                                 // Note: add drop down if last current
576                            } 
577                            else {
578                                // we do not need to take care of this as it is
579                                // already handled above with check of if(pIndex == -1 or nIndex == -1)
580                            }
581                        }
582                    }
583                    
584                } else { // handle data from Previous series.
585                    int pIndex = findPreviousCurrent(aggregateData, dataIndex); 
586                    int nIndex = findNextCurrent(aggregateData, dataIndex);
587                    DataPointInfo<X,Y> prevPoint;
588                    DataPointInfo<X,Y> nextPoint;
589                    if (dataInfo.dropDown) {
590                        if (getXAxis().toNumericValue(dataInfo.x) <= 
591                                getXAxis().toNumericValue(((DataPointInfo<X,Y>)aggregateData.get(firstCurrentIndex)).x) || 
592                                getXAxis().toNumericValue(dataInfo.x) > getXAxis().toNumericValue(((DataPointInfo<X,Y>)aggregateData.get(lastCurrentIndex)).x)) {
593                            currentDataPoint.setValues(item, dataInfo.x, dataInfo.y, dataInfo.displayX, dataInfo.displayY, 
594                                    PartOf.CURRENT, true, false);
595                            currentDataPoint.dropDown = true;
596                            currentSeriesData.add(currentDataPoint);
597                        }
598                    } else {
599                        if (pIndex == -1 || nIndex == -1) {
600                            currentDataPoint.setValues(item, dataInfo.x, dataInfo.y, dataInfo.displayX, dataInfo.displayY, 
601                                    PartOf.CURRENT, true, false);
602                            currentSeriesData.add(currentDataPoint);
603                        } else {
604                            nextPoint = (DataPointInfo<X,Y>)aggregateData.get(nIndex);
605                            if (nextPoint.x.equals(dataInfo.x)) {
606                                // do nothing as the current point is already there.
607                            } else {
608                                // interpolate on the current series.
609                                prevPoint = (DataPointInfo<X,Y>)aggregateData.get(pIndex);
610                                x = getXAxis().getDisplayPosition(item.getCurrentX());
611                                  double dataY = interpolate(getXAxis().toNumericValue(prevPoint.x), 
612                                         getYAxis().toNumericValue(prevPoint.y), 
613                                         getXAxis().toNumericValue(nextPoint.x), 
614                                         getYAxis().toNumericValue(nextPoint.y), 
615                                         getXAxis().toNumericValue(dataInfo.x)); 
616                                y = getYAxis().getDisplayPosition(
617                                    getYAxis().toRealValue(dataY * seriesYAnimMultiplier.getValue()));
618                                y+= -(getYAxis().getZeroPosition() - dataInfo.displayY);
619                                currentDataPoint.setValues(new Data(dataInfo.x, dataY), dataInfo.x, getYAxis().toRealValue(0), x, y, PartOf.CURRENT, true, true);
620                                currentSeriesData.add(currentDataPoint);
621                            }
622                        }
623                    }
624                }
625                dataIndex++;
626                if (firstCurrent) firstCurrent = false;
627                if (lastCurrent) lastCurrent = false;
628            } // end of inner for loop 
629            
630            // Draw the SeriesLine and Series fill
631            seriesLine.getElements().add(new MoveTo(currentSeriesData.get(0).displayX, currentSeriesData.get(0).displayY));
632            fillPath.getElements().add(new MoveTo(currentSeriesData.get(0).displayX, currentSeriesData.get(0).displayY));
633            for (DataPointInfo point : currentSeriesData) {
634                if (!point.lineTo) {
635                    seriesLine.getElements().add(new MoveTo(point.displayX, point.displayY));
636                } else {
637                    seriesLine.getElements().add(new LineTo(point.displayX, point.displayY));
638                }
639                fillPath.getElements().add(new  LineTo(point.displayX, point.displayY));
640                // draw symbols only for actual data points and skip for interpolated points.
641                if (!point.skipSymbol) { 
642                    Node symbol = point.dataItem.getNode();
643                    if (symbol != null) {
644                        final double w = symbol.prefWidth(-1);
645                        final double h = symbol.prefHeight(-1);
646                        symbol.resizeRelocate(point.displayX-(w/2), point.displayY-(h/2),w,h);
647                    }
648                }
649            }
650            for(int i = aggregateData.size()-1; i > 0; i--) {
651                DataPointInfo point = aggregateData.get(i);
652                if (PartOf.PREVIOUS.equals(point.partOf)) {
653                    fillPath.getElements().add(new  LineTo(point.displayX, point.displayY));
654                }
655            }
656            fillPath.getElements().add(new ClosePath());
657             
658        }  // end of out for loop
659     }
660     
661     //-------------------- helper methods to retrieve data points from the previous
662     // or current data series.
663     private int findNextCurrent(ArrayList<DataPointInfo> points, int index) {
664        for(int i = index+1; i < points.size(); i++) {
665            if (points.get(i).partOf.equals(PartOf.CURRENT)) {
666                return i;
667            }
668        }
669        return -1;
670     }
671     
672     private int findPreviousCurrent(ArrayList<DataPointInfo> points, int index) {
673        for(int i = index-1; i >= 0; i--) {
674            if (points.get(i).partOf.equals(PartOf.CURRENT)) {
675                return i;
676            }
677        }
678        return -1;
679     }
680     
681     
682    private int findPreviousPrevious(ArrayList<DataPointInfo> points, int index) {
683       for(int i = index-1; i >= 0; i--) {
684            if (points.get(i).partOf.equals(PartOf.PREVIOUS)) {
685                return i;
686            }
687        }
688        return -1;
689    }
690    private int findNextPrevious(ArrayList<DataPointInfo> points, int index) {
691        for(int i = index+1; i < points.size(); i++) {
692            if (points.get(i).partOf.equals(PartOf.PREVIOUS)) {
693                return i;
694            }
695        }
696        return -1;
697    }
698     
699    
700     private void sortAggregateList(ArrayList<DataPointInfo> aggregateList) {
701        Collections.sort(aggregateList, new Comparator(){
702            public int compare(Object o1, Object o2) {
703                Data<X,Y> d1 = ((DataPointInfo)o1).dataItem;
704                Data<X,Y> d2 = ((DataPointInfo)o2).dataItem;
705                double val1 = getXAxis().toNumericValue(d1.getXValue());
706                double val2 = getXAxis().toNumericValue(d2.getXValue());
707                return (val1 < val2 ? -1 : ( val1 == val2) ? 0 : 1);
708            }
709        });
710     }
711    
712    private double interpolate(double lowX, double lowY, double highX, double highY, double x) {
713         // using y = mx+c find the y for the given x. 
714         return (((highY - lowY)/(highX - lowX))*(x - lowX))+lowY;
715    }
716
717    private Node createSymbol(Series series, int seriesIndex, final Data item, int itemIndex) {
718        Node symbol = item.getNode();
719        // check if symbol has already been created
720        if (symbol == null && getCreateSymbols()) {
721            symbol = new StackPane();
722            item.setNode(symbol);
723        }
724        // set symbol styles
725        // Note not sure if we want to add or check, ie be more careful and efficient here
726        if (symbol != null) symbol.getStyleClass().setAll("chart-area-symbol", "series" + seriesIndex, "data" + itemIndex,
727                series.defaultColorStyleClass);
728        return symbol;
729    }
730
731    /**
732     * This is called whenever a series is added or removed and the legend needs to be updated
733     */
734    @Override protected void updateLegend() {
735        legend.getItems().clear();
736        if (getData() != null) {
737            for (int seriesIndex=0; seriesIndex < getData().size(); seriesIndex++) {
738                Series<X,Y> series = getData().get(seriesIndex);
739                LegendItem legenditem = new LegendItem(series.getName());
740                legenditem.getSymbol().getStyleClass().addAll("chart-area-symbol","series"+seriesIndex,
741                        "area-legend-symbol", series.defaultColorStyleClass);
742                legend.getItems().add(legenditem);
743            }
744        }
745        if (legend.getItems().size() > 0) {
746            if (getLegend() == null) {
747                setLegend(legend);
748            }
749        } else {
750            setLegend(null);
751        }
752    }
753
754    // -------------- INNER CLASSES --------------------------------------------
755    /* 
756     * Helper class to hold data and display and other information for each 
757     * data point
758     */
759    final static class DataPointInfo<X,Y> {
760        X x;
761        Y y;   
762        double displayX;
763        double displayY;
764        Data<X,Y> dataItem;
765        PartOf partOf;
766        boolean skipSymbol = false; // interpolated point - skip drawing symbol
767        boolean lineTo = false; // should there be a lineTo to this point on SeriesLine.
768        boolean dropDown = false; // Is this a drop down point ( non data point).
769        
770        //----- Constructors --------------------
771        DataPointInfo() {}
772
773        DataPointInfo(Data<X,Y> item, X x, Y y, PartOf partOf) {
774            this.dataItem = item;
775            this.x = x;
776            this.y = y;
777            this.partOf = partOf;
778        }
779        
780        DataPointInfo(boolean dropDown) {
781            this.dropDown = dropDown;
782        }
783        
784        void setValues(Data<X,Y> item, X x, Y y, double dx, double dy, 
785                        PartOf partOf, boolean skipSymbol, boolean lineTo) {
786            this.dataItem = item;
787            this.x = x;
788            this.y = y;
789            this.displayX = dx;
790            this.displayY = dy;
791            this.partOf = partOf;
792            this.skipSymbol = skipSymbol;
793            this.lineTo = lineTo;
794        }
795        
796        public final X getX() {
797            return x;
798        }
799        
800        public final Y getY() {
801            return y;
802        }
803    }
804
805    // To indicate if the data point belongs to the current or the previous series.
806    private static enum PartOf { 
807        CURRENT,
808        PREVIOUS
809    }
810    
811    // -------------- STYLESHEET HANDLING --------------------------------------
812    
813    private static class StyleableProperties {
814
815        private static final CssMetaData<StackedAreaChart<?, ?>, Boolean> CREATE_SYMBOLS =
816                new CssMetaData<StackedAreaChart<?, ?>, Boolean>("-fx-create-symbols",
817                BooleanConverter.getInstance(), Boolean.TRUE) {
818            @Override
819            public boolean isSettable(StackedAreaChart node) {
820                return node.createSymbols == null || !node.createSymbols.isBound();
821            }
822
823            @Override
824            public StyleableProperty<Boolean> getStyleableProperty(StackedAreaChart node) {
825                return (StyleableProperty<Boolean>) node.createSymbolsProperty();
826            }
827        };
828        
829        private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
830        
831        static {
832            final List<CssMetaData<? extends Styleable, ?>> styleables =
833                new ArrayList<CssMetaData<? extends Styleable, ?>>(XYChart.getClassCssMetaData());
834            styleables.add(CREATE_SYMBOLS);
835            STYLEABLES = Collections.unmodifiableList(styleables);
836
837        }
838    }
839
840    /**
841     * @return The CssMetaData associated with this class, which may include the
842     * CssMetaData of its super classes.
843     */
844     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
845         return StyleableProperties.STYLEABLES;
846     }
847
848    /**
849     * {@inheritDoc}
850     */
851    @Override
852    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
853        return getClassCssMetaData();
854    }
855
856}