具有对数轴的 JavaFX 折线图没有很好地更新[关闭]
Posted
技术标签:
【中文标题】具有对数轴的 JavaFX 折线图没有很好地更新[关闭]【英文标题】:JavaFX Linechart with logarithmic axis not updating well [closed] 【发布时间】:2021-12-22 01:55:13 【问题描述】:我的应用程序中有两个折线图。一种是线性轴,另一种是对数轴。 当我想只查看图表中的一个系列时,我将其他系列设置为不可见,因此我只能看到该系列,并且我也使用相同的方法再次可视化所有系列。
我已经尝试过使用线程,但我的问题仍然存在:在带有线性轴的图表中我没有任何问题,但对数图表不能很好地更新数据。 某些节点保留或未显示,例如,在添加或删除数据的可见性时似乎图表冻结。只有当我调整窗口大小并且我不明白它为什么相关时,一切才会顺利。这是我只显示具有特定名称的系列的方法:
new Thread(() ->
for (Series<Number, Number> series : lineChart.getData())
Platform.runLater(() ->
if (series.getName().equals(name))
series.getNode().setVisible(!series.getNode().isVisible());
series.getData().forEach(data -> data.getNode().setVisible(series.getNode().isVisible()));
);
).start();
这是我用于对数轴的类:
public class LogarithmicAxis extends ValueAxis<Number>
private Object currentAnimationID;
private final ChartLayoutAnimator animator = new ChartLayoutAnimator(this);
private final DoubleProperty logUpperBound = new SimpleDoubleProperty();
private final DoubleProperty logLowerBound = new SimpleDoubleProperty();
public LogarithmicAxis()
super(0.0001, 1000);
bindLogBoundsToDefaultBounds();
public LogarithmicAxis(double lowerBound, double upperBound)
super(lowerBound, upperBound);
validateBounds(lowerBound, upperBound);
bindLogBoundsToDefaultBounds();
public void setLogarithmicUpperBound(double d)
double nd = Math.pow(10, Math.ceil(Math.log10(d)));
setUpperBound(nd == d ? nd * 10 : nd);
/**
* Binds logarithmic bounds with the super class bounds, consider the
* base 10 logarithmic scale.
*/
private void bindLogBoundsToDefaultBounds()
logLowerBound.bind(new DoubleBinding()
super.bind(lowerBoundProperty());
@Override
protected double computeValue()
return Math.log10(lowerBoundProperty().get());
);
logUpperBound.bind(new DoubleBinding()
super.bind(upperBoundProperty());
@Override
protected double computeValue()
return Math.log10(upperBoundProperty().get());
);
/**
* Validates the bounds by throwing an exception if the values are not
* conform to the mathematics log interval: [0,Double.MAX_VALUE]
*
*/
private void validateBounds(double lowerBound, double upperBound) throws IllegalLogarithmicRangeException
if (lowerBound < 0 || upperBound < 0 || lowerBound > upperBound)
throw new IllegalLogarithmicRangeException(
"The logarithmic range should be in [0,Double.MAX_VALUE] and the lowerBound should be less than the upperBound");
/**
* It is used to get the list of minor tick marks position to display on the axis.
* It's based on the number of minor tick and the logarithmic formula.
*
*/
@Override
protected List<Number> calculateMinorTickMarks()
List<Number> minorTickMarksPositions = new ArrayList<>();
return minorTickMarksPositions;
//Then, the calculateTickValues method
/**
* It is used to calculate a list of all the data values for each tick mark in range,
* represented by the second parameter. Displays one tick each power of 10.
*
*/
@Override
protected List<Number> calculateTickValues(double length, Object range)
LinkedList<Number> tickPositions = new LinkedList<>();
if (range != null)
double lowerBound = ((double[]) range)[0];
double upperBound = ((double[]) range)[1];
for (double i = Math.log10(lowerBound); i <= Math.log10(upperBound); i++)
tickPositions.add(Math.pow(10, i));
if (!tickPositions.isEmpty())
if (tickPositions.getLast().doubleValue() != upperBound)
tickPositions.add(upperBound);
return tickPositions;
/**
* The getRange provides the current range of the axis. A basic
* implementation is to return an array of the lowerBound and upperBound
* properties defined into the ValueAxis class.
*
*/
@Override
protected double[] getRange()
return new double[]
getLowerBound(),
getUpperBound()
;
/**
* The getTickMarkLabel is only used to convert the number value to a string
* that will be displayed under the tickMark.
*
*/
@Override
protected String getTickMarkLabel(Number value)
NumberFormat formatter = NumberFormat.getInstance();
formatter.setMaximumIntegerDigits(10);
formatter.setMinimumIntegerDigits(1);
return formatter.format(value);
/**
* Updates the range when data are added into the chart.
* There is two possibilities, the axis is animated or not. The
* simplest case is to set the lower and upper bound properties directly
* with the new values.
*
*/
@Override
protected void setRange(Object range, boolean animate)
if (range != null)
final double[] rangeProps = (double[]) range;
final double lowerBound = rangeProps[0];
final double upperBound = rangeProps[1];
final double oldLowerBound = getLowerBound();
setLowerBound(lowerBound);
setUpperBound(upperBound);
if (animate)
animator.stop(currentAnimationID);
currentAnimationID = animator.animate(
new KeyFrame(Duration.ZERO,
new KeyValue(currentLowerBound, oldLowerBound)
),
new KeyFrame(Duration.millis(700),
new KeyValue(currentLowerBound, lowerBound)
)
);
else
currentLowerBound.set(lowerBound);
@Override
public Number getValueForDisplay(double displayPosition)
double delta = logUpperBound.get() - logLowerBound.get();
if (getSide().isVertical())
return Math.pow(10, (((displayPosition - getHeight()) / -getHeight()) * delta) + logLowerBound.get());
else
return Math.pow(10, (((displayPosition / getWidth()) * delta) + logLowerBound.get()));
@Override
public double getDisplayPosition(Number value)
double delta = logUpperBound.get() - logLowerBound.get();
double deltaV = Math.log10(value.doubleValue()) - logLowerBound.get();
if (getSide().isVertical())
return (1. - ((deltaV) / delta)) * getHeight();
else
return ((deltaV) / delta) * getWidth();
/**
* Exception to be thrown when a bound value isn't supported by the
* logarithmic axis<br>
*
*/
public static class IllegalLogarithmicRangeException extends RuntimeException
public IllegalLogarithmicRangeException(String message)
super(message);
【问题讨论】:
minimal reproducible example 请.. 【参考方案1】:只查看图表中的一个系列
一种方法是在图表中放置一个系列并删除所有其他系列。
为此,不要试图隐藏其他系列中的节点,而是从数据集中删除其他系列,图表将自动更新。
如果您想要即时更新而不是动画更新,请关闭图表上的动画。
lineChart.setAnimated(false);
我已经尝试过线程
除非你真的需要,否则我不建议使用其他线程。
永远不要从 JavaFX 线程访问与活动场景图关联的数据。这将包括折线图及其数据。
要允许这种情况发生,请运行在 Platform.runlater()
调用中获取 lineChart 数据的逻辑,而不是像在您的问题中那样在您自己的线程中访问 lineChart.getData()
调用。
new Thread(() ->
Platform.runLater(() ->
for (Series<Number, Number> series : lineChart.getData())
if (series.getName().equals(name))
series.getNode().setVisible(!series.getNode().isVisible());
series.getData().forEach(data -> data.getNode().setVisible(series.getNode().isVisible()));
);
).start();
但是,如果你这样做,线程和 runLater 调用似乎毫无意义,因为你可以内联做所有事情:
for (Series<Number, Number> series : lineChart.getData())
if (series.getName().equals(name))
series.getNode().setVisible(!series.getNode().isVisible());
series.getData().forEach(data -> data.getNode().setVisible(series.getNode().isVisible()));
示例代码
我尝试实现一种“仅包含显示系列”的策略。
在实施之后,我认为您最初通过更改可见性来隐藏系列的想法可能会更好。
这比我预期的要复杂一些,因为当您只显示单个系列时,它会按照默认配色方案着色,其中颜色是由数据中系列的顺序位置分配的。因此,当您只显示一个系列而不是多个系列时,所显示系列的颜色实际上会发生变化,除非您覆盖默认配色方案。
您可以在 CSS 样式表中覆盖它,但是您需要更改每个选定系列的样式表,以使系列颜色保持不变,这很麻烦。
可能有更好的方法来处理这个问题。我以为你可以在代码中通过 setStyle 设置 DEFAULT_COLOR_ 查找颜色,但我无法让它工作,所以我只使用了非常丑陋的样式表代码。
我也没有整合对数轴,因为(据我所知)轴类型应该与此无关。
另一个技巧是动画必须关闭才能工作,否则,动画数据的观察者将在重置系列数据时被触发。它会认为添加了重复的数据系列(实际上并没有,所以这是图表动画逻辑的一个奇怪的实现怪癖)。
无论如何,我提供了我想出的代码。
如果没有设置切换,则显示所有系列。如果设置了切换,则仅显示与该切换对应的系列。
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.chart.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.util.Random;
public class SeriesSelectionApp extends Application
private static final int NUM_SERIES = 3;
private static final int NUM_DATA_PER_SERIES = 10;
private static final int DATA_MIN_VALUE = 5;
private static final int DATA_MAX_VALUE = 10;
private static final String[] seriesColors = new String[]
"red", "green", "blue"
;
private static final Random random = new Random(42);
@Override
public void start(Stage stage)
ObservableList<XYChart.Series<Number, Number>> data = generateData();
LineChart<Number, Number> lineChart = new LineChart<>(
new NumberAxis(0, NUM_DATA_PER_SERIES, 1),
new NumberAxis(0, DATA_MAX_VALUE, 1)
);
lineChart.getData().setAll(data);
lineChart.setAnimated(false);
setDefaultChartSeriesColors(lineChart);
HBox controls = new HBox(10);
ToggleGroup seriesSelectionToggleGroup = new ToggleGroup();
for (int i = 0; i < NUM_SERIES; i++)
ToggleButton showSeriesToggleButton = new ToggleButton("Series " + (i+1));
showSeriesToggleButton.setToggleGroup(seriesSelectionToggleGroup);
showSeriesToggleButton.setUserData(i+1);
controls.getChildren().add(showSeriesToggleButton);
seriesSelectionToggleGroup.selectedToggleProperty().addListener((observable, oldValue, selectedToggle) ->
lineChart.getData().clear();
if (selectedToggle == null)
lineChart.getData().addAll(data);
setDefaultChartSeriesColors(lineChart);
else
int selectedSeriesNum = (int) selectedToggle.getUserData();
lineChart.getData().add(data.get(selectedSeriesNum - 1));
setSpecificChartSeriesColor(lineChart, selectedSeriesNum);
);
VBox layout = new VBox(10, controls, lineChart);
layout.setPadding(new Insets(10));
Scene scene = new Scene(layout);
stage.setScene(scene);
stage.show();
private void setDefaultChartSeriesColors(LineChart<Number, Number> lineChart)
lineChart.getStylesheets().setAll(
"""
data:text/css,
.default-color0.chart-line-symbol -fx-background-color: red, white;
.default-color1.chart-line-symbol -fx-background-color: green, white;
.default-color2.chart-line-symbol -fx-background-color: blue, white;
.default-color0.chart-series-line -fx-stroke: red;
.default-color1.chart-series-line -fx-stroke: green;
.default-color2.chart-series-line -fx-stroke: blue;
"""
);
private void setSpecificChartSeriesColor(LineChart<Number, Number> lineChart, int seriesNum)
lineChart.getStylesheets().setAll(
"""
data:text/css,
.default-color0.chart-line-symbol -fx-background-color: MY_COLOR, white;
.default-color0.chart-series-line -fx-stroke: MY_COLOR;
""".replaceAll("MY_COLOR", getSeriesColor(seriesNum))
);
private String getSeriesColor(int seriesNum)
return seriesColors[(seriesNum - 1) % seriesColors.length];
private ObservableList<XYChart.Series<Number, Number>> generateData()
ObservableList<XYChart.Series<Number, Number>> allData = FXCollections.observableArrayList();
for (int seriesNum = 0; seriesNum < NUM_SERIES; seriesNum++)
ObservableList<XYChart.Data<Number, Number>> seriesData = FXCollections.observableArrayList();
for (int x = 0; x < NUM_DATA_PER_SERIES; x++)
int y = random.nextInt(DATA_MAX_VALUE - DATA_MIN_VALUE) + DATA_MIN_VALUE;
XYChart.Data<Number, Number> dataItem = new XYChart.Data<>(x, y);
seriesData.add(dataItem);
XYChart.Series<Number, Number> series = new XYChart.Series<>("Series " + (seriesNum + 1), seriesData);
allData.add(series);
return allData;
public static void main(String[] args)
launch(args);
【讨论】:
我已经将这两个图表设置为非动画但它不会改变任何东西。在图表中添加和删除数据的解决方案对我来说不是最好的解决方案,因为我必须在两个图表中一次管理大量数据,不仅是一次系列,而且是所有数据一次......无论如何,我非常感谢你的回答和花费的时间! 我必须管理大量数据核心 fx 图表并不真正适用于大型数据集(更新数据、混合数据和视图存在已知问题,不是真正可配置的..),您可能会考虑研究 3rd 方实现 @GRETA 如果您的问题更加具体,全面概述您要解决的问题并提供minimal reproducible example,您将获得更适用的答案并充分利用时间。以上是关于具有对数轴的 JavaFX 折线图没有很好地更新[关闭]的主要内容,如果未能解决你的问题,请参考以下文章
如何使用ObservableList在Javafx中创建折线图的BarChart