@fusion.dev/helix-graph 0.119.0 → 0.121.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 +171 -6
- package/dist/connect-graph/arrange.d.ts +18 -0
- package/dist/connect-graph/arrange.js +92 -0
- package/dist/connect-graph/connect-graph.d.ts +58 -17
- package/dist/connect-graph/connect-graph.js +155 -34
- package/dist/connect-graph/diff.d.ts +50 -0
- package/dist/connect-graph/diff.js +48 -0
- package/dist/connect-graph/looks.d.ts +39 -3
- package/dist/connect-graph/looks.js +29 -6
- package/dist/connect-graph/seed.js +11 -2
- package/dist/connect-graph/stencil.d.ts +34 -0
- package/dist/connect-graph/stencil.js +121 -0
- package/dist/cosmos/cosmos-driver.d.ts +67 -0
- package/dist/cosmos/cosmos-driver.js +467 -0
- package/dist/cosmos/index.d.ts +1 -0
- package/dist/cosmos/index.js +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/overlay/overlay.d.ts +59 -0
- package/dist/overlay/overlay.js +296 -0
- package/dist/sigma/node-program.d.ts +71 -0
- package/dist/sigma/node-program.js +170 -0
- package/dist/sigma/sigma-driver.d.ts +44 -3
- package/dist/sigma/sigma-driver.js +197 -40
- package/package.json +25 -6
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { ease, gesturesHeld, groupsOver, signalsOver } from '../overlay/overlay.js';
|
|
2
|
+
import { EMPHASES, HelixNodeProgram, MATERIALS, SHAPES } from './node-program.js';
|
|
1
3
|
const STEP = 250;
|
|
2
4
|
const ZOOM = 1.5;
|
|
3
5
|
const SAMPLE = 250;
|
|
4
6
|
const STILL = 0.004;
|
|
5
7
|
const HELD = 2;
|
|
6
8
|
const IDLE = 2000;
|
|
7
|
-
const pixels = (size) =>
|
|
9
|
+
const pixels = (size) => 1.6 + 1.35 * Math.max(size, 0);
|
|
8
10
|
const hoverOf = (looks) => (context, data, settings) => {
|
|
9
11
|
if (!data.label) {
|
|
10
12
|
return;
|
|
@@ -25,6 +27,33 @@ const hoverOf = (looks) => (context, data, settings) => {
|
|
|
25
27
|
context.fillStyle = looks.ink;
|
|
26
28
|
context.fillText(data.label, x, data.y + size / 3);
|
|
27
29
|
};
|
|
30
|
+
const GROW = 300;
|
|
31
|
+
const labelOf = (looks, claim) => (context, data, settings) => {
|
|
32
|
+
if (!data.label) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const strong = data.strong === true;
|
|
36
|
+
const size = strong ? settings.labelSize : settings.labelSize - 1;
|
|
37
|
+
context.font = `${strong ? 600 : 500} ${size}px ${settings.labelFont}`;
|
|
38
|
+
const x = data.x + data.size + 4;
|
|
39
|
+
const y = data.y + size / 3;
|
|
40
|
+
const box = {
|
|
41
|
+
bottom: y + 4,
|
|
42
|
+
left: x - 2,
|
|
43
|
+
right: x + context.measureText(data.label).width + 2,
|
|
44
|
+
top: y - size,
|
|
45
|
+
};
|
|
46
|
+
if (!claim(box, data.forceLabel === true)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
context.lineJoin = 'round';
|
|
50
|
+
context.lineWidth = 3;
|
|
51
|
+
context.strokeStyle = looks.ground;
|
|
52
|
+
context.strokeText(data.label, x, y);
|
|
53
|
+
context.fillStyle = strong ? looks.ink : looks.quiet;
|
|
54
|
+
context.fillText(data.label, x, y);
|
|
55
|
+
};
|
|
56
|
+
const HUB = 6.5;
|
|
28
57
|
const positionsOf = (graph) => {
|
|
29
58
|
const positions = new Map();
|
|
30
59
|
graph.forEachNode((node, attributes) => {
|
|
@@ -36,35 +65,8 @@ export const sigmaDriver = (sigma, options = {}) => {
|
|
|
36
65
|
const graph = sigma.getGraph();
|
|
37
66
|
const container = sigma.getContainer();
|
|
38
67
|
const camera = sigma.getCamera();
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const hold = (event) => {
|
|
42
|
-
if (mode === 'free') {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
if (mode === 'off') {
|
|
46
|
-
event.stopImmediatePropagation();
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
if (event instanceof WheelEvent) {
|
|
50
|
-
if (!(event.ctrlKey || event.metaKey)) {
|
|
51
|
-
event.stopImmediatePropagation();
|
|
52
|
-
}
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
if (typeof TouchEvent !== 'undefined' && event instanceof TouchEvent) {
|
|
56
|
-
if (event.touches.length < 2) {
|
|
57
|
-
event.stopImmediatePropagation();
|
|
58
|
-
}
|
|
59
|
-
else if (event.type === 'touchstart' && event.cancelable) {
|
|
60
|
-
event.preventDefault();
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
const held = ['wheel', 'touchstart', 'touchmove', 'mousedown'];
|
|
65
|
-
for (const type of held) {
|
|
66
|
-
container.addEventListener(type, hold, { capture: true, passive: false });
|
|
67
|
-
}
|
|
68
|
+
const programs = sigma.getSetting('nodeProgramClasses');
|
|
69
|
+
sigma.setSetting('nodeProgramClasses', { ...programs, helix: HelixNodeProgram });
|
|
68
70
|
const sized = typeof ResizeObserver === 'function'
|
|
69
71
|
? new ResizeObserver(() => {
|
|
70
72
|
sigma.resize(true);
|
|
@@ -72,6 +74,60 @@ export const sigmaDriver = (sigma, options = {}) => {
|
|
|
72
74
|
})
|
|
73
75
|
: undefined;
|
|
74
76
|
sized?.observe(container);
|
|
77
|
+
let hovered;
|
|
78
|
+
const enter = ({ node }) => {
|
|
79
|
+
hovered = node;
|
|
80
|
+
sigma.refresh();
|
|
81
|
+
};
|
|
82
|
+
const leave = () => {
|
|
83
|
+
hovered = undefined;
|
|
84
|
+
sigma.refresh();
|
|
85
|
+
};
|
|
86
|
+
sigma.on('enterNode', enter);
|
|
87
|
+
sigma.on('leaveNode', leave);
|
|
88
|
+
const naming = options.labels ?? 'hover';
|
|
89
|
+
let taken = [];
|
|
90
|
+
const claim = (box, must) => {
|
|
91
|
+
const free = taken.every((other) => box.right < other.left ||
|
|
92
|
+
box.left > other.right ||
|
|
93
|
+
box.bottom < other.top ||
|
|
94
|
+
box.top > other.bottom);
|
|
95
|
+
if (free || must) {
|
|
96
|
+
taken.push(box);
|
|
97
|
+
}
|
|
98
|
+
return free || must;
|
|
99
|
+
};
|
|
100
|
+
const clear = () => {
|
|
101
|
+
taken = [];
|
|
102
|
+
};
|
|
103
|
+
sigma.on('beforeRender', clear);
|
|
104
|
+
const held = gesturesHeld(container);
|
|
105
|
+
const onScreen = (id) => {
|
|
106
|
+
const data = sigma.getNodeDisplayData(id);
|
|
107
|
+
if (!data || data.hidden) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
const at = sigma.framedGraphToViewport(data);
|
|
111
|
+
return { radius: sigma.scaleSize(data.size), x: at.x, y: at.y };
|
|
112
|
+
};
|
|
113
|
+
const signals = signalsOver(container, onScreen);
|
|
114
|
+
const groups = groupsOver(container, onScreen);
|
|
115
|
+
sigma.on('afterRender', groups.draw);
|
|
116
|
+
const grow = (sizes) => {
|
|
117
|
+
const started = performance.now();
|
|
118
|
+
const step = (now) => {
|
|
119
|
+
const along = Math.min((now - started) / GROW, 1);
|
|
120
|
+
for (const [id, size] of sizes) {
|
|
121
|
+
if (graph.hasNode(id)) {
|
|
122
|
+
graph.setNodeAttribute(id, 'size', size * ease(along));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (along < 1) {
|
|
126
|
+
requestAnimationFrame(step);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
requestAnimationFrame(step);
|
|
130
|
+
};
|
|
75
131
|
return {
|
|
76
132
|
center: (id, animate) => {
|
|
77
133
|
const at = sigma.getNodeDisplayData(id);
|
|
@@ -106,37 +162,83 @@ export const sigmaDriver = (sigma, options = {}) => {
|
|
|
106
162
|
}
|
|
107
163
|
}
|
|
108
164
|
},
|
|
165
|
+
fit: (ids, animate) => {
|
|
166
|
+
let low = { x: Number.POSITIVE_INFINITY, y: Number.POSITIVE_INFINITY };
|
|
167
|
+
let high = { x: Number.NEGATIVE_INFINITY, y: Number.NEGATIVE_INFINITY };
|
|
168
|
+
for (const id of ids) {
|
|
169
|
+
const at = sigma.getNodeDisplayData(id);
|
|
170
|
+
if (at) {
|
|
171
|
+
low = { x: Math.min(low.x, at.x), y: Math.min(low.y, at.y) };
|
|
172
|
+
high = { x: Math.max(high.x, at.x), y: Math.max(high.y, at.y) };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!Number.isFinite(low.x)) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const state = {
|
|
179
|
+
ratio: Math.min(Math.max(Math.max(high.x - low.x, high.y - low.y) * 2.4, 0.12), 1),
|
|
180
|
+
x: (low.x + high.x) / 2,
|
|
181
|
+
y: (low.y + high.y) / 2,
|
|
182
|
+
};
|
|
183
|
+
if (animate) {
|
|
184
|
+
camera.animate(state, { duration: STEP * 2 });
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
camera.setState(state);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
109
190
|
gestures: (next) => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
191
|
+
held.set(next);
|
|
192
|
+
},
|
|
193
|
+
groups: (next) => {
|
|
194
|
+
if (options.groupLabels !== false) {
|
|
195
|
+
groups.set(next);
|
|
196
|
+
}
|
|
113
197
|
},
|
|
114
198
|
look: (looks) => {
|
|
199
|
+
signals.look(looks);
|
|
200
|
+
groups.look(looks);
|
|
115
201
|
sigma.setSetting('labelColor', { color: looks.ink });
|
|
202
|
+
sigma.setSetting('labelSize', 13);
|
|
203
|
+
sigma.setSetting('minEdgeThickness', 0.8);
|
|
116
204
|
sigma.setSetting('labelFont', looks.font);
|
|
117
205
|
sigma.setSetting('labelRenderedSizeThreshold', 0);
|
|
206
|
+
sigma.setSetting('labelGridCellSize', 130);
|
|
207
|
+
sigma.setSetting('defaultDrawNodeLabel', labelOf(looks, claim));
|
|
118
208
|
sigma.setSetting('defaultDrawNodeHover', hoverOf(looks));
|
|
119
209
|
sigma.setSetting('nodeReducer', (node, data) => {
|
|
120
210
|
const look = looks.node(node);
|
|
121
211
|
const grown = look.selected ? 1.5 : look.raised ? 1.25 : 1;
|
|
212
|
+
const named = look.label === 'always' ||
|
|
213
|
+
(look.label === 'room' &&
|
|
214
|
+
(look.raised ||
|
|
215
|
+
naming === 'all' ||
|
|
216
|
+
(naming === 'hubs' && typeof data.size === 'number' && data.size >= HUB)));
|
|
122
217
|
return {
|
|
123
218
|
...data,
|
|
124
219
|
color: look.color,
|
|
220
|
+
emphasisCode: EMPHASES[look.emphasis ?? options.emphasis ?? 'solid'],
|
|
125
221
|
forceLabel: look.label === 'always',
|
|
222
|
+
glow: look.glow,
|
|
126
223
|
hidden: look.hidden,
|
|
127
|
-
label:
|
|
224
|
+
label: named ? data.label : null,
|
|
225
|
+
materialCode: MATERIALS[look.material ?? options.material ?? 'flat'],
|
|
226
|
+
shapeCode: SHAPES[look.shape ?? options.shape ?? 'circle'],
|
|
128
227
|
size: typeof data.size === 'number' ? data.size * grown : data.size,
|
|
228
|
+
strong: look.selected || look.raised,
|
|
229
|
+
type: 'helix',
|
|
129
230
|
zIndex: look.selected ? 2 : look.raised ? 1 : 0,
|
|
130
231
|
};
|
|
131
232
|
});
|
|
132
233
|
sigma.setSetting('edgeReducer', (_edge, data) => {
|
|
133
234
|
const look = looks.link(String(data.from), String(data.to));
|
|
235
|
+
const lit = hovered !== undefined && (data.from === hovered || data.to === hovered);
|
|
134
236
|
return {
|
|
135
237
|
...data,
|
|
136
|
-
color: look.color,
|
|
238
|
+
color: lit ? looks.node(hovered).color : look.color,
|
|
137
239
|
hidden: look.hidden,
|
|
138
|
-
size: look.raised ? 1.
|
|
139
|
-
zIndex: look.raised ? 1 : 0,
|
|
240
|
+
size: look.raised || lit ? 1.6 : 0.8,
|
|
241
|
+
zIndex: look.raised || lit ? 1 : 0,
|
|
140
242
|
};
|
|
141
243
|
});
|
|
142
244
|
sigma.refresh();
|
|
@@ -158,12 +260,67 @@ export const sigmaDriver = (sigma, options = {}) => {
|
|
|
158
260
|
camera.setState(to);
|
|
159
261
|
}
|
|
160
262
|
},
|
|
263
|
+
signal: (from, to, colors, animate) => {
|
|
264
|
+
signals.signal(from, to, colors, animate);
|
|
265
|
+
},
|
|
161
266
|
stop: () => {
|
|
267
|
+
sigma.off('afterRender', groups.draw);
|
|
268
|
+
sigma.off('beforeRender', clear);
|
|
269
|
+
groups.stop();
|
|
270
|
+
signals.stop();
|
|
271
|
+
sigma.off('enterNode', enter);
|
|
272
|
+
sigma.off('leaveNode', leave);
|
|
273
|
+
sigma.setSetting('nodeProgramClasses', programs);
|
|
162
274
|
sized?.disconnect();
|
|
163
|
-
|
|
164
|
-
|
|
275
|
+
held.stop();
|
|
276
|
+
},
|
|
277
|
+
update: (changes, animate) => {
|
|
278
|
+
for (const id of changes.removeNodes) {
|
|
279
|
+
if (graph.hasNode(id)) {
|
|
280
|
+
graph.dropNode(id);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (const link of changes.removeLinks) {
|
|
284
|
+
if (graph.hasEdge(link.source, link.target)) {
|
|
285
|
+
graph.dropEdge(link.source, link.target);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const growing = new Map();
|
|
289
|
+
for (const node of changes.addNodes) {
|
|
290
|
+
const size = pixels(node.size);
|
|
291
|
+
graph.addNode(node.id, {
|
|
292
|
+
label: node.label,
|
|
293
|
+
size: animate ? 0 : size,
|
|
294
|
+
x: node.x,
|
|
295
|
+
y: node.y,
|
|
296
|
+
});
|
|
297
|
+
if (animate) {
|
|
298
|
+
growing.set(node.id, size);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
for (const node of changes.changeNodes) {
|
|
302
|
+
graph.mergeNodeAttributes(node.id, {
|
|
303
|
+
label: node.label,
|
|
304
|
+
size: pixels(node.size),
|
|
305
|
+
x: node.x,
|
|
306
|
+
y: node.y,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
for (const link of changes.addLinks) {
|
|
310
|
+
if (graph.hasNode(link.source) &&
|
|
311
|
+
graph.hasNode(link.target) &&
|
|
312
|
+
!graph.hasEdge(link.source, link.target)) {
|
|
313
|
+
graph.addEdge(link.source, link.target, {
|
|
314
|
+
from: link.source,
|
|
315
|
+
label: link.label,
|
|
316
|
+
size: 1,
|
|
317
|
+
to: link.target,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (growing.size > 0) {
|
|
322
|
+
grow(growing);
|
|
165
323
|
}
|
|
166
|
-
container.style.touchAction = touchAction;
|
|
167
324
|
},
|
|
168
325
|
zoomBy: (direction, animate) => {
|
|
169
326
|
const ratio = camera.getState().ratio * (direction > 0 ? 1 / ZOOM : ZOOM);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion.dev/helix-graph",
|
|
3
|
-
"description": "Connects the Helix Graph frame to a graph renderer: Sigma.js, or a driver of your own.",
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "Connects the Helix Graph frame to a graph renderer: Sigma.js, cosmos.gl, or a driver of your own.",
|
|
4
|
+
"version": "0.121.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
"types": "./dist/index.d.ts",
|
|
18
18
|
"default": "./dist/index.js"
|
|
19
19
|
},
|
|
20
|
+
"./cosmos": {
|
|
21
|
+
"types": "./dist/cosmos/index.d.ts",
|
|
22
|
+
"default": "./dist/cosmos/index.js"
|
|
23
|
+
},
|
|
20
24
|
"./sigma": {
|
|
21
25
|
"types": "./dist/sigma/index.d.ts",
|
|
22
26
|
"default": "./dist/sigma/index.js"
|
|
@@ -35,11 +39,12 @@
|
|
|
35
39
|
},
|
|
36
40
|
"bugs": "https://github.com/project-fusion/fusion/issues",
|
|
37
41
|
"dependencies": {
|
|
38
|
-
"@fusion.dev/helix-core": "0.
|
|
42
|
+
"@fusion.dev/helix-core": "0.121.0"
|
|
39
43
|
},
|
|
40
44
|
"devDependencies": {
|
|
41
|
-
"@
|
|
42
|
-
"@fusion.dev/helix-
|
|
45
|
+
"@cosmos.gl/graph": "^3.4.2",
|
|
46
|
+
"@fusion.dev/helix-headless": "0.121.0",
|
|
47
|
+
"@fusion.dev/helix-testing": "0.121.0",
|
|
43
48
|
"@types/bun": "^1.3.0",
|
|
44
49
|
"graphology": "^0.26.0",
|
|
45
50
|
"graphology-layout-forceatlas2": "^0.10.1",
|
|
@@ -48,11 +53,25 @@
|
|
|
48
53
|
},
|
|
49
54
|
"homepage": "https://fusion.dev",
|
|
50
55
|
"keywords": [
|
|
56
|
+
"accessibility",
|
|
57
|
+
"cosmos.gl",
|
|
51
58
|
"graph",
|
|
52
59
|
"network",
|
|
53
60
|
"sigma",
|
|
54
|
-
"
|
|
61
|
+
"webgl"
|
|
55
62
|
],
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"@cosmos.gl/graph": "^3.4.0",
|
|
65
|
+
"sigma": "^3.0.0"
|
|
66
|
+
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"@cosmos.gl/graph": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"sigma": {
|
|
72
|
+
"optional": true
|
|
73
|
+
}
|
|
74
|
+
},
|
|
56
75
|
"publishConfig": {
|
|
57
76
|
"access": "restricted"
|
|
58
77
|
}
|