3

In my project I'm creating multiple AreaCharts as follows:

enter image description here

The thing is maximum count of displayed charts are 10, but length of X axis may vary. In each chart I'm using 4 series:

  • one for black ticks
  • one for red ticks
  • one for green ticks
  • last one for occurrence data

The problem is when length of chart's xAxis reaches something about 1200, scrolling the scrollPane is not smooth at all and hardly scrolls.

My question is: Is there any way to make my chart displaying more efficient?

Edit: Layout hierarchy looks like this:

ScrollPane(VBox(Charts))

ETA: Here's standalone version that presents average efficiency with higher count of values displayed in Chart. Note: Running this program may take a few seconds before charts are displayed. How can I possibly make scrolling of these views much smoother?

Program class:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.AreaChart;
import javafx.scene.chart.XYChart;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Program2 extends Application {

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

    @Override
    public void start(Stage primaryStage) {
        try {
            ScrollPane scrollPane = new ScrollPane();
            VBox vBox = new VBox();
            scrollPane.setContent(vBox);
            Scene scene = new Scene(scrollPane, 600, 600);
            primaryStage.setMaximized(true);
            primaryStage.setScene(scene);
            primaryStage.show();
            for (int i = 0; i < 10; i++) {
                CustomChart2 customChart2 = new CustomChart2();
                AreaChart.Series<Number, Number> series = new AreaChart.Series<>();
                for (int j = 0; j < 2500; j += i + 2) {
                    series.getData().add(new XYChart.Data<>(j, -0.2));
                    series.getData().add(new XYChart.Data<>(j, 1));
                    series.getData().add(new XYChart.Data<>(j + 1, 1));
                    series.getData().add(new XYChart.Data<>(j + 1, -0.2));
                }
                customChart2.getData().add(series);
                AreaChart.Series<Number, Number> series2 = new AreaChart.Series<>();
                for (int j = 0; j < 2500; j += i + 1) {
                    series2.getData().add(new XYChart.Data<>(j, -0.2));
                    series2.getData().add(new XYChart.Data<>(j, 1.5));
                    series2.getData().add(new XYChart.Data<>(j + 0.01, 1.5));
                    series2.getData().add(new XYChart.Data<>(j + 0.01, -0.2));
                }
                customChart2.getData().add(series2);
                AreaChart.Series<Number, Number> series3 = new AreaChart.Series<>();
                for (int j = 2; j < 2500; j += i + 1) {
                    series3.getData().add(new XYChart.Data<>(j, -0.2));
                    series3.getData().add(new XYChart.Data<>(j, 1.2));
                    series3.getData().add(new XYChart.Data<>(j + 0.01, 1.2));
                    series3.getData().add(new XYChart.Data<>(j + 0.01, -0.2));
                }
                customChart2.getData().add(series3);
                vBox.getChildren().add(customChart2);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Modified chart class:

import javafx.scene.chart.AreaChart;
import javafx.scene.chart.NumberAxis;

public class CustomChart2 extends AreaChart<Number, Number> {

    private NumberAxis xAxis;
    private NumberAxis yAxis;

    public CustomChart2() {
        super(new NumberAxis(0, 2500, 5), new NumberAxis(0, 1.5, 1.5));
        this.xAxis = (NumberAxis) getXAxis();
        this.yAxis = (NumberAxis) getYAxis();
        configure();
        configureXAxis();
        configureYAxis();
    }

    private void configure() {
        setAnimated(false);
        setCreateSymbols(false);
        setMinWidth(2500 * 20);
        computeMaxHeight(60);
        setMaxHeight(60);
        maxHeight(60);
        setMinHeight(60);
        setHorizontalGridLinesVisible(false);
        setVerticalGridLinesVisible(false);
        setLegendVisible(false);
        setVerticalZeroLineVisible(false);
        setHorizontalZeroLineVisible(false);
    }

    private void configureXAxis() {
        xAxis.setAutoRanging(false);
    }

    private void configureYAxis() {
        yAxis.setPrefSize(0, 0);
        yAxis.setAutoRanging(false);
        yAxis.setTickMarkVisible(false);
        yAxis.setTickLabelsVisible(false);
        yAxis.setMinorTickVisible(false);
    }
}
hpopiolkiewicz
  • 3,281
  • 4
  • 24
  • 36

2 Answers2

4

The performance issue you mention is kind of odd: the image doesn't look too complicated. It's hard to offer any advice without seeing an mcve.

If you are generating too many nodes (tens of thousands) and it is not possible to reduce that number, some options you have are:

  1. Using a canvas based charting library. An example library is JFreeChart, which can plot to a JavaFX canvas.
  2. Generating the charts up front and taking an image snapshot placed inside the ScrollPane. If using this approach, ensure that animation is switched off on the charts.
  3. Using a virtualized grid, which only renders the charts (grid cells) which are currently visible.

Other general performance tips (such as switching on node caching), may help improve performance.

Virtualized grid seems to be a solution for my problem, BUT I don't see any options for horizontal scrolling so in this case it's useless

I have never used the ControlsFX GridView, but my guess is it works the same as other virtualized controls such as ListView or TableView, which will implicitly show an appropriate scroll when necessary (e.g. if the available height >= the preferred height and the available width < the preferred width, then no vertical scroll will be displayed, but a horizontal scroll will be displayed). Because the scroll behavior and display and implicit, no option is required on the control API for the GridView. Just a guess though.

Update

I tried GridView and unfortunately, it is not as simple as I thought to get horizontal scrolling to work. Implicitly it just does vertical scrolling and, if there is an API to get horizontal scrolling working with the current implementation (which I doubt), it is not documented and would be non-intuitive to use. Adding or documenting the functionality would make a nice feature request request for ControlsFX, but that doesn't help you right now... so perhaps stick to the other performance optimization suggestions I had, or come up with some more of your own or from another answerer.

Update with Sample Code

So I kind of hacked the ControlsFX GridView control to allow it to work with a horizontal scrollbar. Probably the way I did it is not the way it should be done, but it seems to work.

Anyway here is a sample which displays over 9000 charts in a virtual grid view. The ChartGridCell and unpad-chart.css is derived from the answer to How to make the chart content area take up the maximum area available to it? and don't really add anything of interest to this answer other than some test code and data (they are not really integral to the solution).

The example code uses the same series data for every chart but you you could place the series data in the grid view item list and update the charts whenever the item is updated (similar to how the chart title and colors and getting updated in the provided sample).

sample

ManyChartsSample.java

import impl.org.controlsfx.skin.FixedWidthGridViewSkin;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import org.controlsfx.control.GridView;

import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.util.Random;

public class ManyChartsSample extends Application {
    private static final double FIXED_GRID_WIDTH = 3_000;
    private static final int OVER_9000 = 9001;

    private static final Random r = new Random(42);

    @Override
    public void start(Stage stage) throws URISyntaxException, MalformedURLException {
        GridView<ChartGridCell.CellData> grid = new GridView<>();
        grid.setSkin(new FixedWidthGridViewSkin<>(grid, FIXED_GRID_WIDTH));

        grid.setCellFactory(gridView -> new ChartGridCell());

        grid.setHorizontalCellSpacing(5);
        grid.setVerticalCellSpacing(5);

        grid.setCellHeight(200);
        grid.setCellWidth(200);

        for(int i = 0; i < OVER_9000; i++) {
            Color c1 = new Color(r.nextDouble(), r.nextDouble(), r.nextDouble(), 1.0);
            Color c2 = new Color(r.nextDouble(), r.nextDouble(), r.nextDouble(), 1.0);
            ChartGridCell.CellData data = new ChartGridCell.CellData(i, c1, c2);

            grid.getItems().add(data);
        }

        grid.setPrefSize(300, 200);

        Scene scene = new Scene(grid);
        scene.getStylesheets().add(
                getClass().getResource("unpad-chart.css").toURI().toURL().toExternalForm()
        );

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

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

impl.org.controlsfx.skin.FixedWidthGridViewSkin

package impl.org.controlsfx.skin;

import org.controlsfx.control.GridView;

public class FixedWidthGridViewSkin<T> extends GridViewSkin<T> {

    private final double fixedWidth;

    public FixedWidthGridViewSkin(GridView<T> control, double fixedWidth) {
        super(control);
        this.fixedWidth = fixedWidth;
    }

    @Override public GridRow<T> createCell() {
        GridRow<T> row = new GridRow<>();
        row.setPrefWidth(fixedWidth);
        row.updateGridView(getSkinnable());
        return row;
    }

    protected double computeRowWidth() {
        return Math.max(getSkinnable().getWidth() - 18, fixedWidth);
    }
}

ChartGridCell.java

import javafx.scene.chart.AreaChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.paint.Color;
import org.controlsfx.control.GridCell;

public class ChartGridCell extends GridCell<ChartGridCell.CellData> {
    private final AreaChart<Number,Number> chart;

    public static class CellData {

        private final int idx;
        private final Color c1;
        private final Color c2;
        private final Color c1t;
        private final Color c2t;

        public CellData(int idx, Color c1, Color c2) {
            this.idx = idx;
            this.c1 = c1;
            this.c2 = c2;
            c1t = new Color(c1.getRed(), c1.getGreen(), c1.getBlue(), 0.2);
            c2t = new Color(c2.getRed(), c2.getGreen(), c2.getBlue(), 0.2);
        }

        public int getIdx() {
            return idx;
        }

        public Color getC1() {
            return c1;
        }

        public Color getC2() {
            return c2;
        }

        public Color getC1t() {
            return c1t;
        }

        public Color getC2t() {
            return c2t;
        }
    }

    /**
     * Creates a default ColorGridCell instance.
     */
    public ChartGridCell() {
        getStyleClass().add("chart-grid-cell"); //$NON-NLS-1$

        final NumberAxis xAxis = new NumberAxis(1, 31, 1);
        final NumberAxis yAxis = new NumberAxis(0, 28, 1);

        xAxis.setAutoRanging(false);
        xAxis.setMinorTickVisible(false);
        xAxis.setTickMarkVisible(false);
        xAxis.setTickLabelsVisible(false);
        xAxis.setPrefSize(0, 0);
        yAxis.setAutoRanging(false);
        yAxis.setMinorTickVisible(false);
        yAxis.setTickMarkVisible(false);
        yAxis.setTickLabelsVisible(false);
        yAxis.setPrefSize(0, 0);

        chart = new AreaChart<>(xAxis,yAxis);
        chart.setHorizontalGridLinesVisible(false);
        chart.setVerticalGridLinesVisible(false);
        chart.setLegendVisible(false);
        chart.setVerticalZeroLineVisible(false);
        chart.setHorizontalZeroLineVisible(false);
        chart.setAnimated(false);

        XYChart.Series seriesApril= new XYChart.Series();
        seriesApril.setName("April");
        seriesApril.getData().add(new XYChart.Data(1, 4));
        seriesApril.getData().add(new XYChart.Data(3, 10));
        seriesApril.getData().add(new XYChart.Data(6, 15));
        seriesApril.getData().add(new XYChart.Data(9, 8));
        seriesApril.getData().add(new XYChart.Data(12, 5));
        seriesApril.getData().add(new XYChart.Data(15, 18));
        seriesApril.getData().add(new XYChart.Data(18, 15));
        seriesApril.getData().add(new XYChart.Data(21, 13));
        seriesApril.getData().add(new XYChart.Data(24, 19));
        seriesApril.getData().add(new XYChart.Data(27, 21));
        seriesApril.getData().add(new XYChart.Data(30, 21));
        seriesApril.getData().add(new XYChart.Data(31, 19));

        XYChart.Series seriesMay = new XYChart.Series();
        seriesMay.setName("May");
        seriesMay.getData().add(new XYChart.Data(1, 20));
        seriesMay.getData().add(new XYChart.Data(3, 15));
        seriesMay.getData().add(new XYChart.Data(6, 13));
        seriesMay.getData().add(new XYChart.Data(9, 12));
        seriesMay.getData().add(new XYChart.Data(12, 14));
        seriesMay.getData().add(new XYChart.Data(15, 18));
        seriesMay.getData().add(new XYChart.Data(18, 25));
        seriesMay.getData().add(new XYChart.Data(21, 25));
        seriesMay.getData().add(new XYChart.Data(24, 23));
        seriesMay.getData().add(new XYChart.Data(27, 26));
        seriesMay.getData().add(new XYChart.Data(31, 26));

        chart.getData().addAll(seriesApril, seriesMay);
    }

    @Override protected void updateItem(ChartGridCell.CellData item, boolean empty) {
        super.updateItem(item, empty);

        if (empty) {
            setGraphic(null);
        } else {
            chart.setTitle(item.getIdx() + ": " + toRGBCode(item.getC1()));
            chart.setStyle(
                    "CHART_COLOR_1: " + toRGBCode(item.getC1()) + "; " +
                            "CHART_COLOR_1_TRANS_20: " + toRGBCode(item.getC1t()) + ";" +
                            "CHART_COLOR_2: " + toRGBCode(item.getC2()) + "; " +
                            "CHART_COLOR_2_TRANS_20: " + toRGBCode(item.getC2t()) + ";"
            );
            setGraphic(chart);
        }
    }

    private String toRGBCode( Color color ) {
        return String.format( "#%02X%02X%02X%02X",
                (int)( color.getRed() * 255 ),
                (int)( color.getGreen() * 255 ),
                (int)( color.getBlue() * 255 ),
                (int)( color.getOpacity() * 255 )
        );
    }
}

unpad-chart.css

.chart {
    -fx-padding: 0px;
}
.chart-content {
    -fx-padding: 0px;
}
.axis {
    AXIS_COLOR: transparent;
}
.axis:top > .axis-label,
.axis:left > .axis-label {
    -fx-padding: 0;
}
.axis:bottom > .axis-label,
.axis:right > .axis-label {
    -fx-padding: 0;
}
Community
  • 1
  • 1
jewelsea
  • 150,031
  • 14
  • 366
  • 406
  • Virtualized grid seems to be a solution for my problem, BUT I don't see any options for horizontal scrolling so in this case it's useless. – hpopiolkiewicz Dec 13 '16 at 23:51
  • See updated answer for further info on GridView use. – jewelsea Dec 14 '16 at 01:05
  • I'm gonna test it today, however if it ain't work I'll post some standalone application code with my specific usecase to my question so maybe you can check it out in spare time. Nevertheless, comprehensive and complete answer, as always, @jewelsea ! – hpopiolkiewicz Dec 14 '16 at 09:07
  • added standalone program. Could you kindly check it out? Cheers. – hpopiolkiewicz Dec 14 '16 at 18:08
  • Your example ain't working. 9000 charts with small amount of data isn't the same as 10 charts with huge amount of data. – hpopiolkiewicz Dec 18 '16 at 08:50
  • @jewelsea Would you agree it is a better to it in `JFreeChart` if you are looking for performance? – Dan Dec 18 '16 at 11:50
  • 1
    @Dan It depends on the data involved. For many charting applications, the performance of the built-in chart system with JavaFX is perfectly fine. For some applications using JFreeChart is better. The way this question was originally asked, it was unclear which may be a better solution performance-wise for the original poster. For the updated question which indicates the amount of data involved, it is clear that my answer, though it works, isn't really an appropriate solution for the asked problem and solves a different problem entirely. – jewelsea Dec 19 '16 at 05:26
2

The best way to do this is using JFreeChart. This works quickly as in the code you create a graph, I just used the same values you did, and then buffer the graph in an image. This means you are rendering the image of the graph and not the actual graph itself. Hence your not trying to position thousands of data points all at once, you are only positioning the images.

Based off your example here is an example I made.

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.util.ArrayList;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;

import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Pos;
import javafx.scene.CacheHint;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Program2 extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        try {
            StackPane p = new StackPane();
            primaryStage.setTitle("Chart Application");
            Label loader = new Label("Loading...");
            loader.setGraphic(new ImageView(new Image("https://media.giphy.com/media/FmcNeI0PnsAKs/giphy.gif")));
            loader.setFont(new javafx.scene.text.Font(35));
            p.setStyle("-fx-background: #FFFFFF;");
            p.getChildren().add(loader);
            StackPane.setAlignment(loader, Pos.CENTER);

            Scene scene = new Scene(p, 600, 600);
            primaryStage.setScene(scene);
            primaryStage.setMaximized(true);

            Task<ArrayList<ImageView>> loadInitial = new Task<ArrayList<ImageView>>() {
                @Override
                public ArrayList<ImageView> call() {
                    ArrayList<ImageView> images = new ArrayList<ImageView>();

                    for (int i = 0; i < 10; i++) {
                        XYSeries data = new XYSeries(1);
                        XYSeries data2 = new XYSeries(2);
                        XYSeries data3 = new XYSeries(3);

                        System.out.println("Calcuating values for graph" + (i + 1));
                        for (int j = 0; j < 2500; j += i + 2) {
                            data.add(j, -0.2);
                            data.add(j, 1);
                            data.add(j + 1, 1);
                            data.add(j + 1, -0.2);
                        }

                        for (int j = 0; j < 2500; j += i + 1) {
                            data2.add(j, -0.2);
                            data2.add(j, 1.5);
                            data2.add(j + 0.01, 1.5);
                            data2.add(j + 0.01, -0.2);
                        }

                        for (int j = 2; j < 2500; j += i + 1) {
                            data3.add(j, -0.2);
                            data3.add(j, 1.2);
                            data3.add(j + 0.01, 1.2);
                            data3.add(j + 0.01, -0.2);
                        }
                        System.out.println("Finished values for graph" + (i + 1));

                        XYSeriesCollection dataset = new XYSeriesCollection();
                        dataset.addSeries(data);
                        dataset.addSeries(data2);
                        dataset.addSeries(data3);

                        JFreeChart chart = ChartFactory.createXYAreaChart("", "", "", dataset, PlotOrientation.VERTICAL, false, false, false);
                        chart.setBackgroundPaint(Color.WHITE);
                        chart.setBorderVisible(false);
                        chart.setAntiAlias(true);

                        XYPlot plot = (XYPlot) chart.getPlot();

                        ValueAxis range = plot.getRangeAxis();
                        range.setLowerMargin(0);
                        range.setUpperMargin(0);
                        range.setVisible(false);

                        ValueAxis domainAxis = plot.getDomainAxis();
                        domainAxis.setLowerMargin(0);
                        domainAxis.setUpperMargin(0);

                        double maxX = 0;

                        for(Object temp : dataset.getSeries()) {
                            double max = ((XYSeries) temp).getMaxX();
                            if(maxX < max)
                                maxX = max;
                        }

                        Long L = Math.round(maxX);
                        int maxVal = Integer.valueOf(L.intValue());
                        int width = maxVal * 16; //Works out length to nice scale.
                                                 //If you want all values to be the same use 40,000 (commented out)

                        if(width > 250000)
                            width = 250000; //Lags out after this

                        System.out.println("Buffering graph" + (i + 1));
                        BufferedImage capture = chart.createBufferedImage(width, 50);
                        //BufferedImage capture = chart.createBufferedImage(40000, 50);
                        System.out.println("Finished buffering graph" + (i + 1));
                        ImageView imageView = new ImageView();
                        Image chartImg = SwingFXUtils.toFXImage(capture, null);
                        imageView.setImage(chartImg);
                        imageView.setCache(true);
                        imageView.setCacheHint(CacheHint.SPEED);

                        images.add(imageView);
                    }

                    System.out.println("Finished all processes. Loading graphs");
                    return images;
                }
            };

            loadInitial.setOnSucceeded(e -> {
                VBox images = new VBox();
                ArrayList<ImageView> result = loadInitial.getValue();
                for(ImageView image : result) {
                    images.getChildren().add(image);
                }

                ScrollPane scrollPane = new ScrollPane(images);
                scrollPane.setStyle("-fx-background: #FFFFFF;");

                scene.setRoot(scrollPane);
            });

            new Thread(loadInitial).start();

            primaryStage.show();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

I also added a loading screen so the application didn't seize whilst it was loading the data.

The dependancies are jcommon-1.0.23.jar and jfreechart-1.0.19.jar. Download here if you wish or from main download place


Edit
It is worth noting since you are using JavaFX there is a class that allows you to view you graph with a lot more utility, it just won't look like what you posted. This is called ChartViewer. This will allow you to copy graphs or zoom in and out, etc...

The only problem with this is that you need to replace jfreechart-1.0.19.jar with jfreechart-1.0.19-fx.jar and to do this you need to build jfreechart-1.0.19-fx.jar. This is not so easy if you have never used ant before as I found out. So if you wish to use this function you can build build-fx.xml in the ant folder from the official JFreeChart download or you can use the one I built and placed in this google drive folder.

Dan
  • 7,286
  • 6
  • 49
  • 114
  • 1
    I'll take a look at it tommorow. Thanks for the answer! For now I'll upvote it for your effort. – hpopiolkiewicz Dec 18 '16 at 12:03
  • @user629735 Thanks. By the way. If you're wondering how efficient it is. I set the value that `j` loops to at 50,000 instead of 2,500 (20x higher) and it worked fine – Dan Dec 18 '16 at 12:07
  • Has my example lagged in your machine? – hpopiolkiewicz Dec 18 '16 at 12:08
  • @user629735 The code you posted was very laggy on my machine. Even with graphics acceleration turned on – Dan Dec 18 '16 at 12:09
  • 1
    Okay, you have my interest. I'll check it out ASAP. – hpopiolkiewicz Dec 18 '16 at 12:10
  • Checked your answer, so far so good. I'll wait a little longer - maybe someone will come up with solution based on JavaFX Charts. Already tried to snapshot these charts, but wasn't able to accomplish that due to [this jdk issue](https://bugs.openjdk.java.net/browse/JDK-8088198) – hpopiolkiewicz Dec 19 '16 at 10:46
  • 1
    @user629735 No worries. In the mean time. If you wonder why some really big graphs lose the domain axis see [here](http://stackoverflow.com/questions/41207700/why-does-the-x-axis-disappear-when-the-graph-very-big). It's not perfect but it works well for this given scenario – Dan Dec 19 '16 at 23:05
  • Here you go. Your answer deserves the bounty! – hpopiolkiewicz Dec 22 '16 at 18:06
  • @user629735 Thanks – Dan Dec 22 '16 at 18:15