@fusion.dev/helix-graph 0.120.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 +141 -8
- 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 +40 -5
- package/dist/connect-graph/connect-graph.js +81 -18
- package/dist/connect-graph/diff.d.ts +1 -1
- package/dist/connect-graph/looks.d.ts +29 -3
- package/dist/connect-graph/looks.js +24 -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 +2 -1
- package/dist/index.js +1 -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 +31 -3
- package/dist/sigma/sigma-driver.js +125 -140
- 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,11 +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
|
};
|
|
28
|
-
const TRAVEL = 700;
|
|
29
|
-
const PULSE = 450;
|
|
30
30
|
const GROW = 300;
|
|
31
|
-
const
|
|
32
|
-
|
|
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;
|
|
33
57
|
const positionsOf = (graph) => {
|
|
34
58
|
const positions = new Map();
|
|
35
59
|
graph.forEachNode((node, attributes) => {
|
|
@@ -41,35 +65,8 @@ export const sigmaDriver = (sigma, options = {}) => {
|
|
|
41
65
|
const graph = sigma.getGraph();
|
|
42
66
|
const container = sigma.getContainer();
|
|
43
67
|
const camera = sigma.getCamera();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const hold = (event) => {
|
|
47
|
-
if (mode === 'free') {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
if (mode === 'off') {
|
|
51
|
-
event.stopImmediatePropagation();
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
if (event instanceof WheelEvent) {
|
|
55
|
-
if (!(event.ctrlKey || event.metaKey)) {
|
|
56
|
-
event.stopImmediatePropagation();
|
|
57
|
-
}
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
if (typeof TouchEvent !== 'undefined' && event instanceof TouchEvent) {
|
|
61
|
-
if (event.touches.length < 2) {
|
|
62
|
-
event.stopImmediatePropagation();
|
|
63
|
-
}
|
|
64
|
-
else if (event.type === 'touchstart' && event.cancelable) {
|
|
65
|
-
event.preventDefault();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
const held = ['wheel', 'touchstart', 'touchmove', 'mousedown'];
|
|
70
|
-
for (const type of held) {
|
|
71
|
-
container.addEventListener(type, hold, { capture: true, passive: false });
|
|
72
|
-
}
|
|
68
|
+
const programs = sigma.getSetting('nodeProgramClasses');
|
|
69
|
+
sigma.setSetting('nodeProgramClasses', { ...programs, helix: HelixNodeProgram });
|
|
73
70
|
const sized = typeof ResizeObserver === 'function'
|
|
74
71
|
? new ResizeObserver(() => {
|
|
75
72
|
sigma.resize(true);
|
|
@@ -77,10 +74,34 @@ export const sigmaDriver = (sigma, options = {}) => {
|
|
|
77
74
|
})
|
|
78
75
|
: undefined;
|
|
79
76
|
sized?.observe(container);
|
|
80
|
-
let
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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);
|
|
84
105
|
const onScreen = (id) => {
|
|
85
106
|
const data = sigma.getNodeDisplayData(id);
|
|
86
107
|
if (!data || data.hidden) {
|
|
@@ -89,76 +110,9 @@ export const sigmaDriver = (sigma, options = {}) => {
|
|
|
89
110
|
const at = sigma.framedGraphToViewport(data);
|
|
90
111
|
return { radius: sigma.scaleSize(data.size), x: at.x, y: at.y };
|
|
91
112
|
};
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
flights = [];
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
const ratio = window.devicePixelRatio || 1;
|
|
99
|
-
const width = Math.round(container.clientWidth * ratio);
|
|
100
|
-
const height = Math.round(container.clientHeight * ratio);
|
|
101
|
-
if (overlay.width !== width || overlay.height !== height) {
|
|
102
|
-
overlay.width = width;
|
|
103
|
-
overlay.height = height;
|
|
104
|
-
}
|
|
105
|
-
context.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
106
|
-
context.clearRect(0, 0, width, height);
|
|
107
|
-
flights = flights.filter((flight) => {
|
|
108
|
-
const from = onScreen(flight.from);
|
|
109
|
-
const to = onScreen(flight.to);
|
|
110
|
-
const gone = (now - flight.started) / TRAVEL;
|
|
111
|
-
if (!from || !to || gone >= 1 + PULSE / TRAVEL) {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
context.save();
|
|
115
|
-
context.strokeStyle = flight.color;
|
|
116
|
-
context.fillStyle = flight.color;
|
|
117
|
-
if (gone < 1) {
|
|
118
|
-
if (flight.animate) {
|
|
119
|
-
const along = ease(gone);
|
|
120
|
-
const x = from.x + (to.x - from.x) * along;
|
|
121
|
-
const y = from.y + (to.y - from.y) * along;
|
|
122
|
-
context.globalAlpha = 0.6;
|
|
123
|
-
context.lineWidth = 2;
|
|
124
|
-
context.beginPath();
|
|
125
|
-
context.moveTo(from.x, from.y);
|
|
126
|
-
context.lineTo(x, y);
|
|
127
|
-
context.stroke();
|
|
128
|
-
context.globalAlpha = 1;
|
|
129
|
-
context.shadowBlur = 14;
|
|
130
|
-
context.shadowColor = flight.color;
|
|
131
|
-
context.beginPath();
|
|
132
|
-
context.arc(x, y, 5, 0, Math.PI * 2);
|
|
133
|
-
context.fill();
|
|
134
|
-
context.shadowBlur = 0;
|
|
135
|
-
context.fillStyle = 'rgba(255, 255, 255, 0.85)';
|
|
136
|
-
context.beginPath();
|
|
137
|
-
context.arc(x, y, 2, 0, Math.PI * 2);
|
|
138
|
-
context.fill();
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
context.globalAlpha = 1 - gone;
|
|
142
|
-
context.lineWidth = 2;
|
|
143
|
-
context.beginPath();
|
|
144
|
-
context.moveTo(from.x, from.y);
|
|
145
|
-
context.lineTo(to.x, to.y);
|
|
146
|
-
context.stroke();
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
else if (flight.animate) {
|
|
150
|
-
const opened = (gone - 1) / (PULSE / TRAVEL);
|
|
151
|
-
context.globalAlpha = 1 - opened;
|
|
152
|
-
context.lineWidth = 2.5;
|
|
153
|
-
context.beginPath();
|
|
154
|
-
context.arc(to.x, to.y, to.radius + 2 + opened * 16, 0, Math.PI * 2);
|
|
155
|
-
context.stroke();
|
|
156
|
-
}
|
|
157
|
-
context.restore();
|
|
158
|
-
return true;
|
|
159
|
-
});
|
|
160
|
-
flying = flights.length > 0 ? requestAnimationFrame(fly) : 0;
|
|
161
|
-
};
|
|
113
|
+
const signals = signalsOver(container, onScreen);
|
|
114
|
+
const groups = groupsOver(container, onScreen);
|
|
115
|
+
sigma.on('afterRender', groups.draw);
|
|
162
116
|
const grow = (sizes) => {
|
|
163
117
|
const started = performance.now();
|
|
164
118
|
const step = (now) => {
|
|
@@ -208,38 +162,83 @@ export const sigmaDriver = (sigma, options = {}) => {
|
|
|
208
162
|
}
|
|
209
163
|
}
|
|
210
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
|
+
},
|
|
211
190
|
gestures: (next) => {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
191
|
+
held.set(next);
|
|
192
|
+
},
|
|
193
|
+
groups: (next) => {
|
|
194
|
+
if (options.groupLabels !== false) {
|
|
195
|
+
groups.set(next);
|
|
196
|
+
}
|
|
215
197
|
},
|
|
216
198
|
look: (looks) => {
|
|
217
|
-
|
|
199
|
+
signals.look(looks);
|
|
200
|
+
groups.look(looks);
|
|
218
201
|
sigma.setSetting('labelColor', { color: looks.ink });
|
|
202
|
+
sigma.setSetting('labelSize', 13);
|
|
203
|
+
sigma.setSetting('minEdgeThickness', 0.8);
|
|
219
204
|
sigma.setSetting('labelFont', looks.font);
|
|
220
205
|
sigma.setSetting('labelRenderedSizeThreshold', 0);
|
|
206
|
+
sigma.setSetting('labelGridCellSize', 130);
|
|
207
|
+
sigma.setSetting('defaultDrawNodeLabel', labelOf(looks, claim));
|
|
221
208
|
sigma.setSetting('defaultDrawNodeHover', hoverOf(looks));
|
|
222
209
|
sigma.setSetting('nodeReducer', (node, data) => {
|
|
223
210
|
const look = looks.node(node);
|
|
224
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)));
|
|
225
217
|
return {
|
|
226
218
|
...data,
|
|
227
219
|
color: look.color,
|
|
220
|
+
emphasisCode: EMPHASES[look.emphasis ?? options.emphasis ?? 'solid'],
|
|
228
221
|
forceLabel: look.label === 'always',
|
|
222
|
+
glow: look.glow,
|
|
229
223
|
hidden: look.hidden,
|
|
230
|
-
label:
|
|
224
|
+
label: named ? data.label : null,
|
|
225
|
+
materialCode: MATERIALS[look.material ?? options.material ?? 'flat'],
|
|
226
|
+
shapeCode: SHAPES[look.shape ?? options.shape ?? 'circle'],
|
|
231
227
|
size: typeof data.size === 'number' ? data.size * grown : data.size,
|
|
228
|
+
strong: look.selected || look.raised,
|
|
229
|
+
type: 'helix',
|
|
232
230
|
zIndex: look.selected ? 2 : look.raised ? 1 : 0,
|
|
233
231
|
};
|
|
234
232
|
});
|
|
235
233
|
sigma.setSetting('edgeReducer', (_edge, data) => {
|
|
236
234
|
const look = looks.link(String(data.from), String(data.to));
|
|
235
|
+
const lit = hovered !== undefined && (data.from === hovered || data.to === hovered);
|
|
237
236
|
return {
|
|
238
237
|
...data,
|
|
239
|
-
color: look.color,
|
|
238
|
+
color: lit ? looks.node(hovered).color : look.color,
|
|
240
239
|
hidden: look.hidden,
|
|
241
|
-
size: look.raised ? 1.
|
|
242
|
-
zIndex: look.raised ? 1 : 0,
|
|
240
|
+
size: look.raised || lit ? 1.6 : 0.8,
|
|
241
|
+
zIndex: look.raised || lit ? 1 : 0,
|
|
243
242
|
};
|
|
244
243
|
});
|
|
245
244
|
sigma.refresh();
|
|
@@ -261,33 +260,19 @@ export const sigmaDriver = (sigma, options = {}) => {
|
|
|
261
260
|
camera.setState(to);
|
|
262
261
|
}
|
|
263
262
|
},
|
|
264
|
-
signal: (from, to,
|
|
265
|
-
|
|
266
|
-
overlay = document.createElement('canvas');
|
|
267
|
-
overlay.setAttribute('aria-hidden', 'true');
|
|
268
|
-
overlay.style.cssText =
|
|
269
|
-
'position:absolute;inset:0;width:100%;height:100%;pointer-events:none';
|
|
270
|
-
container.append(overlay);
|
|
271
|
-
}
|
|
272
|
-
flights.push({ animate, color: color ?? ink, from, started: performance.now(), to });
|
|
273
|
-
if (flights.length > FLYING) {
|
|
274
|
-
flights = flights.slice(-FLYING);
|
|
275
|
-
}
|
|
276
|
-
if (!flying) {
|
|
277
|
-
flying = requestAnimationFrame(fly);
|
|
278
|
-
}
|
|
263
|
+
signal: (from, to, colors, animate) => {
|
|
264
|
+
signals.signal(from, to, colors, animate);
|
|
279
265
|
},
|
|
280
266
|
stop: () => {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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);
|
|
286
274
|
sized?.disconnect();
|
|
287
|
-
|
|
288
|
-
container.removeEventListener(type, hold, { capture: true });
|
|
289
|
-
}
|
|
290
|
-
container.style.touchAction = touchAction;
|
|
275
|
+
held.stop();
|
|
291
276
|
},
|
|
292
277
|
update: (changes, animate) => {
|
|
293
278
|
for (const id of changes.removeNodes) {
|
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
|
}
|