Michael
Michael

Reputation: 23

Displaying gaps on charts in JavaFX

I'm using XYChart in JavaFX 8 and I would like to display gaps for empty cells for the specified series. When I passed null value then I got NullPointerException:

series.get(index).getData().add(new XYChart.Data<>(Key, null));

I also found the bug https://bugs.openjdk.java.net/browse/JDK-8092134 describing this problem, but I don't know is it still actual.

Does anyone know how to resolve this problem?

Best regards,

Michael

Upvotes: 1

Views: 344

Answers (1)

Sai Dandem
Sai Dandem

Reputation: 10009

As it is quite evident that, this feature is not included.. and if you are very desperate to get this behavior, you can try the below logic.

Having said that, there can be many better ways, but this answer is to give you some initial idea about how you can tweak the current implementation using the protected methods of the chart.

The idea is.. once the plot children layout is done, we recompute the logic of rendering the line path.. and remove the unwanted data points. And as mentioned, this is just for idea purpose only, if you have more data series, then you may need to work accordingly.

[UPDATE] : If you want the paths/data points for each series, append the ".series" to the ".chart-series-line" and ".data" style classes.

Please check the below demo:

enter image description here

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.*;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.layout.VBox;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
import javafx.stage.Stage;

import java.util.*;

public class XYChartDemo extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        VBox root = new VBox();
        CategoryAxis xAxis = new CategoryAxis();
        xAxis.setLabel("days");
        NumberAxis yAxis = new NumberAxis();
        yAxis.setLabel("USD");

        // AS OF NOW THIS IS THE CORE POINT I AM RELYING ON !! YOU CAN THINK OF A BETTER APPROACH TO IDENTIFY THE GAP POINTS.
        List<Integer> s0GapIndexes = Arrays.asList(3, 7);
        List<Integer> s1GapIndexes = Arrays.asList(4, 8);

        Map<Integer, List<Integer>> seriesGap = new HashMap<>();
        seriesGap.put(0, s0GapIndexes);
        seriesGap.put(1, s1GapIndexes);
        XYChart.Series<String, Double> series0 = new XYChart.Series<>();
        series0.getData().add(new Data<>("2000-01-01", 13.2));
        series0.getData().add(new Data<>("2000-01-02", 10.1));
        series0.getData().add(new Data<>("2000-01-03", 14.1));
        series0.getData().add(new Data<>("2000-01-04", 0.0)); // gap (INDEX 3)
        series0.getData().add(new Data<>("2000-01-05", 6.3));
        series0.getData().add(new Data<>("2000-01-06", 9.82));
        series0.getData().add(new Data<>("2000-01-07", 12.82));
        series0.getData().add(new Data<>("2000-01-08", 0.0)); // gap (INDEX 7)
        series0.getData().add(new Data<>("2000-01-09", 4.82));
        series0.getData().add(new Data<>("2000-01-10", 8.82));
        series0.getData().add(new Data<>("2000-01-11", 8.82));

        XYChart.Series<String, Double> series1 = new XYChart.Series<>();
        series1.getData().add(new Data<>("2000-01-01", 20.2));
        series1.getData().add(new Data<>("2000-01-02", 14.1));
        series1.getData().add(new Data<>("2000-01-03", 7.1));
        series1.getData().add(new Data<>("2000-01-04", 9.0));
        series1.getData().add(new Data<>("2000-01-05", 0.0)); // gap (INDEX 4)
        series1.getData().add(new Data<>("2000-01-06", 5.32));
        series1.getData().add(new Data<>("2000-01-07", 11.0));
        series1.getData().add(new Data<>("2000-01-08", 15.3));
        series1.getData().add(new Data<>("2000-01-09", 0.0)); // gap (INDEX 8)
        series1.getData().add(new Data<>("2000-01-10", 4.82));
        series1.getData().add(new Data<>("2000-01-11", 6.82));

        CustomLineChart lineChart = new CustomLineChart(xAxis, yAxis, seriesGap);
        lineChart.getData().addAll(series0, series1);
        root.getChildren().addAll(lineChart);
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    class CustomLineChart<X, Y> extends LineChart<X, Y> {

        Map<Integer, List<Integer>> seriesGap;

        public CustomLineChart(Axis<X> xAxis, Axis<Y> yAxis, Map<Integer, List<Integer>> seriesGap) {
            super(xAxis, yAxis);
            this.seriesGap = seriesGap;
        }

        @Override
        protected void layoutPlotChildren() {
            super.layoutPlotChildren();
            updatePath();
            updateDataPoints();
        }

        private void updatePath() {
            seriesGap.forEach((seriesNo, gapIndexes) -> {
                Path path = (Path) lookup(".chart-series-line.series" + seriesNo);
                System.out.println(path);
                if (!path.getElements().isEmpty()) {
                    int dataSize = getData().get(seriesNo).getData().size();
                    int pathEleSize = path.getElements().size();
                    // Just ensuring we are dealing with right path
                    if (pathEleSize == dataSize + 1) {
                        // Build a new path, by jumping the gap points
                        List<PathElement> newPath = new ArrayList<>();
                        newPath.add(path.getElements().get(0));
                        for (int i = 1; i < path.getElements().size(); i++) {
                            if (gapIndexes.contains(i - 1)) {
                                LineTo lt = (LineTo) path.getElements().get(i + 1);
                                newPath.add(new MoveTo(lt.getX(), lt.getY()));
                            } else {
                                newPath.add(path.getElements().get(i));
                            }
                        }

                        // Update the new path to the current path.
                        path.getElements().clear();
                        path.getElements().addAll(newPath);
                    }
                }
            });

        }

        private void updateDataPoints() {
            Group plotContent = (Group) lookup(".plot-content");
            seriesGap.forEach((seriesNo, gapIndexes) -> {
                // Remove all data points at the  gap indexes
                gapIndexes.forEach(i -> {
                    Node n = lookup(".series" + seriesNo + ".data" + i);
                    if (n != null) {
                        plotContent.getChildren().remove(n);
                    }
                });
            });
        }
    }
}

Upvotes: 3

Related Questions