Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2010, 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
028import java.util.ArrayList;
029import java.util.HashMap;
030import java.util.List;
031import java.util.Map;
032
033import javafx.animation.FadeTransition;
034import javafx.animation.Interpolator;
035import javafx.animation.KeyFrame;
036import javafx.animation.KeyValue;
037import javafx.animation.Timeline;
038import javafx.animation.Animation;
039import javafx.beans.property.BooleanProperty;
040import javafx.beans.property.DoubleProperty;
041import javafx.beans.property.SimpleDoubleProperty;
042import javafx.collections.FXCollections;
043import javafx.collections.ListChangeListener;
044import javafx.collections.ObservableList;
045import javafx.event.ActionEvent;
046import javafx.event.EventHandler;
047import javafx.scene.Node;
048import javafx.scene.layout.StackPane;
049import javafx.scene.shape.LineTo;
050import javafx.scene.shape.MoveTo;
051import javafx.scene.shape.Path;
052import javafx.scene.shape.StrokeLineJoin;
053import javafx.util.Duration;
054
055import com.sun.javafx.charts.Legend;
056import com.sun.javafx.charts.Legend.LegendItem;
057import javafx.css.StyleableBooleanProperty;
058import javafx.css.CssMetaData;
059import com.sun.javafx.css.converters.BooleanConverter;
060import java.util.*;
061import javafx.css.Styleable;
062import javafx.css.StyleableProperty;
063
064/**
065 * Line Chart plots a line connecting the data points in a series. The data points
066 * themselves can be represented by symbols optionally. Line charts are usually used
067 * to view data trends over time or category. 
068 */
069public class LineChart<X,Y> extends XYChart<X,Y> {
070
071    // -------------- PRIVATE FIELDS ------------------------------------------
072
073    /** A multiplier for teh Y values that we store for each series, it is used to animate in a new series */
074    private Map<Series, DoubleProperty> seriesYMultiplierMap = new HashMap<Series, DoubleProperty>();
075    private Legend legend = new Legend();
076    private Timeline dataRemoveTimeline;
077    private Series<X,Y> seriesOfDataRemoved = null;
078    private Data<X,Y> dataItemBeingRemoved = null;
079
080    // -------------- PUBLIC PROPERTIES ----------------------------------------
081
082    /** When true, CSS styleable symbols are created for any data items that don't have a symbol node specified. */
083    private BooleanProperty createSymbols = new StyleableBooleanProperty(true) {
084        @Override protected void invalidated() {
085            for (int seriesIndex=0; seriesIndex < getData().size(); seriesIndex ++) {
086                Series<X,Y> series = getData().get(seriesIndex);
087                for (int itemIndex=0; itemIndex < series.getData().size(); itemIndex ++) {
088                    Data<X,Y> item = series.getData().get(itemIndex);
089                    Node symbol = item.getNode();
090                    if(get() && symbol == null) { // create any symbols
091                        symbol = createSymbol(series, getData().indexOf(series), item, itemIndex);
092                        getPlotChildren().add(symbol);
093                    } else if (!get() && symbol != null) { // remove symbols
094                        getPlotChildren().remove(symbol);
095                        symbol = null;
096                        item.setNode(null);
097                    }
098                }
099            }
100            requestChartLayout();
101        }
102
103        public Object getBean() {
104            return LineChart.this;
105        }
106
107        public String getName() {
108            return "createSymbols";
109        }
110
111        public CssMetaData<LineChart<?,?>,Boolean> getCssMetaData() {
112            return StyleableProperties.CREATE_SYMBOLS;
113        }
114    };
115
116    /**
117     * Indicates whether symbols for data points will be created or not.
118     *
119     * @return true if symbols for data points will be created and false otherwise.
120     */
121    public final boolean getCreateSymbols() { return createSymbols.getValue(); }
122    public final void setCreateSymbols(boolean value) { createSymbols.setValue(value); }
123    public final BooleanProperty createSymbolsProperty() { return createSymbols; }
124
125    // -------------- CONSTRUCTORS ----------------------------------------------
126
127    /**
128     * Construct a new LineChart with the given axis.
129     *
130     * @param xAxis The x axis to use
131     * @param yAxis The y axis to use
132     */
133    public LineChart(Axis<X> xAxis, Axis<Y> yAxis) {
134        this(xAxis, yAxis, FXCollections.<Series<X, Y>>observableArrayList());
135    }
136
137    /**
138     * Construct a new LineChart with the given axis and data.
139     *
140     * @param xAxis The x axis to use
141     * @param yAxis The y axis to use
142     * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart
143     */
144    public LineChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<Series<X,Y>> data) {
145        super(xAxis,yAxis);
146        setLegend(legend);
147        setData(data);
148    }
149
150    // -------------- METHODS ------------------------------------------------------------------------------------------
151
152    @Override protected void dataItemAdded(final Series<X,Y> series, int itemIndex, final Data<X,Y> item) {
153        final Node symbol = createSymbol(series, getData().indexOf(series), item, itemIndex);
154        if (shouldAnimate()) {
155            if (dataRemoveTimeline != null && dataRemoveTimeline.getStatus().equals(Animation.Status.RUNNING)) {
156                if (seriesOfDataRemoved == series) {
157                    dataRemoveTimeline.stop();
158                    dataRemoveTimeline = null;
159                    getPlotChildren().remove(dataItemBeingRemoved.getNode());
160                    removeDataItemFromDisplay(seriesOfDataRemoved, dataItemBeingRemoved);
161                    seriesOfDataRemoved = null;
162                    dataItemBeingRemoved = null;
163                }
164            }
165            boolean animate = false;
166            if (itemIndex > 0 && itemIndex < (series.getData().size()-1)) {
167                animate = true;
168                Data<X,Y> p1 = series.getData().get(itemIndex - 1);
169                Data<X,Y> p2 = series.getData().get(itemIndex + 1);
170                if (p1 != null && p2 != null) {
171                    double x1 = getXAxis().toNumericValue(p1.getXValue());
172                    double y1 = getYAxis().toNumericValue(p1.getYValue());
173                    double x3 = getXAxis().toNumericValue(p2.getXValue());
174                    double y3 = getYAxis().toNumericValue(p2.getYValue());
175
176                    double x2 = getXAxis().toNumericValue(item.getXValue());
177                    //double y2 = getYAxis().toNumericValue(item.getYValue());
178                    if (x2 > x1 && x2 < x3) {
179                         //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1)
180                        double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1);
181                        item.setCurrentY(getYAxis().toRealValue(y));
182                        item.setCurrentX(getXAxis().toRealValue(x2));
183                    } else {
184                        //2. we can simply use the midpoint on the line as well..
185                        double x = (x3 + x1)/2;
186                        double y = (y3 + y1)/2;
187                        item.setCurrentX(getXAxis().toRealValue(x));
188                        item.setCurrentY(getYAxis().toRealValue(y));
189                    }
190                }
191            } else if (itemIndex == 0 && series.getData().size() > 1) {
192                animate = true;
193                item.setCurrentX(series.getData().get(1).getXValue());
194                item.setCurrentY(series.getData().get(1).getYValue());
195            } else if (itemIndex == (series.getData().size() - 1) && series.getData().size() > 1) {
196                animate = true;
197                int last = series.getData().size() - 2;
198                item.setCurrentX(series.getData().get(last).getXValue());
199                item.setCurrentY(series.getData().get(last).getYValue());
200            } else if(symbol != null) {
201                // fade in new symbol
202                FadeTransition ft = new FadeTransition(Duration.millis(500),symbol);
203                ft.setToValue(1);
204                ft.play();
205            }
206            if(symbol != null) {
207                    getPlotChildren().add(symbol);
208            }
209            if (animate) {
210                animate(
211                    new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(),
212                                        item.getCurrentY()),
213                                        new KeyValue(item.currentXProperty(),
214                                        item.getCurrentX())),
215                    new KeyFrame(Duration.millis(700), new KeyValue(item.currentYProperty(),
216                                        item.getYValue(), Interpolator.EASE_BOTH),
217                                        new KeyValue(item.currentXProperty(),
218                                        item.getXValue(), Interpolator.EASE_BOTH))
219                );
220            }
221            
222        } else {
223            if (symbol != null) getPlotChildren().add(symbol);
224        }
225    }
226
227    @Override protected  void dataItemRemoved(final Data<X,Y> item, final Series<X,Y> series) {
228        final Node symbol = item.getNode();
229        // remove item from sorted list
230        int itemIndex = series.getItemIndex(item);
231        if (shouldAnimate()) {
232            boolean animate = false;
233            if (itemIndex > 0 && itemIndex < series.getDataSize()) {
234                animate = true;
235                int index=0; Data<X,Y> d;
236                for (d = series.begin; d != null && index != itemIndex - 1; d=d.next) index++;
237                Data<X,Y> p1 = d;
238                Data<X,Y> p2 = (d.next).next;
239                if (p1 != null && p2 != null) {
240                    double x1 = getXAxis().toNumericValue(p1.getXValue());
241                    double y1 = getYAxis().toNumericValue(p1.getYValue());
242                    double x3 = getXAxis().toNumericValue(p2.getXValue());
243                    double y3 = getYAxis().toNumericValue(p2.getYValue());
244
245                    double x2 = getXAxis().toNumericValue(item.getXValue());
246                    double y2 = getYAxis().toNumericValue(item.getYValue());
247                    if (x2 > x1 && x2 < x3) {
248    //                //1.  y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1)
249                        double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1);
250                        item.setCurrentX(getXAxis().toRealValue(x2));
251                        item.setCurrentY(getYAxis().toRealValue(y2));
252                        item.setXValue(getXAxis().toRealValue(x2));
253                        item.setYValue(getYAxis().toRealValue(y));
254                    } else {
255                    //2.  we can simply use the midpoint on the line as well..
256                        double x = (x3 + x1)/2;
257                        double y = (y3 + y1)/2;
258                        item.setCurrentX(getXAxis().toRealValue(x));
259                        item.setCurrentY(getYAxis().toRealValue(y));
260                    }
261                }
262            } else if (itemIndex == 0 && series.getDataSize() > 1) {
263                animate = true;
264                Iterator<Data<X,Y>> iter = getDisplayedDataIterator(series);
265                if (iter.hasNext()) { // get first data value
266                    Data<X,Y> d = iter.next();
267                    item.setXValue(d.getXValue());
268                    item.setYValue(d.getYValue());
269                }
270            } else if (itemIndex == (series.getDataSize() - 1) && series.getDataSize() > 1) {
271                animate = true;
272                int last = series.getData().size() - 1;
273                item.setXValue(series.getData().get(last).getXValue());
274                item.setYValue(series.getData().get(last).getYValue());
275            } else {
276                // fade out symbol
277                if (symbol != null) {
278                    symbol.setOpacity(0);
279                    FadeTransition ft = new FadeTransition(Duration.millis(500),symbol);
280                    ft.setToValue(0);
281                    ft.setOnFinished(new EventHandler<ActionEvent>() {
282                        @Override public void handle(ActionEvent actionEvent) {
283                            item.setSeries(null);
284                            getPlotChildren().remove(symbol);
285                            removeDataItemFromDisplay(series, item);
286                        }
287                    });
288                    ft.play();
289                }
290            }
291            if (animate) {
292                dataRemoveTimeline = createDataRemoveTimeline(item, symbol, series);
293                seriesOfDataRemoved = series;
294                dataItemBeingRemoved = item;
295                dataRemoveTimeline.play();
296            }
297        } else {
298            item.setSeries(null);
299            if (symbol != null) getPlotChildren().remove(symbol);
300            removeDataItemFromDisplay(series, item);
301        }
302        //Note: better animation here, point should move from old position to new position at center point between prev and next symbols
303    }
304
305    /** @inheritDoc */
306    @Override protected void dataItemChanged(Data<X, Y> item) {
307    }
308    
309    @Override protected void seriesChanged(ListChangeListener.Change<? extends Series> c) {
310        // Update style classes for all series lines and symbols
311        // Note: is there a more efficient way of doing this?
312        for (int i = 0; i < getDataSize(); i++) {
313            final Series<X,Y> s = getData().get(i);
314            Node seriesNode = s.getNode();
315            if(seriesNode != null) seriesNode.getStyleClass().setAll("chart-series-line", "series" + i, s.defaultColorStyleClass);
316        }
317    }
318
319    @Override protected  void seriesAdded(Series<X,Y> series, int seriesIndex) {
320        // create new path for series
321        Path seriesLine = new Path();
322        seriesLine.setStrokeLineJoin(StrokeLineJoin.BEVEL);
323        series.setNode(seriesLine);
324        // create series Y multiplier
325        DoubleProperty seriesYAnimMultiplier = new SimpleDoubleProperty(this, "seriesYMultiplier");
326        seriesYMultiplierMap.put(series, seriesYAnimMultiplier);
327        // handle any data already in series
328        if (shouldAnimate()) {
329            seriesLine.setOpacity(0);
330            seriesYAnimMultiplier.setValue(0d);
331        } else {
332            seriesYAnimMultiplier.setValue(1d);
333        }
334        getPlotChildren().add(seriesLine);
335
336        List<KeyFrame> keyFrames = new ArrayList<KeyFrame>();
337        if (shouldAnimate()) {
338            // animate in new series
339            keyFrames.add(new KeyFrame(Duration.ZERO,
340                new KeyValue(seriesLine.opacityProperty(), 0),
341                new KeyValue(seriesYAnimMultiplier, 0)
342            ));
343            keyFrames.add(new KeyFrame(Duration.millis(200),
344                new KeyValue(seriesLine.opacityProperty(), 1)
345            ));
346            keyFrames.add(new KeyFrame(Duration.millis(500),
347                new KeyValue(seriesYAnimMultiplier, 1)
348            ));
349        }
350        for (int j=0; j<series.getData().size(); j++) {
351            Data item = series.getData().get(j);
352            final Node symbol = createSymbol(series, seriesIndex, item, j);
353            if(symbol != null) {
354                if (shouldAnimate()) symbol.setOpacity(0);
355                getPlotChildren().add(symbol);
356                if (shouldAnimate()) {
357                    // fade in new symbol
358                    keyFrames.add(new KeyFrame(Duration.ZERO, new KeyValue(symbol.opacityProperty(), 0)));
359                    keyFrames.add(new KeyFrame(Duration.millis(200), new KeyValue(symbol.opacityProperty(), 1)));
360                }
361            }
362        }
363        if (shouldAnimate()) animate(keyFrames.toArray(new KeyFrame[keyFrames.size()]));
364    }
365    private void updateDefaultColorIndex(final Series<X,Y> series) {
366        int clearIndex = seriesColorMap.get(series);
367        colorBits.clear(clearIndex);
368        series.getNode().getStyleClass().remove(DEFAULT_COLOR+clearIndex);
369        for (int j=0; j < series.getData().size(); j++) {
370            final Node node = series.getData().get(j).getNode();
371            if(node!=null) {
372                node.getStyleClass().remove(DEFAULT_COLOR+clearIndex);
373            }
374        }
375        seriesColorMap.remove(series);
376    }
377
378    @Override protected  void seriesRemoved(final Series<X,Y> series) {
379        updateDefaultColorIndex(series);
380        // remove all symbol nodes
381        seriesYMultiplierMap.remove(series);
382        if (shouldAnimate()) {
383            // create list of all nodes we need to fade out
384            final List<Node> nodes = new ArrayList<Node>();
385            nodes.add(series.getNode());
386            if (getCreateSymbols()) { // RT-22124 
387                // done need to fade the symbols if createSymbols is false
388                for (Data d: series.getData()) nodes.add(d.getNode());
389            }
390            // fade out old and symbols
391            KeyValue[] startValues = new KeyValue[nodes.size()];
392            KeyValue[] endValues = new KeyValue[nodes.size()];
393            for (int j=0; j < nodes.size(); j++) {
394                startValues[j]   = new KeyValue(nodes.get(j).opacityProperty(),1);
395                endValues[j]       = new KeyValue(nodes.get(j).opacityProperty(),0);
396            }
397            Timeline tl = new Timeline();
398            tl.getKeyFrames().addAll(
399                new KeyFrame(Duration.ZERO,startValues),
400                new KeyFrame(Duration.millis(900), new EventHandler<ActionEvent>() {
401                    @Override public void handle(ActionEvent actionEvent) {
402                        getPlotChildren().removeAll(nodes);
403                        removeSeriesFromDisplay(series);
404                    }
405                },endValues)
406            );
407            tl.play();
408        } else {
409            getPlotChildren().remove(series.getNode());
410            for (Data d:series.getData()) getPlotChildren().remove(d.getNode());
411            removeSeriesFromDisplay(series);
412        }
413    }
414
415    /** @inheritDoc */
416    @Override protected void layoutPlotChildren() {
417        for (int seriesIndex=0; seriesIndex < getDataSize(); seriesIndex++) {
418            Series<X,Y> series = getData().get(seriesIndex);
419            final DoubleProperty seriesYAnimMultiplier = seriesYMultiplierMap.get(series);
420            boolean isFirst = true;
421            if(series.getNode() instanceof  Path) {
422                Path seriesLine = (Path)series.getNode();
423                seriesLine.getElements().clear();
424                for (Data<X,Y> item = series.begin; item != null; item = item.next) {
425                    double x = getXAxis().getDisplayPosition(item.getCurrentX());
426                    double y = getYAxis().getDisplayPosition(
427                            getYAxis().toRealValue(getYAxis().toNumericValue(item.getCurrentY()) * seriesYAnimMultiplier.getValue()));
428                    if (isFirst) {
429                        isFirst = false;
430                        seriesLine.getElements().add(new MoveTo(x, y));
431                    } else {
432                        seriesLine.getElements().add(new LineTo(x, y));
433                    }
434                    Node symbol = item.getNode();
435                    if (symbol != null) {
436                        final double w = symbol.prefWidth(-1);
437                        final double h = symbol.prefHeight(-1);
438                        symbol.resizeRelocate(x-(w/2), y-(h/2),w,h);
439                    }
440                }
441            }
442        }
443    }
444
445    private Timeline createDataRemoveTimeline(final Data<X,Y> item, final Node symbol, final Series<X,Y> series) {
446        Timeline t = new Timeline();
447
448        t.getKeyFrames().addAll(new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(),
449                item.getCurrentY()), new KeyValue(item.currentXProperty(),
450                item.getCurrentX())),
451                new KeyFrame(Duration.millis(500), new EventHandler<ActionEvent>() {
452                    @Override public void handle(ActionEvent actionEvent) {
453                        if (symbol != null) getPlotChildren().remove(symbol);
454                        removeDataItemFromDisplay(series, item);
455                    }
456                },
457                new KeyValue(item.currentYProperty(),
458                item.getYValue(), Interpolator.EASE_BOTH),
459                new KeyValue(item.currentXProperty(),
460                item.getXValue(), Interpolator.EASE_BOTH))
461        );
462        return t;
463    }
464
465    private Node createSymbol(Series<X, Y> series, int seriesIndex, final Data item, int itemIndex) {
466        Node symbol = item.getNode();
467        // check if symbol has already been created
468        if (symbol == null && getCreateSymbols()) {
469            symbol = new StackPane();
470            item.setNode(symbol);
471        }
472        // set symbol styles
473        if (symbol != null) symbol.getStyleClass().addAll("chart-line-symbol", "series" + seriesIndex,
474                "data" + itemIndex, series.defaultColorStyleClass);
475        return symbol;
476    }
477
478    /**
479     * This is called whenever a series is added or removed and the legend needs to be updated
480     */
481     @Override protected void updateLegend() {
482        legend.getItems().clear();
483        if (getData() != null) {
484            for (int seriesIndex=0; seriesIndex < getData().size(); seriesIndex++) {
485                Series<X,Y> series = getData().get(seriesIndex);
486                LegendItem legenditem = new LegendItem(series.getName());
487                legenditem.getSymbol().getStyleClass().addAll("chart-line-symbol", "series"+seriesIndex, series.defaultColorStyleClass);
488                legend.getItems().add(legenditem);
489            }
490        }
491        if (legend.getItems().size() > 0) {
492            if (getLegend() == null) {
493                setLegend(legend);
494            }
495        } else {
496            setLegend(null);
497        }
498    }
499
500    // -------------- STYLESHEET HANDLING --------------------------------------
501
502    private static class StyleableProperties {
503        private static final CssMetaData<LineChart<?,?>,Boolean> CREATE_SYMBOLS = 
504            new CssMetaData<LineChart<?,?>,Boolean>("-fx-create-symbols",
505                BooleanConverter.getInstance(), Boolean.TRUE) {
506
507            @Override
508            public boolean isSettable(LineChart node) {
509                return node.createSymbols == null || !node.createSymbols.isBound();
510            }
511
512            @Override
513            public StyleableProperty<Boolean> getStyleableProperty(LineChart node) {
514                return (StyleableProperty<Boolean>)node.createSymbolsProperty();
515            }
516        };
517
518        private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
519        static {
520            final List<CssMetaData<? extends Styleable, ?>> styleables =
521                new ArrayList<CssMetaData<? extends Styleable, ?>>(XYChart.getClassCssMetaData());
522            styleables.add(CREATE_SYMBOLS);
523            STYLEABLES = Collections.unmodifiableList(styleables);
524        }
525    }
526
527    /**
528     * @return The CssMetaData associated with this class, which may include the
529     * CssMetaData of its super classes.
530     */
531    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
532        return StyleableProperties.STYLEABLES;
533    }
534
535    /**
536     * {@inheritDoc}
537     */
538    @Override
539    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
540        return getClassCssMetaData();
541    }
542
543}