如何在JavaFX中混合堆积条形图和折线图?

时间:2013-05-13 15:45:14

标签: java javafx-2 javafx-8

我已准备好堆积条形图...我想在同一图表中添加折线图,以显示“最高级别”...我该怎么做?

2 个答案:

答案 0 :(得分:3)

RFE:https://javafx-jira.kenai.com/browse/RT-22949

现在没有办法,除非你自己实施。

答案 1 :(得分:1)

/*
 *
 * This class overwrites XYChart, it allows to have a StackedBarChart and
 * additionally to have a "line", this line can be passes as parameter exactly 
 * the same way as when a StackedBarChart is created, but adding the title "Limit".
 * example:
 * ------------------------------------------------
 *       ObservableList<XYChart.Series> barChartData = FXCollections.observableArrayList(
 *               new ParetoChart.Series("Serie # 1", FXCollections.observableArrayList(
 *                   stackedBarCharts)),
 *               new ParetoChart.Series("Limit", FXCollections.observableArrayList(
 *                   limit))
 *       );
 *        XYChart chart = new ParetoChart(xAxis, yAxis, barChartData, 0);
 * 
 * 
 * Where stackedBarCharts and limit are Lists with the corresponding values.
 * ---------------------------------------------
 * @author Juan Jose Robles - robles.gomez.juan@gmail.com
 *
 */


import java.util.*;
import javafx.animation.*;
import javafx.beans.property.DoubleProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import com.sun.javafx.charts.Legend;
import com.sun.javafx.css.StyleableDoubleProperty;
import com.sun.javafx.css.StyleableProperty;
import com.sun.javafx.css.converters.SizeConverter;
import javafx.beans.value.WritableValue;
import javafx.scene.chart.*;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;


    /**
     * ParetoChart is a variation of {@link BarChart} that plots bars indicating 
     * data values for a category. The bars can be vertical or horizontal depending 
     * on which axis is a category axis. 
     * The bar for each series is stacked on top of the previous series.
     */
    public class ParetoChart<X, Y> extends XYChart<X, Y> {

        // -------------- PRIVATE FIELDS -------------------------------------------
        private Map<Series, Map<String, Data<X, Y>>> seriesCategoryMap = new HashMap<Series, Map<String, Data<X, Y>>>();
        private Legend legend = new Legend();
        private final Orientation orientation;
        private CategoryAxis categoryAxis;
        private ValueAxis valueAxis;
        private int seriesDefaultColorIndex = 0;
        private Map<Series<X, Y>, String> seriesDefaultColorMap = new HashMap<Series<X, Y>, String>();
        // -------------- PUBLIC PROPERTIES ----------------------------------------
        /** The gap to leave between bars in separate categories */
        private DoubleProperty categoryGap = new StyleableDoubleProperty(10) {
            @Override protected void invalidated() {
                get();
                requestChartLayout();
            }

            @Override
            public Object getBean() {
                return ParetoChart.this;
            }

            @Override
            public String getName() {
                return "categoryGap";
            }

            public StyleableProperty getStyleableProperty() {
                return ParetoChart.StyleableProperties.CATEGORY_GAP;
            }
        };

        public double getCategoryGap() {
            return categoryGap.getValue();
        }

        public void setCategoryGap(double value) {
            categoryGap.setValue(value);
        }

        public DoubleProperty categoryGapProperty() {
            return categoryGap;
        }

        // -------------- CONSTRUCTOR ----------------------------------------------
        /**
         * Construct a new StackedBarChart with the given axis. The two axis should be a ValueAxis/NumberAxis and a CategoryAxis,
         * they can be in either order depending on if you want a horizontal or vertical bar chart.
         *
         * @param xAxis The x axis to use
         * @param yAxis The y axis to use
         */
        public ParetoChart(Axis<X> xAxis, Axis<Y> yAxis) {
            this(xAxis, yAxis, FXCollections.<Series<X, Y>>observableArrayList());
        }

        /**
         * Construct a new ParetoChart with the given axis and data. The two axis should be a ValueAxis/NumberAxis and a
         * CategoryAxis, they can be in either order depending on if you want a horizontal or vertical bar chart.
         *
         * @param xAxis The x axis to use
         * @param yAxis The y axis to use
         * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart
         */
        public ParetoChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<Series<X, Y>> data) {
            super(xAxis, yAxis);
            getStyleClass().add("stacked-bar-chart");
            setLegend(legend);
            if (!((xAxis instanceof ValueAxis && yAxis instanceof CategoryAxis)
                    || (yAxis instanceof ValueAxis && xAxis instanceof CategoryAxis))) {
                throw new IllegalArgumentException("Axis type incorrect, one of X,Y should be CategoryAxis and the other NumberAxis");
            }
            if (xAxis instanceof CategoryAxis) {
                categoryAxis = (CategoryAxis) xAxis;
                valueAxis = (ValueAxis) yAxis;
                orientation = Orientation.VERTICAL;
            } else {
                categoryAxis = (CategoryAxis) yAxis;
                valueAxis = (ValueAxis) xAxis;
                orientation = Orientation.HORIZONTAL;
            }
            setData(data);
        }

        /**
         * Construct a new ParetoChart with the given axis and data. The two axis should be a ValueAxis/NumberAxis and a
         * CategoryAxis, they can be in either order depending on if you want a horizontal or vertical bar chart.
         *
         * @param xAxis The x axis to use
         * @param yAxis The y axis to use
         * @param data The data to use, this is the actual list used so any changes to it will be reflected in the chart
         * @param categoryGap The gap to leave between bars in separate categories
         */
        public ParetoChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<Series<X, Y>> data, double categoryGap) {
            this(xAxis, yAxis);
            setData(data);
            setCategoryGap(categoryGap);
        }

        // -------------- METHODS --------------------------------------------------
        @Override protected void dataItemAdded(Series<X, Y> series, int itemIndex, Data<X, Y> item) {
            String category;
            if (orientation == Orientation.VERTICAL) {
                category = (String) item.getXValue();
            } else {
                category = (String) item.getYValue();
            }
            // Don't plot if category does not already exist ?
//        if (!categoryAxis.getCategories().contains(category)) return;

            Map<String, Data<X, Y>> categoryMap = seriesCategoryMap.get(series);

            if (categoryMap == null) {
                categoryMap = new HashMap<String, Data<X, Y>>();
                seriesCategoryMap.put(series, categoryMap);
            }
            categoryMap.put(category, item);
            Node bar = createBar(series, getData().indexOf(series), item, itemIndex);
            if (shouldAnimate()) {
                animateDataAdd(item, bar);
            } else {
                getPlotChildren().add(bar);
            }
        }

        @Override protected void dataItemRemoved(final Data<X, Y> item, final Series<X, Y> series) {
            final Node bar = item.getNode();
            if (shouldAnimate()) {
                Timeline t = createDataRemoveTimeline(item, bar, series);
                t.setOnFinished(new EventHandler<ActionEvent>() {

                    public void handle(ActionEvent event) {
                        removeDataItemFromDisplay(series, item);
                    }
                });
                t.play();
            } else {
                getPlotChildren().remove(bar);
                removeDataItemFromDisplay(series, item);
            }
        }

        /** @inheritDoc */
        @Override protected void dataItemChanged(Data<X, Y> item) {
            double barVal;
            double currentVal;
            if (orientation == Orientation.VERTICAL) {
                barVal = ((Number) item.getYValue()).doubleValue();
                currentVal = ((Number) getCurrentDisplayedYValue(item)).doubleValue();
            } else {
                barVal = ((Number) item.getXValue()).doubleValue();
                currentVal = ((Number) getCurrentDisplayedXValue(item)).doubleValue();
            }
            if (currentVal > 0 && barVal < 0) { // going from positive to negative
                // add style class negative
                item.getNode().getStyleClass().add("negative");
            } else if (currentVal < 0 && barVal > 0) { // going from negative to positive
                // remove style class negative
                item.getNode().getStyleClass().add("negative");
            }
        }

        private void animateDataAdd(Data<X, Y> item, Node bar) {
            double barVal;
            if (orientation == Orientation.VERTICAL) {
                barVal = ((Number) item.getYValue()).doubleValue();
                if (barVal < 0) {
                    bar.getStyleClass().add("negative");
                }
                item.setYValue(getYAxis().toRealValue(getYAxis().getZeroPosition()));
                setCurrentDisplayedYValue(item, getYAxis().toRealValue(getYAxis().getZeroPosition()));
                getPlotChildren().add(bar);
                item.setYValue(getYAxis().toRealValue(barVal));
                animate(
                        TimelineBuilder.create().keyFrames(
                        new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedYValueProperty(item), getCurrentDisplayedYValue(item))),
                        new KeyFrame(Duration.millis(700), new KeyValue(currentDisplayedYValueProperty(item), item.getYValue(), Interpolator.EASE_BOTH))).build());
            } else {
                barVal = ((Number) item.getXValue()).doubleValue();
                if (barVal < 0) {
                    bar.getStyleClass().add("negative");
                }
                item.setXValue(getXAxis().toRealValue(getXAxis().getZeroPosition()));
                setCurrentDisplayedXValue(item, getXAxis().toRealValue(getXAxis().getZeroPosition()));
                getPlotChildren().add(bar);
                item.setXValue(getXAxis().toRealValue(barVal));
                animate(
                        TimelineBuilder.create().keyFrames(
                        new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedXValueProperty(item), getCurrentDisplayedXValue(item))),
                        new KeyFrame(Duration.millis(700), new KeyValue(currentDisplayedXValueProperty(item), item.getXValue(), Interpolator.EASE_BOTH))).build());
            }
        }

        /** @inheritDoc */
        @Override protected void seriesAdded(Series<X, Y> series, int seriesIndex) {
            String defaultColorStyleClass = "default-color" + (seriesDefaultColorIndex % 8);
            seriesDefaultColorMap.put(series, defaultColorStyleClass);
            seriesDefaultColorIndex++;
            // handle any data already in series
            // create entry in the map
            Map<String, Data<X, Y>> categoryMap = new HashMap<String, Data<X, Y>>();
            for (int j = 0; j < series.getData().size(); j++) {
                Data<X, Y> item = series.getData().get(j);
                Node bar = createBar(series, seriesIndex, item, j);
                String category;
                if (orientation == Orientation.VERTICAL) {
                    category = (String) item.getXValue();
                } else {
                    category = (String) item.getYValue();
                }
                categoryMap.put(category, item);
                if (shouldAnimate()) {
                    animateDataAdd(item, bar);
                } else {
                    getPlotChildren().add(bar);
                }
            }
            if (categoryMap.size() > 0) {
                seriesCategoryMap.put(series, categoryMap);
            }
            Path seriesPath = new Path();
            seriesPath.getStyleClass().setAll("candlestick-average-line","series"+seriesIndex);
            series.setNode(seriesPath);
            getPlotChildren().add(seriesPath);            
        }

        private Timeline createDataRemoveTimeline(Data<X, Y> item, final Node bar, final Series<X, Y> series) {
            Timeline t = new Timeline();
            if (orientation == Orientation.VERTICAL) {
                item.setYValue(getYAxis().toRealValue(getYAxis().getZeroPosition()));
                t.getKeyFrames().addAll(
                        new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedYValueProperty(item), getCurrentDisplayedYValue(item))),
                        new KeyFrame(Duration.millis(700), new EventHandler<ActionEvent>() {

                    @Override
                    public void handle(ActionEvent actionEvent) {
                        getPlotChildren().remove(bar);
                    }
                },
                        new KeyValue(currentDisplayedYValueProperty(item), item.getYValue(), Interpolator.EASE_BOTH)));
            } else {
                item.setXValue(getXAxis().toRealValue(getXAxis().getZeroPosition()));
                t.getKeyFrames().addAll(
                        new KeyFrame(Duration.ZERO, new KeyValue(currentDisplayedXValueProperty(item), getCurrentDisplayedXValue(item))),
                        new KeyFrame(Duration.millis(700), new EventHandler<ActionEvent>() {

                    @Override
                    public void handle(ActionEvent actionEvent) {
                        getPlotChildren().remove(bar);
                    }
                },
                        new KeyValue(currentDisplayedXValueProperty(item), item.getXValue(), Interpolator.EASE_BOTH)));
            }
            return t;
        }

        @Override protected void seriesRemoved(final Series<X, Y> series) {
            // remove all symbol nodes
            if (shouldAnimate()) {
                ParallelTransition pt = new ParallelTransition();
                pt.setOnFinished(new EventHandler<ActionEvent>() {

                    public void handle(ActionEvent event) {
                        removeSeriesFromDisplay(series);
                    }
                });
                for (Data<X, Y> d : series.getData()) {
                    final Node bar = d.getNode();
                    // Animate series deletion
                    if (getSeriesSize() > 1) {
                        for (int j = 0; j < series.getData().size(); j++) {
                            Data<X, Y> item = series.getData().get(j);
                            Timeline t = createDataRemoveTimeline(item, bar, series);
                            pt.getChildren().add(t);
                        }
                    } else {
                        // fade out last series
                        FadeTransition ft = new FadeTransition(Duration.millis(700), bar);
                        ft.setFromValue(1);
                        ft.setToValue(0);
                        ft.setOnFinished(new EventHandler<ActionEvent>() {

                            @Override
                            public void handle(ActionEvent actionEvent) {
                                getPlotChildren().remove(bar);
                            }
                        });
                        pt.getChildren().add(ft);
                    }
                }
                pt.play();
            } else {
                for (Data<X, Y> d : series.getData()) {
                    final Node bar = d.getNode();
                    getPlotChildren().remove(bar);
                }
                removeSeriesFromDisplay(series);
            }
        }

        /** @inheritDoc */
        @Override protected void updateAxisRange() {
            // This override is necessary to update axis range based on cumulative Y value for the
            // Y axis instead of the inherited way where the max value in the data range is used.
            final Axis<X> xa = getXAxis();
            final Axis<Y> ya = getYAxis();
            if (xa.isAutoRanging()) {
                List xData = new ArrayList<Number>();
                if (xa instanceof CategoryAxis) {
                    xData.addAll(categoryAxis.getCategories());
                } else {
                    int catIndex = 0;
                    for (String category : categoryAxis.getCategories()) {
                        int index = 0;
                        double totalX = 0;
                        Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator();
                        while (seriesIterator.hasNext()) {
                            Series<X, Y> series = seriesIterator.next();
                            final Data<X, Y> item = getDataItem(series, index, catIndex, category);
                            totalX += xa.toNumericValue(item.getXValue());
                        }
                        xData.add(totalX);
                        catIndex++;
                    }
                }
                xa.invalidateRange(xData);
            }
            if (ya.isAutoRanging()) {
                List yData = new ArrayList<Number>();
                if (ya instanceof CategoryAxis) {
                    yData.addAll(categoryAxis.getCategories());
                } else {
                    int catIndex = 0;
                    for (String category : categoryAxis.getCategories()) {
                        int index = 0;
                        double totalY = 0;
                        Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator();
                        while (seriesIterator.hasNext()) {
                            Series<X, Y> series = seriesIterator.next();
                            final Data<X, Y> item = getDataItem(series, index, catIndex, category);
                            if(item != null) 
                                totalY += ya.toNumericValue(item.getYValue());
                        }
                        yData.add(totalY);
                        catIndex++;
                    }
                }
                ya.invalidateRange(yData);
            }
        }

        /** @inheritDoc */
        @Override protected void layoutPlotChildren() {
            double catSpace = categoryAxis.getCategorySpacing();
            // calculate bar spacing
            final double availableBarSpace = catSpace - getCategoryGap();
            final double barWidth = availableBarSpace;
            final double barOffset = -((catSpace - getCategoryGap()) / 2);
            final double zeroPos = valueAxis.getZeroPosition();
            // update bar positions and sizes
            int catIndex = 0;

            for (String category : categoryAxis.getCategories()) {
                int index = 0;
                int currentHeight = 0;
                Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator();
                Path seriesPath = null;
                while (seriesIterator.hasNext()) {
                    Series<X, Y> series = seriesIterator.next();

                    final Data<X, Y> item = getDataItem(series, index, catIndex, category);
                    double valPos = 0;
                    double categoryPos = 0;

                    if (item != null) {
                        final Node bar = item.getNode();

                        if (orientation == Orientation.VERTICAL) {

                            //Results res = (Results)getCurrentDisplayedYValue(item);
                            categoryPos = getXAxis().getDisplayPosition(getCurrentDisplayedXValue(item));
                            valPos = getYAxis().getDisplayPosition(getCurrentDisplayedYValue(item));
                            //valPos = getYAxis().res.getDuration();
                        } else {
                            categoryPos = getYAxis().getDisplayPosition(getCurrentDisplayedYValue(item));
                            valPos = getXAxis().getDisplayPosition(getCurrentDisplayedXValue(item));
                        }
                        final double bottom = currentHeight + Math.min(valPos, zeroPos);
                        final double top = currentHeight + Math.max(valPos, zeroPos);
                        if (orientation == Orientation.VERTICAL) {
                            if (series.getName().equals("Limit")){
                                bar.resizeRelocate(0,
                                    bottom, 0, top - bottom);
                            } 
                            else{
                                bar.resizeRelocate(categoryPos + barOffset,
                                    bottom, barWidth, top - bottom);

                            }
                        } else {
                            //noinspection SuspiciousNameCombination
                            bar.resizeRelocate(bottom,
                                    categoryPos + barOffset,
                                    top - bottom, barWidth);
                        }

                        currentHeight -= top - bottom;
                        index++;
                    }

                    if (series.getName().equals("Limit")){
                        if (series.getNode() instanceof Path) {
                            seriesPath = (Path)series.getNode();
                            //seriesPath.getElements().clear();
                        }                    
                        if (seriesPath != null) {
                            if (seriesPath.getElements().isEmpty()) {
                                seriesPath.getElements().add(new MoveTo( categoryPos,valPos));
                            } else {
                                seriesPath.getElements().add(new LineTo( categoryPos,valPos));
                            }
                        }                        
                    }                    

                }
                catIndex++;
            }

        }

        /**
         * Computes the size of series linked list
         * @return size of series linked list
         */
        int getSeriesSize() {
            int count = 0;
            Iterator<Series<X, Y>> seriesIterator = getDisplayedSeriesIterator();
            while (seriesIterator.hasNext()) {
                seriesIterator.next();
                count++;
            }
            return count;
        }

        /**
         * This is called whenever a series is added or removed and the legend needs to be updated
         */
        @Override protected void updateLegend() {
            legend.getItems().clear();
            if (getData() != null) {
                for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) {
                    Series series = getData().get(seriesIndex);
                    String name = series.getName();
                    if ("".equals(name)){
                        name = "Undefined";
                    }
                    if (name.equals("Limit")){
                        continue;
                    }
                    Legend.LegendItem legenditem = new Legend.LegendItem(name);
                    String defaultColorStyleClass = seriesDefaultColorMap.get(series);
                    legenditem.getSymbol().getStyleClass().addAll("chart-bar", "series" + seriesIndex, "bar-legend-symbol",
                            defaultColorStyleClass);
                    legend.getItems().add(legenditem);

                }
            }
            if (legend.getItems().size() > 0) {
                if (getLegend() == null) {
                    setLegend(legend);
                }
            } else {
                setLegend(null);
            }
        }

        private Node createBar(Series series, int seriesIndex, final Data item, int itemIndex) {
            Node bar = item.getNode();
            if (bar == null) {
                bar = new StackPane();
                item.setNode(bar);
            }
            String defaultColorStyleClass = seriesDefaultColorMap.get(series);
            bar.getStyleClass().setAll("chart-bar", "series" + seriesIndex, "data" + itemIndex, defaultColorStyleClass);
            return bar;
        }

        private Data<X, Y> getDataItem(Series<X, Y> series, int seriesIndex, int itemIndex, String category) {
            Map<String, Data<X, Y>> catmap = seriesCategoryMap.get(series);
            if (catmap == null) return null;
            return catmap.get(category);
        }



// -------------- STYLESHEET HANDLING ------------------------------------------------------------------------------

    /**
      * Super-lazy instantiation pattern from Bill Pugh.
      * @treatAsPrivate implementation detail
      */
     private static class StyleableProperties {

         private static final StyleableProperty<ParetoChart,Number> CATEGORY_GAP = 
             new StyleableProperty<ParetoChart,Number>("-fx-category-gap",
                 SizeConverter.getInstance(), 10.0)  {

            @Override
            public boolean isSettable(ParetoChart node) {
                return node.categoryGap == null || !node.categoryGap.isBound();
            }

            @Override
            public WritableValue<Number> getWritableValue(ParetoChart node) {
                 return node.categoryGapProperty();
            }
        };

         private static final List<StyleableProperty> STYLEABLES;

         static {

            final List<StyleableProperty> styleables =
                new ArrayList<StyleableProperty>(XYChart.impl_CSS_STYLEABLES());
            Collections.addAll(styleables,
                CATEGORY_GAP
            );
            STYLEABLES = Collections.unmodifiableList(styleables);
         }
    }

     /**
     * @treatAsPrivate implementation detail
     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
     */
    @Deprecated
    public static List<StyleableProperty> impl_CSS_STYLEABLES() {
        return ParetoChart.StyleableProperties.STYLEABLES;
    }

    /**
     * RT-19263
     * @treatAsPrivate implementation detail
     * @deprecated This is an experimental API that is not intended for general use and is subject to change in future versions
     */
    @Deprecated
    public List<StyleableProperty> impl_getStyleableProperties() {
        return impl_CSS_STYLEABLES();
    }

}