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