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