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 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 } 141 142 for(int i=c.getFrom(); i<c.getTo() && !c.wasPermutated(); i++) { 143 final Series<X,Y> s = c.getList().get(i); 144 // add new listener to data 145 s.setChart(XYChart.this); 146 if (s.setToRemove) { 147 s.setToRemove = false; 148 s.getChart().seriesBeingRemovedIsAdded(s); 149 } 150 // update linkedList Pointers for series 151 displayedSeries.add(s); 152 // update default color style class 153 int nextClearBit = colorBits.nextClearBit(0); 154 colorBits.set(nextClearBit, true); 155 s.defaultColorStyleClass = DEFAULT_COLOR+(nextClearBit%8); 156 seriesColorMap.put(s, nextClearBit%8); 157 // inform sub-classes of series added 158 seriesAdded(s, i); 159 } 160 if (c.getFrom() < c.getTo()) updateLegend(); 161 seriesChanged(c); 162 163 } 164 // update axis ranges 165 invalidateRange(); 166 // lay everything out 167 requestChartLayout(); 168 }; 169 170 // -------------- PUBLIC PROPERTIES -------------------------------------------------------------------------------- 171 172 private final Axis<X> xAxis; 173 /** Get the X axis, by default it is along the bottom of the plot */ 174 public Axis<X> getXAxis() { return xAxis; } 175 176 private final Axis<Y> yAxis; 177 /** Get the Y axis, by default it is along the left of the plot */ 178 public Axis<Y> getYAxis() { return yAxis; } 179 180 /** XYCharts data */ 181 private ObjectProperty<ObservableList<Series<X,Y>>> data = new ObjectPropertyBase<ObservableList<Series<X,Y>>>() { 182 private ObservableList<Series<X,Y>> old; 183 @Override protected void invalidated() { 184 final ObservableList<Series<X,Y>> current = getValue(); 185 int saveAnimationState = -1; 186 // add remove listeners 187 if(old != null) { 188 old.removeListener(seriesChanged); 189 // Set animated to false so we don't animate both remove and add 190 // at the same time. RT-14163 191 // RT-21295 - disable animated only when current is also not null. 192 if (current != null && old.size() > 0) { 193 saveAnimationState = (old.get(0).getChart().getAnimated()) ? 1 : 2; 194 old.get(0).getChart().setAnimated(false); 195 } 196 } 197 if(current != null) current.addListener(seriesChanged); 198 // fire series change event if series are added or removed 199 if(old != null || current != null) { 200 final List<Series<X,Y>> removed = (old != null) ? old : Collections.<Series<X,Y>>emptyList(); 201 final int toIndex = (current != null) ? current.size() : 0; 202 // let series listener know all old series have been removed and new that have been added 203 if (toIndex > 0 || !removed.isEmpty()) { 204 seriesChanged.onChanged(new NonIterableChange<Series<X,Y>>(0, toIndex, current){ 205 @Override public List<Series<X,Y>> getRemoved() { return removed; } 206 @Override protected int[] getPermutation() { 207 return new int[0]; 208 } 209 }); 210 } 211 } else if (old != null && old.size() > 0) { 212 // let series listener know all old series have been removed 213 seriesChanged.onChanged(new NonIterableChange<Series<X,Y>>(0, 0, current){ 214 @Override public List<Series<X,Y>> getRemoved() { return old; } 215 @Override protected int[] getPermutation() { 216 return new int[0]; 217 } 218 }); 219 } 220 // restore animated on chart. 221 if (current != null && current.size() > 0 && saveAnimationState != -1) { 222 current.get(0).getChart().setAnimated((saveAnimationState == 1) ? true : false); 223 } 224 old = current; 225 } 226 227 public Object getBean() { 228 return XYChart.this; 229 } 230 231 public String getName() { 232 return "data"; 233 } 234 }; 235 public final ObservableList<Series<X,Y>> getData() { return data.getValue(); } 236 public final void setData(ObservableList<Series<X,Y>> value) { data.setValue(value); } 237 public final ObjectProperty<ObservableList<Series<X,Y>>> dataProperty() { return data; } 238 239 /** True if vertical grid lines should be drawn */ 240 private BooleanProperty verticalGridLinesVisible = new StyleableBooleanProperty(true) { 241 @Override protected void invalidated() { 242 requestChartLayout(); 243 } 244 245 @Override 246 public Object getBean() { 247 return XYChart.this; 248 } 249 250 @Override 251 public String getName() { 252 return "verticalGridLinesVisible"; 253 } 254 255 @Override 256 public CssMetaData<XYChart<?,?>,Boolean> getCssMetaData() { 257 return StyleableProperties.VERTICAL_GRID_LINE_VISIBLE; 258 } 259 }; 260 /** 261 * Indicates whether vertical grid lines are visible or not. 262 * 263 * @return true if verticalGridLines are visible else false. 264 * @see #verticalGridLinesVisible 265 */ 266 public final boolean getVerticalGridLinesVisible() { return verticalGridLinesVisible.get(); } 267 public final void setVerticalGridLinesVisible(boolean value) { verticalGridLinesVisible.set(value); } 268 public final BooleanProperty verticalGridLinesVisibleProperty() { return verticalGridLinesVisible; } 269 270 /** True if horizontal grid lines should be drawn */ 271 private BooleanProperty horizontalGridLinesVisible = new StyleableBooleanProperty(true) { 272 @Override protected void invalidated() { 273 requestChartLayout(); 274 } 275 276 @Override 277 public Object getBean() { 278 return XYChart.this; 279 } 280 281 @Override 282 public String getName() { 283 return "horizontalGridLinesVisible"; 284 } 285 286 @Override 287 public CssMetaData<XYChart<?,?>,Boolean> getCssMetaData() { 288 return StyleableProperties.HORIZONTAL_GRID_LINE_VISIBLE; 289 } 290 }; 291 public final boolean isHorizontalGridLinesVisible() { return horizontalGridLinesVisible.get(); } 292 public final void setHorizontalGridLinesVisible(boolean value) { horizontalGridLinesVisible.set(value); } 293 public final BooleanProperty horizontalGridLinesVisibleProperty() { return horizontalGridLinesVisible; } 294 295 /** If true then alternative vertical columns will have fills */ 296 private BooleanProperty alternativeColumnFillVisible = new StyleableBooleanProperty(false) { 297 @Override protected void invalidated() { 298 requestChartLayout(); 299 } 300 301 @Override 302 public Object getBean() { 303 return XYChart.this; 304 } 305 306 @Override 307 public String getName() { 308 return "alternativeColumnFillVisible"; 309 } 310 311 @Override 312 public CssMetaData<XYChart<?,?>,Boolean> getCssMetaData() { 313 return StyleableProperties.ALTERNATIVE_COLUMN_FILL_VISIBLE; 314 } 315 }; 316 public final boolean isAlternativeColumnFillVisible() { return alternativeColumnFillVisible.getValue(); } 317 public final void setAlternativeColumnFillVisible(boolean value) { alternativeColumnFillVisible.setValue(value); } 318 public final BooleanProperty alternativeColumnFillVisibleProperty() { return alternativeColumnFillVisible; } 319 320 /** If true then alternative horizontal rows will have fills */ 321 private BooleanProperty alternativeRowFillVisible = new StyleableBooleanProperty(true) { 322 @Override protected void invalidated() { 323 requestChartLayout(); 324 } 325 326 @Override 327 public Object getBean() { 328 return XYChart.this; 329 } 330 331 @Override 332 public String getName() { 333 return "alternativeRowFillVisible"; 334 } 335 336 @Override 337 public CssMetaData<XYChart<?,?>,Boolean> getCssMetaData() { 338 return StyleableProperties.ALTERNATIVE_ROW_FILL_VISIBLE; 339 } 340 }; 341 public final boolean isAlternativeRowFillVisible() { return alternativeRowFillVisible.getValue(); } 342 public final void setAlternativeRowFillVisible(boolean value) { alternativeRowFillVisible.setValue(value); } 343 public final BooleanProperty alternativeRowFillVisibleProperty() { return alternativeRowFillVisible; } 344 345 /** 346 * If this is true and the vertical axis has both positive and negative values then a additional axis line 347 * will be drawn at the zero point 348 * 349 * @defaultValue true 350 */ 351 private BooleanProperty verticalZeroLineVisible = new StyleableBooleanProperty(true) { 352 @Override protected void invalidated() { 353 requestChartLayout(); 354 } 355 356 @Override 357 public Object getBean() { 358 return XYChart.this; 359 } 360 361 @Override 362 public String getName() { 363 return "verticalZeroLineVisible"; 364 } 365 366 @Override 367 public CssMetaData<XYChart<?,?>,Boolean> getCssMetaData() { 368 return StyleableProperties.VERTICAL_ZERO_LINE_VISIBLE; 369 } 370 }; 371 public final boolean isVerticalZeroLineVisible() { return verticalZeroLineVisible.get(); } 372 public final void setVerticalZeroLineVisible(boolean value) { verticalZeroLineVisible.set(value); } 373 public final BooleanProperty verticalZeroLineVisibleProperty() { return verticalZeroLineVisible; } 374 375 /** 376 * If this is true and the horizontal axis has both positive and negative values then a additional axis line 377 * will be drawn at the zero point 378 * 379 * @defaultValue true 380 */ 381 private BooleanProperty horizontalZeroLineVisible = new StyleableBooleanProperty(true) { 382 @Override protected void invalidated() { 383 requestChartLayout(); 384 } 385 386 @Override 387 public Object getBean() { 388 return XYChart.this; 389 } 390 391 @Override 392 public String getName() { 393 return "horizontalZeroLineVisible"; 394 } 395 396 @Override 397 public CssMetaData<XYChart<?,?>,Boolean> getCssMetaData() { 398 return StyleableProperties.HORIZONTAL_ZERO_LINE_VISIBLE; 399 } 400 }; 401 public final boolean isHorizontalZeroLineVisible() { return horizontalZeroLineVisible.get(); } 402 public final void setHorizontalZeroLineVisible(boolean value) { horizontalZeroLineVisible.set(value); } 403 public final BooleanProperty horizontalZeroLineVisibleProperty() { return horizontalZeroLineVisible; } 404 405 // -------------- PROTECTED PROPERTIES ----------------------------------------------------------------------------- 406 407 /** 408 * Modifiable and observable list of all content in the plot. This is where implementations of XYChart should add 409 * any nodes they use to draw their plot. 410 * 411 * @return Observable list of plot children 412 */ 413 protected ObservableList<Node> getPlotChildren() { 414 return plotContent.getChildren(); 415 } 416 417 // -------------- CONSTRUCTOR -------------------------------------------------------------------------------------- 418 419 /** 420 * Constructs a XYChart given the two axes. The initial content for the chart 421 * plot background and plot area that includes vertical and horizontal grid 422 * lines and fills, are added. 423 * 424 * @param xAxis X Axis for this XY chart 425 * @param yAxis Y Axis for this XY chart 426 */ 427 public XYChart(Axis<X> xAxis, Axis<Y> yAxis) { 428 this.xAxis = xAxis; 429 if (xAxis.getSide() == null) xAxis.setSide(Side.BOTTOM); 430 xAxis.setEffectiveOrientation(Orientation.HORIZONTAL); 431 this.yAxis = yAxis; 432 if (yAxis.getSide() == null) yAxis.setSide(Side.LEFT); 433 yAxis.setEffectiveOrientation(Orientation.VERTICAL); 434 // RT-23123 autoranging leads to charts incorrect appearance. 435 xAxis.autoRangingProperty().addListener((ov, t, t1) -> { 436 updateAxisRange(); 437 }); 438 yAxis.autoRangingProperty().addListener((ov, t, t1) -> { 439 updateAxisRange(); 440 }); 441 // add initial content to chart content 442 getChartChildren().addAll(plotBackground,plotArea,xAxis,yAxis); 443 // We don't want plotArea or plotContent to autoSize or do layout 444 plotArea.setAutoSizeChildren(false); 445 plotContent.setAutoSizeChildren(false); 446 // setup clipping on plot area 447 plotAreaClip.setSmooth(false); 448 plotArea.setClip(plotAreaClip); 449 // add children to plot area 450 plotArea.getChildren().addAll( 451 verticalRowFill, horizontalRowFill, 452 verticalGridLines, horizontalGridLines, 453 verticalZeroLine, horizontalZeroLine, 454 plotContent); 455 // setup css style classes 456 plotContent.getStyleClass().setAll("plot-content"); 457 plotBackground.getStyleClass().setAll("chart-plot-background"); 458 verticalRowFill.getStyleClass().setAll("chart-alternative-column-fill"); 459 horizontalRowFill.getStyleClass().setAll("chart-alternative-row-fill"); 460 verticalGridLines.getStyleClass().setAll("chart-vertical-grid-lines"); 461 horizontalGridLines.getStyleClass().setAll("chart-horizontal-grid-lines"); 462 verticalZeroLine.getStyleClass().setAll("chart-vertical-zero-line"); 463 horizontalZeroLine.getStyleClass().setAll("chart-horizontal-zero-line"); 464 // mark plotContent as unmanaged as its preferred size changes do not effect our layout 465 plotContent.setManaged(false); 466 plotArea.setManaged(false); 467 // listen to animation on/off and sync to axis 468 animatedProperty().addListener((valueModel, oldValue, newValue) -> { 469 if(getXAxis() != null) getXAxis().setAnimated(newValue); 470 if(getYAxis() != null) getYAxis().setAnimated(newValue); 471 }); 472 } 473 474 // -------------- METHODS ------------------------------------------------------------------------------------------ 475 476 /** 477 * Gets the size of the data returning 0 if the data is null 478 * 479 * @return The number of items in data, or null if data is null 480 */ 481 final int getDataSize() { 482 final ObservableList<Series<X,Y>> data = getData(); 483 return (data!=null) ? data.size() : 0; 484 } 485 486 /** Called when a series's name has changed */ 487 private void seriesNameChanged() { 488 updateLegend(); 489 requestChartLayout(); 490 } 491 492 @SuppressWarnings({"UnusedParameters"}) 493 private void dataItemsChanged(Series<X,Y> series, List<Data<X,Y>> removed, int addedFrom, int addedTo, boolean permutation) { 494 for (Data<X,Y> item : removed) { 495 dataItemRemoved(item, series); 496 } 497 for(int i=addedFrom; i<addedTo; i++) { 498 Data<X,Y> item = series.getData().get(i); 499 dataItemAdded(series, i, item); 500 } 501 invalidateRange(); 502 requestChartLayout(); 503 } 504 505 private <T> void dataValueChanged(Data<X,Y> item, T newValue, ObjectProperty<T> currentValueProperty) { 506 if (currentValueProperty.get() != newValue) invalidateRange(); 507 dataItemChanged(item); 508 if (shouldAnimate()) { 509 animate( 510 new KeyFrame(Duration.ZERO, new KeyValue(currentValueProperty, currentValueProperty.get())), 511 new KeyFrame(Duration.millis(700), new KeyValue(currentValueProperty, newValue, Interpolator.EASE_BOTH)) 512 ); 513 } else { 514 currentValueProperty.set(newValue); 515 requestChartLayout(); 516 } 517 } 518 519 /** 520 * This is called whenever a series is added or removed and the legend needs to be updated 521 */ 522 protected void updateLegend(){} 523 524 /** 525 * This method is called when there is an attempt to add series that was 526 * set to be removed, and the removal might not have completed. 527 * @param series 528 */ 529 void seriesBeingRemovedIsAdded(Series<X,Y> series) {} 530 531 /** 532 * This method is called when there is an attempt to add a Data item that was 533 * set to be removed, and the removal might not have completed. 534 * @param data 535 */ 536 void dataBeingRemovedIsAdded(Data<X,Y> item, Series<X,Y> series) {} 537 /** 538 * Called when a data item has been added to a series. This is where implementations of XYChart can create/add new 539 * nodes to getPlotChildren to represent this data item. They also may animate that data add with a fade in or 540 * similar if animated = true. 541 * 542 * @param series The series the data item was added to 543 * @param itemIndex The index of the new item within the series 544 * @param item The new data item that was added 545 */ 546 protected abstract void dataItemAdded(Series<X,Y> series, int itemIndex, Data<X,Y> item); 547 548 /** 549 * Called when a data item has been removed from data model but it is still visible on the chart. Its still visible 550 * so that you can handle animation for removing it in this method. After you are done animating the data item you 551 * must call removeDataItemFromDisplay() to remove the items node from being displayed on the chart. 552 * 553 * @param item The item that has been removed from the series 554 * @param series The series the item was removed from 555 */ 556 protected abstract void dataItemRemoved(Data<X, Y> item, Series<X, Y> series); 557 558 /** 559 * Called when a data item has changed, ie its xValue, yValue or extraValue has changed. 560 * 561 * @param item The data item who was changed 562 */ 563 protected abstract void dataItemChanged(Data<X, Y> item); 564 /** 565 * A series has been added to the charts data model. This is where implementations of XYChart can create/add new 566 * nodes to getPlotChildren to represent this series. Also you have to handle adding any data items that are 567 * already in the series. You may simply call dataItemAdded() for each one or provide some different animation for 568 * a whole series being added. 569 * 570 * @param series The series that has been added 571 * @param seriesIndex The index of the new series 572 */ 573 protected abstract void seriesAdded(Series<X, Y> series, int seriesIndex); 574 575 /** 576 * A series has been removed from the data model but it is still visible on the chart. Its still visible 577 * so that you can handle animation for removing it in this method. After you are done animating the data item you 578 * must call removeSeriesFromDisplay() to remove the series from the display list. 579 * 580 * @param series The series that has been removed 581 */ 582 protected abstract void seriesRemoved(Series<X,Y> series); 583 584 /** Called when each atomic change is made to the list of series for this chart */ 585 protected void seriesChanged(Change<? extends Series> c) {} 586 587 /** 588 * This is called when a data change has happened that may cause the range to be invalid. 589 */ 590 private void invalidateRange() { 591 rangeValid = false; 592 } 593 594 /** 595 * This is called when the range has been invalidated and we need to update it. If the axis are auto 596 * ranging then we compile a list of all data that the given axis has to plot and call invalidateRange() on the 597 * axis passing it that data. 598 */ 599 protected void updateAxisRange() { 600 final Axis<X> xa = getXAxis(); 601 final Axis<Y> ya = getYAxis(); 602 List<X> xData = null; 603 List<Y> yData = null; 604 if(xa.isAutoRanging()) xData = new ArrayList<X>(); 605 if(ya.isAutoRanging()) yData = new ArrayList<Y>(); 606 if(xData != null || yData != null) { 607 for(Series<X,Y> series : getData()) { 608 for(Data<X,Y> data: series.getData()) { 609 if(xData != null) xData.add(data.getXValue()); 610 if(yData != null) yData.add(data.getYValue()); 611 } 612 } 613 if(xData != null) xa.invalidateRange(xData); 614 if(yData != null) ya.invalidateRange(yData); 615 } 616 } 617 618 /** 619 * Called to update and layout the plot children. This should include all work to updates nodes representing 620 * 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 621 * can be got by getting the width of the x axis and its height from the height of the y axis. 622 */ 623 protected abstract void layoutPlotChildren(); 624 625 /** @inheritDoc */ 626 @Override protected final void layoutChartChildren(double top, double left, double width, double height) { 627 if(getData() == null) return; 628 if (!rangeValid) { 629 rangeValid = true; 630 if(getData() != null) updateAxisRange(); 631 } 632 // snap top and left to pixels 633 top = snapPositionY(top); 634 left = snapPositionX(left); 635 // get starting stuff 636 final Axis<X> xa = getXAxis(); 637 final ObservableList<Axis.TickMark<X>> xaTickMarks = xa.getTickMarks(); 638 final Axis<Y> ya = getYAxis(); 639 final ObservableList<Axis.TickMark<Y>> yaTickMarks = ya.getTickMarks(); 640 // check we have 2 axises and know their sides 641 if (xa == null || ya == null) return; 642 // try and work out width and height of axises 643 double xAxisWidth = 0; 644 double xAxisHeight = 30; // guess x axis height to start with 645 double yAxisWidth = 0; 646 double yAxisHeight = 0; 647 for (int count=0; count<5; count ++) { 648 yAxisHeight = snapSizeY(height - xAxisHeight); 649 if (yAxisHeight < 0) { 650 yAxisHeight = 0; 651 } 652 yAxisWidth = ya.prefWidth(yAxisHeight); 653 xAxisWidth = snapSizeX(width - yAxisWidth); 654 if (xAxisWidth < 0) { 655 xAxisWidth = 0; 656 } 657 double newXAxisHeight = xa.prefHeight(xAxisWidth); 658 if (newXAxisHeight == xAxisHeight) break; 659 xAxisHeight = newXAxisHeight; 660 } 661 // round axis sizes up to whole integers to snap to pixel 662 xAxisWidth = Math.ceil(xAxisWidth); 663 xAxisHeight = Math.ceil(xAxisHeight); 664 yAxisWidth = Math.ceil(yAxisWidth); 665 yAxisHeight = Math.ceil(yAxisHeight); 666 // calc xAxis height 667 double xAxisY = 0; 668 switch(xa.getEffectiveSide()) { 669 case TOP: 670 xa.setVisible(true); 671 xAxisY = top+1; 672 top += xAxisHeight; 673 break; 674 case BOTTOM: 675 xa.setVisible(true); 676 xAxisY = top + yAxisHeight; 677 } 678 679 // calc yAxis width 680 double yAxisX = 0; 681 switch(ya.getEffectiveSide()) { 682 case LEFT: 683 ya.setVisible(true); 684 yAxisX = left +1; 685 left += yAxisWidth; 686 break; 687 case RIGHT: 688 ya.setVisible(true); 689 yAxisX = left + xAxisWidth; 690 } 691 // resize axises 692 xa.resizeRelocate(left, xAxisY, xAxisWidth, xAxisHeight); 693 ya.resizeRelocate(yAxisX, top, yAxisWidth, yAxisHeight); 694 // When the chart is resized, need to specifically call out the axises 695 // to lay out as they are unmanaged. 696 xa.requestAxisLayout(); 697 xa.layout(); 698 ya.requestAxisLayout(); 699 ya.layout(); 700 // layout plot content 701 layoutPlotChildren(); 702 // get axis zero points 703 final double xAxisZero = xa.getZeroPosition(); 704 final double yAxisZero = ya.getZeroPosition(); 705 // position vertical and horizontal zero lines 706 if(Double.isNaN(xAxisZero) || !isVerticalZeroLineVisible()) { 707 verticalZeroLine.setVisible(false); 708 } else { 709 verticalZeroLine.setStartX(left+xAxisZero+0.5); 710 verticalZeroLine.setStartY(top); 711 verticalZeroLine.setEndX(left+xAxisZero+0.5); 712 verticalZeroLine.setEndY(top+yAxisHeight); 713 verticalZeroLine.setVisible(true); 714 } 715 if(Double.isNaN(yAxisZero) || !isHorizontalZeroLineVisible()) { 716 horizontalZeroLine.setVisible(false); 717 } else { 718 horizontalZeroLine.setStartX(left); 719 horizontalZeroLine.setStartY(top+yAxisZero+0.5); 720 horizontalZeroLine.setEndX(left+xAxisWidth); 721 horizontalZeroLine.setEndY(top+yAxisZero+0.5); 722 horizontalZeroLine.setVisible(true); 723 } 724 // layout plot background 725 plotBackground.resizeRelocate(left, top, xAxisWidth, yAxisHeight); 726 // update clip 727 plotAreaClip.setX(left); 728 plotAreaClip.setY(top); 729 plotAreaClip.setWidth(xAxisWidth+1); 730 plotAreaClip.setHeight(yAxisHeight+1); 731 // plotArea.setClip(new Rectangle(left, top, xAxisWidth, yAxisHeight)); 732 // position plot group, its origin is the bottom left corner of the plot area 733 plotContent.setLayoutX(left); 734 plotContent.setLayoutY(top); 735 plotContent.requestLayout(); // Note: not sure this is right, maybe plotContent should be resizeable 736 // update vertical grid lines 737 verticalGridLines.getElements().clear(); 738 if(getVerticalGridLinesVisible()) { 739 for(int i=0; i < xaTickMarks.size(); i++) { 740 Axis.TickMark<X> tick = xaTickMarks.get(i); 741 final double x = xa.getDisplayPosition(tick.getValue()); 742 if ((x!=xAxisZero || !isVerticalZeroLineVisible()) && x > 0 && x <= xAxisWidth) { 743 verticalGridLines.getElements().add(new MoveTo(left+x+0.5,top)); 744 verticalGridLines.getElements().add(new LineTo(left+x+0.5,top+yAxisHeight)); 745 } 746 } 747 } 748 // update horizontal grid lines 749 horizontalGridLines.getElements().clear(); 750 if(isHorizontalGridLinesVisible()) { 751 for(int i=0; i < yaTickMarks.size(); i++) { 752 Axis.TickMark<Y> tick = yaTickMarks.get(i); 753 final double y = ya.getDisplayPosition(tick.getValue()); 754 if ((y!=yAxisZero || !isHorizontalZeroLineVisible()) && y >= 0 && y < yAxisHeight) { 755 horizontalGridLines.getElements().add(new MoveTo(left,top+y+0.5)); 756 horizontalGridLines.getElements().add(new LineTo(left+xAxisWidth,top+y+0.5)); 757 } 758 } 759 } 760 // Note: is there a more efficient way to calculate horizontal and vertical row fills? 761 // update vertical row fill 762 verticalRowFill.getElements().clear(); 763 if (isAlternativeColumnFillVisible()) { 764 // tick marks are not sorted so get all the positions and sort them 765 final List<Double> tickPositionsPositive = new ArrayList<Double>(); 766 final List<Double> tickPositionsNegative = new ArrayList<Double>(); 767 for(int i=0; i < xaTickMarks.size(); i++) { 768 double pos = xa.getDisplayPosition((X) xaTickMarks.get(i).getValue()); 769 if (pos == xAxisZero) { 770 tickPositionsPositive.add(pos); 771 tickPositionsNegative.add(pos); 772 } else if (pos < xAxisZero) { 773 tickPositionsPositive.add(pos); 774 } else { 775 tickPositionsNegative.add(pos); 776 } 777 } 778 Collections.sort(tickPositionsPositive); 779 Collections.sort(tickPositionsNegative); 780 // iterate over every pair of positive tick marks and create fill 781 for(int i=1; i < tickPositionsPositive.size(); i+=2) { 782 if((i+1) < tickPositionsPositive.size()) { 783 final double x1 = tickPositionsPositive.get(i); 784 final double x2 = tickPositionsPositive.get(i+1); 785 verticalRowFill.getElements().addAll( 786 new MoveTo(left+x1,top), 787 new LineTo(left+x1,top+yAxisHeight), 788 new LineTo(left+x2,top+yAxisHeight), 789 new LineTo(left+x2,top), 790 new ClosePath()); 791 } 792 } 793 // iterate over every pair of positive tick marks and create fill 794 for(int i=0; i < tickPositionsNegative.size(); i+=2) { 795 if((i+1) < tickPositionsNegative.size()) { 796 final double x1 = tickPositionsNegative.get(i); 797 final double x2 = tickPositionsNegative.get(i+1); 798 verticalRowFill.getElements().addAll( 799 new MoveTo(left+x1,top), 800 new LineTo(left+x1,top+yAxisHeight), 801 new LineTo(left+x2,top+yAxisHeight), 802 new LineTo(left+x2,top), 803 new ClosePath()); 804 } 805 } 806 } 807 // update horizontal row fill 808 horizontalRowFill.getElements().clear(); 809 if (isAlternativeRowFillVisible()) { 810 // tick marks are not sorted so get all the positions and sort them 811 final List<Double> tickPositionsPositive = new ArrayList<Double>(); 812 final List<Double> tickPositionsNegative = new ArrayList<Double>(); 813 for(int i=0; i < yaTickMarks.size(); i++) { 814 double pos = ya.getDisplayPosition((Y) yaTickMarks.get(i).getValue()); 815 if (pos == yAxisZero) { 816 tickPositionsPositive.add(pos); 817 tickPositionsNegative.add(pos); 818 } else if (pos < yAxisZero) { 819 tickPositionsPositive.add(pos); 820 } else { 821 tickPositionsNegative.add(pos); 822 } 823 } 824 Collections.sort(tickPositionsPositive); 825 Collections.sort(tickPositionsNegative); 826 // iterate over every pair of positive tick marks and create fill 827 for(int i=1; i < tickPositionsPositive.size(); i+=2) { 828 if((i+1) < tickPositionsPositive.size()) { 829 final double y1 = tickPositionsPositive.get(i); 830 final double y2 = tickPositionsPositive.get(i+1); 831 horizontalRowFill.getElements().addAll( 832 new MoveTo(left, top + y1), 833 new LineTo(left + xAxisWidth, top + y1), 834 new LineTo(left + xAxisWidth, top + y2), 835 new LineTo(left, top + y2), 836 new ClosePath()); 837 } 838 } 839 // iterate over every pair of positive tick marks and create fill 840 for(int i=0; i < tickPositionsNegative.size(); i+=2) { 841 if((i+1) < tickPositionsNegative.size()) { 842 final double y1 = tickPositionsNegative.get(i); 843 final double y2 = tickPositionsNegative.get(i+1); 844 horizontalRowFill.getElements().addAll( 845 new MoveTo(left, top + y1), 846 new LineTo(left + xAxisWidth, top + y1), 847 new LineTo(left + xAxisWidth, top + y2), 848 new LineTo(left, top + y2), 849 new ClosePath()); 850 } 851 } 852 } 853 // 854 } 855 856 /** 857 * Get the index of the series in the series linked list. 858 * 859 * @param series The series to find index for 860 * @return index of the series in series list 861 */ 862 int getSeriesIndex(Series<X,Y> series) { 863 return displayedSeries.indexOf(series); 864 } 865 866 /** 867 * Computes the size of series linked list 868 * @return size of series linked list 869 */ 870 int getSeriesSize() { 871 return displayedSeries.size(); 872 } 873 874 /** 875 * This should be called from seriesRemoved() when you are finished with any animation for deleting the series from 876 * the chart. It will remove the series from showing up in the Iterator returned by getDisplayedSeriesIterator(). 877 * 878 * @param series The series to remove 879 */ 880 protected final void removeSeriesFromDisplay(Series<X, Y> series) { 881 if (series != null) series.setToRemove = false; 882 series.setChart(null); 883 displayedSeries.remove(series); 884 int idx = seriesColorMap.remove(series); 885 colorBits.clear(idx); 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 }