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