JavaFX диаграмма авто-масштабирование неправильно с низкими числами


Я использую JavaFX для построения StackedBarChart. График будет меняться по мере поступления новых данных. Я имитирую это с помощью кнопки, которая обновляет диаграмму.

В основном это работает нормально, но я заметил, что при низких значениях (значения меньше ~100) метки оси Y кажутся немного не такими, когда я впервые обновляю диаграмму:

Диаграмма JavaFX с окей автоматического масштабирования по оси Y

Еще более странно, если я обновлю диаграмму во второй раз (или в третий, в четвертый...), автоматическое масштабирование оси Y находится далеко:

JavaFX StackedBarChart с плохим автоматическим масштабированием по оси Y

Если я использую большие значения (значения > ~1000), то автоматическое масштабирование работает нормально. Если я деактивирую анимацию диаграммы, то автоматическое масштабирование работает нормально. Автоматическое масштабирование прекрасно работает при первом обновлении диаграммы, но не после этого.

Вот код, который я использую, который в значительной степени идентичен коду из этого учебника 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 дополнительных раздела каждого сложенного бара, которые исчезают, когда анимация завершается. Есть ли способ избавиться от этих дополнительных разделов во время анимации?

2 8

2 ответа:

Вы используете диаграмму, анимацию и модификацию наблюдаемого списка таким образом, что кажется, что это не должно быть сделано. Ну, и я тоже поначалу. Я сталкивался с подобными проблемами, в том числе с исключениями ArrayIndexOutOfBounds. Короче говоря: анимация диаграмм JavaFX очень нарушена, или так кажется. Он кажется ненадежным и, следовательно, непригодным для использования, когда вы непосредственно изменяете список.

Я перестал использовать анимацию или модификацию списка и вместо этого установил список на каждое изменение. Любой из этих методов решил проблему.

Модификация примера с помощью sbc.setData () вместо sbc.метод GetData().clear () и sbc.метод GetData().методы addall(). Это работает, то есть анимация все еще включена:

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, чтобы увидеть изменения в барах.

Но настоящая проблема заключается в том, как вы используете диаграмму. Вы не должны изменять список, но вместо этого вы должны изменить данные элементов списка. Тогда анимация графика работает следующим образом: ожидается, что бары будут расти и падать вместе с данными.

Пример с кнопками" создать "(создает новый список диаграмм) и" изменить " (изменяет данные списка, но не создает новый список):

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

Проблема проистекает из двух вещей.

  1. добавление и удаление данных одновременно (до начала любой анимации)
  2. автоматическое масштабирование оси y в соответствии с анимированными данными

Удаляя старые данные sbc.getData().clear() и добавляя новые данные sbc.getData().addAll(series1, series2); одновременно, вы вызываете запуск двух наборов анимаций одновременно.

Я изменил ваш код (в конце поста), нырнув в функцию update в функции clear и update и добавив множество пуговицы для них.

  • ясно
  • обновление
  • Очистить И Обновить
  • Очистить И Обновить Позже
  • Clear (no anim) & Update

Запустите программу два раза подряд, один с маленькими значениями и один с большими значениями.

Если вы нажмете обновить, вы увидите падение данных для обоих. Если вы очистите эти данные, вы увидите анимацию удаления, но обратите внимание на масштаб оси Y. Она одинакова для обоих наборов данных. Он остается тем же самым, даже если вы используете значения в миллионы.

Теперь, если вы делаете Clear & Update, yAxis будет соответствовать большим анимируемым значениям. При использовании малых значений старые данные доминируют, потому что они растут и остаются больше, чем новые данные во время анимации. При больших значениях новые данные доминируют, потому что они больше, чем конечная точка старых данных.

Затем в конце анимации удаления старые данные очищаются, но ось y остается в покое и в случае малых значений выглядит полностью FUBARd.

Лично я считаю, что это ошибка с JavaFX. Это почти похоже на то, как анимация начинается и заканчивается с y на 650. Я говорю это потому, что при добавлении данных с малыми значениями бары, кажется, опускаются на место, но с большими значениями они, кажется, растут на место. Я не могу сказать наверняка, так как я не делал кодовое погружение.

Первое решение, которое я думаю, это просто сказать оси y, чтобы автоматически масштабировать снова после завершения всей анимации. Однако я не думаю, что в настоящее время есть способ получить уведомление, когда диаграмма будет анимирована. Это может быть неправильно, но я не думаю, что это имеет большое значение в любом случае. Бары будут масштабироваться до очень малых размеров, а затем снова станут большими. Конечный результат прекрасен, но странный и задержанный пользовательский опыт.

править: как Роланд прокомментировал ниже, метод в следующем абзаце имеет немного кодового запаха. Я оставляю его здесь, потому что он работает, но не использует его. Либо используйте мое последнее предложение, либо то, как Роланд предоставляет в своем ответ.

Обходной путь состоит в том, чтобы не запускать обе анимации одновременно. Последняя запущенная анимация должна быть связана с добавлением данных. Это можно увидеть в коде для кнопки Очистить и обновить позже. Однако это все еще выглядит немного странно для меня.

Способ, который я визуально предпочитаю, который, как мне кажется, также решает бонусный вопрос; не анимируйте удаление данных. Если вы выключите его непосредственно перед этим и повторно включите его после этого, вы получите хорошие новые данные, анимирующие, и старые данные просто исчезнут. гомосексуал. Видно с помощью кнопки Clear (no anim) & Update.

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