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 }