JavaFX 图表自动缩放错误的低数字

JavaFX Chart Auto-Scaling Wrong with Low Numbers

我正在使用 JavaFX 构建 StackedBarChart。图表将随着新数据的进入而改变。我正在用一个更新图表的按钮来模拟它。

它大部分工作正常,但我注意到当我第一次更新图表时,对于低值(值小于 ~100),Y 轴标签似乎有点偏离:

更奇怪的是,如果我第二次(或第三次、第四次...)更新图表,Y 轴的自动缩放功能会关闭:

如果我使用更大的值(值 > ~1000),那么自动缩放工作正常。如果我停用图表动画,那么自动缩放就可以正常工作。自动缩放在我第一次更新图表时工作正常,但之后就不行了。

这是我正在使用的代码,它与 this JavaFX 教程中的代码几乎相同。

import java.util.Arrays;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.StackedBarChart;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class StackedBarChartSample extends Application {

    final CategoryAxis xAxis = new CategoryAxis();
    final NumberAxis yAxis = new NumberAxis();
    final StackedBarChart<String, Number> sbc = new StackedBarChart<String, Number>(xAxis, yAxis);

    @Override
    public void start(Stage stage) {

        stage.setTitle("Bar Chart Sample");

        sbc.setAnimated(true); //change this to false and autoscaling works!


        Button button = new Button("update");
        button.setOnAction(new EventHandler<ActionEvent>(){
            @Override
            public void handle(ActionEvent arg0) {
                updateChart();
            }

        });

        VBox contentPane = new VBox();
        contentPane.getChildren().addAll(sbc, button);

        Scene scene = new Scene(contentPane, 800, 600);

        stage.setScene(scene);
        stage.show();
    }


    private void updateChart(){

        int m = 1; //change this to 100 and autoscaling works!


        xAxis.setCategories(FXCollections.<String>observableArrayList());
        sbc.getData().clear();

        final XYChart.Series<String, Number> series1 = new XYChart.Series<String, Number>();
        final XYChart.Series<String, Number> series2 = new XYChart.Series<String, Number>();

        series1.setName("ABC");
        series1.getData().add(new XYChart.Data<String, Number>("one", 25*m));
        series1.getData().add(new XYChart.Data<String, Number>("two", 20*m));
        series1.getData().add(new XYChart.Data<String, Number>("three", 10*m));

        series2.setName("XYZ");
        series2.getData().add(new XYChart.Data<String, Number>("one", 25*m));
        series2.getData().add(new XYChart.Data<String, Number>("two", 20*m));
        series2.getData().add(new XYChart.Data<String, Number>("three", 10*m));


        xAxis.setCategories(FXCollections.<String>observableArrayList(Arrays.asList("one", "two", "three")));
        sbc.getData().addAll(series1, series2);
    }

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

奖金问题:动画,即使在运行时,也会显示每个堆叠条的 2 个额外部分,这些部分在动画完成时消失。有没有办法在动画过程中去掉那些多余的部分?

问题源于两件事。

  1. 在 'same time' 添加和删除数据(在任何动画开始之前)
  2. y 轴自动缩放以适合动画数据

通过同时删除旧数据 sbc.getData().clear() 和添加新数据 sbc.getData().addAll(series1, series2); 可以使两组动画同时启动。

我修改了您的代码(在 post 末尾),将更新功能分为清除功能和更新功能,并为它们添加了各种按钮。

  • 清除
  • 更新
  • 清除并更新
  • 清除并稍后更新
  • 清除(无动画)和更新

运行程序并排两次,一个值小,一个值大。

如果您点击更新,您会看到两者的数据下降。 如果您清除该数据,您会看到删除动画,但会注意到 y 轴刻度。两个数据集都是一样的。即使您使用数百万的值,它也保持不变。

现在,如果您正在执行清除和更新,则 yAxis 将适合较大的动画值。 使用小值旧数据占主导地位,因为它在动画期间增长并保持大于新数据。 对于大值,新数据占主导地位,因为它大于旧数据的最终终点。

然后在删除动画结束时,旧数据被清除,但 y 轴保持不变,在小值的情况下看起来完全 FUBARd。

我个人认为这是 JavaFX 的一个错误。几乎感觉动画以 y 在 650 处开始和停止。我这样说是因为当添加具有小值的数据时,条形图似乎下降到位但具有大值时它们似乎长大到位。我不能肯定地说,因为我没有做过代码潜水。

我想到的第一个解决方案是在所有动画完成后让y轴再次自动缩放。但是,我认为目前没有一种方法可以在图表完成动画时得到通知。这可能是错误的,但我认为这并不重要。条形图会缩放到非常小的尺寸,然后再次变大。最终结果很好,但用户体验奇怪且延迟。

编辑:正如罗兰在下面评论的那样,下一段中的方法有点代码味。我把它留在这里是因为它有效但不要使用它。要么使用我最后的建议,要么使用 Roland 在他的回答中提供的方式。

解决方法是不要同时启动两个动画。最后一个运行动画需要是添加数据的动画。这可以在 Clear & Update Later 按钮的代码中看到。 然而,这对我来说仍然看起来有点奇怪。

我认为我在视觉上更喜欢的方式也解决了奖金问题;不要动画数据删除。如果您在之前将其关闭并在之后重新启用它,您将获得很好的新数据动画,而旧数据就会消失。 使用清除(无动画)和更新按钮查看。

import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.StackedBarChart;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class StackedBarChartSample extends Application
{

    final CategoryAxis xAxis = new CategoryAxis();
    final NumberAxis yAxis = new NumberAxis();
    final StackedBarChart<String, Number> sbc = new StackedBarChart<String, Number>(xAxis, yAxis);

    @Override
    public void start(final Stage stage)
    {

        stage.setTitle("Bar Chart Sample");

        sbc.setAnimated(true);

        final Button updateButton = new Button("update");
        updateButton.setOnAction(new EventHandler<ActionEvent>()
        {
            @Override
            public void handle(final ActionEvent arg0)
            {
                updateChart();
            }

        });

        final Button clearButton = new Button("Clear");
        clearButton.setOnAction(new EventHandler<ActionEvent>()
        {
            @Override
            public void handle(final ActionEvent arg0)
            {
                clearChart(true);
            }

        });

        final Button clearAndUpdateButton = new Button("Clear & Update");
        clearAndUpdateButton.setOnAction(new EventHandler<ActionEvent>()
        {
            @Override
            public void handle(final ActionEvent evt)
            {
                clearChart(true);
                updateChart();
            }

        });

        final Button clearAndUpdateLaterButton = new Button("Clear & Update Later");
        clearAndUpdateLaterButton.setOnAction(new EventHandler<ActionEvent>()
        {
            @Override
            public void handle(final ActionEvent evt)
            {
                clearChart(true);
                new Timer(true).schedule(new TimerTask()
                {
                    @Override
                    public void run()
                    {
                        Platform.runLater(() -> updateChart());
                    }
                }, 1000);
            }

        });

        final Button clearNoAnimAndUpdateButton = new Button("Clear (no anim) & Update");
        clearNoAnimAndUpdateButton.setOnAction(new EventHandler<ActionEvent>()
        {
            @Override
            public void handle(final ActionEvent evt)
            {
                clearChart(false);
                updateChart();
            }

        });

        final VBox contentPane = new VBox();
        contentPane.getChildren().addAll(sbc, clearButton, updateButton, clearAndUpdateButton, clearAndUpdateLaterButton,
                clearNoAnimAndUpdateButton);

        final Scene scene = new Scene(contentPane, 800, 600);

        stage.setScene(scene);
        stage.show();

        xAxis.setCategories(FXCollections.<String> observableArrayList(Arrays.asList("one", "two", "three")));
    }

    private void clearChart(final boolean animate)
    {
        System.out.println("Clear");
        final boolean wasAnimated = sbc.getAnimated();
        if (!animate) {
            sbc.setAnimated(false);
        }

        sbc.getData().clear();

        if (!animate) {
            sbc.setAnimated(wasAnimated);
        }
    }

    private void updateChart()
    {
        System.out.println("Update");
        final int m = 1; // change this to 100 and autoscaling works!

        final XYChart.Series<String, Number> series1 = new XYChart.Series<String, Number>();
        final XYChart.Series<String, Number> series2 = new XYChart.Series<String, Number>();

        series1.setName("ABC");
        series1.getData().add(new XYChart.Data<String, Number>("one", 25 * m));
        series1.getData().add(new XYChart.Data<String, Number>("two", 20 * m));
        series1.getData().add(new XYChart.Data<String, Number>("three", 10 * m));

        series2.setName("XYZ");
        series2.getData().add(new XYChart.Data<String, Number>("one", 25 * m));
        series2.getData().add(new XYChart.Data<String, Number>("two", 20 * m));
        series2.getData().add(new XYChart.Data<String, Number>("three", 10 * m));

        sbc.getData().addAll(series1, series2);
    }

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

您正在使用图表、动画和可观察列表的修改方式,这似乎并不意味着要完成。好吧,我最初也是。我 运行 遇到过这样的问题,其中包括 ArrayIndexOutOfBounds 异常。简而言之:JavaFX 图表的动画非常破损,或者看起来如此。直接修改列表似乎不可靠,因此无法使用。

我不再使用列表的动画或修改,而是在每次更改时设置列表。这两种方法中的任何一种都解决了问题。

通过使用 sbc.setData() 而不是 sbc.getData().clear() 和 sbc.getData().addAll() 来修改示例。这行得通,我。 e.动画还在:

public class StackedBarChartSample extends Application {

    final CategoryAxis xAxis = new CategoryAxis();
    final NumberAxis yAxis = new NumberAxis();
    final StackedBarChart<String, Number> sbc = new StackedBarChart<String, Number>(xAxis, yAxis);

    Random rnd = new Random();

    @Override
    public void start(Stage stage) {

        stage.setTitle("Bar Chart Sample");

        sbc.setAnimated(true); //change this to false and autoscaling works!

        Button button = new Button("update");
        button.setOnAction(new EventHandler<ActionEvent>(){
            @Override
            public void handle(ActionEvent arg0) {
                updateChart();
            }

        });

        VBox contentPane = new VBox();
        contentPane.getChildren().addAll(sbc, button);

        Scene scene = new Scene(contentPane, 800, 600);

        stage.setScene(scene);
        stage.show();
    }


    private void updateChart(){

        int m = 1; //change this to 100 and autoscaling works!

        m = rnd.nextInt(100) + 1; // just some random value to see changes in the chart

        System.out.println( "m = " + m);

        final XYChart.Series<String, Number> series1 = new XYChart.Series<String, Number>();
        final XYChart.Series<String, Number> series2 = new XYChart.Series<String, Number>();

        series1.setName("ABC");
        series1.getData().add(new XYChart.Data<String, Number>("one", 25*m));
        series1.getData().add(new XYChart.Data<String, Number>("two", 20*m));
        series1.getData().add(new XYChart.Data<String, Number>("three", 10*m));

        series2.setName("XYZ");
        series2.getData().add(new XYChart.Data<String, Number>("one", 25*m));
        series2.getData().add(new XYChart.Data<String, Number>("two", 20*m));
        series2.getData().add(new XYChart.Data<String, Number>("three", 10*m));

        sbc.setData( FXCollections.observableArrayList(series1, series2));
    }

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

我为您的 m 添加了随机性,以便查看条形中的变化。

但真正的问题是您使用图表的方式。您不应该修改列表,而应该修改列表项的数据。然后图表动画按预期工作,条形随数据上升和下降。

带有 "create"(创建新图表列表)和 "modify"(修改列表数据,但不创建新列表)按钮的示例:

public class StackedBarChartSample extends Application {

    final CategoryAxis xAxis = new CategoryAxis();
    final NumberAxis yAxis = new NumberAxis();
    final StackedBarChart<String, Number> sbc = new StackedBarChart<String, Number>(xAxis, yAxis);

    Random rnd = new Random();

    final XYChart.Series<String, Number> series1 = new XYChart.Series<String, Number>();
    final XYChart.Series<String, Number> series2 = new XYChart.Series<String, Number>();

    @Override
    public void start(Stage stage) {

        stage.setTitle("Bar Chart Sample");

        sbc.setAnimated(true); //change this to false and autoscaling works!

        Button createButton = new Button("Create");
        createButton.setOnAction(new EventHandler<ActionEvent>(){
            @Override
            public void handle(ActionEvent arg0) {
                createChartData();
            }

        });

        Button modifyButton = new Button("Modify");
        modifyButton.setOnAction(new EventHandler<ActionEvent>(){
            @Override
            public void handle(ActionEvent arg0) {
                modifyChartData();
            }

        });

        VBox contentPane = new VBox();
        contentPane.getChildren().addAll(sbc, createButton, modifyButton);

        Scene scene = new Scene(contentPane, 800, 600);

        stage.setScene(scene);
        stage.show();
    }


    private void createChartData(){

        int m = 1; //change this to 100 and autoscaling works!

        m = rnd.nextInt(100) + 1; // just some random value to see changes in the chart

        System.out.println( "m = " + m);

        series1.setName("ABC");
        series1.getData().add(new XYChart.Data<String, Number>("one", 25*m));
        series1.getData().add(new XYChart.Data<String, Number>("two", 20*m));
        series1.getData().add(new XYChart.Data<String, Number>("three", 10*m));

        series2.setName("XYZ");
        series2.getData().add(new XYChart.Data<String, Number>("one", 25*m));
        series2.getData().add(new XYChart.Data<String, Number>("two", 20*m));
        series2.getData().add(new XYChart.Data<String, Number>("three", 10*m));

        sbc.setData( FXCollections.observableArrayList(series1, series2));
    }

    private void modifyChartData() {

        int m = 1; //change this to 100 and autoscaling works!

        m = rnd.nextInt(100) + 1; // just some random value to see changes in the chart

        System.out.println( "m = " + m);

        for( XYChart.Data<String,Number> data: series1.getData()) {
            data.setYValue(rnd.nextInt(30) * m);
        }

        for( XYChart.Data<String,Number> data: series2.getData()) {
            data.setYValue(rnd.nextInt(30) * m);
        }


    }

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