Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFX Chart Auto-Scaling Wrong with Low Numbers

Tags:

java

javafx

I'm using JavaFX to build a StackedBarChart. The chart will change as new data comes in. I'm simulating this with a button that updates the chart.

It mostly works okay, but I noticed that with low values (values less than ~100), the Y-Axis labels seem a little off when I first update the chart:

JavaFX chart with okay auto-scaling on Y Axis

Weirder still, if I update the chart a second time (or third, fourth...), the auto-scaling of the Y-Axis is way off:

JavaFX StackedBarChart with bad auto-scaling on Y Axis

If I use larger values (values > ~1000), then the auto-scaling works fine. If I deactivate the chart animation, then the auto-scaling works fine. The auto-scaling works fine the first time I update the chart, but not after that.

Here's the code I'm using, which is pretty much identical to the code from this JavaFX tutorial.

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

Bonus question: the animation, even when it works, shows 2 extra sections of each stacked bar, which disappear when the animation completes. Is there a way to get rid of those extra sections during the animation?

like image 968
Kevin Workman Avatar asked Mar 18 '15 14:03

Kevin Workman


2 Answers

You are using the chart, the animation and the modification of the observable list in a way it seems it's not meant to be done. Well, so did I initially. I've run into problems like this, among them ArrayIndexOutOfBounds Exceptions. In short: the animation of the JavaFX charts is very broken, or so it appears. It seems to be unreliable and hence unusable when you directly modify the list.

I stopped using the animation or modification of the list and instead set the list on every change. Either of these methods solved the problem.

Modification of the example by using sbc.setData() instead of sbc.getData().clear() and sbc.getData().addAll(). This works, i. e. the animation is still on:

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

I added randomness for your m in order to see changes in the bars.

But the real problem is the way you use the chart. You shouldn't modify the list, but instead you should modify the data of the list items. Then the chart animation works as expected, the bars rise and fall with the data.

Example with a "create" (creates new chart list) and "modify" (modifies the list's data, but doesn't create a new list) button:

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);
    }
}
like image 80
Roland Avatar answered Nov 13 '22 18:11

Roland


The problem stems from two things.

  1. Adding and removing data at the 'same time' (before the start of any animations)
  2. The y axis auto scaling to fit the animated data

By removing the old data sbc.getData().clear() and adding new data sbc.getData().addAll(series1, series2); at the same time you cause two sets of animations to start at the same time.

I have modified your code (at end of post) by diving up the update function into clear and update functions and added a variety of buttons for them.

  • Clear
  • Update
  • Clear & Update
  • Clear & Update Later
  • Clear (no anim) & Update

Run the program twice side by side, one with small values and one with large values.

If you hit update you see the data drop in for both. If you clear that data you see the remove animation but notice the y axis scale. It is the same for both datasets. It remains the same even if you use values in the millions.

Now if you are doing Clear & Update the yAxis will fit the larger values being animated. Using small values the old data dominates because it grows and stays larger than the new data during its animation. With large values the new data dominates because it is larger than the old datas final ending point.

Then at the end of the remove animation the old data is cleared but the y axis is left alone and in the case of small values looking completely FUBARd.

I personally feel this is a bug with JavaFX. It almost feels like the animations start and stop with y at 650. I say this because when adding data with small values the bars appear to drop down into place but with large values they seem to grow up into place. I can't say for sure since I have not done a code dive.

The first solution I think of is to just tell the y axis to auto scale again after all the animation completes. However I don't think there currently is a way to get notified when the chart is done animating. This may be wrong but I don't think it matters much anyway. The bars would scale to very small sizes then get large again. The end result is fine but a strange and delayed user experience.

Edit: As Roland commented below the method in the next paragraph has a bit of a code smell. I am leaving it here because it works but Don't use it. Either use my last suggestion or the way Roland provides in his answer.

A work around is to not start both animations at the same time. The last running animation needs to be the ones from adding data. This can be seen in the code for the Clear & Update Later button. However this still looks a bit weird to me.

The way I visually prefer which I think also solves the bonus question; Don't animate data removal. If you turn it off just before and re-enable it afterward you get nice new data animating in and the old data just goes poof. Seen with the Clear (no anim) & Update button.

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);
    }
}
like image 2
Appak Avatar answered Nov 13 '22 16:11

Appak