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