具有对数轴的 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 折线图没有很好地更新[关闭]的主要内容,如果未能解决你的问题,请参考以下文章

java JavaFX折线图实时监控

如何使用ObservableList在Javafx中创建折线图的BarChart

如何在具有不同 Y 轴的同一个 seaborn 图中很好地制作条形图和线图?

图表js:更新具有两个数据集的折线图

chart.js 折线图不显示超过图表中某个点的线

Dash Plotly中的烛台图和折线图,它会随着回调而更新,但会消失