1 /* 2 * Copyright (c) 2010, 2016, 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.*; 29 30 import javafx.animation.FadeTransition; 31 import javafx.animation.Interpolator; 32 import javafx.animation.KeyFrame; 33 import javafx.animation.KeyValue; 34 import javafx.animation.Timeline; 35 import javafx.application.Platform; 36 import javafx.beans.NamedArg; 37 import javafx.beans.property.DoubleProperty; 38 import javafx.beans.property.SimpleDoubleProperty; 39 import javafx.collections.FXCollections; 40 import javafx.collections.ListChangeListener; 41 import javafx.collections.ObservableList; 42 import javafx.scene.AccessibleRole; 43 import javafx.scene.Group; 44 import javafx.scene.Node; 45 import javafx.scene.layout.StackPane; 46 import javafx.scene.shape.ClosePath; 47 import javafx.scene.shape.LineTo; 48 import javafx.scene.shape.MoveTo; 49 import javafx.scene.shape.Path; 50 import javafx.scene.shape.PathElement; 51 import javafx.scene.shape.StrokeLineJoin; 52 import javafx.util.Duration; 53 54 import com.sun.javafx.charts.Legend.LegendItem; 55 import javafx.css.converter.BooleanConverter; 56 import javafx.beans.property.BooleanProperty; 57 import javafx.css.CssMetaData; 58 import javafx.css.Styleable; 59 import javafx.css.StyleableBooleanProperty; 60 import javafx.css.StyleableProperty; 61 62 /** 63 * AreaChart - Plots the area between the line that connects the data points and 64 * the 0 line on the Y axis. 65 * @since JavaFX 2.0 66 */ 67 public class AreaChart<X,Y> extends XYChart<X,Y> { 68 69 // -------------- PRIVATE FIELDS ------------------------------------------ 70 71 /** A multiplier for teh Y values that we store for each series, it is used to animate in a new series */ 72 private Map<Series<X,Y>, DoubleProperty> seriesYMultiplierMap = new HashMap<>(); 73 74 // -------------- PUBLIC PROPERTIES ---------------------------------------- 75 76 /** 77 * When true, CSS styleable symbols are created for any data items that don't have a symbol node specified. 78 * @since JavaFX 8.0 79 */ 80 private BooleanProperty createSymbols = new StyleableBooleanProperty(true) { 81 @Override protected void invalidated() { 82 for (int seriesIndex=0; seriesIndex < getData().size(); seriesIndex ++) { 83 Series<X,Y> series = getData().get(seriesIndex); 84 for (int itemIndex=0; itemIndex < series.getData().size(); itemIndex ++) { 85 Data<X,Y> item = series.getData().get(itemIndex); 86 Node symbol = item.getNode(); 87 if(get() && symbol == null) { // create any symbols 88 symbol = createSymbol(series, getData().indexOf(series), item, itemIndex); 89 if (null != symbol) { 90 getPlotChildren().add(symbol); 91 } 92 } else if (!get() && symbol != null) { // remove symbols 93 getPlotChildren().remove(symbol); 94 symbol = null; 95 item.setNode(null); 96 } 97 } 98 } 99 requestChartLayout(); 100 } 101 102 public Object getBean() { 103 return this; 104 } 105 106 public String getName() { 107 return "createSymbols"; 108 } 109 110 public CssMetaData<AreaChart<?, ?>,Boolean> getCssMetaData() { 111 return StyleableProperties.CREATE_SYMBOLS; 112 } 113 }; 114 115 /** 116 * Indicates whether symbols for data points will be created or not. 117 * 118 * @return true if symbols for data points will be created and false otherwise. 119 * @since JavaFX 8.0 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(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") 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(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis, @NamedArg("data") ObservableList<Series<X,Y>> data) { 146 super(xAxis,yAxis); 147 setData(data); 148 } 149 150 // -------------- METHODS ------------------------------------------------------------------------------------------ 151 152 private static double doubleValue(Number number) { return doubleValue(number, 0); } 153 private static double doubleValue(Number number, double nullDefault) { 154 return (number == null) ? nullDefault : number.doubleValue(); 155 } 156 157 /** @inheritDoc */ 158 @Override protected void updateAxisRange() { 159 final Axis<X> xa = getXAxis(); 160 final Axis<Y> ya = getYAxis(); 161 List<X> xData = null; 162 List<Y> yData = null; 163 if(xa.isAutoRanging()) xData = new ArrayList<X>(); 164 if(ya.isAutoRanging()) yData = new ArrayList<Y>(); 165 if(xData != null || yData != null) { 166 for(Series<X,Y> series : getData()) { 167 for(Data<X,Y> data: series.getData()) { 168 if(xData != null) xData.add(data.getXValue()); 169 if(yData != null) yData.add(data.getYValue()); 170 } 171 } 172 if(xData != null && !(xData.size() == 1 && getXAxis().toNumericValue(xData.get(0)) == 0)) { 173 xa.invalidateRange(xData); 174 } 175 if(yData != null && !(yData.size() == 1 && getYAxis().toNumericValue(yData.get(0)) == 0)) { 176 ya.invalidateRange(yData); 177 } 178 } 179 } 180 181 @Override protected void dataItemAdded(Series<X,Y> series, int itemIndex, Data<X,Y> item) { 182 final Node symbol = createSymbol(series, getData().indexOf(series), item, itemIndex); 183 if (shouldAnimate()) { 184 boolean animate = false; 185 if (itemIndex > 0 && itemIndex < (series.getData().size()-1)) { 186 animate = true; 187 Data<X,Y> p1 = series.getData().get(itemIndex - 1); 188 Data<X,Y> p2 = series.getData().get(itemIndex + 1); 189 double x1 = getXAxis().toNumericValue(p1.getXValue()); 190 double y1 = getYAxis().toNumericValue(p1.getYValue()); 191 double x3 = getXAxis().toNumericValue(p2.getXValue()); 192 double y3 = getYAxis().toNumericValue(p2.getYValue()); 193 194 double x2 = getXAxis().toNumericValue(item.getXValue()); 195 double y2 = getYAxis().toNumericValue(item.getYValue()); 196 197 // //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1) 198 double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1); 199 item.setCurrentY(getYAxis().toRealValue(y)); 200 item.setCurrentX(getXAxis().toRealValue(x2)); 201 //2. we can simply use the midpoint on the line as well.. 202 // double x = (x3 + x1)/2; 203 // double y = (y3 + y1)/2; 204 // item.setCurrentX(x); 205 // item.setCurrentY(y); 206 } else if (itemIndex == 0 && series.getData().size() > 1) { 207 animate = true; 208 item.setCurrentX(series.getData().get(1).getXValue()); 209 item.setCurrentY(series.getData().get(1).getYValue()); 210 } else if (itemIndex == (series.getData().size() - 1) && series.getData().size() > 1) { 211 animate = true; 212 int last = series.getData().size() - 2; 213 item.setCurrentX(series.getData().get(last).getXValue()); 214 item.setCurrentY(series.getData().get(last).getYValue()); 215 } 216 if (symbol != null) { 217 // fade in new symbol 218 symbol.setOpacity(0); 219 getPlotChildren().add(symbol); 220 FadeTransition ft = new FadeTransition(Duration.millis(500),symbol); 221 ft.setToValue(1); 222 ft.play(); 223 } 224 if (animate) { 225 animate( 226 new KeyFrame(Duration.ZERO, 227 (e) -> { 228 if (symbol != null && !getPlotChildren().contains(symbol)) { 229 getPlotChildren().add(symbol); 230 } }, 231 new KeyValue(item.currentYProperty(), 232 item.getCurrentY()), 233 new KeyValue(item.currentXProperty(), 234 item.getCurrentX()) 235 ), 236 new KeyFrame(Duration.millis(800), new KeyValue(item.currentYProperty(), 237 item.getYValue(), Interpolator.EASE_BOTH), 238 new KeyValue(item.currentXProperty(), 239 item.getXValue(), Interpolator.EASE_BOTH)) 240 ); 241 } 242 243 } else if (symbol != null) { 244 getPlotChildren().add(symbol); 245 } 246 } 247 248 @Override protected void dataItemRemoved(final Data<X,Y> item, final Series<X,Y> series) { 249 final Node symbol = item.getNode(); 250 251 if (symbol != null) { 252 symbol.focusTraversableProperty().unbind(); 253 } 254 255 // remove item from sorted list 256 int itemIndex = series.getItemIndex(item); 257 if (shouldAnimate()) { 258 boolean animate = false; 259 // dataSize represents size of currently visible data. After this operation, the number will decrement by 1 260 final int dataSize = series.getDataSize(); 261 // This is the size of current data list in Series. Note that it might be totaly different from dataSize as 262 // some big operation might have happened on the list. 263 final int dataListSize = series.getData().size(); 264 if (itemIndex > 0 && itemIndex < dataSize -1) { 265 animate = true; 266 Data<X,Y> p1 = series.getItem(itemIndex - 1); 267 Data<X,Y> p2 = series.getItem(itemIndex + 1); 268 double x1 = getXAxis().toNumericValue(p1.getXValue()); 269 double y1 = getYAxis().toNumericValue(p1.getYValue()); 270 double x3 = getXAxis().toNumericValue(p2.getXValue()); 271 double y3 = getYAxis().toNumericValue(p2.getYValue()); 272 273 double x2 = getXAxis().toNumericValue(item.getXValue()); 274 double y2 = getYAxis().toNumericValue(item.getYValue()); 275 276 // //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1) 277 double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1); 278 item.setCurrentX(getXAxis().toRealValue(x2)); 279 item.setCurrentY(getYAxis().toRealValue(y2)); 280 item.setXValue(getXAxis().toRealValue(x2)); 281 item.setYValue(getYAxis().toRealValue(y)); 282 //2. we can simply use the midpoint on the line as well.. 283 // double x = (x3 + x1)/2; 284 // double y = (y3 + y1)/2; 285 // item.setCurrentX(x); 286 // item.setCurrentY(y); 287 } else if (itemIndex == 0 && dataListSize > 1) { 288 animate = true; 289 item.setXValue(series.getData().get(0).getXValue()); 290 item.setYValue(series.getData().get(0).getYValue()); 291 } else if (itemIndex == (dataSize - 1) && dataListSize > 1) { 292 animate = true; 293 int last = dataListSize - 1; 294 item.setXValue(series.getData().get(last).getXValue()); 295 item.setYValue(series.getData().get(last).getYValue()); 296 } else if (symbol != null) { 297 // fade out symbol 298 symbol.setOpacity(0); 299 FadeTransition ft = new FadeTransition(Duration.millis(500),symbol); 300 ft.setToValue(0); 301 ft.setOnFinished(actionEvent -> { 302 getPlotChildren().remove(symbol); 303 removeDataItemFromDisplay(series, item); 304 }); 305 ft.play(); 306 } else { 307 item.setSeries(null); 308 removeDataItemFromDisplay(series, item); 309 } 310 if (animate) { 311 animate( new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(), 312 item.getCurrentY()), new KeyValue(item.currentXProperty(), 313 item.getCurrentX())), 314 new KeyFrame(Duration.millis(800), actionEvent -> { 315 item.setSeries(null); 316 getPlotChildren().remove(symbol); 317 removeDataItemFromDisplay(series, item); 318 }, 319 new KeyValue(item.currentYProperty(), 320 item.getYValue(), Interpolator.EASE_BOTH), 321 new KeyValue(item.currentXProperty(), 322 item.getXValue(), Interpolator.EASE_BOTH)) 323 ); 324 } 325 } else { 326 item.setSeries(null); 327 getPlotChildren().remove(symbol); 328 removeDataItemFromDisplay(series, item); 329 } 330 //Note: better animation here, point should move from old position to new position at center point between prev and next symbols 331 } 332 333 /** @inheritDoc */ 334 @Override protected void dataItemChanged(Data<X, Y> item) { 335 } 336 337 @Override protected void seriesChanged(ListChangeListener.Change<? extends Series> c) { 338 // Update style classes for all series lines and symbols 339 // Note: is there a more efficient way of doing this? 340 for (int i = 0; i < getDataSize(); i++) { 341 final Series<X,Y> s = getData().get(i); 342 Path seriesLine = (Path)((Group)s.getNode()).getChildren().get(1); 343 Path fillPath = (Path)((Group)s.getNode()).getChildren().get(0); 344 seriesLine.getStyleClass().setAll("chart-series-area-line", "series" + i, s.defaultColorStyleClass); 345 fillPath.getStyleClass().setAll("chart-series-area-fill", "series" + i, s.defaultColorStyleClass); 346 for (int j=0; j < s.getData().size(); j++) { 347 final Data<X,Y> item = s.getData().get(j); 348 final Node node = item.getNode(); 349 if(node!=null) node.getStyleClass().setAll("chart-area-symbol", "series" + i, "data" + j, s.defaultColorStyleClass); 350 } 351 } 352 } 353 354 @Override protected void seriesAdded(Series<X,Y> series, int seriesIndex) { 355 // create new paths for series 356 Path seriesLine = new Path(); 357 Path fillPath = new Path(); 358 seriesLine.setStrokeLineJoin(StrokeLineJoin.BEVEL); 359 Group areaGroup = new Group(fillPath,seriesLine); 360 series.setNode(areaGroup); 361 // create series Y multiplier 362 DoubleProperty seriesYAnimMultiplier = new SimpleDoubleProperty(this, "seriesYMultiplier"); 363 seriesYMultiplierMap.put(series, seriesYAnimMultiplier); 364 // handle any data already in series 365 if (shouldAnimate()) { 366 seriesYAnimMultiplier.setValue(0d); 367 } else { 368 seriesYAnimMultiplier.setValue(1d); 369 } 370 getPlotChildren().add(areaGroup); 371 List<KeyFrame> keyFrames = new ArrayList<KeyFrame>(); 372 if (shouldAnimate()) { 373 // animate in new series 374 keyFrames.add(new KeyFrame(Duration.ZERO, 375 new KeyValue(areaGroup.opacityProperty(), 0), 376 new KeyValue(seriesYAnimMultiplier, 0) 377 )); 378 keyFrames.add(new KeyFrame(Duration.millis(200), 379 new KeyValue(areaGroup.opacityProperty(), 1) 380 )); 381 keyFrames.add(new KeyFrame(Duration.millis(500), 382 new KeyValue(seriesYAnimMultiplier, 1) 383 )); 384 } 385 for (int j=0; j<series.getData().size(); j++) { 386 Data<X,Y> item = series.getData().get(j); 387 final Node symbol = createSymbol(series, seriesIndex, item, j); 388 if (symbol != null) { 389 if (shouldAnimate()) { 390 symbol.setOpacity(0); 391 getPlotChildren().add(symbol); 392 // fade in new symbol 393 keyFrames.add(new KeyFrame(Duration.ZERO, new KeyValue(symbol.opacityProperty(), 0))); 394 keyFrames.add(new KeyFrame(Duration.millis(200), new KeyValue(symbol.opacityProperty(), 1))); 395 } 396 else { 397 getPlotChildren().add(symbol); 398 } 399 } 400 } 401 if (shouldAnimate()) animate(keyFrames.toArray(new KeyFrame[keyFrames.size()])); 402 } 403 404 @Override protected void seriesRemoved(final Series<X,Y> series) { 405 // remove series Y multiplier 406 seriesYMultiplierMap.remove(series); 407 // remove all symbol nodes 408 if (shouldAnimate()) { 409 Timeline tl = new Timeline(createSeriesRemoveTimeLine(series, 400)); 410 tl.play(); 411 } else { 412 getPlotChildren().remove(series.getNode()); 413 for (Data<X,Y> d:series.getData()) getPlotChildren().remove(d.getNode()); 414 removeSeriesFromDisplay(series); 415 } 416 } 417 418 /** @inheritDoc */ 419 @Override protected void layoutPlotChildren() { 420 List<LineTo> constructedPath = new ArrayList<>(getDataSize()); 421 for (int seriesIndex=0; seriesIndex < getDataSize(); seriesIndex++) { 422 Series<X, Y> series = getData().get(seriesIndex); 423 DoubleProperty seriesYAnimMultiplier = seriesYMultiplierMap.get(series); 424 double lastX = 0; 425 final ObservableList<Node> children = ((Group) series.getNode()).getChildren(); 426 ObservableList<PathElement> seriesLine = ((Path) children.get(1)).getElements(); 427 ObservableList<PathElement> fillPath = ((Path) children.get(0)).getElements(); 428 seriesLine.clear(); 429 fillPath.clear(); 430 constructedPath.clear(); 431 for (Iterator<Data<X, Y>> it = getDisplayedDataIterator(series); it.hasNext(); ) { 432 Data<X, Y> item = it.next(); 433 double x = getXAxis().getDisplayPosition(item.getCurrentX()); 434 double y = getYAxis().getDisplayPosition( 435 getYAxis().toRealValue(getYAxis().toNumericValue(item.getCurrentY()) * seriesYAnimMultiplier.getValue())); 436 constructedPath.add(new LineTo(x, y)); 437 if (Double.isNaN(x) || Double.isNaN(y)) { 438 continue; 439 } 440 lastX = x; 441 Node symbol = item.getNode(); 442 if (symbol != null) { 443 final double w = symbol.prefWidth(-1); 444 final double h = symbol.prefHeight(-1); 445 symbol.resizeRelocate(x-(w/2), y-(h/2),w,h); 446 } 447 } 448 449 if (!constructedPath.isEmpty()) { 450 Collections.sort(constructedPath, (e1, e2) -> Double.compare(e1.getX(), e2.getX())); 451 LineTo first = constructedPath.get(0); 452 453 final double displayYPos = first.getY(); 454 final double numericYPos = getYAxis().toNumericValue(getYAxis().getValueForDisplay(displayYPos)); 455 456 // RT-34626: We can't always use getZeroPosition(), as it may be the case 457 // that the zero position of the y-axis is not visible on the chart. In these 458 // cases, we need to use the height between the point and the y-axis line. 459 final double yAxisZeroPos = getYAxis().getZeroPosition(); 460 final boolean isYAxisZeroPosVisible = !Double.isNaN(yAxisZeroPos); 461 final double yAxisHeight = getYAxis().getHeight(); 462 final double yFillPos = isYAxisZeroPosVisible ? yAxisZeroPos : 463 numericYPos < 0 ? numericYPos - yAxisHeight : yAxisHeight; 464 465 seriesLine.add(new MoveTo(first.getX(), displayYPos)); 466 fillPath.add(new MoveTo(first.getX(), yFillPos)); 467 468 seriesLine.addAll(constructedPath); 469 fillPath.addAll(constructedPath); 470 fillPath.add(new LineTo(lastX, yFillPos)); 471 fillPath.add(new ClosePath()); 472 } 473 } 474 } 475 476 private Node createSymbol(Series<X,Y> series, int seriesIndex, final Data<X,Y> item, int itemIndex) { 477 Node symbol = item.getNode(); 478 // check if symbol has already been created 479 if (symbol == null && getCreateSymbols()) { 480 symbol = new StackPane(); 481 symbol.setAccessibleRole(AccessibleRole.TEXT); 482 symbol.setAccessibleRoleDescription("Point"); 483 symbol.focusTraversableProperty().bind(Platform.accessibilityActiveProperty()); 484 item.setNode(symbol); 485 } 486 // set symbol styles 487 // Note: not sure if we want to add or check, ie be more careful and efficient here 488 if (symbol != null) symbol.getStyleClass().setAll("chart-area-symbol", "series" + seriesIndex, "data" + itemIndex, 489 series.defaultColorStyleClass); 490 return symbol; 491 } 492 493 @Override 494 LegendItem createLegendItemForSeries(Series<X, Y> series, int seriesIndex) { 495 LegendItem legendItem = new LegendItem(series.getName()); 496 legendItem.getSymbol().getStyleClass().addAll("chart-area-symbol", "series" + seriesIndex, 497 "area-legend-symbol", series.defaultColorStyleClass); 498 return legendItem; 499 } 500 501 // -------------- STYLESHEET HANDLING -------------------------------------- 502 503 private static class StyleableProperties { 504 private static final CssMetaData<AreaChart<?,?>,Boolean> CREATE_SYMBOLS = 505 new CssMetaData<AreaChart<?,?>,Boolean>("-fx-create-symbols", 506 BooleanConverter.getInstance(), Boolean.TRUE) { 507 508 @Override 509 public boolean isSettable(AreaChart<?,?> node) { 510 return node.createSymbols == null || !node.createSymbols.isBound(); 511 } 512 513 @Override 514 public StyleableProperty<Boolean> getStyleableProperty(AreaChart<?,?> node) { 515 return (StyleableProperty<Boolean>)node.createSymbolsProperty(); 516 } 517 }; 518 519 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 520 static { 521 final List<CssMetaData<? extends Styleable, ?>> styleables = 522 new ArrayList<CssMetaData<? extends Styleable, ?>>(XYChart.getClassCssMetaData()); 523 styleables.add(CREATE_SYMBOLS); 524 STYLEABLES = Collections.unmodifiableList(styleables); 525 } 526 } 527 528 /** 529 * @return The CssMetaData associated with this class, which may include the 530 * CssMetaData of its superclasses. 531 * @since JavaFX 8.0 532 */ 533 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 534 return StyleableProperties.STYLEABLES; 535 } 536 537 /** 538 * {@inheritDoc} 539 * @since JavaFX 8.0 540 */ 541 @Override 542 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 543 return getClassCssMetaData(); 544 } 545 546 }