【问题标题】:How to add shapes on JavaFX LineChart如何在 JavaFX LineChart 上添加形状
【发布时间】:2016-12-16 16:58:58
【问题描述】:

我要在LineChart 上添加一些形状。我将LineChartAnchorPane 放入StackPane。通过从图表系列中获取 x 和 y 坐标,我将形状添加到 AnchorPane。这是一个例子。

LineChartApp.java

package shapes;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class LineChartApp extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setScene(new Scene(new ChartContent()));
        primaryStage.setMaximized(true);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

}

ChartContent.java

package shapes;

import java.util.ArrayList;
import java.util.List;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Shape;
import javafx.util.Duration;

public class ChartContent extends StackPane {

    private AnchorPane objectsLayer;
    private LineChart<Number, Number> chart;
    private NumberAxis xAxis;
    private NumberAxis yAxis;
    private Series<Number, Number> series = new Series<Number, Number>();
    private int level = 0;
    private int datas[][] = { { 15, 8, 12, 11, 16, 21, 13 },
            { 10, 24, 20, 16, 31, 25, 44 }, { 88, 60, 105, 75, 151, 121, 137 },
            { 1000, 1341, 1211, 1562, 1400, 1600, 1550 }

    };
    private List<Shape> shapes = new ArrayList<Shape>();

    public ChartContent() {

        xAxis = new NumberAxis();
        yAxis = new NumberAxis();

        yAxis.setSide(Side.RIGHT);
        yAxis.setForceZeroInRange(false);

        xAxis.setForceZeroInRange(false);

        chart = new LineChart<Number, Number>(xAxis, yAxis);
        chart.setCreateSymbols(false);
        chart.setLegendVisible(false);
        chart.setAnimated(false);
        chart.setVerticalZeroLineVisible(false);

        Timeline timer = new Timeline(new KeyFrame(Duration.seconds(5),
                new EventHandler<ActionEvent>() {

                    @Override
                    public void handle(ActionEvent event) {

                        chartRefresh();
                    }
                }));
        timer.setCycleCount(datas.length - 1);
        timer.play();

        objectsLayer = new AnchorPane();
        objectsLayer.prefHeightProperty().bind(heightProperty());
        objectsLayer.prefWidthProperty().bind(widthProperty());

        getChildren().addAll(chart, objectsLayer);
        chartRefresh();
    }

    private void chartRefresh() {

        series.getData().clear();
        if (level < datas.length) {

            for (int i = 0; i < datas[level].length; i++) {
                series.getData().add(
                        new Data<Number, Number>(i, datas[level][i]));
            }
        }
        level++;

        chart.getData().clear();
        chart.getData().add(series);
        series.getNode().setStyle("-fx-stroke:blue;-fx-stroke-width:1");

        reDrawShapes(series);
    }

    private void reDrawShapes(Series<Number, Number> series) {

        Node chartPlotBackground = chart.lookup(".chart-plot-background");
        chartPlotBackground.setStyle("-fx-background-color:white");

        Circle circle;
        objectsLayer.getChildren().removeAll(shapes);

        shapes.clear();
        double top = chart.getPadding().getTop(), left = chart.getPadding()
                .getLeft();
        double minX = chartPlotBackground.getBoundsInParent().getMinX();
        double minY = chartPlotBackground.getBoundsInParent().getMinY();

        for (Data<Number, Number> data : series.getData()) {

            circle = new Circle(minX
                    + chart.getXAxis().getDisplayPosition(data.getXValue())
                    + left, minY
                    + chart.getYAxis().getDisplayPosition(data.getYValue())
                    + top, 3, Color.RED);

            shapes.add(circle);
        }

        objectsLayer.getChildren().addAll(shapes);
    }
}

我每五秒刷新一次图表系列并重新绘制其形状。但是在将形状添加到AnchorPane 之后,它们不在我期望的位置。


预期结果


实际结果

【问题讨论】:

    标签: java javafx


    【解决方案1】:

    首先,请注意,对于您要实现的确切功能,只需在数据上设置一个节点即可。

    (旁白:可以说,我会说,让节点成为图表中显示的数据的属性几乎违反了在 UI 开发中将视图与数据分离的所有良好实践。Chart API 具有许多糟糕的设计缺陷,恕我直言,这就是其中之一。可能应该有类似图表本身的Function&lt;Data&lt;X,Y&gt;, Node&gt; nodeFactory 属性的东西。但是,它就是这样。)

    private void chartRefresh() {
    
        series.getData().clear();
        if (level < datas.length) {
    
            for (int i = 0; i < datas[level].length; i++) {
                Data<Number, Number> data = new Data<Number, Number>(i, datas[level][i]);
                data.setNode(new Circle(3, Color.RED));
                series.getData().add(data);
            }
        }
        level++;
    
        chart.getData().clear();
        chart.getData().add(series);
        series.getNode().setStyle("-fx-stroke:blue;-fx-stroke-width:1");
    
        // reDrawShapes(series);
    }
    

    如果您的节点足够简单以至于您需要将其集中在该点上,则此方法有效。

    如果您想要更复杂的东西,而这不起作用,支持的机制是子类化图表类并覆盖layoutPlotChildren() 方法。这是使用这种方法的完整类:

    import java.util.ArrayList;
    import java.util.List;
    
    import javafx.animation.KeyFrame;
    import javafx.animation.Timeline;
    import javafx.event.ActionEvent;
    import javafx.event.EventHandler;
    import javafx.geometry.Side;
    import javafx.scene.chart.LineChart;
    import javafx.scene.chart.NumberAxis;
    import javafx.scene.chart.XYChart.Data;
    import javafx.scene.chart.XYChart.Series;
    import javafx.scene.layout.StackPane;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Circle;
    import javafx.scene.shape.Shape;
    import javafx.util.Duration;
    
    public class ChartContent extends StackPane {
    
        private LineChart<Number, Number> chart;
        private NumberAxis xAxis;
        private NumberAxis yAxis;
        private Series<Number, Number> series = new Series<Number, Number>();
        private int level = 0;
        private int datas[][] = { { 15, 8, 12, 11, 16, 21, 13 },
                { 10, 24, 20, 16, 31, 25, 44 }, { 88, 60, 105, 75, 151, 121, 137 },
                { 1000, 1341, 1211, 1562, 1400, 1600, 1550 }
    
        };
    
        public ChartContent() {
    
            xAxis = new NumberAxis();
            yAxis = new NumberAxis();
    
            yAxis.setSide(Side.RIGHT);
            yAxis.setForceZeroInRange(false);
    
            xAxis.setForceZeroInRange(false);
    
            chart = new LineChart<Number, Number>(xAxis, yAxis) {
    
                private List<Shape> shapes = new ArrayList<>();
    
                @Override
                public void layoutPlotChildren() {
                    super.layoutPlotChildren();
                    getPlotChildren().removeAll(shapes);
                    shapes.clear();
                    for (Data<Number, Number> d : series.getData()) {
                        double x = xAxis.getDisplayPosition(d.getXValue());
                        double y = yAxis.getDisplayPosition(d.getYValue());
                        shapes.add(new Circle(x, y, 3, Color.RED));
                    }
                    getPlotChildren().addAll(shapes);
                }
            };
            chart.setCreateSymbols(false);
            chart.setLegendVisible(false);
            chart.setAnimated(false);
            chart.setVerticalZeroLineVisible(false);
    
            Timeline timer = new Timeline(new KeyFrame(Duration.seconds(5),
                    new EventHandler<ActionEvent>() {
    
                        @Override
                        public void handle(ActionEvent event) {
    
                            chartRefresh();
                        }
                    }));
            timer.setCycleCount(datas.length - 1);
            timer.play();
    
            getChildren().addAll(chart);
            chartRefresh();
        }
    
        private void chartRefresh() {
    
            series.getData().clear();
            if (level < datas.length) {
    
                for (int i = 0; i < datas[level].length; i++) {
                    Data<Number, Number> data = new Data<Number, Number>(i, datas[level][i]);
                    data.setNode(new Circle(3, Color.RED));
                    series.getData().add(data);
                }
            }
            level++;
    
            chart.getData().clear();
            chart.getData().add(series);
            series.getNode().setStyle("-fx-stroke:blue;-fx-stroke-width:1");
    
        }
    
    
    }
    

    这导致


    您可以使用此技术,例如,将最佳拟合线添加到散点图或趋势线添加到折线图等。

    我无法确切说明为什么您使用的代码不起作用,但它对布局的管理方式(即chart-plot-background 相对于整个图表本身的位置)以及何时进行了一些假设进行测量以执行诸如计算从“图表坐标”到“像素坐标”映射的轴中的比例之类的事情。不难想象,当数据发生变化并且仅在布局过程开始时重新计算时,这些会变得无效。记录“数据值”(data.getXValue()data.getYValue())以及从 Axis.getDisplayValue(...) 获得的值,这表明可能是类似于后一种解释的情况,因为这些似乎肯定不会产生正确的转变。

    挂钩到layoutPlotChildren() 方法更可靠。

    【讨论】:

    • 非常感谢。这是一个绝妙的答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-08-20
    • 1970-01-01
    • 2019-05-25
    • 2012-08-17
    • 1970-01-01
    • 1970-01-01
    • 2017-06-16
    相关资源
    最近更新 更多