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