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