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