Scrolling XYChart with JavaFX


Question

I'm trying to implement a network usage graph similar to the one in Windows. I'm showing the last 30 seconds worth of data with new values appearing at the right hand side and moving to the left.

I've got it all working apart from I cannot get the vertical grid lines to move with the data. Instead the major ticks always stay static and the just the line on the graph moves.

i.e. in the image below I'm expecting the major ticks to stay at the tickUnit of 5 and be shown at 55 and 60. The label at 65 would be off the screen.

example screenshot

  • I've disabled autoRanging and I'm manually updating lowerBound and upperBound properties on the axis.
  • With autoRanging enabled you get a snake style effect where the line moves towards the yAxis for a few seconds and then jumps by a major tick distance, and so on.
  • I've also tried enabling the animation property on both the chart and the axis.

I'm after any solution / workaround that builds upon what's already there rather than having to write from scratch. Is it possible to customize the behaviour by subclassing?

import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class StackOverflow25383566 extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        final NumberAxis xAxis = new NumberAxis();
        xAxis.setAutoRanging(false);
        xAxis.setForceZeroInRange(false);
        xAxis.setTickUnit(5);

        final NumberAxis yAxis = new NumberAxis(0, 15, 1);
        yAxis.setAutoRanging(false);

        final LineChart<Number, Number> chart = new LineChart<Number, Number>(
                xAxis, yAxis);
        chart.setAnimated(false);
        chart.setCreateSymbols(false);
        chart.setLegendVisible(false);
        final XYChart.Series<Number, Number> series = new XYChart.Series<Number, Number>();
        chart.getData().add(series);
        series.getData().addListener(
                new ListChangeListener<Data<Number, Number>>() {

                    @Override
                    public void onChanged(
                            ListChangeListener.Change<? extends Data<Number, Number>> arg0) {
                        ObservableList<Data<Number, Number>> data = series
                                .getData();
                        xAxis.setLowerBound(data.get(0).getXValue()
                                .doubleValue());
                        xAxis.setUpperBound(data.get(data.size() - 1)
                                .getXValue().doubleValue());
                    }

                });
        stage.setScene(new Scene(new BorderPane(chart), 800, 600));
        stage.show();

        final Runnable update = new Runnable() {
            private int clock;

            @Override
            public void run() {
                try {
                    ObservableList<Data<Number, Number>> data = series
                            .getData();
                    if (data.size() > 10) {
                        data.remove(0);
                    }
                    data.add(new XYChart.Data<Number, Number>(clock, clock % 13));
                    clock++;
                } catch (Throwable e) {
                    // executors silently swallow exceptions
                    e.printStackTrace();
                    throw e;
                }
            }

        };
        Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(
                new Runnable() {

                    @Override
                    public void run() {
                        Platform.runLater(update);
                    }

                }, 0, 500, TimeUnit.MILLISECONDS);
    }

    public static void main(String[] args) {
        launch(args);
    }
}
1
2
8/20/2014 5:54:38 AM

Accepted Answer

I was able to solve this by subclassing ValueAxis so that it returns tick markers that are always multiples of tickUnit.

  • Since developing this solution, I've been made aware of jfxutils which contains a class called StableTickAxis, however its JavaDoc states "Not ready to be used"

This picture shows what I was after, i.e. not necessarily starting with a major tick.

Graph showing fixed behaviour

private class SpecialAxis extends ValueAxis<Number> {
    private SimpleObjectProperty<Double> tickUnitProperty = new SimpleObjectProperty<Double>(
            5d);

    @Override
    protected List<Number> calculateMinorTickMarks() {
        List<Number> ticks = new ArrayList<Number>();
        double tickUnit = tickUnitProperty.get() / getMinorTickCount();
        double start = Math.floor(getLowerBound() / tickUnit) * tickUnit;
        for (double value = start; value < getUpperBound(); value += tickUnit) {
            ticks.add(value);
        }
        return ticks;
    }

    @Override
    protected List<Number> calculateTickValues(double arg0, Object arg1) {
        List<Number> ticks = new ArrayList<Number>();
        double tickUnit = tickUnitProperty.get();
        double start = Math.floor(getLowerBound() / tickUnit) * tickUnit;
        for (double value = start; value < getUpperBound(); value += tickUnit) {
            ticks.add(value);
        }
        return ticks;
    }

    @Override
    protected Object getRange() {
        // not sure how this is used??
        return null;
    }

    @Override
    protected String getTickMarkLabel(Number label) {
        return label.toString();
    }

    @Override
    protected void setRange(Object range, boolean arg1) {
        // not sure how this is used??
    }

    public SimpleObjectProperty<Double> getTickUnitProperty() {
        return tickUnitProperty;
    }

    public void setTickUnit(double tickUnit) {
        tickUnitProperty.set(tickUnit);
    }

}
2
8/20/2014 6:22:48 AM

To get a moving chart where the grid moves too, you can try using a LineChart<Number,Number>. For the x axis, use a NumberAxis, and set a proper tick label formatter. As you will be removing data, just set the forceZeroInRange option in the x axis to false, as the older time values shold not be plotted. This must be done with autoRanging on.

With something like this code snippet:

NumberAxis xAxis = new NumberAxis();
xAxis.setAutoRanging(true);
xAxis.setForceZeroInRange(false);
xAxis.setTickLabelFormatter(new StringConverter<Number>(){
    @Override
    public String toString(Number t) {
        return new SimpleDateFormat("HH:mm:ss").format(new Date(t.longValue()));
    }
    @Override
    public Number fromString(String string) {
        throw new UnsupportedOperationException("Not supported yet.");
    }
});

NumberAxis yAxis = new NumberAxis();
LineChart<Number,Number> chart = new LineChart<>(xAxis,yAxis);

you'll see that the vertical major ticks move from right to left, whenever the first major ticket in the left disappear.


Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon