@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.
@@ -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) => 2 + 2 * Math.sqrt(Math.max(size, 0));
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
- let mode = 'free';
40
- const touchAction = container.style.touchAction;
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
- mode = next;
111
- container.style.touchAction =
112
- next === 'free' ? touchAction : next === 'cooperative' ? 'pan-x pan-y' : 'auto';
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: look.label === 'never' ? null : data.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.5 : 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
- for (const type of held) {
164
- container.removeEventListener(type, hold, { capture: true });
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.119.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.119.0"
42
+ "@fusion.dev/helix-core": "0.121.0"
39
43
  },
40
44
  "devDependencies": {
41
- "@fusion.dev/helix-headless": "0.119.0",
42
- "@fusion.dev/helix-testing": "0.119.0",
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
- "accessibility"
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
  }