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 javafx.css.converter.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 // dataSize represents size of currently visible data. After this operation, the number will decrement by 1 248 final int dataSize = series.getDataSize(); 249 // This is the size of current data list in Series. Note that it might be totaly different from dataSize as 250 // some big operation might have happened on the list. 251 final int dataListSize = series.getData().size(); 252 if (itemIndex > 0 && itemIndex < dataSize - 1) { 253 animate = true; 254 Data<X,Y> p1 = series.getItem(itemIndex - 1); 255 Data<X,Y> p2 = series.getItem(itemIndex + 1); 256 double x1 = getXAxis().toNumericValue(p1.getXValue()); 257 double y1 = getYAxis().toNumericValue(p1.getYValue()); 258 double x3 = getXAxis().toNumericValue(p2.getXValue()); 259 double y3 = getYAxis().toNumericValue(p2.getYValue()); 260 261 double x2 = getXAxis().toNumericValue(item.getXValue()); 262 double y2 = getYAxis().toNumericValue(item.getYValue()); 263 264 // //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1) 265 double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1); 266 item.setCurrentX(getXAxis().toRealValue(x2)); 267 item.setCurrentY(getYAxis().toRealValue(y2)); 268 item.setXValue(getXAxis().toRealValue(x2)); 269 item.setYValue(getYAxis().toRealValue(y)); 270 //2. we can simply use the midpoint on the line as well.. 271 // double x = (x3 + x1)/2; 272 // double y = (y3 + y1)/2; 273 // item.setCurrentX(x); 274 // item.setCurrentY(y); 275 } else if (itemIndex == 0 && dataListSize > 1) { 276 animate = true; 277 item.setXValue(series.getData().get(0).getXValue()); 278 item.setYValue(series.getData().get(0).getYValue()); 279 } else if (itemIndex == (dataSize - 1) && dataListSize > 1) { 280 animate = true; 281 int last = dataListSize - 1; 282 item.setXValue(series.getData().get(last).getXValue()); 283 item.setYValue(series.getData().get(last).getYValue()); 284 } else if (symbol != null) { 285 // fade out symbol 286 symbol.setOpacity(0); 287 FadeTransition ft = new FadeTransition(Duration.millis(500),symbol); 288 ft.setToValue(0); 289 ft.setOnFinished(actionEvent -> { 290 getPlotChildren().remove(symbol); 291 removeDataItemFromDisplay(series, item); 292 symbol.setOpacity(1.0); 293 }); 294 ft.play(); 295 } 296 if (animate) { 297 animate( new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(), 298 item.getCurrentY()), new KeyValue(item.currentXProperty(), 299 item.getCurrentX())), 300 new KeyFrame(Duration.millis(800), actionEvent -> { 301 getPlotChildren().remove(symbol); 302 removeDataItemFromDisplay(series, item); 303 }, 304 new KeyValue(item.currentYProperty(), 305 item.getYValue(), Interpolator.EASE_BOTH), 306 new KeyValue(item.currentXProperty(), 307 item.getXValue(), Interpolator.EASE_BOTH)) 308 ); 309 } 310 } else { 311 getPlotChildren().remove(symbol); 312 removeDataItemFromDisplay(series, item); 313 } 314 //Note: better animation here, point should move from old position to new position at center point between prev and next symbols 315 } 316 317 /** @inheritDoc */ 318 @Override protected void dataItemChanged(Data<X, Y> item) { 319 } 320 321 @Override protected void seriesChanged(ListChangeListener.Change<? extends Series> c) { 322 // Update style classes for all series lines and symbols 323 for (int i = 0; i < getDataSize(); i++) { 324 final Series<X,Y> s = getData().get(i); 325 Path seriesLine = (Path)((Group)s.getNode()).getChildren().get(1); 326 Path fillPath = (Path)((Group)s.getNode()).getChildren().get(0); 327 seriesLine.getStyleClass().setAll("chart-series-area-line", "series" + i, s.defaultColorStyleClass); 328 fillPath.getStyleClass().setAll("chart-series-area-fill", "series" + i, s.defaultColorStyleClass); 329 for (int j=0; j < s.getData().size(); j++) { 330 final Data<X,Y> item = s.getData().get(j); 331 final Node node = item.getNode(); 332 if(node!=null) node.getStyleClass().setAll("chart-area-symbol", "series" + i, "data" + j, s.defaultColorStyleClass); 333 } 334 } 335 } 336 337 @Override protected void seriesAdded(Series<X,Y> series, int seriesIndex) { 338 // create new paths for series 339 Path seriesLine = new Path(); 340 Path fillPath = new Path(); 341 seriesLine.setStrokeLineJoin(StrokeLineJoin.BEVEL); 342 fillPath.setStrokeLineJoin(StrokeLineJoin.BEVEL); 343 Group areaGroup = new Group(fillPath,seriesLine); 344 series.setNode(areaGroup); 345 // create series Y multiplier 346 DoubleProperty seriesYAnimMultiplier = new SimpleDoubleProperty(this, "seriesYMultiplier"); 347 seriesYMultiplierMap.put(series, seriesYAnimMultiplier); 348 // handle any data already in series 349 if (shouldAnimate()) { 350 seriesYAnimMultiplier.setValue(0d); 351 } else { 352 seriesYAnimMultiplier.setValue(1d); 353 } 354 getPlotChildren().add(areaGroup); 355 List<KeyFrame> keyFrames = new ArrayList<KeyFrame>(); 356 if (shouldAnimate()) { 357 // animate in new series 358 keyFrames.add(new KeyFrame(Duration.ZERO, 359 new KeyValue(areaGroup.opacityProperty(), 0), 360 new KeyValue(seriesYAnimMultiplier, 0) 361 )); 362 keyFrames.add(new KeyFrame(Duration.millis(200), 363 new KeyValue(areaGroup.opacityProperty(), 1) 364 )); 365 keyFrames.add(new KeyFrame(Duration.millis(500), 366 new KeyValue(seriesYAnimMultiplier, 1) 367 )); 368 } 369 for (int j=0; j<series.getData().size(); j++) { 370 Data<X,Y> item = series.getData().get(j); 371 final Node symbol = createSymbol(series, seriesIndex, item, j); 372 if (symbol != null) { 373 if (shouldAnimate()) symbol.setOpacity(0); 374 getPlotChildren().add(symbol); 375 if (shouldAnimate()) { 376 // fade in new symbol 377 keyFrames.add(new KeyFrame(Duration.ZERO, new KeyValue(symbol.opacityProperty(), 0))); 378 keyFrames.add(new KeyFrame(Duration.millis(200), new KeyValue(symbol.opacityProperty(), 1))); 379 } 380 } 381 } 382 if (shouldAnimate()) animate(keyFrames.toArray(new KeyFrame[keyFrames.size()])); 383 } 384 385 @Override protected void seriesRemoved(final Series<X,Y> series) { 386 // remove series Y multiplier 387 seriesYMultiplierMap.remove(series); 388 // remove all symbol nodes 389 if (shouldAnimate()) { 390 Timeline tl = new Timeline(createSeriesRemoveTimeLine(series, 400)); 391 tl.play(); 392 } else { 393 getPlotChildren().remove(series.getNode()); 394 for (Data<X,Y> d:series.getData()) getPlotChildren().remove(d.getNode()); 395 removeSeriesFromDisplay(series); 396 } 397 } 398 399 /** @inheritDoc */ 400 @Override protected void updateAxisRange() { 401 // This override is necessary to update axis range based on cumulative Y value for the 402 // Y axis instead of the normal way where max value in the data range is used. 403 final Axis<X> xa = getXAxis(); 404 final Axis<Y> ya = getYAxis(); 405 if (xa.isAutoRanging()) { 406 List xData = new ArrayList<Number>(); 407 for(Series<X,Y> series : getData()) { 408 for(Data<X,Y> data: series.getData()) { 409 xData.add(data.getXValue()); 410 } 411 } 412 xa.invalidateRange(xData); 413 } 414 if (ya.isAutoRanging()) { 415 double totalMinY = Double.MAX_VALUE; 416 Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator(); 417 boolean first = true; 418 NavigableMap<Double, Double> accum = new TreeMap<>(); 419 NavigableMap<Double, Double> prevAccum = new TreeMap<>(); 420 NavigableMap<Double, Double> currentValues = new TreeMap<>(); 421 while (seriesIterator.hasNext()) { 422 currentValues.clear(); 423 Series<X, Y> series = seriesIterator.next(); 424 for(Data<X,Y> item : series.getData()) { 425 if(item != null) { 426 final double xv = xa.toNumericValue(item.getXValue()); 427 final double yv = ya.toNumericValue(item.getYValue()); 428 currentValues.put(xv, yv); 429 if (first) { 430 // On the first pass, just fill the map 431 accum.put(xv, yv); 432 // minimum is applicable only in the first series 433 totalMinY = Math.min(totalMinY, yv); 434 } else { 435 if (prevAccum.containsKey(xv)) { 436 accum.put(xv, prevAccum.get(xv) + yv); 437 } else { 438 // If the point wasn't yet in the previous (accumulated) series 439 Map.Entry<Double, Double> he = prevAccum.higherEntry(xv); 440 Map.Entry<Double, Double> le = prevAccum.lowerEntry(xv); 441 if (he != null && le != null) { 442 // If there's both point above and below this point, interpolate 443 accum.put(xv, ((xv - le.getKey()) / (he.getKey() - le.getKey())) * 444 (le.getValue() + he.getValue()) + yv); 445 } else if (he != null) { 446 // The point is before the first point in the previously accumulated series 447 accum.put(xv, he.getValue() + yv); 448 } else if (le != null) { 449 // The point is after the last point in the previously accumulated series 450 accum.put(xv, le.getValue() + yv); 451 } else { 452 // The previously accumulated series is empty 453 accum.put(xv, yv); 454 } 455 } 456 } 457 } 458 } 459 // Now update all the keys that were in the previous series, but not in the new one 460 for (Map.Entry<Double, Double> e : prevAccum.entrySet()) { 461 if (accum.keySet().contains(e.getKey())) { 462 continue; 463 } 464 Double k = e.getKey(); 465 final Double v = e.getValue(); 466 // Look at the values of the current series 467 Map.Entry<Double, Double> he = currentValues.higherEntry(k); 468 Map.Entry<Double, Double> le = currentValues.lowerEntry(k); 469 if (he != null && le != null) { 470 // Interpolate the for the point from current series and add the accumulated value 471 accum.put(k, ((k - le.getKey()) / (he.getKey() - le.getKey())) * 472 (le.getValue() + he.getValue()) + v); 473 } else if (he != null) { 474 // There accumulated value is before the first value in the current series 475 accum.put(k, he.getValue() + v); 476 } else if (le != null) { 477 // There accumulated value is after the last value in the current series 478 accum.put(k, le.getValue() + v); 479 } else { 480 // The current series are empty 481 accum.put(k, v); 482 } 483 484 } 485 486 prevAccum.clear(); 487 prevAccum.putAll(accum); 488 accum.clear(); 489 first = (totalMinY == Double.MAX_VALUE); // If there was already some value in the series, we can consider as 490 // being past the first series 491 492 } 493 if(totalMinY != Double.MAX_VALUE) ya.invalidateRange(Arrays.asList(ya.toRealValue(totalMinY), 494 ya.toRealValue(Collections.max(prevAccum.values())))); 495 496 } 497 } 498 499 500 /** @inheritDoc */ 501 @Override protected void layoutPlotChildren() { 502 ArrayList<DataPointInfo<X, Y>> currentSeriesData = new ArrayList<>(); 503 // AggregateData hold the data points of both the current and the previous series. 504 // The goal is to collect all the data, sort it and iterate. 505 ArrayList<DataPointInfo<X, Y>> aggregateData = new ArrayList<>(); 506 for (int seriesIndex=0; seriesIndex < getDataSize(); seriesIndex++) { // for every series 507 Series<X, Y> series = getData().get(seriesIndex); 508 aggregateData.clear(); 509 // copy currentSeriesData accumulated in the previous iteration to aggregate. 510 for(DataPointInfo<X, Y> data : currentSeriesData) { 511 data.partOf = PartOf.PREVIOUS; 512 aggregateData.add(data); 513 } 514 currentSeriesData.clear(); 515 // now copy actual data of the current series. 516 for (Iterator<Data<X, Y>> it = getDisplayedDataIterator(series); it.hasNext(); ) { 517 Data<X, Y> item = it.next(); 518 DataPointInfo<X, Y> itemInfo = new DataPointInfo<>(item, item.getXValue(), 519 item.getYValue(), PartOf.CURRENT); 520 aggregateData.add(itemInfo); 521 } 522 DoubleProperty seriesYAnimMultiplier = seriesYMultiplierMap.get(series); 523 Path seriesLine = (Path)((Group)series.getNode()).getChildren().get(1); 524 Path fillPath = (Path)((Group)series.getNode()).getChildren().get(0); 525 seriesLine.getElements().clear(); 526 fillPath.getElements().clear(); 527 int dataIndex = 0; 528 // Sort data points from prev and current series 529 sortAggregateList(aggregateData); 530 531 Axis<Y> yAxis = getYAxis(); 532 Axis<X> xAxis = getXAxis(); 533 boolean firstCurrent = false; 534 boolean lastCurrent = false; 535 int firstCurrentIndex = findNextCurrent(aggregateData, -1); 536 int lastCurrentIndex = findPreviousCurrent(aggregateData, aggregateData.size()); 537 double basePosition = yAxis.getZeroPosition(); 538 if (Double.isNaN(basePosition)) { 539 ValueAxis<Number> valueYAxis = (ValueAxis<Number>) yAxis; 540 if (valueYAxis.getLowerBound() > 0) { 541 basePosition = valueYAxis.getDisplayPosition(valueYAxis.getLowerBound()); 542 } else { 543 basePosition = valueYAxis.getDisplayPosition(valueYAxis.getUpperBound()); 544 } 545 } 546 // Iterate over the aggregate data : this process accumulates data points 547 // cumulatively from the bottom to top of stack 548 549 for (DataPointInfo<X, Y> dataInfo : aggregateData) { 550 if (dataIndex == lastCurrentIndex) lastCurrent = true; 551 if (dataIndex == firstCurrentIndex) firstCurrent = true; 552 final Data<X,Y> item = dataInfo.dataItem; 553 if (dataInfo.partOf.equals(PartOf.CURRENT)) { // handle data from current series 554 int pIndex = findPreviousPrevious(aggregateData, dataIndex); 555 int nIndex = findNextPrevious(aggregateData, dataIndex); 556 DataPointInfo<X, Y> prevPoint; 557 DataPointInfo<X, Y> nextPoint; 558 if (pIndex == -1 || (nIndex == -1 && !(aggregateData.get(pIndex).x.equals(dataInfo.x)))) { 559 if (firstCurrent) { 560 // Need to add the drop down point. 561 Data<X, Y> ddItem = new Data(dataInfo.x, 0); 562 addDropDown(currentSeriesData, ddItem, ddItem.getXValue(), ddItem.getYValue(), 563 xAxis.getDisplayPosition(ddItem.getCurrentX()), basePosition); 564 } 565 double x = xAxis.getDisplayPosition(item.getCurrentX()); 566 double y = yAxis.getDisplayPosition( 567 yAxis.toRealValue(yAxis.toNumericValue(item.getCurrentY()) * seriesYAnimMultiplier.getValue())); 568 addPoint(currentSeriesData, item, item.getXValue(), item.getYValue(), x, y, 569 PartOf.CURRENT, false, (firstCurrent) ? false : true); 570 if (dataIndex == lastCurrentIndex) { 571 // need to add drop down point 572 Data<X, Y> ddItem = new Data(dataInfo.x, 0); 573 addDropDown(currentSeriesData, ddItem, ddItem.getXValue(), ddItem.getYValue(), 574 xAxis.getDisplayPosition(ddItem.getCurrentX()), basePosition); 575 } 576 } else { 577 prevPoint = aggregateData.get(pIndex); 578 if (prevPoint.x.equals(dataInfo.x)) { // Need to add Y values 579 // Check if prevPoint is a dropdown - as the stable sort preserves the order. 580 // If so, find the non dropdown previous point on previous series. 581 if (prevPoint.dropDown) { 582 pIndex = findPreviousPrevious(aggregateData, pIndex); 583 prevPoint = aggregateData.get(pIndex); 584 // If lastCurrent - add this drop down 585 } 586 if (prevPoint.x.equals(dataInfo.x)) { // simply add 587 double x = xAxis.getDisplayPosition(item.getCurrentX()); 588 final double yv = yAxis.toNumericValue(item.getCurrentY()) + yAxis.toNumericValue(prevPoint.y); 589 double y = yAxis.getDisplayPosition( 590 yAxis.toRealValue(yv * seriesYAnimMultiplier.getValue())); 591 addPoint(currentSeriesData, item, dataInfo.x, yAxis.toRealValue(yv), x, y, PartOf.CURRENT, false, 592 (firstCurrent) ? false : true); 593 } 594 if (lastCurrent) { 595 addDropDown(currentSeriesData, item, prevPoint.x, prevPoint.y, prevPoint.displayX, prevPoint.displayY); 596 } 597 } else { 598 // interpolate 599 nextPoint = (nIndex == -1) ? null : aggregateData.get(nIndex); 600 prevPoint = (pIndex == -1) ? null : aggregateData.get(pIndex); 601 final double yValue = yAxis.toNumericValue(item.getCurrentY()); 602 if (prevPoint != null && nextPoint != null) { 603 double x = xAxis.getDisplayPosition(item.getCurrentX()); 604 double displayY = interpolate(prevPoint.displayX, 605 prevPoint.displayY, nextPoint.displayX, nextPoint.displayY, x); 606 double dataY = interpolate(xAxis.toNumericValue(prevPoint.x), 607 yAxis.toNumericValue(prevPoint.y), 608 xAxis.toNumericValue(nextPoint.x), 609 yAxis.toNumericValue(nextPoint.y), 610 xAxis.toNumericValue(dataInfo.x)); 611 if (firstCurrent) { 612 // now create the drop down point 613 Data<X, Y> ddItem = new Data(dataInfo.x, dataY); 614 addDropDown(currentSeriesData, ddItem, dataInfo.x, yAxis.toRealValue(dataY), x, displayY); 615 } 616 double y = yAxis.getDisplayPosition(yAxis.toRealValue((yValue + dataY) * seriesYAnimMultiplier.getValue())); 617 // Add the current point 618 addPoint(currentSeriesData, item, dataInfo.x, yAxis.toRealValue(yValue + dataY), x, y, PartOf.CURRENT, false, 619 (firstCurrent) ? false : true); 620 if (dataIndex == lastCurrentIndex) { 621 // add drop down point 622 Data<X, Y> ddItem = new Data(dataInfo.x, dataY); 623 addDropDown(currentSeriesData, ddItem, dataInfo.x, yAxis.toRealValue(dataY), x, displayY); 624 } 625 // Note: add drop down if last current 626 } 627 else { 628 // we do not need to take care of this as it is 629 // already handled above with check of if(pIndex == -1 or nIndex == -1) 630 } 631 } 632 } 633 634 } else { // handle data from Previous series. 635 int pIndex = findPreviousCurrent(aggregateData, dataIndex); 636 int nIndex = findNextCurrent(aggregateData, dataIndex); 637 DataPointInfo<X, Y> prevPoint; 638 DataPointInfo<X, Y> nextPoint; 639 if (dataInfo.dropDown) { 640 if (xAxis.toNumericValue(dataInfo.x) <= 641 xAxis.toNumericValue(aggregateData.get(firstCurrentIndex).x) || 642 xAxis.toNumericValue(dataInfo.x) > xAxis.toNumericValue(aggregateData.get(lastCurrentIndex).x)) { 643 addDropDown(currentSeriesData, item, dataInfo.x, dataInfo.y, dataInfo.displayX, dataInfo.displayY); 644 } 645 } else { 646 if (pIndex == -1 || nIndex == -1) { 647 addPoint(currentSeriesData, item, dataInfo.x, dataInfo.y, dataInfo.displayX, dataInfo.displayY, 648 PartOf.CURRENT, true, false); 649 } else { 650 nextPoint = aggregateData.get(nIndex); 651 if (nextPoint.x.equals(dataInfo.x)) { 652 // do nothing as the current point is already there. 653 } else { 654 // interpolate on the current series. 655 prevPoint = aggregateData.get(pIndex); 656 double x = xAxis.getDisplayPosition(item.getCurrentX()); 657 double dataY = interpolate(xAxis.toNumericValue(prevPoint.x), 658 yAxis.toNumericValue(prevPoint.y), 659 xAxis.toNumericValue(nextPoint.x), 660 yAxis.toNumericValue(nextPoint.y), 661 xAxis.toNumericValue(dataInfo.x)); 662 final double yv = yAxis.toNumericValue(dataInfo.y) + dataY; 663 double y = yAxis.getDisplayPosition( 664 yAxis.toRealValue(yv * seriesYAnimMultiplier.getValue())); 665 addPoint(currentSeriesData, new Data(dataInfo.x, dataY), dataInfo.x, yAxis.toRealValue(yv), x, y, PartOf.CURRENT, true, true); 666 } 667 } 668 } 669 } 670 dataIndex++; 671 if (firstCurrent) firstCurrent = false; 672 if (lastCurrent) lastCurrent = false; 673 } // end of inner for loop 674 675 // Draw the SeriesLine and Series fill 676 if (!currentSeriesData.isEmpty()) { 677 seriesLine.getElements().add(new MoveTo(currentSeriesData.get(0).displayX, currentSeriesData.get(0).displayY)); 678 fillPath.getElements().add(new MoveTo(currentSeriesData.get(0).displayX, currentSeriesData.get(0).displayY)); 679 } 680 for (DataPointInfo<X, Y> point : currentSeriesData) { 681 if (point.lineTo) { 682 seriesLine.getElements().add(new LineTo(point.displayX, point.displayY)); 683 } else { 684 seriesLine.getElements().add(new MoveTo(point.displayX, point.displayY)); 685 } 686 fillPath.getElements().add(new LineTo(point.displayX, point.displayY)); 687 // draw symbols only for actual data points and skip for interpolated points. 688 if (!point.skipSymbol) { 689 Node symbol = point.dataItem.getNode(); 690 if (symbol != null) { 691 final double w = symbol.prefWidth(-1); 692 final double h = symbol.prefHeight(-1); 693 symbol.resizeRelocate(point.displayX-(w/2), point.displayY-(h/2),w,h); 694 } 695 } 696 } 697 for(int i = aggregateData.size()-1; i > 0; i--) { 698 DataPointInfo<X, Y> point = aggregateData.get(i); 699 if (PartOf.PREVIOUS.equals(point.partOf)) { 700 fillPath.getElements().add(new LineTo(point.displayX, point.displayY)); 701 } 702 } 703 if (!fillPath.getElements().isEmpty()) { 704 fillPath.getElements().add(new ClosePath()); 705 } 706 707 } // end of out for loop 708 } 709 710 private void addDropDown(ArrayList<DataPointInfo<X, Y>> currentSeriesData, Data<X, Y> item, X xValue, Y yValue, double x, double y) { 711 DataPointInfo<X, Y> dropDownDataPoint = new DataPointInfo<>(true); 712 dropDownDataPoint.setValues(item, xValue, yValue, x, y, PartOf.CURRENT, true, false); 713 currentSeriesData.add(dropDownDataPoint); 714 } 715 716 private void addPoint(ArrayList<DataPointInfo<X, Y>> currentSeriesData, Data<X, Y> item, X xValue, Y yValue, double x, double y, PartOf partof, 717 boolean symbol, boolean lineTo) { 718 DataPointInfo<X, Y> currentDataPoint = new DataPointInfo<>(); 719 currentDataPoint.setValues(item, xValue, yValue, x, y, partof, symbol, lineTo); 720 currentSeriesData.add(currentDataPoint); 721 } 722 723 //-------------------- helper methods to retrieve data points from the previous 724 // or current data series. 725 private int findNextCurrent(ArrayList<DataPointInfo<X, Y>> points, int index) { 726 for(int i = index+1; i < points.size(); i++) { 727 if (points.get(i).partOf.equals(PartOf.CURRENT)) { 728 return i; 729 } 730 } 731 return -1; 732 } 733 734 private int findPreviousCurrent(ArrayList<DataPointInfo<X, Y>> points, int index) { 735 for(int i = index-1; i >= 0; i--) { 736 if (points.get(i).partOf.equals(PartOf.CURRENT)) { 737 return i; 738 } 739 } 740 return -1; 741 } 742 743 744 private int findPreviousPrevious(ArrayList<DataPointInfo<X, Y>> points, int index) { 745 for(int i = index-1; i >= 0; i--) { 746 if (points.get(i).partOf.equals(PartOf.PREVIOUS)) { 747 return i; 748 } 749 } 750 return -1; 751 } 752 private int findNextPrevious(ArrayList<DataPointInfo<X, Y>> points, int index) { 753 for(int i = index+1; i < points.size(); i++) { 754 if (points.get(i).partOf.equals(PartOf.PREVIOUS)) { 755 return i; 756 } 757 } 758 return -1; 759 } 760 761 762 private void sortAggregateList(ArrayList<DataPointInfo<X, Y>> aggregateList) { 763 Collections.sort(aggregateList, (o1, o2) -> { 764 Data<X,Y> d1 = o1.dataItem; 765 Data<X,Y> d2 = o2.dataItem; 766 double val1 = getXAxis().toNumericValue(d1.getXValue()); 767 double val2 = getXAxis().toNumericValue(d2.getXValue()); 768 return (val1 < val2 ? -1 : ( val1 == val2) ? 0 : 1); 769 }); 770 } 771 772 private double interpolate(double lowX, double lowY, double highX, double highY, double x) { 773 // using y = mx+c find the y for the given x. 774 return (((highY - lowY)/(highX - lowX))*(x - lowX))+lowY; 775 } 776 777 private Node createSymbol(Series<X,Y> series, int seriesIndex, final Data<X,Y> item, int itemIndex) { 778 Node symbol = item.getNode(); 779 // check if symbol has already been created 780 if (symbol == null && getCreateSymbols()) { 781 symbol = new StackPane(); 782 symbol.setAccessibleRole(AccessibleRole.TEXT); 783 symbol.setAccessibleRoleDescription("Point"); 784 symbol.focusTraversableProperty().bind(Platform.accessibilityActiveProperty()); 785 item.setNode(symbol); 786 } 787 // set symbol styles 788 // Note not sure if we want to add or check, ie be more careful and efficient here 789 if (symbol != null) symbol.getStyleClass().setAll("chart-area-symbol", "series" + seriesIndex, "data" + itemIndex, 790 series.defaultColorStyleClass); 791 return symbol; 792 } 793 794 /** 795 * This is called whenever a series is added or removed and the legend needs to be updated 796 */ 797 @Override protected void updateLegend() { 798 legend.getItems().clear(); 799 if (getData() != null) { 800 for (int seriesIndex=0; seriesIndex < getData().size(); seriesIndex++) { 801 Series<X,Y> series = getData().get(seriesIndex); 802 LegendItem legenditem = new LegendItem(series.getName()); 803 legenditem.getSymbol().getStyleClass().addAll("chart-area-symbol","series"+seriesIndex, 804 "area-legend-symbol", series.defaultColorStyleClass); 805 legend.getItems().add(legenditem); 806 } 807 } 808 if (legend.getItems().size() > 0) { 809 if (getLegend() == null) { 810 setLegend(legend); 811 } 812 } else { 813 setLegend(null); 814 } 815 } 816 817 // -------------- INNER CLASSES -------------------------------------------- 818 /* 819 * Helper class to hold data and display and other information for each 820 * data point 821 */ 822 final static class DataPointInfo<X, Y> { 823 X x; 824 Y y; 825 double displayX; 826 double displayY; 827 Data<X,Y> dataItem; 828 PartOf partOf; 829 boolean skipSymbol = false; // interpolated point - skip drawing symbol 830 boolean lineTo = false; // should there be a lineTo to this point on SeriesLine. 831 boolean dropDown = false; // Is this a drop down point ( non data point). 832 833 //----- Constructors -------------------- 834 DataPointInfo() {} 835 836 DataPointInfo(Data<X,Y> item, X x, Y y, PartOf partOf) { 837 this.dataItem = item; 838 this.x = x; 839 this.y = y; 840 this.partOf = partOf; 841 } 842 843 DataPointInfo(boolean dropDown) { 844 this.dropDown = dropDown; 845 } 846 847 void setValues(Data<X,Y> item, X x, Y y, double dx, double dy, 848 PartOf partOf, boolean skipSymbol, boolean lineTo) { 849 this.dataItem = item; 850 this.x = x; 851 this.y = y; 852 this.displayX = dx; 853 this.displayY = dy; 854 this.partOf = partOf; 855 this.skipSymbol = skipSymbol; 856 this.lineTo = lineTo; 857 } 858 859 public final X getX() { 860 return x; 861 } 862 863 public final Y getY() { 864 return y; 865 } 866 } 867 868 // To indicate if the data point belongs to the current or the previous series. 869 private static enum PartOf { 870 CURRENT, 871 PREVIOUS 872 } 873 874 // -------------- STYLESHEET HANDLING -------------------------------------- 875 876 private static class StyleableProperties { 877 878 private static final CssMetaData<StackedAreaChart<?, ?>, Boolean> CREATE_SYMBOLS = 879 new CssMetaData<StackedAreaChart<?, ?>, Boolean>("-fx-create-symbols", 880 BooleanConverter.getInstance(), Boolean.TRUE) { 881 @Override 882 public boolean isSettable(StackedAreaChart<?,?> node) { 883 return node.createSymbols == null || !node.createSymbols.isBound(); 884 } 885 886 @Override 887 public StyleableProperty<Boolean> getStyleableProperty(StackedAreaChart<?,?> node) { 888 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.createSymbolsProperty(); 889 } 890 }; 891 892 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 893 894 static { 895 final List<CssMetaData<? extends Styleable, ?>> styleables = 896 new ArrayList<CssMetaData<? extends Styleable, ?>>(XYChart.getClassCssMetaData()); 897 styleables.add(CREATE_SYMBOLS); 898 STYLEABLES = Collections.unmodifiableList(styleables); 899 900 } 901 } 902 903 /** 904 * @return The CssMetaData associated with this class, which may include the 905 * CssMetaData of its super classes. 906 * @since JavaFX 8.0 907 */ 908 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 909 return StyleableProperties.STYLEABLES; 910 } 911 912 /** 913 * {@inheritDoc} 914 * @since JavaFX 8.0 915 */ 916 @Override 917 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 918 return getClassCssMetaData(); 919 } 920 921 }