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;
};
}
}