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 }