1 /*
   2  * Copyright (c) 2011, 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.*;
  30 
  31 import javafx.animation.*;
  32 import javafx.application.Platform;
  33 import javafx.beans.NamedArg;
  34 import javafx.beans.property.DoubleProperty;
  35 import javafx.beans.value.WritableValue;
  36 import javafx.collections.FXCollections;
  37 import javafx.collections.ObservableList;
  38 import javafx.geometry.Orientation;
  39 import javafx.scene.AccessibleRole;
  40 import javafx.scene.Node;
  41 import javafx.scene.layout.StackPane;
  42 import javafx.util.Duration;
  43 
  44 import com.sun.javafx.charts.Legend;
  45 
  46 import javafx.css.StyleableDoubleProperty;
  47 import javafx.css.CssMetaData;
  48 import javafx.css.PseudoClass;
  49 
  50 import javafx.css.converter.SizeConverter;
  51 
  52 import javafx.collections.ListChangeListener;
  53 import javafx.css.Styleable;
  54 import javafx.css.StyleableProperty;
  55 
  56 
  57 /**
  58  * StackedBarChart is a variation of {@link BarChart} that plots bars indicating
  59  * data values for a category. The bars can be vertical or horizontal depending
  60  * on which axis is a category axis.
  61  * The bar for each series is stacked on top of the previous series.
  62  * @since JavaFX 2.1
  63  */
  64 public class StackedBarChart<X, Y> extends XYChart<X, Y> {
  65 
  66     // -------------- PRIVATE FIELDS -------------------------------------------
  67     private Map<Series, Map<String, List<Data<X, Y>>>> seriesCategoryMap =
  68                          new HashMap<Series, Map<String, List<Data<X, Y>>>>();
  69     private Legend legend = new Legend();
  70     private final Orientation orientation;
  71     private CategoryAxis categoryAxis;
  72     private ValueAxis valueAxis;
  73     // RT-23125 handling data removal when a category is removed.
  74     private ListChangeListener<String> categoriesListener = new ListChangeListener<String>() {
  75         @Override public void onChanged(ListChangeListener.Change<? extends String> c) {
  76             while (c.next()) {
  77                 for(String cat : c.getRemoved()) {
  78                     for (Series<X,Y> series : getData()) {
  79                         for (Data<X, Y> data : series.getData()) {
  80                             if ((cat).equals((orientation == orientation.VERTICAL) ?
  81                                     data.getXValue() : data.getYValue())) {
  82                                 boolean animatedOn = getAnimated();
  83                                 setAnimated(false);
  84                                 dataItemRemoved(data, series);
  85                                 setAnimated(animatedOn);
  86                             }
  87                         }
  88                     }
  89                     requestChartLayout();
  90                 }
  91             }
  92         }
  93     };
  94 
  95     // -------------- PUBLIC PROPERTIES ----------------------------------------
  96     /** The gap to leave between bars in separate categories */
  97     private DoubleProperty categoryGap = new StyleableDoubleProperty(10) {
  98         @Override protected void invalidated() {
  99             get();
 100             requestChartLayout();
 101         }
 102 
 103         @Override
 104         public Object getBean() {
 105             return StackedBarChart.this;
 106         }
 107 
 108         @Override
 109         public String getName() {
 110             return "categoryGap";
 111         }
 112 
 113         public CssMetaData<StackedBarChart<?,?>,Number> getCssMetaData() {
 114             return StackedBarChart.StyleableProperties.CATEGORY_GAP;
 115         }
 116     };
 117 
 118     public double getCategoryGap() {
 119         return categoryGap.getValue();
 120     }
 121 
 122     public void setCategoryGap(double value) {
 123         categoryGap.setValue(value);
 124     }
 125 
 126     public DoubleProperty categoryGapProperty() {
 127         return categoryGap;
 128     }
 129 
 130     // -------------- CONSTRUCTOR ----------------------------------------------
 131     /**
 132      * Construct a new StackedBarChart with the given axis. The two axis should be a ValueAxis/NumberAxis and a CategoryAxis,
 133      * they can be in either order depending on if you want a horizontal or vertical bar chart.
 134      *
 135      * @param xAxis The x axis to use
 136      * @param yAxis The y axis to use
 137      */
 138     public StackedBarChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis) {
 139         this(xAxis, yAxis, FXCollections.<Series<X, Y>>observableArrayList());
 140     }
 141 
 142     /**
 143      * Construct a new StackedBarChart with the given axis and data. The two axis should be a ValueAxis/NumberAxis and a
 144      * CategoryAxis, they can be in either order depending on if you want a horizontal or vertical bar chart.
 145      *
 146      * @param xAxis The x axis to use
 147      * @param yAxis The y axis to use
 148      * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart
 149      */
 150     public StackedBarChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis, @NamedArg("data") ObservableList<Series<X, Y>> data) {
 151         super(xAxis, yAxis);
 152         getStyleClass().add("stacked-bar-chart");
 153         setLegend(legend);
 154         if (!((xAxis instanceof ValueAxis && yAxis instanceof CategoryAxis)
 155                 || (yAxis instanceof ValueAxis && xAxis instanceof CategoryAxis))) {
 156             throw new IllegalArgumentException("Axis type incorrect, one of X,Y should be CategoryAxis and the other NumberAxis");
 157         }
 158         if (xAxis instanceof CategoryAxis) {
 159             categoryAxis = (CategoryAxis) xAxis;
 160             valueAxis = (ValueAxis) yAxis;
 161             orientation = Orientation.VERTICAL;
 162         } else {
 163             categoryAxis = (CategoryAxis) yAxis;
 164             valueAxis = (ValueAxis) xAxis;
 165             orientation = Orientation.HORIZONTAL;
 166         }
 167         // update css
 168         pseudoClassStateChanged(HORIZONTAL_PSEUDOCLASS_STATE, orientation == Orientation.HORIZONTAL);
 169         pseudoClassStateChanged(VERTICAL_PSEUDOCLASS_STATE, orientation == Orientation.VERTICAL);
 170         setData(data);
 171         categoryAxis.getCategories().addListener(categoriesListener);
 172     }
 173 
 174     /**
 175      * Construct a new StackedBarChart with the given axis and data. The two axis should be a ValueAxis/NumberAxis and a
 176      * CategoryAxis, they can be in either order depending on if you want a horizontal or vertical bar chart.
 177      *
 178      * @param xAxis The x axis to use
 179      * @param yAxis The y axis to use
 180      * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart
 181      * @param categoryGap The gap to leave between bars in separate categories
 182      */
 183     public StackedBarChart(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis, @NamedArg("data") ObservableList<Series<X, Y>> data, @NamedArg("categoryGap") double categoryGap) {
 184         this(xAxis, yAxis);
 185         setData(data);
 186         setCategoryGap(categoryGap);
 187     }
 188 
 189     // -------------- METHODS --------------------------------------------------
 190     @Override protected void dataItemAdded(Series<X, Y> series, int itemIndex, Data<X, Y> item) {
 191         String category;
 192         if (orientation == Orientation.VERTICAL) {
 193             category = (String) item.getXValue();
 194         } else {
 195             category = (String) item.getYValue();
 196         }
 197         // Don't plot if category does not already exist ?
 198 //        if (!categoryAxis.getCategories().contains(category)) return;
 199 
 200         Map<String, List<Data<X, Y>>> categoryMap = seriesCategoryMap.get(series);
 201 
 202         if (categoryMap == null) {
 203             categoryMap = new HashMap<String, List<Data<X, Y>>>();
 204             seriesCategoryMap.put(series, categoryMap);
 205         }
 206         // list to hold more that one bar "positive and negative"
 207         List<Data<X, Y>> itemList = categoryMap.get(category) != null ? categoryMap.get(category) : new ArrayList<Data<X, Y>>();
 208         itemList.add(item);
 209         categoryMap.put(category, itemList);
 210 //        categoryMap.put(category, item);
 211         Node bar = createBar(series, getData().indexOf(series), item, itemIndex);
 212         if (shouldAnimate()) {
 213             animateDataAdd(item, bar);
 214         } else {
 215             getPlotChildren().add(bar);
 216         }
 217     }
 218 
 219     @Override protected void dataItemRemoved(final Data<X, Y> item, final Series<X, Y> series) {
 220         final Node bar = item.getNode();
 221 
 222         if (bar != null) {
 223             bar.focusTraversableProperty().unbind();
 224         }
 225 
 226         if (shouldAnimate()) {
 227             Timeline t = createDataRemoveTimeline(item, bar, series);
 228             t.setOnFinished(event -> {
 229                 removeDataItemFromDisplay(series, item);
 230             });
 231             t.play();
 232         } else {
 233             getPlotChildren().remove(bar);
 234             removeDataItemFromDisplay(series, item);
 235         }
 236     }
 237 
 238     /** @inheritDoc */
 239     @Override protected void dataItemChanged(Data<X, Y> item) {
 240         double barVal;
 241         double currentVal;
 242         if (orientation == Orientation.VERTICAL) {
 243             barVal = ((Number) item.getYValue()).doubleValue();
 244             currentVal = ((Number) getCurrentDisplayedYValue(item)).doubleValue();
 245         } else {
 246             barVal = ((Number) item.getXValue()).doubleValue();
 247             currentVal = ((Number) getCurrentDisplayedXValue(item)).doubleValue();
 248         }
 249         if (currentVal > 0 && barVal < 0) { // going from positive to negative
 250             // add style class negative
 251             item.getNode().getStyleClass().add("negative");
 252         } else if (currentVal < 0 && barVal > 0) { // going from negative to positive
 253             // remove style class negative
 254             item.getNode().getStyleClass().remove("negative");
 255         }
 256     }
 257 
 258     private void animateDataAdd(Data<X, Y> item, Node bar) {
 259         double barVal;
 260         if (orientation == Orientation.VERTICAL) {
 261             barVal = ((Number) item.getYValue()).doubleValue();
 262             if (barVal < 0) {
 263                 bar.getStyleClass().add("negative");
 264             }
 265             item.setYValue(getYAxis().toRealValue(getYAxis().getZeroPosition()));
 266             setCurrentDisplayedYValue(item, getYAxis().toRealValue(getYAxis().getZeroPosition()));
 267             getPlotChildren().add(bar);
 268             item.setYValue(getYAxis().toRealValue(barVal));
 269             animate(new Timeline(
 270                         new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedYValueProperty(item), getCurrentDisplayedYValue(item))),
 271                         new KeyFrame(Duration.millis(700), new KeyValue(currentDisplayedYValueProperty(item), item.getYValue(), Interpolator.EASE_BOTH)))
 272                     );
 273         } else {
 274             barVal = ((Number) item.getXValue()).doubleValue();
 275             if (barVal < 0) {
 276                 bar.getStyleClass().add("negative");
 277             }
 278             item.setXValue(getXAxis().toRealValue(getXAxis().getZeroPosition()));
 279             setCurrentDisplayedXValue(item, getXAxis().toRealValue(getXAxis().getZeroPosition()));
 280             getPlotChildren().add(bar);
 281             item.setXValue(getXAxis().toRealValue(barVal));
 282             animate(new Timeline(
 283                         new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedXValueProperty(item), getCurrentDisplayedXValue(item))),
 284                         new KeyFrame(Duration.millis(700), new KeyValue(currentDisplayedXValueProperty(item), item.getXValue(), Interpolator.EASE_BOTH)))
 285                     );
 286         }
 287     }
 288 
 289     @Override protected void seriesChanged(ListChangeListener.Change<? extends Series> c) {
 290         // Update style classes for all series lines and symbols
 291         // Note: is there a more efficient way of doing this?
 292         for (int i = 0; i < getDataSize(); i++) {
 293             final Series<X,Y> series = getData().get(i);
 294             for (int j=0; j<series.getData().size(); j++) {
 295                 Data<X,Y> item = series.getData().get(j);
 296                 Node bar = item.getNode();
 297                 bar.getStyleClass().setAll("chart-bar", "series" + i, "data" + j, series.defaultColorStyleClass);
 298             }
 299         }
 300     }
 301 
 302     /** @inheritDoc */
 303     @Override protected void seriesAdded(Series<X, Y> series, int seriesIndex) {
 304         // handle any data already in series
 305         // create entry in the map
 306         Map<String, List<Data<X, Y>>> categoryMap = new HashMap<String, List<Data<X, Y>>>();
 307         for (int j = 0; j < series.getData().size(); j++) {
 308             Data<X, Y> item = series.getData().get(j);
 309             Node bar = createBar(series, seriesIndex, item, j);
 310             String category;
 311             if (orientation == Orientation.VERTICAL) {
 312                 category = (String) item.getXValue();
 313             } else {
 314                 category = (String) item.getYValue();
 315             }
 316             // list of two item positive and negative
 317             List<Data<X, Y>> itemList = categoryMap.get(category) != null ? categoryMap.get(category) : new ArrayList<Data<X, Y>>();
 318             itemList.add(item);
 319             categoryMap.put(category, itemList);
 320             if (shouldAnimate()) {
 321                 animateDataAdd(item, bar);
 322             } else {
 323                 double barVal = (orientation == Orientation.VERTICAL) ? ((Number)item.getYValue()).doubleValue() :
 324                     ((Number)item.getXValue()).doubleValue();
 325                 if (barVal < 0) {
 326                     bar.getStyleClass().add("negative");
 327                 }
 328                 getPlotChildren().add(bar);
 329             }
 330         }
 331         if (categoryMap.size() > 0) {
 332             seriesCategoryMap.put(series, categoryMap);
 333         }
 334     }
 335 
 336     private Timeline createDataRemoveTimeline(Data<X, Y> item, final Node bar, final Series<X, Y> series) {
 337         Timeline t = new Timeline();
 338         if (orientation == Orientation.VERTICAL) {
 339             item.setYValue(getYAxis().toRealValue(getYAxis().getZeroPosition()));
 340             t.getKeyFrames().addAll(
 341                     new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedYValueProperty(item),
 342                     getCurrentDisplayedYValue(item))),
 343                     new KeyFrame(Duration.millis(700), actionEvent -> {
 344                         getPlotChildren().remove(bar);
 345                     },
 346                     new KeyValue(currentDisplayedYValueProperty(item), item.getYValue(), Interpolator.EASE_BOTH)));
 347         } else {
 348             item.setXValue(getXAxis().toRealValue(getXAxis().getZeroPosition()));
 349             t.getKeyFrames().addAll(
 350                     new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedXValueProperty(item),
 351                     getCurrentDisplayedXValue(item))),
 352                     new KeyFrame(Duration.millis(700), actionEvent -> {
 353                         getPlotChildren().remove(bar);
 354                     },
 355                     new KeyValue(currentDisplayedXValueProperty(item), item.getXValue(), Interpolator.EASE_BOTH)));
 356         }
 357         return t;
 358     }
 359 
 360     @Override protected void seriesRemoved(final Series<X, Y> series) {
 361         // remove all symbol nodes
 362         if (shouldAnimate()) {
 363             ParallelTransition pt = new ParallelTransition();
 364             pt.setOnFinished(event -> {
 365                 removeSeriesFromDisplay(series);
 366                 requestChartLayout();
 367             });
 368             for (Data<X, Y> d : series.getData()) {
 369                 final Node bar = d.getNode();
 370                 // Animate series deletion
 371                 if (getSeriesSize() > 1) {
 372                     for (int j = 0; j < series.getData().size(); j++) {
 373                         Data<X, Y> item = series.getData().get(j);
 374                         Timeline t = createDataRemoveTimeline(item, bar, series);
 375                         pt.getChildren().add(t);
 376                     }
 377                 } else {
 378                     // fade out last series
 379                     FadeTransition ft = new FadeTransition(Duration.millis(700), bar);
 380                     ft.setFromValue(1);
 381                     ft.setToValue(0);
 382                     ft.setOnFinished(actionEvent -> {
 383                         getPlotChildren().remove(bar);
 384                         bar.setOpacity(1.0);
 385                     });
 386                     pt.getChildren().add(ft);
 387                 }
 388             }
 389             pt.play();
 390         } else {
 391             for (Data<X, Y> d : series.getData()) {
 392                 final Node bar = d.getNode();
 393                 getPlotChildren().remove(bar);
 394             }
 395             removeSeriesFromDisplay(series);
 396             requestChartLayout();
 397         }
 398     }
 399 
 400     /** @inheritDoc */
 401     @Override protected void updateAxisRange() {
 402         // This override is necessary to update axis range based on cumulative Y value for the
 403         // Y axis instead of the inherited way where the max value in the data range is used.
 404         boolean categoryIsX = categoryAxis == getXAxis();
 405         if (categoryAxis.isAutoRanging()) {
 406             List cData = new ArrayList();
 407             for (Series<X, Y> series : getData()) {
 408                 for (Data<X, Y> data : series.getData()) {
 409                     if (data != null) cData.add(categoryIsX ? data.getXValue() : data.getYValue());
 410                 }
 411             }
 412             categoryAxis.invalidateRange(cData);
 413         }
 414         if (valueAxis.isAutoRanging()) {
 415             List<Number> vData = new ArrayList<>();
 416             for (String category : categoryAxis.getAllDataCategories()) {
 417                 double totalXN = 0;
 418                 double totalXP = 0;
 419                 Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator();
 420                 while (seriesIterator.hasNext()) {
 421                     Series<X, Y> series = seriesIterator.next();
 422                     for (final Data<X, Y> item : getDataItem(series, category)) {
 423                         if (item != null) {
 424                             boolean isNegative = item.getNode().getStyleClass().contains("negative");
 425                             Number value = (Number) (categoryIsX ? item.getYValue() : item.getXValue());
 426                             if (!isNegative) {
 427                                 totalXP += valueAxis.toNumericValue(value);
 428                             } else {
 429                                 totalXN += valueAxis.toNumericValue(value);
 430                             }
 431                         }
 432                     }
 433                 }
 434                 vData.add(totalXP);
 435                 vData.add(totalXN);
 436             }
 437             valueAxis.invalidateRange(vData);
 438         }
 439     }
 440 
 441     /** @inheritDoc */
 442     @Override protected void layoutPlotChildren() {
 443         double catSpace = categoryAxis.getCategorySpacing();
 444         // calculate bar spacing
 445         final double availableBarSpace = catSpace - getCategoryGap();
 446         final double barWidth = availableBarSpace;
 447         final double barOffset = -((catSpace - getCategoryGap()) / 2);
 448         final double lowerBoundValue = valueAxis.getLowerBound();
 449         final double upperBoundValue = valueAxis.getUpperBound();
 450         // update bar positions and sizes
 451         for (String category : categoryAxis.getCategories()) {
 452             double currentPositiveValue = 0;
 453             double currentNegativeValue = 0;
 454             Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator();
 455             while (seriesIterator.hasNext()) {
 456                 Series<X, Y> series = seriesIterator.next();
 457                 for (final Data<X, Y> item : getDataItem(series, category)) {
 458                     if (item != null) {
 459                         final Node bar = item.getNode();
 460                         final double categoryPos;
 461                         final double valNumber;
 462                         final X xValue = getCurrentDisplayedXValue(item);
 463                         final Y yValue = getCurrentDisplayedYValue(item);
 464                         if (orientation == Orientation.VERTICAL) {
 465                             categoryPos = getXAxis().getDisplayPosition(xValue);
 466                             valNumber = getYAxis().toNumericValue(yValue);
 467                         } else {
 468                             categoryPos = getYAxis().getDisplayPosition(yValue);
 469                             valNumber = getXAxis().toNumericValue(xValue);
 470                         }
 471                         double bottom;
 472                         double top;
 473                         boolean isNegative = bar.getStyleClass().contains("negative");
 474                         if (!isNegative) {
 475                             bottom = valueAxis.getDisplayPosition(currentPositiveValue);
 476                             top = valueAxis.getDisplayPosition(currentPositiveValue + valNumber);
 477                             currentPositiveValue += valNumber;
 478                         } else {
 479                             bottom = valueAxis.getDisplayPosition(currentNegativeValue + valNumber);
 480                             top = valueAxis.getDisplayPosition(currentNegativeValue);
 481                             currentNegativeValue += valNumber;
 482                         }
 483 
 484                         if (orientation == Orientation.VERTICAL) {
 485                             bar.resizeRelocate(categoryPos + barOffset,
 486                                     top, barWidth, bottom - top);
 487                         } else {
 488                             bar.resizeRelocate(bottom,
 489                                     categoryPos + barOffset,
 490                                     top - bottom, barWidth);
 491                         }
 492                     }
 493                 }
 494             }
 495         }
 496     }
 497 
 498     /**
 499      * Computes the size of series linked list
 500      * @return size of series linked list
 501      */
 502     @Override int getSeriesSize() {
 503         int count = 0;
 504         Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator();
 505         while (seriesIterator.hasNext()) {
 506             seriesIterator.next();
 507             count++;
 508         }
 509         return count;
 510     }
 511 
 512     /**
 513      * This is called whenever a series is added or removed and the legend needs to be updated
 514      */
 515     @Override protected void updateLegend() {
 516         legend.getItems().clear();
 517         if (getData() != null) {
 518             for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) {
 519                 Series<X,Y> series = getData().get(seriesIndex);
 520                 Legend.LegendItem legenditem = new Legend.LegendItem(series.getName());
 521                 legenditem.getSymbol().getStyleClass().addAll("chart-bar", "series" + seriesIndex, "bar-legend-symbol",
 522                         series.defaultColorStyleClass);
 523                 legend.getItems().add(legenditem);
 524             }
 525         }
 526         if (legend.getItems().size() > 0) {
 527             if (getLegend() == null) {
 528                 setLegend(legend);
 529             }
 530         } else {
 531             setLegend(null);
 532         }
 533     }
 534 
 535     private Node createBar(Series series, int seriesIndex, final Data item, int itemIndex) {
 536         Node bar = item.getNode();
 537         if (bar == null) {
 538             bar = new StackPane();
 539             bar.setAccessibleRole(AccessibleRole.TEXT);
 540             bar.setAccessibleRoleDescription("Bar");
 541             bar.focusTraversableProperty().bind(Platform.accessibilityActiveProperty());
 542             item.setNode(bar);
 543         }
 544         bar.getStyleClass().setAll("chart-bar", "series" + seriesIndex, "data" + itemIndex, series.defaultColorStyleClass);
 545         return bar;
 546     }
 547 
 548     private List<Data<X, Y>> getDataItem(Series<X, Y> series, String category) {
 549         Map<String, List<Data<X, Y>>> catmap = seriesCategoryMap.get(series);
 550         return catmap != null ? catmap.get(category) != null ?
 551             catmap.get(category) : new ArrayList<Data<X, Y>>() : new ArrayList<Data<X, Y>>();
 552     }
 553 
 554 // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------
 555 
 556     /*
 557      * Super-lazy instantiation pattern from Bill Pugh.
 558      */
 559     private static class StyleableProperties {
 560 
 561         private static final CssMetaData<StackedBarChart<?,?>,Number> CATEGORY_GAP =
 562             new CssMetaData<StackedBarChart<?,?>,Number>("-fx-category-gap",
 563                 SizeConverter.getInstance(), 10.0)  {
 564 
 565             @Override
 566             public boolean isSettable(StackedBarChart<?,?> node) {
 567                 return node.categoryGap == null || !node.categoryGap.isBound();
 568             }
 569 
 570             @Override
 571             public StyleableProperty<Number> getStyleableProperty(StackedBarChart<?,?> node) {
 572                 return (StyleableProperty<Number>)(WritableValue<Number>)node.categoryGapProperty();
 573             }
 574         };
 575 
 576         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 577         static {
 578 
 579             final List<CssMetaData<? extends Styleable, ?>> styleables =
 580                 new ArrayList<CssMetaData<? extends Styleable, ?>>(XYChart.getClassCssMetaData());
 581             styleables.add(CATEGORY_GAP);
 582             STYLEABLES = Collections.unmodifiableList(styleables);
 583         }
 584     }
 585 
 586     /**
 587      * @return The CssMetaData associated with this class, which may include the
 588      * CssMetaData of its super classes.
 589      * @since JavaFX 8.0
 590      */
 591     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 592         return StyleableProperties.STYLEABLES;
 593     }
 594 
 595     /**
 596      * {@inheritDoc}
 597      * @since JavaFX 8.0
 598      */
 599     @Override
 600     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 601         return getClassCssMetaData();
 602     }
 603 
 604     /** Pseudoclass indicating this is a vertical chart. */
 605     private static final PseudoClass VERTICAL_PSEUDOCLASS_STATE =
 606             PseudoClass.getPseudoClass("vertical");
 607     /** Pseudoclass indicating this is a horizontal chart. */
 608     private static final PseudoClass HORIZONTAL_PSEUDOCLASS_STATE =
 609             PseudoClass.getPseudoClass("horizontal");
 610 
 611 }