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