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