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