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 import javafx.scene.AccessibleRole;
  42 import javafx.application.Platform;
  43 
  44 import com.sun.javafx.charts.Legend.LegendItem;
  45 
  46 /**
  47  * Chart type that plots bubbles for the data points in a series. The extra value property of Data is used to represent
  48  * the radius of the bubble it should be a java.lang.Number.
  49  * @since JavaFX 2.0
  50  */
  51 public class BubbleChart<X,Y> extends XYChart<X,Y> {
  52 
  53     // -------------- CONSTRUCTORS ----------------------------------------------
  54 
  55     /**
  56      * Construct a new BubbleChart with the given axis. BubbleChart does not use a Category Axis.
  57      * Both X and Y axes should be of type NumberAxis.
  58      *
  59      * @param xAxis The x axis to use
  60      * @param yAxis The y axis to use
  61      */
  62     public BubbleChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis) {
  63         this(xAxis, yAxis, FXCollections.<Series<X, Y>>observableArrayList());
  64     }
  65 
  66     /**
  67      * Construct a new BubbleChart with the given axis and data. BubbleChart does not
  68      * use a Category Axis. Both X and Y axes should be of type NumberAxis.
  69      *
  70      * @param xAxis The x axis to use
  71      * @param yAxis The y axis to use
  72      * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart
  73      */
  74     public BubbleChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis, @NamedArg("data") ObservableList<Series<X,Y>> data) {
  75         super(xAxis, yAxis);
  76         if (!(xAxis instanceof ValueAxis && yAxis instanceof ValueAxis)) {
  77             throw new IllegalArgumentException("Axis type incorrect, X and Y should both be NumberAxis");
  78         }
  79         setData(data);
  80     }
  81 
  82     // -------------- METHODS ------------------------------------------------------------------------------------------
  83 
  84     /**
  85      * Used to get a double value from a object that can be a Number object or null
  86      *
  87      * @param number Object possibly a instance of Number
  88      * @param nullDefault What value to return if the number object is null or not a Number
  89      * @return number converted to double or nullDefault
  90      */
  91     private static double getDoubleValue(Object number, double nullDefault) {
  92         return !(number instanceof Number) ? nullDefault : ((Number)number).doubleValue();
  93     }
  94 
  95     /** {@inheritDoc} */
  96     @Override protected void layoutPlotChildren() {
  97         // update bubble positions
  98       for (int seriesIndex=0; seriesIndex < getDataSize(); seriesIndex++) {
  99             Series<X,Y> series = getData().get(seriesIndex);
 100 //            for (Data<X,Y> item = series.begin; item != null; item = item.next) {
 101             Iterator<Data<X,Y>> iter = getDisplayedDataIterator(series);
 102             while(iter.hasNext()) {
 103                 Data<X,Y> item = iter.next();
 104                 double x = getXAxis().getDisplayPosition(item.getCurrentX());
 105                 double y = getYAxis().getDisplayPosition(item.getCurrentY());
 106                 if (Double.isNaN(x) || Double.isNaN(y)) {
 107                     continue;
 108                 }
 109                 Node bubble = item.getNode();
 110                 Ellipse ellipse;
 111                 if (bubble != null) {
 112                     if (bubble instanceof StackPane) {
 113                         StackPane region = (StackPane)item.getNode();
 114                         if (region.getShape() == null) {
 115                             ellipse = new Ellipse(getDoubleValue(item.getExtraValue(), 1), getDoubleValue(item.getExtraValue(), 1));
 116                         } else if (region.getShape() instanceof Ellipse) {
 117                             ellipse = (Ellipse)region.getShape();
 118                         } else {
 119                             return;
 120                         }
 121                         ellipse.setRadiusX(getDoubleValue(item.getExtraValue(), 1) * ((getXAxis() instanceof NumberAxis) ? Math.abs(((NumberAxis)getXAxis()).getScale()) : 1));
 122                         ellipse.setRadiusY(getDoubleValue(item.getExtraValue(), 1) * ((getYAxis() instanceof NumberAxis) ? Math.abs(((NumberAxis)getYAxis()).getScale()) : 1));
 123                         // Note: workaround for RT-7689 - saw this in ProgressControlSkin
 124                         // The region doesn't update itself when the shape is mutated in place, so we
 125                         // null out and then restore the shape in order to force invalidation.
 126                         region.setShape(null);
 127                         region.setShape(ellipse);
 128                         region.setScaleShape(false);
 129                         region.setCenterShape(false);
 130                         region.setCacheShape(false);
 131                         // position the bubble
 132                         bubble.setLayoutX(x);
 133                         bubble.setLayoutY(y);
 134                     }
 135                 }
 136             }
 137         }
 138     }
 139 
 140     @Override protected void dataItemAdded(Series<X,Y> series, int itemIndex, Data<X,Y> item) {
 141         Node bubble = createBubble(series, getData().indexOf(series), item, itemIndex);
 142         if (shouldAnimate()) {
 143             // fade in new bubble
 144             bubble.setOpacity(0);
 145             getPlotChildren().add(bubble);
 146             FadeTransition ft = new FadeTransition(Duration.millis(500),bubble);
 147             ft.setToValue(1);
 148             ft.play();
 149         } else {
 150             getPlotChildren().add(bubble);
 151         }
 152     }
 153 
 154     @Override protected  void dataItemRemoved(final Data<X,Y> item, final Series<X,Y> series) {
 155         final Node bubble = item.getNode();
 156         if (shouldAnimate()) {
 157             // fade out old bubble
 158             FadeTransition ft = new FadeTransition(Duration.millis(500),bubble);
 159             ft.setToValue(0);
 160             ft.setOnFinished(actionEvent -> {
 161                 getPlotChildren().remove(bubble);
 162                 removeDataItemFromDisplay(series, item);
 163                 bubble.setOpacity(1.0);
 164             });
 165             ft.play();
 166         } else {
 167             getPlotChildren().remove(bubble);
 168             removeDataItemFromDisplay(series, item);
 169         }
 170     }
 171 
 172     /** {@inheritDoc} */
 173     @Override protected void dataItemChanged(Data<X, Y> item) {
 174     }
 175 
 176     @Override protected  void seriesAdded(Series<X,Y> series, int seriesIndex) {
 177         // handle any data already in series
 178         for (int j=0; j<series.getData().size(); j++) {
 179             Data<X,Y> item = series.getData().get(j);
 180             Node bubble = createBubble(series, seriesIndex, item, j);
 181             if (shouldAnimate()) {
 182                 bubble.setOpacity(0);
 183                 getPlotChildren().add(bubble);
 184                 // fade in new bubble
 185                 FadeTransition ft = new FadeTransition(Duration.millis(500),bubble);
 186                 ft.setToValue(1);
 187                 ft.play();
 188             } else {
 189                 getPlotChildren().add(bubble);
 190             }
 191         }
 192     }
 193 
 194     @Override protected  void seriesRemoved(final Series<X,Y> series) {
 195         // remove all bubble nodes
 196         if (shouldAnimate()) {
 197             ParallelTransition pt = new ParallelTransition();
 198             pt.setOnFinished(event -> {
 199                 removeSeriesFromDisplay(series);
 200             });
 201             for (XYChart.Data<X,Y> d : series.getData()) {
 202                 final Node bubble = d.getNode();
 203                 // fade out old bubble
 204                 FadeTransition ft = new FadeTransition(Duration.millis(500),bubble);
 205                 ft.setToValue(0);
 206                 ft.setOnFinished(actionEvent -> {
 207                     getPlotChildren().remove(bubble);
 208                     bubble.setOpacity(1.0);
 209                 });
 210                 pt.getChildren().add(ft);
 211             }
 212             pt.play();
 213         } else {
 214             for (XYChart.Data<X,Y> d : series.getData()) {
 215                 final Node bubble = d.getNode();
 216                 getPlotChildren().remove(bubble);
 217             }
 218             removeSeriesFromDisplay(series);
 219         }
 220 
 221     }
 222 
 223     /**
 224      * Create a Bubble for a given data item if it doesn't already have a node
 225      *
 226      *
 227      * @param series
 228      * @param seriesIndex The index of the series containing the item
 229      * @param item        The data item to create node for
 230      * @param itemIndex   The index of the data item in the series
 231      * @return Node used for given data item
 232      */
 233     private Node createBubble(Series<X, Y> series, int seriesIndex, final Data<X,Y> item, int itemIndex) {
 234         Node bubble = item.getNode();
 235         // check if bubble has already been created
 236         if (bubble == null) {
 237             bubble = new StackPane();
 238             bubble.setAccessibleRole(AccessibleRole.TEXT);
 239             bubble.setAccessibleRoleDescription("Bubble");
 240             bubble.focusTraversableProperty().bind(Platform.accessibilityActiveProperty());
 241             item.setNode(bubble);
 242         }
 243         // set bubble styles
 244         bubble.getStyleClass().setAll("chart-bubble", "series" + seriesIndex, "data" + itemIndex,
 245                 series.defaultColorStyleClass);
 246         return bubble;
 247     }
 248 
 249     /**
 250      * This is called when the range has been invalidated and we need to update it. If the axis are auto
 251      * ranging then we compile a list of all data that the given axis has to plot and call invalidateRange() on the
 252      * axis passing it that data.
 253      */
 254     @Override protected void updateAxisRange() {
 255         // For bubble chart we need to override this method as we need to let the axis know that they need to be able
 256         // to cover the whole area occupied by the bubble not just its center data value
 257         final Axis<X> xa = getXAxis();
 258         final Axis<Y> ya = getYAxis();
 259         List<X> xData = null;
 260         List<Y> yData = null;
 261         if(xa.isAutoRanging()) xData = new ArrayList<X>();
 262         if(ya.isAutoRanging()) yData = new ArrayList<Y>();
 263         final boolean xIsCategory = xa instanceof CategoryAxis;
 264         final boolean yIsCategory = ya instanceof CategoryAxis;
 265         if(xData != null || yData != null) {
 266             for(Series<X,Y> series : getData()) {
 267                 for(Data<X,Y> data: series.getData()) {
 268                     if(xData != null) {
 269                         if(xIsCategory) {
 270                             xData.add(data.getXValue());
 271                         } else {
 272                             xData.add(xa.toRealValue(xa.toNumericValue(data.getXValue()) + getDoubleValue(data.getExtraValue(), 0)));
 273                             xData.add(xa.toRealValue(xa.toNumericValue(data.getXValue()) - getDoubleValue(data.getExtraValue(), 0)));
 274                         }
 275                     }
 276                     if(yData != null){
 277                         if(yIsCategory) {
 278                             yData.add(data.getYValue());
 279                         } else {
 280                             yData.add(ya.toRealValue(ya.toNumericValue(data.getYValue()) + getDoubleValue(data.getExtraValue(), 0)));
 281                             yData.add(ya.toRealValue(ya.toNumericValue(data.getYValue()) - getDoubleValue(data.getExtraValue(), 0)));
 282                         }
 283                     }
 284                 }
 285             }
 286             if(xData != null) xa.invalidateRange(xData);
 287             if(yData != null) ya.invalidateRange(yData);
 288         }
 289     }
 290 
 291     @Override
 292     LegendItem createLegendItemForSeries(Series<X, Y> series, int seriesIndex) {
 293         LegendItem legendItem = new LegendItem(series.getName());
 294         legendItem.getSymbol().getStyleClass().addAll("series" + seriesIndex, "chart-bubble",
 295                 "bubble-legend-symbol", series.defaultColorStyleClass);
 296         return legendItem;
 297     }
 298 }