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