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 }