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<X, Y>, Map<String, List<Data<X, Y>>>> seriesCategoryMap =
  68             new HashMap<>();
  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             processDataRemove(series, item);
 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     @Override protected void seriesChanged(ListChangeListener.Change<? extends Series> c) {
 259         // Update style classes for all series lines and symbols
 260         // Note: is there a more efficient way of doing this?
 261         for (int i = 0; i < getDataSize(); i++) {
 262             final Series<X,Y> series = getData().get(i);
 263             for (int j=0; j<series.getData().size(); j++) {
 264                 Data<X,Y> item = series.getData().get(j);
 265                 Node bar = item.getNode();
 266                 bar.getStyleClass().setAll("chart-bar", "series" + i, "data" + j, series.defaultColorStyleClass);
 267             }
 268         }
 269     }
 270 
 271     /** @inheritDoc */
 272     @Override protected void seriesAdded(Series<X, Y> series, int seriesIndex) {
 273         // handle any data already in series
 274         // create entry in the map
 275         Map<String, List<Data<X, Y>>> categoryMap = new HashMap<String, List<Data<X, Y>>>();
 276         for (int j = 0; j < series.getData().size(); j++) {
 277             Data<X, Y> item = series.getData().get(j);
 278             Node bar = createBar(series, seriesIndex, item, j);
 279             String category;
 280             if (orientation == Orientation.VERTICAL) {
 281                 category = (String) item.getXValue();
 282             } else {
 283                 category = (String) item.getYValue();
 284             }
 285             // list of two item positive and negative
 286             List<Data<X, Y>> itemList = categoryMap.get(category) != null ? categoryMap.get(category) : new ArrayList<Data<X, Y>>();
 287             itemList.add(item);
 288             categoryMap.put(category, itemList);
 289             if (shouldAnimate()) {
 290                 animateDataAdd(item, bar);
 291             } else {
 292                 double barVal = (orientation == Orientation.VERTICAL) ? ((Number)item.getYValue()).doubleValue() :
 293                     ((Number)item.getXValue()).doubleValue();
 294                 if (barVal < 0) {
 295                     bar.getStyleClass().add("negative");
 296                 }
 297                 getPlotChildren().add(bar);
 298             }
 299         }
 300         if (categoryMap.size() > 0) {
 301             seriesCategoryMap.put(series, categoryMap);
 302         }
 303     }
 304 
 305     @Override protected void seriesRemoved(final Series<X, Y> series) {
 306         // remove all symbol nodes
 307         if (shouldAnimate()) {
 308             ParallelTransition pt = new ParallelTransition();
 309             pt.setOnFinished(event -> {
 310                 removeSeriesFromDisplay(series);
 311                 requestChartLayout();
 312             });
 313             for (Data<X, Y> d : series.getData()) {
 314                 final Node bar = d.getNode();
 315                 // Animate series deletion
 316                 if (getSeriesSize() > 1) {
 317                     for (int j = 0; j < series.getData().size(); j++) {
 318                         Data<X, Y> item = series.getData().get(j);
 319                         Timeline t = createDataRemoveTimeline(item, bar, series);
 320                         pt.getChildren().add(t);
 321                     }
 322                 } else {
 323                     // fade out last series
 324                     FadeTransition ft = new FadeTransition(Duration.millis(700), bar);
 325                     ft.setFromValue(1);
 326                     ft.setToValue(0);
 327                     ft.setOnFinished(actionEvent -> {
 328                         processDataRemove(series, d);
 329                         bar.setOpacity(1.0);
 330                     });
 331                     pt.getChildren().add(ft);
 332                 }
 333             }
 334             pt.play();
 335         } else {
 336             for (Data<X, Y> d : series.getData()) {
 337                 processDataRemove(series, d);
 338             }
 339             removeSeriesFromDisplay(series);
 340             requestChartLayout();
 341         }
 342     }
 343 
 344     /** @inheritDoc */
 345     @Override protected void updateAxisRange() {
 346         // This override is necessary to update axis range based on cumulative Y value for the
 347         // Y axis instead of the inherited way where the max value in the data range is used.
 348         boolean categoryIsX = categoryAxis == getXAxis();
 349         if (categoryAxis.isAutoRanging()) {
 350             List cData = new ArrayList();
 351             for (Series<X, Y> series : getData()) {
 352                 for (Data<X, Y> data : series.getData()) {
 353                     if (data != null) cData.add(categoryIsX ? data.getXValue() : data.getYValue());
 354                 }
 355             }
 356             categoryAxis.invalidateRange(cData);
 357         }
 358         if (valueAxis.isAutoRanging()) {
 359             List<Number> vData = new ArrayList<>();
 360             for (String category : categoryAxis.getAllDataCategories()) {
 361                 double totalXN = 0;
 362                 double totalXP = 0;
 363                 Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator();
 364                 while (seriesIterator.hasNext()) {
 365                     Series<X, Y> series = seriesIterator.next();
 366                     for (final Data<X, Y> item : getDataItem(series, category)) {
 367                         if (item != null) {
 368                             boolean isNegative = item.getNode().getStyleClass().contains("negative");
 369                             Number value = (Number) (categoryIsX ? item.getYValue() : item.getXValue());
 370                             if (!isNegative) {
 371                                 totalXP += valueAxis.toNumericValue(value);
 372                             } else {
 373                                 totalXN += valueAxis.toNumericValue(value);
 374                             }
 375                         }
 376                     }
 377                 }
 378                 vData.add(totalXP);
 379                 vData.add(totalXN);
 380             }
 381             valueAxis.invalidateRange(vData);
 382         }
 383     }
 384 
 385     /** @inheritDoc */
 386     @Override protected void layoutPlotChildren() {
 387         double catSpace = categoryAxis.getCategorySpacing();
 388         // calculate bar spacing
 389         final double availableBarSpace = catSpace - getCategoryGap();
 390         final double barWidth = availableBarSpace;
 391         final double barOffset = -((catSpace - getCategoryGap()) / 2);
 392         // update bar positions and sizes
 393         for (String category : categoryAxis.getCategories()) {
 394             double currentPositiveValue = 0;
 395             double currentNegativeValue = 0;
 396             Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator();
 397             while (seriesIterator.hasNext()) {
 398                 Series<X, Y> series = seriesIterator.next();
 399                 for (final Data<X, Y> item : getDataItem(series, category)) {
 400                     if (item != null) {
 401                         final Node bar = item.getNode();
 402                         final double categoryPos;
 403                         final double valNumber;
 404                         final X xValue = getCurrentDisplayedXValue(item);
 405                         final Y yValue = getCurrentDisplayedYValue(item);
 406                         if (orientation == Orientation.VERTICAL) {
 407                             categoryPos = getXAxis().getDisplayPosition(xValue);
 408                             valNumber = getYAxis().toNumericValue(yValue);
 409                         } else {
 410                             categoryPos = getYAxis().getDisplayPosition(yValue);
 411                             valNumber = getXAxis().toNumericValue(xValue);
 412                         }
 413                         double bottom;
 414                         double top;
 415                         boolean isNegative = bar.getStyleClass().contains("negative");
 416                         if (!isNegative) {
 417                             bottom = valueAxis.getDisplayPosition(currentPositiveValue);
 418                             top = valueAxis.getDisplayPosition(currentPositiveValue + valNumber);
 419                             currentPositiveValue += valNumber;
 420                         } else {
 421                             bottom = valueAxis.getDisplayPosition(currentNegativeValue + valNumber);
 422                             top = valueAxis.getDisplayPosition(currentNegativeValue);
 423                             currentNegativeValue += valNumber;
 424                         }
 425 
 426                         if (orientation == Orientation.VERTICAL) {
 427                             bar.resizeRelocate(categoryPos + barOffset,
 428                                     top, barWidth, bottom - top);
 429                         } else {
 430                             bar.resizeRelocate(bottom,
 431                                     categoryPos + barOffset,
 432                                     top - bottom, barWidth);
 433                         }
 434                     }
 435                 }
 436             }
 437         }
 438     }
 439 
 440     /**
 441      * Computes the size of series linked list
 442      * @return size of series linked list
 443      */
 444     @Override int getSeriesSize() {
 445         int count = 0;
 446         Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator();
 447         while (seriesIterator.hasNext()) {
 448             seriesIterator.next();
 449             count++;
 450         }
 451         return count;
 452     }
 453 
 454     /**
 455      * This is called whenever a series is added or removed and the legend needs to be updated
 456      */
 457     @Override protected void updateLegend() {
 458         legend.getItems().clear();
 459         if (getData() != null) {
 460             for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) {
 461                 Series<X,Y> series = getData().get(seriesIndex);
 462                 Legend.LegendItem legenditem = new Legend.LegendItem(series.getName());
 463                 legenditem.getSymbol().getStyleClass().addAll("chart-bar", "series" + seriesIndex, "bar-legend-symbol",
 464                         series.defaultColorStyleClass);
 465                 legend.getItems().add(legenditem);
 466             }
 467         }
 468         if (legend.getItems().size() > 0) {
 469             if (getLegend() == null) {
 470                 setLegend(legend);
 471             }
 472         } else {
 473             setLegend(null);
 474         }
 475     }
 476 
 477     private void updateMap(Series<X,Y> series, Data<X,Y> item) {
 478         final String category = (orientation == Orientation.VERTICAL) ? (String)item.getXValue() :
 479                                      (String)item.getYValue();
 480         Map<String, List<Data<X, Y>>> categoryMap = seriesCategoryMap.get(series);
 481         if (categoryMap != null) {
 482             categoryMap.remove(category);
 483             if (categoryMap.isEmpty()) seriesCategoryMap.remove(series);
 484         }
 485         if (seriesCategoryMap.isEmpty() && categoryAxis.isAutoRanging()) categoryAxis.getCategories().clear();
 486     }
 487 
 488     private void processDataRemove(final Series<X,Y> series, final Data<X,Y> item) {
 489         Node bar = item.getNode();
 490         getPlotChildren().remove(bar);
 491         updateMap(series, item);
 492     }
 493 
 494     private void animateDataAdd(Data<X, Y> item, Node bar) {
 495         double barVal;
 496         if (orientation == Orientation.VERTICAL) {
 497             barVal = ((Number) item.getYValue()).doubleValue();
 498             if (barVal < 0) {
 499                 bar.getStyleClass().add("negative");
 500             }
 501             item.setYValue(getYAxis().toRealValue(getYAxis().getZeroPosition()));
 502             setCurrentDisplayedYValue(item, getYAxis().toRealValue(getYAxis().getZeroPosition()));
 503             getPlotChildren().add(bar);
 504             item.setYValue(getYAxis().toRealValue(barVal));
 505             animate(
 506                     new KeyFrame(Duration.ZERO, new KeyValue(
 507                             currentDisplayedYValueProperty(item),
 508                             getCurrentDisplayedYValue(item))),
 509                     new KeyFrame(Duration.millis(700), new KeyValue(
 510                             currentDisplayedYValueProperty(item),
 511                             item.getYValue(), Interpolator.EASE_BOTH))
 512             );
 513         } else {
 514             barVal = ((Number) item.getXValue()).doubleValue();
 515             if (barVal < 0) {
 516                 bar.getStyleClass().add("negative");
 517             }
 518             item.setXValue(getXAxis().toRealValue(getXAxis().getZeroPosition()));
 519             setCurrentDisplayedXValue(item, getXAxis().toRealValue(getXAxis().getZeroPosition()));
 520             getPlotChildren().add(bar);
 521             item.setXValue(getXAxis().toRealValue(barVal));
 522             animate(
 523                     new KeyFrame(Duration.ZERO, new KeyValue(
 524                             currentDisplayedXValueProperty(item),
 525                             getCurrentDisplayedXValue(item))),
 526                     new KeyFrame(Duration.millis(700), new KeyValue(
 527                             currentDisplayedXValueProperty(item),
 528                             item.getXValue(), Interpolator.EASE_BOTH))
 529             );
 530         }
 531     }
 532 
 533     private Timeline createDataRemoveTimeline(Data<X, Y> item, final Node bar, final Series<X, Y> series) {
 534         Timeline t = new Timeline();
 535         if (orientation == Orientation.VERTICAL) {
 536             item.setYValue(getYAxis().toRealValue(getYAxis().getZeroPosition()));
 537             t.getKeyFrames().addAll(
 538                     new KeyFrame(Duration.ZERO, new KeyValue(
 539                             currentDisplayedYValueProperty(item),
 540                             getCurrentDisplayedYValue(item))),
 541                     new KeyFrame(Duration.millis(700), actionEvent -> {
 542                         processDataRemove(series, item);
 543                     }, new KeyValue(
 544                             currentDisplayedYValueProperty(item),
 545                             item.getYValue(), Interpolator.EASE_BOTH))
 546             );
 547         } else {
 548             item.setXValue(getXAxis().toRealValue(getXAxis().getZeroPosition()));
 549             t.getKeyFrames().addAll(
 550                     new KeyFrame(Duration.ZERO, new KeyValue(
 551                             currentDisplayedXValueProperty(item),
 552                             getCurrentDisplayedXValue(item))),
 553                     new KeyFrame(Duration.millis(700), actionEvent -> {
 554                         processDataRemove(series, item);
 555                     }, new KeyValue(
 556                             currentDisplayedXValueProperty(item),
 557                             item.getXValue(), Interpolator.EASE_BOTH))
 558             );
 559         }
 560         return t;
 561     }
 562 
 563     private Node createBar(Series<X, Y> series, int seriesIndex, final Data<X, Y> item, int itemIndex) {
 564         Node bar = item.getNode();
 565         if (bar == null) {
 566             bar = new StackPane();
 567             bar.setAccessibleRole(AccessibleRole.TEXT);
 568             bar.setAccessibleRoleDescription("Bar");
 569             bar.focusTraversableProperty().bind(Platform.accessibilityActiveProperty());
 570             item.setNode(bar);
 571         }
 572         bar.getStyleClass().setAll("chart-bar", "series" + seriesIndex, "data" + itemIndex, series.defaultColorStyleClass);
 573         return bar;
 574     }
 575 
 576     private List<Data<X, Y>> getDataItem(Series<X, Y> series, String category) {
 577         Map<String, List<Data<X, Y>>> catmap = seriesCategoryMap.get(series);
 578         return catmap != null ? catmap.get(category) != null ?
 579             catmap.get(category) : new ArrayList<Data<X, Y>>() : new ArrayList<Data<X, Y>>();
 580     }
 581 
 582 // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------
 583 
 584     /*
 585      * Super-lazy instantiation pattern from Bill Pugh.
 586      */
 587     private static class StyleableProperties {
 588 
 589         private static final CssMetaData<StackedBarChart<?,?>,Number> CATEGORY_GAP =
 590             new CssMetaData<StackedBarChart<?,?>,Number>("-fx-category-gap",
 591                 SizeConverter.getInstance(), 10.0)  {
 592 
 593             @Override
 594             public boolean isSettable(StackedBarChart<?,?> node) {
 595                 return node.categoryGap == null || !node.categoryGap.isBound();
 596             }
 597 
 598             @Override
 599             public StyleableProperty<Number> getStyleableProperty(StackedBarChart<?,?> node) {
 600                 return (StyleableProperty<Number>)(WritableValue<Number>)node.categoryGapProperty();
 601             }
 602         };
 603 
 604         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 605         static {
 606 
 607             final List<CssMetaData<? extends Styleable, ?>> styleables =
 608                     new ArrayList<>(XYChart.getClassCssMetaData());
 609             styleables.add(CATEGORY_GAP);
 610             STYLEABLES = Collections.unmodifiableList(styleables);
 611         }
 612     }
 613 
 614     /**
 615      * @return The CssMetaData associated with this class, which may include the
 616      * CssMetaData of its super classes.
 617      * @since JavaFX 8.0
 618      */
 619     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 620         return StyleableProperties.STYLEABLES;
 621     }
 622 
 623     /**
 624      * {@inheritDoc}
 625      * @since JavaFX 8.0
 626      */
 627     @Override
 628     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 629         return getClassCssMetaData();
 630     }
 631 
 632     /** Pseudoclass indicating this is a vertical chart. */
 633     private static final PseudoClass VERTICAL_PSEUDOCLASS_STATE =
 634             PseudoClass.getPseudoClass("vertical");
 635 
 636     /** Pseudoclass indicating this is a horizontal chart. */
 637     private static final PseudoClass HORIZONTAL_PSEUDOCLASS_STATE =
 638             PseudoClass.getPseudoClass("horizontal");
 639 
 640 }