Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use JavaFX chart API to draw chart image

I only want to generate a chart image from JavaFX chart API. I don't want to show the app Window, or launch the application(if it's not necessary).

public class LineChartSample extends Application {
    private List<Integer> data;

    @Override public void start(Stage stage) {
        stage.setTitle("Line Chart Sample");
        final CategoryAxis xAxis = new CategoryAxis();
        final NumberAxis yAxis = new NumberAxis();
        xAxis.setLabel("Month");       

        final LineChart<String,Number> lineChart = 
                new LineChart<String,Number>(xAxis,yAxis);

        lineChart.setTitle("Stock Monitoring, 2010");

        XYChart.Series series = new XYChart.Series();
        series.setName("My portfolio");

        series.getData().add(new XYChart.Data("Jan", 23));
        series.getData().add(new XYChart.Data("Feb", 14));        

        Scene scene  = new Scene(lineChart,800,600);
        lineChart.getData().add(series);

        WritableImage image = scene.snapshot(null);
        ImageIO.write(SwingFXUtils.fromFXImage(image, null), "png", chartFile);

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

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

   public setData(List<Integer> data) {this.data = data;}
}

Inside the start method, I actually need to access outside data in order to build up the series data, but there seems no way to access outside data from start method, if I store the data inside the member variable data, it's null when the start is called. I actually don't care about stage and scene object, as long as the chart image can be rendered, how should I solve the problem? I want to build a API that can be called with input data and draw the chart with the data, and returns the file.

public File toLineChart(List<Integer> data) {
...
}
like image 530
Sawyer Avatar asked Sep 28 '16 06:09

Sawyer


1 Answers

You don't need to show a Stage but the Node must be attached to a Scene. From the doc of snapshot:

NOTE: In order for CSS and layout to function correctly, the node must be part of a Scene (the Scene may be attached to a Stage, but need not be).

One restriction to modify a Scene is, that it must happen on the JavaFX Application Thread, which has the pre-requisite that the JavaFX Toolkit must be initialized.

The initialization can be done by extending the Application class where the launch method will do it for you, or as a workaround you can create a new JFXPanel instance on the Swing Event Dispatcher Thread.

If you are extending Application and you execute some code in the start method, it is ensured that this code will be executed on the JavaFX Application Thread, otherwise you can use the Platform.runLater(...) block called from a different thread to ensure the same.

Here is a possible example:

The class provides one static method to plot a chart into a file and returns the File or null if the creation was successful or not.

In this method the JavaFX Toolkit is initialized by creating a JFXPanel on the Swing EDT then the chart creation is done JavaFX Application Thread. Two booleans are used in the method to store that the operation is completed and successful.

The method will not return until the completed flag switches to true.

Note: This one is really just a (working) example which could be improved a lot.

public class JavaFXPlotter {

    public static File toLineChart(String title, String seriesName, List<Integer> times, List<Integer> data) {

        File chartFile = new File("D:\\charttest.png");

        // results: {completed, successful}
        Boolean[] results = new Boolean[] { false, false };

        SwingUtilities.invokeLater(() -> {

            // Initialize FX Toolkit
            new JFXPanel();

            Platform.runLater(() -> {
                final NumberAxis xAxis = new NumberAxis();
                final NumberAxis yAxis = new NumberAxis();

                final LineChart<Number, Number> lineChart = new LineChart<Number, Number>(xAxis, yAxis);

                lineChart.setTitle(title);

                XYChart.Series<Number, Number> series = new XYChart.Series<>();
                series.setName(seriesName);

                for (int i = 0; i < times.size(); i++)
                    series.getData().add(new XYChart.Data<Number, Number>(times.get(i), data.get(i)));

                lineChart.getData().add(series);

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

                WritableImage image = scene.snapshot(null);

                try {
                    ImageIO.write(SwingFXUtils.fromFXImage(image, null), "png", chartFile);
                    results[1] = true;
                } catch (Exception e) {
                    results[0] = true;
                } finally {
                    results[0] = true;
                }
            });
        });

        while (!results[0]) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return (results[1]) ? chartFile : null;
    }


}

and a possible usage

List<Integer> times = Arrays.asList(new Integer[] { 0, 1, 2, 3, 4, 5 });
List<Integer> data = Arrays.asList(new Integer[] { 4, 1, 5, 3, 0, 7 });

File lineChart = JavaFXPlotter.toLineChart("Sample", "Some sample data", times, data);

if (lineChart != null)
    System.out.println("Image generation is done! Path: " + lineChart.getAbsolutePath());
else
    System.out.println("File creation failed!");

System.exit(0);

and the generated picture (charttest.png)

enter image description here

like image 141
DVarga Avatar answered Sep 30 '22 01:09

DVarga