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