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