0

I am creating simple JavaFx application which visualizes mathematical graph in grid. Each vert can have max 4 connection. This is how Graph looks like

I use AnchorPane to store data. Everythings goes fine while i am dealing with small graphs like 100x100, but when I increase amounts of rows and columns to something like 1000x1000 I have big issues with performance.

Is there any easy way to render only visible verts or any other solution to optimise graph rendering?

I have read about virtualized controls but I cannot find implementation of it.

Example app

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.paint.*;
import javafx.scene.shape.&;
import javafx.stage.Stage;

import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

public class GraphApp extends Application {
    private static final double D = 10;

    public static final String CSS = "data:text/css," + // language=CSS
            """
            .root { -fx-base: cornsilk; }
            .scroll-pane > .viewport  { -fx-background-color: lightblue; }
            """;

    @Override
    public void start(Stage stage) {
        int[] sizes = { 3, 10, 50, 100, 200, 300, 500, 800, 1_000 };

        ScrollPane graphView = new ScrollPane();
        graphView.setPannable(true);
        graphView.setPrefSize(600, 600);

        ToggleGroup sizeToggleGroup = new ToggleGroup();

        HBox controls = new HBox(
                10
        );
        controls.setPadding(new Insets(10));

        controls.getChildren().addAll(
                Arrays.stream(sizes).mapToObj(
                        n -> createSizeSelector(n, sizeToggleGroup, graphView)
                ).toList()
        );

        ((RadioButton) controls.getChildren().get(0)).fire();

        VBox layout = new VBox(controls, graphView);

        Scene scene = new Scene(layout);
        layout.getStylesheets().add(CSS);

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

    private Node createSizeSelector(int n, ToggleGroup toggleGroup, ScrollPane graphView) {
        RadioButton sizeSelector = new RadioButton(n + "");
        sizeSelector.setOnAction(
                e -> graphView.setContent(
                        createGraphView(n)
                )
        );
        sizeSelector.setToggleGroup(toggleGroup);

        return sizeSelector;
    }

    private Parent createGraphView(int n) {
        Group group = new Group();
        Graph graph = new Graph(n);

        for (int row = 0; row < n; row++) {
            for (int col = 0; col < n; col++) {
                GraphNode curNode = graph.getNodes()[row][col];
                GraphNode eastNode = curNode.getConnections()[Direction.E.ordinal()];
                GraphNode southNode = curNode.getConnections()[Direction.S.ordinal()];

                if (eastNode != null) {
                    Line line = new Line(
                            mapX(col), mapY(row), mapX(col + 1), mapY(row)
                    );
                    line.setStroke(
                            new LinearGradient(
                                    0, 0,
                                    1, 0,
                                    true,
                                    CycleMethod.NO_CYCLE,
                                    new Stop(0, curNode.getColor()),
                                    new Stop(1, eastNode.getColor())
                            )
                    );
                    line.setStrokeWidth(D / 4);
                    group.getChildren().add(line);
                }

                if (southNode != null) {
                    Line line = new Line(
                            mapX(col), mapY(row), mapX(col), mapY(row + 1)
                    );
                    line.setStroke(
                            new LinearGradient(
                                    0, 0,
                                    0, 1,
                                    true,
                                    CycleMethod.NO_CYCLE,
                                    new Stop(0, curNode.getColor()),
                                    new Stop(1, southNode.getColor())
                            )
                    );
                    line.setStrokeWidth(D / 4);
                    group.getChildren().add(line);
                }

                Circle circle = new Circle(
                        mapX(col),
                        mapY(row),
                        D / 2,
                        curNode.getColor()
                );

                group.getChildren().add(circle);
            }
        }

        return group;
    }

    private double mapX(int col) {
        return D + col * D * 2;
    }

    private double mapY(int row) {
        return D + row * D * 2;
    }

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

class Graph {
    private static final double CONNECTIVITY_RATIO = 0.8;
    private final int n;
    private final GraphNode[][] nodes;

    public Graph(int n) {
        this.n = n;

        nodes = new GraphNode[n][n];

        for (int row = 0; row < this.n; row++) {
            for (int col = 0; col < this.n; col++) {
                nodes[row][col] = new GraphNode();
            }
        }

        for (int row = 0; row < this.n; row++) {
            for (int col = 0; col < this.n; col++) {
                connectEast(row, col);
                connectSouth(row, col);
            }
        }

        // print();
    }

    public void print() {
        for (int row = 0; row < n; row++) {
            for (int col = 0; col < n; col++) {
                System.out.println(nodes[row][col]);
            }
        }

        System.out.println(this);
    }

    private void connectEast(int row, int col) {
        if (col >= n - 1 || !isConnected()) {
            return;
        }

        GraphNode fromNode = nodes[row][col];
        GraphNode toNode = nodes[row][col+1];

        fromNode.connectTo(toNode, Direction.E);
    }

    private void connectSouth(int row, int col) {
        if (row >= n - 1 || !isConnected()) {
            return;
        }

        GraphNode fromNode = nodes[row][col];
        GraphNode toNode = nodes[row+1][col];

        fromNode.connectTo(toNode, Direction.S);
    }

    private boolean isConnected() {
        return ThreadLocalRandom.current().nextDouble() < CONNECTIVITY_RATIO;
    }

    public GraphNode[][] getNodes() {
        return nodes;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();

        for (int row = 0; row < n; row++) {
            for (int col = 0; col < n; col++) {
                GraphNode curNode = nodes[row][col];
                builder.append('*');
                if (col < n - 1) {
                    builder.append(
                            curNode.getConnections()[Direction.E.ordinal()] != null
                                    ? '-'
                                    : ' '
                    );
                }
            }

            if (row < n - 1) {
                builder.append('\n');
                for (int col = 0; col < n; col++) {
                    GraphNode curNode = nodes[row][col];
                    builder.append(
                            curNode.getConnections()[Direction.S.ordinal()] != null
                                    ? '|'
                                    : ' '
                    );

                    if (col < n - 1) {
                        builder.append(' ');
                    }
                }
                builder.append('\n');
            }
        }

        return builder.toString();
    }
}

class GraphNode {
    private final int id;
    private final GraphNode[] connections = new GraphNode[4];
    private final Color color;
    private static final Color[] colorTable = { Color.RED, Color.GREEN, Color.BLUE };
    private static final AtomicInteger sequence = new AtomicInteger();

    public GraphNode() {
        id = sequence.getAndIncrement();
        color = colorTable[
                    ThreadLocalRandom.current().nextInt(
                            colorTable.length
                    )
                ];
    }

    public GraphNode[] getConnections() {
        return connections;
    }

    public Color getColor() {
        return color;
    }

    public void connectTo(GraphNode toNode, Direction direction) {
        // System.out.println("connected: " + id + " via " + direction + " to " + toNode.id);
        connections[direction.ordinal()] = toNode;
        toNode.getConnections()[direction.opposite().ordinal()] = this;
    }

    public int getId() {
        return id;
    }

    @Override
    public String toString() {
        return "GraphNode{" +
                "id=" + id +
                ", color=" + color +
                ", connections=" +
                    Arrays.stream(connections).map(
                            n -> n != null
                                    ? n.id + ""
                                    : "-"
                    ).collect(
                            Collectors.joining(
                                    ","
                            )
                    ) +
                '}';
    }
}

enum Direction {
    N, S, E, W;

    public Direction opposite() {
        return switch (this) {
            case N -> S;
            case S -> N;
            case E -> W;
            case W -> E;
        };
    }
}
jewelsea
  • 150,031
  • 14
  • 366
  • 406
Alokin
  • 9
  • 2
  • 3
    Don't render millions of nodes, the runtime is not built to do that and you can't see all of the nodes anyway. – jewelsea May 09 '22 at 19:40
  • Perhaps you could construct a [MeshView](https://openjfx.io/javadoc/17/javafx.graphics/javafx/scene/shape/MeshView.html) for your graph. – jewelsea May 09 '22 at 19:44
  • You can find implementations of virtualized controls in the [JavaFX library source code](https://github.com/openjdk/jfx) (ListView and TableView implementations). Those can be pretty complicated, ignore TableView and just concentrate on ListView. Or in ControlsFX, with [GridView](https://github.com/controlsfx/controlsfx/blob/master/controlsfx/src/main/java/org/controlsfx/control/GridView.java). – jewelsea May 09 '22 at 19:50
  • I don't know if a virtualized control is what you want or not. The main characteristic is that a virtualized control is backed by a model with a [cell factory](https://openjfx.io/javadoc/17/javafx.controls/javafx/scene/control/ListView.html#cellFactoryProperty()). – jewelsea May 09 '22 at 19:53
  • 1
    You could also consider a [tile engine](https://www.javacodegeeks.com/2013/01/writing-a-tile-engine-in-javafx.html) style approach if that is appropriate, perhaps something in [FXGL](https://github.com/AlmasB/FXGL) could assist with that. – jewelsea May 09 '22 at 19:55
  • I think the question is currently too general to get a specific answer in a StackOverflow context, it probably [needs more focus](https://meta.stackoverflow.com/questions/417476/question-close-reasons-definitions-and-guidance/417486#417486), e.g. provide a [mcve] demonstrating the issue and perhaps somebody may help with optimizing the display. – jewelsea May 09 '22 at 20:46
  • I [edited your question](https://stackoverflow.com/posts/72176545/revisions) to add an example app that replicates the issue. I don't normally do that, and please delete the edit if it is not applicable. I just thought it might be nice for somebody to actually see what happens with different node sizes when you try to render them. – jewelsea May 09 '22 at 23:41
  • I took the example app that I added to your question. What makes a big difference in performance is applying the first two hints in [this answer](https://stackoverflow.com/questions/14467719/what-is-the-best-way-to-display-millions-of-images-in-java) to the group containing the graph: tell the JavaFX system to cache the node and set the render hint to speed. That will allow panning a graph of up to 500x500 nodes, greater than that it is much slower. It is possible that may be due to a hardware render texture size limit rather than JavaFX. – jewelsea May 10 '22 at 00:13

0 Answers0