1 /*
   2  * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.chart;
  27 
  28 
  29 import java.util.*;
  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         if (shouldAnimate()) {
 224             Timeline t = createDataRemoveTimeline(item, bar, series);
 225             t.setOnFinished(event -> {
 226                 removeDataItemFromDisplay(series, item);
 227             });
 228             t.play();
 229         } else {
 230             getPlotChildren().remove(bar);
 231             removeDataItemFromDisplay(series, item);
 232         }
 233     }
 234 
 235     /** @inheritDoc */
 236     @Override protected void dataItemChanged(Data<X, Y> item) {
 237         double barVal;
 238         double currentVal;
 239         if (orientation == Orientation.VERTICAL) {
 240             barVal = ((Number) item.getYValue()).doubleValue();
 241             currentVal = ((Number) getCurrentDisplayedYValue(item)).doubleValue();
 242         } else {
 243             barVal = ((Number) item.getXValue()).doubleValue();
 244             currentVal = ((Number) getCurrentDisplayedXValue(item)).doubleValue();
 245         }
 246         if (currentVal > 0 && barVal < 0) { // going from positive to negative
 247             // add style class negative
 248             item.getNode().getStyleClass().add("negative");
 249         } else if (currentVal < 0 && barVal > 0) { // going from negative to positive
 250             // remove style class negative
 251             item.getNode().getStyleClass().remove("negative");
 252         }
 253     }
 254 
 255     private void animateDataAdd(Data<X, Y> item, Node bar) {
 256         double barVal;
 257         if (orientation == Orientation.VERTICAL) {
 258             barVal = ((Number) item.getYValue()).doubleValue();
 259             if (barVal < 0) {
 260                 bar.getStyleClass().add("negative");
 261             }
 262             item.setYValue(getYAxis().toRealValue(getYAxis().getZeroPosition()));
 263             setCurrentDisplayedYValue(item, getYAxis().toRealValue(getYAxis().getZeroPosition()));
 264             getPlotChildren().add(bar);
 265             item.setYValue(getYAxis().toRealValue(barVal));
 266             animate(new Timeline(
 267                         new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedYValueProperty(item), getCurrentDisplayedYValue(item))),
 268                         new KeyFrame(Duration.millis(700), new KeyValue(currentDisplayedYValueProperty(item), item.getYValue(), Interpolator.EASE_BOTH)))
 269                     );
 270         } else {
 271             barVal = ((Number) item.getXValue()).doubleValue();
 272             if (barVal < 0) {
 273                 bar.getStyleClass().add("negative");
 274             }
 275             item.setXValue(getXAxis().toRealValue(getXAxis().getZeroPosition()));
 276             setCurrentDisplayedXValue(item, getXAxis().toRealValue(getXAxis().getZeroPosition()));
 277             getPlotChildren().add(bar);
 278             item.setXValue(getXAxis().toRealValue(barVal));
 279             animate(new Timeline(
 280                         new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedXValueProperty(item), getCurrentDisplayedXValue(item))),
 281                         new KeyFrame(Duration.millis(700), new KeyValue(currentDisplayedXValueProperty(item), item.getXValue(), Interpolator.EASE_BOTH)))
 282                     );
 283         }
 284     }
 285 
 286     /** @inheritDoc */
 287     @Override protected void seriesAdded(Series<X, Y> series, int seriesIndex) {
 288         String defaultColorStyleClass = "default-color" + (seriesDefaultColorIndex % 8);
 289         seriesDefaultColorMap.put(series, defaultColorStyleClass);
 290         seriesDefaultColorIndex++;
 291         // handle any data already in series
 292         // create entry in the map
 293         Map<String, List<Data<X, Y>>> categoryMap = new HashMap<String, List<Data<X, Y>>>();
 294         for (int j = 0; j < series.getData().size(); j++) {
 295             Data<X, Y> item = series.getData().get(j);
 296             Node bar = createBar(series, seriesIndex, item, j);
 297             String category;
 298             if (orientation == Orientation.VERTICAL) {
 299                 category = (String) item.getXValue();
 300             } else {
 301                 category = (String) item.getYValue();
 302             }
 303             // list of two item positive and negative
 304             List<Data<X, Y>> itemList = categoryMap.get(category) != null ? categoryMap.get(category) : new ArrayList<Data<X, Y>>();
 305             itemList.add(item);
 306             categoryMap.put(category, itemList);
 307             if (shouldAnimate()) {
 308                 animateDataAdd(item, bar);
 309             } else {
 310                 double barVal = (orientation == Orientation.VERTICAL) ? ((Number)item.getYValue()).doubleValue() :
 311                     ((Number)item.getXValue()).doubleValue();
 312                 if (barVal < 0) {
 313                     bar.getStyleClass().add("negative");
 314                 }
 315                 getPlotChildren().add(bar);
 316             }
 317         }
 318         if (categoryMap.size() > 0) {
 319             seriesCategoryMap.put(series, categoryMap);
 320         }
 321     }
 322 
 323     private Timeline createDataRemoveTimeline(Data<X, Y> item, final Node bar, final Series<X, Y> series) {
 324         Timeline t = new Timeline();
 325         if (orientation == Orientation.VERTICAL) {
 326             item.setYValue(getYAxis().toRealValue(getYAxis().getZeroPosition()));
 327             t.getKeyFrames().addAll(
 328                     new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedYValueProperty(item), 
 329                     getCurrentDisplayedYValue(item))),
 330                     new KeyFrame(Duration.millis(700), actionEvent -> {
 331                         getPlotChildren().remove(bar);
 332                     },
 333                     new KeyValue(currentDisplayedYValueProperty(item), item.getYValue(), Interpolator.EASE_BOTH)));
 334         } else {
 335             item.setXValue(getXAxis().toRealValue(getXAxis().getZeroPosition()));
 336             t.getKeyFrames().addAll(
 337                     new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedXValueProperty(item), 
 338                     getCurrentDisplayedXValue(item))),
 339                     new KeyFrame(Duration.millis(700), actionEvent -> {
 340                         getPlotChildren().remove(bar);
 341                     },
 342                     new KeyValue(currentDisplayedXValueProperty(item), item.getXValue(), Interpolator.EASE_BOTH)));
 343         }
 344         return t;
 345     }
 346 
 347     @Override protected void seriesRemoved(final Series<X, Y> series) {
 348         // Added for RT-40104
 349         seriesDefaultColorIndex--;
 350 
 351         // remove all symbol nodes
 352         if (shouldAnimate()) {
 353             ParallelTransition pt = new ParallelTransition();
 354             pt.setOnFinished(event -> {
 355                 removeSeriesFromDisplay(series);
 356                 requestChartLayout();
 357             });
 358             for (Data<X, Y> d : series.getData()) {
 359                 final Node bar = d.getNode();
 360                 // Animate series deletion
 361                 if (getSeriesSize() > 1) {
 362                     for (int j = 0; j < series.getData().size(); j++) {
 363                         Data<X, Y> item = series.getData().get(j);
 364                         Timeline t = createDataRemoveTimeline(item, bar, series);
 365                         pt.getChildren().add(t);
 366                     }
 367                 } else {
 368                     // fade out last series
 369                     FadeTransition ft = new FadeTransition(Duration.millis(700), bar);
 370                     ft.setFromValue(1);
 371                     ft.setToValue(0);
 372                     ft.setOnFinished(actionEvent -> {
 373                         getPlotChildren().remove(bar);
 374                         bar.setOpacity(1.0);
 375                     });
 376                     pt.getChildren().add(ft);
 377                 }
 378             }
 379             pt.play();
 380         } else {
 381             for (Data<X, Y> d : series.getData()) {
 382                 final Node bar = d.getNode();
 383                 getPlotChildren().remove(bar);
 384             }
 385             removeSeriesFromDisplay(series);
 386             requestChartLayout();
 387         }
 388     }
 389 
 390     /** @inheritDoc */
 391     @Override protected void updateAxisRange() {
 392         // This override is necessary to update axis range based on cumulative Y value for the
 393         // Y axis instead of the inherited way where the max value in the data range is used.
 394         boolean categoryIsX = categoryAxis == getXAxis();
 395         if (categoryAxis.isAutoRanging()) {
 396             List cData = new ArrayList();
 397             for (Series<X, Y> series : getData()) {
 398                 for (Data<X, Y> data : series.getData()) {
 399                     if (data != null) cData.add(categoryIsX ? data.getXValue() : data.getYValue());
 400                 }
 401             }
 402             categoryAxis.invalidateRange(cData);
 403         }
 404         if (valueAxis.isAutoRanging()) {
 405             List<Number> vData = new ArrayList<>();
 406             for (String category : categoryAxis.getAllDataCategories()) {
 407                 double totalXN = 0;
 408                 double totalXP = 0;
 409                 Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator();
 410                 while (seriesIterator.hasNext()) {
 411                     Series<X, Y> series = seriesIterator.next();
 412                     for (final Data<X, Y> item : getDataItem(series, category)) {
 413                         if (item != null) {
 414                             boolean isNegative = item.getNode().getStyleClass().contains("negative");
 415                             Number value = (Number) (categoryIsX ? item.getYValue() : item.getXValue());
 416                             if (!isNegative) {
 417                                 totalXP += valueAxis.toNumericValue(value);
 418                             } else {
 419                                 totalXN += valueAxis.toNumericValue(value);
 420                             }
 421                         }
 422                     }
 423                 }
 424                 vData.add(totalXP);
 425                 vData.add(totalXN);
 426             }
 427             valueAxis.invalidateRange(vData);
 428         }
 429     }
 430 
 431     /** @inheritDoc */
 432     @Override protected void layoutPlotChildren() {
 433         double catSpace = categoryAxis.getCategorySpacing();
 434         // calculate bar spacing
 435         final double availableBarSpace = catSpace - getCategoryGap();
 436         final double barWidth = availableBarSpace;
 437         final double barOffset = -((catSpace - getCategoryGap()) / 2);
 438         final double lowerBoundValue = valueAxis.getLowerBound();
 439         final double upperBoundValue = valueAxis.getUpperBound();
 440         // update bar positions and sizes
 441         for (String category : categoryAxis.getCategories()) {
 442             double currentPositiveValue = 0;
 443             double currentNegativeValue = 0;
 444             Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator();
 445             while (seriesIterator.hasNext()) {
 446                 Series<X, Y> series = seriesIterator.next();
 447                 for (final Data<X, Y> item : getDataItem(series, category)) {
 448                     if (item != null) {
 449                         final Node bar = item.getNode();
 450                         final double categoryPos;
 451                         final double valNumber;
 452                         final X xValue = getCurrentDisplayedXValue(item);
 453                         final Y yValue = getCurrentDisplayedYValue(item);
 454                         if (orientation == Orientation.VERTICAL) {
 455                             categoryPos = getXAxis().getDisplayPosition(xValue);
 456                             valNumber = getYAxis().toNumericValue(yValue);
 457                         } else {
 458                             categoryPos = getYAxis().getDisplayPosition(yValue);
 459                             valNumber = getXAxis().toNumericValue(xValue);
 460                         }
 461                         double bottom;
 462                         double top;
 463                         boolean isNegative = bar.getStyleClass().contains("negative");
 464                         if (!isNegative) {
 465                             bottom = valueAxis.getDisplayPosition(currentPositiveValue);
 466                             top = valueAxis.getDisplayPosition(currentPositiveValue + valNumber);
 467                             currentPositiveValue += valNumber;
 468                         } else {
 469                             bottom = valueAxis.getDisplayPosition(currentNegativeValue + valNumber);
 470                             top = valueAxis.getDisplayPosition(currentNegativeValue);
 471                             currentNegativeValue += valNumber;
 472                         }
 473 
 474                         if (orientation == Orientation.VERTICAL) {
 475                             bar.resizeRelocate(categoryPos + barOffset,
 476                                     top, barWidth, bottom - top);
 477                         } else {
 478                             bar.resizeRelocate(bottom,
 479                                     categoryPos + barOffset,
 480                                     top - bottom, barWidth);
 481                         }
 482                     }
 483                 }
 484             }
 485         }
 486     }
 487 
 488     /**
 489      * Computes the size of series linked list
 490      * @return size of series linked list
 491      */
 492     @Override int getSeriesSize() {
 493         int count = 0;
 494         Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator();
 495         while (seriesIterator.hasNext()) {
 496             seriesIterator.next();
 497             count++;
 498         }
 499         return count;
 500     }
 501 
 502     /**
 503      * This is called whenever a series is added or removed and the legend needs to be updated
 504      */
 505     @Override protected void updateLegend() {
 506         legend.getItems().clear();
 507         if (getData() != null) {
 508             for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) {
 509                 Series<X,Y> series = getData().get(seriesIndex);
 510                 Legend.LegendItem legenditem = new Legend.LegendItem(series.getName());
 511                 String defaultColorStyleClass = seriesDefaultColorMap.get(series);
 512                 legenditem.getSymbol().getStyleClass().addAll("chart-bar", "series" + seriesIndex, "bar-legend-symbol",
 513                         defaultColorStyleClass);
 514                 legend.getItems().add(legenditem);
 515             }
 516         }
 517         if (legend.getItems().size() > 0) {
 518             if (getLegend() == null) {
 519                 setLegend(legend);
 520             }
 521         } else {
 522             setLegend(null);
 523         }
 524     }
 525 
 526     private Node createBar(Series series, int seriesIndex, final Data item, int itemIndex) {
 527         Node bar = item.getNode();
 528         if (bar == null) {
 529             bar = new StackPane();
 530             bar.setAccessibleRole(AccessibleRole.TEXT);
 531             bar.setAccessibleRoleDescription("Bar");
 532             bar.focusTraversableProperty().bind(Platform.accessibilityActiveProperty());
 533             item.setNode(bar);
 534         }
 535         String defaultColorStyleClass = seriesDefaultColorMap.get(series);
 536         bar.getStyleClass().setAll("chart-bar", "series" + seriesIndex, "data" + itemIndex, defaultColorStyleClass);
 537         return bar;
 538     }
 539 
 540     private List<Data<X, Y>> getDataItem(Series<X, Y> series, String category) {
 541         Map<String, List<Data<X, Y>>> catmap = seriesCategoryMap.get(series);
 542         return catmap != null ? catmap.get(category) != null ?
 543             catmap.get(category) : new ArrayList<Data<X, Y>>() : new ArrayList<Data<X, Y>>();
 544     }
 545 
 546 // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------
 547 
 548     /**
 549     * Super-lazy instantiation pattern from Bill Pugh.
 550     * @treatAsPrivate implementation detail
 551     */
 552     private static class StyleableProperties {
 553 
 554         private static final CssMetaData<StackedBarChart<?,?>,Number> CATEGORY_GAP = 
 555             new CssMetaData<StackedBarChart<?,?>,Number>("-fx-category-gap",
 556                 SizeConverter.getInstance(), 10.0)  {
 557 
 558             @Override
 559             public boolean isSettable(StackedBarChart<?,?> node) {
 560                 return node.categoryGap == null || !node.categoryGap.isBound();
 561             }
 562 
 563             @Override
 564             public StyleableProperty<Number> getStyleableProperty(StackedBarChart<?,?> node) {
 565                 return (StyleableProperty<Number>)(WritableValue<Number>)node.categoryGapProperty();
 566             }
 567         };
 568 
 569         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 570         static {
 571 
 572             final List<CssMetaData<? extends Styleable, ?>> styleables =
 573                 new ArrayList<CssMetaData<? extends Styleable, ?>>(XYChart.getClassCssMetaData());
 574             styleables.add(CATEGORY_GAP);
 575             STYLEABLES = Collections.unmodifiableList(styleables);
 576         }
 577     }
 578 
 579     /**
 580      * @return The CssMetaData associated with this class, which may include the
 581      * CssMetaData of its super classes.
 582      * @since JavaFX 8.0
 583      */
 584     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 585         return StyleableProperties.STYLEABLES;
 586     }
 587 
 588     /**
 589      * {@inheritDoc}
 590      * @since JavaFX 8.0
 591      */
 592     @Override
 593     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 594         return getClassCssMetaData();
 595     }
 596 
 597     /** Pseudoclass indicating this is a vertical chart. */
 598     private static final PseudoClass VERTICAL_PSEUDOCLASS_STATE = 
 599             PseudoClass.getPseudoClass("vertical");
 600     /** Pseudoclass indicating this is a horizontal chart. */
 601     private static final PseudoClass HORIZONTAL_PSEUDOCLASS_STATE = 
 602             PseudoClass.getPseudoClass("horizontal");
 603 
 604 }