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 // dataSize represents size of currently visible data. After this operation, the number will decrement by 1 308 final int dataSize = series.getDataSize(); 309 // This is the size of current data list in Series. Note that it might be totaly different from dataSize as 310 // some big operation might have happened on the list. 311 final int dataListSize = series.getData().size(); 312 if (itemIndex > 0 && itemIndex < dataSize - 1) { 313 animate = true; 314 Data<X,Y> p1 = series.getItem(itemIndex - 1); 315 Data<X,Y> p2 = series.getItem(itemIndex + 1); 316 double x1 = getXAxis().toNumericValue(p1.getXValue()); 317 double y1 = getYAxis().toNumericValue(p1.getYValue()); 318 double x3 = getXAxis().toNumericValue(p2.getXValue()); 319 double y3 = getYAxis().toNumericValue(p2.getYValue()); 320 321 double x2 = getXAxis().toNumericValue(item.getXValue()); 322 double y2 = getYAxis().toNumericValue(item.getYValue()); 323 if (x2 > x1 && x2 < x3) { 324 // //1. y intercept of the line : y = ((y3-y1)/(x3-x1)) * x2 + (x3y1 - y3x1)/(x3 -x1) 325 double y = ((y3-y1)/(x3-x1)) * x2 + (x3*y1 - y3*x1)/(x3-x1); 326 item.setCurrentX(getXAxis().toRealValue(x2)); 327 item.setCurrentY(getYAxis().toRealValue(y2)); 328 item.setXValue(getXAxis().toRealValue(x2)); 329 item.setYValue(getYAxis().toRealValue(y)); 330 } else { 331 //2. we can simply use the midpoint on the line as well.. 332 double x = (x3 + x1)/2; 333 double y = (y3 + y1)/2; 334 item.setCurrentX(getXAxis().toRealValue(x)); 335 item.setCurrentY(getYAxis().toRealValue(y)); 336 } 337 } else if (itemIndex == 0 && dataListSize > 1) { 338 animate = true; 339 item.setXValue(series.getData().get(0).getXValue()); 340 item.setYValue(series.getData().get(0).getYValue()); 341 } else if (itemIndex == (dataSize - 1) && dataListSize > 1) { 342 animate = true; 343 int last = dataListSize - 1; 344 item.setXValue(series.getData().get(last).getXValue()); 345 item.setYValue(series.getData().get(last).getYValue()); 346 } else if (symbol != null) { 347 // fade out symbol 348 fadeSymbolTransition = new FadeTransition(Duration.millis(500),symbol); 349 fadeSymbolTransition.setToValue(0); 350 fadeSymbolTransition.setOnFinished(actionEvent -> { 351 item.setSeries(null); 352 getPlotChildren().remove(symbol); 353 removeDataItemFromDisplay(series, item); 354 symbol.setOpacity(1.0); 355 }); 356 fadeSymbolTransition.play(); 357 } 358 if (animate) { 359 dataRemoveTimeline = createDataRemoveTimeline(item, symbol, series); 360 seriesOfDataRemoved = series; 361 dataItemBeingRemoved = item; 362 dataRemoveTimeline.play(); 363 } 364 } else { 365 item.setSeries(null); 366 if (symbol != null) getPlotChildren().remove(symbol); 367 removeDataItemFromDisplay(series, item); 368 } 369 //Note: better animation here, point should move from old position to new position at center point between prev and next symbols 370 } 371 372 /** @inheritDoc */ 373 @Override protected void dataItemChanged(Data<X, Y> item) { 374 } 375 376 @Override protected void seriesChanged(ListChangeListener.Change<? extends Series> c) { 377 // Update style classes for all series lines and symbols 378 // Note: is there a more efficient way of doing this? 379 for (int i = 0; i < getDataSize(); i++) { 380 final Series<X,Y> s = getData().get(i); 381 Node seriesNode = s.getNode(); 382 if(seriesNode != null) seriesNode.getStyleClass().setAll("chart-series-line", "series" + i, s.defaultColorStyleClass); 383 } 384 } 385 386 @Override protected void seriesAdded(Series<X,Y> series, int seriesIndex) { 387 // create new path for series 388 Path seriesLine = new Path(); 389 seriesLine.setStrokeLineJoin(StrokeLineJoin.BEVEL); 390 series.setNode(seriesLine); 391 // create series Y multiplier 392 DoubleProperty seriesYAnimMultiplier = new SimpleDoubleProperty(this, "seriesYMultiplier"); 393 seriesYMultiplierMap.put(series, seriesYAnimMultiplier); 394 // handle any data already in series 395 if (shouldAnimate()) { 396 seriesLine.setOpacity(0); 397 seriesYAnimMultiplier.setValue(0d); 398 } else { 399 seriesYAnimMultiplier.setValue(1d); 400 } 401 getPlotChildren().add(seriesLine); 402 403 List<KeyFrame> keyFrames = new ArrayList<KeyFrame>(); 404 if (shouldAnimate()) { 405 // animate in new series 406 keyFrames.add(new KeyFrame(Duration.ZERO, 407 new KeyValue(seriesLine.opacityProperty(), 0), 408 new KeyValue(seriesYAnimMultiplier, 0) 409 )); 410 keyFrames.add(new KeyFrame(Duration.millis(200), 411 new KeyValue(seriesLine.opacityProperty(), 1) 412 )); 413 keyFrames.add(new KeyFrame(Duration.millis(500), 414 new KeyValue(seriesYAnimMultiplier, 1) 415 )); 416 } 417 for (int j=0; j<series.getData().size(); j++) { 418 Data<X,Y> item = series.getData().get(j); 419 final Node symbol = createSymbol(series, seriesIndex, item, j); 420 if(symbol != null) { 421 if (shouldAnimate()) symbol.setOpacity(0); 422 getPlotChildren().add(symbol); 423 if (shouldAnimate()) { 424 // fade in new symbol 425 keyFrames.add(new KeyFrame(Duration.ZERO, new KeyValue(symbol.opacityProperty(), 0))); 426 keyFrames.add(new KeyFrame(Duration.millis(200), new KeyValue(symbol.opacityProperty(), 1))); 427 } 428 } 429 } 430 if (shouldAnimate()) animate(keyFrames.toArray(new KeyFrame[keyFrames.size()])); 431 } 432 private void updateDefaultColorIndex(final Series<X,Y> series) { 433 int clearIndex = seriesColorMap.get(series); 434 series.getNode().getStyleClass().remove(DEFAULT_COLOR+clearIndex); 435 for (int j=0; j < series.getData().size(); j++) { 436 final Node node = series.getData().get(j).getNode(); 437 if(node!=null) { 438 node.getStyleClass().remove(DEFAULT_COLOR+clearIndex); 439 } 440 } 441 } 442 443 @Override protected void seriesRemoved(final Series<X,Y> series) { 444 updateDefaultColorIndex(series); 445 // remove all symbol nodes 446 seriesYMultiplierMap.remove(series); 447 if (shouldAnimate()) { 448 seriesRemoveTimeline = new Timeline(createSeriesRemoveTimeLine(series, 900)); 449 seriesRemoveTimeline.play(); 450 } else { 451 getPlotChildren().remove(series.getNode()); 452 for (Data<X,Y> d:series.getData()) getPlotChildren().remove(d.getNode()); 453 removeSeriesFromDisplay(series); 454 } 455 } 456 457 /** @inheritDoc */ 458 @Override protected void layoutPlotChildren() { 459 List<LineTo> constructedPath = new ArrayList<>(getDataSize()); 460 for (int seriesIndex=0; seriesIndex < getDataSize(); seriesIndex++) { 461 Series<X,Y> series = getData().get(seriesIndex); 462 final DoubleProperty seriesYAnimMultiplier = seriesYMultiplierMap.get(series); 463 if(series.getNode() instanceof Path) { 464 final ObservableList<PathElement> seriesLine = ((Path)series.getNode()).getElements(); 465 seriesLine.clear(); 466 constructedPath.clear(); 467 for (Iterator<Data<X, Y>> it = getDisplayedDataIterator(series); it.hasNext(); ) { 468 Data<X, Y> item = it.next(); 469 double x = getXAxis().getDisplayPosition(item.getCurrentX()); 470 double y = getYAxis().getDisplayPosition( 471 getYAxis().toRealValue(getYAxis().toNumericValue(item.getCurrentY()) * seriesYAnimMultiplier.getValue())); 472 if (Double.isNaN(x) || Double.isNaN(y)) { 473 continue; 474 } 475 constructedPath.add(new LineTo(x, y)); 476 477 Node symbol = item.getNode(); 478 if (symbol != null) { 479 final double w = symbol.prefWidth(-1); 480 final double h = symbol.prefHeight(-1); 481 symbol.resizeRelocate(x-(w/2), y-(h/2),w,h); 482 } 483 } 484 switch (getAxisSortingPolicy()) { 485 case X_AXIS: 486 Collections.sort(constructedPath, (e1, e2) -> Double.compare(e1.getX(), e2.getX())); 487 break; 488 case Y_AXIS: 489 Collections.sort(constructedPath, (e1, e2) -> Double.compare(e1.getY(), e2.getY())); 490 break; 491 } 492 493 if (!constructedPath.isEmpty()) { 494 LineTo first = constructedPath.get(0); 495 seriesLine.add(new MoveTo(first.getX(), first.getY())); 496 seriesLine.addAll(constructedPath); 497 } 498 } 499 } 500 } 501 /** @inheritDoc */ 502 @Override void dataBeingRemovedIsAdded(Data item, Series series) { 503 if (fadeSymbolTransition != null) { 504 fadeSymbolTransition.setOnFinished(null); 505 fadeSymbolTransition.stop(); 506 } 507 if (dataRemoveTimeline != null) { 508 dataRemoveTimeline.setOnFinished(null); 509 dataRemoveTimeline.stop(); 510 } 511 final Node symbol = item.getNode(); 512 if (symbol != null) getPlotChildren().remove(symbol); 513 514 item.setSeries(null); 515 removeDataItemFromDisplay(series, item); 516 517 // restore values to item 518 Double value = XYValueMap.get(item); 519 if (value != null) { 520 item.setYValue(value); 521 item.setCurrentY(value); 522 } 523 XYValueMap.clear(); 524 } 525 /** @inheritDoc */ 526 @Override void seriesBeingRemovedIsAdded(Series<X,Y> series) { 527 if (seriesRemoveTimeline != null) { 528 seriesRemoveTimeline.setOnFinished(null); 529 seriesRemoveTimeline.stop(); 530 getPlotChildren().remove(series.getNode()); 531 for (Data<X,Y> d:series.getData()) getPlotChildren().remove(d.getNode()); 532 removeSeriesFromDisplay(series); 533 } 534 } 535 536 private Timeline createDataRemoveTimeline(final Data<X,Y> item, final Node symbol, final Series<X,Y> series) { 537 Timeline t = new Timeline(); 538 // save data values in case the same data item gets added immediately. 539 XYValueMap.put(item, ((Number)item.getYValue()).doubleValue()); 540 541 t.getKeyFrames().addAll(new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(), 542 item.getCurrentY()), new KeyValue(item.currentXProperty(), 543 item.getCurrentX())), 544 new KeyFrame(Duration.millis(500), actionEvent -> { 545 if (symbol != null) getPlotChildren().remove(symbol); 546 removeDataItemFromDisplay(series, item); 547 XYValueMap.clear(); 548 }, 549 new KeyValue(item.currentYProperty(), 550 item.getYValue(), Interpolator.EASE_BOTH), 551 new KeyValue(item.currentXProperty(), 552 item.getXValue(), Interpolator.EASE_BOTH)) 553 ); 554 return t; 555 } 556 557 private Node createSymbol(Series<X, Y> series, int seriesIndex, final Data<X,Y> item, int itemIndex) { 558 Node symbol = item.getNode(); 559 // check if symbol has already been created 560 if (symbol == null && getCreateSymbols()) { 561 symbol = new StackPane(); 562 symbol.setAccessibleRole(AccessibleRole.TEXT); 563 symbol.setAccessibleRoleDescription("Point"); 564 symbol.focusTraversableProperty().bind(Platform.accessibilityActiveProperty()); 565 item.setNode(symbol); 566 } 567 // set symbol styles 568 if (symbol != null) symbol.getStyleClass().addAll("chart-line-symbol", "series" + seriesIndex, 569 "data" + itemIndex, series.defaultColorStyleClass); 570 return symbol; 571 } 572 573 /** 574 * This is called whenever a series is added or removed and the legend needs to be updated 575 */ 576 @Override protected void updateLegend() { 577 legend.getItems().clear(); 578 if (getData() != null) { 579 for (int seriesIndex=0; seriesIndex < getData().size(); seriesIndex++) { 580 Series<X,Y> series = getData().get(seriesIndex); 581 LegendItem legenditem = new LegendItem(series.getName()); 582 legenditem.getSymbol().getStyleClass().addAll("chart-line-symbol", "series"+seriesIndex, series.defaultColorStyleClass); 583 legend.getItems().add(legenditem); 584 } 585 } 586 if (legend.getItems().size() > 0) { 587 if (getLegend() == null) { 588 setLegend(legend); 589 } 590 } else { 591 setLegend(null); 592 } 593 } 594 595 // -------------- STYLESHEET HANDLING -------------------------------------- 596 597 private static class StyleableProperties { 598 private static final CssMetaData<LineChart<?,?>,Boolean> CREATE_SYMBOLS = 599 new CssMetaData<LineChart<?,?>,Boolean>("-fx-create-symbols", 600 BooleanConverter.getInstance(), Boolean.TRUE) { 601 602 @Override 603 public boolean isSettable(LineChart<?,?> node) { 604 return node.createSymbols == null || !node.createSymbols.isBound(); 605 } 606 607 @Override 608 public StyleableProperty<Boolean> getStyleableProperty(LineChart<?,?> node) { 609 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.createSymbolsProperty(); 610 } 611 }; 612 613 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 614 static { 615 final List<CssMetaData<? extends Styleable, ?>> styleables = 616 new ArrayList<CssMetaData<? extends Styleable, ?>>(XYChart.getClassCssMetaData()); 617 styleables.add(CREATE_SYMBOLS); 618 STYLEABLES = Collections.unmodifiableList(styleables); 619 } 620 } 621 622 /** 623 * @return The CssMetaData associated with this class, which may include the 624 * CssMetaData of its super classes. 625 * @since JavaFX 8.0 626 */ 627 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 628 return StyleableProperties.STYLEABLES; 629 } 630 631 /** 632 * {@inheritDoc} 633 * @since JavaFX 8.0 634 */ 635 @Override 636 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 637 return getClassCssMetaData(); 638 } 639 640 /** 641 * This enum defines a policy for {@link LineChart#axisSortingPolicyProperty()}. 642 * @since JavaFX 8u40 643 */ 644 public static enum SortingPolicy { 645 /** 646 * The data should be left in the order defined by the list in {@link javafx.scene.chart.LineChart#dataProperty()}. 647 */ 648 NONE, 649 /** 650 * The data is ordered by x axis. 651 */ 652 X_AXIS, 653 /** 654 * The data is ordered by y axis. 655 */ 656 Y_AXIS 657 } 658 }