如何在JavaFX LineChart上添加形状(How to add shapes on JavaFX LineChart)

编程入门 行业动态 更新时间:2024-10-28 07:24:50
如何在JavaFX LineChart上添加形状(How to add shapes on JavaFX LineChart)

我将在LineChart上添加一些形状。 我把LineChart和AnchorPane放入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 ,它们不在我所期望的那里。


预期结果


实际结果

I am going to add some shapes on LineChart. I put LineChart and AnchorPane into the StackPane. I added shapes to AnchorPane by getting x and y coordinates from the chart series. 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

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.


Expected Result


Actual Result

最满意答案

首先,请注意,对于您尝试实现的确切功能,只需在数据上设置节点即可完成此操作。

(另外:可以说,我认为,将节点作为图表中显示的数据的属性违反了几乎所有在UI开发中将数据与视图分开的良好做法。Chart API有许多不好的设计缺陷,imho,这就是其中之一,这可能应该是图表本身的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); }

如果您的节点足够简单,可以将其集中在点上,那么这就可以了。

如果你想要一些更复杂的东西,但这不起作用,支持的机制是子类化图表类并覆盖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 values”( data.getXValue()和data.getYValue() )与您从Axis.getDisplayValue(...)获得的值一起Axis.getDisplayValue(...)这些值,这表明类似于后面的解释可能是这种情况,那些肯定似乎没有产生正确的转换。

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

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.)

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. 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

Line chart with red circles at data points


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 (i.e. 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". 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.

Hooking into the layoutPlotChildren() method is more reliable.

更多推荐

本文发布于:2023-08-05 16:53:00,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1437513.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:形状   如何在   JavaFX   LineChart   add

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!