Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2011, 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
028
029import java.util.*;
030import javafx.animation.*;
031import javafx.beans.property.DoubleProperty;
032import javafx.collections.FXCollections;
033import javafx.collections.ObservableList;
034import javafx.event.ActionEvent;
035import javafx.event.EventHandler;
036import javafx.geometry.Orientation;
037import javafx.scene.Node;
038import javafx.scene.layout.StackPane;
039import javafx.util.Duration;
040import com.sun.javafx.charts.Legend;
041import javafx.css.StyleableDoubleProperty;
042import javafx.css.CssMetaData;
043import javafx.css.PseudoClass;
044import com.sun.javafx.css.converters.SizeConverter;
045import javafx.collections.ListChangeListener;
046import javafx.css.Styleable;
047import javafx.css.StyleableProperty;
048
049
050/**
051    * StackedBarChart is a variation of {@link BarChart} that plots bars indicating 
052    * data values for a category. The bars can be vertical or horizontal depending 
053    * on which axis is a category axis. 
054    * The bar for each series is stacked on top of the previous series.
055    */
056public class StackedBarChart<X, Y> extends XYChart<X, Y> {
057        
058    // -------------- PRIVATE FIELDS -------------------------------------------
059    private Map<Series, Map<String, List<Data<X, Y>>>> seriesCategoryMap = new HashMap<Series, Map<String, List<Data<X, Y>>>>();
060    private Legend legend = new Legend();
061    private final Orientation orientation;
062    private CategoryAxis categoryAxis;
063    private ValueAxis valueAxis;
064    private int seriesDefaultColorIndex = 0;
065    private Map<Series<X, Y>, String> seriesDefaultColorMap = new HashMap<Series<X, Y>, String>();
066    // RT-23125 handling data removal when a category is removed.
067    private ListChangeListener<String> categoriesListener = new ListChangeListener<String>() {
068        @Override public void onChanged(ListChangeListener.Change<? extends String> c) {
069            while (c.next()) {
070                for(String cat : c.getRemoved()) {
071                    for (Series<X,Y> series : getData()) {
072                        for (Data<X, Y> data : series.getData()) {
073                            if ((cat).equals((orientation == orientation.VERTICAL) ? 
074                                    data.getXValue() : data.getYValue())) {
075                                boolean animatedOn = getAnimated();
076                                setAnimated(false);
077                                dataItemRemoved(data, series);
078                                setAnimated(animatedOn);
079                            }
080                        }
081                    }
082                    requestChartLayout();
083                }
084            }
085        }
086    };
087    
088    // -------------- PUBLIC PROPERTIES ----------------------------------------
089    /** The gap to leave between bars in separate categories */
090    private DoubleProperty categoryGap = new StyleableDoubleProperty(10) {
091        @Override protected void invalidated() {
092            get();
093            requestChartLayout();
094        }
095
096        @Override
097        public Object getBean() {
098            return StackedBarChart.this;
099        }
100
101        @Override
102        public String getName() {
103            return "categoryGap";
104        }
105
106        public CssMetaData<StackedBarChart<?,?>,Number> getCssMetaData() {
107            return StackedBarChart.StyleableProperties.CATEGORY_GAP;
108        }
109    };
110
111    public double getCategoryGap() {
112        return categoryGap.getValue();
113    }
114
115    public void setCategoryGap(double value) {
116        categoryGap.setValue(value);
117    }
118
119    public DoubleProperty categoryGapProperty() {
120        return categoryGap;
121    }
122
123    // -------------- CONSTRUCTOR ----------------------------------------------
124    /**
125        * Construct a new StackedBarChart with the given axis. The two axis should be a ValueAxis/NumberAxis and a CategoryAxis,
126        * they can be in either order depending on if you want a horizontal or vertical bar chart.
127        *
128        * @param xAxis The x axis to use
129        * @param yAxis The y axis to use
130        */
131    public StackedBarChart(Axis<X> xAxis, Axis<Y> yAxis) {
132        this(xAxis, yAxis, FXCollections.<Series<X, Y>>observableArrayList());
133    }
134
135    /**
136        * Construct a new StackedBarChart with the given axis and data. The two axis should be a ValueAxis/NumberAxis and a
137        * CategoryAxis, 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        * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart
142        */
143    public StackedBarChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<Series<X, Y>> data) {
144        super(xAxis, yAxis);
145        getStyleClass().add("stacked-bar-chart");
146        setLegend(legend);
147        if (!((xAxis instanceof ValueAxis && yAxis instanceof CategoryAxis)
148                || (yAxis instanceof ValueAxis && xAxis instanceof CategoryAxis))) {
149            throw new IllegalArgumentException("Axis type incorrect, one of X,Y should be CategoryAxis and the other NumberAxis");
150        }
151        if (xAxis instanceof CategoryAxis) {
152            categoryAxis = (CategoryAxis) xAxis;
153            valueAxis = (ValueAxis) yAxis;
154            orientation = Orientation.VERTICAL;
155        } else {
156            categoryAxis = (CategoryAxis) yAxis;
157            valueAxis = (ValueAxis) xAxis;
158            orientation = Orientation.HORIZONTAL;
159        }
160        // update css
161        pseudoClassStateChanged(HORIZONTAL_PSEUDOCLASS_STATE, orientation == Orientation.HORIZONTAL);
162        pseudoClassStateChanged(VERTICAL_PSEUDOCLASS_STATE, orientation == Orientation.VERTICAL);
163        setData(data);
164        categoryAxis.getCategories().addListener(categoriesListener);
165    }
166
167    /**
168        * Construct a new StackedBarChart with the given axis and data. The two axis should be a ValueAxis/NumberAxis and a
169        * CategoryAxis, they can be in either order depending on if you want a horizontal or vertical bar chart.
170        *
171        * @param xAxis The x axis to use
172        * @param yAxis The y axis to use
173        * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart
174        * @param categoryGap The gap to leave between bars in separate categories
175        */
176    public StackedBarChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<Series<X, Y>> data, double categoryGap) {
177        this(xAxis, yAxis);
178        setData(data);
179        setCategoryGap(categoryGap);
180    }
181
182    // -------------- METHODS --------------------------------------------------
183    @Override protected void dataItemAdded(Series<X, Y> series, int itemIndex, Data<X, Y> item) {
184        String category;
185        if (orientation == Orientation.VERTICAL) {
186            category = (String) item.getXValue();
187        } else {
188            category = (String) item.getYValue();
189        }
190        // Don't plot if category does not already exist ?
191//        if (!categoryAxis.getCategories().contains(category)) return;
192
193        Map<String, List<Data<X, Y>>> categoryMap = seriesCategoryMap.get(series);
194
195        if (categoryMap == null) {
196            categoryMap = new HashMap<String, List<Data<X, Y>>>();
197            seriesCategoryMap.put(series, categoryMap);
198        }
199        // list to hold more that one bar "positive and negative"
200        List<Data<X, Y>> itemList = categoryMap.get(category) != null ? categoryMap.get(category) : new ArrayList<Data<X, Y>>();
201        itemList.add(item);
202        categoryMap.put(category, itemList);
203//        categoryMap.put(category, item);
204        Node bar = createBar(series, getData().indexOf(series), item, itemIndex);
205        if (shouldAnimate()) {
206            animateDataAdd(item, bar);
207        } else {
208            getPlotChildren().add(bar);
209        }
210    }
211
212    @Override protected void dataItemRemoved(final Data<X, Y> item, final Series<X, Y> series) {
213        final Node bar = item.getNode();
214        if (shouldAnimate()) {
215            Timeline t = createDataRemoveTimeline(item, bar, series);
216            t.setOnFinished(new EventHandler<ActionEvent>() {
217
218                public void handle(ActionEvent event) {
219                    removeDataItemFromDisplay(series, item);
220                }
221            });
222            t.play();
223        } else {
224            getPlotChildren().remove(bar);
225            removeDataItemFromDisplay(series, item);
226        }
227    }
228
229    /** @inheritDoc */
230    @Override protected void dataItemChanged(Data<X, Y> item) {
231        double barVal;
232        double currentVal;
233        if (orientation == Orientation.VERTICAL) {
234            barVal = ((Number) item.getYValue()).doubleValue();
235            currentVal = ((Number) getCurrentDisplayedYValue(item)).doubleValue();
236        } else {
237            barVal = ((Number) item.getXValue()).doubleValue();
238            currentVal = ((Number) getCurrentDisplayedXValue(item)).doubleValue();
239        }
240        if (currentVal > 0 && barVal < 0) { // going from positive to negative
241            // add style class negative
242            item.getNode().getStyleClass().add("negative");
243        } else if (currentVal < 0 && barVal > 0) { // going from negative to positive
244            // remove style class negative
245            item.getNode().getStyleClass().remove("negative");
246        }
247    }
248
249    private void animateDataAdd(Data<X, Y> item, Node bar) {
250        double barVal;
251        if (orientation == Orientation.VERTICAL) {
252            barVal = ((Number) item.getYValue()).doubleValue();
253            if (barVal < 0) {
254                bar.getStyleClass().add("negative");
255            }
256            item.setYValue(getYAxis().toRealValue(getYAxis().getZeroPosition()));
257            setCurrentDisplayedYValue(item, getYAxis().toRealValue(getYAxis().getZeroPosition()));
258            getPlotChildren().add(bar);
259            item.setYValue(getYAxis().toRealValue(barVal));
260            animate(new Timeline(
261                        new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedYValueProperty(item), getCurrentDisplayedYValue(item))),
262                        new KeyFrame(Duration.millis(700), new KeyValue(currentDisplayedYValueProperty(item), item.getYValue(), Interpolator.EASE_BOTH)))
263                    );
264        } else {
265            barVal = ((Number) item.getXValue()).doubleValue();
266            if (barVal < 0) {
267                bar.getStyleClass().add("negative");
268            }
269            item.setXValue(getXAxis().toRealValue(getXAxis().getZeroPosition()));
270            setCurrentDisplayedXValue(item, getXAxis().toRealValue(getXAxis().getZeroPosition()));
271            getPlotChildren().add(bar);
272            item.setXValue(getXAxis().toRealValue(barVal));
273            animate(new Timeline(
274                        new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedXValueProperty(item), getCurrentDisplayedXValue(item))),
275                        new KeyFrame(Duration.millis(700), new KeyValue(currentDisplayedXValueProperty(item), item.getXValue(), Interpolator.EASE_BOTH)))
276                    );
277        }
278    }
279
280    /** @inheritDoc */
281    @Override protected void seriesAdded(Series<X, Y> series, int seriesIndex) {
282        String defaultColorStyleClass = "default-color" + (seriesDefaultColorIndex % 8);
283        seriesDefaultColorMap.put(series, defaultColorStyleClass);
284        seriesDefaultColorIndex++;
285        // handle any data already in series
286        // create entry in the map
287        Map<String, List<Data<X, Y>>> categoryMap = new HashMap<String, List<Data<X, Y>>>();
288        for (int j = 0; j < series.getData().size(); j++) {
289            Data<X, Y> item = series.getData().get(j);
290            Node bar = createBar(series, seriesIndex, item, j);
291            String category;
292            if (orientation == Orientation.VERTICAL) {
293                category = (String) item.getXValue();
294            } else {
295                category = (String) item.getYValue();
296            }
297            // list of two item positive and negative
298            List<Data<X, Y>> itemList = categoryMap.get(category) != null ? categoryMap.get(category) : new ArrayList<Data<X, Y>>();
299            itemList.add(item);
300            categoryMap.put(category, itemList);
301            if (shouldAnimate()) {
302                animateDataAdd(item, bar);
303            } else {
304                double barVal = (orientation == Orientation.VERTICAL) ? ((Number)item.getYValue()).doubleValue() :
305                    ((Number)item.getXValue()).doubleValue();
306                if (barVal < 0) {
307                    bar.getStyleClass().add("negative");
308                }
309                getPlotChildren().add(bar);
310            }
311        }
312        if (categoryMap.size() > 0) {
313            seriesCategoryMap.put(series, categoryMap);
314        }
315    }
316
317    private Timeline createDataRemoveTimeline(Data<X, Y> item, final Node bar, final Series<X, Y> series) {
318        Timeline t = new Timeline();
319        if (orientation == Orientation.VERTICAL) {
320            item.setYValue(getYAxis().toRealValue(getYAxis().getZeroPosition()));
321            t.getKeyFrames().addAll(
322                    new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedYValueProperty(item), 
323                    getCurrentDisplayedYValue(item))),
324                    new KeyFrame(Duration.millis(700), new EventHandler<ActionEvent>() {
325                        @Override public void handle(ActionEvent actionEvent) {
326                            getPlotChildren().remove(bar);
327                        }
328                    },
329                    new KeyValue(currentDisplayedYValueProperty(item), item.getYValue(), Interpolator.EASE_BOTH)));
330        } else {
331            item.setXValue(getXAxis().toRealValue(getXAxis().getZeroPosition()));
332            t.getKeyFrames().addAll(
333                    new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedXValueProperty(item), 
334                    getCurrentDisplayedXValue(item))),
335                    new KeyFrame(Duration.millis(700), new EventHandler<ActionEvent>() {
336                        @Override public void handle(ActionEvent actionEvent) {
337                            getPlotChildren().remove(bar);
338                        }
339                    },
340                    new KeyValue(currentDisplayedXValueProperty(item), item.getXValue(), Interpolator.EASE_BOTH)));
341        }
342        return t;
343    }
344
345    @Override protected void seriesRemoved(final Series<X, Y> series) {
346        // remove all symbol nodes
347        if (shouldAnimate()) {
348            ParallelTransition pt = new ParallelTransition();
349            pt.setOnFinished(new EventHandler<ActionEvent>() {
350
351                public void handle(ActionEvent event) {
352                    removeSeriesFromDisplay(series);
353                    requestChartLayout();
354                }
355            });
356            for (Data<X, Y> d : series.getData()) {
357                final Node bar = d.getNode();
358                // Animate series deletion
359                if (getSeriesSize() > 1) {
360                    for (int j = 0; j < series.getData().size(); j++) {
361                        Data<X, Y> item = series.getData().get(j);
362                        Timeline t = createDataRemoveTimeline(item, bar, series);
363                        pt.getChildren().add(t);
364                    }
365                } else {
366                    // fade out last series
367                    FadeTransition ft = new FadeTransition(Duration.millis(700), bar);
368                    ft.setFromValue(1);
369                    ft.setToValue(0);
370                    ft.setOnFinished(new EventHandler<ActionEvent>() {
371
372                        @Override
373                        public void handle(ActionEvent actionEvent) {
374                            getPlotChildren().remove(bar);
375                        }
376                    });
377                    pt.getChildren().add(ft);
378                }
379            }
380            pt.play();
381        } else {
382            for (Data<X, Y> d : series.getData()) {
383                final Node bar = d.getNode();
384                getPlotChildren().remove(bar);
385            }
386            removeSeriesFromDisplay(series);
387            requestChartLayout();
388        }
389    }
390
391    /** @inheritDoc */
392    @Override protected void updateAxisRange() {
393        // This override is necessary to update axis range based on cumulative Y value for the
394        // Y axis instead of the inherited way where the max value in the data range is used.
395        final Axis<X> xa = getXAxis();
396        final Axis<Y> ya = getYAxis();
397        if (xa.isAutoRanging()) {
398            List xData = new ArrayList<Number>();
399            if (xa instanceof CategoryAxis) {
400                xData.addAll(categoryAxis.getCategories());
401            } else {
402                int catIndex = 0;
403                for (String category : categoryAxis.getCategories()) {
404                    int index = 0;
405                    double totalXN = 0;
406                    double totalXP = 0;
407                    Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator();
408                    while (seriesIterator.hasNext()) {
409                        Series<X, Y> series = seriesIterator.next();
410                        for (final Data<X, Y> item : getDataItem(series, index, catIndex, category)) {;
411                            if (item != null) {
412                                boolean isNegative = item.getNode().getStyleClass().contains("negative");
413                                if (!isNegative) {
414                                    totalXP += xa.toNumericValue(item.getXValue());
415                                } else {
416                                    totalXN += xa.toNumericValue(item.getXValue());
417                                }
418                            }
419                        }
420                    }
421                    xData.add(totalXP);
422                    xData.add(totalXN);
423                    catIndex++;
424                }
425            }
426            xa.invalidateRange(xData);
427        }
428        if (ya.isAutoRanging()) {
429            List yData = new ArrayList<Number>();
430            if (ya instanceof CategoryAxis) {
431                yData.addAll(categoryAxis.getCategories());
432            } else {
433                int catIndex = 0;
434                for (String category : categoryAxis.getCategories()) {
435                    int index = 0;
436                    double totalYP = 0;
437                    double totalYN = 0;
438                    Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator();
439                    while (seriesIterator.hasNext()) {
440                        Series<X, Y> series = seriesIterator.next();
441                        for (final Data<X, Y> item : getDataItem(series, index, catIndex, category)) {;
442                            if(item != null) {
443                                boolean isNegative = item.getNode().getStyleClass().contains("negative");
444                                if (!isNegative) {
445                                    totalYP += ya.toNumericValue(item.getYValue());
446                                } else {
447                                    totalYN += ya.toNumericValue(item.getYValue());
448                                }
449                            }
450                        }
451                    }
452                    yData.add(totalYP);
453                    yData.add(totalYN);
454                    catIndex++;
455                }
456            }
457            ya.invalidateRange(yData);
458        }
459    }
460
461    /** @inheritDoc */
462    @Override protected void layoutPlotChildren() {
463        double catSpace = categoryAxis.getCategorySpacing();
464        // calculate bar spacing
465        final double availableBarSpace = catSpace - getCategoryGap();
466        final double barWidth = availableBarSpace;
467        final double barOffset = -((catSpace - getCategoryGap()) / 2);
468        final double zeroPos = valueAxis.getZeroPosition();
469        // update bar positions and sizes
470        int catIndex = 0;
471        for (String category : categoryAxis.getCategories()) {
472            int index = 0;
473            int currentPositiveHeight = 0;
474            int currentNegativeHeight = 0;
475            Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator();
476            while (seriesIterator.hasNext()) {
477                Series<X, Y> series = seriesIterator.next();
478                for (final Data<X, Y> item : getDataItem(series, index, catIndex, category)) {;
479                    if (item != null) {
480                        final Node bar = item.getNode();
481                        final double categoryPos;
482                        final double valPos;
483                        if (orientation == Orientation.VERTICAL) {
484                            categoryPos = getXAxis().getDisplayPosition(getCurrentDisplayedXValue(item));
485                            valPos = getYAxis().getDisplayPosition(getCurrentDisplayedYValue(item));
486                        } else {
487                            categoryPos = getYAxis().getDisplayPosition(getCurrentDisplayedYValue(item));
488                            valPos = getXAxis().getDisplayPosition(getCurrentDisplayedXValue(item));
489                        }
490                        final double bottom;
491                        final double top;
492                        boolean isNegative = bar.getStyleClass().contains("negative");
493                        if (!isNegative) {
494                            bottom = currentPositiveHeight + Math.min(valPos, zeroPos);
495                            top = currentPositiveHeight + Math.max(valPos, zeroPos);
496                            if (orientation == Orientation.VERTICAL) {
497                                currentPositiveHeight -= top - bottom;
498                            } else {
499                                currentPositiveHeight += top - bottom;
500                            }
501                        } else {
502                            bottom = currentNegativeHeight + Math.min(valPos, zeroPos);
503                            top = currentNegativeHeight + Math.max(valPos, zeroPos);
504                            if (orientation == Orientation.VERTICAL) {
505                                currentNegativeHeight += top - bottom;
506                            } else {
507                                currentNegativeHeight += top - bottom;
508                            }
509                        }
510                        if (orientation == Orientation.VERTICAL) {
511                            bar.resizeRelocate(categoryPos + barOffset,
512                                    bottom, barWidth, top - bottom);
513                        } else {
514                            //noinspection SuspiciousNameCombination
515                            bar.resizeRelocate(bottom,
516                                    categoryPos + barOffset,
517                                    top - bottom, barWidth);
518                        }
519                        index++;
520                    }
521                }
522            }
523            catIndex++;
524        }
525    }
526
527    /**
528        * Computes the size of series linked list
529        * @return size of series linked list
530        */
531    @Override int getSeriesSize() {
532        int count = 0;
533        Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator();
534        while (seriesIterator.hasNext()) {
535            seriesIterator.next();
536            count++;
537        }
538        return count;
539    }
540
541    /**
542        * This is called whenever a series is added or removed and the legend needs to be updated
543        */
544    @Override protected void updateLegend() {
545        legend.getItems().clear();
546        if (getData() != null) {
547            for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) {
548                Series series = getData().get(seriesIndex);
549                Legend.LegendItem legenditem = new Legend.LegendItem(series.getName());
550                String defaultColorStyleClass = seriesDefaultColorMap.get(series);
551                legenditem.getSymbol().getStyleClass().addAll("chart-bar", "series" + seriesIndex, "bar-legend-symbol",
552                        defaultColorStyleClass);
553                legend.getItems().add(legenditem);
554            }
555        }
556        if (legend.getItems().size() > 0) {
557            if (getLegend() == null) {
558                setLegend(legend);
559            }
560        } else {
561            setLegend(null);
562        }
563    }
564
565    private Node createBar(Series series, int seriesIndex, final Data item, int itemIndex) {
566        Node bar = item.getNode();
567        if (bar == null) {
568            bar = new StackPane();
569            item.setNode(bar);
570        }
571        String defaultColorStyleClass = seriesDefaultColorMap.get(series);
572        bar.getStyleClass().setAll("chart-bar", "series" + seriesIndex, "data" + itemIndex, defaultColorStyleClass);
573        return bar;
574    }
575
576    private List<Data<X, Y>> getDataItem(Series<X, Y> series, int seriesIndex, int itemIndex, String category) {
577        Map<String, List<Data<X, Y>>> catmap = seriesCategoryMap.get(series);
578        return catmap != null ? catmap.get(category) != null ? catmap.get(category) : new ArrayList<Data<X, Y>>() : new ArrayList<Data<X, Y>>();
579    }
580
581// -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------
582
583    /**
584    * Super-lazy instantiation pattern from Bill Pugh.
585    * @treatAsPrivate implementation detail
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>)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<CssMetaData<? extends Styleable, ?>>(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     */
618    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
619        return StyleableProperties.STYLEABLES;
620    }
621
622    /**
623     * {@inheritDoc}
624     */
625    @Override
626    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
627        return getClassCssMetaData();
628    }
629
630    /** Pseudoclass indicating this is a vertical chart. */
631    private static final PseudoClass VERTICAL_PSEUDOCLASS_STATE = 
632            PseudoClass.getPseudoClass("vertical");
633    /** Pseudoclass indicating this is a horizontal chart. */
634    private static final PseudoClass HORIZONTAL_PSEUDOCLASS_STATE = 
635            PseudoClass.getPseudoClass("horizontal");
636
637}