1 /* 2 * Copyright (c) 2010, 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 import java.util.ArrayList; 29 import java.util.HashMap; 30 import java.util.List; 31 import java.util.Map; 32 33 import javafx.animation.FadeTransition; 34 import javafx.animation.Interpolator; 35 import javafx.animation.KeyFrame; 36 import javafx.animation.KeyValue; 37 import javafx.animation.Timeline; 38 import javafx.animation.Animation; 39 import javafx.application.Platform; 40 import javafx.beans.NamedArg; 41 import javafx.beans.property.BooleanProperty; 42 import javafx.beans.property.DoubleProperty; 43 import javafx.beans.property.ObjectProperty; 44 import javafx.beans.property.ObjectPropertyBase; 45 import javafx.beans.property.SimpleDoubleProperty; 46 import javafx.beans.value.WritableValue; 47 import javafx.collections.FXCollections; 48 import javafx.collections.ListChangeListener; 49 import javafx.collections.ObservableList; 50 import javafx.scene.AccessibleRole; 51 import javafx.scene.Node; 52 import javafx.scene.layout.StackPane; 53 import javafx.scene.shape.LineTo; 54 import javafx.scene.shape.MoveTo; 55 import javafx.scene.shape.Path; 56 import javafx.scene.shape.PathElement; 57 import javafx.scene.shape.StrokeLineJoin; 58 import javafx.util.Duration; 59 60 import com.sun.javafx.charts.Legend; 61 import com.sun.javafx.charts.Legend.LegendItem; 62 63 import javafx.css.StyleableBooleanProperty; 64 import javafx.css.CssMetaData; 65 66 import com.sun.javafx.css.converters.BooleanConverter; 67 68 import java.util.*; 69 70 import javafx.css.Styleable; 71 import javafx.css.StyleableProperty; 72 73 /** 74 * Line Chart plots a line connecting the data points in a series. The data points 75 * themselves can be represented by symbols optionally. Line charts are usually used 76 * to view data trends over time or category. 77 * @since JavaFX 2.0 78 */ 79 public class LineChart<X,Y> extends XYChart<X,Y> { 80 81 // -------------- PRIVATE FIELDS ------------------------------------------ 82 83 /** A multiplier for the Y values that we store for each series, it is used to animate in a new series */ 84 private Map<Series<X,Y>, DoubleProperty> seriesYMultiplierMap = new HashMap<>(); 85 private Legend legend = new Legend(); 86 private Timeline dataRemoveTimeline; 87 private Series<X,Y> seriesOfDataRemoved = null; 88 private Data<X,Y> dataItemBeingRemoved = null; 89 private FadeTransition fadeSymbolTransition = null; 90 private Map<Data<X,Y>, Double> XYValueMap = 91 new HashMap<Data<X,Y>, Double>(); 92 private Timeline seriesRemoveTimeline = null; 93 // -------------- PUBLIC PROPERTIES ---------------------------------------- 94 95 /** When true, CSS styleable symbols are created for any data items that don't have a symbol node specified. */ 96 private BooleanProperty createSymbols = new StyleableBooleanProperty(true) { 97 @Override protected void invalidated() { 98 for (int seriesIndex=0; seriesIndex < getData().size(); seriesIndex ++) { 99 Series<X,Y> series = getData().get(seriesIndex); 100 for (int itemIndex=0; itemIndex < series.getData().size(); itemIndex ++) { 101 Data<X,Y> item = series.getData().get(itemIndex); 102 Node symbol = item.getNode(); 103 if(get() && symbol == null) { // create any symbols 104 symbol = createSymbol(series, getData().indexOf(series), item, itemIndex); 105 getPlotChildren().add(symbol); 106 } else if (!get() && symbol != null) { // remove symbols 107 getPlotChildren().remove(symbol); 108 symbol = null; 109 item.setNode(null); 110 } 111 } 112 } 113 requestChartLayout(); 114 } 115 116 public Object getBean() { 117 return LineChart.this; 118 } 119 120 public String getName() { 121 return "createSymbols"; 122 } 123 124 public CssMetaData<LineChart<?,?>,Boolean> getCssMetaData() { 125 return StyleableProperties.CREATE_SYMBOLS; 126 } 127 }; 128 129 /** 130 * Indicates whether symbols for data points will be created or not. 131 * 132 * @return true if symbols for data points will be created and false otherwise. 133 */ 134 public final boolean getCreateSymbols() { return createSymbols.getValue(); } 135 public final void setCreateSymbols(boolean value) { createSymbols.setValue(value); } 136 public final BooleanProperty createSymbolsProperty() { return createSymbols; } 137 138 139 /** 140 * Indicates whether the data passed to LineChart should be sorted by natural order of one of the axes. 141 * If this is set to {@link SortingPolicy#NONE}, the order in {@link #dataProperty()} will be used. 142 * 143 * @since JavaFX 8u40 144 * @see SortingPolicy 145 * @defaultValue SortingPolicy#X_AXIS 146 */ 147 private ObjectProperty<SortingPolicy> axisSortingPolicy = new ObjectPropertyBase<SortingPolicy>(SortingPolicy.X_AXIS) { 148 @Override protected void invalidated() { 149 requestChartLayout(); 150 } 151 152 public Object getBean() { 153 return LineChart.this; 154 } 155 156 public String getName() { 157 return "axisSortingPolicy"; 158 } 159 160 }; 161 162 public final SortingPolicy getAxisSortingPolicy() { return axisSortingPolicy.getValue(); } 163 public final void setAxisSortingPolicy(SortingPolicy value) { axisSortingPolicy.setValue(value); } 164 public final ObjectProperty<SortingPolicy> axisSortingPolicyProperty() { return axisSortingPolicy; } 165 166 // -------------- CONSTRUCTORS ---------------------------------------------- 167 168 /** 169 * Construct a new LineChart with the given axis. 170 * 171 * @param xAxis The x axis to use 172 * @param yAxis The y axis to use 173 */ 174 public LineChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis) { 175 this(xAxis, yAxis, FXCollections.<Series<X, Y>>observableArrayList()); 176 } 177 178 /** 179 * Construct a new LineChart with the given axis and data. 180 * 181 * @param xAxis The x axis to use 182 * @param yAxis The y axis to use 183 * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart 184 */ 185 public LineChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis, @NamedArg("data") ObservableList<Series<X,Y>> data) { 186 super(xAxis,yAxis); 187 setLegend(legend); 188 setData(data); 189 } 190 191 // -------------- METHODS ------------------------------------------------------------------------------------------ 192 193 /** @inheritDoc */ 194 @Override protected void updateAxisRange() { 195 final Axis<X> xa = getXAxis(); 196 final Axis<Y> ya = getYAxis(); 197 List<X> xData = null; 198 List<Y> yData = null; 199 if(xa.isAutoRanging()) xData = new ArrayList<X>(); 200 if(ya.isAutoRanging()) yData = new ArrayList<Y>(); 201 if(xData != null || yData != null) { 202 for(Series<X,Y> series : getData()) { 203 for(Data<X,Y> data: series.getData()) { 204 if(xData != null) xData.add(data.getXValue()); 205 if(yData != null) yData.add(data.getYValue()); 206 } 207 } 208 // RT-32838 No need to invalidate range if there is one data item - whose value is zero. 209 if(xData != null && !(xData.size() == 1 && getXAxis().toNumericValue(xData.get(0)) == 0)) { 210 xa.invalidateRange(xData); 211 } 212 if(yData != null && !(yData.size() == 1 && getYAxis().toNumericValue(yData.get(0)) == 0)) { 213 ya.invalidateRange(yData); 214 } 215 216 } 217 } 218 219 @Override protected void dataItemAdded(final Series<X,Y> series, int itemIndex, final Data<X,Y> item) { 220 final Node symbol = createSymbol(series, getData().indexOf(series), item, itemIndex); 221 if (shouldAnimate()) { 222 if (dataRemoveTimeline != null && dataRemoveTimeline.getStatus().equals(Animation.Status.RUNNING)) { 223 if (seriesOfDataRemoved == series) { 224 dataRemoveTimeline.stop(); 225 dataRemoveTimeline = null; 226 getPlotChildren().remove(dataItemBeingRemoved.getNode()); 227 removeDataItemFromDisplay(seriesOfDataRemoved, dataItemBeingRemoved); 228 seriesOfDataRemoved = null; 229 dataItemBeingRemoved = null; 230 } 231 } 232 boolean animate = false; 233 if (itemIndex > 0 && itemIndex < (series.getData().size()-1)) { 234 animate = true; 235 Data<X,Y> p1 = series.getData().get(itemIndex - 1); 236 Data<X,Y> p2 = series.getData().get(itemIndex + 1); 237 if (p1 != null && p2 != null) { 238 double x1 = getXAxis().toNumericValue(p1.getXValue()); 239 double y1 = getYAxis().toNumericValue(p1.getYValue()); 240 double x3 = getXAxis().toNumericValue(p2.getXValue()); 241 double y3 = getYAxis().toNumericValue(p2.getYValue()); 242 243 double x2 = getXAxis().toNumericValue(item.getXValue()); 244 //double y2 = getYAxis().toNumericValue(item.getYValue()); 245 if (x2 > x1 && x2 < x3) { 246 //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1) 247 double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1); 248 item.setCurrentY(getYAxis().toRealValue(y)); 249 item.setCurrentX(getXAxis().toRealValue(x2)); 250 } else { 251 //2. we can simply use the midpoint on the line as well.. 252 double x = (x3 + x1)/2; 253 double y = (y3 + y1)/2; 254 item.setCurrentX(getXAxis().toRealValue(x)); 255 item.setCurrentY(getYAxis().toRealValue(y)); 256 } 257 } 258 } else if (itemIndex == 0 && series.getData().size() > 1) { 259 animate = true; 260 item.setCurrentX(series.getData().get(1).getXValue()); 261 item.setCurrentY(series.getData().get(1).getYValue()); 262 } else if (itemIndex == (series.getData().size() - 1) && series.getData().size() > 1) { 263 animate = true; 264 int last = series.getData().size() - 2; 265 item.setCurrentX(series.getData().get(last).getXValue()); 266 item.setCurrentY(series.getData().get(last).getYValue()); 267 } else if(symbol != null) { 268 // fade in new symbol 269 symbol.setOpacity(0); 270 getPlotChildren().add(symbol); 271 FadeTransition ft = new FadeTransition(Duration.millis(500),symbol); 272 ft.setToValue(1); 273 ft.play(); 274 } 275 if (animate) { 276 animate( 277 new KeyFrame(Duration.ZERO, 278 (e) -> { if (symbol != null && !getPlotChildren().contains(symbol)) getPlotChildren().add(symbol); }, 279 new KeyValue(item.currentYProperty(), 280 item.getCurrentY()), 281 new KeyValue(item.currentXProperty(), 282 item.getCurrentX())), 283 new KeyFrame(Duration.millis(700), new KeyValue(item.currentYProperty(), 284 item.getYValue(), Interpolator.EASE_BOTH), 285 new KeyValue(item.currentXProperty(), 286 item.getXValue(), Interpolator.EASE_BOTH)) 287 ); 288 } 289 290 } else { 291 if (symbol != null) getPlotChildren().add(symbol); 292 } 293 } 294 295 @Override protected void dataItemRemoved(final Data<X,Y> item, final Series<X,Y> series) { 296 final Node symbol = item.getNode(); 297 298 if (symbol != null) { 299 symbol.focusTraversableProperty().unbind(); 300 } 301 302 // remove item from sorted list 303 int itemIndex = series.getItemIndex(item); 304 if (shouldAnimate()) { 305 XYValueMap.clear(); 306 boolean animate = false; 307 if (itemIndex > 0 && itemIndex < series.getDataSize()) { 308 animate = true; 309 Data<X,Y> p1 = series.getItem(itemIndex - 1); 310 Data<X,Y> p2 = series.getItem(itemIndex + 1); 311 if (p1 != null && p2 != null) { 312 double x1 = getXAxis().toNumericValue(p1.getXValue()); 313 double y1 = getYAxis().toNumericValue(p1.getYValue()); 314 double x3 = getXAxis().toNumericValue(p2.getXValue()); 315 double y3 = getYAxis().toNumericValue(p2.getYValue()); 316 317 double x2 = getXAxis().toNumericValue(item.getXValue()); 318 double y2 = getYAxis().toNumericValue(item.getYValue()); 319 if (x2 > x1 && x2 < x3) { 320 // //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1) 321 double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1); 322 item.setCurrentX(getXAxis().toRealValue(x2)); 323 item.setCurrentY(getYAxis().toRealValue(y2)); 324 item.setXValue(getXAxis().toRealValue(x2)); 325 item.setYValue(getYAxis().toRealValue(y)); 326 } else { 327 //2. we can simply use the midpoint on the line as well.. 328 double x = (x3 + x1)/2; 329 double y = (y3 + y1)/2; 330 item.setCurrentX(getXAxis().toRealValue(x)); 331 item.setCurrentY(getYAxis().toRealValue(y)); 332 } 333 } 334 } else if (itemIndex == 0 && series.getDataSize() > 1) { 335 animate = true; 336 Iterator<Data<X,Y>> iter = getDisplayedDataIterator(series); 337 if (iter.hasNext()) { // get first data value 338 Data<X,Y> d = iter.next(); 339 item.setXValue(d.getXValue()); 340 item.setYValue(d.getYValue()); 341 } 342 } else if (itemIndex == (series.getDataSize() - 1) && series.getDataSize() > 1) { 343 animate = true; 344 int last = series.getData().size() - 1; 345 item.setXValue(series.getData().get(last).getXValue()); 346 item.setYValue(series.getData().get(last).getYValue()); 347 } else { 348 // fade out symbol 349 if (symbol != null) { 350 fadeSymbolTransition = new FadeTransition(Duration.millis(500),symbol); 351 fadeSymbolTransition.setToValue(0); 352 fadeSymbolTransition.setOnFinished(actionEvent -> { 353 item.setSeries(null); 354 getPlotChildren().remove(symbol); 355 removeDataItemFromDisplay(series, item); 356 symbol.setOpacity(1.0); 357 }); 358 fadeSymbolTransition.play(); 359 } 360 } 361 if (animate) { 362 dataRemoveTimeline = createDataRemoveTimeline(item, symbol, series); 363 seriesOfDataRemoved = series; 364 dataItemBeingRemoved = item; 365 dataRemoveTimeline.play(); 366 } 367 } else { 368 item.setSeries(null); 369 if (symbol != null) getPlotChildren().remove(symbol); 370 removeDataItemFromDisplay(series, item); 371 } 372 //Note: better animation here, point should move from old position to new position at center point between prev and next symbols 373 } 374 375 /** @inheritDoc */ 376 @Override protected void dataItemChanged(Data<X, Y> item) { 377 } 378 379 @Override protected void seriesChanged(ListChangeListener.Change<? extends Series> c) { 380 // Update style classes for all series lines and symbols 381 // Note: is there a more efficient way of doing this? 382 for (int i = 0; i < getDataSize(); i++) { 383 final Series<X,Y> s = getData().get(i); 384 Node seriesNode = s.getNode(); 385 if(seriesNode != null) seriesNode.getStyleClass().setAll("chart-series-line", "series" + i, s.defaultColorStyleClass); 386 } 387 } 388 389 @Override protected void seriesAdded(Series<X,Y> series, int seriesIndex) { 390 // create new path for series 391 Path seriesLine = new Path(); 392 seriesLine.setStrokeLineJoin(StrokeLineJoin.BEVEL); 393 series.setNode(seriesLine); 394 // create series Y multiplier 395 DoubleProperty seriesYAnimMultiplier = new SimpleDoubleProperty(this, "seriesYMultiplier"); 396 seriesYMultiplierMap.put(series, seriesYAnimMultiplier); 397 // handle any data already in series 398 if (shouldAnimate()) { 399 seriesLine.setOpacity(0); 400 seriesYAnimMultiplier.setValue(0d); 401 } else { 402 seriesYAnimMultiplier.setValue(1d); 403 } 404 getPlotChildren().add(seriesLine); 405 406 List<KeyFrame> keyFrames = new ArrayList<KeyFrame>(); 407 if (shouldAnimate()) { 408 // animate in new series 409 keyFrames.add(new KeyFrame(Duration.ZERO, 410 new KeyValue(seriesLine.opacityProperty(), 0), 411 new KeyValue(seriesYAnimMultiplier, 0) 412 )); 413 keyFrames.add(new KeyFrame(Duration.millis(200), 414 new KeyValue(seriesLine.opacityProperty(), 1) 415 )); 416 keyFrames.add(new KeyFrame(Duration.millis(500), 417 new KeyValue(seriesYAnimMultiplier, 1) 418 )); 419 } 420 for (int j=0; j<series.getData().size(); j++) { 421 Data<X,Y> item = series.getData().get(j); 422 final Node symbol = createSymbol(series, seriesIndex, item, j); 423 if(symbol != null) { 424 if (shouldAnimate()) symbol.setOpacity(0); 425 getPlotChildren().add(symbol); 426 if (shouldAnimate()) { 427 // fade in new symbol 428 keyFrames.add(new KeyFrame(Duration.ZERO, new KeyValue(symbol.opacityProperty(), 0))); 429 keyFrames.add(new KeyFrame(Duration.millis(200), new KeyValue(symbol.opacityProperty(), 1))); 430 } 431 } 432 } 433 if (shouldAnimate()) animate(keyFrames.toArray(new KeyFrame[keyFrames.size()])); 434 } 435 private void updateDefaultColorIndex(final Series<X,Y> series) { 436 int clearIndex = seriesColorMap.get(series); 437 series.getNode().getStyleClass().remove(DEFAULT_COLOR+clearIndex); 438 for (int j=0; j < series.getData().size(); j++) { 439 final Node node = series.getData().get(j).getNode(); 440 if(node!=null) { 441 node.getStyleClass().remove(DEFAULT_COLOR+clearIndex); 442 } 443 } 444 } 445 446 @Override protected void seriesRemoved(final Series<X,Y> series) { 447 updateDefaultColorIndex(series); 448 // remove all symbol nodes 449 seriesYMultiplierMap.remove(series); 450 if (shouldAnimate()) { 451 // create list of all nodes we need to fade out 452 final List<Node> nodes = new ArrayList<Node>(); 453 nodes.add(series.getNode()); 454 if (getCreateSymbols()) { // RT-22124 455 // done need to fade the symbols if createSymbols is false 456 for (Data<X,Y> d: series.getData()) nodes.add(d.getNode()); 457 } 458 // fade out old and symbols 459 KeyValue[] startValues = new KeyValue[nodes.size()]; 460 KeyValue[] endValues = new KeyValue[nodes.size()]; 461 for (int j=0; j < nodes.size(); j++) { 462 startValues[j] = new KeyValue(nodes.get(j).opacityProperty(),1); 463 endValues[j] = new KeyValue(nodes.get(j).opacityProperty(),0); 464 } 465 seriesRemoveTimeline = new Timeline(); 466 seriesRemoveTimeline.getKeyFrames().addAll( 467 new KeyFrame(Duration.ZERO,startValues), 468 new KeyFrame(Duration.millis(900), actionEvent -> { 469 getPlotChildren().removeAll(nodes); 470 removeSeriesFromDisplay(series); 471 },endValues) 472 ); 473 seriesRemoveTimeline.play(); 474 } else { 475 getPlotChildren().remove(series.getNode()); 476 for (Data<X,Y> d:series.getData()) getPlotChildren().remove(d.getNode()); 477 removeSeriesFromDisplay(series); 478 } 479 } 480 481 /** @inheritDoc */ 482 @Override protected void layoutPlotChildren() { 483 List<LineTo> constructedPath = new ArrayList<>(getDataSize()); 484 for (int seriesIndex=0; seriesIndex < getDataSize(); seriesIndex++) { 485 Series<X,Y> series = getData().get(seriesIndex); 486 final DoubleProperty seriesYAnimMultiplier = seriesYMultiplierMap.get(series); 487 if(series.getNode() instanceof Path) { 488 final ObservableList<PathElement> seriesLine = ((Path)series.getNode()).getElements(); 489 seriesLine.clear(); 490 constructedPath.clear(); 491 for (Iterator<Data<X, Y>> it = getDisplayedDataIterator(series); it.hasNext(); ) { 492 Data<X, Y> item = it.next(); 493 double x = getXAxis().getDisplayPosition(item.getCurrentX()); 494 double y = getYAxis().getDisplayPosition( 495 getYAxis().toRealValue(getYAxis().toNumericValue(item.getCurrentY()) * seriesYAnimMultiplier.getValue())); 496 if (Double.isNaN(x) || Double.isNaN(y)) { 497 continue; 498 } 499 constructedPath.add(new LineTo(x, y)); 500 501 Node symbol = item.getNode(); 502 if (symbol != null) { 503 final double w = symbol.prefWidth(-1); 504 final double h = symbol.prefHeight(-1); 505 symbol.resizeRelocate(x-(w/2), y-(h/2),w,h); 506 } 507 } 508 switch (getAxisSortingPolicy()) { 509 case X_AXIS: 510 Collections.sort(constructedPath, (e1, e2) -> Double.compare(e1.getX(), e2.getX())); 511 break; 512 case Y_AXIS: 513 Collections.sort(constructedPath, (e1, e2) -> Double.compare(e1.getY(), e2.getY())); 514 break; 515 } 516 517 if (!constructedPath.isEmpty()) { 518 LineTo first = constructedPath.get(0); 519 seriesLine.add(new MoveTo(first.getX(), first.getY())); 520 seriesLine.addAll(constructedPath); 521 } 522 } 523 } 524 } 525 /** @inheritDoc */ 526 @Override void dataBeingRemovedIsAdded(Data item, Series series) { 527 if (fadeSymbolTransition != null) { 528 fadeSymbolTransition.setOnFinished(null); 529 fadeSymbolTransition.stop(); 530 } 531 if (dataRemoveTimeline != null) { 532 dataRemoveTimeline.setOnFinished(null); 533 dataRemoveTimeline.stop(); 534 } 535 final Node symbol = item.getNode(); 536 if (symbol != null) getPlotChildren().remove(symbol); 537 538 item.setSeries(null); 539 removeDataItemFromDisplay(series, item); 540 541 // restore values to item 542 Double value = XYValueMap.get(item); 543 if (value != null) { 544 item.setYValue(value); 545 item.setCurrentY(value); 546 } 547 XYValueMap.clear(); 548 } 549 /** @inheritDoc */ 550 @Override void seriesBeingRemovedIsAdded(Series<X,Y> series) { 551 if (seriesRemoveTimeline != null) { 552 seriesRemoveTimeline.setOnFinished(null); 553 seriesRemoveTimeline.stop(); 554 getPlotChildren().remove(series.getNode()); 555 for (Data<X,Y> d:series.getData()) getPlotChildren().remove(d.getNode()); 556 removeSeriesFromDisplay(series); 557 } 558 } 559 560 private Timeline createDataRemoveTimeline(final Data<X,Y> item, final Node symbol, final Series<X,Y> series) { 561 Timeline t = new Timeline(); 562 // save data values in case the same data item gets added immediately. 563 XYValueMap.put(item, ((Number)item.getYValue()).doubleValue()); 564 565 t.getKeyFrames().addAll(new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(), 566 item.getCurrentY()), new KeyValue(item.currentXProperty(), 567 item.getCurrentX())), 568 new KeyFrame(Duration.millis(500), actionEvent -> { 569 if (symbol != null) getPlotChildren().remove(symbol); 570 removeDataItemFromDisplay(series, item); 571 XYValueMap.clear(); 572 }, 573 new KeyValue(item.currentYProperty(), 574 item.getYValue(), Interpolator.EASE_BOTH), 575 new KeyValue(item.currentXProperty(), 576 item.getXValue(), Interpolator.EASE_BOTH)) 577 ); 578 return t; 579 } 580 581 private Node createSymbol(Series<X, Y> series, int seriesIndex, final Data<X,Y> item, int itemIndex) { 582 Node symbol = item.getNode(); 583 // check if symbol has already been created 584 if (symbol == null && getCreateSymbols()) { 585 symbol = new StackPane(); 586 symbol.setAccessibleRole(AccessibleRole.TEXT); 587 symbol.setAccessibleRoleDescription("Point"); 588 symbol.focusTraversableProperty().bind(Platform.accessibilityActiveProperty()); 589 item.setNode(symbol); 590 } 591 // set symbol styles 592 if (symbol != null) symbol.getStyleClass().addAll("chart-line-symbol", "series" + seriesIndex, 593 "data" + itemIndex, series.defaultColorStyleClass); 594 return symbol; 595 } 596 597 /** 598 * This is called whenever a series is added or removed and the legend needs to be updated 599 */ 600 @Override protected void updateLegend() { 601 legend.getItems().clear(); 602 if (getData() != null) { 603 for (int seriesIndex=0; seriesIndex < getData().size(); seriesIndex++) { 604 Series<X,Y> series = getData().get(seriesIndex); 605 LegendItem legenditem = new LegendItem(series.getName()); 606 legenditem.getSymbol().getStyleClass().addAll("chart-line-symbol", "series"+seriesIndex, series.defaultColorStyleClass); 607 legend.getItems().add(legenditem); 608 } 609 } 610 if (legend.getItems().size() > 0) { 611 if (getLegend() == null) { 612 setLegend(legend); 613 } 614 } else { 615 setLegend(null); 616 } 617 } 618 619 // -------------- STYLESHEET HANDLING -------------------------------------- 620 621 private static class StyleableProperties { 622 private static final CssMetaData<LineChart<?,?>,Boolean> CREATE_SYMBOLS = 623 new CssMetaData<LineChart<?,?>,Boolean>("-fx-create-symbols", 624 BooleanConverter.getInstance(), Boolean.TRUE) { 625 626 @Override 627 public boolean isSettable(LineChart<?,?> node) { 628 return node.createSymbols == null || !node.createSymbols.isBound(); 629 } 630 631 @Override 632 public StyleableProperty<Boolean> getStyleableProperty(LineChart<?,?> node) { 633 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.createSymbolsProperty(); 634 } 635 }; 636 637 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 638 static { 639 final List<CssMetaData<? extends Styleable, ?>> styleables = 640 new ArrayList<CssMetaData<? extends Styleable, ?>>(XYChart.getClassCssMetaData()); 641 styleables.add(CREATE_SYMBOLS); 642 STYLEABLES = Collections.unmodifiableList(styleables); 643 } 644 } 645 646 /** 647 * @return The CssMetaData associated with this class, which may include the 648 * CssMetaData of its super classes. 649 * @since JavaFX 8.0 650 */ 651 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 652 return StyleableProperties.STYLEABLES; 653 } 654 655 /** 656 * {@inheritDoc} 657 * @since JavaFX 8.0 658 */ 659 @Override 660 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 661 return getClassCssMetaData(); 662 } 663 664 /** 665 * This enum defines a policy for {@link LineChart#axisSortingPolicyProperty()}. 666 * @since JavaFX 8u40 667 */ 668 public static enum SortingPolicy { 669 /** 670 * The data should be left in the order defined by the list in {@link javafx.scene.chart.LineChart#dataProperty()}. 671 */ 672 NONE, 673 /** 674 * The data is ordered by x axis. 675 */ 676 X_AXIS, 677 /** 678 * The data is ordered by y axis. 679 */ 680 Y_AXIS 681 } 682 }