Skip to content

Commit eaef505

Browse files
committed
Add pagination to split large graph visualizations
1 parent fc72025 commit eaef505

File tree

4 files changed

+143
-57
lines changed

4 files changed

+143
-57
lines changed

graph-visualization/artifactDependenciesGraph/artifactDependenciesGraph.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<input type="password" id="neo4j-server-password" name="neo4j-server-password" />
2020
<input type="submit" id="neo4j-server-login" value="Login" onClick="draw()" />
2121
</span>
22-
<div id="viz"></div>
22+
<div id="visualizations"></div>
2323
</body>
2424

2525
</html>
Lines changed: 105 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,77 @@
1-
function draw() {
2-
const config = {
3-
containerId: "viz",
4-
neo4j: {
5-
serverUrl: "bolt://localhost:7687",
6-
serverUser: "neo4j",
7-
serverPassword: document.getElementById("neo4j-server-password").value || "neo4j",
1+
function getHierarchicalVisConfiguration() {
2+
return {
3+
nodes: {
4+
shape: "hexagon",
5+
shadow: false,
6+
font: {
7+
strokeWidth: 4,
8+
strokeColor: "#F2F2FF",
9+
size: 9,
10+
},
11+
size: 22,
12+
widthConstraint: {
13+
maximum: 50
14+
}
815
},
9-
visConfig: {
10-
nodes: {
11-
shape: "hexagon",
12-
shadow: false,
13-
font: {
14-
strokeWidth: 15,
15-
strokeColor: "#F2F2FF",
16-
size: 11,
16+
edges: {
17+
arrows: {
18+
to: {
19+
enabled: true,
20+
scaleFactor: 0.5,
1721
},
18-
size: 20,
1922
},
20-
edges: {
21-
arrows: {
22-
to: {
23-
enabled: true,
24-
scaleFactor: 0.6
25-
},
26-
},
27-
scaling: {
28-
max: 7,
29-
},
23+
scaling: {
24+
max: 8,
3025
},
31-
physics: {
32-
hierarchicalRepulsion: {
33-
nodeDistance: 200, // 100
34-
centralGravity: 0.5, // 0.2
35-
springLength: 180, // 200
36-
springConstant: 0.06, // 0.05
37-
damping: 0.09, // 0.09
38-
avoidOverlap: 0.1, // 0
39-
},
40-
solver: "hierarchicalRepulsion", // barnesHut
26+
},
27+
physics: {
28+
hierarchicalRepulsion: {
29+
nodeDistance: 200, // 100
30+
centralGravity: 0.5, // 0.2
31+
springLength: 200, // 200
32+
springConstant: 0.06, // 0.05
33+
damping: 0.09, // 0.09
34+
avoidOverlap: 1, // 0
4135
},
42-
layout: {
43-
hierarchical: {
44-
enabled: true,
45-
sortMethod: "directed",
46-
},
36+
solver: "hierarchicalRepulsion", // barnesHut
37+
},
38+
layout: {
39+
hierarchical: {
40+
enabled: true,
41+
sortMethod: "directed",
4742
},
4843
},
44+
};
45+
}
46+
47+
function getNeo4jCredentials() {
48+
return {
49+
serverUrl: "bolt://localhost:7687",
50+
serverUser: "neo4j",
51+
serverPassword: document.getElementById("neo4j-server-password").value || "neo4j",
52+
};
53+
}
54+
55+
function getConfiguration(containerId, credentials, visConfiguration) {
56+
return {
57+
containerId: containerId,
58+
neo4j: credentials,
59+
visConfig: visConfiguration,
4960
labels: {
5061
Artifact: {
5162
[NeoVis.NEOVIS_ADVANCED_CONFIG]: {
5263
function: {
5364
// Print all properties for the title (when nodes are clicked)
5465
title: NeoVis.objectToTitleHtml,
55-
// Use "fileName" as label. Remove leading slash, trailing ".jar" and version number.
66+
// Use "fileName" as label. Remove leading slash, trailing ".jar", version number and a trailing word like "Final".
5667
label: (node) =>
5768
node.properties.fileName
5869
.replace("/", "")
5970
.replace(".jar", "")
60-
.replace(/-[\d\\.]+/, ""),
71+
.replace(/[\d\.\-\_v]+\w+$/gm, "") +
72+
"(" +
73+
node.properties.maxDistanceFromSource +
74+
")",
6175
},
6276
},
6377
},
@@ -68,9 +82,53 @@ function draw() {
6882
value: "weight",
6983
},
7084
},
71-
initialCypher: "MATCH (s:Artifact)-[r:DEPENDS_ON]->(d:Artifact) RETURN s,r,d",
85+
initialCypher:
86+
//"MATCH (a1:Artifact)-[r1:DEPENDS_ON*0..1]->(a2:Artifact) WHERE a2.topologicalSortIndex >= $startIndex AND a2.topologicalSortIndex < $endIndex RETURN a1,r1,a2 ORDER BY a2.topologicalSortIndex",
87+
//"MATCH (a1:Artifact)-[r1:DEPENDS_ON*0..1]->(a2:Artifact) WHERE a2.topologicalSortIndex >= 0 RETURN a1,r1,a2 ORDER BY a2.topologicalSortIndex SKIP toInteger($startIndex) LIMIT toInteger($blockSize)",
88+
"MATCH (a1:Artifact)-[r1:DEPENDS_ON*0..1]->(a2:Artifact) WHERE a1.topologicalSortIndex >= 0 AND a2.topologicalSortIndex >= 0 AND a1 <> a2 RETURN a1,r1,a2 ORDER BY a2.topologicalSortIndex, a1.topologicalSortIndex SKIP toInteger($startIndex) LIMIT toInteger($blockSize)",
7289
};
90+
}
91+
92+
function markAsFinished(indexedVisualizationContainerElement) {
93+
indexedVisualizationContainerElement.classList.add("visualization-finished");
94+
const unfinishedVisualizations = document.querySelectorAll(".indexedVisualization:not(.visualization-finished)");
95+
if (unfinishedVisualizations.length === 0) {
96+
indexedVisualizationContainerElement.parentElement.classList.add("visualization-finished");
97+
}
98+
}
99+
100+
function draw() {
101+
const containerElement = document.getElementById("visualizations");
102+
103+
// Render at most 50 (maxIndex) visualizations. Rendering ends when no query records are left.
104+
const maxVisualizations = 100; //FIXME larger values lead to a bug where "visualization-finished" isn't written
105+
const recordsPerVisualization = 160;
106+
107+
for (let index = 0; (index < maxVisualizations); index++) {
108+
const indexedVisualizationContainer = document.createElement("div");
109+
indexedVisualizationContainer.id = `viz-${index}`;
110+
indexedVisualizationContainer.classList.add("indexedVisualization");
111+
containerElement.appendChild(indexedVisualizationContainer);
112+
113+
const config = getConfiguration(indexedVisualizationContainer.id, getNeo4jCredentials(), getHierarchicalVisConfiguration());
114+
const neoViz = new NeoVis.default(config);
73115

74-
const neoViz = new NeoVis.default(config);
75-
neoViz.render();
76-
}
116+
neoViz.registerOnEvent(NeoVis.NeoVisEvents.CompletionEvent, (event) => {
117+
if (event.recordCount == 0) {
118+
indexedVisualizationContainer.remove(); // remove an empty canvas
119+
} else {
120+
setTimeout(() => {
121+
neoViz.stabilize();
122+
markAsFinished(indexedVisualizationContainer);
123+
}, 5000);
124+
}
125+
});
126+
neoViz.registerOnEvent(NeoVis.NeoVisEvents.ErrorEvent, (event) => {
127+
indexedVisualizationContainer.classList.add("visualization-failed");
128+
indexedVisualizationContainer.textContent = event.error.message;
129+
markAsFinished(indexedVisualizationContainer);
130+
});
131+
const parameters = {blockSize: recordsPerVisualization, startIndex: index * recordsPerVisualization, endIndex: (index + 1) * recordsPerVisualization };
132+
neoViz.render(undefined, parameters);
133+
}
134+
}

graph-visualization/index.css

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,32 @@
11
div {
22
width: 100vw;
33
height: 100vh;
4-
}
4+
}
5+
6+
.indexedVisualization {
7+
width: 98vw;
8+
height: 100vh;
9+
display:inline-block;
10+
/* border-width: 2px;
11+
border-style: dotted;
12+
border-color: orange; */
13+
}
14+
15+
.indexedVisualization.visualization-finished {
16+
border-width: 1px;
17+
border-style: solid;
18+
border-color: lightgreen;
19+
}
20+
21+
.indexedVisualization:not(.visualization-finished) {
22+
border-width: 2px;
23+
border-style: dotted;
24+
border-color: orange;
25+
}
26+
27+
.indexedVisualization.visualization-finished.visualization-failed {
28+
height: 6em;
29+
border-width: 2px;
30+
border-style: solid;
31+
border-color: red;
32+
}

graph-visualization/renderVisualizations.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const camelToKebabCase = (str) => str.replace(/[A-Z]/g, (letter) => `-${letter.t
2525
*/
2626
const takeCanvasScreenshots = async (browser, htmlFilename) => {
2727
const page = await browser.newPage();
28-
await page.setViewport({ width: 8000, height: 12000, isMobile: false, isLandscape: true, hasTouch: false, deviceScaleFactor: 1 });
28+
await page.setViewport({ width: 12000, height: 6000, isMobile: false, isLandscape: true, hasTouch: false, deviceScaleFactor: 1 });
2929

3030
console.log(`Loading ${htmlFilename}`);
3131
await page.goto(`file://${htmlFilename}`);
@@ -36,26 +36,26 @@ const takeCanvasScreenshots = async (browser, htmlFilename) => {
3636
await loginButton.click();
3737

3838
// Wait for the graph visualization to be rendered onto a HTML5 canvas
39-
await page.waitForSelector("div canvas");
39+
console.log(`Waiting for visualizations to be finished`)
40+
await page.waitForSelector(".visualization-finished", {timeout: 50_000});
4041

4142
// Get all HTML canvas tag elements
42-
const canvasElements = await page.$$("div canvas");
43+
const canvasElements = await page.$$("canvas");
4344
if (canvasElements.length <= 0) {
44-
console.error(`No elements with CSS selector 'div canvas' found in ${htmlFilename}`);
45+
console.error(`No elements with CSS selector 'canvas' found in ${htmlFilename}`);
4546
}
47+
console.log(`Found ${canvasElements.length} visualizations`)
4648

4749
// Take a png screenshot of every canvas element and save them with increasing indices
4850
const reportName = basename(htmlFilename, ".html");
4951
const directoryName = camelToKebabCase(reportName);
5052
if (!existsSync(directoryName)) {
5153
mkdirSync(directoryName);
5254
}
53-
let index = 1;
5455
await Promise.all(
55-
canvasElements.map(async (canvasElement) => {
56-
console.log(`Taking screenshot ${reportName} of canvas ${index} in ${htmlFilename} ...`);
56+
Array.from(canvasElements).map(async (canvasElement, index) => {
57+
console.log(`Taking screenshot ${reportName} of canvas ${index} in ${htmlFilename} of element...`);
5758
await canvasElement.screenshot({ path: `./${directoryName}/${reportName}-${index}.png`, omitBackground: true });
58-
index++;
5959
})
6060
);
6161
};

0 commit comments

Comments
 (0)