简体   繁体   English

如何在 JavaFX LineChart 上添加形状

[英]How to add shapes on JavaFX LineChart

I am going to add some shapes on LineChart .我将在LineChart上添加一些形状。 I put LineChart and AnchorPane into the StackPane .我将LineChartAnchorPane放入StackPane I added shapes to AnchorPane by getting x and y coordinates from the chart series.我通过从图表系列中获取 x 和 y 坐标向AnchorPane添加了形状。 Here is example.这是示例。

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图表内容.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);
    }
}

I am refreshing chart series every five seconds and redrawing its shapes as well.我每五秒刷新一次图表系列并重新绘制其形状。 But after the shapes added to the AnchorPane , they are not there where I expect them to be.但是在将形状添加到AnchorPane ,它们不在我期望的位置。


Expected Result预期结果

预期结果:数据点处带有红色圆圈的折线图


Actual Result实际结果

实际结果:在看似随机的地方带有红色圆圈的折线图

First, note that for the exact functionality you're trying to achieve, this can be done simply by setting a node on the data.首先,请注意,对于您要实现的确切功能,只需在数据上设置节点即可完成。

(Aside: it could be argued, and I would argue, that making a node a property of the data displayed in the chart violates pretty much every good practice on the separation of view from data in UI development. The Chart API has a number of bad design flaws, imho, and this is one of them. There probably should be something like a Function<Data<X,Y>, Node> nodeFactory property of the Chart itself for this. However, it is what it is.) (旁白:可以争论,我会争论,使节点成为图表中显示的数据的属性几乎违反了 UI 开发中将视图与数据分离的所有良好实践。图表 API 有许多糟糕的设计缺陷,恕我直言,这就是其中之一。可能应该有类似 Chart 本身的Function<Data<X,Y>, Node> 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);
}

This works if your node is simple enough that centering it on the point is what you need.如果您的节点足够简单以至于您需要将其集中在该点上,则此方法有效。

If you want something more complex, for which this doesn't work, the supported mechanism is to subclass the chart class and override the layoutPlotChildren() method.如果您想要更复杂的东西,而这对其不起作用,则支持的机制是子类化图表类并覆盖layoutPlotChildren()方法。 Here's the complete class using this approach:这是使用这种方法的完整类:

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");

    }


}

This results in这导致

数据点处带有红色圆圈的折线图


You can use this technique to, for example, add best fit lines to scatter plots or trend lines to line charts, etc.例如,您可以使用此技术将最佳拟合线添加到散点图或将趋势线添加到折线图等。

I can't tell exactly why the code you used doesn't work, but it makes several assumptions about how the layout is managed (ie the location of chart-plot-background in relation to the overall chart itself) and also about when measurements are taken in order to do things like compute the scale in the axes for the mapping from "chart coordinates" to "pixel coordinates".我无法确切说明为什么您使用的代码不起作用,但它对布局的管理方式(即chart-plot-background相对于整体图表本身的位置)以及何时进行测量做出了一些假设是为了做一些事情,比如计算从“图表坐标”到“像素坐标”的映射的轴中的比例。 It's not too hard to imagine these becoming invalid when the data changes and only being recalculated at the beginning of the layout process, for example.例如,不难想象当数据更改时这些数据会变得无效并且仅在布局过程开始时重新计算。 Logging the "data values" ( data.getXValue() and data.getYValue() ) alongside the values you get from Axis.getDisplayValue(...) for those values suggests that something akin to the latter explanation may be the case, as those definitely do not seem to produce the correct transformations.将“数据值”( data.getXValue()data.getYValue() )与您从Axis.getDisplayValue(...)获得的值一起Axis.getDisplayValue(...)这些值表明可能类似于后一种解释的情况,如那些肯定不会产生正确的转换。

Hooking into the layoutPlotChildren() method is more reliable.连接到layoutPlotChildren()方法更可靠。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM