@neo4j-nvl/layout-workers 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/lib/cosebilkent-layout/CoseBilkentLayout.worker.d.ts +1 -0
- package/lib/cosebilkent-layout/CoseBilkentLayout.worker.js +9 -0
- package/lib/cosebilkent-layout/cose-bilkent-layout-impl.d.ts +3 -0
- package/lib/cosebilkent-layout/cose-bilkent-layout-impl.js +32 -0
- package/lib/cosebilkent-layout/workerFactory.d.ts +1 -0
- package/lib/cosebilkent-layout/workerFactory.js +1 -0
- package/lib/hierarchical-layout/HierarchicalLayout.worker.d.ts +1 -0
- package/lib/hierarchical-layout/HierarchicalLayout.worker.js +16 -0
- package/lib/hierarchical-layout/constants.d.ts +27 -0
- package/lib/hierarchical-layout/constants.js +28 -0
- package/lib/hierarchical-layout/dagre-layout-impl.d.ts +5 -0
- package/lib/hierarchical-layout/dagre-layout-impl.js +361 -0
- package/lib/hierarchical-layout/workerFactory.d.ts +1 -0
- package/lib/hierarchical-layout/workerFactory.js +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +3 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Layout Workers
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { layout } from './cose-bilkent-layout-impl.js';
|
|
2
|
+
const onMessage = ({ data, target }) => {
|
|
3
|
+
if (target.name === 'CoseBilkentLayout') {
|
|
4
|
+
const { elements, boundingBox, spacingFactor, nodeCenterPoint } = data;
|
|
5
|
+
const layoutData = layout(elements, boundingBox, spacingFactor, nodeCenterPoint);
|
|
6
|
+
postMessage(layoutData);
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
self.addEventListener('message', onMessage);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import cytoscape from 'cytoscape';
|
|
2
|
+
import coseBilkent from 'cytoscape-cose-bilkent';
|
|
3
|
+
cytoscape.use(coseBilkent);
|
|
4
|
+
const type = 'cose-bilkent';
|
|
5
|
+
export const layout = (elements, boundingBox, spacingFactor, nodeCenterPoint) => {
|
|
6
|
+
const c = cytoscape({
|
|
7
|
+
headless: true,
|
|
8
|
+
styleEnabled: false,
|
|
9
|
+
boundingBox
|
|
10
|
+
});
|
|
11
|
+
c.add(elements);
|
|
12
|
+
const positions = {};
|
|
13
|
+
const cytoLayout = c.layout({
|
|
14
|
+
name: type,
|
|
15
|
+
animate: false,
|
|
16
|
+
spacingFactor,
|
|
17
|
+
quality: 'default',
|
|
18
|
+
tile: false,
|
|
19
|
+
randomize: true,
|
|
20
|
+
stop: () => {
|
|
21
|
+
c.nodes().forEach(n => {
|
|
22
|
+
positions[n.id()] = { ...n.position() };
|
|
23
|
+
positions[n.id()].x += nodeCenterPoint[0];
|
|
24
|
+
positions[n.id()].y += nodeCenterPoint[1];
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
cytoLayout.run();
|
|
29
|
+
return {
|
|
30
|
+
positions
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function createCoseBilkentLayoutWorker(): Worker;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const createCoseBilkentLayoutWorker = () => new Worker(new URL('./CoseBilkentLayout.worker.js', import.meta.url), { type: 'module', name: 'CoseBilkentLayout' });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { layout } from './dagre-layout-impl.js';
|
|
2
|
+
const onMessage = ({ data, target }) => {
|
|
3
|
+
if (target.name === 'HierarchicalLayout') {
|
|
4
|
+
const { nodes, nodeIds, idToPosition, rels, direction, packing, pixelRatio, forcedDelay = 0 } = data;
|
|
5
|
+
const layoutData = layout(nodes, nodeIds, idToPosition, rels, direction, packing, pixelRatio);
|
|
6
|
+
if (forcedDelay) {
|
|
7
|
+
setTimeout(() => {
|
|
8
|
+
postMessage(layoutData);
|
|
9
|
+
}, forcedDelay);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
postMessage(layoutData);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
self.addEventListener('message', onMessage);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const Ranker: "tight-tree";
|
|
2
|
+
export const SubGraphSpacing: 100;
|
|
3
|
+
export const DirectionUp: "up";
|
|
4
|
+
export const DirectionDown: "down";
|
|
5
|
+
export const DirectionLeft: "left";
|
|
6
|
+
export const DirectionRight: "right";
|
|
7
|
+
export namespace Directions {
|
|
8
|
+
const up: string;
|
|
9
|
+
const down: string;
|
|
10
|
+
const left: string;
|
|
11
|
+
const right: string;
|
|
12
|
+
}
|
|
13
|
+
export const PackingBin: "bin";
|
|
14
|
+
export const PackingStack: "stack";
|
|
15
|
+
export const Packings: string[];
|
|
16
|
+
export const PhysTextureSize: 256;
|
|
17
|
+
export const SpringTextureSize: 4096;
|
|
18
|
+
export const MaxIndexBuffer: 65536;
|
|
19
|
+
export const MinRelLength: 100;
|
|
20
|
+
export const Gravity: 100;
|
|
21
|
+
export const SimulationStopVelocity: 100;
|
|
22
|
+
export const MappingEnd: 999999;
|
|
23
|
+
export const ActiveAnimDuration: 1000;
|
|
24
|
+
export const DefaultNodeSize: 25;
|
|
25
|
+
export const DefaultRelWidth: 1;
|
|
26
|
+
export const DisabledRelColor: "#ededed";
|
|
27
|
+
export const GlAdjust: number;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const Ranker = 'tight-tree';
|
|
2
|
+
export const SubGraphSpacing = 100;
|
|
3
|
+
export const DirectionUp = 'up';
|
|
4
|
+
export const DirectionDown = 'down';
|
|
5
|
+
export const DirectionLeft = 'left';
|
|
6
|
+
export const DirectionRight = 'right';
|
|
7
|
+
export const Directions = {
|
|
8
|
+
[DirectionUp]: 'BT',
|
|
9
|
+
[DirectionDown]: 'TB',
|
|
10
|
+
[DirectionLeft]: 'RL',
|
|
11
|
+
[DirectionRight]: 'LR'
|
|
12
|
+
};
|
|
13
|
+
export const PackingBin = 'bin';
|
|
14
|
+
export const PackingStack = 'stack';
|
|
15
|
+
export const Packings = [PackingBin, PackingStack];
|
|
16
|
+
export const PhysTextureSize = 256;
|
|
17
|
+
export const SpringTextureSize = 4096;
|
|
18
|
+
export const MaxIndexBuffer = 65536;
|
|
19
|
+
export const MinRelLength = 100;
|
|
20
|
+
export const Gravity = 100;
|
|
21
|
+
export const SimulationStopVelocity = 100;
|
|
22
|
+
export const MappingEnd = 999999;
|
|
23
|
+
export const ActiveAnimDuration = 1000;
|
|
24
|
+
export const DefaultNodeSize = 25;
|
|
25
|
+
export const DefaultRelWidth = 1;
|
|
26
|
+
export const DisabledRelColor = '#ededed';
|
|
27
|
+
// Factor to make nodes same size in WebGL renderer and Canvas
|
|
28
|
+
export const GlAdjust = 1 / 0.38;
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import dagre from '@neo4j-bloom/dagre';
|
|
2
|
+
import graphlib from 'graphlib';
|
|
3
|
+
import binPack from 'bin-pack';
|
|
4
|
+
import { Ranker, SubGraphSpacing, DirectionUp, DirectionDown, DirectionRight, Directions, PackingBin, DefaultNodeSize, GlAdjust } from './constants.js';
|
|
5
|
+
const isDirectionVertical = (direction) => direction === DirectionUp || direction === DirectionDown;
|
|
6
|
+
const isDirectionNatural = (direction) => direction === DirectionDown || direction === DirectionRight;
|
|
7
|
+
const getGraphDimensions = (g) => {
|
|
8
|
+
let minX = null;
|
|
9
|
+
let minY = null;
|
|
10
|
+
let maxX = null;
|
|
11
|
+
let maxY = null;
|
|
12
|
+
let minCenterX = null;
|
|
13
|
+
let minCenterY = null;
|
|
14
|
+
let maxCenterX = null;
|
|
15
|
+
let maxCenterY = null;
|
|
16
|
+
for (const id of g.nodes()) {
|
|
17
|
+
const sn = g.node(id);
|
|
18
|
+
if (minCenterX === null || sn.x < minCenterX) {
|
|
19
|
+
minCenterX = sn.x;
|
|
20
|
+
}
|
|
21
|
+
if (minCenterY === null || sn.y < minCenterY) {
|
|
22
|
+
minCenterY = sn.y;
|
|
23
|
+
}
|
|
24
|
+
if (maxCenterX === null || sn.x > maxCenterX) {
|
|
25
|
+
maxCenterX = sn.x;
|
|
26
|
+
}
|
|
27
|
+
if (maxCenterY === null || sn.y > maxCenterY) {
|
|
28
|
+
maxCenterY = sn.y;
|
|
29
|
+
}
|
|
30
|
+
const halfSize = Math.ceil(sn.width / 2.0);
|
|
31
|
+
if (minX === null || (sn.x - halfSize) < minX) {
|
|
32
|
+
minX = sn.x - halfSize;
|
|
33
|
+
}
|
|
34
|
+
if (minY === null || (sn.y - halfSize) < minY) {
|
|
35
|
+
minY = sn.y - halfSize;
|
|
36
|
+
}
|
|
37
|
+
if (maxX === null || (sn.x + halfSize) > maxX) {
|
|
38
|
+
maxX = sn.x + halfSize;
|
|
39
|
+
}
|
|
40
|
+
if (maxY === null || (sn.y + halfSize) > maxY) {
|
|
41
|
+
maxY = sn.y + halfSize;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
minX,
|
|
46
|
+
minY,
|
|
47
|
+
maxX,
|
|
48
|
+
maxY,
|
|
49
|
+
minCenterX,
|
|
50
|
+
minCenterY,
|
|
51
|
+
maxCenterX,
|
|
52
|
+
maxCenterY,
|
|
53
|
+
width: maxX - minX,
|
|
54
|
+
height: maxY - minY,
|
|
55
|
+
xOffset: minCenterX - minX,
|
|
56
|
+
yOffset: minCenterY - minY
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
const createGraph = (pixelRatio) => {
|
|
60
|
+
// Create a new directed graph
|
|
61
|
+
const g = new dagre.graphlib.Graph();
|
|
62
|
+
// Set an object for the graph label
|
|
63
|
+
g.setGraph({});
|
|
64
|
+
// Default to assigning a new object as a label for each new edge.
|
|
65
|
+
g.setDefaultEdgeLabel(() => ({}));
|
|
66
|
+
// Configuration https://github.com/dagrejs/dagre/wiki#configuring-the-layout
|
|
67
|
+
// Number of pixels that separate nodes horizontally in the layout.
|
|
68
|
+
g.graph().nodesep = 75 * pixelRatio;
|
|
69
|
+
// Number of pixels between each rank in the layout.
|
|
70
|
+
g.graph().ranksep = 75 * pixelRatio;
|
|
71
|
+
return g;
|
|
72
|
+
};
|
|
73
|
+
const findParent = (id, layoutGraph) => {
|
|
74
|
+
// predecessors uses inEdges, successors uses outEdges
|
|
75
|
+
let pId = findParentForEdges(id, layoutGraph.predecessors(id), layoutGraph);
|
|
76
|
+
if (pId === null) {
|
|
77
|
+
pId = findParentForEdges(id, layoutGraph.successors(id), layoutGraph);
|
|
78
|
+
}
|
|
79
|
+
return pId;
|
|
80
|
+
};
|
|
81
|
+
const findParentForEdges = (id, connectedNodes, layoutGraph) => {
|
|
82
|
+
const { rank: currentRank } = layoutGraph.node(id);
|
|
83
|
+
let pRank = null;
|
|
84
|
+
let pId = null;
|
|
85
|
+
for (const otherId of connectedNodes) {
|
|
86
|
+
const { rank } = layoutGraph.node(otherId);
|
|
87
|
+
if (otherId === id || rank >= currentRank) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
else if (rank === currentRank - 1) {
|
|
91
|
+
pRank = rank;
|
|
92
|
+
pId = otherId;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
else if ((pRank === null && pId === null) || rank > pRank) {
|
|
96
|
+
pRank = rank;
|
|
97
|
+
pId = otherId;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return pId;
|
|
101
|
+
};
|
|
102
|
+
const getConnectedSubGraphs = (g, pixelRatio) => {
|
|
103
|
+
const subGraphs = [];
|
|
104
|
+
const components = graphlib.alg.components(g);
|
|
105
|
+
if (components.length > 1) {
|
|
106
|
+
for (const component of components) {
|
|
107
|
+
const subGraph = createGraph(pixelRatio);
|
|
108
|
+
for (const id of component) {
|
|
109
|
+
const n = g.node(id);
|
|
110
|
+
subGraph.setNode(id, { width: n.width, height: n.height });
|
|
111
|
+
const outEdges = g.outEdges(id);
|
|
112
|
+
if (outEdges) {
|
|
113
|
+
for (const e of outEdges) {
|
|
114
|
+
subGraph.setEdge(e.v, e.w);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
subGraphs.push(subGraph);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
subGraphs.push(g);
|
|
123
|
+
}
|
|
124
|
+
return subGraphs;
|
|
125
|
+
};
|
|
126
|
+
const layoutGraph = (g, direction, parents) => {
|
|
127
|
+
g.graph().ranker = Ranker;
|
|
128
|
+
g.graph().rankdir = Directions[direction];
|
|
129
|
+
const layoutGraph = dagre.layout(g);
|
|
130
|
+
for (const id of layoutGraph.nodes()) {
|
|
131
|
+
const pId = findParent(id, layoutGraph);
|
|
132
|
+
if (pId !== null) {
|
|
133
|
+
parents[id] = pId;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const getDistance = (p1, p2) => Math.sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y));
|
|
138
|
+
const mergeStraightPoints = (points) => {
|
|
139
|
+
const mergedPoints = [points[0]];
|
|
140
|
+
let prevSegment = { p1: points[0], p2: points[1] };
|
|
141
|
+
let prevLength = getDistance(prevSegment.p1, prevSegment.p2);
|
|
142
|
+
for (let i = 2; i < points.length; i++) {
|
|
143
|
+
let currentSegment = { p1: points[i - 1], p2: points[i] };
|
|
144
|
+
let currentLength = getDistance(currentSegment.p1, currentSegment.p2);
|
|
145
|
+
const compositeSegment = { p1: prevSegment.p1, p2: currentSegment.p2 };
|
|
146
|
+
const compositeLength = getDistance(compositeSegment.p1, compositeSegment.p2);
|
|
147
|
+
// if 2 consecutive segments are parallel then join them
|
|
148
|
+
// Use the triangular inequality for checking the parallelism
|
|
149
|
+
if (currentLength + prevLength - compositeLength < 0.1) {
|
|
150
|
+
mergedPoints.pop();
|
|
151
|
+
currentSegment = compositeSegment;
|
|
152
|
+
currentLength = compositeLength;
|
|
153
|
+
}
|
|
154
|
+
mergedPoints.push(currentSegment.p1);
|
|
155
|
+
prevSegment = currentSegment;
|
|
156
|
+
prevLength = currentLength;
|
|
157
|
+
}
|
|
158
|
+
mergedPoints.push(points[points.length - 1]);
|
|
159
|
+
return mergedPoints;
|
|
160
|
+
};
|
|
161
|
+
export const layout = (nodes, nodeIds, idToPosition, rels, direction, packing, pixelRatio = 1) => {
|
|
162
|
+
const g = createGraph(pixelRatio);
|
|
163
|
+
const parents = {};
|
|
164
|
+
const positionSum = { x: 0, y: 0 };
|
|
165
|
+
const numNodes = nodes.length;
|
|
166
|
+
// Add nodes to the graph. The first argument is the node id. The second is
|
|
167
|
+
// metadata about the node.
|
|
168
|
+
for (const n of nodes) {
|
|
169
|
+
const position = idToPosition[n.id];
|
|
170
|
+
positionSum.x += position?.x || 0;
|
|
171
|
+
positionSum.y += position?.y || 0;
|
|
172
|
+
const size = (n.size || DefaultNodeSize) * GlAdjust * pixelRatio;
|
|
173
|
+
g.setNode(n.id, { width: size, height: size });
|
|
174
|
+
}
|
|
175
|
+
const prevNodeCenterPoint = numNodes ? [positionSum.x / numNodes, positionSum.y / numNodes] : [0, 0];
|
|
176
|
+
// Add edges to the graph.
|
|
177
|
+
const addedRel = {};
|
|
178
|
+
for (const r of rels) {
|
|
179
|
+
if (nodeIds[r.from] && nodeIds[r.to] && r.from !== r.to) {
|
|
180
|
+
const relKey = r.from < r.to ? `${r.from}-${r.to}` : `${r.to}-${r.from}`;
|
|
181
|
+
if (!addedRel[relKey]) {
|
|
182
|
+
addedRel[relKey] = 1;
|
|
183
|
+
g.setEdge(r.from, r.to);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const subGraphs = getConnectedSubGraphs(g, pixelRatio);
|
|
188
|
+
if (subGraphs.length > 1) {
|
|
189
|
+
subGraphs.forEach(subGraph => layoutGraph(subGraph, direction, parents));
|
|
190
|
+
const isVertical = isDirectionVertical(direction);
|
|
191
|
+
const isNatural = isDirectionNatural(direction);
|
|
192
|
+
const singleNodeGraphs = subGraphs.filter(g => g.nodeCount() === 1);
|
|
193
|
+
const multiNodeGraphs = subGraphs.filter(g => g.nodeCount() !== 1);
|
|
194
|
+
if (packing === PackingBin) {
|
|
195
|
+
multiNodeGraphs.sort((a, b) => b.nodeCount() - a.nodeCount());
|
|
196
|
+
const adjustDimensionPaddingNormal = ({ width, height, ...rest }) => ({ ...rest, width: width + SubGraphSpacing, height: height + SubGraphSpacing });
|
|
197
|
+
const adjustDimensionPaddingFlip = ({ width, height, ...rest }) => ({ ...rest, width: height + SubGraphSpacing, height: width + SubGraphSpacing });
|
|
198
|
+
const adjustDimensionPadding = isVertical ? adjustDimensionPaddingNormal : adjustDimensionPaddingFlip;
|
|
199
|
+
const multiGraphDimensions = multiNodeGraphs.map(getGraphDimensions).map(adjustDimensionPadding);
|
|
200
|
+
const singleGraphDimensions = singleNodeGraphs.map(getGraphDimensions).map(adjustDimensionPadding);
|
|
201
|
+
const bins = multiGraphDimensions.concat(singleGraphDimensions);
|
|
202
|
+
binPack(bins, { inPlace: true });
|
|
203
|
+
const halfSpacing = Math.floor(SubGraphSpacing / 2);
|
|
204
|
+
const xProp = isVertical ? 'x' : 'y';
|
|
205
|
+
const yProp = isVertical ? 'y' : 'x';
|
|
206
|
+
if (!isNatural) {
|
|
207
|
+
const positionProp = isVertical ? 'y' : 'x';
|
|
208
|
+
const extentProp = isVertical ? 'height' : 'width';
|
|
209
|
+
const min = bins.reduce((min, d) => min === null ? d[positionProp] : Math.min(d[positionProp], min[extentProp] || 0), null);
|
|
210
|
+
const max = bins.reduce((max, d) => max === null ? (d[positionProp] + d[extentProp]) : Math.max((d[positionProp] + d[extentProp]), max[extentProp] || 0), null);
|
|
211
|
+
bins.forEach(d => {
|
|
212
|
+
d[positionProp] = min + (max - (d[positionProp] + d[extentProp]));
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const assignPositions = (subGraph, dimensions) => {
|
|
216
|
+
for (const id of subGraph.nodes()) {
|
|
217
|
+
const sn = subGraph.node(id);
|
|
218
|
+
const n = g.node(id);
|
|
219
|
+
n.x = sn.x - dimensions.xOffset + dimensions[xProp] + halfSpacing;
|
|
220
|
+
n.y = sn.y - dimensions.yOffset + dimensions[yProp] + halfSpacing;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
for (let i = 0; i < multiNodeGraphs.length; i++) {
|
|
224
|
+
const subGraph = multiNodeGraphs[i];
|
|
225
|
+
const dimensions = multiGraphDimensions[i];
|
|
226
|
+
assignPositions(subGraph, dimensions);
|
|
227
|
+
}
|
|
228
|
+
for (let i = 0; i < singleNodeGraphs.length; i++) {
|
|
229
|
+
const subGraph = singleNodeGraphs[i];
|
|
230
|
+
const dimensions = singleGraphDimensions[i];
|
|
231
|
+
assignPositions(subGraph, dimensions);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
multiNodeGraphs.sort(isNatural ? (a, b) => b.nodeCount() - a.nodeCount() : (a, b) => a.nodeCount() - b.nodeCount());
|
|
236
|
+
const multiGraphDimensions = multiNodeGraphs.map(getGraphDimensions);
|
|
237
|
+
const singleNodesAcc = singleNodeGraphs.reduce((acc, subGraph) => acc + g.node(subGraph.nodes()[0]).width, 0);
|
|
238
|
+
const singleNodesMaxSize = singleNodeGraphs.reduce((maxSize, subGraph) => Math.max(maxSize, g.node(subGraph.nodes()[0]).width), 0);
|
|
239
|
+
const singleNodesSize = singleNodeGraphs.length > 0 ? singleNodesAcc + (singleNodeGraphs.length - 1) * SubGraphSpacing : 0;
|
|
240
|
+
const maxSubGraphWidth = multiGraphDimensions.reduce((maxWidth, { width }) => Math.max(maxWidth, width), 0);
|
|
241
|
+
const graphWidth = Math.max(maxSubGraphWidth, singleNodesSize);
|
|
242
|
+
const maxSubGraphHeight = multiGraphDimensions.reduce((maxHeight, { height }) => Math.max(maxHeight, height), 0);
|
|
243
|
+
const graphHeight = Math.max(maxSubGraphHeight, singleNodesSize);
|
|
244
|
+
let position = 0;
|
|
245
|
+
const positionMultiNodeGraphs = () => {
|
|
246
|
+
for (let i = 0; i < multiNodeGraphs.length; i++) {
|
|
247
|
+
const subGraph = multiNodeGraphs[i];
|
|
248
|
+
const dimensions = multiGraphDimensions[i];
|
|
249
|
+
const centerOffset = isVertical ? Math.floor((graphWidth - dimensions.width) / 2.0) : Math.floor((graphHeight - dimensions.height) / 2.0);
|
|
250
|
+
for (const id of subGraph.nodes()) {
|
|
251
|
+
const sn = subGraph.node(id);
|
|
252
|
+
const n = g.node(id);
|
|
253
|
+
if (isVertical) {
|
|
254
|
+
n.x = sn.x - dimensions.minX + centerOffset;
|
|
255
|
+
n.y = sn.y - dimensions.minY + position;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
n.x = sn.x - dimensions.minX + position;
|
|
259
|
+
n.y = sn.y - dimensions.minY + centerOffset;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (const id of subGraph.edges()) {
|
|
263
|
+
const sedge = subGraph.edge(id);
|
|
264
|
+
const edge = g.edge(id);
|
|
265
|
+
if (sedge.points && sedge.points.length > 3) {
|
|
266
|
+
edge.points = sedge.points.map(({ x, y }) => ({
|
|
267
|
+
x: x - dimensions.minX + (isVertical ? centerOffset : position),
|
|
268
|
+
y: y - dimensions.minY + (isVertical ? position : centerOffset)
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
position += (isVertical ? dimensions.height : dimensions.width) + SubGraphSpacing;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
const positionSingleNodeGraphs = () => {
|
|
276
|
+
const singleCenterOffset = Math.floor(((isVertical ? graphWidth : graphHeight) - singleNodesSize) / 2.0);
|
|
277
|
+
position += Math.floor(singleNodesMaxSize / 2.0);
|
|
278
|
+
let singlePosition = singleCenterOffset;
|
|
279
|
+
for (const subGraph of singleNodeGraphs) {
|
|
280
|
+
const id = subGraph.nodes()[0];
|
|
281
|
+
const n = g.node(id);
|
|
282
|
+
if (isVertical) {
|
|
283
|
+
n.x = singlePosition + Math.floor(n.width / 2.0);
|
|
284
|
+
n.y = position;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
n.x = position;
|
|
288
|
+
n.y = singlePosition + Math.floor(n.width / 2.0);
|
|
289
|
+
}
|
|
290
|
+
singlePosition += SubGraphSpacing + n.width;
|
|
291
|
+
}
|
|
292
|
+
position = singleNodesMaxSize + SubGraphSpacing;
|
|
293
|
+
};
|
|
294
|
+
if (isNatural) {
|
|
295
|
+
positionMultiNodeGraphs();
|
|
296
|
+
positionSingleNodeGraphs();
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
positionSingleNodeGraphs();
|
|
300
|
+
positionMultiNodeGraphs();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
layoutGraph(g, direction, parents);
|
|
306
|
+
}
|
|
307
|
+
positionSum.x = 0;
|
|
308
|
+
positionSum.y = 0;
|
|
309
|
+
const positions = {};
|
|
310
|
+
for (const id of g.nodes()) {
|
|
311
|
+
const n = g.node(id);
|
|
312
|
+
positionSum.x += n.x || 0;
|
|
313
|
+
positionSum.y += n.y || 0;
|
|
314
|
+
positions[id] = { x: n.x, y: n.y };
|
|
315
|
+
}
|
|
316
|
+
const newNodeCenterPoint = numNodes ? [positionSum.x / numNodes, positionSum.y / numNodes] : [0, 0];
|
|
317
|
+
const translateX = prevNodeCenterPoint[0] - newNodeCenterPoint[0];
|
|
318
|
+
const translateY = prevNodeCenterPoint[1] - newNodeCenterPoint[1];
|
|
319
|
+
for (const key in positions) {
|
|
320
|
+
positions[key].x += translateX;
|
|
321
|
+
positions[key].y += translateY;
|
|
322
|
+
}
|
|
323
|
+
const waypoints = {};
|
|
324
|
+
for (const id of g.edges()) {
|
|
325
|
+
const e = g.edge(id);
|
|
326
|
+
if (e.points && e.points.length > 3) {
|
|
327
|
+
const mergedPoints = mergeStraightPoints(e.points);
|
|
328
|
+
for (const p of mergedPoints) {
|
|
329
|
+
p.x += translateX;
|
|
330
|
+
p.y += translateY;
|
|
331
|
+
}
|
|
332
|
+
waypoints[`${id.v}-${id.w}`] = {
|
|
333
|
+
points: [...mergedPoints],
|
|
334
|
+
from: {
|
|
335
|
+
x: positions[id.v].x,
|
|
336
|
+
y: positions[id.v].y
|
|
337
|
+
},
|
|
338
|
+
to: {
|
|
339
|
+
x: positions[id.w].x,
|
|
340
|
+
y: positions[id.w].y
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
waypoints[`${id.w}-${id.v}`] = {
|
|
344
|
+
points: mergedPoints.reverse(),
|
|
345
|
+
from: {
|
|
346
|
+
x: positions[id.w].x,
|
|
347
|
+
y: positions[id.w].y
|
|
348
|
+
},
|
|
349
|
+
to: {
|
|
350
|
+
x: positions[id.v].x,
|
|
351
|
+
y: positions[id.v].y
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
positions,
|
|
358
|
+
parents,
|
|
359
|
+
waypoints
|
|
360
|
+
};
|
|
361
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function createHierarchicalLayoutWorker(): Worker;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const createHierarchicalLayoutWorker = () => new Worker(new URL('./HierarchicalLayout.worker.js', import.meta.url), { type: 'module', name: 'HierarchicalLayout' });
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neo4j-nvl/layout-workers",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "lib/index.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"lib"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"prebuild": "rm -rf lib/",
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"test": "echo 'not implemented'"
|
|
13
|
+
},
|
|
14
|
+
"typedoc": {
|
|
15
|
+
"entryPoint": "./src/index.js",
|
|
16
|
+
"readmeFile": "./README.md",
|
|
17
|
+
"displayName": "Layout Workers",
|
|
18
|
+
"tsconfig": "./tsconfig.json"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"yarn": "^1.10.1"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@neo4j-bloom/dagre": "^0.8.14",
|
|
25
|
+
"bin-pack": "^1.0.2",
|
|
26
|
+
"cytoscape": "^3.23.0",
|
|
27
|
+
"cytoscape-cose-bilkent": "^4.1.0",
|
|
28
|
+
"graphlib": "^2.1.8"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^18.11.11",
|
|
32
|
+
"typedoc": "^0.23.26"
|
|
33
|
+
}
|
|
34
|
+
}
|