1 /* 2 * Copyright (c) 2011, 2015, 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 29 import java.util.*; 30 31 import javafx.animation.*; 32 import javafx.application.Platform; 33 import javafx.beans.NamedArg; 34 import javafx.beans.property.DoubleProperty; 35 import javafx.beans.property.SimpleDoubleProperty; 36 import javafx.collections.FXCollections; 37 import javafx.collections.ListChangeListener; 38 import javafx.collections.ObservableList; 39 import javafx.scene.AccessibleRole; 40 import javafx.scene.Group; 41 import javafx.scene.Node; 42 import javafx.scene.layout.StackPane; 43 import javafx.scene.shape.*; 44 import javafx.util.Duration; 45 46 import com.sun.javafx.charts.Legend; 47 import com.sun.javafx.charts.Legend.LegendItem; 48 import com.sun.javafx.css.converters.BooleanConverter; 49 50 import javafx.beans.property.BooleanProperty; 51 import javafx.beans.value.WritableValue; 52 import javafx.css.CssMetaData; 53 import javafx.css.Styleable; 54 import javafx.css.StyleableBooleanProperty; 55 import javafx.css.StyleableProperty; 56 57 /** 58 * StackedAreaChart is a variation of {@link AreaChart} that displays trends of the 59 * contribution of each value. (over time e.g.) The areas are stacked so that each 60 * series adjoins but does not overlap the preceding series. This contrasts with 61 * the Area chart where each series overlays the preceding series. 62 * 63 * The cumulative nature of the StackedAreaChart gives an idea of the total Y data 64 * value at any given point along the X axis. 65 * 66 * Since data points across multiple series may not be common, StackedAreaChart 67 * interpolates values along the line joining the data points whenever necessary. 68 * 69 * @since JavaFX 2.1 70 */ 71 public class StackedAreaChart<X,Y> extends XYChart<X,Y> { 72 73 // -------------- PRIVATE FIELDS ------------------------------------------ 74 75 /** A multiplier for teh Y values that we store for each series, it is used to animate in a new series */ 76 private Map<Series<X,Y>, DoubleProperty> seriesYMultiplierMap = new HashMap<>(); 77 private Legend legend = new Legend(); 78 79 // -------------- PUBLIC PROPERTIES ---------------------------------------- 80 /** 81 * When true, CSS styleable symbols are created for any data items that 82 * don't have a symbol node specified. 83 * @since JavaFX 8.0 84 */ 85 private BooleanProperty createSymbols = new StyleableBooleanProperty(true) { 86 @Override 87 protected void invalidated() { 88 for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) { 89 Series<X, Y> series = getData().get(seriesIndex); 90 for (int itemIndex = 0; itemIndex < series.getData().size(); itemIndex++) { 91 Data<X, Y> item = series.getData().get(itemIndex); 92 Node symbol = item.getNode(); 93 if (get() && symbol == null) { // create any symbols 94 symbol = createSymbol(series, getData().indexOf(series), item, itemIndex); 95 if (null != symbol) { 96 getPlotChildren().add(symbol); 97 } 98 } else if (!get() && symbol != null) { // remove symbols 99 getPlotChildren().remove(symbol); 100 symbol = null; 101 item.setNode(null); 102 } 103 } 104 } 105 requestChartLayout(); 106 } 107 108 public Object getBean() { 109 return this; 110 } 111 112 public String getName() { 113 return "createSymbols"; 114 } 115 116 public CssMetaData<StackedAreaChart<?, ?>,Boolean> getCssMetaData() { 117 return StyleableProperties.CREATE_SYMBOLS; 118 } 119 }; 120 121 /** 122 * Indicates whether symbols for data points will be created or not. 123 * 124 * @return true if symbols for data points will be created and false otherwise. 125 * @since JavaFX 8.0 126 */ 127 public final boolean getCreateSymbols() { return createSymbols.getValue(); } 128 public final void setCreateSymbols(boolean value) { createSymbols.setValue(value); } 129 public final BooleanProperty createSymbolsProperty() { return createSymbols; } 130 131 // -------------- CONSTRUCTORS ---------------------------------------------- 132 133 /** 134 * Construct a new Area Chart with the given axis 135 * 136 * @param xAxis The x axis to use 137 * @param yAxis The y axis to use 138 */ 139 public StackedAreaChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis) { 140 this(xAxis,yAxis, FXCollections.<Series<X,Y>>observableArrayList()); 141 } 142 143 /** 144 * Construct a new Area Chart with the given axis and data. 145 * <p> 146 * Note: yAxis must be a ValueAxis, otherwise {@code IllegalArgumentException} is thrown. 147 * 148 * @param xAxis The x axis to use 149 * @param yAxis The y axis to use 150 * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart 151 * 152 * @throws java.lang.IllegalArgumentException if yAxis is not a ValueAxis 153 */ 154 public StackedAreaChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis, @NamedArg("data") ObservableList<Series<X,Y>> data) { 155 super(xAxis,yAxis); 156 if (!(yAxis instanceof ValueAxis)) { 157 throw new IllegalArgumentException("Axis type incorrect, yAxis must be of ValueAxis type."); 158 } 159 setLegend(legend); 160 setData(data); 161 } 162 163 // -------------- METHODS ------------------------------------------------------------------------------------------ 164 165 private static double doubleValue(Number number) { return doubleValue(number, 0); } 166 private static double doubleValue(Number number, double nullDefault) { 167 return (number == null) ? nullDefault : number.doubleValue(); 168 } 169 170 @Override protected void dataItemAdded(Series<X,Y> series, int itemIndex, Data<X,Y> item) { 171 final Node symbol = createSymbol(series, getData().indexOf(series), item, itemIndex); 172 if (shouldAnimate()) { 173 boolean animate = false; 174 if (itemIndex > 0 && itemIndex < (series.getData().size()-1)) { 175 animate = true; 176 Data<X,Y> p1 = series.getData().get(itemIndex - 1); 177 Data<X,Y> p2 = series.getData().get(itemIndex + 1); 178 double x1 = getXAxis().toNumericValue(p1.getXValue()); 179 double y1 = getYAxis().toNumericValue(p1.getYValue()); 180 double x3 = getXAxis().toNumericValue(p2.getXValue()); 181 double y3 = getYAxis().toNumericValue(p2.getYValue()); 182 183 double x2 = getXAxis().toNumericValue(item.getXValue()); 184 double y2 = getYAxis().toNumericValue(item.getYValue()); 185 186 // //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1) 187 double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1); 188 item.setCurrentY(getYAxis().toRealValue(y)); 189 item.setCurrentX(getXAxis().toRealValue(x2)); 190 //2. we can simply use the midpoint on the line as well.. 191 // double x = (x3 + x1)/2; 192 // double y = (y3 + y1)/2; 193 // item.setCurrentX(x); 194 // item.setCurrentY(y); 195 } else if (itemIndex == 0 && series.getData().size() > 1) { 196 animate = true; 197 item.setCurrentX(series.getData().get(1).getXValue()); 198 item.setCurrentY(series.getData().get(1).getYValue()); 199 } else if (itemIndex == (series.getData().size() - 1) && series.getData().size() > 1) { 200 animate = true; 201 int last = series.getData().size() - 2; 202 item.setCurrentX(series.getData().get(last).getXValue()); 203 item.setCurrentY(series.getData().get(last).getYValue()); 204 } else if (symbol != null) { 205 // fade in new symbol 206 symbol.setOpacity(0); 207 getPlotChildren().add(symbol); 208 FadeTransition ft = new FadeTransition(Duration.millis(500),symbol); 209 ft.setToValue(1); 210 ft.play(); 211 } 212 if (animate) { 213 animate( 214 new KeyFrame(Duration.ZERO, 215 (e) -> { 216 if (symbol != null && !getPlotChildren().contains(symbol)) { 217 getPlotChildren().add(symbol); 218 } }, 219 new KeyValue(item.currentYProperty(), 220 item.getCurrentY()), 221 new KeyValue(item.currentXProperty(), 222 item.getCurrentX()) 223 ), 224 new KeyFrame(Duration.millis(800), new KeyValue(item.currentYProperty(), 225 item.getYValue(), Interpolator.EASE_BOTH), 226 new KeyValue(item.currentXProperty(), 227 item.getXValue(), Interpolator.EASE_BOTH)) 228 ); 229 } 230 231 } else if (symbol != null) { 232 getPlotChildren().add(symbol); 233 } 234 } 235 236 @Override protected void dataItemRemoved(final Data<X,Y> item, final Series<X,Y> series) { 237 final Node symbol = item.getNode(); 238 239 if (symbol != null) { 240 symbol.focusTraversableProperty().unbind(); 241 } 242 243 // remove item from sorted list 244 int itemIndex = series.getItemIndex(item); 245 if (shouldAnimate()) { 246 boolean animate = false; 247 if (itemIndex > 0 && itemIndex < series.getDataSize()) { 248 animate = true; 249 int index=0; 250 251 Data<X,Y> p1 = series.getItem(itemIndex - 1); 252 Data<X,Y> p2 = series.getItem(itemIndex + 1); 253 254 if (p2 == null) { 255 return; 256 } 257 258 double x1 = getXAxis().toNumericValue(p1.getXValue()); 259 double y1 = getYAxis().toNumericValue(p1.getYValue()); 260 double x3 = getXAxis().toNumericValue(p2.getXValue()); 261 double y3 = getYAxis().toNumericValue(p2.getYValue()); 262 263 double x2 = getXAxis().toNumericValue(item.getXValue()); 264 double y2 = getYAxis().toNumericValue(item.getYValue()); 265 266 // //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1) 267 double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1); 268 item.setCurrentX(getXAxis().toRealValue(x2)); 269 item.setCurrentY(getYAxis().toRealValue(y2)); 270 item.setXValue(getXAxis().toRealValue(x2)); 271 item.setYValue(getYAxis().toRealValue(y)); 272 //2. we can simply use the midpoint on the line as well.. 273 // double x = (x3 + x1)/2; 274 // double y = (y3 + y1)/2; 275 // item.setCurrentX(x); 276 // item.setCurrentY(y); 277 } else if (itemIndex == 0 && series.getDataSize() > 1) { 278 animate = true; 279 item.setXValue(series.getData().get(0).getXValue()); 280 item.setYValue(series.getData().get(0).getYValue()); 281 } else if (itemIndex == (series.getDataSize() - 1) && series.getDataSize() > 1) { 282 animate = true; 283 int last = series.getData().size() - 1; 284 item.setXValue(series.getData().get(last).getXValue()); 285 item.setYValue(series.getData().get(last).getYValue()); 286 } else { 287 // fade out symbol 288 symbol.setOpacity(0); 289 FadeTransition ft = new FadeTransition(Duration.millis(500),symbol); 290 ft.setToValue(0); 291 ft.setOnFinished(actionEvent -> { 292 getPlotChildren().remove(symbol); 293 removeDataItemFromDisplay(series, item); 294 symbol.setOpacity(1.0); 295 }); 296 ft.play(); 297 } 298 if (animate) { 299 animate( new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(), 300 item.getCurrentY()), new KeyValue(item.currentXProperty(), 301 item.getCurrentX())), 302 new KeyFrame(Duration.millis(800), actionEvent -> { 303 getPlotChildren().remove(symbol); 304 removeDataItemFromDisplay(series, item); 305 }, 306 new KeyValue(item.currentYProperty(), 307 item.getYValue(), Interpolator.EASE_BOTH), 308 new KeyValue(item.currentXProperty(), 309 item.getXValue(), Interpolator.EASE_BOTH)) 310 ); 311 } 312 } else { 313 getPlotChildren().remove(symbol); 314 removeDataItemFromDisplay(series, item); 315 } 316 //Note: better animation here, point should move from old position to new position at center point between prev and next symbols 317 } 318 319 /** @inheritDoc */ 320 @Override protected void dataItemChanged(Data<X, Y> item) { 321 } 322 323 @Override protected void seriesChanged(ListChangeListener.Change<? extends Series> c) { 324 // Update style classes for all series lines and symbols 325 for (int i = 0; i < getDataSize(); i++) { 326 final Series<X,Y> s = getData().get(i); 327 Path seriesLine = (Path)((Group)s.getNode()).getChildren().get(1); 328 Path fillPath = (Path)((Group)s.getNode()).getChildren().get(0); 329 seriesLine.getStyleClass().setAll("chart-series-area-line", "series" + i, s.defaultColorStyleClass); 330 fillPath.getStyleClass().setAll("chart-series-area-fill", "series" + i, s.defaultColorStyleClass); 331 for (int j=0; j < s.getData().size(); j++) { 332 final Data<X,Y> item = s.getData().get(j); 333 final Node node = item.getNode(); 334 if(node!=null) node.getStyleClass().setAll("chart-area-symbol", "series" + i, "data" + j, s.defaultColorStyleClass); 335 } 336 } 337 } 338 339 @Override protected void seriesAdded(Series<X,Y> series, int seriesIndex) { 340 // create new paths for series 341 Path seriesLine = new Path(); 342 Path fillPath = new Path(); 343 seriesLine.setStrokeLineJoin(StrokeLineJoin.BEVEL); 344 fillPath.setStrokeLineJoin(StrokeLineJoin.BEVEL); 345 Group areaGroup = new Group(fillPath,seriesLine); 346 series.setNode(areaGroup); 347 // create series Y multiplier 348 DoubleProperty seriesYAnimMultiplier = new SimpleDoubleProperty(this, "seriesYMultiplier"); 349 seriesYMultiplierMap.put(series, seriesYAnimMultiplier); 350 // handle any data already in series 351 if (shouldAnimate()) { 352 seriesYAnimMultiplier.setValue(0d); 353 } else { 354 seriesYAnimMultiplier.setValue(1d); 355 } 356 getPlotChildren().add(areaGroup); 357 List<KeyFrame> keyFrames = new ArrayList<KeyFrame>(); 358 if (shouldAnimate()) { 359 // animate in new series 360 keyFrames.add(new KeyFrame(Duration.ZERO, 361 new KeyValue(areaGroup.opacityProperty(), 0), 362 new KeyValue(seriesYAnimMultiplier, 0) 363 )); 364 keyFrames.add(new KeyFrame(Duration.millis(200), 365 new KeyValue(areaGroup.opacityProperty(), 1) 366 )); 367 keyFrames.add(new KeyFrame(Duration.millis(500), 368 new KeyValue(seriesYAnimMultiplier, 1) 369 )); 370 } 371 for (int j=0; j<series.getData().size(); j++) { 372 Data<X,Y> item = series.getData().get(j); 373 final Node symbol = createSymbol(series, seriesIndex, item, j); 374 if (symbol != null) { 375 if (shouldAnimate()) symbol.setOpacity(0); 376 getPlotChildren().add(symbol); 377 if (shouldAnimate()) { 378 // fade in new symbol 379 keyFrames.add(new KeyFrame(Duration.ZERO, new KeyValue(symbol.opacityProperty(), 0))); 380 keyFrames.add(new KeyFrame(Duration.millis(200), new KeyValue(symbol.opacityProperty(), 1))); 381 } 382 } 383 } 384 if (shouldAnimate()) animate(keyFrames.toArray(new KeyFrame[keyFrames.size()])); 385 } 386 387 @Override protected void seriesRemoved(final Series<X,Y> series) { 388 // remove series Y multiplier 389 seriesYMultiplierMap.remove(series); 390 // remove all symbol nodes 391 if (shouldAnimate()) { 392 // create list of all nodes we need to fade out 393 final List<Node> nodes = new ArrayList<Node>(); 394 nodes.add(series.getNode()); 395 for (Data<X,Y> d: series.getData()) nodes.add(d.getNode()); 396 // fade out old and symbols 397 if (getCreateSymbols()) { 398 KeyValue[] startValues = new KeyValue[nodes.size()]; 399 KeyValue[] endValues = new KeyValue[nodes.size()]; 400 for (int j=0; j < nodes.size(); j++) { 401 startValues[j] = new KeyValue(nodes.get(j).opacityProperty(),1); 402 endValues[j] = new KeyValue(nodes.get(j).opacityProperty(),0); 403 } 404 Timeline tl = new Timeline(); 405 tl.getKeyFrames().addAll( 406 new KeyFrame(Duration.ZERO,startValues), 407 new KeyFrame(Duration.millis(400), actionEvent -> { 408 getPlotChildren().removeAll(nodes); 409 removeSeriesFromDisplay(series); 410 },endValues) 411 ); 412 tl.play(); 413 } else { 414 Timeline tl = new Timeline(); 415 tl.getKeyFrames().addAll( 416 new KeyFrame(Duration.millis(400), actionEvent -> { 417 getPlotChildren().removeAll(nodes); 418 removeSeriesFromDisplay(series); 419 }) 420 ); 421 tl.play(); 422 } 423 } else { 424 getPlotChildren().remove(series.getNode()); 425 for (Data<X,Y> d:series.getData()) getPlotChildren().remove(d.getNode()); 426 removeSeriesFromDisplay(series); 427 } 428 } 429 430 /** @inheritDoc */ 431 @Override protected void updateAxisRange() { 432 // This override is necessary to update axis range based on cumulative Y value for the 433 // Y axis instead of the normal way where max value in the data range is used. 434 final Axis<X> xa = getXAxis(); 435 final Axis<Y> ya = getYAxis(); 436 if (xa.isAutoRanging()) { 437 List xData = new ArrayList<Number>(); 438 for(Series<X,Y> series : getData()) { 439 for(Data<X,Y> data: series.getData()) { 440 xData.add(data.getXValue()); 441 } 442 } 443 xa.invalidateRange(xData); 444 } 445 if (ya.isAutoRanging()) { 446 double totalMinY = Double.MAX_VALUE; 447 Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator(); 448 boolean first = true; 449 NavigableMap<Double, Double> accum = new TreeMap<>(); 450 NavigableMap<Double, Double> prevAccum = new TreeMap<>(); 451 NavigableMap<Double, Double> currentValues = new TreeMap<>(); 452 while (seriesIterator.hasNext()) { 453 currentValues.clear(); 454 Series<X, Y> series = seriesIterator.next(); 455 for(Data<X,Y> item : series.getData()) { 456 if(item != null) { 457 final double xv = xa.toNumericValue(item.getXValue()); 458 final double yv = ya.toNumericValue(item.getYValue()); 459 currentValues.put(xv, yv); 460 if (first) { 461 // On the first pass, just fill the map 462 accum.put(xv, yv); 463 // minimum is applicable only in the first series 464 totalMinY = Math.min(totalMinY, yv); 465 } else { 466 if (prevAccum.containsKey(xv)) { 467 accum.put(xv, prevAccum.get(xv) + yv); 468 } else { 469 // If the point wasn't yet in the previous (accumulated) series 470 Map.Entry<Double, Double> he = prevAccum.higherEntry(xv); 471 Map.Entry<Double, Double> le = prevAccum.lowerEntry(xv); 472 if (he != null && le != null) { 473 // If there's both point above and below this point, interpolate 474 accum.put(xv, ((xv - le.getKey()) / (he.getKey() - le.getKey())) * 475 (le.getValue() + he.getValue()) + yv); 476 } else if (he != null) { 477 // The point is before the first point in the previously accumulated series 478 accum.put(xv, he.getValue() + yv); 479 } else if (le != null) { 480 // The point is after the last point in the previously accumulated series 481 accum.put(xv, le.getValue() + yv); 482 } else { 483 // The previously accumulated series is empty 484 accum.put(xv, yv); 485 } 486 } 487 } 488 } 489 } 490 // Now update all the keys that were in the previous series, but not in the new one 491 for (Map.Entry<Double, Double> e : prevAccum.entrySet()) { 492 if (accum.keySet().contains(e.getKey())) { 493 continue; 494 } 495 Double k = e.getKey(); 496 final Double v = e.getValue(); 497 // Look at the values of the current series 498 Map.Entry<Double, Double> he = currentValues.higherEntry(k); 499 Map.Entry<Double, Double> le = currentValues.lowerEntry(k); 500 if (he != null && le != null) { 501 // Interpolate the for the point from current series and add the accumulated value 502 accum.put(k, ((k - le.getKey()) / (he.getKey() - le.getKey())) * 503 (le.getValue() + he.getValue()) + v); 504 } else if (he != null) { 505 // There accumulated value is before the first value in the current series 506 accum.put(k, he.getValue() + v); 507 } else if (le != null) { 508 // There accumulated value is after the last value in the current series 509 accum.put(k, le.getValue() + v); 510 } else { 511 // The current series are empty 512 accum.put(k, v); 513 } 514 515 } 516 517 prevAccum.clear(); 518 prevAccum.putAll(accum); 519 accum.clear(); 520 first = (totalMinY == Double.MAX_VALUE); // If there was already some value in the series, we can consider as 521 // being past the first series 522 523 } 524 if(totalMinY != Double.MAX_VALUE) ya.invalidateRange(Arrays.asList(ya.toRealValue(totalMinY), 525 ya.toRealValue(Collections.max(prevAccum.values())))); 526 527 } 528 } 529 530 531 /** @inheritDoc */ 532 @Override protected void layoutPlotChildren() { 533 ArrayList<DataPointInfo<X, Y>> currentSeriesData = new ArrayList<>(); 534 // AggregateData hold the data points of both the current and the previous series. 535 // The goal is to collect all the data, sort it and iterate. 536 ArrayList<DataPointInfo<X, Y>> aggregateData = new ArrayList<>(); 537 for (int seriesIndex=0; seriesIndex < getDataSize(); seriesIndex++) { // for every series 538 Series<X, Y> series = getData().get(seriesIndex); 539 aggregateData.clear(); 540 // copy currentSeriesData accumulated in the previous iteration to aggregate. 541 for(DataPointInfo<X, Y> data : currentSeriesData) { 542 data.partOf = PartOf.PREVIOUS; 543 aggregateData.add(data); 544 } 545 currentSeriesData.clear(); 546 // now copy actual data of the current series. 547 for (Iterator<Data<X, Y>> it = getDisplayedDataIterator(series); it.hasNext(); ) { 548 Data<X, Y> item = it.next(); 549 DataPointInfo<X, Y> itemInfo = new DataPointInfo<>(item, item.getXValue(), 550 item.getYValue(), PartOf.CURRENT); 551 aggregateData.add(itemInfo); 552 } 553 DoubleProperty seriesYAnimMultiplier = seriesYMultiplierMap.get(series); 554 Path seriesLine = (Path)((Group)series.getNode()).getChildren().get(1); 555 Path fillPath = (Path)((Group)series.getNode()).getChildren().get(0); 556 seriesLine.getElements().clear(); 557 fillPath.getElements().clear(); 558 int dataIndex = 0; 559 // Sort data points from prev and current series 560 sortAggregateList(aggregateData); 561 562 Axis<Y> yAxis = getYAxis(); 563 Axis<X> xAxis = getXAxis(); 564 boolean firstCurrent = false; 565 boolean lastCurrent = false; 566 int firstCurrentIndex = findNextCurrent(aggregateData, -1); 567 int lastCurrentIndex = findPreviousCurrent(aggregateData, aggregateData.size()); 568 double basePosition = yAxis.getZeroPosition(); 569 if (Double.isNaN(basePosition)) { 570 ValueAxis<Number> valueYAxis = (ValueAxis<Number>) yAxis; 571 if (valueYAxis.getLowerBound() > 0) { 572 basePosition = valueYAxis.getDisplayPosition(valueYAxis.getLowerBound()); 573 } else { 574 basePosition = valueYAxis.getDisplayPosition(valueYAxis.getUpperBound()); 575 } 576 } 577 // Iterate over the aggregate data : this process accumulates data points 578 // cumulatively from the bottom to top of stack 579 580 for (DataPointInfo<X, Y> dataInfo : aggregateData) { 581 if (dataIndex == lastCurrentIndex) lastCurrent = true; 582 if (dataIndex == firstCurrentIndex) firstCurrent = true; 583 final Data<X,Y> item = dataInfo.dataItem; 584 if (dataInfo.partOf.equals(PartOf.CURRENT)) { // handle data from current series 585 int pIndex = findPreviousPrevious(aggregateData, dataIndex); 586 int nIndex = findNextPrevious(aggregateData, dataIndex); 587 DataPointInfo<X, Y> prevPoint; 588 DataPointInfo<X, Y> nextPoint; 589 if (pIndex == -1 || (nIndex == -1 && !(aggregateData.get(pIndex).x.equals(dataInfo.x)))) { 590 if (firstCurrent) { 591 // Need to add the drop down point. 592 Data<X, Y> ddItem = new Data(dataInfo.x, 0); 593 addDropDown(currentSeriesData, ddItem, ddItem.getXValue(), ddItem.getYValue(), 594 xAxis.getDisplayPosition(ddItem.getCurrentX()), basePosition); 595 } 596 double x = xAxis.getDisplayPosition(item.getCurrentX()); 597 double y = yAxis.getDisplayPosition( 598 yAxis.toRealValue(yAxis.toNumericValue(item.getCurrentY()) * seriesYAnimMultiplier.getValue())); 599 addPoint(currentSeriesData, item, item.getXValue(), item.getYValue(), x, y, 600 PartOf.CURRENT, false, (firstCurrent) ? false : true); 601 if (dataIndex == lastCurrentIndex) { 602 // need to add drop down point 603 Data<X, Y> ddItem = new Data(dataInfo.x, 0); 604 addDropDown(currentSeriesData, ddItem, ddItem.getXValue(), ddItem.getYValue(), 605 xAxis.getDisplayPosition(ddItem.getCurrentX()), basePosition); 606 } 607 } else { 608 prevPoint = aggregateData.get(pIndex); 609 if (prevPoint.x.equals(dataInfo.x)) { // Need to add Y values 610 // Check if prevPoint is a dropdown - as the stable sort preserves the order. 611 // If so, find the non dropdown previous point on previous series. 612 if (prevPoint.dropDown) { 613 pIndex = findPreviousPrevious(aggregateData, pIndex); 614 prevPoint = aggregateData.get(pIndex); 615 // If lastCurrent - add this drop down 616 } 617 if (prevPoint.x.equals(dataInfo.x)) { // simply add 618 double x = xAxis.getDisplayPosition(item.getCurrentX()); 619 final double yv = yAxis.toNumericValue(item.getCurrentY()) + yAxis.toNumericValue(prevPoint.y); 620 double y = yAxis.getDisplayPosition( 621 yAxis.toRealValue(yv * seriesYAnimMultiplier.getValue())); 622 addPoint(currentSeriesData, item, dataInfo.x, yAxis.toRealValue(yv), x, y, PartOf.CURRENT, false, 623 (firstCurrent) ? false : true); 624 } 625 if (lastCurrent) { 626 addDropDown(currentSeriesData, item, prevPoint.x, prevPoint.y, prevPoint.displayX, prevPoint.displayY); 627 } 628 } else { 629 // interpolate 630 nextPoint = (nIndex == -1) ? null : aggregateData.get(nIndex); 631 prevPoint = (pIndex == -1) ? null : aggregateData.get(pIndex); 632 final double yValue = yAxis.toNumericValue(item.getCurrentY()); 633 if (prevPoint != null && nextPoint != null) { 634 double x = xAxis.getDisplayPosition(item.getCurrentX()); 635 double displayY = interpolate(prevPoint.displayX, 636 prevPoint.displayY, nextPoint.displayX, nextPoint.displayY, x); 637 double dataY = interpolate(xAxis.toNumericValue(prevPoint.x), 638 yAxis.toNumericValue(prevPoint.y), 639 xAxis.toNumericValue(nextPoint.x), 640 yAxis.toNumericValue(nextPoint.y), 641 xAxis.toNumericValue(dataInfo.x)); 642 if (firstCurrent) { 643 // now create the drop down point 644 Data<X, Y> ddItem = new Data(dataInfo.x, dataY); 645 addDropDown(currentSeriesData, ddItem, dataInfo.x, yAxis.toRealValue(dataY), x, displayY); 646 } 647 double y = yAxis.getDisplayPosition(yAxis.toRealValue((yValue + dataY) * seriesYAnimMultiplier.getValue())); 648 // Add the current point 649 addPoint(currentSeriesData, item, dataInfo.x, yAxis.toRealValue(yValue + dataY), x, y, PartOf.CURRENT, false, 650 (firstCurrent) ? false : true); 651 if (dataIndex == lastCurrentIndex) { 652 // add drop down point 653 Data<X, Y> ddItem = new Data(dataInfo.x, dataY); 654 addDropDown(currentSeriesData, ddItem, dataInfo.x, yAxis.toRealValue(dataY), x, displayY); 655 } 656 // Note: add drop down if last current 657 } 658 else { 659 // we do not need to take care of this as it is 660 // already handled above with check of if(pIndex == -1 or nIndex == -1) 661 } 662 } 663 } 664 665 } else { // handle data from Previous series. 666 int pIndex = findPreviousCurrent(aggregateData, dataIndex); 667 int nIndex = findNextCurrent(aggregateData, dataIndex); 668 DataPointInfo<X, Y> prevPoint; 669 DataPointInfo<X, Y> nextPoint; 670 if (dataInfo.dropDown) { 671 if (xAxis.toNumericValue(dataInfo.x) <= 672 xAxis.toNumericValue(aggregateData.get(firstCurrentIndex).x) || 673 xAxis.toNumericValue(dataInfo.x) > xAxis.toNumericValue(aggregateData.get(lastCurrentIndex).x)) { 674 addDropDown(currentSeriesData, item, dataInfo.x, dataInfo.y, dataInfo.displayX, dataInfo.displayY); 675 } 676 } else { 677 if (pIndex == -1 || nIndex == -1) { 678 addPoint(currentSeriesData, item, dataInfo.x, dataInfo.y, dataInfo.displayX, dataInfo.displayY, 679 PartOf.CURRENT, true, false); 680 } else { 681 nextPoint = aggregateData.get(nIndex); 682 if (nextPoint.x.equals(dataInfo.x)) { 683 // do nothing as the current point is already there. 684 } else { 685 // interpolate on the current series. 686 prevPoint = aggregateData.get(pIndex); 687 double x = xAxis.getDisplayPosition(item.getCurrentX()); 688 double dataY = interpolate(xAxis.toNumericValue(prevPoint.x), 689 yAxis.toNumericValue(prevPoint.y), 690 xAxis.toNumericValue(nextPoint.x), 691 yAxis.toNumericValue(nextPoint.y), 692 xAxis.toNumericValue(dataInfo.x)); 693 final double yv = yAxis.toNumericValue(dataInfo.y) + dataY; 694 double y = yAxis.getDisplayPosition( 695 yAxis.toRealValue(yv * seriesYAnimMultiplier.getValue())); 696 addPoint(currentSeriesData, new Data(dataInfo.x, dataY), dataInfo.x, yAxis.toRealValue(yv), x, y, PartOf.CURRENT, true, true); 697 } 698 } 699 } 700 } 701 dataIndex++; 702 if (firstCurrent) firstCurrent = false; 703 if (lastCurrent) lastCurrent = false; 704 } // end of inner for loop 705 706 // Draw the SeriesLine and Series fill 707 if (!currentSeriesData.isEmpty()) { 708 seriesLine.getElements().add(new MoveTo(currentSeriesData.get(0).displayX, currentSeriesData.get(0).displayY)); 709 fillPath.getElements().add(new MoveTo(currentSeriesData.get(0).displayX, currentSeriesData.get(0).displayY)); 710 } 711 for (DataPointInfo<X, Y> point : currentSeriesData) { 712 if (point.lineTo) { 713 seriesLine.getElements().add(new LineTo(point.displayX, point.displayY)); 714 } else { 715 seriesLine.getElements().add(new MoveTo(point.displayX, point.displayY)); 716 } 717 fillPath.getElements().add(new LineTo(point.displayX, point.displayY)); 718 // draw symbols only for actual data points and skip for interpolated points. 719 if (!point.skipSymbol) { 720 Node symbol = point.dataItem.getNode(); 721 if (symbol != null) { 722 final double w = symbol.prefWidth(-1); 723 final double h = symbol.prefHeight(-1); 724 symbol.resizeRelocate(point.displayX-(w/2), point.displayY-(h/2),w,h); 725 } 726 } 727 } 728 for(int i = aggregateData.size()-1; i > 0; i--) { 729 DataPointInfo<X, Y> point = aggregateData.get(i); 730 if (PartOf.PREVIOUS.equals(point.partOf)) { 731 fillPath.getElements().add(new LineTo(point.displayX, point.displayY)); 732 } 733 } 734 if (!fillPath.getElements().isEmpty()) { 735 fillPath.getElements().add(new ClosePath()); 736 } 737 738 } // end of out for loop 739 } 740 741 private void addDropDown(ArrayList<DataPointInfo<X, Y>> currentSeriesData, Data<X, Y> item, X xValue, Y yValue, double x, double y) { 742 DataPointInfo<X, Y> dropDownDataPoint = new DataPointInfo<>(true); 743 dropDownDataPoint.setValues(item, xValue, yValue, x, y, PartOf.CURRENT, true, false); 744 currentSeriesData.add(dropDownDataPoint); 745 } 746 747 private void addPoint(ArrayList<DataPointInfo<X, Y>> currentSeriesData, Data<X, Y> item, X xValue, Y yValue, double x, double y, PartOf partof, 748 boolean symbol, boolean lineTo) { 749 DataPointInfo<X, Y> currentDataPoint = new DataPointInfo<>(); 750 currentDataPoint.setValues(item, xValue, yValue, x, y, partof, symbol, lineTo); 751 currentSeriesData.add(currentDataPoint); 752 } 753 754 //-------------------- helper methods to retrieve data points from the previous 755 // or current data series. 756 private int findNextCurrent(ArrayList<DataPointInfo<X, Y>> points, int index) { 757 for(int i = index+1; i < points.size(); i++) { 758 if (points.get(i).partOf.equals(PartOf.CURRENT)) { 759 return i; 760 } 761 } 762 return -1; 763 } 764 765 private int findPreviousCurrent(ArrayList<DataPointInfo<X, Y>> points, int index) { 766 for(int i = index-1; i >= 0; i--) { 767 if (points.get(i).partOf.equals(PartOf.CURRENT)) { 768 return i; 769 } 770 } 771 return -1; 772 } 773 774 775 private int findPreviousPrevious(ArrayList<DataPointInfo<X, Y>> points, int index) { 776 for(int i = index-1; i >= 0; i--) { 777 if (points.get(i).partOf.equals(PartOf.PREVIOUS)) { 778 return i; 779 } 780 } 781 return -1; 782 } 783 private int findNextPrevious(ArrayList<DataPointInfo<X, Y>> points, int index) { 784 for(int i = index+1; i < points.size(); i++) { 785 if (points.get(i).partOf.equals(PartOf.PREVIOUS)) { 786 return i; 787 } 788 } 789 return -1; 790 } 791 792 793 private void sortAggregateList(ArrayList<DataPointInfo<X, Y>> aggregateList) { 794 Collections.sort(aggregateList, (o1, o2) -> { 795 Data<X,Y> d1 = o1.dataItem; 796 Data<X,Y> d2 = o2.dataItem; 797 double val1 = getXAxis().toNumericValue(d1.getXValue()); 798 double val2 = getXAxis().toNumericValue(d2.getXValue()); 799 return (val1 < val2 ? -1 : ( val1 == val2) ? 0 : 1); 800 }); 801 } 802 803 private double interpolate(double lowX, double lowY, double highX, double highY, double x) { 804 // using y = mx+c find the y for the given x. 805 return (((highY - lowY)/(highX - lowX))*(x - lowX))+lowY; 806 } 807 808 private Node createSymbol(Series<X,Y> series, int seriesIndex, final Data<X,Y> item, int itemIndex) { 809 Node symbol = item.getNode(); 810 // check if symbol has already been created 811 if (symbol == null && getCreateSymbols()) { 812 symbol = new StackPane(); 813 symbol.setAccessibleRole(AccessibleRole.TEXT); 814 symbol.setAccessibleRoleDescription("Point"); 815 symbol.focusTraversableProperty().bind(Platform.accessibilityActiveProperty()); 816 item.setNode(symbol); 817 } 818 // set symbol styles 819 // Note not sure if we want to add or check, ie be more careful and efficient here 820 if (symbol != null) symbol.getStyleClass().setAll("chart-area-symbol", "series" + seriesIndex, "data" + itemIndex, 821 series.defaultColorStyleClass); 822 return symbol; 823 } 824 825 /** 826 * This is called whenever a series is added or removed and the legend needs to be updated 827 */ 828 @Override protected void updateLegend() { 829 legend.getItems().clear(); 830 if (getData() != null) { 831 for (int seriesIndex=0; seriesIndex < getData().size(); seriesIndex++) { 832 Series<X,Y> series = getData().get(seriesIndex); 833 LegendItem legenditem = new LegendItem(series.getName()); 834 legenditem.getSymbol().getStyleClass().addAll("chart-area-symbol","series"+seriesIndex, 835 "area-legend-symbol", series.defaultColorStyleClass); 836 legend.getItems().add(legenditem); 837 } 838 } 839 if (legend.getItems().size() > 0) { 840 if (getLegend() == null) { 841 setLegend(legend); 842 } 843 } else { 844 setLegend(null); 845 } 846 } 847 848 // -------------- INNER CLASSES -------------------------------------------- 849 /* 850 * Helper class to hold data and display and other information for each 851 * data point 852 */ 853 final static class DataPointInfo<X, Y> { 854 X x; 855 Y y; 856 double displayX; 857 double displayY; 858 Data<X,Y> dataItem; 859 PartOf partOf; 860 boolean skipSymbol = false; // interpolated point - skip drawing symbol 861 boolean lineTo = false; // should there be a lineTo to this point on SeriesLine. 862 boolean dropDown = false; // Is this a drop down point ( non data point). 863 864 //----- Constructors -------------------- 865 DataPointInfo() {} 866 867 DataPointInfo(Data<X,Y> item, X x, Y y, PartOf partOf) { 868 this.dataItem = item; 869 this.x = x; 870 this.y = y; 871 this.partOf = partOf; 872 } 873 874 DataPointInfo(boolean dropDown) { 875 this.dropDown = dropDown; 876 } 877 878 void setValues(Data<X,Y> item, X x, Y y, double dx, double dy, 879 PartOf partOf, boolean skipSymbol, boolean lineTo) { 880 this.dataItem = item; 881 this.x = x; 882 this.y = y; 883 this.displayX = dx; 884 this.displayY = dy; 885 this.partOf = partOf; 886 this.skipSymbol = skipSymbol; 887 this.lineTo = lineTo; 888 } 889 890 public final X getX() { 891 return x; 892 } 893 894 public final Y getY() { 895 return y; 896 } 897 } 898 899 // To indicate if the data point belongs to the current or the previous series. 900 private static enum PartOf { 901 CURRENT, 902 PREVIOUS 903 } 904 905 // -------------- STYLESHEET HANDLING -------------------------------------- 906 907 private static class StyleableProperties { 908 909 private static final CssMetaData<StackedAreaChart<?, ?>, Boolean> CREATE_SYMBOLS = 910 new CssMetaData<StackedAreaChart<?, ?>, Boolean>("-fx-create-symbols", 911 BooleanConverter.getInstance(), Boolean.TRUE) { 912 @Override 913 public boolean isSettable(StackedAreaChart<?,?> node) { 914 return node.createSymbols == null || !node.createSymbols.isBound(); 915 } 916 917 @Override 918 public StyleableProperty<Boolean> getStyleableProperty(StackedAreaChart<?,?> node) { 919 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.createSymbolsProperty(); 920 } 921 }; 922 923 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 924 925 static { 926 final List<CssMetaData<? extends Styleable, ?>> styleables = 927 new ArrayList<CssMetaData<? extends Styleable, ?>>(XYChart.getClassCssMetaData()); 928 styleables.add(CREATE_SYMBOLS); 929 STYLEABLES = Collections.unmodifiableList(styleables); 930 931 } 932 } 933 934 /** 935 * @return The CssMetaData associated with this class, which may include the 936 * CssMetaData of its super classes. 937 * @since JavaFX 8.0 938 */ 939 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 940 return StyleableProperties.STYLEABLES; 941 } 942 943 /** 944 * {@inheritDoc} 945 * @since JavaFX 8.0 946 */ 947 @Override 948 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 949 return getClassCssMetaData(); 950 } 951 952 }