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.Iterator;
030import java.util.List;
031
032import javafx.animation.FadeTransition;
033import javafx.animation.ParallelTransition;
034import javafx.collections.FXCollections;
035import javafx.collections.ObservableList;
036import javafx.event.ActionEvent;
037import javafx.event.EventHandler;
038import javafx.scene.Node;
039import javafx.scene.layout.StackPane;
040import javafx.scene.shape.Ellipse;
041import javafx.util.Duration;
042
043import com.sun.javafx.charts.Legend;
044import com.sun.javafx.charts.Legend.LegendItem;
045
046/**
047 * Chart type that plots bubbles for the data points in a series. The extra value property of Data is used to represent
048 * the radius of the bubble it should be a java.lang.Number.
049 */
050public class BubbleChart<X,Y> extends XYChart<X,Y> {
051
052    // -------------- PRIVATE FIELDS ------------------------------------------
053
054    private Legend legend = new Legend();
055
056    // -------------- CONSTRUCTORS ----------------------------------------------
057
058    /**
059     * Construct a new BubbleChart with the given axis. BubbleChart does not use a Category Axis. 
060     * Both X and Y axes should be of type NumberAxis.
061     *
062     * @param xAxis The x axis to use
063     * @param yAxis The y axis to use
064     */
065    public BubbleChart(Axis<X> xAxis, Axis<Y> yAxis) {
066        this(xAxis, yAxis, FXCollections.<Series<X, Y>>observableArrayList());
067    }
068
069    /**
070     * Construct a new BubbleChart with the given axis and data. BubbleChart does not 
071     * use a Category Axis. Both X and Y axes should be of type NumberAxis.
072     *
073     * @param xAxis The x axis to use
074     * @param yAxis The y axis to use
075     * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart
076     */
077    public BubbleChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<Series<X,Y>> data) {
078        super(xAxis, yAxis);
079        setLegend(legend);
080        if (!(xAxis instanceof ValueAxis && yAxis instanceof ValueAxis)) {
081            throw new IllegalArgumentException("Axis type incorrect, X and Y should both be NumberAxis");
082        }
083        setData(data);
084    }
085
086    // -------------- METHODS ------------------------------------------------------------------------------------------
087
088    /**
089     * Used to get a double value from a object that can be a Number object or null
090     *
091     * @param number Object possibly a instance of Number
092     * @param nullDefault What value to return if the number object is null or not a Number
093     * @return number converted to double or nullDefault
094     */
095    private static double getDoubleValue(Object number, double nullDefault) {
096        return !(number instanceof Number) ? nullDefault : ((Number)number).doubleValue();
097    }
098
099    /** @inheritDoc */
100    @Override protected void layoutPlotChildren() {
101        // update bubble positions
102      for (int seriesIndex=0; seriesIndex < getDataSize(); seriesIndex++) {
103            Series<X,Y> series = getData().get(seriesIndex);
104//            for (Data<X,Y> item = series.begin; item != null; item = item.next) {
105            Iterator<Data<X,Y>> iter = getDisplayedDataIterator(series);
106            while(iter.hasNext()) {
107                Data<X,Y> item = iter.next();
108                double x = getXAxis().getDisplayPosition(item.getCurrentX());
109                double y = getYAxis().getDisplayPosition(item.getCurrentY());
110                Node bubble = item.getNode();
111                Ellipse ellipse;
112                if (bubble != null) {
113                    if (bubble instanceof StackPane) {
114                        StackPane region = (StackPane)item.getNode();
115                        if (region.getShape() == null) {
116                            ellipse = new Ellipse(getDoubleValue(item.getExtraValue(), 1), getDoubleValue(item.getExtraValue(), 1));
117                        } else if (region.getShape() instanceof Ellipse) {
118                            ellipse = (Ellipse)region.getShape();
119                        } else {
120                            return;
121                        }
122                        ellipse.setRadiusX(getDoubleValue(item.getExtraValue(), 1) * ((getXAxis() instanceof NumberAxis) ? Math.abs(((NumberAxis)getXAxis()).getScale()) : 1));
123                        ellipse.setRadiusY(getDoubleValue(item.getExtraValue(), 1) * ((getYAxis() instanceof NumberAxis) ? Math.abs(((NumberAxis)getYAxis()).getScale()) : 1));
124                        // Note: workaround for RT-7689 - saw this in ProgressControlSkin
125                        // The region doesn't update itself when the shape is mutated in place, so we
126                        // null out and then restore the shape in order to force invalidation.
127                        region.setShape(null);
128                        region.setShape(ellipse);
129                        region.setScaleShape(false);
130                        region.setCenterShape(false);
131                        region.setCacheShape(false);
132                        // position the bubble
133                        bubble.setLayoutX(x);
134                        bubble.setLayoutY(y);
135                    }
136                }
137            }
138        }
139    }
140
141    @Override protected void dataItemAdded(Series<X,Y> series, int itemIndex, Data<X,Y> item) {
142        Node bubble = createBubble(series, getData().indexOf(series), item, itemIndex);
143        if (shouldAnimate()) {
144            bubble.setOpacity(0);
145            getPlotChildren().add(bubble);
146            // fade in new bubble
147            FadeTransition ft = new FadeTransition(Duration.millis(500),bubble);
148            ft.setToValue(1);
149            ft.play();
150        } else {
151            getPlotChildren().add(bubble);
152        }
153    }
154
155    @Override protected  void dataItemRemoved(final Data<X,Y> item, final Series<X,Y> series) {
156        final Node bubble = item.getNode();
157        if (shouldAnimate()) {
158            // fade out old bubble
159            FadeTransition ft = new FadeTransition(Duration.millis(500),bubble);
160            ft.setToValue(0);
161            ft.setOnFinished(new EventHandler<ActionEvent>() {
162                @Override public void handle(ActionEvent actionEvent) {
163                    getPlotChildren().remove(bubble);
164                    removeDataItemFromDisplay(series, item);
165                }
166            });
167            ft.play();
168        } else {
169            getPlotChildren().remove(bubble);
170            removeDataItemFromDisplay(series, item);
171        }
172    }
173
174    /** @inheritDoc */
175    @Override protected void dataItemChanged(Data<X, Y> item) {
176    }
177    
178    @Override protected  void seriesAdded(Series<X,Y> series, int seriesIndex) {
179        // handle any data already in series
180        for (int j=0; j<series.getData().size(); j++) {
181            Data item = series.getData().get(j);
182            Node bubble = createBubble(series, seriesIndex, item, j);
183            if (shouldAnimate()) {
184                bubble.setOpacity(0);
185                getPlotChildren().add(bubble);
186                // fade in new bubble
187                FadeTransition ft = new FadeTransition(Duration.millis(500),bubble);
188                ft.setToValue(1);
189                ft.play();
190            } else {
191                getPlotChildren().add(bubble);
192            }
193        }
194    }
195
196    @Override protected  void seriesRemoved(final Series<X,Y> series) {
197        // remove all bubble nodes
198        if (shouldAnimate()) {
199            ParallelTransition pt = new ParallelTransition();
200            pt.setOnFinished(new EventHandler<ActionEvent>() {
201                public void handle(ActionEvent event) {
202                    removeSeriesFromDisplay(series);
203                }
204            });
205            for (XYChart.Data<X,Y> d : series.getData()) {
206                final Node bubble = d.getNode();
207                // fade out old bubble
208                FadeTransition ft = new FadeTransition(Duration.millis(500),bubble);
209                ft.setToValue(0);
210                ft.setOnFinished(new EventHandler<ActionEvent>() {
211                    @Override public void handle(ActionEvent actionEvent) {
212                        getPlotChildren().remove(bubble);
213                    }
214                });
215                pt.getChildren().add(ft);
216            }
217            pt.play();
218        } else {
219            for (XYChart.Data<X,Y> d : series.getData()) {
220                final Node bubble = d.getNode();
221                getPlotChildren().remove(bubble);
222            }
223            removeSeriesFromDisplay(series);
224        }
225
226    }
227
228    /**
229     * Create a Bubble for a given data item if it doesn't already have a node
230     *
231     *
232     * @param series
233     * @param seriesIndex The index of the series containing the item
234     * @param item        The data item to create node for
235     * @param itemIndex   The index of the data item in the series
236     * @return Node used for given data item
237     */
238    private Node createBubble(Series<X, Y> series, int seriesIndex, final Data item, int itemIndex) {
239        Node bubble = item.getNode();
240        // check if bubble has already been created
241        if (bubble == null) {
242            bubble = new StackPane();
243            item.setNode(bubble);
244        }
245        // set bubble styles
246        bubble.getStyleClass().setAll("chart-bubble", "series" + seriesIndex, "data" + itemIndex,
247                series.defaultColorStyleClass);
248        return bubble;
249    }
250
251    /**
252     * This is called when the range has been invalidated and we need to update it. If the axis are auto
253     * ranging then we compile a list of all data that the given axis has to plot and call invalidateRange() on the
254     * axis passing it that data.
255     */
256    @Override protected void updateAxisRange() {
257        // For bubble chart we need to override this method as we need to let the axis know that they need to be able
258        // to cover the whole area occupied by the bubble not just its center data value
259        final Axis<X> xa = getXAxis();
260        final Axis<Y> ya = getYAxis();
261        List<X> xData = null;
262        List<Y> yData = null;
263        if(xa.isAutoRanging()) xData = new ArrayList<X>();
264        if(ya.isAutoRanging()) yData = new ArrayList<Y>();
265        final boolean xIsCategory = xa instanceof CategoryAxis;
266        final boolean yIsCategory = ya instanceof CategoryAxis;
267        if(xData != null || yData != null) {
268            for(Series<X,Y> series : getData()) {
269                for(Data<X,Y> data: series.getData()) {
270                    if(xData != null) {
271                        if(xIsCategory) {
272                            xData.add(data.getXValue());
273                        } else {
274                            xData.add(xa.toRealValue(xa.toNumericValue(data.getXValue()) + getDoubleValue(data.getExtraValue(), 0)));
275                            xData.add(xa.toRealValue(xa.toNumericValue(data.getXValue()) - getDoubleValue(data.getExtraValue(), 0)));
276                        }
277                    }
278                    if(yData != null){
279                        if(yIsCategory) {
280                            yData.add(data.getYValue());
281                        } else {
282                            yData.add(ya.toRealValue(ya.toNumericValue(data.getYValue()) + getDoubleValue(data.getExtraValue(), 0)));
283                            yData.add(ya.toRealValue(ya.toNumericValue(data.getYValue()) - getDoubleValue(data.getExtraValue(), 0)));
284                        }
285                    }
286                }
287            }
288            if(xData != null) xa.invalidateRange(xData);
289            if(yData != null) ya.invalidateRange(yData);
290        }
291    }
292
293    /**
294     * This is called whenever a series is added or removed and the legend needs to be updated
295     */
296    @Override protected void updateLegend() {
297        legend.getItems().clear();
298        if (getData() != null) {
299            for (int seriesIndex=0; seriesIndex< getData().size(); seriesIndex++) {
300                Series<X,Y> series = getData().get(seriesIndex);
301                LegendItem legenditem = new LegendItem(series.getName());
302                legenditem.getSymbol().getStyleClass().addAll("series"+seriesIndex,"chart-bubble",
303                        "bubble-legend-symbol", series.defaultColorStyleClass);
304                legend.getItems().add(legenditem);
305            }
306        }
307        if (legend.getItems().size() > 0) {
308            if (getLegend() == null) {
309                setLegend(legend);
310            }
311        } else {
312            setLegend(null);
313        }
314    }
315}