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