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