1 /*
   2  * Copyright (c) 2010, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.chart;
  27 
  28 import java.util.ArrayList;
  29 import java.util.Iterator;
  30 import java.util.List;
  31 
  32 import javafx.animation.FadeTransition;
  33 import javafx.animation.ParallelTransition;
  34 import javafx.beans.NamedArg;
  35 import javafx.collections.FXCollections;
  36 import javafx.collections.ObservableList;
  37 import javafx.scene.Node;
  38 import javafx.scene.layout.StackPane;
  39 import javafx.scene.shape.Ellipse;
  40 import javafx.util.Duration;
  41 
  42 import com.sun.javafx.charts.Legend.LegendItem;
  43 
  44 /**
  45  * Chart type that plots bubbles for the data points in a series. The extra value property of Data is used to represent
  46  * the radius of the bubble it should be a java.lang.Number.
  47  * @since JavaFX 2.0
  48  */
  49 public class BubbleChart<X,Y> extends XYChart<X,Y> {
  50 
  51     // -------------- CONSTRUCTORS ----------------------------------------------
  52 
  53     /**
  54      * Construct a new BubbleChart with the given axis. BubbleChart does not use a Category Axis.
  55      * Both X and Y axes should be of type NumberAxis.
  56      *
  57      * @param xAxis The x axis to use
  58      * @param yAxis The y axis to use
  59      */
  60     public BubbleChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis) {
  61         this(xAxis, yAxis, FXCollections.<Series<X, Y>>observableArrayList());
  62     }
  63 
  64     /**
  65      * Construct a new BubbleChart with the given axis and data. BubbleChart does not
  66      * use a Category Axis. Both X and Y axes should be of type NumberAxis.
  67      *
  68      * @param xAxis The x axis to use
  69      * @param yAxis The y axis to use
  70      * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart
  71      */
  72     public BubbleChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis, @NamedArg("data") ObservableList<Series<X,Y>> data) {
  73         super(xAxis, yAxis);
  74         if (!(xAxis instanceof ValueAxis && yAxis instanceof ValueAxis)) {
  75             throw new IllegalArgumentException("Axis type incorrect, X and Y should both be NumberAxis");
  76         }
  77         setData(data);
  78     }
  79 
  80     // -------------- METHODS ------------------------------------------------------------------------------------------
  81 
  82     /**
  83      * Used to get a double value from a object that can be a Number object or null
  84      *
  85      * @param number Object possibly a instance of Number
  86      * @param nullDefault What value to return if the number object is null or not a Number
  87      * @return number converted to double or nullDefault
  88      */
  89     private static double getDoubleValue(Object number, double nullDefault) {
  90         return !(number instanceof Number) ? nullDefault : ((Number)number).doubleValue();
  91     }
  92 
  93     /** {@inheritDoc} */
  94     @Override protected void layoutPlotChildren() {
  95         // update bubble positions
  96       for (int seriesIndex=0; seriesIndex < getDataSize(); seriesIndex++) {
  97             Series<X,Y> series = getData().get(seriesIndex);
  98 //            for (Data<X,Y> item = series.begin; item != null; item = item.next) {
  99             Iterator<Data<X,Y>> iter = getDisplayedDataIterator(series);
 100             while(iter.hasNext()) {
 101                 Data<X,Y> item = iter.next();
 102                 double x = getXAxis().getDisplayPosition(item.getCurrentX());
 103                 double y = getYAxis().getDisplayPosition(item.getCurrentY());
 104                 if (Double.isNaN(x) || Double.isNaN(y)) {
 105                     continue;
 106                 }
 107                 Node bubble = item.getNode();
 108                 Ellipse ellipse;
 109                 if (bubble != null) {
 110                     if (bubble instanceof StackPane) {
 111                         StackPane region = (StackPane)item.getNode();
 112                         if (region.getShape() == null) {
 113                             ellipse = new Ellipse(getDoubleValue(item.getExtraValue(), 1), getDoubleValue(item.getExtraValue(), 1));
 114                         } else if (region.getShape() instanceof Ellipse) {
 115                             ellipse = (Ellipse)region.getShape();
 116                         } else {
 117                             return;
 118                         }
 119                         ellipse.setRadiusX(getDoubleValue(item.getExtraValue(), 1) * ((getXAxis() instanceof NumberAxis) ? Math.abs(((NumberAxis)getXAxis()).getScale()) : 1));
 120                         ellipse.setRadiusY(getDoubleValue(item.getExtraValue(), 1) * ((getYAxis() instanceof NumberAxis) ? Math.abs(((NumberAxis)getYAxis()).getScale()) : 1));
 121                         // Note: workaround for RT-7689 - saw this in ProgressControlSkin
 122                         // The region doesn't update itself when the shape is mutated in place, so we
 123                         // null out and then restore the shape in order to force invalidation.
 124                         region.setShape(null);
 125                         region.setShape(ellipse);
 126                         region.setScaleShape(false);
 127                         region.setCenterShape(false);
 128                         region.setCacheShape(false);
 129                         // position the bubble
 130                         bubble.setLayoutX(x);
 131                         bubble.setLayoutY(y);
 132                     }
 133                 }
 134             }
 135         }
 136     }
 137 
 138     @Override protected void dataItemAdded(Series<X,Y> series, int itemIndex, Data<X,Y> item) {
 139         Node bubble = createBubble(series, getData().indexOf(series), item, itemIndex);
 140         if (shouldAnimate()) {
 141             // fade in new bubble
 142             bubble.setOpacity(0);
 143             getPlotChildren().add(bubble);
 144             FadeTransition ft = new FadeTransition(Duration.millis(500),bubble);
 145             ft.setToValue(1);
 146             ft.play();
 147         } else {
 148             getPlotChildren().add(bubble);
 149         }
 150     }
 151 
 152     @Override protected  void dataItemRemoved(final Data<X,Y> item, final Series<X,Y> series) {
 153         final Node bubble = item.getNode();
 154         if (shouldAnimate()) {
 155             // fade out old bubble
 156             FadeTransition ft = new FadeTransition(Duration.millis(500),bubble);
 157             ft.setToValue(0);
 158             ft.setOnFinished(actionEvent -> {
 159                 getPlotChildren().remove(bubble);
 160                 removeDataItemFromDisplay(series, item);
 161                 bubble.setOpacity(1.0);
 162             });
 163             ft.play();
 164         } else {
 165             getPlotChildren().remove(bubble);
 166             removeDataItemFromDisplay(series, item);
 167         }
 168     }
 169 
 170     /** {@inheritDoc} */
 171     @Override protected void dataItemChanged(Data<X, Y> item) {
 172     }
 173 
 174     @Override protected  void seriesAdded(Series<X,Y> series, int seriesIndex) {
 175         // handle any data already in series
 176         for (int j=0; j<series.getData().size(); j++) {
 177             Data<X,Y> item = series.getData().get(j);
 178             Node bubble = createBubble(series, seriesIndex, item, j);
 179             if (shouldAnimate()) {
 180                 bubble.setOpacity(0);
 181                 getPlotChildren().add(bubble);
 182                 // fade in new bubble
 183                 FadeTransition ft = new FadeTransition(Duration.millis(500),bubble);
 184                 ft.setToValue(1);
 185                 ft.play();
 186             } else {
 187                 getPlotChildren().add(bubble);
 188             }
 189         }
 190     }
 191 
 192     @Override protected  void seriesRemoved(final Series<X,Y> series) {
 193         // remove all bubble nodes
 194         if (shouldAnimate()) {
 195             ParallelTransition pt = new ParallelTransition();
 196             pt.setOnFinished(event -> {
 197                 removeSeriesFromDisplay(series);
 198             });
 199             for (XYChart.Data<X,Y> d : series.getData()) {
 200                 final Node bubble = d.getNode();
 201                 // fade out old bubble
 202                 FadeTransition ft = new FadeTransition(Duration.millis(500),bubble);
 203                 ft.setToValue(0);
 204                 ft.setOnFinished(actionEvent -> {
 205                     getPlotChildren().remove(bubble);
 206                     bubble.setOpacity(1.0);
 207                 });
 208                 pt.getChildren().add(ft);
 209             }
 210             pt.play();
 211         } else {
 212             for (XYChart.Data<X,Y> d : series.getData()) {
 213                 final Node bubble = d.getNode();
 214                 getPlotChildren().remove(bubble);
 215             }
 216             removeSeriesFromDisplay(series);
 217         }
 218 
 219     }
 220 
 221     /**
 222      * Create a Bubble for a given data item if it doesn't already have a node
 223      *
 224      *
 225      * @param series
 226      * @param seriesIndex The index of the series containing the item
 227      * @param item        The data item to create node for
 228      * @param itemIndex   The index of the data item in the series
 229      * @return Node used for given data item
 230      */
 231     private Node createBubble(Series<X, Y> series, int seriesIndex, final Data<X,Y> item, int itemIndex) {
 232         Node bubble = item.getNode();
 233         // check if bubble has already been created
 234         if (bubble == null) {
 235             bubble = new StackPane();
 236             item.setNode(bubble);
 237         }
 238         // set bubble styles
 239         bubble.getStyleClass().setAll("chart-bubble", "series" + seriesIndex, "data" + itemIndex,
 240                 series.defaultColorStyleClass);
 241         return bubble;
 242     }
 243 
 244     /**
 245      * This is called when the range has been invalidated and we need to update it. If the axis are auto
 246      * ranging then we compile a list of all data that the given axis has to plot and call invalidateRange() on the
 247      * axis passing it that data.
 248      */
 249     @Override protected void updateAxisRange() {
 250         // For bubble chart we need to override this method as we need to let the axis know that they need to be able
 251         // to cover the whole area occupied by the bubble not just its center data value
 252         final Axis<X> xa = getXAxis();
 253         final Axis<Y> ya = getYAxis();
 254         List<X> xData = null;
 255         List<Y> yData = null;
 256         if(xa.isAutoRanging()) xData = new ArrayList<X>();
 257         if(ya.isAutoRanging()) yData = new ArrayList<Y>();
 258         final boolean xIsCategory = xa instanceof CategoryAxis;
 259         final boolean yIsCategory = ya instanceof CategoryAxis;
 260         if(xData != null || yData != null) {
 261             for(Series<X,Y> series : getData()) {
 262                 for(Data<X,Y> data: series.getData()) {
 263                     if(xData != null) {
 264                         if(xIsCategory) {
 265                             xData.add(data.getXValue());
 266                         } else {
 267                             xData.add(xa.toRealValue(xa.toNumericValue(data.getXValue()) + getDoubleValue(data.getExtraValue(), 0)));
 268                             xData.add(xa.toRealValue(xa.toNumericValue(data.getXValue()) - getDoubleValue(data.getExtraValue(), 0)));
 269                         }
 270                     }
 271                     if(yData != null){
 272                         if(yIsCategory) {
 273                             yData.add(data.getYValue());
 274                         } else {
 275                             yData.add(ya.toRealValue(ya.toNumericValue(data.getYValue()) + getDoubleValue(data.getExtraValue(), 0)));
 276                             yData.add(ya.toRealValue(ya.toNumericValue(data.getYValue()) - getDoubleValue(data.getExtraValue(), 0)));
 277                         }
 278                     }
 279                 }
 280             }
 281             if(xData != null) xa.invalidateRange(xData);
 282             if(yData != null) ya.invalidateRange(yData);
 283         }
 284     }
 285 
 286     @Override
 287     LegendItem createLegendItemForSeries(Series<X, Y> series, int seriesIndex) {
 288         LegendItem legendItem = new LegendItem(series.getName());
 289         legendItem.getSymbol().getStyleClass().addAll("series" + seriesIndex, "chart-bubble",
 290                 "bubble-legend-symbol", series.defaultColorStyleClass);
 291         return legendItem;
 292     }
 293 }