react-html-graph 1.2.1 → 1.2.3
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/dist/behaviour/move-behaviour.d.ts +50 -0
- package/dist/behaviour/move-behaviour.d.ts.map +1 -0
- package/dist/behaviour/move-behaviour.js +71 -0
- package/dist/behaviour/move-behaviour.js.map +1 -0
- package/dist/calculations/index.d.ts +15 -1
- package/dist/calculations/index.d.ts.map +1 -1
- package/dist/calculations/index.js +1026 -0
- package/dist/calculations/index.js.map +1 -1
- package/dist/calculations/types.d.ts +70 -0
- package/dist/calculations/types.d.ts.map +1 -1
- package/dist/context/connection-context.d.ts.map +1 -1
- package/dist/context/connection-context.js.map +1 -1
- package/dist/context/error-context.d.ts.map +1 -1
- package/dist/context/error-context.js.map +1 -1
- package/dist/context/graph-context.d.ts.map +1 -1
- package/dist/context/graph-context.js.map +1 -1
- package/dist/context/graph-event-bus-context.d.ts +4 -0
- package/dist/context/graph-event-bus-context.d.ts.map +1 -0
- package/dist/context/graph-event-bus-context.js +8 -0
- package/dist/context/graph-event-bus-context.js.map +1 -0
- package/dist/context/graph-root-context.d.ts.map +1 -1
- package/dist/context/graph-root-context.js.map +1 -1
- package/dist/context/link-info-context.d.ts +4 -0
- package/dist/context/link-info-context.d.ts.map +1 -0
- package/dist/context/link-info-context.js +15 -0
- package/dist/context/link-info-context.js.map +1 -0
- package/dist/context/node-event-context.d.ts +4 -0
- package/dist/context/node-event-context.d.ts.map +1 -0
- package/dist/context/node-event-context.js +10 -0
- package/dist/context/node-event-context.js.map +1 -0
- package/dist/context/node-registry-context.d.ts +4 -0
- package/dist/context/node-registry-context.d.ts.map +1 -0
- package/dist/context/node-registry-context.js +13 -0
- package/dist/context/node-registry-context.js.map +1 -0
- package/dist/graph/index.d.ts +2 -2
- package/dist/graph/index.d.ts.map +1 -1
- package/dist/graph/index.js +446 -36
- package/dist/graph/index.js.map +1 -1
- package/dist/hooks/connection.d.ts +1 -1
- package/dist/hooks/connection.d.ts.map +1 -1
- package/dist/hooks/connection.js +5 -2
- package/dist/hooks/connection.js.map +1 -1
- package/dist/hooks/get-viewbox.d.ts +9 -0
- package/dist/hooks/get-viewbox.d.ts.map +1 -0
- package/dist/hooks/get-viewbox.js +20 -0
- package/dist/hooks/get-viewbox.js.map +1 -0
- package/dist/hooks/graph-event-bus.d.ts +3 -0
- package/dist/hooks/graph-event-bus.d.ts.map +1 -0
- package/dist/hooks/graph-event-bus.js +7 -0
- package/dist/hooks/graph-event-bus.js.map +1 -0
- package/dist/hooks/link-info.d.ts +4 -0
- package/dist/hooks/link-info.d.ts.map +1 -0
- package/dist/hooks/link-info.js +7 -0
- package/dist/hooks/link-info.js.map +1 -0
- package/dist/hooks/node-registry.d.ts +3 -0
- package/dist/hooks/node-registry.d.ts.map +1 -0
- package/dist/hooks/node-registry.js +7 -0
- package/dist/hooks/node-registry.js.map +1 -0
- package/dist/hooks/nodeEvent.d.ts +7 -0
- package/dist/hooks/nodeEvent.d.ts.map +1 -0
- package/dist/hooks/nodeEvent.js +17 -0
- package/dist/hooks/nodeEvent.js.map +1 -0
- package/dist/hooks/use-graph-api.d.ts +50 -0
- package/dist/hooks/use-graph-api.d.ts.map +1 -0
- package/dist/hooks/use-graph-api.js +97 -0
- package/dist/hooks/use-graph-api.js.map +1 -0
- package/dist/layouts/centralizar.d.ts +1 -0
- package/dist/layouts/centralizar.d.ts.map +1 -0
- package/dist/layouts/centralizar.js +2 -0
- package/dist/layouts/centralizar.js.map +1 -0
- package/dist/layouts/force-direction-layout.d.ts +9 -0
- package/dist/layouts/force-direction-layout.d.ts.map +1 -0
- package/dist/layouts/force-direction-layout.js +11 -0
- package/dist/layouts/force-direction-layout.js.map +1 -0
- package/dist/layouts/index.d.ts +13 -0
- package/dist/layouts/index.d.ts.map +1 -0
- package/dist/layouts/index.js +17 -0
- package/dist/layouts/index.js.map +1 -0
- package/dist/layouts/organic-layout.d.ts +9 -0
- package/dist/layouts/organic-layout.d.ts.map +1 -0
- package/dist/layouts/organic-layout.js +11 -0
- package/dist/layouts/organic-layout.js.map +1 -0
- package/dist/layouts/radial-layout.d.ts +9 -0
- package/dist/layouts/radial-layout.d.ts.map +1 -0
- package/dist/layouts/radial-layout.js +11 -0
- package/dist/layouts/radial-layout.js.map +1 -0
- package/dist/layouts/sequential-layout.d.ts +9 -0
- package/dist/layouts/sequential-layout.d.ts.map +1 -0
- package/dist/layouts/sequential-layout.js +11 -0
- package/dist/layouts/sequential-layout.js.map +1 -0
- package/dist/layouts/shared.d.ts +10 -0
- package/dist/layouts/shared.d.ts.map +1 -0
- package/dist/layouts/shared.js +39 -0
- package/dist/layouts/shared.js.map +1 -0
- package/dist/layouts/structural-layout.d.ts +9 -0
- package/dist/layouts/structural-layout.d.ts.map +1 -0
- package/dist/layouts/structural-layout.js +11 -0
- package/dist/layouts/structural-layout.js.map +1 -0
- package/dist/layouts/tree-layout.d.ts +9 -0
- package/dist/layouts/tree-layout.d.ts.map +1 -0
- package/dist/layouts/tree-layout.js +11 -0
- package/dist/layouts/tree-layout.js.map +1 -0
- package/dist/link/base.d.ts +10 -20
- package/dist/link/base.d.ts.map +1 -1
- package/dist/link/base.js +17 -197
- package/dist/link/base.js.map +1 -1
- package/dist/link/temp-link.d.ts.map +1 -1
- package/dist/link/temp-link.js +24 -7
- package/dist/link/temp-link.js.map +1 -1
- package/dist/module.d.ts +11 -1
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +11 -1
- package/dist/module.js.map +1 -1
- package/dist/nodes/base.d.ts +2 -1
- package/dist/nodes/base.d.ts.map +1 -1
- package/dist/nodes/base.js +75 -66
- package/dist/nodes/base.js.map +1 -1
- package/dist/paths/bidirectional-path.d.ts +16 -0
- package/dist/paths/bidirectional-path.d.ts.map +1 -0
- package/dist/paths/bidirectional-path.js +260 -0
- package/dist/paths/bidirectional-path.js.map +1 -0
- package/dist/ports/base.d.ts.map +1 -1
- package/dist/ports/base.js +26 -13
- package/dist/ports/base.js.map +1 -1
- package/dist/providers/connection-provider.d.ts +1 -1
- package/dist/providers/connection-provider.d.ts.map +1 -1
- package/dist/providers/connection-provider.js +30 -16
- package/dist/providers/connection-provider.js.map +1 -1
- package/dist/providers/graph-event-bus-provider.d.ts +11 -0
- package/dist/providers/graph-event-bus-provider.d.ts.map +1 -0
- package/dist/providers/graph-event-bus-provider.js +46 -0
- package/dist/providers/graph-event-bus-provider.js.map +1 -0
- package/dist/providers/link-info-provider.d.ts +23 -0
- package/dist/providers/link-info-provider.d.ts.map +1 -0
- package/dist/providers/link-info-provider.js +31 -0
- package/dist/providers/link-info-provider.js.map +1 -0
- package/dist/providers/node-event-context.d.ts +3 -0
- package/dist/providers/node-event-context.d.ts.map +1 -0
- package/dist/providers/node-event-context.js +81 -0
- package/dist/providers/node-event-context.js.map +1 -0
- package/dist/providers/node-registry-provider.d.ts +11 -0
- package/dist/providers/node-registry-provider.d.ts.map +1 -0
- package/dist/providers/node-registry-provider.js +54 -0
- package/dist/providers/node-registry-provider.js.map +1 -0
- package/dist/style.css +1 -0
- package/dist/types.d.ts +359 -38
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/proxy.d.ts +2 -0
- package/dist/utils/proxy.d.ts.map +1 -0
- package/dist/utils/proxy.js +18 -0
- package/dist/utils/proxy.js.map +1 -0
- package/dist/utils/symbols.d.ts +2 -0
- package/dist/utils/symbols.d.ts.map +1 -0
- package/dist/utils/symbols.js +2 -0
- package/dist/utils/symbols.js.map +1 -0
- package/package.json +1 -1
|
@@ -93,6 +93,1006 @@ function workerSource() {
|
|
|
93
93
|
default: return { x: x + dist, y };
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
+
function sortNodeIds(nodes) {
|
|
97
|
+
return nodes.map(node => node.id);
|
|
98
|
+
}
|
|
99
|
+
function buildGraphIndex(nodes, links) {
|
|
100
|
+
const outgoing = new Map();
|
|
101
|
+
const incoming = new Map();
|
|
102
|
+
const undirected = new Map();
|
|
103
|
+
const indegree = new Map();
|
|
104
|
+
const nodeIds = new Set(nodes.map(node => node.id));
|
|
105
|
+
for (const node of nodes) {
|
|
106
|
+
outgoing.set(node.id, []);
|
|
107
|
+
incoming.set(node.id, []);
|
|
108
|
+
undirected.set(node.id, []);
|
|
109
|
+
indegree.set(node.id, 0);
|
|
110
|
+
}
|
|
111
|
+
for (const link of links) {
|
|
112
|
+
if (!nodeIds.has(link.from) || !nodeIds.has(link.to))
|
|
113
|
+
continue;
|
|
114
|
+
outgoing.get(link.from)?.push(link.to);
|
|
115
|
+
incoming.get(link.to)?.push(link.from);
|
|
116
|
+
undirected.get(link.from)?.push(link.to);
|
|
117
|
+
undirected.get(link.to)?.push(link.from);
|
|
118
|
+
indegree.set(link.to, (indegree.get(link.to) ?? 0) + 1);
|
|
119
|
+
}
|
|
120
|
+
return { outgoing, incoming, undirected, indegree };
|
|
121
|
+
}
|
|
122
|
+
function getLayoutBounds(nodes, positions) {
|
|
123
|
+
if (nodes.length === 0 || positions.length === 0) {
|
|
124
|
+
return { left: 0, top: 0, width: 0, height: 0 };
|
|
125
|
+
}
|
|
126
|
+
const nodeMap = new Map(nodes.map(node => [node.id, node]));
|
|
127
|
+
let left = Infinity;
|
|
128
|
+
let top = Infinity;
|
|
129
|
+
let right = -Infinity;
|
|
130
|
+
let bottom = -Infinity;
|
|
131
|
+
for (const position of positions) {
|
|
132
|
+
const node = nodeMap.get(position.id);
|
|
133
|
+
if (!node)
|
|
134
|
+
continue;
|
|
135
|
+
left = Math.min(left, position.x);
|
|
136
|
+
top = Math.min(top, position.y);
|
|
137
|
+
right = Math.max(right, position.x + node.width);
|
|
138
|
+
bottom = Math.max(bottom, position.y + node.height);
|
|
139
|
+
}
|
|
140
|
+
if (!Number.isFinite(left) || !Number.isFinite(top) || !Number.isFinite(right) || !Number.isFinite(bottom)) {
|
|
141
|
+
return { left: 0, top: 0, width: 0, height: 0 };
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
left,
|
|
145
|
+
top,
|
|
146
|
+
width: Math.max(0, right - left),
|
|
147
|
+
height: Math.max(0, bottom - top),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function getConnectedComponents(nodes, links) {
|
|
151
|
+
const { undirected } = buildGraphIndex(nodes, links);
|
|
152
|
+
const visited = new Set();
|
|
153
|
+
const components = [];
|
|
154
|
+
for (const node of nodes) {
|
|
155
|
+
if (visited.has(node.id))
|
|
156
|
+
continue;
|
|
157
|
+
const queue = [node.id];
|
|
158
|
+
const component = [];
|
|
159
|
+
while (queue.length > 0) {
|
|
160
|
+
const current = queue.shift();
|
|
161
|
+
if (visited.has(current))
|
|
162
|
+
continue;
|
|
163
|
+
visited.add(current);
|
|
164
|
+
component.push(current);
|
|
165
|
+
for (const next of undirected.get(current) ?? []) {
|
|
166
|
+
if (!visited.has(next)) {
|
|
167
|
+
queue.push(next);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
components.push(component);
|
|
172
|
+
}
|
|
173
|
+
return components;
|
|
174
|
+
}
|
|
175
|
+
function packDisconnectedComponents(nodes, links, positions, options = {}) {
|
|
176
|
+
const components = getConnectedComponents(nodes, links);
|
|
177
|
+
if (components.length <= 1)
|
|
178
|
+
return positions;
|
|
179
|
+
const nodeMap = new Map(nodes.map(node => [node.id, node]));
|
|
180
|
+
const positionMap = new Map(positions.map(position => [position.id, position]));
|
|
181
|
+
const gapX = Math.max(options.gapX ?? 140, 60);
|
|
182
|
+
const gapY = Math.max(options.gapY ?? 100, 60);
|
|
183
|
+
const componentEntries = components.map(ids => {
|
|
184
|
+
const componentPositions = ids
|
|
185
|
+
.map(id => positionMap.get(id))
|
|
186
|
+
.filter((position) => Boolean(position));
|
|
187
|
+
const componentNodes = ids
|
|
188
|
+
.map(id => nodeMap.get(id))
|
|
189
|
+
.filter((node) => Boolean(node));
|
|
190
|
+
const bounds = getLayoutBounds(componentNodes, componentPositions);
|
|
191
|
+
return {
|
|
192
|
+
ids,
|
|
193
|
+
bounds,
|
|
194
|
+
area: Math.max(1, bounds.width * bounds.height),
|
|
195
|
+
};
|
|
196
|
+
}).sort((a, b) => b.area - a.area);
|
|
197
|
+
const totalArea = componentEntries.reduce((sum, entry) => sum + entry.area, 0);
|
|
198
|
+
const maxComponentWidth = componentEntries.reduce((max, entry) => Math.max(max, entry.bounds.width), 0);
|
|
199
|
+
const targetRowWidth = Math.max(maxComponentWidth, Math.sqrt(totalArea) * 1.25);
|
|
200
|
+
let cursorX = 0;
|
|
201
|
+
let cursorY = 0;
|
|
202
|
+
let rowHeight = 0;
|
|
203
|
+
const componentOffsetMap = new Map();
|
|
204
|
+
for (const entry of componentEntries) {
|
|
205
|
+
if (cursorX > 0 && cursorX + entry.bounds.width > targetRowWidth) {
|
|
206
|
+
cursorX = 0;
|
|
207
|
+
cursorY += rowHeight + gapY;
|
|
208
|
+
rowHeight = 0;
|
|
209
|
+
}
|
|
210
|
+
const offset = {
|
|
211
|
+
x: cursorX - entry.bounds.left,
|
|
212
|
+
y: cursorY - entry.bounds.top,
|
|
213
|
+
};
|
|
214
|
+
for (const id of entry.ids) {
|
|
215
|
+
componentOffsetMap.set(id, offset);
|
|
216
|
+
}
|
|
217
|
+
cursorX += entry.bounds.width + gapX;
|
|
218
|
+
rowHeight = Math.max(rowHeight, entry.bounds.height);
|
|
219
|
+
}
|
|
220
|
+
return positions.map(position => {
|
|
221
|
+
const offset = componentOffsetMap.get(position.id);
|
|
222
|
+
if (!offset)
|
|
223
|
+
return position;
|
|
224
|
+
return {
|
|
225
|
+
...position,
|
|
226
|
+
x: position.x + offset.x,
|
|
227
|
+
y: position.y + offset.y,
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
function normalizeLayout(nodes, positions, options = {}) {
|
|
232
|
+
const padding = options.padding ?? 48;
|
|
233
|
+
const bounds = getLayoutBounds(nodes, positions);
|
|
234
|
+
const offsetX = options.center
|
|
235
|
+
? options.center.x - (bounds.left + bounds.width / 2)
|
|
236
|
+
: padding - bounds.left;
|
|
237
|
+
const offsetY = options.center
|
|
238
|
+
? options.center.y - (bounds.top + bounds.height / 2)
|
|
239
|
+
: padding - bounds.top;
|
|
240
|
+
const normalized = positions.map(position => ({
|
|
241
|
+
...position,
|
|
242
|
+
x: position.x + offsetX,
|
|
243
|
+
y: position.y + offsetY,
|
|
244
|
+
}));
|
|
245
|
+
return {
|
|
246
|
+
positions: normalized,
|
|
247
|
+
bounds: getLayoutBounds(nodes, normalized),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function flipLayout(nodes, positions, axis) {
|
|
251
|
+
const bounds = getLayoutBounds(nodes, positions);
|
|
252
|
+
const nodeMap = new Map(nodes.map(node => [node.id, node]));
|
|
253
|
+
return positions.map(position => {
|
|
254
|
+
const node = nodeMap.get(position.id);
|
|
255
|
+
if (!node)
|
|
256
|
+
return position;
|
|
257
|
+
if (axis === "x") {
|
|
258
|
+
return {
|
|
259
|
+
...position,
|
|
260
|
+
x: bounds.left + bounds.width - (position.x - bounds.left) - node.width,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
...position,
|
|
265
|
+
y: bounds.top + bounds.height - (position.y - bounds.top) - node.height,
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
function getTopologicalOrder(nodes, links) {
|
|
270
|
+
const { outgoing, indegree } = buildGraphIndex(nodes, links);
|
|
271
|
+
const fallbackOrder = sortNodeIds(nodes);
|
|
272
|
+
const pending = new Map(indegree);
|
|
273
|
+
const queue = fallbackOrder.filter(id => (pending.get(id) ?? 0) === 0);
|
|
274
|
+
const order = [];
|
|
275
|
+
while (queue.length > 0) {
|
|
276
|
+
const id = queue.shift();
|
|
277
|
+
order.push(id);
|
|
278
|
+
for (const next of outgoing.get(id) ?? []) {
|
|
279
|
+
const newDegree = (pending.get(next) ?? 0) - 1;
|
|
280
|
+
pending.set(next, newDegree);
|
|
281
|
+
if (newDegree === 0) {
|
|
282
|
+
queue.push(next);
|
|
283
|
+
queue.sort((a, b) => fallbackOrder.indexOf(a) - fallbackOrder.indexOf(b));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
for (const id of fallbackOrder) {
|
|
288
|
+
if (!order.includes(id))
|
|
289
|
+
order.push(id);
|
|
290
|
+
}
|
|
291
|
+
return order;
|
|
292
|
+
}
|
|
293
|
+
function getHierarchicalDepths(nodes, links, treeMode) {
|
|
294
|
+
const { outgoing, indegree } = buildGraphIndex(nodes, links);
|
|
295
|
+
const order = getTopologicalOrder(nodes, links);
|
|
296
|
+
const depths = new Map();
|
|
297
|
+
const fallbackOrder = sortNodeIds(nodes);
|
|
298
|
+
for (const id of fallbackOrder) {
|
|
299
|
+
depths.set(id, 0);
|
|
300
|
+
}
|
|
301
|
+
if (treeMode) {
|
|
302
|
+
const roots = fallbackOrder.filter(id => (indegree.get(id) ?? 0) === 0);
|
|
303
|
+
const queue = [...roots, ...fallbackOrder.filter(id => !roots.includes(id))];
|
|
304
|
+
const visited = new Set();
|
|
305
|
+
while (queue.length > 0) {
|
|
306
|
+
const current = queue.shift();
|
|
307
|
+
if (visited.has(current))
|
|
308
|
+
continue;
|
|
309
|
+
visited.add(current);
|
|
310
|
+
const baseDepth = depths.get(current) ?? 0;
|
|
311
|
+
for (const next of outgoing.get(current) ?? []) {
|
|
312
|
+
if ((depths.get(next) ?? 0) < baseDepth + 1) {
|
|
313
|
+
depths.set(next, baseDepth + 1);
|
|
314
|
+
}
|
|
315
|
+
queue.push(next);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return depths;
|
|
319
|
+
}
|
|
320
|
+
for (const id of order) {
|
|
321
|
+
const depth = depths.get(id) ?? 0;
|
|
322
|
+
for (const next of outgoing.get(id) ?? []) {
|
|
323
|
+
if ((depths.get(next) ?? 0) < depth + 1) {
|
|
324
|
+
depths.set(next, depth + 1);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return depths;
|
|
329
|
+
}
|
|
330
|
+
function getViewportScales(options = {}) {
|
|
331
|
+
const viewportWidth = options.viewport?.width ?? 0;
|
|
332
|
+
const viewportHeight = options.viewport?.height ?? 0;
|
|
333
|
+
if (viewportWidth <= 0 || viewportHeight <= 0) {
|
|
334
|
+
return { scaleX: 1, scaleY: 1 };
|
|
335
|
+
}
|
|
336
|
+
if (viewportWidth >= viewportHeight) {
|
|
337
|
+
return {
|
|
338
|
+
scaleX: Math.min(viewportWidth / viewportHeight, 1.8),
|
|
339
|
+
scaleY: 1,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
scaleX: 1,
|
|
344
|
+
scaleY: Math.min(viewportHeight / viewportWidth, 1.8),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function getLayoutDirection(options = {}, fallback, respectViewport = true) {
|
|
348
|
+
if (options.direction)
|
|
349
|
+
return options.direction;
|
|
350
|
+
if (!respectViewport)
|
|
351
|
+
return fallback;
|
|
352
|
+
const viewportWidth = options.viewport?.width ?? 0;
|
|
353
|
+
const viewportHeight = options.viewport?.height ?? 0;
|
|
354
|
+
if (viewportWidth <= 0 || viewportHeight <= 0)
|
|
355
|
+
return fallback;
|
|
356
|
+
return viewportWidth >= viewportHeight ? "LR" : "TB";
|
|
357
|
+
}
|
|
358
|
+
function buildLayerOrderIndex(layers) {
|
|
359
|
+
const layerOrderIndex = new Map();
|
|
360
|
+
for (const ids of layers.values()) {
|
|
361
|
+
ids.forEach((id, index) => {
|
|
362
|
+
layerOrderIndex.set(id, index);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
return layerOrderIndex;
|
|
366
|
+
}
|
|
367
|
+
function getNeighborBarycenter(nodeId, depths, neighbors, layerOrderIndex, currentDepth, relation) {
|
|
368
|
+
const positions = (neighbors.get(nodeId) ?? [])
|
|
369
|
+
.filter(neighborId => {
|
|
370
|
+
const neighborDepth = depths.get(neighborId);
|
|
371
|
+
if (neighborDepth === undefined)
|
|
372
|
+
return false;
|
|
373
|
+
return relation === "before"
|
|
374
|
+
? neighborDepth < currentDepth
|
|
375
|
+
: neighborDepth > currentDepth;
|
|
376
|
+
})
|
|
377
|
+
.map(neighborId => layerOrderIndex.get(neighborId))
|
|
378
|
+
.filter((value) => value !== undefined);
|
|
379
|
+
if (positions.length === 0)
|
|
380
|
+
return null;
|
|
381
|
+
return positions.reduce((sum, value) => sum + value, 0) / positions.length;
|
|
382
|
+
}
|
|
383
|
+
function reorderLayerByBarycenter(ids, depths, neighbors, layerOrderIndex, fallbackOrderIndex, currentDepth, relation) {
|
|
384
|
+
return [...ids].sort((leftId, rightId) => {
|
|
385
|
+
const leftBarycenter = getNeighborBarycenter(leftId, depths, neighbors, layerOrderIndex, currentDepth, relation);
|
|
386
|
+
const rightBarycenter = getNeighborBarycenter(rightId, depths, neighbors, layerOrderIndex, currentDepth, relation);
|
|
387
|
+
if (leftBarycenter !== null
|
|
388
|
+
&& rightBarycenter !== null
|
|
389
|
+
&& leftBarycenter !== rightBarycenter) {
|
|
390
|
+
return leftBarycenter - rightBarycenter;
|
|
391
|
+
}
|
|
392
|
+
if (leftBarycenter !== null)
|
|
393
|
+
return -1;
|
|
394
|
+
if (rightBarycenter !== null)
|
|
395
|
+
return 1;
|
|
396
|
+
return (fallbackOrderIndex.get(leftId) ?? 0) - (fallbackOrderIndex.get(rightId) ?? 0);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function optimizeHierarchicalLayerOrder(layers, depths, incoming, outgoing, fallbackOrderIndex) {
|
|
400
|
+
const layerKeys = [...layers.keys()].sort((a, b) => a - b);
|
|
401
|
+
if (layerKeys.length <= 1)
|
|
402
|
+
return layers;
|
|
403
|
+
for (let sweep = 0; sweep < 4; sweep++) {
|
|
404
|
+
let layerOrderIndex = buildLayerOrderIndex(layers);
|
|
405
|
+
for (let index = 1; index < layerKeys.length; index++) {
|
|
406
|
+
const depth = layerKeys[index];
|
|
407
|
+
const ids = layers.get(depth) ?? [];
|
|
408
|
+
layers.set(depth, reorderLayerByBarycenter(ids, depths, incoming, layerOrderIndex, fallbackOrderIndex, depth, "before"));
|
|
409
|
+
layerOrderIndex = buildLayerOrderIndex(layers);
|
|
410
|
+
}
|
|
411
|
+
layerOrderIndex = buildLayerOrderIndex(layers);
|
|
412
|
+
for (let index = layerKeys.length - 2; index >= 0; index--) {
|
|
413
|
+
const depth = layerKeys[index];
|
|
414
|
+
const ids = layers.get(depth) ?? [];
|
|
415
|
+
layers.set(depth, reorderLayerByBarycenter(ids, depths, outgoing, layerOrderIndex, fallbackOrderIndex, depth, "after"));
|
|
416
|
+
layerOrderIndex = buildLayerOrderIndex(layers);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return layers;
|
|
420
|
+
}
|
|
421
|
+
function layoutSequential(nodes, links, options = {}) {
|
|
422
|
+
const gapX = options.gapX ?? 80;
|
|
423
|
+
const gapY = options.gapY ?? 80;
|
|
424
|
+
const order = getTopologicalOrder(nodes, links);
|
|
425
|
+
const nodeMap = new Map(nodes.map(node => [node.id, node]));
|
|
426
|
+
const viewportWidth = options.viewport?.width ?? 0;
|
|
427
|
+
const viewportHeight = options.viewport?.height ?? 0;
|
|
428
|
+
const aspect = viewportWidth > 0 && viewportHeight > 0 ? viewportWidth / viewportHeight : 1;
|
|
429
|
+
const defaultColumns = Math.ceil(Math.sqrt(Math.max(1, nodes.length)) * Math.sqrt(Math.max(aspect, 0.5)));
|
|
430
|
+
const columns = Math.max(1, Math.floor(options.columns ?? defaultColumns));
|
|
431
|
+
let x = 0;
|
|
432
|
+
let y = 0;
|
|
433
|
+
let rowHeight = 0;
|
|
434
|
+
let column = 0;
|
|
435
|
+
const positions = [];
|
|
436
|
+
for (const id of order) {
|
|
437
|
+
const node = nodeMap.get(id);
|
|
438
|
+
if (!node)
|
|
439
|
+
continue;
|
|
440
|
+
positions.push({ id, x, y, z: node.z });
|
|
441
|
+
rowHeight = Math.max(rowHeight, node.height);
|
|
442
|
+
column += 1;
|
|
443
|
+
if (column >= columns) {
|
|
444
|
+
column = 0;
|
|
445
|
+
x = 0;
|
|
446
|
+
y += rowHeight + gapY;
|
|
447
|
+
rowHeight = 0;
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
x += node.width + gapX;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return normalizeLayout(nodes, packDisconnectedComponents(nodes, links, positions, options), options);
|
|
454
|
+
}
|
|
455
|
+
function layoutHierarchical(nodes, links, options = {}, treeMode = false, respectViewport = false) {
|
|
456
|
+
const { outgoing, incoming } = buildGraphIndex(nodes, links);
|
|
457
|
+
const gapX = options.gapX ?? 100;
|
|
458
|
+
const gapY = options.gapY ?? 80;
|
|
459
|
+
const direction = getLayoutDirection(options, treeMode ? "TB" : "LR", respectViewport);
|
|
460
|
+
const depths = getHierarchicalDepths(nodes, links, treeMode);
|
|
461
|
+
const order = sortNodeIds(nodes);
|
|
462
|
+
const fallbackOrderIndex = new Map(order.map((id, index) => [id, index]));
|
|
463
|
+
const nodeMap = new Map(nodes.map(node => [node.id, node]));
|
|
464
|
+
const layers = new Map();
|
|
465
|
+
for (const id of order) {
|
|
466
|
+
const depth = depths.get(id) ?? 0;
|
|
467
|
+
const group = layers.get(depth) ?? [];
|
|
468
|
+
group.push(id);
|
|
469
|
+
layers.set(depth, group);
|
|
470
|
+
}
|
|
471
|
+
optimizeHierarchicalLayerOrder(layers, depths, incoming, outgoing, fallbackOrderIndex);
|
|
472
|
+
const layerKeys = [...layers.keys()].sort((a, b) => a - b);
|
|
473
|
+
const positions = [];
|
|
474
|
+
let primaryCursor = 0;
|
|
475
|
+
for (const layerKey of layerKeys) {
|
|
476
|
+
const ids = layers.get(layerKey) ?? [];
|
|
477
|
+
let secondaryCursor = 0;
|
|
478
|
+
let primarySize = 0;
|
|
479
|
+
for (const id of ids) {
|
|
480
|
+
const node = nodeMap.get(id);
|
|
481
|
+
if (!node)
|
|
482
|
+
continue;
|
|
483
|
+
if (direction === "LR" || direction === "RL") {
|
|
484
|
+
positions.push({ id, x: primaryCursor, y: secondaryCursor, z: node.z });
|
|
485
|
+
secondaryCursor += node.height + gapY;
|
|
486
|
+
primarySize = Math.max(primarySize, node.width);
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
positions.push({ id, x: secondaryCursor, y: primaryCursor, z: node.z });
|
|
490
|
+
secondaryCursor += node.width + gapX;
|
|
491
|
+
primarySize = Math.max(primarySize, node.height);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
primaryCursor += primarySize + (direction === "LR" || direction === "RL" ? gapX : gapY);
|
|
495
|
+
}
|
|
496
|
+
let adjusted = positions;
|
|
497
|
+
if (direction === "RL")
|
|
498
|
+
adjusted = flipLayout(nodes, adjusted, "x");
|
|
499
|
+
if (direction === "BT")
|
|
500
|
+
adjusted = flipLayout(nodes, adjusted, "y");
|
|
501
|
+
return normalizeLayout(nodes, packDisconnectedComponents(nodes, links, adjusted, options), options);
|
|
502
|
+
}
|
|
503
|
+
function layoutTree(nodes, links, options = {}) {
|
|
504
|
+
const { outgoing, incoming, indegree } = buildGraphIndex(nodes, links);
|
|
505
|
+
const gapX = options.gapX ?? 100;
|
|
506
|
+
const gapY = options.gapY ?? 80;
|
|
507
|
+
const direction = getLayoutDirection(options, "TB", false);
|
|
508
|
+
const depths = getHierarchicalDepths(nodes, links, true);
|
|
509
|
+
const order = sortNodeIds(nodes);
|
|
510
|
+
const fallbackOrderIndex = new Map(order.map((id, index) => [id, index]));
|
|
511
|
+
const nodeMap = new Map(nodes.map(node => [node.id, node]));
|
|
512
|
+
const layers = new Map();
|
|
513
|
+
for (const id of order) {
|
|
514
|
+
const depth = depths.get(id) ?? 0;
|
|
515
|
+
const group = layers.get(depth) ?? [];
|
|
516
|
+
group.push(id);
|
|
517
|
+
layers.set(depth, group);
|
|
518
|
+
}
|
|
519
|
+
optimizeHierarchicalLayerOrder(layers, depths, incoming, outgoing, fallbackOrderIndex);
|
|
520
|
+
const layerOrderIndex = buildLayerOrderIndex(layers);
|
|
521
|
+
const roots = order.filter(id => (indegree.get(id) ?? 0) === 0);
|
|
522
|
+
const rootSet = new Set(roots);
|
|
523
|
+
const forestRoots = [];
|
|
524
|
+
const assigned = new Set();
|
|
525
|
+
const children = new Map();
|
|
526
|
+
const sortByTreeOrder = (leftId, rightId) => {
|
|
527
|
+
const leftDepth = depths.get(leftId) ?? 0;
|
|
528
|
+
const rightDepth = depths.get(rightId) ?? 0;
|
|
529
|
+
if (leftDepth !== rightDepth)
|
|
530
|
+
return leftDepth - rightDepth;
|
|
531
|
+
return (layerOrderIndex.get(leftId) ?? fallbackOrderIndex.get(leftId) ?? 0)
|
|
532
|
+
- (layerOrderIndex.get(rightId) ?? fallbackOrderIndex.get(rightId) ?? 0);
|
|
533
|
+
};
|
|
534
|
+
const buildTree = (nodeId) => {
|
|
535
|
+
const currentDepth = depths.get(nodeId) ?? 0;
|
|
536
|
+
const childIds = (outgoing.get(nodeId) ?? [])
|
|
537
|
+
.filter(childId => childId !== nodeId && (depths.get(childId) ?? currentDepth) > currentDepth && !assigned.has(childId))
|
|
538
|
+
.sort(sortByTreeOrder);
|
|
539
|
+
children.set(nodeId, childIds);
|
|
540
|
+
for (const childId of childIds) {
|
|
541
|
+
assigned.add(childId);
|
|
542
|
+
buildTree(childId);
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
for (const rootId of [...roots, ...order.filter(id => !rootSet.has(id))]) {
|
|
546
|
+
if (assigned.has(rootId))
|
|
547
|
+
continue;
|
|
548
|
+
assigned.add(rootId);
|
|
549
|
+
forestRoots.push(rootId);
|
|
550
|
+
buildTree(rootId);
|
|
551
|
+
}
|
|
552
|
+
const layerKeys = [...layers.keys()].sort((a, b) => a - b);
|
|
553
|
+
const primaryOffsetByDepth = new Map();
|
|
554
|
+
let primaryCursor = 0;
|
|
555
|
+
for (const layerKey of layerKeys) {
|
|
556
|
+
const ids = layers.get(layerKey) ?? [];
|
|
557
|
+
const primarySize = ids.reduce((max, id) => {
|
|
558
|
+
const node = nodeMap.get(id);
|
|
559
|
+
if (!node)
|
|
560
|
+
return max;
|
|
561
|
+
return Math.max(max, direction === "LR" || direction === "RL" ? node.width : node.height);
|
|
562
|
+
}, 0);
|
|
563
|
+
primaryOffsetByDepth.set(layerKey, primaryCursor);
|
|
564
|
+
primaryCursor += primarySize + (direction === "LR" || direction === "RL" ? gapX : gapY);
|
|
565
|
+
}
|
|
566
|
+
const secondaryGap = direction === "LR" || direction === "RL" ? gapY : gapX;
|
|
567
|
+
const secondarySize = (nodeId) => {
|
|
568
|
+
const node = nodeMap.get(nodeId);
|
|
569
|
+
if (!node)
|
|
570
|
+
return 0;
|
|
571
|
+
return direction === "LR" || direction === "RL" ? node.height : node.width;
|
|
572
|
+
};
|
|
573
|
+
const secondaryPositionById = new Map();
|
|
574
|
+
let secondaryCursor = 0;
|
|
575
|
+
const placeSubtree = (nodeId) => {
|
|
576
|
+
const node = nodeMap.get(nodeId);
|
|
577
|
+
if (!node) {
|
|
578
|
+
return { start: secondaryCursor, end: secondaryCursor };
|
|
579
|
+
}
|
|
580
|
+
const nodeSecondarySize = secondarySize(nodeId);
|
|
581
|
+
const childIds = children.get(nodeId) ?? [];
|
|
582
|
+
if (childIds.length === 0) {
|
|
583
|
+
const start = secondaryCursor;
|
|
584
|
+
secondaryPositionById.set(nodeId, start);
|
|
585
|
+
secondaryCursor += nodeSecondarySize + secondaryGap;
|
|
586
|
+
return { start, end: start + nodeSecondarySize };
|
|
587
|
+
}
|
|
588
|
+
let start = Infinity;
|
|
589
|
+
let end = -Infinity;
|
|
590
|
+
for (const childId of childIds) {
|
|
591
|
+
const bounds = placeSubtree(childId);
|
|
592
|
+
start = Math.min(start, bounds.start);
|
|
593
|
+
end = Math.max(end, bounds.end);
|
|
594
|
+
}
|
|
595
|
+
const centeredStart = start + (end - start - nodeSecondarySize) / 2;
|
|
596
|
+
secondaryPositionById.set(nodeId, centeredStart);
|
|
597
|
+
return {
|
|
598
|
+
start: Math.min(start, centeredStart),
|
|
599
|
+
end: Math.max(end, centeredStart + nodeSecondarySize),
|
|
600
|
+
};
|
|
601
|
+
};
|
|
602
|
+
for (const rootId of forestRoots) {
|
|
603
|
+
placeSubtree(rootId);
|
|
604
|
+
secondaryCursor += secondaryGap;
|
|
605
|
+
}
|
|
606
|
+
const positions = order
|
|
607
|
+
.map(id => {
|
|
608
|
+
const node = nodeMap.get(id);
|
|
609
|
+
if (!node)
|
|
610
|
+
return null;
|
|
611
|
+
const depth = depths.get(id) ?? 0;
|
|
612
|
+
const primary = primaryOffsetByDepth.get(depth) ?? 0;
|
|
613
|
+
const secondary = secondaryPositionById.get(id) ?? 0;
|
|
614
|
+
if (direction === "LR" || direction === "RL") {
|
|
615
|
+
return { id, x: primary, y: secondary, z: node.z };
|
|
616
|
+
}
|
|
617
|
+
return { id, x: secondary, y: primary, z: node.z };
|
|
618
|
+
})
|
|
619
|
+
.filter((position) => Boolean(position));
|
|
620
|
+
let adjusted = positions;
|
|
621
|
+
if (direction === "RL")
|
|
622
|
+
adjusted = flipLayout(nodes, adjusted, "x");
|
|
623
|
+
if (direction === "BT")
|
|
624
|
+
adjusted = flipLayout(nodes, adjusted, "y");
|
|
625
|
+
return normalizeLayout(nodes, packDisconnectedComponents(nodes, links, adjusted, options), options);
|
|
626
|
+
}
|
|
627
|
+
function layoutRadial(nodes, links, options = {}) {
|
|
628
|
+
const { outgoing, incoming, indegree, undirected } = buildGraphIndex(nodes, links);
|
|
629
|
+
const order = sortNodeIds(nodes);
|
|
630
|
+
const orderIndex = new Map(order.map((id, index) => [id, index]));
|
|
631
|
+
const nodeMap = new Map(nodes.map(node => [node.id, node]));
|
|
632
|
+
const { scaleX, scaleY } = getViewportScales(options);
|
|
633
|
+
const radiusStep = options.radiusStep ?? Math.max((options.gapX ?? 96) * 0.8, (options.gapY ?? 80) * 0.8, 72);
|
|
634
|
+
const angularGap = Math.max((options.gapX ?? 96) * 0.2, 16);
|
|
635
|
+
const positions = [];
|
|
636
|
+
const adjacency = new Map();
|
|
637
|
+
for (const node of nodes) {
|
|
638
|
+
const combined = [
|
|
639
|
+
...(outgoing.get(node.id) ?? []),
|
|
640
|
+
...(incoming.get(node.id) ?? []),
|
|
641
|
+
...(undirected.get(node.id) ?? []),
|
|
642
|
+
];
|
|
643
|
+
adjacency.set(node.id, [...new Set(combined)].sort((a, b) => (orderIndex.get(a) ?? 0) - (orderIndex.get(b) ?? 0)));
|
|
644
|
+
}
|
|
645
|
+
const components = getConnectedComponents(nodes, links);
|
|
646
|
+
for (const component of components) {
|
|
647
|
+
const componentSet = new Set(component);
|
|
648
|
+
const root = component.find(id => (indegree.get(id) ?? 0) === 0) ?? component[0];
|
|
649
|
+
const children = new Map();
|
|
650
|
+
const depth = new Map();
|
|
651
|
+
const visited = new Set();
|
|
652
|
+
const buildTree = (nodeId, parentId, currentDepth = 0) => {
|
|
653
|
+
visited.add(nodeId);
|
|
654
|
+
depth.set(nodeId, currentDepth);
|
|
655
|
+
const childIds = (adjacency.get(nodeId) ?? [])
|
|
656
|
+
.filter(next => componentSet.has(next) && next !== parentId && !visited.has(next));
|
|
657
|
+
children.set(nodeId, childIds);
|
|
658
|
+
for (const childId of childIds) {
|
|
659
|
+
buildTree(childId, nodeId, currentDepth + 1);
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
buildTree(root);
|
|
663
|
+
const subtreeWeight = new Map();
|
|
664
|
+
const calculateWeight = (nodeId) => {
|
|
665
|
+
const childIds = children.get(nodeId) ?? [];
|
|
666
|
+
if (childIds.length === 0) {
|
|
667
|
+
subtreeWeight.set(nodeId, 1);
|
|
668
|
+
return 1;
|
|
669
|
+
}
|
|
670
|
+
const weight = childIds.reduce((sum, childId) => sum + calculateWeight(childId), 0);
|
|
671
|
+
subtreeWeight.set(nodeId, Math.max(1, weight));
|
|
672
|
+
return Math.max(1, weight);
|
|
673
|
+
};
|
|
674
|
+
calculateWeight(root);
|
|
675
|
+
const angle = new Map();
|
|
676
|
+
const assignAngles = (nodeId, startAngle, endAngle) => {
|
|
677
|
+
angle.set(nodeId, (startAngle + endAngle) / 2);
|
|
678
|
+
const childIds = children.get(nodeId) ?? [];
|
|
679
|
+
if (childIds.length === 0)
|
|
680
|
+
return;
|
|
681
|
+
const totalWeight = childIds.reduce((sum, childId) => sum + (subtreeWeight.get(childId) ?? 1), 0);
|
|
682
|
+
let cursor = startAngle;
|
|
683
|
+
for (const childId of childIds) {
|
|
684
|
+
const span = ((endAngle - startAngle) * (subtreeWeight.get(childId) ?? 1)) / Math.max(totalWeight, 1);
|
|
685
|
+
assignAngles(childId, cursor, cursor + span);
|
|
686
|
+
cursor += span;
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
assignAngles(root, -Math.PI / 2, (Math.PI * 3) / 2);
|
|
690
|
+
const layers = new Map();
|
|
691
|
+
for (const nodeId of component) {
|
|
692
|
+
const layer = depth.get(nodeId) ?? 0;
|
|
693
|
+
const group = layers.get(layer) ?? [];
|
|
694
|
+
group.push(nodeId);
|
|
695
|
+
layers.set(layer, group);
|
|
696
|
+
}
|
|
697
|
+
const layerKeys = [...layers.keys()].sort((a, b) => a - b);
|
|
698
|
+
const radiusByLayer = new Map();
|
|
699
|
+
let outerRadius = 0;
|
|
700
|
+
for (const layer of layerKeys) {
|
|
701
|
+
const layerIds = layers.get(layer) ?? [];
|
|
702
|
+
if (layer === 0) {
|
|
703
|
+
radiusByLayer.set(layer, 0);
|
|
704
|
+
const rootNode = nodeMap.get(root);
|
|
705
|
+
outerRadius = rootNode
|
|
706
|
+
? Math.sqrt(rootNode.width * rootNode.width + rootNode.height * rootNode.height) / 2
|
|
707
|
+
: 0;
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
const sortedLayerIds = [...layerIds].sort((a, b) => (angle.get(a) ?? 0) - (angle.get(b) ?? 0));
|
|
711
|
+
let layerRadius = outerRadius + radiusStep;
|
|
712
|
+
if (sortedLayerIds.length > 1) {
|
|
713
|
+
for (let index = 0; index < sortedLayerIds.length; index++) {
|
|
714
|
+
const currentId = sortedLayerIds[index];
|
|
715
|
+
const nextId = sortedLayerIds[(index + 1) % sortedLayerIds.length];
|
|
716
|
+
const currentNode = nodeMap.get(currentId);
|
|
717
|
+
const nextNode = nodeMap.get(nextId);
|
|
718
|
+
if (!currentNode || !nextNode)
|
|
719
|
+
continue;
|
|
720
|
+
const currentAngle = angle.get(currentId) ?? 0;
|
|
721
|
+
const nextAngle = angle.get(nextId) ?? 0;
|
|
722
|
+
const angleDeltaRaw = nextAngle > currentAngle
|
|
723
|
+
? nextAngle - currentAngle
|
|
724
|
+
: nextAngle + Math.PI * 2 - currentAngle;
|
|
725
|
+
const angleDelta = Math.max(angleDeltaRaw, 0.2);
|
|
726
|
+
const requiredArc = (Math.sqrt(currentNode.width * currentNode.width + currentNode.height * currentNode.height) / 2 +
|
|
727
|
+
Math.sqrt(nextNode.width * nextNode.width + nextNode.height * nextNode.height) / 2 +
|
|
728
|
+
angularGap);
|
|
729
|
+
layerRadius = Math.max(layerRadius, requiredArc / angleDelta);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
radiusByLayer.set(layer, layerRadius);
|
|
733
|
+
const maxHalfDiagonal = layerIds.reduce((max, nodeId) => {
|
|
734
|
+
const node = nodeMap.get(nodeId);
|
|
735
|
+
if (!node)
|
|
736
|
+
return max;
|
|
737
|
+
return Math.max(max, Math.sqrt(node.width * node.width + node.height * node.height) / 2);
|
|
738
|
+
}, 0);
|
|
739
|
+
outerRadius = layerRadius + maxHalfDiagonal;
|
|
740
|
+
}
|
|
741
|
+
for (const nodeId of component) {
|
|
742
|
+
const node = nodeMap.get(nodeId);
|
|
743
|
+
if (!node)
|
|
744
|
+
continue;
|
|
745
|
+
const currentDepth = depth.get(nodeId) ?? 0;
|
|
746
|
+
const radius = radiusByLayer.get(currentDepth) ?? 0;
|
|
747
|
+
const theta = angle.get(nodeId) ?? 0;
|
|
748
|
+
positions.push({
|
|
749
|
+
id: nodeId,
|
|
750
|
+
x: Math.cos(theta) * radius * scaleX - node.width / 2,
|
|
751
|
+
y: Math.sin(theta) * radius * scaleY - node.height / 2,
|
|
752
|
+
z: node.z,
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return normalizeLayout(nodes, packDisconnectedComponents(nodes, links, positions, options), options);
|
|
757
|
+
}
|
|
758
|
+
function separateOverlappingNodes(nodes, centers, gapX, gapY) {
|
|
759
|
+
for (let iteration = 0; iteration < 12; iteration++) {
|
|
760
|
+
let moved = false;
|
|
761
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
762
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
763
|
+
const minDx = (nodes[i].width + nodes[j].width) / 2 + gapX * 0.35;
|
|
764
|
+
const minDy = (nodes[i].height + nodes[j].height) / 2 + gapY * 0.35;
|
|
765
|
+
const dx = centers[j].x - centers[i].x;
|
|
766
|
+
const dy = centers[j].y - centers[i].y;
|
|
767
|
+
const overlapX = minDx - Math.abs(dx);
|
|
768
|
+
const overlapY = minDy - Math.abs(dy);
|
|
769
|
+
if (overlapX <= 0 || overlapY <= 0)
|
|
770
|
+
continue;
|
|
771
|
+
moved = true;
|
|
772
|
+
if (overlapX < overlapY) {
|
|
773
|
+
const shift = overlapX / 2 + 1;
|
|
774
|
+
const sign = dx >= 0 ? 1 : -1;
|
|
775
|
+
centers[i].x -= sign * shift;
|
|
776
|
+
centers[j].x += sign * shift;
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
const shift = overlapY / 2 + 1;
|
|
780
|
+
const sign = dy >= 0 ? 1 : -1;
|
|
781
|
+
centers[i].y -= sign * shift;
|
|
782
|
+
centers[j].y += sign * shift;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (!moved) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function getMidpoint(from, to) {
|
|
792
|
+
return {
|
|
793
|
+
x: (from.x + to.x) / 2,
|
|
794
|
+
y: (from.y + to.y) / 2,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
function getNormalizedVector(x, y) {
|
|
798
|
+
const length = Math.sqrt(x * x + y * y) || 1;
|
|
799
|
+
return { x: x / length, y: y / length };
|
|
800
|
+
}
|
|
801
|
+
function segmentsIntersect(leftFrom, leftTo, rightFrom, rightTo) {
|
|
802
|
+
const epsilon = 0.001;
|
|
803
|
+
const leftToRightFrom = (leftTo.x - leftFrom.x) * (rightFrom.y - leftFrom.y) - (leftTo.y - leftFrom.y) * (rightFrom.x - leftFrom.x);
|
|
804
|
+
const leftToRightTo = (leftTo.x - leftFrom.x) * (rightTo.y - leftFrom.y) - (leftTo.y - leftFrom.y) * (rightTo.x - leftFrom.x);
|
|
805
|
+
const rightToLeftFrom = (rightTo.x - rightFrom.x) * (leftFrom.y - rightFrom.y) - (rightTo.y - rightFrom.y) * (leftFrom.x - rightFrom.x);
|
|
806
|
+
const rightToLeftTo = (rightTo.x - rightFrom.x) * (leftTo.y - rightFrom.y) - (rightTo.y - rightFrom.y) * (leftTo.x - rightFrom.x);
|
|
807
|
+
return (((leftToRightFrom > epsilon && leftToRightTo < -epsilon) || (leftToRightFrom < -epsilon && leftToRightTo > epsilon))
|
|
808
|
+
&& ((rightToLeftFrom > epsilon && rightToLeftTo < -epsilon) || (rightToLeftFrom < -epsilon && rightToLeftTo > epsilon)));
|
|
809
|
+
}
|
|
810
|
+
function applyLinkCrossingForces(edges, centers, displacement, strength, seedCenters) {
|
|
811
|
+
if (strength <= 0 || edges.length < 2)
|
|
812
|
+
return 0;
|
|
813
|
+
let crossingCount = 0;
|
|
814
|
+
for (let leftIndex = 0; leftIndex < edges.length; leftIndex++) {
|
|
815
|
+
const leftEdge = edges[leftIndex];
|
|
816
|
+
const leftFrom = centers[leftEdge.fromIndex];
|
|
817
|
+
const leftTo = centers[leftEdge.toIndex];
|
|
818
|
+
for (let rightIndex = leftIndex + 1; rightIndex < edges.length; rightIndex++) {
|
|
819
|
+
const rightEdge = edges[rightIndex];
|
|
820
|
+
if (leftEdge.fromIndex === rightEdge.fromIndex
|
|
821
|
+
|| leftEdge.fromIndex === rightEdge.toIndex
|
|
822
|
+
|| leftEdge.toIndex === rightEdge.fromIndex
|
|
823
|
+
|| leftEdge.toIndex === rightEdge.toIndex) {
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
const rightFrom = centers[rightEdge.fromIndex];
|
|
827
|
+
const rightTo = centers[rightEdge.toIndex];
|
|
828
|
+
if (!segmentsIntersect(leftFrom, leftTo, rightFrom, rightTo))
|
|
829
|
+
continue;
|
|
830
|
+
crossingCount += 1;
|
|
831
|
+
const leftMid = getMidpoint(leftFrom, leftTo);
|
|
832
|
+
const rightMid = getMidpoint(rightFrom, rightTo);
|
|
833
|
+
let separationX = leftMid.x - rightMid.x;
|
|
834
|
+
let separationY = leftMid.y - rightMid.y;
|
|
835
|
+
const leftSeedFrom = seedCenters?.[leftEdge.fromIndex];
|
|
836
|
+
const leftSeedTo = seedCenters?.[leftEdge.toIndex];
|
|
837
|
+
const rightSeedFrom = seedCenters?.[rightEdge.fromIndex];
|
|
838
|
+
const rightSeedTo = seedCenters?.[rightEdge.toIndex];
|
|
839
|
+
if (leftSeedFrom && leftSeedTo && rightSeedFrom && rightSeedTo) {
|
|
840
|
+
const leftSeedMid = getMidpoint(leftSeedFrom, leftSeedTo);
|
|
841
|
+
const rightSeedMid = getMidpoint(rightSeedFrom, rightSeedTo);
|
|
842
|
+
separationX = leftSeedMid.x - rightSeedMid.x;
|
|
843
|
+
separationY = leftSeedMid.y - rightSeedMid.y;
|
|
844
|
+
}
|
|
845
|
+
if (Math.abs(separationX) < 0.001 && Math.abs(separationY) < 0.001) {
|
|
846
|
+
separationX = (leftFrom.x + leftTo.x) - (rightFrom.x + rightTo.x);
|
|
847
|
+
separationY = (leftFrom.y + leftTo.y) - (rightFrom.y + rightTo.y);
|
|
848
|
+
}
|
|
849
|
+
const direction = getNormalizedVector(separationX, separationY);
|
|
850
|
+
const pushX = direction.x * strength;
|
|
851
|
+
const pushY = direction.y * strength;
|
|
852
|
+
displacement[leftEdge.fromIndex].x += pushX * 0.5;
|
|
853
|
+
displacement[leftEdge.fromIndex].y += pushY * 0.5;
|
|
854
|
+
displacement[leftEdge.toIndex].x += pushX * 0.5;
|
|
855
|
+
displacement[leftEdge.toIndex].y += pushY * 0.5;
|
|
856
|
+
displacement[rightEdge.fromIndex].x -= pushX * 0.5;
|
|
857
|
+
displacement[rightEdge.fromIndex].y -= pushY * 0.5;
|
|
858
|
+
displacement[rightEdge.toIndex].x -= pushX * 0.5;
|
|
859
|
+
displacement[rightEdge.toIndex].y -= pushY * 0.5;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return crossingCount;
|
|
863
|
+
}
|
|
864
|
+
function resolveLinkCrossings(nodes, edges, centers, spacing, gapX, gapY, seedCenters) {
|
|
865
|
+
if (edges.length < 2)
|
|
866
|
+
return;
|
|
867
|
+
for (let iteration = 0; iteration < 20; iteration++) {
|
|
868
|
+
const displacement = nodes.map(() => ({ x: 0, y: 0 }));
|
|
869
|
+
const crossingCount = applyLinkCrossingForces(edges, centers, displacement, Math.max(spacing * 0.12, 6), seedCenters);
|
|
870
|
+
if (crossingCount === 0) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
for (let index = 0; index < centers.length; index++) {
|
|
874
|
+
centers[index].x += displacement[index].x;
|
|
875
|
+
centers[index].y += displacement[index].y;
|
|
876
|
+
}
|
|
877
|
+
separateOverlappingNodes(nodes, centers, gapX, gapY);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
function runForceLayout(nodes, links, options = {}, directedBias = 0, gravity = 0.015, seedPositions) {
|
|
881
|
+
const gapX = options.gapX ?? 140;
|
|
882
|
+
const gapY = options.gapY ?? 100;
|
|
883
|
+
const averageNodeSize = nodes.reduce((sum, node) => sum + Math.max(node.width, node.height), 0) / Math.max(nodes.length, 1);
|
|
884
|
+
const baseSpacing = Math.max(gapX, gapY, averageNodeSize * 0.85, 48);
|
|
885
|
+
const overlapGapX = gapX > 0 ? gapX : Math.min(baseSpacing * 0.25, 24);
|
|
886
|
+
const overlapGapY = gapY > 0 ? gapY : Math.min(baseSpacing * 0.25, 24);
|
|
887
|
+
const iterations = Math.max(1, Math.floor(options.iterations ?? 220));
|
|
888
|
+
const nodeMap = new Map(nodes.map((node, index) => [node.id, { node, index }]));
|
|
889
|
+
const { outgoing, incoming } = buildGraphIndex(nodes, links);
|
|
890
|
+
const nodeDegrees = nodes.map(node => {
|
|
891
|
+
const degree = new Set([
|
|
892
|
+
...(outgoing.get(node.id) ?? []),
|
|
893
|
+
...(incoming.get(node.id) ?? []),
|
|
894
|
+
]);
|
|
895
|
+
return degree.size;
|
|
896
|
+
});
|
|
897
|
+
const edges = links
|
|
898
|
+
.map(link => {
|
|
899
|
+
const fromEntry = nodeMap.get(link.from);
|
|
900
|
+
const toEntry = nodeMap.get(link.to);
|
|
901
|
+
if (!fromEntry || !toEntry)
|
|
902
|
+
return null;
|
|
903
|
+
return {
|
|
904
|
+
fromIndex: fromEntry.index,
|
|
905
|
+
toIndex: toEntry.index,
|
|
906
|
+
};
|
|
907
|
+
})
|
|
908
|
+
.filter((edge) => Boolean(edge));
|
|
909
|
+
const nodeRadii = nodes.map(node => Math.sqrt(node.width * node.width + node.height * node.height) / 2);
|
|
910
|
+
const seedPositionMap = new Map((seedPositions ?? []).map(position => [position.id, position]));
|
|
911
|
+
const seedCenters = nodes.map(node => {
|
|
912
|
+
const seed = seedPositionMap.get(node.id);
|
|
913
|
+
if (!seed)
|
|
914
|
+
return null;
|
|
915
|
+
return {
|
|
916
|
+
x: seed.x + node.width / 2,
|
|
917
|
+
y: seed.y + node.height / 2,
|
|
918
|
+
};
|
|
919
|
+
});
|
|
920
|
+
const rawPositions = nodes.map(node => {
|
|
921
|
+
const seed = seedPositionMap.get(node.id);
|
|
922
|
+
return {
|
|
923
|
+
id: node.id,
|
|
924
|
+
x: seed?.x ?? node.x,
|
|
925
|
+
y: seed?.y ?? node.y,
|
|
926
|
+
z: seed?.z ?? node.z,
|
|
927
|
+
};
|
|
928
|
+
});
|
|
929
|
+
const rawBounds = getLayoutBounds(nodes, rawPositions);
|
|
930
|
+
const rawCenterX = rawBounds.left + rawBounds.width / 2;
|
|
931
|
+
const rawCenterY = rawBounds.top + rawBounds.height / 2;
|
|
932
|
+
const { scaleX, scaleY } = getViewportScales(options);
|
|
933
|
+
const desiredSpanX = baseSpacing * Math.max(2, Math.sqrt(Math.max(1, nodes.length))) * scaleX;
|
|
934
|
+
const desiredSpanY = baseSpacing * Math.max(2, Math.sqrt(Math.max(1, nodes.length))) * scaleY;
|
|
935
|
+
const currentSpanX = Math.max(rawBounds.width, 1);
|
|
936
|
+
const currentSpanY = Math.max(rawBounds.height, 1);
|
|
937
|
+
const compactScaleX = Math.min(1, desiredSpanX / currentSpanX);
|
|
938
|
+
const compactScaleY = Math.min(1, desiredSpanY / currentSpanY);
|
|
939
|
+
const useCircularSeed = rawBounds.width < 4 && rawBounds.height < 4;
|
|
940
|
+
const centers = nodes.map((node, index) => {
|
|
941
|
+
const rawPosition = rawPositions[index] ?? { x: node.x, y: node.y };
|
|
942
|
+
if (useCircularSeed) {
|
|
943
|
+
const seedRadius = baseSpacing * 0.8;
|
|
944
|
+
const angle = (Math.PI * 2 * index) / Math.max(1, nodes.length);
|
|
945
|
+
return {
|
|
946
|
+
x: Math.cos(angle) * seedRadius * scaleX,
|
|
947
|
+
y: Math.sin(angle) * seedRadius * scaleY,
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
return {
|
|
951
|
+
x: (rawPosition.x + node.width / 2 - rawCenterX) * compactScaleX,
|
|
952
|
+
y: (rawPosition.y + node.height / 2 - rawCenterY) * compactScaleY,
|
|
953
|
+
};
|
|
954
|
+
});
|
|
955
|
+
const totalNodeArea = nodes.reduce((sum, node) => sum + node.width * node.height, 0);
|
|
956
|
+
const area = Math.max(totalNodeArea + baseSpacing * baseSpacing * Math.max(1, nodes.length) * 0.35, 1);
|
|
957
|
+
const k = Math.sqrt(area / Math.max(1, nodes.length));
|
|
958
|
+
let temperature = Math.max(baseSpacing, 60);
|
|
959
|
+
for (let iteration = 0; iteration < iterations; iteration++) {
|
|
960
|
+
const displacement = nodes.map(() => ({ x: 0, y: 0 }));
|
|
961
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
962
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
963
|
+
let dx = centers[i].x - centers[j].x;
|
|
964
|
+
let dy = centers[i].y - centers[j].y;
|
|
965
|
+
let distance = Math.sqrt(dx * dx + dy * dy);
|
|
966
|
+
if (distance < 0.001) {
|
|
967
|
+
dx = (Math.random() - 0.5) * 0.01;
|
|
968
|
+
dy = (Math.random() - 0.5) * 0.01;
|
|
969
|
+
distance = Math.sqrt(dx * dx + dy * dy);
|
|
970
|
+
}
|
|
971
|
+
const minimumDistance = nodeRadii[i] + nodeRadii[j] + baseSpacing * 0.18;
|
|
972
|
+
const safeDistance = Math.max(distance - nodeRadii[i] - nodeRadii[j], 1);
|
|
973
|
+
const force = (k * k) / (safeDistance * 1.5);
|
|
974
|
+
const fx = (dx / distance) * force;
|
|
975
|
+
const fy = (dy / distance) * force;
|
|
976
|
+
displacement[i].x += fx;
|
|
977
|
+
displacement[i].y += fy;
|
|
978
|
+
displacement[j].x -= fx;
|
|
979
|
+
displacement[j].y -= fy;
|
|
980
|
+
if (distance < minimumDistance) {
|
|
981
|
+
const collisionForce = (minimumDistance - distance) * 1.1;
|
|
982
|
+
displacement[i].x += (dx / distance) * collisionForce;
|
|
983
|
+
displacement[i].y += (dy / distance) * collisionForce;
|
|
984
|
+
displacement[j].x -= (dx / distance) * collisionForce;
|
|
985
|
+
displacement[j].y -= (dy / distance) * collisionForce;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
for (const link of links) {
|
|
990
|
+
const fromEntry = nodeMap.get(link.from);
|
|
991
|
+
const toEntry = nodeMap.get(link.to);
|
|
992
|
+
if (!fromEntry || !toEntry)
|
|
993
|
+
continue;
|
|
994
|
+
const i = fromEntry.index;
|
|
995
|
+
const j = toEntry.index;
|
|
996
|
+
let dx = centers[i].x - centers[j].x;
|
|
997
|
+
let dy = centers[i].y - centers[j].y;
|
|
998
|
+
let distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
999
|
+
const springLength = nodeRadii[i] + nodeRadii[j] + baseSpacing * 0.28;
|
|
1000
|
+
const force = (distance - springLength) * 0.2;
|
|
1001
|
+
const fx = (dx / distance) * force;
|
|
1002
|
+
const fy = (dy / distance) * force;
|
|
1003
|
+
displacement[i].x -= fx;
|
|
1004
|
+
displacement[i].y -= fy;
|
|
1005
|
+
displacement[j].x += fx;
|
|
1006
|
+
displacement[j].y += fy;
|
|
1007
|
+
if (directedBias !== 0) {
|
|
1008
|
+
displacement[i].x -= directedBias;
|
|
1009
|
+
displacement[j].x += directedBias;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
if (seedCenters.some(center => center !== null)) {
|
|
1013
|
+
const settlingFactor = 0.35 + (iteration / Math.max(iterations - 1, 1)) * 0.85;
|
|
1014
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1015
|
+
const seedCenter = seedCenters[i];
|
|
1016
|
+
if (!seedCenter)
|
|
1017
|
+
continue;
|
|
1018
|
+
const degree = nodeDegrees[i];
|
|
1019
|
+
const leafBias = degree <= 1 ? 1.6 : degree === 2 ? 1.15 : 0.9;
|
|
1020
|
+
const seedStrength = 0.04 * leafBias * settlingFactor;
|
|
1021
|
+
displacement[i].x += (seedCenter.x - centers[i].x) * seedStrength;
|
|
1022
|
+
displacement[i].y += (seedCenter.y - centers[i].y) * seedStrength;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1026
|
+
displacement[i].x -= centers[i].x * gravity;
|
|
1027
|
+
displacement[i].y -= centers[i].y * gravity;
|
|
1028
|
+
const length = Math.sqrt(displacement[i].x * displacement[i].x + displacement[i].y * displacement[i].y) || 1;
|
|
1029
|
+
const limited = Math.min(length, temperature);
|
|
1030
|
+
centers[i].x += (displacement[i].x / length) * limited;
|
|
1031
|
+
centers[i].y += (displacement[i].y / length) * limited;
|
|
1032
|
+
}
|
|
1033
|
+
temperature *= 0.96;
|
|
1034
|
+
}
|
|
1035
|
+
separateOverlappingNodes(nodes, centers, overlapGapX, overlapGapY);
|
|
1036
|
+
resolveLinkCrossings(nodes, edges, centers, baseSpacing, overlapGapX, overlapGapY, seedCenters);
|
|
1037
|
+
separateOverlappingNodes(nodes, centers, overlapGapX, overlapGapY);
|
|
1038
|
+
const positions = nodes.map((node, index) => ({
|
|
1039
|
+
id: node.id,
|
|
1040
|
+
x: centers[index].x - node.width / 2,
|
|
1041
|
+
y: centers[index].y - node.height / 2,
|
|
1042
|
+
z: node.z,
|
|
1043
|
+
}));
|
|
1044
|
+
return normalizeLayout(nodes, packDisconnectedComponents(nodes, links, positions, options), options);
|
|
1045
|
+
}
|
|
1046
|
+
function calculateLayoutResult(data) {
|
|
1047
|
+
const nodes = (data.nodes ?? []);
|
|
1048
|
+
const links = (data.links ?? []);
|
|
1049
|
+
const options = data.options ?? {};
|
|
1050
|
+
switch (data.algorithm) {
|
|
1051
|
+
case "sequential":
|
|
1052
|
+
return layoutSequential(nodes, links, options);
|
|
1053
|
+
case "radial":
|
|
1054
|
+
return layoutRadial(nodes, links, options);
|
|
1055
|
+
case "tree":
|
|
1056
|
+
return layoutTree(nodes, links, options);
|
|
1057
|
+
case "structural":
|
|
1058
|
+
return layoutHierarchical(nodes, links, options, false);
|
|
1059
|
+
case "organic": {
|
|
1060
|
+
const radialSeed = layoutRadial(nodes, links, options);
|
|
1061
|
+
return runForceLayout(nodes, links, {
|
|
1062
|
+
...options,
|
|
1063
|
+
iterations: Math.min(options.iterations ?? 150, 130),
|
|
1064
|
+
}, 0, 0.02, radialSeed.positions);
|
|
1065
|
+
}
|
|
1066
|
+
case "force-direction": {
|
|
1067
|
+
const structuralSeed = layoutHierarchical(nodes, links, options, false);
|
|
1068
|
+
return runForceLayout(nodes, links, {
|
|
1069
|
+
...options,
|
|
1070
|
+
iterations: Math.min(options.iterations ?? 140, 120),
|
|
1071
|
+
}, Math.max((options.gapX ?? 140) * 0.03, 1), 0.018, structuralSeed.positions);
|
|
1072
|
+
}
|
|
1073
|
+
default:
|
|
1074
|
+
return runForceLayout(nodes, links, options, Math.max((options.gapX ?? 140) * 0.03, 1), 0.018);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
function calculateFitViewResult(data) {
|
|
1078
|
+
const nodes = (data.nodes ?? []);
|
|
1079
|
+
const bounds = getLayoutBounds(nodes, nodes.map(node => ({ id: node.id, x: node.x, y: node.y, z: node.z })));
|
|
1080
|
+
if (nodes.length === 0 || data.viewportWidth <= 0 || data.viewportHeight <= 0) {
|
|
1081
|
+
return { x: 0, y: 0, zoom: 1, bounds };
|
|
1082
|
+
}
|
|
1083
|
+
const padding = Math.max(0, data.padding ?? 32);
|
|
1084
|
+
const safeWidth = Math.max(bounds.width, 1);
|
|
1085
|
+
const safeHeight = Math.max(bounds.height, 1);
|
|
1086
|
+
const availableWidth = Math.max(1, data.viewportWidth - padding * 2);
|
|
1087
|
+
const availableHeight = Math.max(1, data.viewportHeight - padding * 2);
|
|
1088
|
+
const zoomX = availableWidth / safeWidth;
|
|
1089
|
+
const zoomY = availableHeight / safeHeight;
|
|
1090
|
+
const unclampedZoom = Math.min(zoomX, zoomY);
|
|
1091
|
+
const zoom = Math.min(data.maxZoom ?? 2, Math.max(data.minZoom ?? 0.1, unclampedZoom));
|
|
1092
|
+
const x = bounds.left + bounds.width / 2 - data.viewportWidth / (2 * zoom);
|
|
1093
|
+
const y = bounds.top + bounds.height / 2 - data.viewportHeight / (2 * zoom);
|
|
1094
|
+
return { x, y, zoom, bounds };
|
|
1095
|
+
}
|
|
96
1096
|
// eslint-disable-next-line no-restricted-globals
|
|
97
1097
|
self.onmessage = function (e) {
|
|
98
1098
|
const { type, id, ...data } = e.data;
|
|
@@ -170,6 +1170,14 @@ function workerSource() {
|
|
|
170
1170
|
});
|
|
171
1171
|
post({ type: "calculateLabels", id, positions });
|
|
172
1172
|
}
|
|
1173
|
+
else if (type === "calculateLayout") {
|
|
1174
|
+
const result = calculateLayoutResult(data);
|
|
1175
|
+
post({ type: "calculateLayout", id, ...result });
|
|
1176
|
+
}
|
|
1177
|
+
else if (type === "calculateFitView") {
|
|
1178
|
+
const result = calculateFitViewResult(data);
|
|
1179
|
+
post({ type: "calculateFitView", id, ...result });
|
|
1180
|
+
}
|
|
173
1181
|
};
|
|
174
1182
|
}
|
|
175
1183
|
let pool = null;
|
|
@@ -244,4 +1252,22 @@ export function calculatePath(input) {
|
|
|
244
1252
|
export function calculateLabels(input) {
|
|
245
1253
|
return request({ type: "calculateLabels", ...input });
|
|
246
1254
|
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Calcula um layout completo para os nós do grafo em um worker do pool.
|
|
1257
|
+
*
|
|
1258
|
+
* @param input Dados de entrada do algoritmo de layout
|
|
1259
|
+
* @returns Promise com as novas posições e bounds resultantes
|
|
1260
|
+
*/
|
|
1261
|
+
export function calculateLayout(input) {
|
|
1262
|
+
return request({ type: "calculateLayout", ...input });
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Calcula o viewbox ideal para enquadrar todos os nós em uma viewport.
|
|
1266
|
+
*
|
|
1267
|
+
* @param input Dados de entrada para fit da viewport
|
|
1268
|
+
* @returns Promise com x, y e zoom calculados
|
|
1269
|
+
*/
|
|
1270
|
+
export function calculateFitView(input) {
|
|
1271
|
+
return request({ type: "calculateFitView", ...input });
|
|
1272
|
+
}
|
|
247
1273
|
//# sourceMappingURL=index.js.map
|