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      * Creates an array of KeyFrames for fading out nodes representing a series
 929      *
 930      * @param series The series to remove
 931      * @param fadeOutTime Time to fade out, in milliseconds
 932      * @return array of two KeyFrames from zero to fadeOutTime
 933      */
 934     protected final KeyFrame[] createSeriesRemoveTimeLine(Series<X, Y> series, long fadeOutTime) {
 935         final List<Node> nodes = new ArrayList<>();
 936         nodes.add(series.getNode());
 937         for (Data<X, Y> d : series.getData()) {
 938             if (d.getNode() != null) {
 939                 nodes.add(d.getNode());
 940             }
 941         }
 942         // fade out series node and symbols
 943         KeyValue[] startValues = new KeyValue[nodes.size()];
 944         KeyValue[] endValues = new KeyValue[nodes.size()];
 945         for (int j = 0; j < nodes.size(); j++) {
 946             startValues[j] = new KeyValue(nodes.get(j).opacityProperty(), 1);
 947             endValues[j] = new KeyValue(nodes.get(j).opacityProperty(), 0);
 948         }
 949         return new KeyFrame[] {
 950             new KeyFrame(Duration.ZERO, startValues),
 951             new KeyFrame(Duration.millis(fadeOutTime), actionEvent -> {
 952                 getPlotChildren().removeAll(nodes);
 953                 removeSeriesFromDisplay(series);
 954             }, endValues)
 955         };
 956     }
 957     /**
 958      * The current displayed data value plotted on the X axis. This may be the same as xValue or different. It is
 959      * used by XYChart to animate the xValue from the old value to the new value. This is what you should plot
 960      * in any custom XYChart implementations. Some XYChart chart implementations such as LineChart also use this
 961      * to animate when data is added or removed.
 962      */
 963     protected final X getCurrentDisplayedXValue(Data<X,Y> item) { return item.getCurrentX(); }
 964 
 965     /** Set the current displayed data value plotted on X axis.
 966      *
 967      * @param item The XYChart.Data item from which the current X axis data value is obtained.
 968      * @see #getCurrentDisplayedXValue
 969      */
 970     protected final void setCurrentDisplayedXValue(Data<X,Y> item, X value) { item.setCurrentX(value); }
 971 
 972     /** The current displayed data value property that is plotted on X axis.
 973      *
 974      * @param item The XYChart.Data item from which the current X axis data value property object is obtained.
 975      * @return The current displayed X data value ObjectProperty.
 976      * @see #getCurrentDisplayedXValue
 977      */
 978     protected final ObjectProperty<X> currentDisplayedXValueProperty(Data<X,Y> item) { return item.currentXProperty(); }
 979 
 980     /**
 981      * The current displayed data value plotted on the Y axis. This may be the same as yValue or different. It is
 982      * used by XYChart to animate the yValue from the old value to the new value. This is what you should plot
 983      * in any custom XYChart implementations. Some XYChart chart implementations such as LineChart also use this
 984      * to animate when data is added or removed.
 985      */
 986     protected final Y getCurrentDisplayedYValue(Data<X,Y> item) { return item.getCurrentY(); }
 987 
 988     /**
 989      * Set the current displayed data value plotted on Y axis.
 990      *
 991      * @param item The XYChart.Data item from which the current Y axis data value is obtained.
 992      * @see #getCurrentDisplayedYValue
 993      */
 994     protected final void setCurrentDisplayedYValue(Data<X,Y> item, Y value) { item.setCurrentY(value); }
 995 
 996     /** The current displayed data value property that is plotted on Y axis.
 997      *
 998      * @param item The XYChart.Data item from which the current Y axis data value property object is obtained.
 999      * @return The current displayed Y data value ObjectProperty.
1000      * @see #getCurrentDisplayedYValue
1001      */
1002     protected final ObjectProperty<Y> currentDisplayedYValueProperty(Data<X,Y> item) { return item.currentYProperty(); }
1003 
1004     /**
1005      * The current displayed data extra value. This may be the same as extraValue or different. It is
1006      * used by XYChart to animate the extraValue from the old value to the new value. This is what you should plot
1007      * in any custom XYChart implementations.
1008      */
1009     protected final Object getCurrentDisplayedExtraValue(Data<X,Y> item) { return item.getCurrentExtraValue(); }
1010 
1011     /**
1012      * Set the current displayed data extra value.
1013      *
1014      * @param item The XYChart.Data item from which the current extra value is obtained.
1015      * @see #getCurrentDisplayedExtraValue
1016      */
1017     protected final void setCurrentDisplayedExtraValue(Data<X,Y> item, Object value) { item.setCurrentExtraValue(value); }
1018 
1019     /**
1020      * The current displayed extra value property.
1021      *
1022      * @param item The XYChart.Data item from which the current extra value property object is obtained.
1023      * @return ObjectProperty<Object> The current extra value ObjectProperty
1024      * @see #getCurrentDisplayedExtraValue
1025      */
1026     protected final ObjectProperty<Object> currentDisplayedExtraValueProperty(Data<X,Y> item) { return item.currentExtraValueProperty(); }
1027 
1028     /**
1029      * XYChart maintains a list of all items currently displayed this includes all current data + any data items
1030      * recently deleted that are in the process of being faded out. This creates and returns a iterator over
1031      * that list. This is what implementations of XYChart should use when plotting data.
1032      *
1033      * @param series The series to get displayed data for
1034      * @return iterator over currently displayed items from this series
1035      */
1036     protected final Iterator<Data<X,Y>> getDisplayedDataIterator(final Series<X,Y> series) {
1037         return Collections.unmodifiableList(series.displayedData).iterator();
1038     }
1039 
1040     /**
1041      * This should be called from dataItemRemoved() when you are finished with any animation for deleting the item from the
1042      * chart. It will remove the data item from showing up in the Iterator returned by getDisplayedDataIterator().
1043      *
1044      * @param series The series to remove
1045      * @param item   The item to remove from series's display list
1046      */
1047     protected final void removeDataItemFromDisplay(Series<X, Y> series, Data<X, Y> item) {
1048         series.removeDataItemRef(item);
1049     }
1050 
1051     // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------
1052 
1053     private static class StyleableProperties {
1054         private static final CssMetaData<XYChart<?,?>,Boolean> HORIZONTAL_GRID_LINE_VISIBLE =
1055             new CssMetaData<XYChart<?,?>,Boolean>("-fx-horizontal-grid-lines-visible",
1056                 BooleanConverter.getInstance(), Boolean.TRUE) {
1057 
1058             @Override
1059             public boolean isSettable(XYChart<?,?> node) {
1060                 return node.horizontalGridLinesVisible == null ||
1061                         !node.horizontalGridLinesVisible.isBound();
1062             }
1063 
1064             @Override
1065             public StyleableProperty<Boolean> getStyleableProperty(XYChart<?,?> node) {
1066                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.horizontalGridLinesVisibleProperty();
1067             }
1068         };
1069 
1070         private static final CssMetaData<XYChart<?,?>,Boolean> HORIZONTAL_ZERO_LINE_VISIBLE =
1071             new CssMetaData<XYChart<?,?>,Boolean>("-fx-horizontal-zero-line-visible",
1072                 BooleanConverter.getInstance(), Boolean.TRUE) {
1073 
1074             @Override
1075             public boolean isSettable(XYChart<?,?> node) {
1076                 return node.horizontalZeroLineVisible == null ||
1077                         !node.horizontalZeroLineVisible.isBound();
1078             }
1079 
1080             @Override
1081             public StyleableProperty<Boolean> getStyleableProperty(XYChart<?,?> node) {
1082                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.horizontalZeroLineVisibleProperty();
1083             }
1084         };
1085 
1086         private static final CssMetaData<XYChart<?,?>,Boolean> ALTERNATIVE_ROW_FILL_VISIBLE =
1087             new CssMetaData<XYChart<?,?>,Boolean>("-fx-alternative-row-fill-visible",
1088                 BooleanConverter.getInstance(), Boolean.TRUE) {
1089 
1090             @Override
1091             public boolean isSettable(XYChart<?,?> node) {
1092                 return node.alternativeRowFillVisible == null ||
1093                         !node.alternativeRowFillVisible.isBound();
1094             }
1095 
1096             @Override
1097             public StyleableProperty<Boolean> getStyleableProperty(XYChart<?,?> node) {
1098                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.alternativeRowFillVisibleProperty();
1099             }
1100         };
1101 
1102         private static final CssMetaData<XYChart<?,?>,Boolean> VERTICAL_GRID_LINE_VISIBLE =
1103             new CssMetaData<XYChart<?,?>,Boolean>("-fx-vertical-grid-lines-visible",
1104                 BooleanConverter.getInstance(), Boolean.TRUE) {
1105 
1106             @Override
1107             public boolean isSettable(XYChart<?,?> node) {
1108                 return node.verticalGridLinesVisible == null ||
1109                         !node.verticalGridLinesVisible.isBound();
1110             }
1111 
1112             @Override
1113             public StyleableProperty<Boolean> getStyleableProperty(XYChart<?,?> node) {
1114                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.verticalGridLinesVisibleProperty();
1115             }
1116         };
1117 
1118         private static final CssMetaData<XYChart<?,?>,Boolean> VERTICAL_ZERO_LINE_VISIBLE =
1119             new CssMetaData<XYChart<?,?>,Boolean>("-fx-vertical-zero-line-visible",
1120                 BooleanConverter.getInstance(), Boolean.TRUE) {
1121 
1122             @Override
1123             public boolean isSettable(XYChart<?,?> node) {
1124                 return node.verticalZeroLineVisible == null ||
1125                         !node.verticalZeroLineVisible.isBound();
1126             }
1127 
1128             @Override
1129             public StyleableProperty<Boolean> getStyleableProperty(XYChart<?,?> node) {
1130                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.verticalZeroLineVisibleProperty();
1131             }
1132         };
1133 
1134         private static final CssMetaData<XYChart<?,?>,Boolean> ALTERNATIVE_COLUMN_FILL_VISIBLE =
1135             new CssMetaData<XYChart<?,?>,Boolean>("-fx-alternative-column-fill-visible",
1136                 BooleanConverter.getInstance(), Boolean.TRUE) {
1137 
1138             @Override
1139             public boolean isSettable(XYChart<?,?> node) {
1140                 return node.alternativeColumnFillVisible == null ||
1141                         !node.alternativeColumnFillVisible.isBound();
1142             }
1143 
1144             @Override
1145             public StyleableProperty<Boolean> getStyleableProperty(XYChart<?,?> node) {
1146                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)node.alternativeColumnFillVisibleProperty();
1147             }
1148         };
1149 
1150         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
1151         static {
1152             final List<CssMetaData<? extends Styleable, ?>> styleables =
1153                 new ArrayList<CssMetaData<? extends Styleable, ?>>(Chart.getClassCssMetaData());
1154             styleables.add(HORIZONTAL_GRID_LINE_VISIBLE);
1155             styleables.add(HORIZONTAL_ZERO_LINE_VISIBLE);
1156             styleables.add(ALTERNATIVE_ROW_FILL_VISIBLE);
1157             styleables.add(VERTICAL_GRID_LINE_VISIBLE);
1158             styleables.add(VERTICAL_ZERO_LINE_VISIBLE);
1159             styleables.add(ALTERNATIVE_COLUMN_FILL_VISIBLE);
1160             STYLEABLES = Collections.unmodifiableList(styleables);
1161         }
1162     }
1163 
1164     /**
1165      * @return The CssMetaData associated with this class, which may include the
1166      * CssMetaData of its super classes.
1167      * @since JavaFX 8.0
1168      */
1169     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
1170         return StyleableProperties.STYLEABLES;
1171     }
1172 
1173     /**
1174      * {@inheritDoc}
1175      * @since JavaFX 8.0
1176      */
1177     @Override
1178     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
1179         return getClassCssMetaData();
1180     }
1181 
1182     // -------------- INNER CLASSES ------------------------------------------------------------------------------------
1183 
1184     /**
1185      * A single data item with data for 2 axis charts
1186      * @since JavaFX 2.0
1187      */
1188     public final static class Data<X,Y> {
1189         // -------------- PUBLIC PROPERTIES ----------------------------------------
1190 
1191         private boolean setToRemove = false;
1192         /** The series this data belongs to */
1193         private Series<X,Y> series;
1194         void setSeries(Series<X,Y> series) {
1195             this.series = series;
1196         }
1197 
1198         /** The generic data value to be plotted on the X axis */
1199         private ObjectProperty<X> xValue = new ObjectPropertyBase<X>() {
1200             @Override protected void invalidated() {
1201                 // Note: calling get to make non-lazy, replace with change listener when available
1202                 get();
1203                 if (series!=null) {
1204                     XYChart<X,Y> chart = series.getChart();
1205                     if(chart!=null) chart.dataXValueChanged(Data.this);
1206                 } else {
1207                     // data has not been added to series yet :
1208                     // so currentX and X should be the same
1209                     setCurrentX(get());
1210                 }
1211             }
1212 
1213             @Override
1214             public Object getBean() {
1215                 return Data.this;
1216             }
1217 
1218             @Override
1219             public String getName() {
1220                 return "XValue";
1221             }
1222         };
1223         /**
1224          * Gets the generic data value to be plotted on the X axis.
1225          * @return the generic data value to be plotted on the X axis.
1226          */
1227         public final X getXValue() { return xValue.get(); }
1228         /**
1229          * Sets the generic data value to be plotted on the X axis.
1230          * @param value the generic data value to be plotted on the X axis.
1231          */
1232         public final void setXValue(X value) {
1233             xValue.set(value);
1234             // handle the case where this is a init because the default constructor was used
1235             // and the case when series is not associated to a chart due to a remove series
1236             if (currentX.get() == null ||
1237                     (series != null && series.getChart() == null)) currentX.setValue(value);
1238         }
1239         /**
1240          * The generic data value to be plotted on the X axis.
1241          * @return The XValue property
1242          */
1243         public final ObjectProperty<X> XValueProperty() { return xValue; }
1244 
1245         /** The generic data value to be plotted on the Y axis */
1246         private ObjectProperty<Y> yValue = new ObjectPropertyBase<Y>() {
1247             @Override protected void invalidated() {
1248                 // Note: calling get to make non-lazy, replace with change listener when available
1249                 get();
1250                 if (series!=null) {
1251                     XYChart<X,Y> chart = series.getChart();
1252                     if(chart!=null) chart.dataYValueChanged(Data.this);
1253                 } else {
1254                     // data has not been added to series yet :
1255                     // so currentY and Y should be the same
1256                     setCurrentY(get());
1257                 }
1258             }
1259 
1260             @Override
1261             public Object getBean() {
1262                 return Data.this;
1263             }
1264 
1265             @Override
1266             public String getName() {
1267                 return "YValue";
1268             }
1269         };
1270         /**
1271          * Gets the generic data value to be plotted on the Y axis.
1272          * @return the generic data value to be plotted on the Y axis.
1273          */
1274         public final Y getYValue() { return yValue.get(); }
1275         /**
1276          * Sets the generic data value to be plotted on the Y axis.
1277          * @param value the generic data value to be plotted on the Y axis.
1278          */
1279         public final void setYValue(Y value) {
1280             yValue.set(value);
1281             // handle the case where this is a init because the default constructor was used
1282             // and the case when series is not associated to a chart due to a remove series
1283             if (currentY.get() == null ||
1284                     (series != null && series.getChart() == null)) currentY.setValue(value);
1285 
1286         }
1287         /**
1288          * The generic data value to be plotted on the Y axis.
1289          * @return the YValue property
1290          */
1291         public final ObjectProperty<Y> YValueProperty() { return yValue; }
1292 
1293         /**
1294          * The generic data value to be plotted in any way the chart needs. For example used as the radius
1295          * for BubbleChart.
1296          */
1297         private ObjectProperty<Object> extraValue = new ObjectPropertyBase<Object>() {
1298             @Override protected void invalidated() {
1299                 // Note: calling get to make non-lazy, replace with change listener when available
1300                 get();
1301                 if (series!=null) {
1302                     XYChart<X,Y> chart = series.getChart();
1303                     if(chart!=null) chart.dataExtraValueChanged(Data.this);
1304                 }
1305             }
1306 
1307             @Override
1308             public Object getBean() {
1309                 return Data.this;
1310             }
1311 
1312             @Override
1313             public String getName() {
1314                 return "extraValue";
1315             }
1316         };
1317         public final Object getExtraValue() { return extraValue.get(); }
1318         public final void setExtraValue(Object value) { extraValue.set(value); }
1319         public final ObjectProperty<Object> extraValueProperty() { return extraValue; }
1320 
1321         /**
1322          * The node to display for this data item. You can either create your own node and set it on the data item
1323          * before you add the item to the chart. Otherwise the chart will create a node for you that has the default
1324          * representation for the chart type. This node will be set as soon as the data is added to the chart. You can
1325          * then get it to add mouse listeners etc. Charts will do their best to position and size the node
1326          * appropriately, for example on a Line or Scatter chart this node will be positioned centered on the data
1327          * values position. For a bar chart this is positioned and resized as the bar for this data item.
1328          */
1329         private ObjectProperty<Node> node = new SimpleObjectProperty<Node>(this, "node") {
1330             protected void invalidated() {
1331                 Node node = get();
1332                 if (node != null) {
1333                     node.accessibleTextProperty().unbind();
1334                     node.accessibleTextProperty().bind(new StringBinding() {
1335                         {bind(currentXProperty(), currentYProperty());}
1336                         @Override protected String computeValue() {
1337                             String seriesName = series != null ? series.getName() : "";
1338                             return seriesName + " X Axis is " + getCurrentX() + " Y Axis is " + getCurrentY();
1339                         }
1340                     });
1341                 }
1342             };
1343         };
1344         public final Node getNode() { return node.get(); }
1345         public final void setNode(Node value) { node.set(value); }
1346         public final ObjectProperty<Node> nodeProperty() { return node; }
1347 
1348         /**
1349          * The current displayed data value plotted on the X axis. This may be the same as xValue or different. It is
1350          * used by XYChart to animate the xValue from the old value to the new value. This is what you should plot
1351          * in any custom XYChart implementations. Some XYChart chart implementations such as LineChart also use this
1352          * to animate when data is added or removed.
1353          */
1354         private ObjectProperty<X> currentX = new SimpleObjectProperty<X>(this, "currentX");
1355         final X getCurrentX() { return currentX.get(); }
1356         final void setCurrentX(X value) { currentX.set(value); }
1357         final ObjectProperty<X> currentXProperty() { return currentX; }
1358 
1359         /**
1360          * The current displayed data value plotted on the Y axis. This may be the same as yValue or different. It is
1361          * used by XYChart to animate the yValue from the old value to the new value. This is what you should plot
1362          * in any custom XYChart implementations. Some XYChart chart implementations such as LineChart also use this
1363          * to animate when data is added or removed.
1364          */
1365         private ObjectProperty<Y> currentY = new SimpleObjectProperty<Y>(this, "currentY");
1366         final Y getCurrentY() { return currentY.get(); }
1367         final void setCurrentY(Y value) { currentY.set(value); }
1368         final ObjectProperty<Y> currentYProperty() { return currentY; }
1369 
1370         /**
1371          * The current displayed data extra value. This may be the same as extraValue or different. It is
1372          * used by XYChart to animate the extraValue from the old value to the new value. This is what you should plot
1373          * in any custom XYChart implementations.
1374          */
1375         private ObjectProperty<Object> currentExtraValue = new SimpleObjectProperty<Object>(this, "currentExtraValue");
1376         final Object getCurrentExtraValue() { return currentExtraValue.getValue(); }
1377         final void setCurrentExtraValue(Object value) { currentExtraValue.setValue(value); }
1378         final ObjectProperty<Object> currentExtraValueProperty() { return currentExtraValue; }
1379 
1380         // -------------- CONSTRUCTOR -------------------------------------------------
1381 
1382         /**
1383          * Creates an empty XYChart.Data object.
1384          */
1385         public Data() {}
1386 
1387         /**
1388          * Creates an instance of XYChart.Data object and initializes the X,Y
1389          * data values.
1390          *
1391          * @param xValue The X axis data value
1392          * @param yValue The Y axis data value
1393          */
1394         public Data(X xValue, Y yValue) {
1395             setXValue(xValue);
1396             setYValue(yValue);
1397             setCurrentX(xValue);
1398             setCurrentY(yValue);
1399         }
1400 
1401         /**
1402          * Creates an instance of XYChart.Data object and initializes the X,Y
1403          * data values and extraValue.
1404          *
1405          * @param xValue The X axis data value.
1406          * @param yValue The Y axis data value.
1407          * @param extraValue Chart extra value.
1408          */
1409         public Data(X xValue, Y yValue, Object extraValue) {
1410             setXValue(xValue);
1411             setYValue(yValue);
1412             setExtraValue(extraValue);
1413             setCurrentX(xValue);
1414             setCurrentY(yValue);
1415             setCurrentExtraValue(extraValue);
1416         }
1417 
1418         // -------------- PUBLIC METHODS ----------------------------------------------
1419 
1420         /**
1421          * Returns a string representation of this {@code Data} object.
1422          * @return a string representation of this {@code Data} object.
1423          */
1424         @Override public String toString() {
1425             return "Data["+getXValue()+","+getYValue()+","+getExtraValue()+"]";
1426         }
1427 
1428     }
1429 
1430     /**
1431      * A named series of data items
1432      * @since JavaFX 2.0
1433      */
1434     public static final class Series<X,Y> {
1435 
1436         // -------------- PRIVATE PROPERTIES ----------------------------------------
1437 
1438         /** the style class for default color for this series */
1439         String defaultColorStyleClass;
1440         boolean setToRemove = false;
1441 
1442         private List<Data<X, Y>> displayedData = new ArrayList<>();
1443 
1444         private final ListChangeListener<Data<X,Y>> dataChangeListener = new ListChangeListener<Data<X, Y>>() {
1445             @Override public void onChanged(Change<? extends Data<X, Y>> c) {
1446                 ObservableList<? extends Data<X, Y>> data = c.getList();
1447                 final XYChart<X, Y> chart = getChart();
1448                 while (c.next()) {
1449                     if (chart != null) {
1450                         // RT-25187 Probably a sort happened, just reorder the pointers and return.
1451                         if (c.wasPermutated()) {
1452                             displayedData.sort((o1, o2) -> data.indexOf(o2) - data.indexOf(o1));
1453                             return;
1454                         }
1455 
1456                         Set<Data<X, Y>> dupCheck = new HashSet<>(displayedData);
1457                         dupCheck.removeAll(c.getRemoved());
1458                         for (Data<X, Y> d : c.getAddedSubList()) {
1459                             if (!dupCheck.add(d)) {
1460                                 throw new IllegalArgumentException("Duplicate data added");
1461                             }
1462                         }
1463 
1464                         // update data items reference to series
1465                         for (Data<X, Y> item : c.getRemoved()) {
1466                             item.setToRemove = true;
1467                         }
1468 
1469                         if (c.getAddedSize() > 0) {
1470                             for (Data<X, Y> itemPtr : c.getAddedSubList()) {
1471                                 if (itemPtr.setToRemove) {
1472                                     if (chart != null) chart.dataBeingRemovedIsAdded(itemPtr, Series.this);
1473                                     itemPtr.setToRemove = false;
1474                                 }
1475                             }
1476 
1477                             for (Data<X, Y> d : c.getAddedSubList()) {
1478                                 d.setSeries(Series.this);
1479                             }
1480                             if (c.getFrom() == 0) {
1481                                 displayedData.addAll(0, c.getAddedSubList());
1482                             } else {
1483                                 displayedData.addAll(displayedData.indexOf(data.get(c.getFrom() - 1)) + 1, c.getAddedSubList());
1484                             }
1485                         }
1486                         // inform chart
1487                         chart.dataItemsChanged(Series.this,
1488                                 (List<Data<X, Y>>) c.getRemoved(), c.getFrom(), c.getTo(), c.wasPermutated());
1489                     } else {
1490                         Set<Data<X, Y>> dupCheck = new HashSet<>();
1491                         for (Data<X, Y> d : data) {
1492                             if (!dupCheck.add(d)) {
1493                                 throw new IllegalArgumentException("Duplicate data added");
1494                             }
1495                         }
1496 
1497                         for (Data<X, Y> d : c.getAddedSubList()) {
1498                             d.setSeries(Series.this);
1499                         }
1500 
1501                     }
1502                 }
1503             }
1504         };
1505 
1506         // -------------- PUBLIC PROPERTIES ----------------------------------------
1507 
1508         /** Reference to the chart this series belongs to */
1509         private final ReadOnlyObjectWrapper<XYChart<X,Y>> chart = new ReadOnlyObjectWrapper<XYChart<X,Y>>(this, "chart") {
1510             @Override
1511             protected void invalidated() {
1512                 if (get() == null) {
1513                     displayedData.clear();
1514                 } else {
1515                     displayedData.addAll(getData());
1516                 }
1517             }
1518         };
1519         public final XYChart<X,Y> getChart() { return chart.get(); }
1520         private void setChart(XYChart<X,Y> value) { chart.set(value); }
1521         public final ReadOnlyObjectProperty<XYChart<X,Y>> chartProperty() { return chart.getReadOnlyProperty(); }
1522 
1523         /** The user displayable name for this series */
1524         private final StringProperty name = new StringPropertyBase() {
1525             @Override protected void invalidated() {
1526                 get(); // make non-lazy
1527                 if(getChart() != null) getChart().seriesNameChanged();
1528             }
1529 
1530             @Override
1531             public Object getBean() {
1532                 return Series.this;
1533             }
1534 
1535             @Override
1536             public String getName() {
1537                 return "name";
1538             }
1539         };
1540         public final String getName() { return name.get(); }
1541         public final void setName(String value) { name.set(value); }
1542         public final StringProperty nameProperty() { return name; }
1543 
1544         /**
1545          * The node to display for this series. This is created by the chart if it uses nodes to represent the whole
1546          * series. For example line chart uses this for the line but scatter chart does not use it. This node will be
1547          * set as soon as the series is added to the chart. You can then get it to add mouse listeners etc.
1548          */
1549         private ObjectProperty<Node> node = new SimpleObjectProperty<Node>(this, "node");
1550         public final Node getNode() { return node.get(); }
1551         public final void setNode(Node value) { node.set(value); }
1552         public final ObjectProperty<Node> nodeProperty() { return node; }
1553 
1554         /** ObservableList of data items that make up this series */
1555         private final ObjectProperty<ObservableList<Data<X,Y>>> data = new ObjectPropertyBase<ObservableList<Data<X,Y>>>() {
1556             private ObservableList<Data<X,Y>> old;
1557             @Override protected void invalidated() {
1558                 final ObservableList<Data<X,Y>> current = getValue();
1559                 // add remove listeners
1560                 if(old != null) old.removeListener(dataChangeListener);
1561                 if(current != null) current.addListener(dataChangeListener);
1562                 // fire data change event if series are added or removed
1563                 if(old != null || current != null) {
1564                     final List<Data<X,Y>> removed = (old != null) ? old : Collections.<Data<X,Y>>emptyList();
1565                     final int toIndex = (current != null) ? current.size() : 0;
1566                     // let data listener know all old data have been removed and new data that has been added
1567                     if (toIndex > 0 || !removed.isEmpty()) {
1568                         dataChangeListener.onChanged(new NonIterableChange<Data<X,Y>>(0, toIndex, current){
1569                             @Override public List<Data<X,Y>> getRemoved() { return removed; }
1570 
1571                             @Override protected int[] getPermutation() {
1572                                 return new int[0];
1573                             }
1574                         });
1575                     }
1576                 } else if (old != null && old.size() > 0) {
1577                     // let series listener know all old series have been removed
1578                     dataChangeListener.onChanged(new NonIterableChange<Data<X,Y>>(0, 0, current){
1579                         @Override public List<Data<X,Y>> getRemoved() { return old; }
1580                         @Override protected int[] getPermutation() {
1581                             return new int[0];
1582                         }
1583                     });
1584                 }
1585                 old = current;
1586             }
1587 
1588             @Override
1589             public Object getBean() {
1590                 return Series.this;
1591             }
1592 
1593             @Override
1594             public String getName() {
1595                 return "data";
1596             }
1597         };
1598         public final ObservableList<Data<X,Y>> getData() { return data.getValue(); }
1599         public final void setData(ObservableList<Data<X,Y>> value) { data.setValue(value); }
1600         public final ObjectProperty<ObservableList<Data<X,Y>>> dataProperty() { return data; }
1601 
1602         // -------------- CONSTRUCTORS ----------------------------------------------
1603 
1604         /**
1605          * Construct a empty series
1606          */
1607         public Series() {
1608             this(FXCollections.<Data<X,Y>>observableArrayList());
1609         }
1610 
1611         /**
1612          * Constructs a Series and populates it with the given {@link ObservableList} data.
1613          *
1614          * @param data ObservableList of XYChart.Data
1615          */
1616         public Series(ObservableList<Data<X,Y>> data) {
1617             setData(data);
1618             for(Data<X,Y> item:data) item.setSeries(this);
1619         }
1620 
1621         /**
1622          * Constructs a named Series and populates it with the given {@link ObservableList} data.
1623          *
1624          * @param name a name for the series
1625          * @param data ObservableList of XYChart.Data
1626          */
1627         public Series(String name, ObservableList<Data<X,Y>> data) {
1628             this(data);
1629             setName(name);
1630         }
1631 
1632         // -------------- PUBLIC METHODS ----------------------------------------------
1633 
1634         /**
1635          * Returns a string representation of this {@code Series} object.
1636          * @return a string representation of this {@code Series} object.
1637          */
1638         @Override public String toString() {
1639             return "Series["+getName()+"]";
1640         }
1641 
1642         // -------------- PRIVATE/PROTECTED METHODS -----------------------------------
1643 
1644         /*
1645          * The following methods are for manipulating the pointers in the linked list
1646          * when data is deleted.
1647          */
1648         private void removeDataItemRef(Data<X,Y> item) {
1649             if (item != null) item.setToRemove = false;
1650             displayedData.remove(item);
1651         }
1652 
1653         int getItemIndex(Data<X,Y> item) {
1654             return displayedData.indexOf(item);
1655         }
1656 
1657         Data<X, Y> getItem(int i) {
1658             return displayedData.get(i);
1659         }
1660 
1661         int getDataSize() {
1662             return displayedData.size();
1663         }
1664     }
1665     
1666 }