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