Spec-Zone .ru
спецификации, руководства, описания, API
|
001/* 002 * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved. 003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 004 * 005 * This code is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU General Public License version 2 only, as 007 * published by the Free Software Foundation. Oracle designates this 008 * particular file as subject to the "Classpath" exception as provided 009 * by Oracle in the LICENSE file that accompanied this code. 010 * 011 * This code is distributed in the hope that it will be useful, but WITHOUT 012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 014 * version 2 for more details (a copy is included in the LICENSE file that 015 * accompanied this code). 016 * 017 * You should have received a copy of the GNU General Public License version 018 * 2 along with this work; if not, write to the Free Software Foundation, 019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 020 * 021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 022 * or visit www.oracle.com if you need additional information or have any 023 * questions. 024 */ 025 026package javafx.scene.chart; 027 028 029import java.util.*; 030import javafx.animation.*; 031import javafx.beans.property.DoubleProperty; 032import javafx.beans.property.SimpleDoubleProperty; 033import javafx.collections.FXCollections; 034import javafx.collections.ListChangeListener; 035import javafx.collections.ObservableList; 036import javafx.event.ActionEvent; 037import javafx.event.EventHandler; 038import javafx.scene.Group; 039import javafx.scene.Node; 040import javafx.scene.layout.StackPane; 041import javafx.scene.shape.*; 042import javafx.util.Duration; 043import com.sun.javafx.charts.Legend; 044import com.sun.javafx.charts.Legend.LegendItem; 045import com.sun.javafx.css.converters.BooleanConverter; 046import javafx.beans.property.BooleanProperty; 047import javafx.css.CssMetaData; 048import javafx.css.Styleable; 049import javafx.css.StyleableBooleanProperty; 050import javafx.css.StyleableProperty; 051import javafx.scene.paint.Color; 052 053/** 054 * StackedAreaChart is a variation of {@link AreaChart} that displays trends of the 055 * contribution of each value. (over time e.g.) The areas are stacked so that each 056 * series adjoins but does not overlap the preceding series. This contrasts with 057 * the Area chart where each series overlays the preceding series. 058 * 059 * The cumulative nature of the StackedAreaChart gives an idea of the total Y data 060 * value at any given point along the X axis. 061 * 062 * Since data points across multiple series may not be common, StackedAreaChart 063 * interpolates values along the line joining the data points whenever necessary. 064 * 065 */ 066public class StackedAreaChart<X,Y> extends XYChart<X,Y> { 067 068 // -------------- PRIVATE FIELDS ------------------------------------------ 069 070 /** A multiplier for teh Y values that we store for each series, it is used to animate in a new series */ 071 private Map<Series, DoubleProperty> seriesYMultiplierMap = new HashMap<Series, DoubleProperty>(); 072 private Legend legend = new Legend(); 073 074 // -------------- PUBLIC PROPERTIES ---------------------------------------- 075 /** 076 * When true, CSS styleable symbols are created for any data items that 077 * don't have a symbol node specified. 078 */ 079 private BooleanProperty createSymbols = new StyleableBooleanProperty(true) { 080 @Override 081 protected void invalidated() { 082 for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) { 083 Series<X, Y> series = getData().get(seriesIndex); 084 for (int itemIndex = 0; itemIndex < series.getData().size(); itemIndex++) { 085 Data<X, Y> item = series.getData().get(itemIndex); 086 Node symbol = item.getNode(); 087 if (get() && symbol == null) { // create any symbols 088 symbol = createSymbol(series, getData().indexOf(series), item, itemIndex); 089 if (null != symbol) { 090 getPlotChildren().add(symbol); 091 } 092 } else if (!get() && symbol != null) { // remove symbols 093 getPlotChildren().remove(symbol); 094 symbol = null; 095 item.setNode(null); 096 } 097 } 098 } 099 requestChartLayout(); 100 } 101 102 public Object getBean() { 103 return this; 104 } 105 106 public String getName() { 107 return "createSymbols"; 108 } 109 110 public CssMetaData getCssMetaData() { 111 return StyleableProperties.CREATE_SYMBOLS; 112 } 113 }; 114 115 /** 116 * Indicates whether symbols for data points will be created or not. 117 * 118 * @return true if symbols for data points will be created and false otherwise. 119 */ 120 public final boolean getCreateSymbols() { return createSymbols.getValue(); } 121 public final void setCreateSymbols(boolean value) { createSymbols.setValue(value); } 122 public final BooleanProperty createSymbolsProperty() { return createSymbols; } 123 124 // -------------- CONSTRUCTORS ---------------------------------------------- 125 126 /** 127 * Construct a new Area Chart with the given axis 128 * 129 * @param xAxis The x axis to use 130 * @param yAxis The y axis to use 131 */ 132 public StackedAreaChart(Axis<X> xAxis, Axis<Y> yAxis) { 133 this(xAxis,yAxis, FXCollections.<Series<X,Y>>observableArrayList()); 134 } 135 136 /** 137 * Construct a new Area Chart with the given axis and data 138 * 139 * @param xAxis The x axis to use 140 * @param yAxis The y axis to use 141 * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart 142 */ 143 public StackedAreaChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<Series<X,Y>> data) { 144 super(xAxis,yAxis); 145 setLegend(legend); 146 setData(data); 147 } 148 149 // -------------- METHODS ------------------------------------------------------------------------------------------ 150 151 private static double doubleValue(Number number) { return doubleValue(number, 0); } 152 private static double doubleValue(Number number, double nullDefault) { 153 return (number == null) ? nullDefault : number.doubleValue(); 154 } 155 156 @Override protected void dataItemAdded(Series<X,Y> series, int itemIndex, Data<X,Y> item) { 157 final Node symbol = createSymbol(series, getData().indexOf(series), item, itemIndex); 158 if (shouldAnimate()) { 159 boolean animate = false; 160 if (itemIndex > 0 && itemIndex < (series.getData().size()-1)) { 161 animate = true; 162 Data<X,Y> p1 = series.getData().get(itemIndex - 1); 163 Data<X,Y> p2 = series.getData().get(itemIndex + 1); 164 double x1 = getXAxis().toNumericValue(p1.getXValue()); 165 double y1 = getYAxis().toNumericValue(p1.getYValue()); 166 double x3 = getXAxis().toNumericValue(p2.getXValue()); 167 double y3 = getYAxis().toNumericValue(p2.getYValue()); 168 169 double x2 = getXAxis().toNumericValue(item.getXValue()); 170 double y2 = getYAxis().toNumericValue(item.getYValue()); 171 172// //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1) 173 double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1); 174 item.setCurrentY(getYAxis().toRealValue(y)); 175 item.setCurrentX(getXAxis().toRealValue(x2)); 176 //2. we can simply use the midpoint on the line as well.. 177// double x = (x3 + x1)/2; 178// double y = (y3 + y1)/2; 179// item.setCurrentX(x); 180// item.setCurrentY(y); 181 } else if (itemIndex == 0 && series.getData().size() > 1) { 182 animate = true; 183 item.setCurrentX(series.getData().get(1).getXValue()); 184 item.setCurrentY(series.getData().get(1).getYValue()); 185 } else if (itemIndex == (series.getData().size() - 1) && series.getData().size() > 1) { 186 animate = true; 187 int last = series.getData().size() - 2; 188 item.setCurrentX(series.getData().get(last).getXValue()); 189 item.setCurrentY(series.getData().get(last).getYValue()); 190 } else if (symbol != null) { 191 // fade in new symbol 192 FadeTransition ft = new FadeTransition(Duration.millis(500),symbol); 193 ft.setToValue(1); 194 ft.play(); 195 } 196 if (animate) { 197 animate( 198 new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(), 199 item.getCurrentY()), 200 new KeyValue(item.currentXProperty(), 201 item.getCurrentX())), 202 new KeyFrame(Duration.millis(800), new KeyValue(item.currentYProperty(), 203 item.getYValue(), Interpolator.EASE_BOTH), 204 new KeyValue(item.currentXProperty(), 205 item.getXValue(), Interpolator.EASE_BOTH)) 206 ); 207 } 208 209 } else if (symbol != null) { 210 getPlotChildren().add(symbol); 211 } 212 } 213 214 @Override protected void dataItemRemoved(final Data<X,Y> item, final Series<X,Y> series) { 215 final Node symbol = item.getNode(); 216 // remove item from sorted list 217 int itemIndex = series.getItemIndex(item); 218 if (shouldAnimate()) { 219 boolean animate = false; 220 if (itemIndex > 0 && itemIndex < series.getDataSize()) { 221 animate = true; 222 int index=0; Data<X,Y> d; 223 for (d = series.begin; d != null && index != itemIndex - 1; d=d.next) index++; 224 Data<X,Y> p1 = d; 225 Data<X,Y> p2 = (d.next).next; 226 double x1 = getXAxis().toNumericValue(p1.getXValue()); 227 double y1 = getYAxis().toNumericValue(p1.getYValue()); 228 double x3 = getXAxis().toNumericValue(p2.getXValue()); 229 double y3 = getYAxis().toNumericValue(p2.getYValue()); 230 231 double x2 = getXAxis().toNumericValue(item.getXValue()); 232 double y2 = getYAxis().toNumericValue(item.getYValue()); 233 234// //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1) 235 double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1); 236 item.setCurrentX(getXAxis().toRealValue(x2)); 237 item.setCurrentY(getYAxis().toRealValue(y2)); 238 item.setXValue(getXAxis().toRealValue(x2)); 239 item.setYValue(getYAxis().toRealValue(y)); 240 //2. we can simply use the midpoint on the line as well.. 241// double x = (x3 + x1)/2; 242// double y = (y3 + y1)/2; 243// item.setCurrentX(x); 244// item.setCurrentY(y); 245 } else if (itemIndex == 0 && series.getDataSize() > 1) { 246 animate = true; 247 item.setXValue(series.getData().get(0).getXValue()); 248 item.setYValue(series.getData().get(0).getYValue()); 249 } else if (itemIndex == (series.getDataSize() - 1) && series.getDataSize() > 1) { 250 animate = true; 251 int last = series.getData().size() - 1; 252 item.setXValue(series.getData().get(last).getXValue()); 253 item.setYValue(series.getData().get(last).getYValue()); 254 } else { 255 // fade out symbol 256 symbol.setOpacity(0); 257 FadeTransition ft = new FadeTransition(Duration.millis(500),symbol); 258 ft.setToValue(0); 259 ft.setOnFinished(new EventHandler<ActionEvent>() { 260 @Override public void handle(ActionEvent actionEvent) { 261 getPlotChildren().remove(symbol); 262 removeDataItemFromDisplay(series, item); 263 } 264 }); 265 ft.play(); 266 } 267 if (animate) { 268 animate( new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(), 269 item.getCurrentY()), new KeyValue(item.currentXProperty(), 270 item.getCurrentX())), 271 new KeyFrame(Duration.millis(800), new EventHandler<ActionEvent>() { 272 @Override public void handle(ActionEvent actionEvent) { 273 getPlotChildren().remove(symbol); 274 removeDataItemFromDisplay(series, item); 275 } 276 }, 277 new KeyValue(item.currentYProperty(), 278 item.getYValue(), Interpolator.EASE_BOTH), 279 new KeyValue(item.currentXProperty(), 280 item.getXValue(), Interpolator.EASE_BOTH)) 281 ); 282 } 283 } else { 284 getPlotChildren().remove(symbol); 285 removeDataItemFromDisplay(series, item); 286 } 287 //Note: better animation here, point should move from old position to new position at center point between prev and next symbols 288 } 289 290 /** @inheritDoc */ 291 @Override protected void dataItemChanged(Data<X, Y> item) { 292 } 293 294 @Override protected void seriesChanged(ListChangeListener.Change<? extends Series> c) { 295 // Update style classes for all series lines and symbols 296 for (int i = 0; i < getDataSize(); i++) { 297 final Series<X,Y> s = getData().get(i); 298 Path seriesLine = (Path)((Group)s.getNode()).getChildren().get(1); 299 Path fillPath = (Path)((Group)s.getNode()).getChildren().get(0); 300 seriesLine.getStyleClass().setAll("chart-series-area-line", "series" + i, s.defaultColorStyleClass); 301 fillPath.getStyleClass().setAll("chart-series-area-fill", "series" + i, s.defaultColorStyleClass); 302 for (int j=0; j < s.getData().size(); j++) { 303 final Data item = s.getData().get(j); 304 final Node node = item.getNode(); 305 if(node!=null) node.getStyleClass().setAll("chart-area-symbol", "series" + i, "data" + j, s.defaultColorStyleClass); 306 } 307 } 308 } 309 310 @Override protected void seriesAdded(Series<X,Y> series, int seriesIndex) { 311 // create new paths for series 312 Path seriesLine = new Path(); 313 Path fillPath = new Path(); 314 seriesLine.setStrokeLineJoin(StrokeLineJoin.BEVEL); 315 fillPath.setStrokeLineJoin(StrokeLineJoin.BEVEL); 316 Group areaGroup = new Group(fillPath,seriesLine); 317 series.setNode(areaGroup); 318 // create series Y multiplier 319 DoubleProperty seriesYAnimMultiplier = new SimpleDoubleProperty(this, "seriesYMultiplier"); 320 seriesYMultiplierMap.put(series, seriesYAnimMultiplier); 321 // handle any data already in series 322 if (shouldAnimate()) { 323 seriesYAnimMultiplier.setValue(0d); 324 } else { 325 seriesYAnimMultiplier.setValue(1d); 326 } 327 getPlotChildren().add(areaGroup); 328 List<KeyFrame> keyFrames = new ArrayList<KeyFrame>(); 329 if (shouldAnimate()) { 330 // animate in new series 331 keyFrames.add(new KeyFrame(Duration.ZERO, 332 new KeyValue(areaGroup.opacityProperty(), 0), 333 new KeyValue(seriesYAnimMultiplier, 0) 334 )); 335 keyFrames.add(new KeyFrame(Duration.millis(200), 336 new KeyValue(areaGroup.opacityProperty(), 1) 337 )); 338 keyFrames.add(new KeyFrame(Duration.millis(500), 339 new KeyValue(seriesYAnimMultiplier, 1) 340 )); 341 } 342 for (int j=0; j<series.getData().size(); j++) { 343 Data item = series.getData().get(j); 344 final Node symbol = createSymbol(series, seriesIndex, item, j); 345 if (symbol != null) { 346 if (shouldAnimate()) symbol.setOpacity(0); 347 getPlotChildren().add(symbol); 348 if (shouldAnimate()) { 349 // fade in new symbol 350 keyFrames.add(new KeyFrame(Duration.ZERO, new KeyValue(symbol.opacityProperty(), 0))); 351 keyFrames.add(new KeyFrame(Duration.millis(200), new KeyValue(symbol.opacityProperty(), 1))); 352 } 353 } 354 } 355 if (shouldAnimate()) animate(keyFrames.toArray(new KeyFrame[keyFrames.size()])); 356 } 357 358 @Override protected void seriesRemoved(final Series<X,Y> series) { 359 // remove series Y multiplier 360 seriesYMultiplierMap.remove(series); 361 // remove all symbol nodes 362 if (shouldAnimate()) { 363 // create list of all nodes we need to fade out 364 final List<Node> nodes = new ArrayList<Node>(); 365 nodes.add(series.getNode()); 366 for (Data d: series.getData()) nodes.add(d.getNode()); 367 // fade out old and symbols 368 if (getCreateSymbols()) { 369 KeyValue[] startValues = new KeyValue[nodes.size()]; 370 KeyValue[] endValues = new KeyValue[nodes.size()]; 371 for (int j=0; j < nodes.size(); j++) { 372 startValues[j] = new KeyValue(nodes.get(j).opacityProperty(),1); 373 endValues[j] = new KeyValue(nodes.get(j).opacityProperty(),0); 374 } 375 Timeline tl = new Timeline(); 376 tl.getKeyFrames().addAll( 377 new KeyFrame(Duration.ZERO,startValues), 378 new KeyFrame(Duration.millis(400), new EventHandler<ActionEvent>() { 379 @Override public void handle(ActionEvent actionEvent) { 380 getPlotChildren().removeAll(nodes); 381 removeSeriesFromDisplay(series); 382 } 383 },endValues) 384 ); 385 tl.play(); 386 } else { 387 Timeline tl = new Timeline(); 388 tl.getKeyFrames().addAll( 389 new KeyFrame(Duration.millis(400), new EventHandler<ActionEvent>() { 390 @Override public void handle(ActionEvent actionEvent) { 391 getPlotChildren().removeAll(nodes); 392 removeSeriesFromDisplay(series); 393 } 394 }) 395 ); 396 tl.play(); 397 } 398 } else { 399 getPlotChildren().remove(series.getNode()); 400 for (Data d:series.getData()) getPlotChildren().remove(d.getNode()); 401 removeSeriesFromDisplay(series); 402 } 403 } 404 405 /** @inheritDoc */ 406 @Override protected void updateAxisRange() { 407 // This override is necessary to update axis range based on cumulative Y value for the 408 // Y axis instead of the normal way where max value in the data range is used. 409 final Axis<X> xa = getXAxis(); 410 final Axis<Y> ya = getYAxis(); 411 if (xa.isAutoRanging()) { 412 List xData = new ArrayList<Number>(); 413 if(xData != null) { 414 for(Series<X,Y> series : getData()) { 415 for(Data<X,Y> data: series.getData()) { 416 if(xData != null) xData.add(data.getXValue()); 417 } 418 } 419 if(xData != null) xa.invalidateRange(xData); 420 } 421 } 422 if (ya.isAutoRanging()) { 423 List yData = new ArrayList<Number>(); 424 if(yData != null) { 425 double totalY = 0; 426 Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator(); 427 while (seriesIterator.hasNext()) { 428 double maxY = 0; 429 Series<X, Y> series = seriesIterator.next(); 430 for(Data<X,Y> item : series.getData()) { 431 if(item != null) maxY = Math.max(maxY, ya.toNumericValue(item.getYValue())); 432 } 433 totalY += maxY; 434 435 } 436 if(totalY > 0) yData.add(totalY); 437 ya.invalidateRange(yData); 438 } 439 } 440 } 441 442 443 /** @inheritDoc */ 444 @Override protected void layoutPlotChildren() { 445 ArrayList<DataPointInfo> currentSeriesData = 446 new ArrayList<DataPointInfo>(); 447 // AggregateData hold the data points of both the current and the previous series. 448 // The goal is to collect all the data, sort it and iterate. 449 ArrayList<DataPointInfo> aggregateData = 450 new ArrayList<DataPointInfo>(); 451 for (int seriesIndex=0; seriesIndex < getDataSize(); seriesIndex++) { // for every series 452 Series<X, Y> series = getData().get(seriesIndex); 453 aggregateData.clear(); 454 // copy currentSeriesData accumulated in the previous iteration to aggregate. 455 for(DataPointInfo<X,Y> data : currentSeriesData) { 456 data.partOf = PartOf.PREVIOUS; 457 aggregateData.add(data); 458 } 459 currentSeriesData.clear(); 460 // now copy actual data of the current series. 461 for(Data<X, Y> item = series.begin; item != null; item = item.next) { 462 DataPointInfo<X,Y> itemInfo = new DataPointInfo(item, item.getXValue(), 463 item.getYValue(), PartOf.CURRENT); 464 aggregateData.add(itemInfo); 465 } 466 DoubleProperty seriesYAnimMultiplier = seriesYMultiplierMap.get(series); 467 Path seriesLine = (Path)((Group)series.getNode()).getChildren().get(1); 468 Path fillPath = (Path)((Group)series.getNode()).getChildren().get(0); 469 seriesLine.getElements().clear(); 470 fillPath.getElements().clear(); 471 int dataIndex = 0; 472 // Sort data points from prev and current series 473 sortAggregateList(aggregateData); 474 475 boolean firstCurrent = false; 476 boolean lastCurrent = false; 477 int firstCurrentIndex = findNextCurrent(aggregateData, -1); 478 int lastCurrentIndex = findPreviousCurrent(aggregateData, aggregateData.size()); 479 // Iterate over the aggregate data : this process accumulates data points 480 // cumulatively from the bottom to top of stack 481 for (DataPointInfo<X,Y> dataInfo : aggregateData) { 482 if (dataIndex == lastCurrentIndex) lastCurrent = true; 483 if (dataIndex == firstCurrentIndex) firstCurrent = true; 484 double x = 0; 485 double y = 0; 486 DataPointInfo<X,Y> currentDataPoint = new DataPointInfo(); 487 DataPointInfo<X,Y> dropDownDataPoint = new DataPointInfo(true); 488 Data<X,Y> item = dataInfo.dataItem; 489 if (dataInfo.partOf.equals(PartOf.CURRENT)) { // handle data from current series 490 int pIndex = findPreviousPrevious(aggregateData, dataIndex); 491 int nIndex = findNextPrevious(aggregateData, dataIndex); 492 DataPointInfo<X,Y> prevPoint; 493 DataPointInfo<X,Y> nextPoint; 494 if (pIndex == -1 || (nIndex == -1 && !(aggregateData.get(pIndex).x.equals(dataInfo.x)))) { 495 if (firstCurrent) { 496 // Need to add the drop down point. 497 item = new Data(dataInfo.x, 0); 498 x = getXAxis().getDisplayPosition(item.getCurrentX()); 499 y = getYAxis().getZeroPosition(); 500 dropDownDataPoint.setValues(item, item.getXValue(), item.getYValue(), x, y, PartOf.CURRENT, true, false); 501 currentSeriesData.add(dropDownDataPoint); 502 } 503 // And add current point. 504 item = dataInfo.dataItem; 505 x = getXAxis().getDisplayPosition(item.getCurrentX()); 506 y = getYAxis().getDisplayPosition( 507 getYAxis().toRealValue(getYAxis().toNumericValue(item.getCurrentY()) * seriesYAnimMultiplier.getValue())); 508 currentDataPoint.setValues(item, item.getXValue(), item.getYValue(), x, y, PartOf.CURRENT, false, (firstCurrent) ? false : true); 509 currentSeriesData.add(currentDataPoint); 510 if (dataIndex == lastCurrentIndex) { 511 // need to add drop down point 512 item = new Data(dataInfo.x, 0); 513 x = getXAxis().getDisplayPosition(item.getCurrentX()); 514 y = getYAxis().getZeroPosition(); 515 dropDownDataPoint.setValues(item, item.getXValue(), item.getYValue(), x, y, PartOf.CURRENT, true, false); 516 currentSeriesData.add(dropDownDataPoint); 517 } 518 } else { 519 prevPoint = aggregateData.get(pIndex); 520 if (prevPoint.x.equals(dataInfo.x)) { // Need to add Y values 521 // Check if prevPoint is a dropdown - as the stable sort preserves the order. 522 // If so, find the non dropdown previous point on previous series. 523 DataPointInfo<X,Y> ddPoint = prevPoint; 524 if (prevPoint.dropDown) { 525 pIndex = findPreviousPrevious(aggregateData, pIndex); 526 prevPoint = (DataPointInfo<X,Y>)aggregateData.get(pIndex); 527 // If lastCurrent - add this drop down 528 } 529 if (prevPoint.x.equals(dataInfo.x)) { // simply add 530 x = getXAxis().getDisplayPosition(item.getCurrentX()); 531 y = getYAxis().getDisplayPosition( 532 getYAxis().toRealValue(getYAxis().toNumericValue(item.getCurrentY()) * seriesYAnimMultiplier.getValue())); 533 y += -(getYAxis().getZeroPosition() - prevPoint.displayY); 534 currentDataPoint.setValues(item, dataInfo.x, dataInfo.y, x, y, PartOf.CURRENT, false, 535 (firstCurrent) ? false : true); 536 currentSeriesData.add(currentDataPoint); 537 } 538 if (lastCurrent) { 539 dropDownDataPoint.setValues(ddPoint.dataItem, ddPoint.x, ddPoint.y, 540 ddPoint.displayX, ddPoint.displayY, PartOf.CURRENT, true, false); 541 currentSeriesData.add(dropDownDataPoint); 542 } 543 } else { 544 // interpolate 545 nextPoint = (nIndex == -1) ? null : (DataPointInfo<X,Y>)aggregateData.get(nIndex); 546 prevPoint = (pIndex == -1) ? null : (DataPointInfo<X,Y>)aggregateData.get(pIndex); 547 x = getXAxis().getDisplayPosition(item.getCurrentX()); 548 y = getYAxis().getDisplayPosition( 549 getYAxis().toRealValue(getYAxis().toNumericValue(item.getCurrentY()) * seriesYAnimMultiplier.getValue())); 550 if (prevPoint != null && nextPoint != null) { 551 double displayY = interpolate(prevPoint.displayX, 552 prevPoint.displayY, nextPoint.displayX, nextPoint.displayY, x); 553 y+= -(getYAxis().getZeroPosition() - displayY); 554 double dataY = interpolate(getXAxis().toNumericValue(prevPoint.x), 555 getYAxis().toNumericValue(prevPoint.y), 556 getXAxis().toNumericValue(nextPoint.x), 557 getYAxis().toNumericValue(nextPoint.y), 558 getXAxis().toNumericValue(dataInfo.x)); 559 if (firstCurrent) { 560 // now create the drop down point 561 item = new Data(dataInfo.x, dataY); 562 dropDownDataPoint.setValues(item, dataInfo.x, getYAxis().toRealValue(dataY), x, displayY, PartOf.CURRENT, true, false); 563 currentSeriesData.add(dropDownDataPoint); 564 } 565 // Add the current point 566 currentDataPoint.setValues(item, dataInfo.x, dataInfo.y, x, y, PartOf.CURRENT, false, 567 (firstCurrent) ? false : true); 568 currentSeriesData.add(currentDataPoint); 569 if (dataIndex == lastCurrentIndex) { 570 // add drop down point 571 item = new Data(dataInfo.x, dataY); 572 dropDownDataPoint.setValues(item, dataInfo.x, getYAxis().toRealValue(dataY), x, displayY, PartOf.CURRENT, true, false); 573 currentSeriesData.add(dropDownDataPoint); 574 } 575 // Note: add drop down if last current 576 } 577 else { 578 // we do not need to take care of this as it is 579 // already handled above with check of if(pIndex == -1 or nIndex == -1) 580 } 581 } 582 } 583 584 } else { // handle data from Previous series. 585 int pIndex = findPreviousCurrent(aggregateData, dataIndex); 586 int nIndex = findNextCurrent(aggregateData, dataIndex); 587 DataPointInfo<X,Y> prevPoint; 588 DataPointInfo<X,Y> nextPoint; 589 if (dataInfo.dropDown) { 590 if (getXAxis().toNumericValue(dataInfo.x) <= 591 getXAxis().toNumericValue(((DataPointInfo<X,Y>)aggregateData.get(firstCurrentIndex)).x) || 592 getXAxis().toNumericValue(dataInfo.x) > getXAxis().toNumericValue(((DataPointInfo<X,Y>)aggregateData.get(lastCurrentIndex)).x)) { 593 currentDataPoint.setValues(item, dataInfo.x, dataInfo.y, dataInfo.displayX, dataInfo.displayY, 594 PartOf.CURRENT, true, false); 595 currentDataPoint.dropDown = true; 596 currentSeriesData.add(currentDataPoint); 597 } 598 } else { 599 if (pIndex == -1 || nIndex == -1) { 600 currentDataPoint.setValues(item, dataInfo.x, dataInfo.y, dataInfo.displayX, dataInfo.displayY, 601 PartOf.CURRENT, true, false); 602 currentSeriesData.add(currentDataPoint); 603 } else { 604 nextPoint = (DataPointInfo<X,Y>)aggregateData.get(nIndex); 605 if (nextPoint.x.equals(dataInfo.x)) { 606 // do nothing as the current point is already there. 607 } else { 608 // interpolate on the current series. 609 prevPoint = (DataPointInfo<X,Y>)aggregateData.get(pIndex); 610 x = getXAxis().getDisplayPosition(item.getCurrentX()); 611 double dataY = interpolate(getXAxis().toNumericValue(prevPoint.x), 612 getYAxis().toNumericValue(prevPoint.y), 613 getXAxis().toNumericValue(nextPoint.x), 614 getYAxis().toNumericValue(nextPoint.y), 615 getXAxis().toNumericValue(dataInfo.x)); 616 y = getYAxis().getDisplayPosition( 617 getYAxis().toRealValue(dataY * seriesYAnimMultiplier.getValue())); 618 y+= -(getYAxis().getZeroPosition() - dataInfo.displayY); 619 currentDataPoint.setValues(new Data(dataInfo.x, dataY), dataInfo.x, getYAxis().toRealValue(0), x, y, PartOf.CURRENT, true, true); 620 currentSeriesData.add(currentDataPoint); 621 } 622 } 623 } 624 } 625 dataIndex++; 626 if (firstCurrent) firstCurrent = false; 627 if (lastCurrent) lastCurrent = false; 628 } // end of inner for loop 629 630 // Draw the SeriesLine and Series fill 631 seriesLine.getElements().add(new MoveTo(currentSeriesData.get(0).displayX, currentSeriesData.get(0).displayY)); 632 fillPath.getElements().add(new MoveTo(currentSeriesData.get(0).displayX, currentSeriesData.get(0).displayY)); 633 for (DataPointInfo point : currentSeriesData) { 634 if (!point.lineTo) { 635 seriesLine.getElements().add(new MoveTo(point.displayX, point.displayY)); 636 } else { 637 seriesLine.getElements().add(new LineTo(point.displayX, point.displayY)); 638 } 639 fillPath.getElements().add(new LineTo(point.displayX, point.displayY)); 640 // draw symbols only for actual data points and skip for interpolated points. 641 if (!point.skipSymbol) { 642 Node symbol = point.dataItem.getNode(); 643 if (symbol != null) { 644 final double w = symbol.prefWidth(-1); 645 final double h = symbol.prefHeight(-1); 646 symbol.resizeRelocate(point.displayX-(w/2), point.displayY-(h/2),w,h); 647 } 648 } 649 } 650 for(int i = aggregateData.size()-1; i > 0; i--) { 651 DataPointInfo point = aggregateData.get(i); 652 if (PartOf.PREVIOUS.equals(point.partOf)) { 653 fillPath.getElements().add(new LineTo(point.displayX, point.displayY)); 654 } 655 } 656 fillPath.getElements().add(new ClosePath()); 657 658 } // end of out for loop 659 } 660 661 //-------------------- helper methods to retrieve data points from the previous 662 // or current data series. 663 private int findNextCurrent(ArrayList<DataPointInfo> points, int index) { 664 for(int i = index+1; i < points.size(); i++) { 665 if (points.get(i).partOf.equals(PartOf.CURRENT)) { 666 return i; 667 } 668 } 669 return -1; 670 } 671 672 private int findPreviousCurrent(ArrayList<DataPointInfo> points, int index) { 673 for(int i = index-1; i >= 0; i--) { 674 if (points.get(i).partOf.equals(PartOf.CURRENT)) { 675 return i; 676 } 677 } 678 return -1; 679 } 680 681 682 private int findPreviousPrevious(ArrayList<DataPointInfo> points, int index) { 683 for(int i = index-1; i >= 0; i--) { 684 if (points.get(i).partOf.equals(PartOf.PREVIOUS)) { 685 return i; 686 } 687 } 688 return -1; 689 } 690 private int findNextPrevious(ArrayList<DataPointInfo> points, int index) { 691 for(int i = index+1; i < points.size(); i++) { 692 if (points.get(i).partOf.equals(PartOf.PREVIOUS)) { 693 return i; 694 } 695 } 696 return -1; 697 } 698 699 700 private void sortAggregateList(ArrayList<DataPointInfo> aggregateList) { 701 Collections.sort(aggregateList, new Comparator(){ 702 public int compare(Object o1, Object o2) { 703 Data<X,Y> d1 = ((DataPointInfo)o1).dataItem; 704 Data<X,Y> d2 = ((DataPointInfo)o2).dataItem; 705 double val1 = getXAxis().toNumericValue(d1.getXValue()); 706 double val2 = getXAxis().toNumericValue(d2.getXValue()); 707 return (val1 < val2 ? -1 : ( val1 == val2) ? 0 : 1); 708 } 709 }); 710 } 711 712 private double interpolate(double lowX, double lowY, double highX, double highY, double x) { 713 // using y = mx+c find the y for the given x. 714 return (((highY - lowY)/(highX - lowX))*(x - lowX))+lowY; 715 } 716 717 private Node createSymbol(Series series, int seriesIndex, final Data item, int itemIndex) { 718 Node symbol = item.getNode(); 719 // check if symbol has already been created 720 if (symbol == null && getCreateSymbols()) { 721 symbol = new StackPane(); 722 item.setNode(symbol); 723 } 724 // set symbol styles 725 // Note not sure if we want to add or check, ie be more careful and efficient here 726 if (symbol != null) symbol.getStyleClass().setAll("chart-area-symbol", "series" + seriesIndex, "data" + itemIndex, 727 series.defaultColorStyleClass); 728 return symbol; 729 } 730 731 /** 732 * This is called whenever a series is added or removed and the legend needs to be updated 733 */ 734 @Override protected void updateLegend() { 735 legend.getItems().clear(); 736 if (getData() != null) { 737 for (int seriesIndex=0; seriesIndex < getData().size(); seriesIndex++) { 738 Series<X,Y> series = getData().get(seriesIndex); 739 LegendItem legenditem = new LegendItem(series.getName()); 740 legenditem.getSymbol().getStyleClass().addAll("chart-area-symbol","series"+seriesIndex, 741 "area-legend-symbol", series.defaultColorStyleClass); 742 legend.getItems().add(legenditem); 743 } 744 } 745 if (legend.getItems().size() > 0) { 746 if (getLegend() == null) { 747 setLegend(legend); 748 } 749 } else { 750 setLegend(null); 751 } 752 } 753 754 // -------------- INNER CLASSES -------------------------------------------- 755 /* 756 * Helper class to hold data and display and other information for each 757 * data point 758 */ 759 final static class DataPointInfo<X,Y> { 760 X x; 761 Y y; 762 double displayX; 763 double displayY; 764 Data<X,Y> dataItem; 765 PartOf partOf; 766 boolean skipSymbol = false; // interpolated point - skip drawing symbol 767 boolean lineTo = false; // should there be a lineTo to this point on SeriesLine. 768 boolean dropDown = false; // Is this a drop down point ( non data point). 769 770 //----- Constructors -------------------- 771 DataPointInfo() {} 772 773 DataPointInfo(Data<X,Y> item, X x, Y y, PartOf partOf) { 774 this.dataItem = item; 775 this.x = x; 776 this.y = y; 777 this.partOf = partOf; 778 } 779 780 DataPointInfo(boolean dropDown) { 781 this.dropDown = dropDown; 782 } 783 784 void setValues(Data<X,Y> item, X x, Y y, double dx, double dy, 785 PartOf partOf, boolean skipSymbol, boolean lineTo) { 786 this.dataItem = item; 787 this.x = x; 788 this.y = y; 789 this.displayX = dx; 790 this.displayY = dy; 791 this.partOf = partOf; 792 this.skipSymbol = skipSymbol; 793 this.lineTo = lineTo; 794 } 795 796 public final X getX() { 797 return x; 798 } 799 800 public final Y getY() { 801 return y; 802 } 803 } 804 805 // To indicate if the data point belongs to the current or the previous series. 806 private static enum PartOf { 807 CURRENT, 808 PREVIOUS 809 } 810 811 // -------------- STYLESHEET HANDLING -------------------------------------- 812 813 private static class StyleableProperties { 814 815 private static final CssMetaData<StackedAreaChart<?, ?>, Boolean> CREATE_SYMBOLS = 816 new CssMetaData<StackedAreaChart<?, ?>, Boolean>("-fx-create-symbols", 817 BooleanConverter.getInstance(), Boolean.TRUE) { 818 @Override 819 public boolean isSettable(StackedAreaChart node) { 820 return node.createSymbols == null || !node.createSymbols.isBound(); 821 } 822 823 @Override 824 public StyleableProperty<Boolean> getStyleableProperty(StackedAreaChart node) { 825 return (StyleableProperty<Boolean>) node.createSymbolsProperty(); 826 } 827 }; 828 829 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 830 831 static { 832 final List<CssMetaData<? extends Styleable, ?>> styleables = 833 new ArrayList<CssMetaData<? extends Styleable, ?>>(XYChart.getClassCssMetaData()); 834 styleables.add(CREATE_SYMBOLS); 835 STYLEABLES = Collections.unmodifiableList(styleables); 836 837 } 838 } 839 840 /** 841 * @return The CssMetaData associated with this class, which may include the 842 * CssMetaData of its super classes. 843 */ 844 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 845 return StyleableProperties.STYLEABLES; 846 } 847 848 /** 849 * {@inheritDoc} 850 */ 851 @Override 852 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 853 return getClassCssMetaData(); 854 } 855 856}