1 /* 2 * Copyright (c) 2010, 2014, 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.ArrayList; 30 import java.util.BitSet; 31 import java.util.Collections; 32 import java.util.HashMap; 33 import java.util.HashSet; 34 import java.util.Iterator; 35 import java.util.List; 36 import java.util.Map; 37 import java.util.Set; 38 39 import javafx.animation.Interpolator; 40 import javafx.animation.KeyFrame; 41 import javafx.animation.KeyValue; 42 import javafx.beans.binding.StringBinding; 43 import javafx.beans.property.BooleanProperty; 44 import javafx.beans.property.ObjectProperty; 45 import javafx.beans.property.ObjectPropertyBase; 46 import javafx.beans.property.ReadOnlyObjectProperty; 47 import javafx.beans.property.ReadOnlyObjectWrapper; 48 import javafx.beans.property.SimpleObjectProperty; 49 import javafx.beans.property.StringProperty; 50 import javafx.beans.property.StringPropertyBase; 51 import javafx.beans.value.WritableValue; 52 import javafx.collections.FXCollections; 53 import javafx.collections.ListChangeListener; 54 import javafx.collections.ListChangeListener.Change; 55 import javafx.collections.ObservableList; 56 import javafx.css.CssMetaData; 57 import javafx.css.Styleable; 58 import javafx.css.StyleableBooleanProperty; 59 import javafx.css.StyleableProperty; 60 import javafx.geometry.Orientation; 61 import javafx.geometry.Side; 62 import javafx.scene.Group; 63 import javafx.scene.Node; 64 import javafx.scene.layout.Region; 65 import javafx.scene.shape.ClosePath; 66 import javafx.scene.shape.Line; 67 import javafx.scene.shape.LineTo; 68 import javafx.scene.shape.MoveTo; 69 import javafx.scene.shape.Path; 70 import javafx.scene.shape.Rectangle; 71 import javafx.util.Duration; 72 73 import com.sun.javafx.collections.NonIterableChange; 74 import com.sun.javafx.css.converters.BooleanConverter; 75 76 77 78 /** 79 * Chart base class for all 2 axis charts. It is responsible for drawing the two 80 * axes and the plot content. It contains a list of all content in the plot and 81 * implementations of XYChart can add nodes to this list that need to be rendered. 82 * 83 * <p>It is possible to install Tooltips on data items / symbols. 84 * For example the following code snippet installs Tooltip on the 1st data item. 85 * 86 * <pre><code> 87 * XYChart.Data item = ( XYChart.Data)series.getData().get(0); 88 * Tooltip.install(item.getNode(), new Tooltip("Symbol-0")); 89 * </code></pre> 90 * 91 * @since JavaFX 2.0 92 */ 93 public abstract class XYChart<X,Y> extends Chart { 94 95 // -------------- PRIVATE FIELDS ----------------------------------------------------------------------------------- 96 97 // to indicate which colors are being used for the series 98 private final BitSet colorBits = new BitSet(8); 99 static String DEFAULT_COLOR = "default-color"; 100 final Map<Series<X,Y>, Integer> seriesColorMap = new HashMap<>(); 101 private boolean rangeValid = false; 102 private final Line verticalZeroLine = new Line(); 103 private final Line horizontalZeroLine = new Line(); 104 private final Path verticalGridLines = new Path(); 105 private final Path horizontalGridLines = new Path(); 106 private final Path horizontalRowFill = new Path(); 107 private final Path verticalRowFill = new Path(); 108 private final Region plotBackground = new Region(); 109 private final Group plotArea = new Group(){ 110 @Override public void requestLayout() {} // suppress layout requests 111 }; 112 private final Group plotContent = new Group(); 113 private final Rectangle plotAreaClip = new Rectangle(); 114 115 private final List<Series<X, Y>> displayedSeries = new ArrayList<>(); 116 117 /** This is called when a series is added or removed from the chart */ 118 private final ListChangeListener<Series<X,Y>> seriesChanged = c -> { 119 ObservableList<? extends Series<X, Y>> series = c.getList(); 120 while (c.next()) { 121 // RT-12069, linked list pointers should update when list is permutated. 122 if (c.wasPermutated()) { 123 displayedSeries.sort((o1, o2) -> series.indexOf(o2) - series.indexOf(o1)); 124 125 } 126 127 if (c.getRemoved().size() > 0) updateLegend(); 128 129 Set<Series<X, Y>> dupCheck = new HashSet<>(displayedSeries); 130 dupCheck.removeAll(c.getRemoved()); 131 for (Series<X, Y> d : c.getAddedSubList()) { 132 if (!dupCheck.add(d)) { 133 throw new IllegalArgumentException("Duplicate series added"); 134 } 135 } 136 137 for (Series<X,Y> s : c.getRemoved()) { 138 s.setToRemove = true; 139 seriesRemoved(s); 140 int idx = seriesColorMap.remove(s); 141 colorBits.clear(idx); 142 } 143 144 for(int i=c.getFrom(); i<c.getTo() && !c.wasPermutated(); i++) { 145 final Series<X,Y> s = c.getList().get(i); 146 // add new listener to data 147 s.setChart(XYChart.this); 148 if (s.setToRemove) { 149 s.setToRemove = false; 150 s.getChart().seriesBeingRemovedIsAdded(s); 151 } 152 // update linkedList Pointers for series 153 displayedSeries.add(s); 154 // update default color style class 155 int nextClearBit = colorBits.nextClearBit(0); 156 colorBits.set(nextClearBit, true); 157 s.defaultColorStyleClass = DEFAULT_COLOR+(nextClearBit%8); 158 seriesColorMap.put(s, nextClearBit%8); 159 // inform sub-classes of series added 160 seriesAdded(s, i); 161 } 162 if (c.getFrom() < c.getTo()) updateLegend(); 163 seriesChanged(c); 164 165 } 166 // update axis ranges 167 invalidateRange(); 168 // lay everything out 169 requestChartLayout(); 170 }; 171 172 // -------------- PUBLIC PROPERTIES -------------------------------------------------------------------------------- 173 174 private final Axis<X> xAxis; 175 /** Get the X axis, by default it is along the bottom of the plot */ 176 public Axis<X> getXAxis() { return xAxis; } 177 178 private final Axis<Y> yAxis; 179 /** Get the Y axis, by default it is along the left of the plot */ 180 public Axis<Y> getYAxis() { return yAxis; } 181 182 /** XYCharts data */ 183 private ObjectProperty<ObservableList<Series<X,Y>>> data = new ObjectPropertyBase<ObservableList<Series<X,Y>>>() { 184 private ObservableList<Series<X,Y>> old; 185 @Override protected void invalidated() { 186 final ObservableList<Series<X,Y>> current = getValue(); 187 int saveAnimationState = -1; 188 // add remove listeners 189 if(old != null) { 190 old.removeListener(seriesChanged); 191 // Set animated to false so we don't animate both remove and add 192 // at the same time. RT-14163 193 // RT-21295 - disable animated only when current is also not null. 194 if (current != null && old.size() > 0) { 195 saveAnimationState = (old.get(0).getChart().getAnimated()) ? 1 : 2; 196 old.get(0).getChart().setAnimated(false); 197 } 198 } 199 if(current != null) current.addListener(seriesChanged); 200 // fire series change event if series are added or removed 201 if(old != null || current != null) { 202 final List<Series<X,Y>> removed = (old != null) ? old : Collections.<Series<X,Y>>emptyList(); 203 final int toIndex = (current != null) ? current.size() : 0; 204 // let series listener know all old series have been removed and new that have been added 205 if (toIndex > 0 || !removed.isEmpty()) { 206 seriesChanged.onChanged(new NonIterableChange<Series<X,Y>>(0, toIndex, current){ 207 @Override public List<Series<X,Y>> getRemoved() { return removed; } 208 @Override protected int[] getPermutation() { 209 return new int[0]; 210 } 211 }); 212 } 213 } else if (old != null && old.size() > 0) { 214 // let series listener know all old series have been removed 215 seriesChanged.onChanged(new NonIterableChange<Series<X,Y>>(0, 0, current){ 216 @Override public List<Series<X,Y>> getRemoved() { return old; } 217 @Override protected int[] getPermutation() { 218 return new int[0]; 219 } 220 }); 221 } 222 // restore animated on chart. 223 if (current != null && current.size() > 0 && saveAnimationState != -1) { 224 current.get(0).getChart().setAnimated((saveAnimationState == 1) ? true : false); 225 } 226 old = current; 227 } 228 229 public Object getBean() { 230 return XYChart.this; 231 } 232 233 public String getName() { 234 return "data"; 235 } 236 }; 237 public final ObservableList<Series<X,Y>> getData() { return data.getValue(); } 238 public final void setData(ObservableList<Series<X,Y>> value) { data.setValue(value); } 239 public final ObjectProperty<ObservableList<Series<X,Y>>> dataProperty() { return data; } 240 241 /** True if vertical grid lines should be drawn */ 242 private BooleanProperty verticalGridLinesVisible = new StyleableBooleanProperty(true) { 243 @Override protected void invalidated() { 244 requestChartLayout(); 245 } 246 247 @Override 248 public Object getBean() { 249 return XYChart.this; 250 } 251 252 @Override 253 public String getName() { 254 return "verticalGridLinesVisible"; 255 } 256 257 @Override 258 public CssMetaData<XYChart<?,?>,Boolean> getCssMetaData() { 259 return StyleableProperties.VERTICAL_GRID_LINE_VISIBLE; 260 } 261 }; 262 /** 263 * Indicates whether vertical grid lines are visible or not. 264 * 265 * @return true if verticalGridLines are visible else false. 266 * @see #verticalGridLinesVisible 267 */ 268 public final boolean getVerticalGridLinesVisible() { return verticalGridLinesVisible.get(); } 269 public final void setVerticalGridLinesVisible(boolean value) { verticalGridLinesVisible.set(value); } 270 public final BooleanProperty verticalGridLinesVisibleProperty() { return verticalGridLinesVisible; } 271 272 /** True if horizontal grid lines should be drawn */ 273 private BooleanProperty horizontalGridLinesVisible = new StyleableBooleanProperty(true) { 274 @Override protected void invalidated() { 275 requestChartLayout(); 276 } 277 278 @Override 279 public Object getBean() { 280 return XYChart.this; 281 } 282 283 @Override 284 public String getName() { 285 return "horizontalGridLinesVisible"; 286 } 287 288 @Override 289 public CssMetaData<XYChart<?,?>,Boolean> getCssMetaData() { 290 return StyleableProperties.HORIZONTAL_GRID_LINE_VISIBLE; 291 } 292 }; 293 public final boolean isHorizontalGridLinesVisible() { return horizontalGridLinesVisible.get(); } 294 public final void setHorizontalGridLinesVisible(boolean value) { horizontalGridLinesVisible.set(value); } 295 public final BooleanProperty horizontalGridLinesVisibleProperty() { return horizontalGridLinesVisible; } 296 297 /** If true then alternative vertical columns will have fills */ 298 private BooleanProperty alternativeColumnFillVisible = new StyleableBooleanProperty(false) { 299 @Override protected void invalidated() { 300 requestChartLayout(); 301 } 302 303 @Override 304 public Object getBean() { 305 return XYChart.this; 306 } 307 308 @Override 309 public String getName() { 310 return "alternativeColumnFillVisible"; 311 } 312 313 @Override 314 public CssMetaData<XYChart<?,?>,Boolean> getCssMetaData() { 315 return StyleableProperties.ALTERNATIVE_COLUMN_FILL_VISIBLE; 316 } 317 }; 318 public final boolean isAlternativeColumnFillVisible() { return alternativeColumnFillVisible.getValue(); } 319 public final void setAlternativeColumnFillVisible(boolean value) { alternativeColumnFillVisible.setValue(value); } 320 public final BooleanProperty alternativeColumnFillVisibleProperty() { return alternativeColumnFillVisible; } 321 322 /** If true then alternative horizontal rows will have fills */ 323 private BooleanProperty alternativeRowFillVisible = new StyleableBooleanProperty(true) { 324 @Override protected void invalidated() { 325 requestChartLayout(); 326 } 327 328 @Override 329 public Object getBean() { 330 return XYChart.this; 331 } 332 333 @Override 334 public String getName() { 335 return "alternativeRowFillVisible"; 336 } 337 338 @Override 339 public CssMetaData<XYChart<?,?>,Boolean> getCssMetaData() { 340 return StyleableProperties.ALTERNATIVE_ROW_FILL_VISIBLE; 341 } 342 }; 343 public final boolean isAlternativeRowFillVisible() { return alternativeRowFillVisible.getValue(); } 344 public final void setAlternativeRowFillVisible(boolean value) { alternativeRowFillVisible.setValue(value); } 345 public final BooleanProperty alternativeRowFillVisibleProperty() { return alternativeRowFillVisible; } 346 347 /** 348 * If this is true and the vertical axis has both positive and negative values then a additional axis line 349 * will be drawn at the zero point 350 * 351 * @defaultValue true 352 */ 353 private BooleanProperty verticalZeroLineVisible = new StyleableBooleanProperty(true) { 354 @Override protected void invalidated() { 355 requestChartLayout(); 356 } 357 358 @Override 359 public Object getBean() { 360 return XYChart.this; 361 } 362 363 @Override 364 public String getName() { 365 return "verticalZeroLineVisible"; 366 } 367 368 @Override 369 public CssMetaData<XYChart<?,?>,Boolean> getCssMetaData() { 370 return StyleableProperties.VERTICAL_ZERO_LINE_VISIBLE; 371 } 372 }; 373 public final boolean isVerticalZeroLineVisible() { return verticalZeroLineVisible.get(); } 374 public final void setVerticalZeroLineVisible(boolean value) { verticalZeroLineVisible.set(value); } 375 public final BooleanProperty verticalZeroLineVisibleProperty() { return verticalZeroLineVisible; } 376 377 /** 378 * If this is true and the horizontal axis has both positive and negative values then a additional axis line 379 * will be drawn at the zero point 380 * 381 * @defaultValue true 382 */ 383 private BooleanProperty horizontalZeroLineVisible = new StyleableBooleanProperty(true) { 384 @Override protected void invalidated() { 385 requestChartLayout(); 386 } 387 388 @Override 389 public Object getBean() { 390 return XYChart.this; 391 } 392 393 @Override 394 public String getName() { 395 return "horizontalZeroLineVisible"; 396 } 397 398 @Override 399 public CssMetaData<XYChart<?,?>,Boolean> getCssMetaData() { 400 return StyleableProperties.HORIZONTAL_ZERO_LINE_VISIBLE; 401 } 402 }; 403 public final boolean isHorizontalZeroLineVisible() { return horizontalZeroLineVisible.get(); } 404 public final void setHorizontalZeroLineVisible(boolean value) { horizontalZeroLineVisible.set(value); } 405 public final BooleanProperty horizontalZeroLineVisibleProperty() { return horizontalZeroLineVisible; } 406 407 // -------------- PROTECTED PROPERTIES ----------------------------------------------------------------------------- 408 409 /** 410 * Modifiable and observable list of all content in the plot. This is where implementations of XYChart should add 411 * any nodes they use to draw their plot. 412 * 413 * @return Observable list of plot children 414 */ 415 protected ObservableList<Node> getPlotChildren() { 416 return plotContent.getChildren(); 417 } 418 419 // -------------- CONSTRUCTOR -------------------------------------------------------------------------------------- 420 421 /** 422 * Constructs a XYChart given the two axes. The initial content for the chart 423 * plot background and plot area that includes vertical and horizontal grid 424 * lines and fills, are added. 425 * 426 * @param xAxis X Axis for this XY chart 427 * @param yAxis Y Axis for this XY chart 428 */ 429 public XYChart(Axis<X> xAxis, Axis<Y> yAxis) { 430 this.xAxis = xAxis; 431 if (xAxis.getSide() == null) xAxis.setSide(Side.BOTTOM); 432 xAxis.setEffectiveOrientation(Orientation.HORIZONTAL); 433 this.yAxis = yAxis; 434 if (yAxis.getSide() == null) yAxis.setSide(Side.LEFT); 435 yAxis.setEffectiveOrientation(Orientation.VERTICAL); 436 // RT-23123 autoranging leads to charts incorrect appearance. 437 xAxis.autoRangingProperty().addListener((ov, t, t1) -> { 438 updateAxisRange(); 439 }); 440 yAxis.autoRangingProperty().addListener((ov, t, t1) -> { 441 updateAxisRange(); 442 }); 443 // add initial content to chart content 444 getChartChildren().addAll(plotBackground,plotArea,xAxis,yAxis); 445 // We don't want plotArea or plotContent to autoSize or do layout 446 plotArea.setAutoSizeChildren(false); 447 plotContent.setAutoSizeChildren(false); 448 // setup clipping on plot area 449 plotAreaClip.setSmooth(false); 450 plotArea.setClip(plotAreaClip); 451 // add children to plot area 452 plotArea.getChildren().addAll( 453 verticalRowFill, horizontalRowFill, 454 verticalGridLines, horizontalGridLines, 455 verticalZeroLine, horizontalZeroLine, 456 plotContent); 457 // setup css style classes 458 plotContent.getStyleClass().setAll("plot-content"); 459 plotBackground.getStyleClass().setAll("chart-plot-background"); 460 verticalRowFill.getStyleClass().setAll("chart-alternative-column-fill"); 461 horizontalRowFill.getStyleClass().setAll("chart-alternative-row-fill"); 462 verticalGridLines.getStyleClass().setAll("chart-vertical-grid-lines"); 463 horizontalGridLines.getStyleClass().setAll("chart-horizontal-grid-lines"); 464 verticalZeroLine.getStyleClass().setAll("chart-vertical-zero-line"); 465 horizontalZeroLine.getStyleClass().setAll("chart-horizontal-zero-line"); 466 // mark plotContent as unmanaged as its preferred size changes do not effect our layout 467 plotContent.setManaged(false); 468 plotArea.setManaged(false); 469 // listen to animation on/off and sync to axis 470 animatedProperty().addListener((valueModel, oldValue, newValue) -> { 471 if(getXAxis() != null) getXAxis().setAnimated(newValue); 472 if(getYAxis() != null) getYAxis().setAnimated(newValue); 473 }); 474 } 475 476 // -------------- METHODS ------------------------------------------------------------------------------------------ 477 478 /** 479 * Gets the size of the data returning 0 if the data is null 480 * 481 * @return The number of items in data, or null if data is null 482 */ 483 final int getDataSize() { 484 final ObservableList<Series<X,Y>> data = getData(); 485 return (data!=null) ? data.size() : 0; 486 } 487 488 /** Called when a series's name has changed */ 489 private void seriesNameChanged() { 490 updateLegend(); 491 requestChartLayout(); 492 } 493 494 @SuppressWarnings({"UnusedParameters"}) 495 private void dataItemsChanged(Series<X,Y> series, List<Data<X,Y>> removed, int addedFrom, int addedTo, boolean permutation) { 496 for (Data<X,Y> item : removed) { 497 dataItemRemoved(item, series); 498 } 499 for(int i=addedFrom; i<addedTo; i++) { 500 Data<X,Y> item = series.getData().get(i); 501 dataItemAdded(series, i, item); 502 } 503 invalidateRange(); 504 requestChartLayout(); 505 } 506 507 private void dataXValueChanged(Data<X,Y> item) { 508 if(item.getCurrentX() != item.getXValue()) invalidateRange(); 509 dataItemChanged(item); 510 if (shouldAnimate()) { 511 animate( 512 new KeyFrame(Duration.ZERO, new KeyValue(item.currentXProperty(), item.getCurrentX())), 513 new KeyFrame(Duration.millis(700), new KeyValue(item.currentXProperty(), item.getXValue(), Interpolator.EASE_BOTH)) 514 ); 515 } else { 516 item.setCurrentX(item.getXValue()); 517 requestChartLayout(); 518 } 519 } 520 521 private void dataYValueChanged(Data<X,Y> item) { 522 if(item.getCurrentY() != item.getYValue()) invalidateRange(); 523 dataItemChanged(item); 524 if (shouldAnimate()) { 525 animate( 526 new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(), item.getCurrentY())), 527 new KeyFrame(Duration.millis(700), new KeyValue(item.currentYProperty(), item.getYValue(), Interpolator.EASE_BOTH)) 528 ); 529 } else { 530 item.setCurrentY(item.getYValue()); 531 requestChartLayout(); 532 } 533 } 534 535 private void dataExtraValueChanged(Data<X,Y> item) { 536 if(item.getCurrentY() != item.getYValue()) invalidateRange(); 537 dataItemChanged(item); 538 if (shouldAnimate()) { 539 animate( 540 new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(), item.getCurrentY())), 541 new KeyFrame(Duration.millis(700), new KeyValue(item.currentYProperty(), item.getYValue(), Interpolator.EASE_BOTH)) 542 ); 543 } else { 544 item.setCurrentY(item.getYValue()); 545 requestChartLayout(); 546 } 547 } 548 549 /** 550 * This is called whenever a series is added or removed and the legend needs to be updated 551 */ 552 protected void updateLegend(){} 553 554 /** 555 * This method is called when there is an attempt to add series that was 556 * set to be removed, and the removal might not have completed. 557 * @param series 558 */ 559 void seriesBeingRemovedIsAdded(Series<X,Y> series) {} 560 561 /** 562 * This method is called when there is an attempt to add a Data item that was 563 * set to be removed, and the removal might not have completed. 564 * @param data 565 */ 566 void dataBeingRemovedIsAdded(Data<X,Y> item, Series<X,Y> series) {} 567 /** 568 * Called when a data item has been added to a series. This is where implementations of XYChart can create/add new 569 * nodes to getPlotChildren to represent this data item. They also may animate that data add with a fade in or 570 * similar if animated = true. 571 * 572 * @param series The series the data item was added to 573 * @param itemIndex The index of the new item within the series 574 * @param item The new data item that was added 575 */ 576 protected abstract void dataItemAdded(Series<X,Y> series, int itemIndex, Data<X,Y> item); 577 578 /** 579 * Called when a data item has been removed from data model but it is still visible on the chart. Its still visible 580 * so that you can handle animation for removing it in this method. After you are done animating the data item you 581 * must call removeDataItemFromDisplay() to remove the items node from being displayed on the chart. 582 * 583 * @param item The item that has been removed from the series 584 * @param series The series the item was removed from 585 */ 586 protected abstract void dataItemRemoved(Data<X, Y> item, Series<X, Y> series); 587 588 /** 589 * Called when a data item has changed, ie its xValue, yValue or extraValue has changed. 590 * 591 * @param item The data item who was changed 592 */ 593 protected abstract void dataItemChanged(Data<X, Y> item); 594 /** 595 * A series has been added to the charts data model. This is where implementations of XYChart can create/add new 596 * nodes to getPlotChildren to represent this series. Also you have to handle adding any data items that are 597 * already in the series. You may simply call dataItemAdded() for each one or provide some different animation for 598 * a whole series being added. 599 * 600 * @param series The series that has been added 601 * @param seriesIndex The index of the new series 602 */ 603 protected abstract void seriesAdded(Series<X, Y> series, int seriesIndex); 604 605 /** 606 * A series has been removed from the data model but it is still visible on the chart. Its still visible 607 * so that you can handle animation for removing it in this method. After you are done animating the data item you 608 * must call removeSeriesFromDisplay() to remove the series from the display list. 609 * 610 * @param series The series that has been removed 611 */ 612 protected abstract void seriesRemoved(Series<X,Y> series); 613 614 /** Called when each atomic change is made to the list of series for this chart */ 615 protected void seriesChanged(Change<? extends Series> c) {} 616 617 /** 618 * This is called when a data change has happened that may cause the range to be invalid. 619 */ 620 private void invalidateRange() { 621 rangeValid = false; 622 } 623 624 /** 625 * This is called when the range has been invalidated and we need to update it. If the axis are auto 626 * ranging then we compile a list of all data that the given axis has to plot and call invalidateRange() on the 627 * axis passing it that data. 628 */ 629 protected void updateAxisRange() { 630 final Axis<X> xa = getXAxis(); 631 final Axis<Y> ya = getYAxis(); 632 List<X> xData = null; 633 List<Y> yData = null; 634 if(xa.isAutoRanging()) xData = new ArrayList<X>(); 635 if(ya.isAutoRanging()) yData = new ArrayList<Y>(); 636 if(xData != null || yData != null) { 637 for(Series<X,Y> series : getData()) { 638 for(Data<X,Y> data: series.getData()) { 639 if(xData != null) xData.add(data.getXValue()); 640 if(yData != null) yData.add(data.getYValue()); 641 } 642 } 643 if(xData != null) xa.invalidateRange(xData); 644 if(yData != null) ya.invalidateRange(yData); 645 } 646 } 647 648 /** 649 * Called to update and layout the plot children. This should include all work to updates nodes representing 650 * the plot on top of the axis and grid lines etc. The origin is the top left of the plot area, the plot area with 651 * can be got by getting the width of the x axis and its height from the height of the y axis. 652 */ 653 protected abstract void layoutPlotChildren(); 654 655 /** @inheritDoc */ 656 @Override protected final void layoutChartChildren(double top, double left, double width, double height) { 657 if(getData() == null) return; 658 if (!rangeValid) { 659 rangeValid = true; 660 if(getData() != null) updateAxisRange(); 661 } 662 // snap top and left to pixels 663 top = snapPosition(top); 664 left = snapPosition(left); 665 // get starting stuff 666 final Axis<X> xa = getXAxis(); 667 final ObservableList<Axis.TickMark<X>> xaTickMarks = xa.getTickMarks(); 668 final Axis<Y> ya = getYAxis(); 669 final ObservableList<Axis.TickMark<Y>> yaTickMarks = ya.getTickMarks(); 670 // check we have 2 axises and know their sides 671 if (xa == null || ya == null) return; 672 // try and work out width and height of axises 673 double xAxisWidth = 0; 674 double xAxisHeight = 30; // guess x axis height to start with 675 double yAxisWidth = 0; 676 double yAxisHeight = 0; 677 for (int count=0; count<5; count ++) { 678 yAxisHeight = snapSize(height - xAxisHeight); 679 if (yAxisHeight < 0) { 680 yAxisHeight = 0; 681 } 682 yAxisWidth = ya.prefWidth(yAxisHeight); 683 xAxisWidth = snapSize(width - yAxisWidth); 684 if (xAxisWidth < 0) { 685 xAxisWidth = 0; 686 } 687 double newXAxisHeight = xa.prefHeight(xAxisWidth); 688 if (newXAxisHeight == xAxisHeight) break; 689 xAxisHeight = newXAxisHeight; 690 } 691 // round axis sizes up to whole integers to snap to pixel 692 xAxisWidth = Math.ceil(xAxisWidth); 693 xAxisHeight = Math.ceil(xAxisHeight); 694 yAxisWidth = Math.ceil(yAxisWidth); 695 yAxisHeight = Math.ceil(yAxisHeight); 696 // calc xAxis height 697 double xAxisY = 0; 698 switch(xa.getEffectiveSide()) { 699 case TOP: 700 xa.setVisible(true); 701 xAxisY = top+1; 702 top += xAxisHeight; 703 break; 704 case BOTTOM: 705 xa.setVisible(true); 706 xAxisY = top + yAxisHeight; 707 } 708 709 // calc yAxis width 710 double yAxisX = 0; 711 switch(ya.getEffectiveSide()) { 712 case LEFT: 713 ya.setVisible(true); 714 yAxisX = left +1; 715 left += yAxisWidth; 716 break; 717 case RIGHT: 718 ya.setVisible(true); 719 yAxisX = left + xAxisWidth; 720 } 721 // resize axises 722 xa.resizeRelocate(left, xAxisY, xAxisWidth, xAxisHeight); 723 ya.resizeRelocate(yAxisX, top, yAxisWidth, yAxisHeight); 724 // When the chart is resized, need to specifically call out the axises 725 // to lay out as they are unmanaged. 726 xa.requestAxisLayout(); 727 xa.layout(); 728 ya.requestAxisLayout(); 729 ya.layout(); 730 // layout plot content 731 layoutPlotChildren(); 732 // get axis zero points 733 final double xAxisZero = xa.getZeroPosition(); 734 final double yAxisZero = ya.getZeroPosition(); 735 // position vertical and horizontal zero lines 736 if(Double.isNaN(xAxisZero) || !isVerticalZeroLineVisible()) { 737 verticalZeroLine.setVisible(false); 738 } else { 739 verticalZeroLine.setStartX(left+xAxisZero+0.5); 740 verticalZeroLine.setStartY(top); 741 verticalZeroLine.setEndX(left+xAxisZero+0.5); 742 verticalZeroLine.setEndY(top+yAxisHeight); 743 verticalZeroLine.setVisible(true); 744 } 745 if(Double.isNaN(yAxisZero) || !isHorizontalZeroLineVisible()) { 746 horizontalZeroLine.setVisible(false); 747 } else { 748 horizontalZeroLine.setStartX(left); 749 horizontalZeroLine.setStartY(top+yAxisZero+0.5); 750 horizontalZeroLine.setEndX(left+xAxisWidth); 751 horizontalZeroLine.setEndY(top+yAxisZero+0.5); 752 horizontalZeroLine.setVisible(true); 753 } 754 // layout plot background 755 plotBackground.resizeRelocate(left, top, xAxisWidth, yAxisHeight); 756 // update clip 757 plotAreaClip.setX(left); 758 plotAreaClip.setY(top); 759 plotAreaClip.setWidth(xAxisWidth+1); 760 plotAreaClip.setHeight(yAxisHeight+1); 761 // plotArea.setClip(new Rectangle(left, top, xAxisWidth, yAxisHeight)); 762 // position plot group, its origin is the bottom left corner of the plot area 763 plotContent.setLayoutX(left); 764 plotContent.setLayoutY(top); 765 plotContent.requestLayout(); // Note: not sure this is right, maybe plotContent should be resizeable 766 // update vertical grid lines 767 verticalGridLines.getElements().clear(); 768 if(getVerticalGridLinesVisible()) { 769 for(int i=0; i < xaTickMarks.size(); i++) { 770 Axis.TickMark<X> tick = xaTickMarks.get(i); 771 final double x = xa.getDisplayPosition(tick.getValue()); 772 if ((x!=xAxisZero || !isVerticalZeroLineVisible()) && x > 0 && x <= xAxisWidth) { 773 verticalGridLines.getElements().add(new MoveTo(left+x+0.5,top)); 774 verticalGridLines.getElements().add(new LineTo(left+x+0.5,top+yAxisHeight)); 775 } 776 } 777 } 778 // update horizontal grid lines 779 horizontalGridLines.getElements().clear(); 780 if(isHorizontalGridLinesVisible()) { 781 for(int i=0; i < yaTickMarks.size(); i++) { 782 Axis.TickMark<Y> tick = yaTickMarks.get(i); 783 final double y = ya.getDisplayPosition(tick.getValue()); 784 if ((y!=yAxisZero || !isHorizontalZeroLineVisible()) && y >= 0 && y < yAxisHeight) { 785 horizontalGridLines.getElements().add(new MoveTo(left,top+y+0.5)); 786 horizontalGridLines.getElements().add(new LineTo(left+xAxisWidth,top+y+0.5)); 787 } 788 } 789 } 790 // Note: is there a more efficient way to calculate horizontal and vertical row fills? 791 // update vertical row fill 792 verticalRowFill.getElements().clear(); 793 if (isAlternativeColumnFillVisible()) { 794 // tick marks are not sorted so get all the positions and sort them 795 final List<Double> tickPositionsPositive = new ArrayList<Double>(); 796 final List<Double> tickPositionsNegative = new ArrayList<Double>(); 797 for(int i=0; i < xaTickMarks.size(); i++) { 798 double pos = xa.getDisplayPosition((X) xaTickMarks.get(i).getValue()); 799 if (pos == xAxisZero) { 800 tickPositionsPositive.add(pos); 801 tickPositionsNegative.add(pos); 802 } else if (pos < xAxisZero) { 803 tickPositionsPositive.add(pos); 804 } else { 805 tickPositionsNegative.add(pos); 806 } 807 } 808 Collections.sort(tickPositionsPositive); 809 Collections.sort(tickPositionsNegative); 810 // iterate over every pair of positive tick marks and create fill 811 for(int i=1; i < tickPositionsPositive.size(); i+=2) { 812 if((i+1) < tickPositionsPositive.size()) { 813 final double x1 = tickPositionsPositive.get(i); 814 final double x2 = tickPositionsPositive.get(i+1); 815 verticalRowFill.getElements().addAll( 816 new MoveTo(left+x1,top), 817 new LineTo(left+x1,top+yAxisHeight), 818 new LineTo(left+x2,top+yAxisHeight), 819 new LineTo(left+x2,top), 820 new ClosePath()); 821 } 822 } 823 // iterate over every pair of positive tick marks and create fill 824 for(int i=0; i < tickPositionsNegative.size(); i+=2) { 825 if((i+1) < tickPositionsNegative.size()) { 826 final double x1 = tickPositionsNegative.get(i); 827 final double x2 = tickPositionsNegative.get(i+1); 828 verticalRowFill.getElements().addAll( 829 new MoveTo(left+x1,top), 830 new LineTo(left+x1,top+yAxisHeight), 831 new LineTo(left+x2,top+yAxisHeight), 832 new LineTo(left+x2,top), 833 new ClosePath()); 834 } 835 } 836 } 837 // update horizontal row fill 838 horizontalRowFill.getElements().clear(); 839 if (isAlternativeRowFillVisible()) { 840 // tick marks are not sorted so get all the positions and sort them 841 final List<Double> tickPositionsPositive = new ArrayList<Double>(); 842 final List<Double> tickPositionsNegative = new ArrayList<Double>(); 843 for(int i=0; i < yaTickMarks.size(); i++) { 844 double pos = ya.getDisplayPosition((Y) yaTickMarks.get(i).getValue()); 845 if (pos == yAxisZero) { 846 tickPositionsPositive.add(pos); 847 tickPositionsNegative.add(pos); 848 } else if (pos < yAxisZero) { 849 tickPositionsPositive.add(pos); 850 } else { 851 tickPositionsNegative.add(pos); 852 } 853 } 854 Collections.sort(tickPositionsPositive); 855 Collections.sort(tickPositionsNegative); 856 // iterate over every pair of positive tick marks and create fill 857 for(int i=1; i < tickPositionsPositive.size(); i+=2) { 858 if((i+1) < tickPositionsPositive.size()) { 859 final double y1 = tickPositionsPositive.get(i); 860 final double y2 = tickPositionsPositive.get(i+1); 861 horizontalRowFill.getElements().addAll( 862 new MoveTo(left, top + y1), 863 new LineTo(left + xAxisWidth, top + y1), 864 new LineTo(left + xAxisWidth, top + y2), 865 new LineTo(left, top + y2), 866 new ClosePath()); 867 } 868 } 869 // iterate over every pair of positive tick marks and create fill 870 for(int i=0; i < tickPositionsNegative.size(); i+=2) { 871 if((i+1) < tickPositionsNegative.size()) { 872 final double y1 = tickPositionsNegative.get(i); 873 final double y2 = tickPositionsNegative.get(i+1); 874 horizontalRowFill.getElements().addAll( 875 new MoveTo(left, top + y1), 876 new LineTo(left + xAxisWidth, top + y1), 877 new LineTo(left + xAxisWidth, top + y2), 878 new LineTo(left, top + y2), 879 new ClosePath()); 880 } 881 } 882 } 883 // 884 } 885 886 /** 887 * Get the index of the series in the series linked list. 888 * 889 * @param series The series to find index for 890 * @return index of the series in series list 891 */ 892 int getSeriesIndex(Series<X,Y> series) { 893 return displayedSeries.indexOf(series); 894 } 895 896 /** 897 * Computes the size of series linked list 898 * @return size of series linked list 899 */ 900 int getSeriesSize() { 901 return displayedSeries.size(); 902 } 903 904 /** 905 * This should be called from seriesRemoved() when you are finished with any animation for deleting the series from 906 * the chart. It will remove the series from showing up in the Iterator returned by getDisplayedSeriesIterator(). 907 * 908 * @param series The series to remove 909 */ 910 protected final void removeSeriesFromDisplay(Series<X, Y> series) { 911 if (series != null) series.setToRemove = false; 912 series.setChart(null); 913 displayedSeries.remove(series); 914 } 915 916 /** 917 * XYChart maintains a list of all series currently displayed this includes all current series + any series that 918 * have recently been deleted that are in the process of being faded(animated) out. This creates and returns a 919 * iterator over that list. This is what implementations of XYChart should use when plotting data. 920 * 921 * @return iterator over currently displayed series 922 */ 923 protected final Iterator<Series<X,Y>> getDisplayedSeriesIterator() { 924 return Collections.unmodifiableList(displayedSeries).iterator(); 925 } 926 927 /** 928 * Creates an array of KeyFrames for fading out nodes representing a series 929 * 930 * @param series The series to remove 931 * @param fadeOutTime Time to fade out, in milliseconds 932 * @return array of two KeyFrames from zero to fadeOutTime 933 */ 934 protected final KeyFrame[] createSeriesRemoveTimeLine(Series<X, Y> series, long fadeOutTime) { 935 final List<Node> nodes = new ArrayList<>(); 936 nodes.add(series.getNode()); 937 for (Data<X, Y> d : series.getData()) { 938 if (d.getNode() != null) { 939 nodes.add(d.getNode()); 940 } 941 } 942 // fade out series node and symbols 943 KeyValue[] startValues = new KeyValue[nodes.size()]; 944 KeyValue[] endValues = new KeyValue[nodes.size()]; 945 for (int j = 0; j < nodes.size(); j++) { 946 startValues[j] = new KeyValue(nodes.get(j).opacityProperty(), 1); 947 endValues[j] = new KeyValue(nodes.get(j).opacityProperty(), 0); 948 } 949 return new KeyFrame[] { 950 new KeyFrame(Duration.ZERO, startValues), 951 new KeyFrame(Duration.millis(fadeOutTime), actionEvent -> { 952 getPlotChildren().removeAll(nodes); 953 removeSeriesFromDisplay(series); 954 }, endValues) 955 }; 956 } 957 /** 958 * The current displayed data value plotted on the X axis. This may be the same as xValue or different. It is 959 * used by XYChart to animate the xValue from the old value to the new value. This is what you should plot 960 * in any custom XYChart implementations. Some XYChart chart implementations such as LineChart also use this 961 * to animate when data is added or removed. 962 */ 963 protected final X getCurrentDisplayedXValue(Data<X,Y> item) { return item.getCurrentX(); } 964 965 /** Set the current displayed data value plotted on X axis. 966 * 967 * @param item The XYChart.Data item from which the current X axis data value is obtained. 968 * @see #getCurrentDisplayedXValue 969 */ 970 protected final void setCurrentDisplayedXValue(Data<X,Y> item, X value) { item.setCurrentX(value); } 971 972 /** The current displayed data value property that is plotted on X axis. 973 * 974 * @param item The XYChart.Data item from which the current X axis data value property object is obtained. 975 * @return The current displayed X data value ObjectProperty. 976 * @see #getCurrentDisplayedXValue 977 */ 978 protected final ObjectProperty<X> currentDisplayedXValueProperty(Data<X,Y> item) { return item.currentXProperty(); } 979 980 /** 981 * The current displayed data value plotted on the Y axis. This may be the same as yValue or different. It is 982 * used by XYChart to animate the yValue from the old value to the new value. This is what you should plot 983 * in any custom XYChart implementations. Some XYChart chart implementations such as LineChart also use this 984 * to animate when data is added or removed. 985 */ 986 protected final Y getCurrentDisplayedYValue(Data<X,Y> item) { return item.getCurrentY(); } 987 988 /** 989 * Set the current displayed data value plotted on Y axis. 990 * 991 * @param item The XYChart.Data item from which the current Y axis data value is obtained. 992 * @see #getCurrentDisplayedYValue 993 */ 994 protected final void setCurrentDisplayedYValue(Data<X,Y> item, Y value) { item.setCurrentY(value); } 995 996 /** The current displayed data value property that is plotted on Y axis. 997 * 998 * @param item The XYChart.Data item from which the current Y axis data value property object is obtained. 999 * @return The current displayed Y data value ObjectProperty. 1000 * @see #getCurrentDisplayedYValue 1001 */ 1002 protected final ObjectProperty<Y> currentDisplayedYValueProperty(Data<X,Y> item) { return item.currentYProperty(); } 1003 1004 /** 1005 * The current displayed data extra value. This may be the same as extraValue or different. It is 1006 * used by XYChart to animate the extraValue from the old value to the new value. This is what you should plot 1007 * in any custom XYChart implementations. 1008 */ 1009 protected final Object getCurrentDisplayedExtraValue(Data<X,Y> item) { return item.getCurrentExtraValue(); } 1010 1011 /** 1012 * Set the current displayed data extra value. 1013 * 1014 * @param item The XYChart.Data item from which the current extra value is obtained. 1015 * @see #getCurrentDisplayedExtraValue 1016 */ 1017 protected final void setCurrentDisplayedExtraValue(Data<X,Y> item, Object value) { item.setCurrentExtraValue(value); } 1018 1019 /** 1020 * The current displayed extra value property. 1021 * 1022 * @param item The XYChart.Data item from which the current extra value property object is obtained. 1023 * @return ObjectProperty<Object> The current extra value ObjectProperty 1024 * @see #getCurrentDisplayedExtraValue 1025 */ 1026 protected final ObjectProperty<Object> currentDisplayedExtraValueProperty(Data<X,Y> item) { return item.currentExtraValueProperty(); } 1027 1028 /** 1029 * XYChart maintains a list of all items currently displayed this includes all current data + any data items 1030 * recently deleted that are in the process of being faded out. This creates and returns a iterator over 1031 * that list. This is what implementations of XYChart should use when plotting data. 1032 * 1033 * @param series The series to get displayed data for 1034 * @return iterator over currently displayed items from this series 1035 */ 1036 protected final Iterator<Data<X,Y>> getDisplayedDataIterator(final Series<X,Y> series) { 1037 return Collections.unmodifiableList(series.displayedData).iterator(); 1038 } 1039 1040 /** 1041 * This should be called from dataItemRemoved() when you are finished with any animation for deleting the item from the 1042 * chart. It will remove the data item from showing up in the Iterator returned by getDisplayedDataIterator(). 1043 * 1044 * @param series The series to remove 1045 * @param item The item to remove from series's display list 1046 */ 1047 protected final void removeDataItemFromDisplay(Series<X, Y> series, Data<X, Y> item) { 1048 series.removeDataItemRef(item); 1049 } 1050 1051 // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------ 1052 1053 private static class StyleableProperties { 1054 private static final CssMetaData<XYChart<?,?>,Boolean> HORIZONTAL_GRID_LINE_VISIBLE = 1055 new CssMetaData<XYChart<?,?>,Boolean>("-fx-horizontal-grid-lines-visible", 1056 BooleanConverter.getInstance(), Boolean.TRUE) { 1057 1058 @Override 1059 public boolean isSettable(XYChart<?,?> node) { 1060 return node.horizontalGridLinesVisible == null || 1061 !node.horizontalGridLinesVisible.isBound(); 1062 } 1063 1064 @Override 1065 public StyleableProperty<Boolean> getStyleableProperty(XYChart<?,?> node) { 1066 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.horizontalGridLinesVisibleProperty(); 1067 } 1068 }; 1069 1070 private static final CssMetaData<XYChart<?,?>,Boolean> HORIZONTAL_ZERO_LINE_VISIBLE = 1071 new CssMetaData<XYChart<?,?>,Boolean>("-fx-horizontal-zero-line-visible", 1072 BooleanConverter.getInstance(), Boolean.TRUE) { 1073 1074 @Override 1075 public boolean isSettable(XYChart<?,?> node) { 1076 return node.horizontalZeroLineVisible == null || 1077 !node.horizontalZeroLineVisible.isBound(); 1078 } 1079 1080 @Override 1081 public StyleableProperty<Boolean> getStyleableProperty(XYChart<?,?> node) { 1082 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.horizontalZeroLineVisibleProperty(); 1083 } 1084 }; 1085 1086 private static final CssMetaData<XYChart<?,?>,Boolean> ALTERNATIVE_ROW_FILL_VISIBLE = 1087 new CssMetaData<XYChart<?,?>,Boolean>("-fx-alternative-row-fill-visible", 1088 BooleanConverter.getInstance(), Boolean.TRUE) { 1089 1090 @Override 1091 public boolean isSettable(XYChart<?,?> node) { 1092 return node.alternativeRowFillVisible == null || 1093 !node.alternativeRowFillVisible.isBound(); 1094 } 1095 1096 @Override 1097 public StyleableProperty<Boolean> getStyleableProperty(XYChart<?,?> node) { 1098 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.alternativeRowFillVisibleProperty(); 1099 } 1100 }; 1101 1102 private static final CssMetaData<XYChart<?,?>,Boolean> VERTICAL_GRID_LINE_VISIBLE = 1103 new CssMetaData<XYChart<?,?>,Boolean>("-fx-vertical-grid-lines-visible", 1104 BooleanConverter.getInstance(), Boolean.TRUE) { 1105 1106 @Override 1107 public boolean isSettable(XYChart<?,?> node) { 1108 return node.verticalGridLinesVisible == null || 1109 !node.verticalGridLinesVisible.isBound(); 1110 } 1111 1112 @Override 1113 public StyleableProperty<Boolean> getStyleableProperty(XYChart<?,?> node) { 1114 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.verticalGridLinesVisibleProperty(); 1115 } 1116 }; 1117 1118 private static final CssMetaData<XYChart<?,?>,Boolean> VERTICAL_ZERO_LINE_VISIBLE = 1119 new CssMetaData<XYChart<?,?>,Boolean>("-fx-vertical-zero-line-visible", 1120 BooleanConverter.getInstance(), Boolean.TRUE) { 1121 1122 @Override 1123 public boolean isSettable(XYChart<?,?> node) { 1124 return node.verticalZeroLineVisible == null || 1125 !node.verticalZeroLineVisible.isBound(); 1126 } 1127 1128 @Override 1129 public StyleableProperty<Boolean> getStyleableProperty(XYChart<?,?> node) { 1130 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.verticalZeroLineVisibleProperty(); 1131 } 1132 }; 1133 1134 private static final CssMetaData<XYChart<?,?>,Boolean> ALTERNATIVE_COLUMN_FILL_VISIBLE = 1135 new CssMetaData<XYChart<?,?>,Boolean>("-fx-alternative-column-fill-visible", 1136 BooleanConverter.getInstance(), Boolean.TRUE) { 1137 1138 @Override 1139 public boolean isSettable(XYChart<?,?> node) { 1140 return node.alternativeColumnFillVisible == null || 1141 !node.alternativeColumnFillVisible.isBound(); 1142 } 1143 1144 @Override 1145 public StyleableProperty<Boolean> getStyleableProperty(XYChart<?,?> node) { 1146 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.alternativeColumnFillVisibleProperty(); 1147 } 1148 }; 1149 1150 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 1151 static { 1152 final List<CssMetaData<? extends Styleable, ?>> styleables = 1153 new ArrayList<CssMetaData<? extends Styleable, ?>>(Chart.getClassCssMetaData()); 1154 styleables.add(HORIZONTAL_GRID_LINE_VISIBLE); 1155 styleables.add(HORIZONTAL_ZERO_LINE_VISIBLE); 1156 styleables.add(ALTERNATIVE_ROW_FILL_VISIBLE); 1157 styleables.add(VERTICAL_GRID_LINE_VISIBLE); 1158 styleables.add(VERTICAL_ZERO_LINE_VISIBLE); 1159 styleables.add(ALTERNATIVE_COLUMN_FILL_VISIBLE); 1160 STYLEABLES = Collections.unmodifiableList(styleables); 1161 } 1162 } 1163 1164 /** 1165 * @return The CssMetaData associated with this class, which may include the 1166 * CssMetaData of its super classes. 1167 * @since JavaFX 8.0 1168 */ 1169 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 1170 return StyleableProperties.STYLEABLES; 1171 } 1172 1173 /** 1174 * {@inheritDoc} 1175 * @since JavaFX 8.0 1176 */ 1177 @Override 1178 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 1179 return getClassCssMetaData(); 1180 } 1181 1182 // -------------- INNER CLASSES ------------------------------------------------------------------------------------ 1183 1184 /** 1185 * A single data item with data for 2 axis charts 1186 * @since JavaFX 2.0 1187 */ 1188 public final static class Data<X,Y> { 1189 // -------------- PUBLIC PROPERTIES ---------------------------------------- 1190 1191 private boolean setToRemove = false; 1192 /** The series this data belongs to */ 1193 private Series<X,Y> series; 1194 void setSeries(Series<X,Y> series) { 1195 this.series = series; 1196 } 1197 1198 /** The generic data value to be plotted on the X axis */ 1199 private ObjectProperty<X> xValue = new ObjectPropertyBase<X>() { 1200 @Override protected void invalidated() { 1201 // Note: calling get to make non-lazy, replace with change listener when available 1202 get(); 1203 if (series!=null) { 1204 XYChart<X,Y> chart = series.getChart(); 1205 if(chart!=null) chart.dataXValueChanged(Data.this); 1206 } else { 1207 // data has not been added to series yet : 1208 // so currentX and X should be the same 1209 setCurrentX(get()); 1210 } 1211 } 1212 1213 @Override 1214 public Object getBean() { 1215 return Data.this; 1216 } 1217 1218 @Override 1219 public String getName() { 1220 return "XValue"; 1221 } 1222 }; 1223 /** 1224 * Gets the generic data value to be plotted on the X axis. 1225 * @return the generic data value to be plotted on the X axis. 1226 */ 1227 public final X getXValue() { return xValue.get(); } 1228 /** 1229 * Sets the generic data value to be plotted on the X axis. 1230 * @param value the generic data value to be plotted on the X axis. 1231 */ 1232 public final void setXValue(X value) { 1233 xValue.set(value); 1234 // handle the case where this is a init because the default constructor was used 1235 // and the case when series is not associated to a chart due to a remove series 1236 if (currentX.get() == null || 1237 (series != null && series.getChart() == null)) currentX.setValue(value); 1238 } 1239 /** 1240 * The generic data value to be plotted on the X axis. 1241 * @return The XValue property 1242 */ 1243 public final ObjectProperty<X> XValueProperty() { return xValue; } 1244 1245 /** The generic data value to be plotted on the Y axis */ 1246 private ObjectProperty<Y> yValue = new ObjectPropertyBase<Y>() { 1247 @Override protected void invalidated() { 1248 // Note: calling get to make non-lazy, replace with change listener when available 1249 get(); 1250 if (series!=null) { 1251 XYChart<X,Y> chart = series.getChart(); 1252 if(chart!=null) chart.dataYValueChanged(Data.this); 1253 } else { 1254 // data has not been added to series yet : 1255 // so currentY and Y should be the same 1256 setCurrentY(get()); 1257 } 1258 } 1259 1260 @Override 1261 public Object getBean() { 1262 return Data.this; 1263 } 1264 1265 @Override 1266 public String getName() { 1267 return "YValue"; 1268 } 1269 }; 1270 /** 1271 * Gets the generic data value to be plotted on the Y axis. 1272 * @return the generic data value to be plotted on the Y axis. 1273 */ 1274 public final Y getYValue() { return yValue.get(); } 1275 /** 1276 * Sets the generic data value to be plotted on the Y axis. 1277 * @param value the generic data value to be plotted on the Y axis. 1278 */ 1279 public final void setYValue(Y value) { 1280 yValue.set(value); 1281 // handle the case where this is a init because the default constructor was used 1282 // and the case when series is not associated to a chart due to a remove series 1283 if (currentY.get() == null || 1284 (series != null && series.getChart() == null)) currentY.setValue(value); 1285 1286 } 1287 /** 1288 * The generic data value to be plotted on the Y axis. 1289 * @return the YValue property 1290 */ 1291 public final ObjectProperty<Y> YValueProperty() { return yValue; } 1292 1293 /** 1294 * The generic data value to be plotted in any way the chart needs. For example used as the radius 1295 * for BubbleChart. 1296 */ 1297 private ObjectProperty<Object> extraValue = new ObjectPropertyBase<Object>() { 1298 @Override protected void invalidated() { 1299 // Note: calling get to make non-lazy, replace with change listener when available 1300 get(); 1301 if (series!=null) { 1302 XYChart<X,Y> chart = series.getChart(); 1303 if(chart!=null) chart.dataExtraValueChanged(Data.this); 1304 } 1305 } 1306 1307 @Override 1308 public Object getBean() { 1309 return Data.this; 1310 } 1311 1312 @Override 1313 public String getName() { 1314 return "extraValue"; 1315 } 1316 }; 1317 public final Object getExtraValue() { return extraValue.get(); } 1318 public final void setExtraValue(Object value) { extraValue.set(value); } 1319 public final ObjectProperty<Object> extraValueProperty() { return extraValue; } 1320 1321 /** 1322 * The node to display for this data item. You can either create your own node and set it on the data item 1323 * before you add the item to the chart. Otherwise the chart will create a node for you that has the default 1324 * representation for the chart type. This node will be set as soon as the data is added to the chart. You can 1325 * then get it to add mouse listeners etc. Charts will do their best to position and size the node 1326 * appropriately, for example on a Line or Scatter chart this node will be positioned centered on the data 1327 * values position. For a bar chart this is positioned and resized as the bar for this data item. 1328 */ 1329 private ObjectProperty<Node> node = new SimpleObjectProperty<Node>(this, "node") { 1330 protected void invalidated() { 1331 Node node = get(); 1332 if (node != null) { 1333 node.accessibleTextProperty().unbind(); 1334 node.accessibleTextProperty().bind(new StringBinding() { 1335 {bind(currentXProperty(), currentYProperty());} 1336 @Override protected String computeValue() { 1337 String seriesName = series != null ? series.getName() : ""; 1338 return seriesName + " X Axis is " + getCurrentX() + " Y Axis is " + getCurrentY(); 1339 } 1340 }); 1341 } 1342 }; 1343 }; 1344 public final Node getNode() { return node.get(); } 1345 public final void setNode(Node value) { node.set(value); } 1346 public final ObjectProperty<Node> nodeProperty() { return node; } 1347 1348 /** 1349 * The current displayed data value plotted on the X axis. This may be the same as xValue or different. It is 1350 * used by XYChart to animate the xValue from the old value to the new value. This is what you should plot 1351 * in any custom XYChart implementations. Some XYChart chart implementations such as LineChart also use this 1352 * to animate when data is added or removed. 1353 */ 1354 private ObjectProperty<X> currentX = new SimpleObjectProperty<X>(this, "currentX"); 1355 final X getCurrentX() { return currentX.get(); } 1356 final void setCurrentX(X value) { currentX.set(value); } 1357 final ObjectProperty<X> currentXProperty() { return currentX; } 1358 1359 /** 1360 * The current displayed data value plotted on the Y axis. This may be the same as yValue or different. It is 1361 * used by XYChart to animate the yValue from the old value to the new value. This is what you should plot 1362 * in any custom XYChart implementations. Some XYChart chart implementations such as LineChart also use this 1363 * to animate when data is added or removed. 1364 */ 1365 private ObjectProperty<Y> currentY = new SimpleObjectProperty<Y>(this, "currentY"); 1366 final Y getCurrentY() { return currentY.get(); } 1367 final void setCurrentY(Y value) { currentY.set(value); } 1368 final ObjectProperty<Y> currentYProperty() { return currentY; } 1369 1370 /** 1371 * The current displayed data extra value. This may be the same as extraValue or different. It is 1372 * used by XYChart to animate the extraValue from the old value to the new value. This is what you should plot 1373 * in any custom XYChart implementations. 1374 */ 1375 private ObjectProperty<Object> currentExtraValue = new SimpleObjectProperty<Object>(this, "currentExtraValue"); 1376 final Object getCurrentExtraValue() { return currentExtraValue.getValue(); } 1377 final void setCurrentExtraValue(Object value) { currentExtraValue.setValue(value); } 1378 final ObjectProperty<Object> currentExtraValueProperty() { return currentExtraValue; } 1379 1380 // -------------- CONSTRUCTOR ------------------------------------------------- 1381 1382 /** 1383 * Creates an empty XYChart.Data object. 1384 */ 1385 public Data() {} 1386 1387 /** 1388 * Creates an instance of XYChart.Data object and initializes the X,Y 1389 * data values. 1390 * 1391 * @param xValue The X axis data value 1392 * @param yValue The Y axis data value 1393 */ 1394 public Data(X xValue, Y yValue) { 1395 setXValue(xValue); 1396 setYValue(yValue); 1397 setCurrentX(xValue); 1398 setCurrentY(yValue); 1399 } 1400 1401 /** 1402 * Creates an instance of XYChart.Data object and initializes the X,Y 1403 * data values and extraValue. 1404 * 1405 * @param xValue The X axis data value. 1406 * @param yValue The Y axis data value. 1407 * @param extraValue Chart extra value. 1408 */ 1409 public Data(X xValue, Y yValue, Object extraValue) { 1410 setXValue(xValue); 1411 setYValue(yValue); 1412 setExtraValue(extraValue); 1413 setCurrentX(xValue); 1414 setCurrentY(yValue); 1415 setCurrentExtraValue(extraValue); 1416 } 1417 1418 // -------------- PUBLIC METHODS ---------------------------------------------- 1419 1420 /** 1421 * Returns a string representation of this {@code Data} object. 1422 * @return a string representation of this {@code Data} object. 1423 */ 1424 @Override public String toString() { 1425 return "Data["+getXValue()+","+getYValue()+","+getExtraValue()+"]"; 1426 } 1427 1428 } 1429 1430 /** 1431 * A named series of data items 1432 * @since JavaFX 2.0 1433 */ 1434 public static final class Series<X,Y> { 1435 1436 // -------------- PRIVATE PROPERTIES ---------------------------------------- 1437 1438 /** the style class for default color for this series */ 1439 String defaultColorStyleClass; 1440 boolean setToRemove = false; 1441 1442 private List<Data<X, Y>> displayedData = new ArrayList<>(); 1443 1444 private final ListChangeListener<Data<X,Y>> dataChangeListener = new ListChangeListener<Data<X, Y>>() { 1445 @Override public void onChanged(Change<? extends Data<X, Y>> c) { 1446 ObservableList<? extends Data<X, Y>> data = c.getList(); 1447 final XYChart<X, Y> chart = getChart(); 1448 while (c.next()) { 1449 if (chart != null) { 1450 // RT-25187 Probably a sort happened, just reorder the pointers and return. 1451 if (c.wasPermutated()) { 1452 displayedData.sort((o1, o2) -> data.indexOf(o2) - data.indexOf(o1)); 1453 return; 1454 } 1455 1456 Set<Data<X, Y>> dupCheck = new HashSet<>(displayedData); 1457 dupCheck.removeAll(c.getRemoved()); 1458 for (Data<X, Y> d : c.getAddedSubList()) { 1459 if (!dupCheck.add(d)) { 1460 throw new IllegalArgumentException("Duplicate data added"); 1461 } 1462 } 1463 1464 // update data items reference to series 1465 for (Data<X, Y> item : c.getRemoved()) { 1466 item.setToRemove = true; 1467 } 1468 1469 if (c.getAddedSize() > 0) { 1470 for (Data<X, Y> itemPtr : c.getAddedSubList()) { 1471 if (itemPtr.setToRemove) { 1472 if (chart != null) chart.dataBeingRemovedIsAdded(itemPtr, Series.this); 1473 itemPtr.setToRemove = false; 1474 } 1475 } 1476 1477 for (Data<X, Y> d : c.getAddedSubList()) { 1478 d.setSeries(Series.this); 1479 } 1480 if (c.getFrom() == 0) { 1481 displayedData.addAll(0, c.getAddedSubList()); 1482 } else { 1483 displayedData.addAll(displayedData.indexOf(data.get(c.getFrom() - 1)) + 1, c.getAddedSubList()); 1484 } 1485 } 1486 // inform chart 1487 chart.dataItemsChanged(Series.this, 1488 (List<Data<X, Y>>) c.getRemoved(), c.getFrom(), c.getTo(), c.wasPermutated()); 1489 } else { 1490 Set<Data<X, Y>> dupCheck = new HashSet<>(); 1491 for (Data<X, Y> d : data) { 1492 if (!dupCheck.add(d)) { 1493 throw new IllegalArgumentException("Duplicate data added"); 1494 } 1495 } 1496 1497 for (Data<X, Y> d : c.getAddedSubList()) { 1498 d.setSeries(Series.this); 1499 } 1500 1501 } 1502 } 1503 } 1504 }; 1505 1506 // -------------- PUBLIC PROPERTIES ---------------------------------------- 1507 1508 /** Reference to the chart this series belongs to */ 1509 private final ReadOnlyObjectWrapper<XYChart<X,Y>> chart = new ReadOnlyObjectWrapper<XYChart<X,Y>>(this, "chart") { 1510 @Override 1511 protected void invalidated() { 1512 if (get() == null) { 1513 displayedData.clear(); 1514 } else { 1515 displayedData.addAll(getData()); 1516 } 1517 } 1518 }; 1519 public final XYChart<X,Y> getChart() { return chart.get(); } 1520 private void setChart(XYChart<X,Y> value) { chart.set(value); } 1521 public final ReadOnlyObjectProperty<XYChart<X,Y>> chartProperty() { return chart.getReadOnlyProperty(); } 1522 1523 /** The user displayable name for this series */ 1524 private final StringProperty name = new StringPropertyBase() { 1525 @Override protected void invalidated() { 1526 get(); // make non-lazy 1527 if(getChart() != null) getChart().seriesNameChanged(); 1528 } 1529 1530 @Override 1531 public Object getBean() { 1532 return Series.this; 1533 } 1534 1535 @Override 1536 public String getName() { 1537 return "name"; 1538 } 1539 }; 1540 public final String getName() { return name.get(); } 1541 public final void setName(String value) { name.set(value); } 1542 public final StringProperty nameProperty() { return name; } 1543 1544 /** 1545 * The node to display for this series. This is created by the chart if it uses nodes to represent the whole 1546 * series. For example line chart uses this for the line but scatter chart does not use it. This node will be 1547 * set as soon as the series is added to the chart. You can then get it to add mouse listeners etc. 1548 */ 1549 private ObjectProperty<Node> node = new SimpleObjectProperty<Node>(this, "node"); 1550 public final Node getNode() { return node.get(); } 1551 public final void setNode(Node value) { node.set(value); } 1552 public final ObjectProperty<Node> nodeProperty() { return node; } 1553 1554 /** ObservableList of data items that make up this series */ 1555 private final ObjectProperty<ObservableList<Data<X,Y>>> data = new ObjectPropertyBase<ObservableList<Data<X,Y>>>() { 1556 private ObservableList<Data<X,Y>> old; 1557 @Override protected void invalidated() { 1558 final ObservableList<Data<X,Y>> current = getValue(); 1559 // add remove listeners 1560 if(old != null) old.removeListener(dataChangeListener); 1561 if(current != null) current.addListener(dataChangeListener); 1562 // fire data change event if series are added or removed 1563 if(old != null || current != null) { 1564 final List<Data<X,Y>> removed = (old != null) ? old : Collections.<Data<X,Y>>emptyList(); 1565 final int toIndex = (current != null) ? current.size() : 0; 1566 // let data listener know all old data have been removed and new data that has been added 1567 if (toIndex > 0 || !removed.isEmpty()) { 1568 dataChangeListener.onChanged(new NonIterableChange<Data<X,Y>>(0, toIndex, current){ 1569 @Override public List<Data<X,Y>> getRemoved() { return removed; } 1570 1571 @Override protected int[] getPermutation() { 1572 return new int[0]; 1573 } 1574 }); 1575 } 1576 } else if (old != null && old.size() > 0) { 1577 // let series listener know all old series have been removed 1578 dataChangeListener.onChanged(new NonIterableChange<Data<X,Y>>(0, 0, current){ 1579 @Override public List<Data<X,Y>> getRemoved() { return old; } 1580 @Override protected int[] getPermutation() { 1581 return new int[0]; 1582 } 1583 }); 1584 } 1585 old = current; 1586 } 1587 1588 @Override 1589 public Object getBean() { 1590 return Series.this; 1591 } 1592 1593 @Override 1594 public String getName() { 1595 return "data"; 1596 } 1597 }; 1598 public final ObservableList<Data<X,Y>> getData() { return data.getValue(); } 1599 public final void setData(ObservableList<Data<X,Y>> value) { data.setValue(value); } 1600 public final ObjectProperty<ObservableList<Data<X,Y>>> dataProperty() { return data; } 1601 1602 // -------------- CONSTRUCTORS ---------------------------------------------- 1603 1604 /** 1605 * Construct a empty series 1606 */ 1607 public Series() { 1608 this(FXCollections.<Data<X,Y>>observableArrayList()); 1609 } 1610 1611 /** 1612 * Constructs a Series and populates it with the given {@link ObservableList} data. 1613 * 1614 * @param data ObservableList of XYChart.Data 1615 */ 1616 public Series(ObservableList<Data<X,Y>> data) { 1617 setData(data); 1618 for(Data<X,Y> item:data) item.setSeries(this); 1619 } 1620 1621 /** 1622 * Constructs a named Series and populates it with the given {@link ObservableList} data. 1623 * 1624 * @param name a name for the series 1625 * @param data ObservableList of XYChart.Data 1626 */ 1627 public Series(String name, ObservableList<Data<X,Y>> data) { 1628 this(data); 1629 setName(name); 1630 } 1631 1632 // -------------- PUBLIC METHODS ---------------------------------------------- 1633 1634 /** 1635 * Returns a string representation of this {@code Series} object. 1636 * @return a string representation of this {@code Series} object. 1637 */ 1638 @Override public String toString() { 1639 return "Series["+getName()+"]"; 1640 } 1641 1642 // -------------- PRIVATE/PROTECTED METHODS ----------------------------------- 1643 1644 /* 1645 * The following methods are for manipulating the pointers in the linked list 1646 * when data is deleted. 1647 */ 1648 private void removeDataItemRef(Data<X,Y> item) { 1649 if (item != null) item.setToRemove = false; 1650 displayedData.remove(item); 1651 } 1652 1653 int getItemIndex(Data<X,Y> item) { 1654 return displayedData.indexOf(item); 1655 } 1656 1657 Data<X, Y> getItem(int i) { 1658 return displayedData.get(i); 1659 } 1660 1661 int getDataSize() { 1662 return displayedData.size(); 1663 } 1664 } 1665 1666 }