Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.scene.chart;
027
028import java.util.HashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Set;
032import java.util.ArrayList;
033import java.util.Collections;
034
035import javafx.animation.Animation;
036import javafx.animation.FadeTransition;
037import javafx.animation.Interpolator;
038import javafx.animation.KeyFrame;
039import javafx.animation.KeyValue;
040import javafx.animation.ParallelTransition;
041import javafx.animation.Timeline;
042import javafx.beans.property.DoubleProperty;
043import javafx.collections.FXCollections;
044import javafx.collections.ObservableList;
045import javafx.event.ActionEvent;
046import javafx.event.EventHandler;
047import javafx.geometry.Orientation;
048import javafx.scene.Node;
049import javafx.scene.layout.StackPane;
050import javafx.util.Duration;
051
052import com.sun.javafx.charts.Legend;
053import com.sun.javafx.charts.Legend.LegendItem;
054import javafx.css.StyleableDoubleProperty;
055import javafx.css.CssMetaData;
056import javafx.css.PseudoClass;
057import com.sun.javafx.css.converters.SizeConverter;
058import javafx.css.Styleable;
059import javafx.css.StyleableProperty;
060import javafx.event.EventType;
061import static javafx.scene.chart.XYChart.DEFAULT_COLOR;
062
063/**
064 * A chart that plots bars indicating data values for a category. The bars can be vertical or horizontal depending on
065 * which axis is a category axis.
066 */
067public class BarChart<X,Y> extends XYChart<X,Y> {
068
069    // -------------- PRIVATE FIELDS -------------------------------------------
070    
071    private Map<Series, Map<String, Data<X,Y>>> seriesCategoryMap = 
072                                new HashMap<Series, Map<String, Data<X,Y>>>();
073    private Legend legend = new Legend();
074    private boolean seriesRemove = false;
075    private final Orientation orientation;
076    private CategoryAxis categoryAxis;
077    private ValueAxis valueAxis;
078    private Timeline dataRemoveTimeline;
079    private Data<X,Y> dataItemBeingRemoved = null;
080    private Series<X,Y> seriesOfDataRemoved = null;
081    private double bottomPos  = 0;
082    private static String NEGATIVE_STYLE = "negative";
083    // -------------- PUBLIC PROPERTIES ----------------------------------------
084
085    /** The gap to leave between bars in the same category */
086    private DoubleProperty barGap = new StyleableDoubleProperty(4) {
087        @Override protected void invalidated() {
088            get();
089            requestChartLayout();
090        }
091        
092        public Object getBean() {
093            return BarChart.this;
094        }
095
096        public String getName() {
097            return "barGap";
098        }
099
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(Axis<X> xAxis, 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(Axis<X> xAxis, Axis<Y> yAxis, 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(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<Series<X,Y>> data, 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 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            if (dataRemoveTimeline != null && dataRemoveTimeline.getStatus().equals(Animation.Status.RUNNING)) {
223                if (dataItemBeingRemoved != null && dataItemBeingRemoved == item) {
224                    dataRemoveTimeline.stop();
225                    getPlotChildren().remove(bar);
226                    removeDataItemFromDisplay(seriesOfDataRemoved, item);
227                    dataItemBeingRemoved = null;
228                    seriesOfDataRemoved = null;
229                }
230            }
231            animateDataAdd(item, bar);
232        } else {
233            getPlotChildren().add(bar);
234        }
235    }
236
237    @Override protected void dataItemRemoved(final Data<X,Y> item, final Series<X,Y> series) {
238        final Node bar = item.getNode();
239        if (shouldAnimate()) {
240            dataRemoveTimeline = createDataRemoveTimeline(item, bar, series);
241            dataItemBeingRemoved = item;
242            seriesOfDataRemoved = series;
243            dataRemoveTimeline.setOnFinished(new EventHandler<ActionEvent>() {
244                public void handle(ActionEvent event) {
245                    item.setSeries(null);
246                    getPlotChildren().remove(bar);
247                    removeDataItemFromDisplay(series, item);
248                    dataItemBeingRemoved = null;
249                    updateMap(series, item);
250                }
251            });
252            dataRemoveTimeline.play();
253        } else {
254            item.setSeries(null);
255            getPlotChildren().remove(bar);
256            removeDataItemFromDisplay(series, item);
257            updateMap(series, item);
258        }
259    }
260
261    /** @inheritDoc */
262    @Override protected void dataItemChanged(Data<X, Y> item) {
263         double barVal;
264         double currentVal;
265        if (orientation == Orientation.VERTICAL) {
266             barVal = ((Number)item.getYValue()).doubleValue();
267             currentVal = ((Number)item.getCurrentY()).doubleValue();
268        } else {
269             barVal = ((Number)item.getXValue()).doubleValue();
270             currentVal = ((Number)item.getCurrentX()).doubleValue();
271        }
272         if (currentVal > 0 && barVal < 0) { // going from positive to negative
273             // add style class negative
274             item.getNode().getStyleClass().add(NEGATIVE_STYLE);
275         } else if (currentVal < 0 && barVal > 0) { // going from negative to positive
276             // remove style class negative
277             // RT-21164 upside down bars: was adding NEGATIVE_STYLE styleclass
278             // instead of removing it; when going from negative to positive
279             item.getNode().getStyleClass().remove(NEGATIVE_STYLE);
280         }
281    }
282    
283    @Override protected void seriesAdded(Series<X,Y> series, int seriesIndex) {
284        // handle any data already in series
285        // create entry in the map
286        Map<String, Data<X,Y>> categoryMap = new HashMap<String, Data<X,Y>>();
287        for (int j=0; j<series.getData().size(); j++) {
288            Data<X,Y> item = series.getData().get(j);
289            Node bar = createBar(series, seriesIndex, item, j);
290            String category;
291            if (orientation == Orientation.VERTICAL) {
292                category = (String)item.getXValue();
293            } else {
294                category = (String)item.getYValue();
295            }
296            categoryMap.put(category, item);
297            if (shouldAnimate()) {
298                animateDataAdd(item, bar);
299            } else {
300                // RT-21164 check if bar value is negative to add NEGATIVE_STYLE style class 
301                double barVal = (orientation == Orientation.VERTICAL) ? ((Number)item.getYValue()).doubleValue() :
302                        ((Number)item.getXValue()).doubleValue();
303                if (barVal < 0) {
304                    bar.getStyleClass().add(NEGATIVE_STYLE);
305                }
306                getPlotChildren().add(bar);
307            }
308        }
309        if (categoryMap.size() > 0) seriesCategoryMap.put(series, categoryMap);
310    }
311    
312    @Override protected void seriesRemoved(final Series<X,Y> series) {
313        updateDefaultColorIndex(series);
314        // remove all symbol nodes
315        if (shouldAnimate()) {
316            ParallelTransition pt = new ParallelTransition();
317            pt.setOnFinished(new EventHandler<ActionEvent>() {
318                public void handle(ActionEvent event) {
319                    removeSeriesFromDisplay(series);
320                }
321            });
322            for (final Data<X,Y> d : series.getData()) {
323                final Node bar = d.getNode();
324                seriesRemove = true;
325                // Animate series deletion
326                if (getSeriesSize() > 1) {
327                    for (int j=0; j< series.getData().size(); j++) {
328                        Data<X,Y> item = series.getData().get(j);
329                        Timeline t = createDataRemoveTimeline(item, bar, series);
330                        pt.getChildren().add(t);
331                    }
332                } else {
333                    // fade out last series
334                    FadeTransition ft = new FadeTransition(Duration.millis(700),bar);
335                    ft.setFromValue(1);
336                    ft.setToValue(0);
337                    ft.setOnFinished(new EventHandler<ActionEvent>() {
338                         @Override public void handle(ActionEvent actionEvent) {
339                            getPlotChildren().remove(bar);
340                            updateMap(series, d);
341                         }
342                    });
343                    pt.getChildren().add(ft);
344                }
345            }
346            pt.play();
347        } else {
348            for (Data<X,Y> d : series.getData()) {
349                final Node bar = d.getNode();
350                getPlotChildren().remove(bar);
351                updateMap(series, d);
352            }
353            removeSeriesFromDisplay(series);
354        }
355    }
356
357    /** @inheritDoc */
358    @Override protected void layoutPlotChildren() {
359        double catSpace = categoryAxis.getCategorySpacing();
360        // calculate bar spacing
361        final double avilableBarSpace = catSpace - (getCategoryGap() + getBarGap());
362        final double barWidth = (avilableBarSpace / getSeriesSize()) - getBarGap();
363        final double barOffset = -((catSpace - getCategoryGap()) / 2);
364        final double zeroPos = (valueAxis.getLowerBound() > 0) ? 
365                valueAxis.getDisplayPosition(valueAxis.getLowerBound()) : valueAxis.getZeroPosition();
366        // update bar positions and sizes
367        int catIndex = 0;
368        for (String category : categoryAxis.getCategories()) {
369            int index = 0;
370            for (Series<X,Y> series = begin; series != null; series = series.next) {
371                final Data<X,Y> item = getDataItem(series, index, catIndex, category);
372                if (item != null) {
373                    final Node bar = item.getNode();
374                    final double categoryPos;
375                    final double valPos;
376                    if (orientation == Orientation.VERTICAL) {
377                        categoryPos = getXAxis().getDisplayPosition(item.getCurrentX());
378                        valPos = getYAxis().getDisplayPosition(item.getCurrentY());
379                    } else {
380                        categoryPos = getYAxis().getDisplayPosition(item.getCurrentY());
381                        valPos = getXAxis().getDisplayPosition(item.getCurrentX());
382                    }
383                    final double bottom = Math.min(valPos,zeroPos);
384                    final double top = Math.max(valPos,zeroPos);
385                    bottomPos = bottom;
386                    if (orientation == Orientation.VERTICAL) {
387                        bar.resizeRelocate( categoryPos + barOffset + (barWidth + getBarGap()) * index,
388                                            bottom, barWidth, top-bottom);
389                    } else {
390                        //noinspection SuspiciousNameCombination
391                        bar.resizeRelocate( bottom, categoryPos + barOffset + (barWidth + getBarGap()) * index,
392                                            top-bottom, barWidth);
393                    }
394
395                    index++;
396                }
397            }
398            catIndex++;
399        }
400    }
401
402    /**
403     * This is called whenever a series is added or removed and the legend needs to be updated
404     */
405    @Override protected void updateLegend() {
406        legend.getItems().clear();
407        if (getData() != null) {
408            for (int seriesIndex=0; seriesIndex < getData().size(); seriesIndex++) {
409                Series series = getData().get(seriesIndex);
410                LegendItem legenditem = new LegendItem(series.getName());
411                legenditem.getSymbol().getStyleClass().addAll("chart-bar","series"+seriesIndex,"bar-legend-symbol",
412                        series.defaultColorStyleClass);
413                legend.getItems().add(legenditem);
414            }
415        }
416        if (legend.getItems().size() > 0) {
417            if (getLegend() == null) {
418                setLegend(legend);
419            }
420        } else {
421            setLegend(null);
422        }
423    }
424    
425    // -------------- PRIVATE METHODS ------------------------------------------
426    
427    private void updateMap(Series series, Data item) {
428        final String category = (orientation == Orientation.VERTICAL) ? (String)item.getXValue() :
429                                     (String)item.getYValue();
430        Map<String, Data<X,Y>> categoryMap = seriesCategoryMap.get(series);
431        if (categoryMap != null) {
432            categoryMap.remove(category);
433            if (categoryMap.isEmpty()) seriesCategoryMap.remove(series);
434        }
435        if (seriesCategoryMap.isEmpty() && categoryAxis.isAutoRanging()) categoryAxis.getCategories().clear();
436    }
437    private void animateDataAdd(Data<X,Y> item, Node bar) {
438        double barVal;
439        if (orientation == Orientation.VERTICAL) {
440            barVal = ((Number)item.getYValue()).doubleValue();
441            if (barVal < 0) {
442                bar.getStyleClass().add(NEGATIVE_STYLE);
443            }
444            item.setCurrentY(getYAxis().toRealValue((barVal < 0) ? -bottomPos : bottomPos));
445            getPlotChildren().add(bar);
446            item.setYValue(getYAxis().toRealValue(barVal));
447            animate(
448                new KeyFrame(Duration.ZERO, new KeyValue(item.currentYProperty(),
449                item.getCurrentY())),
450                new KeyFrame(Duration.millis(700),
451                new KeyValue(item.currentYProperty(), item.getYValue(), Interpolator.EASE_BOTH))
452            );
453        } else {
454            barVal = ((Number)item.getXValue()).doubleValue();
455            if (barVal < 0) {
456                bar.getStyleClass().add(NEGATIVE_STYLE);
457            }
458            item.setCurrentX(getXAxis().toRealValue((barVal < 0) ? -bottomPos : bottomPos));
459            getPlotChildren().add(bar);
460            item.setXValue(getXAxis().toRealValue(barVal));
461            animate(
462                new KeyFrame(Duration.ZERO, new KeyValue(item.currentXProperty(),
463                item.getCurrentX())),
464                new KeyFrame(Duration.millis(700),
465                new KeyValue(item.currentXProperty(), item.getXValue(), Interpolator.EASE_BOTH))
466            );
467        }
468    }
469
470    private Timeline createDataRemoveTimeline(final Data<X,Y> item, final Node bar, final Series<X,Y> series) {
471        Timeline t = new Timeline();
472        if (orientation == Orientation.VERTICAL) {
473//            item.setYValue(getYAxis().toRealValue(getYAxis().getZeroPosition()));
474            item.setYValue(getYAxis().toRealValue(bottomPos));
475            t.getKeyFrames().addAll(new KeyFrame(Duration.ZERO,
476                                    new KeyValue(item.currentYProperty(), item.getCurrentY())),
477                                    new KeyFrame(Duration.millis(700), new EventHandler<ActionEvent>() {
478                                    @Override public void handle(ActionEvent actionEvent) {
479                                        getPlotChildren().remove(bar);
480                                        updateMap(series, item);
481                                    }
482                                },
483                                new KeyValue(item.currentYProperty(), item.getYValue(),
484                                Interpolator.EASE_BOTH) ));
485        } else {
486            item.setXValue(getXAxis().toRealValue(getXAxis().getZeroPosition()));
487            t.getKeyFrames().addAll(new KeyFrame(Duration.ZERO, new KeyValue(item.currentXProperty(), item.getCurrentX())),
488                new KeyFrame(Duration.millis(700), new EventHandler<ActionEvent>() {
489                        @Override public void handle(ActionEvent actionEvent) {
490                            getPlotChildren().remove(bar);
491                            updateMap(series, item);
492                        }
493                    },
494                    new KeyValue(item.currentXProperty(), item.getXValue(),
495                            Interpolator.EASE_BOTH) ));
496        }
497        return t;
498    }
499    
500    private void updateDefaultColorIndex(final Series<X,Y> series) {
501        int clearIndex = seriesColorMap.get(series);
502        colorBits.clear(clearIndex);
503        for (Data<X,Y> d : series.getData()) {
504            final Node bar = d.getNode();
505            if (bar != null) {
506                bar.getStyleClass().remove(DEFAULT_COLOR+clearIndex);
507                colorBits.clear(clearIndex);
508            }
509        }
510        seriesColorMap.remove(series);
511    }
512
513    private Node createBar(Series series, int seriesIndex, final Data item, int itemIndex) {
514        Node bar = item.getNode();
515        if (bar == null) {
516            bar = new StackPane();
517            item.setNode(bar);
518        }
519        bar.getStyleClass().addAll("chart-bar", "series" + seriesIndex, "data" + itemIndex,series.defaultColorStyleClass);
520        return bar;
521    }
522
523    private Data<X,Y> getDataItem(Series<X,Y> series, int seriesIndex, int itemIndex, String category) {
524        Map<String, Data<X,Y>> catmap = seriesCategoryMap.get(series);
525        return (catmap != null) ? catmap.get(category) : null;
526    }
527
528    // -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------
529
530    /**
531      * Super-lazy instantiation pattern from Bill Pugh.
532      * @treatAsPrivate implementation detail
533      */
534     private static class StyleableProperties {
535         private static final CssMetaData<BarChart<?,?>,Number> BAR_GAP = 
536             new CssMetaData<BarChart<?,?>,Number>("-fx-bar-gap",
537                 SizeConverter.getInstance(), 4.0) {
538
539            @Override
540            public boolean isSettable(BarChart<?,?> node) {
541                return node.barGap == null || !node.barGap.isBound();
542            }
543
544            @Override
545            public StyleableProperty<Number> getStyleableProperty(BarChart<?,?> node) {
546                return (StyleableProperty<Number>)node.barGapProperty();
547            }
548        };
549         
550         private static final CssMetaData<BarChart<?,?>,Number> CATEGORY_GAP = 
551             new CssMetaData<BarChart<?,?>,Number>("-fx-category-gap",
552                 SizeConverter.getInstance(), 10.0)  {
553
554            @Override
555            public boolean isSettable(BarChart<?,?> node) {
556                return node.categoryGap == null || !node.categoryGap.isBound();
557            }
558
559            @Override
560            public StyleableProperty<Number> getStyleableProperty(BarChart<?,?> node) {
561                return (StyleableProperty<Number>)node.categoryGapProperty();
562            }
563        };
564
565         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
566         static {
567
568            final List<CssMetaData<? extends Styleable, ?>> styleables =
569                new ArrayList<CssMetaData<? extends Styleable, ?>>(XYChart.getClassCssMetaData());
570            styleables.add(BAR_GAP);
571            styleables.add(CATEGORY_GAP);
572            STYLEABLES = Collections.unmodifiableList(styleables);
573         }
574    }
575
576    /**
577     * @return The CssMetaData associated with this class, which may include the
578     * CssMetaData of its super classes.
579     */
580    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
581        return StyleableProperties.STYLEABLES;
582    }
583
584    /**
585     * {@inheritDoc}
586     */
587    @Override
588    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
589        return getClassCssMetaData();
590    }
591
592    /** Pseudoclass indicating this is a vertical chart. */
593    private static final PseudoClass VERTICAL_PSEUDOCLASS_STATE =
594            PseudoClass.getPseudoClass("vertical");
595
596    /** Pseudoclass indicating this is a horizontal chart. */
597    private static final PseudoClass HORIZONTAL_PSEUDOCLASS_STATE = 
598            PseudoClass.getPseudoClass("horizontal");
599
600}