0

I would like to create a simple method that plots a line chart given an object that contains the points to be displayed and some other chart attributes (like the chart title) and saves it as an image.

Here is a code example:

public class MyChartApp {

    public static void main(String[] args) throws IOException {
        ChartData chartData = generateChartData();
        WritableImage image = constructChartImage(chartData);
        save(image, "chart.png");
    }

    public static WritableImage constructChartImage(ChartData data) {
        // code to be written
    }

    private static ChartData generateChartData() {
        ChartData chartData = new ChartData();
        chartData.setValuesX(new double[]{1,2,3,4,5,6,7,8,9});
        chartData.setValuesY(new double[]{9,8,7,6,5,4,3,2,1});
        chartData.setChartTitle("My Chart");
        return chartData;
    }

    public static void save(WritableImage image, String path) throws IOException {
        BufferedImage bImage = SwingFXUtils.fromFXImage(image, null);
        ImageIO.write(bImage, "png", new File(path));
    }

    /**
     * JavaFX application used to construct the chart.
     */
    public static class MyChart extends Application {

        @Override
        public void start(Stage stage) throws Exception {

            // Somehow retrieve chartData passed to constructChartImage()
            ChartData chartData = ... 

            //defining the axes
            final NumberAxis xAxis = new NumberAxis();
            final NumberAxis yAxis = new NumberAxis();

            //creating the chart
            final LineChart<Number,Number> lineChart = 
                    new LineChart<>(xAxis,yAxis);

            lineChart.setAnimated(false);
            lineChart.setTitle(chartData.getChartTitle());

            //defining a series
            XYChart.Series series = new XYChart.Series();

            //populating the series with data
            addAll(series, chartData);

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


            lineChart.applyCss();
            lineChart.layout();

            stage.setScene(scene);

            WritableImage image = scene.snapshot(null);

            // somehow pass the image object to constructChartImage().
        }

        // utility methods for adding data to the chart //

        private static void addAll(XYChart.Series series, ChartData chartData) {
            double[] valuesX = chartData.getValuesX();
            double[] valuesY = chartData.getValuesY();
            for (int i = 0; i < valuesX.length; i++) {
                add(series, valuesX[i], valuesY[i]);
            }
        }

        private static void add(XYChart.Series series, double x, double y) {
            series.getData().add(new XYChart.Data(x,y));
        }

    }

    public static class ChartData {
        private double[] valuesX;
        private double[] valuesY;
        private String chartTitle;

        public String getChartTitle() {
            return chartTitle;
        }

        public void setChartTitle(String chartTitle) {
            this.chartTitle = chartTitle;
        }

        public double[] getValuesX() {
            return valuesX;
        }

        public void setValuesX(double[] valuesX) {
            this.valuesX = valuesX;
        }

        public double[] getValuesY() {
            return valuesY;
        }

        public void setValuesY(double[] valuesY) {
            this.valuesY = valuesY;
        }
    }

}

Although the idea might have seemed simple, the implementation is not. I do not know how to pass the chart data from the main application to the JavaFX application. Generating the data inside the JavaFX thread (for example by using the generateChartData() method) is not a preferred solution.

There is a similar question here but the suggested solution did not worked for me. It also uses static shared fields which I think is not a good practice. In addition, my case is simpler because I do not want to update the data over time.

Any ideas ?

Community
  • 1
  • 1
pgmank
  • 5,303
  • 5
  • 36
  • 52
  • Why do you need to do this? In the usual use case, the "main application" *is* the JavaFX application, and you just create the chart and the data in the `start()` method in the usual way. If you have a pressing need to set things up differently (which is really quite difficult), you need to explain why and how you are organizing your application. – James_D Oct 31 '16 at 12:30
  • The way my application is structured makes it easier if I just pass somehow the data to the JavaFX application rather than generating the data within the JavaFX application. – pgmank Oct 31 '16 at 12:55
  • "Easier" in the sense that you don't know how to do it???? You're sort of missing the point. The `start(...)` method in a JavaFX application basically replaces the `main()` method. Just move the content of your `main()` method to `start()` (though you need to be aware that `start()` is on the FX Application thread). Your structure will not work, because you need to create a `Scene` before the FX toolkit is started. – James_D Oct 31 '16 at 12:56
  • If I do that, I am afraid that there will be performance issues because my whole application will run in the JavaFX application thread instead of the main Java thread. – pgmank Oct 31 '16 at 13:04
  • Then just start another thread in `start()` and run the non-UI code there. Again, the `start()` method in the `Application` class is the place where the application starts. – James_D Oct 31 '16 at 13:04
  • OK will try it. Although in my particular case your solution will work, I believe that this question still stands and needs an answer. There is got to be a way to pass an object from the main thread to JavaFX thread. – pgmank Oct 31 '16 at 13:09
  • That last statement doesn't even make sense. You don't pass data "between threads", you pass data to objects via method calls. So what *object* are you meaning you want to pass the data to? You generally don't get access to the `Application` instance that is created on starting the FX toolkit other than from inside its `start()` method, so the general solution is that you do whatever you need in the `start()` method. If you just want to start the FX toolkit for "future use", see my answer to http://stackoverflow.com/questions/32739199 (though there is no reason to go to these lengths imho). – James_D Oct 31 '16 at 13:16

1 Answers1

1

If the aim is just to create a chart and save it to an image file, why don't you just do

public class MyChartApp extends Application {

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


    @Override
    public void start(Stage stage) throws Exception {

        // other app code...

        MyChart myChart = new MyChart();
        ChartData chartData = myChart.generateChartData();
        WritableImage image = myChart.constructChartImage(chartData);
        myChart.save(image, "chart.png");

        // other app code...
    }

    public static class MyChart {

        public WritableImage constructChartImage(ChartData data) {
            //defining the axes
            final NumberAxis xAxis = new NumberAxis();
            final NumberAxis yAxis = new NumberAxis();

            //creating the chart
            final LineChart<Number,Number> lineChart = 
                    new LineChart<>(xAxis,yAxis);

            lineChart.setAnimated(false);
            lineChart.setTitle(chartData.getChartTitle());

            //defining a series
            XYChart.Series series = new XYChart.Series();

            //populating the series with data
            addAll(series, chartData);

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


            lineChart.applyCss();
            lineChart.layout();

            WritableImage image = scene.snapshot(null);

            return image ;
        }

        private ChartData generateChartData() {
            ChartData chartData = new ChartData();
            chartData.setValuesX(new double[]{1,2,3,4,5,6,7,8,9});
            chartData.setValuesY(new double[]{9,8,7,6,5,4,3,2,1});
            chartData.setChartTitle("My Chart");
            return chartData;
        }

        public void save(WritableImage image, String path) throws IOException {
            BufferedImage bImage = SwingFXUtils.fromFXImage(image, null);
            ImageIO.write(bImage, "png", new File(path));
        }

        private void addAll(XYChart.Series series, ChartData chartData) {
            double[] valuesX = chartData.getValuesX();
            double[] valuesY = chartData.getValuesX();
            for (int i = 0; i < valuesX.length; i++) {
                add(series, valuesX[i], valuesY[i]);
            }
        }

        private void add(XYChart.Series series, double x, double y) {
            series.getData().add(new XYChart.Data(x,y));
        }

    }

    public static class ChartData {
        private double[] valuesX;
        private double[] valuesY;
        private String chartTitle;

        public String getChartTitle() {
            return chartTitle;
        }

        public void setChartTitle(String chartTitle) {
            this.chartTitle = chartTitle;
        }

        public double[] getValuesX() {
            return valuesX;
        }

        public void setValuesX(double[] valuesX) {
            this.valuesX = valuesX;
        }

        public double[] getValuesY() {
            return valuesY;
        }

        public void setValuesY(double[] valuesY) {
            this.valuesY = valuesY;
        }
    }

}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • This works fine for the simple example code I posted but not for the complex application I am working on. I would like the JavaFX application to be an extension of my main application and not my main application. – pgmank Oct 31 '16 at 12:58
  • @pgmank See update (which simply factors the chart-specific code into a class separate from the application class). – James_D Oct 31 '16 at 13:38