This discussion is archived
1 2 Previous Next 16 Replies Latest reply: Nov 13, 2012 6:12 AM by 796425 RSS

Render charts in background

796425 Newbie
Currently Being Moderated
Hello,

I have an application that is used for data analysis. A big part of the application is the ability to be able to show charts based on the data assembled, and to be able to export a large number of tasks in one batch-operation. Up until now I have used JFreeChart, but I would like to use the native JavaFX Charts. I am on JavaFX 2.2.1 (IIRC).

I am able to generate the charts in one batch, but that means that I have to freeze the User Interface (UI), as these charts have to be rendered on the JavaFX Application Thread.

I have tried using Platform.runLater(), but then I am running into the issue with feedback, as I would also like to update a progress bar throughout the process.

Any suggestions or hints as to how this can be achieved ?
  • 1. Re: Render charts in background
    MiPa Pro
    Currently Being Moderated
    To my knowledge it is currently not possible to render anything in JavaFX outside of the application thread. This leads to this undesireable situation that you cannot offload any time-consuming rendering to a separate task and thus you have to block your UI at some point. Even the Canvas node does not render directly into its backing image.
  • 2. Re: Render charts in background
    796425 Newbie
    Currently Being Moderated
    Thank you for your answer! It's not the answer I was hoping for, though :)

    I wonder if it would be possible to start up another JavaFX Application Thread, or if that might lead me into a whole different world of concurrency issues :)
  • 3. Re: Render charts in background
    bouye Journeyer
    Currently Being Moderated
    Actually, you can use Service and Task to fill Data and Series into your chart in a background task. As long as you have not attached the chart to a Scene yet, you can work in a background task without trouble.

    As far as default charts are concerned, I used to have huge performance issues in 2.0 and 2.1 (leading to some posts in this forums and a video posted to YouTube) but in 2.2, it works pretty well (but not fully smooth yet in some instances).
    I still have some performances issues with charts that use some custom rendering through as I probably haven't optimized them well enough yet.

    Except for that, there are some CSS issues too (the limitation to 8 series for color cycling, chart CSS syntax not very clear to deal with), lack of customization of the legend, of the Y axis (though we were told by one of the speakers at JavaOne 2012 "make sure to tell 'em they can do logarithmic scale now!" :P and lack of auxliary axis...).

    Oh, and memory space occupation may cause trouble too. Usings chart is not so cheap as most of the stuff is using Double and observable (array?) list which takes a huge toll when on 64bit JVM (loved that session by a guy from IBM on why it is evil to use object storage for numbers instead of litterals and be careful of memory issues in the JVM's standard collections).
  • 4. Re: Render charts in background
    MiPa Pro
    Currently Being Moderated
    You might be able to do your rendering in a separate process if you are willing
    to handle all the resulting IPC stuff.
  • 5. Re: Render charts in background
    796425 Newbie
    Currently Being Moderated
    I did think about doing that as well.. But it really seems like an overkill solution just to be able to render charts off-screen and saved to disk :)
  • 6. Re: Render charts in background
    bouye Journeyer
    Currently Being Moderated
    Keep in mind that you can also use Canvas to create bitmap / dead pixels based charts with your own code instead of using the node based/vector graphics ones from the API.
    Any chart library that relies a lot on Java2D base features (ie: not too many transforms or composite) should not be too hard to translate to Canvas bitmap drawing code instead.
  • 7. Re: Render charts in background
    jsmith Guru
    Currently Being Moderated
    Not sure if it helps, but you maybe take a look at the solution in:
    LineChart in PDF with IText "Thread: LineChart in PDF with IText"
    and use the asynchronous snapshot function instead of the synchronous one in the solution:
    http://docs.oracle.com/javafx/2/api/javafx/scene/Node.html#snapshot%28javafx.util.Callback,%20javafx.scene.SnapshotParameters,%20javafx.scene.image.WritableImage%29

    Using the asynchronous snapshot function it would only seem possible to "update a progress bar throughout the process" if the progress bar was indefinite rather than something that showed percentage progress. Maybe that's good enough though.

    Also, note that if you want to, you can do all of the chart creation and manipulation off of the JavaFX Application Thread. It's only when you add the chart to a scene and take a snapshot of it, that you need to have it on the JavaFX application thread (assuming you are taking a snapshot of the chart).
  • 8. Re: Render charts in background
    jsmith Guru
    Currently Being Moderated
    In my experience charts render quickly and I haven't seen them freeze the UI. I've only tried charts with small data sets. Also, none of the charts in Ensemble seem to have this kind of issue.

    How long does your app "freeze the User Interface (UI)"?
    I wonder what it is about your charts which cause the UI to be frozen for a perceptible period of time?
  • 9. Re: Render charts in background
    796425 Newbie
    Currently Being Moderated
    One chart renders quite fast, its the fact that I am generating 300 charts in the background, and persisting to disk that makes the UI freeze :)
  • 10. Re: Render charts in background
    jsmith Guru
    Currently Being Moderated
    import java.awt.image.BufferedImage;
    import java.io.*;
    import java.text.SimpleDateFormat;
    import java.util.*;
    import java.util.concurrent.*;
    import java.util.logging.*;
    import javafx.application.*;
    import javafx.beans.binding.*;
    import javafx.beans.property.*;
    import javafx.beans.value.*;
    import javafx.collections.*;
    import javafx.concurrent.*;
    import javafx.embed.swing.SwingFXUtils;
    import javafx.event.*;
    import javafx.scene.*;
    import javafx.stage.Stage;
    import javafx.scene.chart.*;
    import javafx.scene.control.*;
    import javafx.scene.image.*;
    import javafx.scene.layout.*;
    import javafx.scene.paint.Color;
    import javafx.util.Callback;
    import javax.imageio.ImageIO;
     
    public class OffScreenOffThreadCharts extends Application {
      private static final String CHART_FILE_PREFIX = "chart_";
      private static final String WORKING_DIR = System.getProperty("user.dir");
      
      private final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss.SSS");
      private final Random random = new Random();
      
      private final int N_CHARTS     = 300;
      private final int PREVIEW_SIZE = 600;
      private final int CHART_SIZE   = 600;
    
      final ExecutorService saveChartsExecutor = createExecutor("SaveCharts");
              
      @Override public void start(Stage stage) throws IOException {
        stage.setTitle("Chart Export Sample");
    
        final SaveChartsTask saveChartsTask = new SaveChartsTask(N_CHARTS);
    
        final VBox layout = new VBox(10);
        layout.getChildren().addAll(
          createProgressPane(saveChartsTask), 
          createChartImagePagination(saveChartsTask)
        );
        layout.setStyle("-fx-background-color: cornsilk; -fx-padding: 15;");
    
        stage.setOnCloseRequest(new EventHandler() {
          @Override public void handle(Event event) {
            saveChartsTask.cancel();
          }
        });
        
        stage.setScene(new Scene(layout));
        stage.show();
        
        saveChartsExecutor.execute(saveChartsTask);
      }
      
      @Override public void stop() throws Exception {
        saveChartsExecutor.shutdown();
        saveChartsExecutor.awaitTermination(5, TimeUnit.SECONDS);
      }
      
      private Pagination createChartImagePagination(final SaveChartsTask saveChartsTask) {
        final Pagination pagination = new Pagination(N_CHARTS);
        pagination.setMinSize(PREVIEW_SIZE + 100, PREVIEW_SIZE + 100);
        pagination.setPageFactory(new Callback<Integer, Node>() {
          @Override public Node call(final Integer chartNumber) {
            final StackPane page = new StackPane();
            page.setStyle("-fx-background-color: antiquewhite;");
            
            if (chartNumber < saveChartsTask.getWorkDone()) {
              page.getChildren().setAll(createImageViewForChartFile(chartNumber));
            } else {
              ProgressIndicator progressIndicator = new ProgressIndicator();
              progressIndicator.setMaxSize(PREVIEW_SIZE * 1/4, PREVIEW_SIZE * 1/4);
              page.getChildren().setAll(progressIndicator);
              
              final ChangeListener<Number> WORK_DONE_LISTENER = new ChangeListener<Number>() {
                @Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
                  if (chartNumber < saveChartsTask.getWorkDone()) {
                    page.getChildren().setAll(createImageViewForChartFile(chartNumber));
                    saveChartsTask.workDoneProperty().removeListener(this);
                  }
                }
              };
    
              saveChartsTask.workDoneProperty().addListener(WORK_DONE_LISTENER);
            }
            
            return page;
          }
        });
        
        return pagination;
      }
    
      private ImageView createImageViewForChartFile(Integer chartNumber) {
        ImageView imageView = new ImageView(new Image("file:///" + getChartFilePath(chartNumber)));
        imageView.setFitWidth(PREVIEW_SIZE);
        imageView.setPreserveRatio(true);
        return imageView;
      }
    
      private Pane createProgressPane(SaveChartsTask saveChartsTask) {
        GridPane progressPane = new GridPane();
        
        progressPane.setHgap(5);
        progressPane.setVgap(5);
        progressPane.addRow(0, new Label("Create:"),     createBoundProgressBar(saveChartsTask.chartsCreationProgressProperty()));
        progressPane.addRow(1, new Label("Snapshot:"),   createBoundProgressBar(saveChartsTask.chartsSnapshotProgressProperty()));
        progressPane.addRow(2, new Label("Save:"),       createBoundProgressBar(saveChartsTask.imagesExportProgressProperty()));
        progressPane.addRow(3, new Label("Processing:"), 
          createBoundProgressBar(
            Bindings
              .when(saveChartsTask.stateProperty().isEqualTo(Worker.State.SUCCEEDED))
                .then(new SimpleDoubleProperty(1))
                .otherwise(new SimpleDoubleProperty(ProgressBar.INDETERMINATE_PROGRESS))
          )
        );
    
        return progressPane;
      }
    
      private ProgressBar createBoundProgressBar(NumberExpression progressProperty) {
        ProgressBar progressBar = new ProgressBar();
        progressBar.setMaxWidth(Double.MAX_VALUE);
        progressBar.progressProperty().bind(progressProperty);
        GridPane.setHgrow(progressBar, Priority.ALWAYS);
        return progressBar;
      }
    
      class ChartsCreationTask extends Task<Void> {
        private final int nCharts;
        private final BlockingQueue<Parent> charts;
        
        ChartsCreationTask(BlockingQueue<Parent> charts, final int nCharts) {
          this.charts = charts;
          this.nCharts = nCharts;
          updateProgress(0, nCharts);
        }
        
        @Override protected Void call() throws Exception {
          int i = nCharts;
          while (i > 0) {
            if (isCancelled()) {
              break;
            }
            charts.put(createChart());
            i--;
            updateProgress(nCharts - i, nCharts);
          }
          
          return null;
        }
        
        private Parent createChart() {
          // create a chart.
          final PieChart chart = new PieChart();
          ObservableList<PieChart.Data> pieChartData =
            FXCollections.observableArrayList(
              new PieChart.Data("Grapefruit", random.nextInt(30)),
              new PieChart.Data("Oranges",    random.nextInt(30)),
              new PieChart.Data("Plums",      random.nextInt(30)),
              new PieChart.Data("Pears",      random.nextInt(30)),
              new PieChart.Data("Apples",     random.nextInt(30))
            );
          chart.setData(pieChartData);
          chart.setTitle("Imported Fruits - " + dateFormat.format(new Date()));
    
          // Place the chart in a container pane.
          final Pane chartContainer = new Pane();
          chartContainer.getChildren().add(chart);
          chart.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
          chart.setPrefSize(CHART_SIZE, CHART_SIZE);
          chart.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
          chart.setStyle("-fx-font-size: 16px;");
          
          return chartContainer;
        }
      }
      
      class ChartsSnapshotTask extends Task<Void> {
        private final int nCharts;
        private final BlockingQueue<Parent> charts;
        private final BlockingQueue<BufferedImage> images;
        
        ChartsSnapshotTask(BlockingQueue<Parent> charts, BlockingQueue<BufferedImage> images, final int nCharts) {
          this.charts = charts;
          this.images = images;
          this.nCharts = nCharts;
          updateProgress(0, nCharts);
        }
        
        @Override protected Void call() throws Exception {
          int i = nCharts;
          while (i > 0) {
            if (isCancelled()) {
              break;
            }
            images.put(snapshotChart(charts.take()));
            i--;
            updateProgress(nCharts - i, nCharts);
          }
          
          return null;
        }
        
        private BufferedImage snapshotChart(final Parent chartContainer) throws InterruptedException {
          final CountDownLatch latch = new CountDownLatch(1);
          // render the chart in an offscreen scene (scene is used to allow css processing) and snapshot it to an image.
          // the snapshot is done in runlater as it must occur on the javafx application thread.
          final SimpleObjectProperty<BufferedImage> imageProperty = new SimpleObjectProperty();
          Platform.runLater(new Runnable() {
            @Override public void run() {
              Scene snapshotScene = new Scene(chartContainer);
              final SnapshotParameters params = new SnapshotParameters();
              params.setFill(Color.ALICEBLUE);
              chartContainer.snapshot(
                new Callback<SnapshotResult, Void>() {
                  @Override public Void call(SnapshotResult result) {
                    imageProperty.set(SwingFXUtils.fromFXImage(result.getImage(), null));
                    latch.countDown();
                    return null;
                  }
                },
                params, 
                null
              );
            }
          });
    
          latch.await();
          
          return imageProperty.get();
        }
      }
      
      class PngsExportTask extends Task<Void> {
        private final int nImages;
        private final BlockingQueue<BufferedImage> images;
        
        PngsExportTask(BlockingQueue<BufferedImage> images, final int nImages) {
          this.images = images;
          this.nImages = nImages;
          updateProgress(0, nImages);
        }
        
        @Override protected Void call() throws Exception {
          int i = nImages;
          while (i > 0) {
            if (isCancelled()) {
              break;
            }
            exportPng(images.take(), getChartFilePath(nImages - i));
            i--;
            updateProgress(nImages - i, nImages);
          }
          
          return null;
        }
        
        private void exportPng(BufferedImage image, String filename) {
          try {
            ImageIO.write(image, "png", new File(filename));
          } catch (IOException ex) {
            Logger.getLogger(OffScreenOffThreadCharts.class.getName()).log(Level.SEVERE, null, ex);
          }
        }
      }
      
      class SaveChartsTask<Void> extends Task {
        private final BlockingQueue<Parent>        charts         = new ArrayBlockingQueue(10);
        private final BlockingQueue<BufferedImage> bufferedImages = new ArrayBlockingQueue(10);
        private final ExecutorService    chartsCreationExecutor   = createExecutor("CreateCharts");
        private final ExecutorService    chartsSnapshotExecutor   = createExecutor("TakeSnapshots");
        private final ExecutorService    imagesExportExecutor     = createExecutor("ExportImages");
        private final ChartsCreationTask chartsCreationTask;
        private final ChartsSnapshotTask chartsSnapshotTask;
        private final PngsExportTask     imagesExportTask;
        
        SaveChartsTask(final int nCharts) {
          chartsCreationTask = new ChartsCreationTask(charts, nCharts);
          chartsSnapshotTask = new ChartsSnapshotTask(charts, bufferedImages, nCharts);
          imagesExportTask   = new PngsExportTask(bufferedImages, nCharts);
    
          setOnCancelled(new EventHandler() {
            @Override public void handle(Event event) {
              chartsCreationTask.cancel();
              chartsSnapshotTask.cancel();
              imagesExportTask.cancel();
            }
          });
          
          imagesExportTask.workDoneProperty().addListener(new ChangeListener<Number>() {
            @Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number workDone) {
              updateProgress(workDone.intValue(), nCharts);
            }
          });
        }
        
        ReadOnlyDoubleProperty chartsCreationProgressProperty() {
          return chartsCreationTask.progressProperty();
        }
               
        ReadOnlyDoubleProperty chartsSnapshotProgressProperty() {
          return chartsSnapshotTask.progressProperty();
        }
               
        ReadOnlyDoubleProperty imagesExportProgressProperty() {
          return imagesExportTask.progressProperty();
        }
               
        @Override protected Void call() throws Exception {
          chartsCreationExecutor.execute(chartsCreationTask);
          chartsSnapshotExecutor.execute(chartsSnapshotTask);
          imagesExportExecutor.execute(imagesExportTask);
          
          chartsCreationExecutor.shutdown();
          chartsSnapshotExecutor.shutdown();
          imagesExportExecutor.shutdown();
          
          try {
            imagesExportExecutor.awaitTermination(1, TimeUnit.DAYS);
          } catch (InterruptedException e) {
            /** no action required */
          } 
          
          return null;
        }
      }
      
      private String getChartFilePath(int chartNumber) {
        return new File(WORKING_DIR, CHART_FILE_PREFIX + chartNumber + ".png").getPath();
      }
     
      private ExecutorService createExecutor(final String name) {       
        ThreadFactory factory = new ThreadFactory() {
          @Override public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setName(name);
            t.setDaemon(true);
            return t;
          }
        };
        
        return Executors.newSingleThreadExecutor(factory);
      }  
      
      public static void main(String[] args) { launch(args); }
    }
  • 11. Re: Render charts in background
    MiPa Pro
    Currently Being Moderated
    A very nice example for offline rendering. One should point out though that this only works and does not block the UI here because the whole rendering task can be split up into individual smaller tasks but admittedly that's what the OP asked for. I'd be interested to know what you would propose if a single rendering operation takes too long like rendering a complicated map for example.
  • 12. Re: Render charts in background
    796425 Newbie
    Currently Being Moderated
    Thank you a LOT for that code example! Very nice indeed!

    I will try this out over this weekend and respond back with my results!
  • 13. Re: Render charts in background
    James_D Guru
    Currently Being Moderated
    That's a really cool example. You (or someone) should consider posting this somewhere prominent (FX experience?), as an example of how to incorporate the java.util.concurrent API with javafx.

    @MiPa: For your example, I think you can use an offscreen Canvas. Create the Canvas and get the GraphicsContext from it; "draw" on the GraphicsContext in a background thread, and once it's complete add the Canvas to the Scene graph in the JavaFX Application thread. The javadocs for GraphicsContext state:

    A Canvas only contains one GraphicsContext, and only one buffer. If it is not attached to any scene, then it can be modified by any thread, as long as it is only used from one thread at a time.

    If you're working pixel by pixel (think of a Mandelbrot set...), you can also create a Writable Image, get the PixelWriter from it, and modify pixels in a background thread as long as the image is not part of the scene graph. Then, again, set the image in an ImageView in the JavaFX application thread once you're done.
  • 14. Re: Render charts in background
    MiPa Pro
    Currently Being Moderated
    @James D
    You have to distinguish two phases of the rendering process. The first one is the preparation, where you issue your drawing commands. This can indeed be done offline but during this phase the Canvas only adds your drawing commands to a render list but does not perform any actual drawing if it is not yet attached to a scene. The second phase is started when you add the Canvas to a scene. Then this list is executed and that is what I was talking about. If this actual rendering (of this list) takes too long it will block the application thread and to my knowledge there is no way arround this. This can happen quite easily when you, for example, try to render country outlines on a map.
1 2 Previous Next

Legend

  • Correct Answers - 10 points
  • Helpful Answers - 5 points