@fusion.dev/helix-graph 0.213.0 → 0.215.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,81 +1,23 @@
1
- import { isRtl } from '@fusion.dev/helix-core';
1
+ import { rgbOf } from '../connect-graph/looks.js';
2
2
  import { pixelsOf } from '../connect-graph/sizes.js';
3
- import { chromeOver, clearOf, ease, gesturesHeld, groupsOver, nameStart, signalsOver, } from '../overlay/overlay.js';
4
- import { HelixEdgeProgram } from './edge-program.js';
5
- import { EMPHASES, HelixNodeProgram, MATERIALS, SHAPES } from './node-program.js';
6
- const SETTINGS = [
7
- 'defaultDrawNodeHover',
8
- 'defaultDrawNodeLabel',
9
- 'defaultEdgeType',
10
- 'edgeProgramClasses',
11
- 'edgeReducer',
12
- 'enableCameraPanning',
13
- 'enableCameraZooming',
14
- 'labelColor',
15
- 'labelFont',
16
- 'labelGridCellSize',
17
- 'labelRenderedSizeThreshold',
18
- 'labelSize',
19
- 'minEdgeThickness',
20
- 'nodeProgramClasses',
21
- 'nodeReducer',
22
- ];
3
+ import { ease, gesturesHeld, groupsOver, namesOver, signalsOver, } from '../overlay/overlay.js';
4
+ import { SPREAD } from './edge-program.js';
5
+ import { DIAMOND, EMPHASES, MATERIALS, SHAPES } from './node-program.js';
6
+ import { reducersOf } from './sigma-options.js';
7
+ const SETTINGS = ['enableCameraPanning', 'enableCameraZooming'];
23
8
  const STEP = 250;
24
9
  const ZOOM = 1.5;
25
10
  const SAMPLE = 250;
26
11
  const STILL = 0.004;
27
12
  const HELD = 2;
28
13
  const IDLE = 2000;
29
- const hoverOf = (looks, rightToLeft) => (context, data, settings) => {
30
- if (!data.label) {
31
- return;
32
- }
33
- const size = settings.labelSize;
34
- context.font = `${settings.labelWeight} ${size}px ${settings.labelFont}`;
35
- const width = context.measureText(data.label).width;
36
- const x = rightToLeft() ? data.x - data.size - 2 - width : data.x + data.size + 2;
37
- const height = size + 8;
38
- context.save();
39
- context.shadowBlur = 8;
40
- context.shadowColor = 'rgba(0, 0, 0, 0.25)';
41
- context.fillStyle = looks.overlay;
42
- context.beginPath();
43
- context.roundRect(x - 4, data.y - height / 2, width + 8, height, 4);
44
- context.fill();
45
- context.restore();
46
- context.fillStyle = looks.ink;
47
- context.textAlign = 'left';
48
- context.fillText(data.label, x, data.y + size / 3);
49
- };
50
14
  const GROW = 300;
51
- const labelOf = (looks, claim, start) => (context, data, settings) => {
52
- if (!data.label) {
53
- return;
54
- }
55
- const strong = data.strong === true;
56
- const size = strong ? settings.labelSize : settings.labelSize - 1;
57
- context.font = `${strong ? 600 : 500} ${size}px ${settings.labelFont}`;
58
- const width = context.measureText(data.label).width;
59
- const y = data.y + size / 3;
60
- const boxAt = (from) => ({
61
- bottom: y + 4,
62
- left: from - 2,
63
- right: from + width + 2,
64
- top: y - size,
65
- });
66
- const x = start({ radius: data.size, x: data.x }, width, boxAt);
67
- const box = boxAt(x);
68
- if (!claim(box, data.forceLabel === true)) {
69
- return;
70
- }
71
- context.lineJoin = 'round';
72
- context.lineWidth = 3;
73
- context.strokeStyle = looks.ground;
74
- context.textAlign = 'left';
75
- context.strokeText(data.label, x, y);
76
- context.fillStyle = strong ? looks.ink : looks.quiet;
77
- context.fillText(data.label, x, y);
15
+ const faded = (color, opacity) => {
16
+ const [red, green, blue] = rgbOf(color);
17
+ return `rgba(${red}, ${green}, ${blue}, ${opacity})`;
78
18
  };
19
+ const devicePixels = () => typeof devicePixelRatio === 'number' && devicePixelRatio > 0 ? devicePixelRatio : 1;
20
+ const cellOf = (labels) => (labels === 'all' ? 8 : 130);
79
21
  const HUB = 6.5;
80
22
  const positionsOf = (graph) => {
81
23
  const positions = new Map();
@@ -84,16 +26,47 @@ const positionsOf = (graph) => {
84
26
  });
85
27
  return positions;
86
28
  };
29
+ export const namedOf = (graph, looks, labels, hovered, onScreen) => {
30
+ const asked = [];
31
+ graph.forEachNode((id, attributes) => {
32
+ const look = looks.node(id);
33
+ const size = Number(attributes.size);
34
+ const must = look.label === 'always' || id === hovered || look.keyed === true;
35
+ const named = must ||
36
+ (look.label === 'room' &&
37
+ (look.raised || labels === 'all' || (labels === 'hubs' && size >= HUB)));
38
+ const name = attributes.label;
39
+ if (!named || typeof name !== 'string' || name === '') {
40
+ return;
41
+ }
42
+ const at = onScreen(id);
43
+ if (at) {
44
+ asked.push({ id, must, name, size, strong: must || look.raised, x: at.x, y: at.y });
45
+ }
46
+ });
47
+ asked.sort((one, other) => Number(other.must) - Number(one.must) || other.size - one.size);
48
+ const cell = cellOf(labels);
49
+ const cells = new Set();
50
+ return asked.flatMap(({ id, must, name, strong, x, y }) => {
51
+ const at = `${Math.floor(x / cell)}:${Math.floor(y / cell)}`;
52
+ if (!must) {
53
+ if (cells.has(at)) {
54
+ return [];
55
+ }
56
+ cells.add(at);
57
+ }
58
+ return [{ id, must, name, strong }];
59
+ });
60
+ };
87
61
  export const sigmaDriver = (sigma, options = {}) => {
62
+ const reducers = reducersOf(sigma);
63
+ if (!reducers) {
64
+ throw new TypeError('sigmaDriver: make the renderer with sigmaOptions(), as new Sigma(graph, box, sigmaOptions()). Sigma 4 takes its programs only when it is made.');
65
+ }
88
66
  const graph = sigma.getGraph();
89
67
  const container = sigma.getContainer();
90
68
  const camera = sigma.getCamera();
91
69
  const before = new Map(SETTINGS.map((name) => [name, sigma.getSetting(name)]));
92
- const programs = before.get('nodeProgramClasses');
93
- sigma.setSetting('nodeProgramClasses', { ...programs, helix: HelixNodeProgram });
94
- const edgePrograms = before.get('edgeProgramClasses');
95
- sigma.setSetting('edgeProgramClasses', { ...edgePrograms, helix: HelixEdgeProgram });
96
- sigma.setSetting('defaultEdgeType', 'helix');
97
70
  const sized = typeof ResizeObserver === 'function'
98
71
  ? new ResizeObserver(() => {
99
72
  sigma.resize(true);
@@ -122,32 +95,36 @@ export const sigmaDriver = (sigma, options = {}) => {
122
95
  sigma.on('enterNode', enter);
123
96
  sigma.on('leaveNode', leave);
124
97
  const naming = options.labels ?? 'hover';
125
- let taken = [];
126
- const claim = (box, must) => {
127
- const free = clearOf(box, taken);
128
- if (free || must) {
129
- taken.push(box);
130
- }
131
- return free || must;
132
- };
133
- let rightToLeft = false;
134
- const clear = () => {
135
- taken = chromeOver(container);
136
- rightToLeft = isRtl(container);
137
- };
138
- sigma.on('beforeRender', clear);
139
98
  const held = gesturesHeld(container);
140
99
  const onScreen = (id) => {
141
100
  const data = sigma.getNodeDisplayData(id);
142
- if (!data || data.hidden) {
101
+ if (!data || data.visibility === 'hidden') {
143
102
  return undefined;
144
103
  }
145
104
  const at = sigma.framedGraphToViewport(data);
146
105
  return { radius: sigma.scaleSize(data.size), x: at.x, y: at.y };
147
106
  };
148
107
  const signals = signalsOver(container, onScreen);
149
- const groups = groupsOver(container, onScreen, () => taken);
150
- sigma.on('afterRender', groups.draw);
108
+ const names = namesOver(container, onScreen);
109
+ let looks;
110
+ const drawNames = () => {
111
+ if (looks) {
112
+ names.draw(looks, namedOf(graph, looks, naming, hovered, onScreen));
113
+ }
114
+ };
115
+ const groups = groupsOver(container, onScreen, () => names.boxes());
116
+ const redraw = () => {
117
+ drawNames();
118
+ groups.draw();
119
+ };
120
+ sigma.on('afterRender', redraw);
121
+ const ringed = new Set();
122
+ const rezoomed = () => {
123
+ if (ringed.size > 0) {
124
+ sigma.refresh({ partialGraph: { nodes: [...ringed].filter((id) => graph.hasNode(id)) } });
125
+ }
126
+ };
127
+ camera.on('updated', rezoomed);
151
128
  let gliding = 0;
152
129
  const glide = (sizes, moves) => {
153
130
  cancelAnimationFrame(gliding);
@@ -242,58 +219,84 @@ export const sigmaDriver = (sigma, options = {}) => {
242
219
  groups.set(next);
243
220
  }
244
221
  },
245
- look: (looks) => {
246
- signals.look(looks);
247
- groups.look(looks);
248
- sigma.setSetting('labelColor', { color: looks.ink });
249
- sigma.setSetting('labelSize', 13);
250
- sigma.setSetting('minEdgeThickness', 0.8);
251
- sigma.setSetting('labelFont', looks.font);
252
- sigma.setSetting('labelRenderedSizeThreshold', 0);
253
- sigma.setSetting('labelGridCellSize', naming === 'all' ? 8 : 130);
254
- sigma.setSetting('defaultDrawNodeLabel', labelOf(looks, claim, (at, width, box) => nameStart(at, width, box, taken, container.clientWidth, rightToLeft)));
255
- sigma.setSetting('defaultDrawNodeHover', hoverOf(looks, () => rightToLeft));
256
- sigma.setSetting('nodeReducer', (node, data) => {
257
- const look = looks.node(node);
222
+ look: (next) => {
223
+ looks = next;
224
+ signals.look(next);
225
+ groups.look(next);
226
+ reducers.node = (node, attributes) => {
227
+ const look = next.node(node);
228
+ const density = devicePixels();
229
+ const shape = look.shape ?? options.shape ?? 'circle';
230
+ const material = look.material ?? options.material ?? 'flat';
258
231
  const grown = look.selected ? 1.5 : look.raised ? 1.25 : 1;
259
- const named = look.label === 'always' ||
260
- (look.label === 'room' &&
261
- (look.raised ||
262
- naming === 'all' ||
263
- (naming === 'hubs' && typeof data.size === 'number' && data.size >= HUB)));
232
+ const size = Number(attributes.size) * grown;
233
+ const radius = sigma.scaleSize(size) * density;
234
+ const glow = look.glow > 0
235
+ ? {
236
+ backdropBorderColor: faded(look.color, 0.8 * look.glow),
237
+ backdropBorderWidth: Math.max(radius * 0.14, 1.5 * density),
238
+ backdropColor: next.ground,
239
+ backdropPadding: radius * 0.32 + Math.max(radius * 0.07, 0.75 * density),
240
+ backdropShadowBlur: radius * 2,
241
+ backdropShadowColor: faded(look.color, 0.6 * look.glow),
242
+ backdropVisibility: 'visible',
243
+ }
244
+ : material === 'mark'
245
+ ? {
246
+ backdropBorderWidth: 0,
247
+ backdropColor: faded(look.color, 0.22),
248
+ backdropPadding: radius * 0.4,
249
+ backdropShadowBlur: radius * 0.9,
250
+ backdropShadowColor: faded(look.color, 0.3),
251
+ backdropVisibility: 'visible',
252
+ }
253
+ : { backdropVisibility: 'hidden' };
254
+ const keyed = look.keyed === true
255
+ ? {
256
+ backdropBorderColor: next.ink,
257
+ backdropBorderWidth: 2 * density,
258
+ backdropColor: next.ground,
259
+ backdropPadding: Math.max(radius * 0.32, 3 * density),
260
+ backdropVisibility: 'visible',
261
+ }
262
+ : {};
263
+ if (glow.backdropVisibility === 'visible' || look.keyed === true) {
264
+ ringed.add(node);
265
+ }
266
+ else {
267
+ ringed.delete(node);
268
+ }
264
269
  return {
265
- ...data,
270
+ ...glow,
271
+ ...keyed,
272
+ backdropArea: 'node',
266
273
  color: look.color,
267
- emphasisCode: EMPHASES[look.emphasis ?? options.emphasis ?? 'solid'],
268
- forceLabel: look.label === 'always',
274
+ emphasisCode: shape === 'ring'
275
+ ? EMPHASES.outlined
276
+ : EMPHASES[look.emphasis ?? options.emphasis ?? 'solid'],
269
277
  glow: look.glow,
270
- hidden: look.hidden,
271
- highlighted: look.keyed === true,
272
- label: named ? data.label : null,
273
- materialCode: MATERIALS[look.material ?? options.material ?? 'flat'],
274
- shapeCode: SHAPES[look.shape ?? options.shape ?? 'circle'],
275
- size: typeof data.size === 'number' ? data.size * grown : data.size,
276
- strong: look.selected || look.raised,
277
- type: 'helix',
278
+ materialCode: MATERIALS[material],
279
+ shape: SHAPES[shape],
280
+ size: shape === 'diamond' ? size * DIAMOND : size,
281
+ visibility: look.hidden ? 'hidden' : 'visible',
278
282
  zIndex: look.selected ? 2 : look.raised ? 1 : 0,
279
283
  };
280
- });
281
- sigma.setSetting('edgeReducer', (_edge, data) => {
282
- const look = looks.link(String(data.from), String(data.to));
283
- const near = hovered ?? looks.keyed;
284
- const lit = near !== undefined && (data.from === near || data.to === near);
284
+ };
285
+ reducers.edge = (_edge, attributes) => {
286
+ const look = next.link(String(attributes.from), String(attributes.to));
287
+ const near = hovered ?? next.keyed;
288
+ const lit = near !== undefined && (attributes.from === near || attributes.to === near);
285
289
  const strong = look.raised || lit;
286
290
  return {
287
- ...data,
288
- color: lit ? looks.node(near).color : look.color,
291
+ color: lit ? next.node(near).color : look.color,
292
+ depth: strong ? 'topEdges' : 'edges',
289
293
  glow: strong ? 1 : 0,
290
- hidden: look.hidden,
291
- size: strong ? 1.3 : 0.8,
292
- strong,
293
- type: 'helix',
294
+ head: strong && next.node(String(attributes.to)).glow === 0 ? 'arrow' : 'none',
295
+ size: strong ? 2.6 * SPREAD : 1.6,
296
+ visibility: look.hidden ? 'hidden' : 'visible',
294
297
  zIndex: strong ? 1 : 0,
295
298
  };
296
- });
299
+ };
297
300
  sigma.refresh();
298
301
  },
299
302
  onPress: (press) => {
@@ -320,19 +323,23 @@ export const sigmaDriver = (sigma, options = {}) => {
320
323
  signals.signal(from, to, colors, animate);
321
324
  },
322
325
  stop: () => {
323
- sigma.off('afterRender', groups.draw);
324
- sigma.off('beforeRender', clear);
326
+ sigma.off('afterRender', redraw);
327
+ camera.off('updated', rezoomed);
328
+ names.stop();
325
329
  groups.stop();
326
330
  signals.stop();
327
331
  sigma.off('enterNode', enter);
328
332
  sigma.off('leaveNode', leave);
329
333
  cancelAnimationFrame(gliding);
330
334
  container.style.cursor = cursor;
335
+ reducers.edge = undefined;
336
+ reducers.node = undefined;
331
337
  for (const [name, value] of before) {
332
- sigma.setSetting(name, value);
338
+ sigma.setSetting(name, value !== false);
333
339
  }
334
340
  sized?.disconnect();
335
341
  held.stop();
342
+ sigma.refresh();
336
343
  },
337
344
  update: (changes, animate) => {
338
345
  for (const id of changes.removeNodes) {
@@ -0,0 +1,72 @@
1
+ /** What a reducer adds to a note's or a link's display data, by its key and its attributes. */
2
+ export type Reduce = (key: string, attributes: Record<string, unknown>) => Record<string, unknown>;
3
+ /** The reducers a driver puts in place, which the renderer's own call through to. */
4
+ export interface Reducers {
5
+ edge?: Reduce | undefined;
6
+ node?: Reduce | undefined;
7
+ }
8
+ /** The reducers of a renderer made with `sigmaOptions()`, or nothing for one made without. */
9
+ export declare const reducersOf: (sigma: {
10
+ getPrimitives(): {
11
+ nodes?: object | undefined;
12
+ };
13
+ }) => Reducers | undefined;
14
+ /** What `sigmaOptions` takes. */
15
+ export interface SigmaOptionsOverrides {
16
+ /** Sigma's settings, over the ones Helix sets. */
17
+ settings?: Record<string, unknown> | undefined;
18
+ }
19
+ /**
20
+ * The options to make a Sigma renderer with for `sigmaDriver`: Helix's
21
+ * programs for the notes and the links, and the reducers the driver draws
22
+ * them through.
23
+ *
24
+ * ```js
25
+ * const sigma = new Sigma(new Graph(), box, sigmaOptions())
26
+ * connectGraph(frame, sigmaDriver(sigma, { layout }))
27
+ * ```
28
+ *
29
+ * **Sigma draws no names**: the driver names notes over the drawing, as it
30
+ * names groups, so a name keeps clear of the frame's controls and of the
31
+ * names beside it. `settings` are Sigma's own, over the ones set here.
32
+ */
33
+ export declare const sigmaOptions: (overrides?: SigmaOptionsOverrides) => {
34
+ readonly edgeReducer: (edge: string, _data: unknown, attributes: Record<string, unknown>) => Record<string, unknown>;
35
+ readonly nodeReducer: (node: string, _data: unknown, attributes: Record<string, unknown>) => Record<string, unknown>;
36
+ readonly primitives: {
37
+ readonly edges: import("sigma/primitives").EdgePrimitives;
38
+ readonly nodes: import("sigma/primitives").NodePrimitives;
39
+ };
40
+ readonly settings: {
41
+ readonly enableCameraMouseRotation: false;
42
+ readonly itemSizesReference: 'screen';
43
+ readonly minEdgeThickness: 1.6;
44
+ readonly renderEdgeLabels: false;
45
+ readonly renderLabels: false;
46
+ };
47
+ readonly styles: {
48
+ readonly edges: {
49
+ readonly depth: 'edges';
50
+ readonly path: 'straight';
51
+ readonly size: {
52
+ readonly attribute: 'size';
53
+ };
54
+ };
55
+ readonly nodes: {
56
+ readonly depth: 'nodes';
57
+ readonly label: {
58
+ readonly attribute: 'label';
59
+ };
60
+ readonly labelVisibility: 'hidden';
61
+ readonly size: {
62
+ readonly attribute: 'size';
63
+ };
64
+ readonly x: {
65
+ readonly attribute: 'x';
66
+ };
67
+ readonly y: {
68
+ readonly attribute: 'y';
69
+ };
70
+ };
71
+ };
72
+ };
@@ -0,0 +1,36 @@
1
+ import { edgePrimitives } from './edge-program.js';
2
+ import { nodePrimitives } from './node-program.js';
3
+ const made = new WeakMap();
4
+ export const reducersOf = (sigma) => {
5
+ const nodes = sigma.getPrimitives().nodes;
6
+ return nodes ? made.get(nodes) : undefined;
7
+ };
8
+ export const sigmaOptions = (overrides = {}) => {
9
+ const nodes = nodePrimitives();
10
+ const reducers = {};
11
+ made.set(nodes, reducers);
12
+ return {
13
+ edgeReducer: (edge, _data, attributes) => reducers.edge?.(edge, attributes) ?? {},
14
+ nodeReducer: (node, _data, attributes) => reducers.node?.(node, attributes) ?? {},
15
+ primitives: { edges: edgePrimitives(), nodes },
16
+ settings: {
17
+ enableCameraMouseRotation: false,
18
+ itemSizesReference: 'screen',
19
+ minEdgeThickness: 1.6,
20
+ renderEdgeLabels: false,
21
+ renderLabels: false,
22
+ ...overrides.settings,
23
+ },
24
+ styles: {
25
+ edges: { depth: 'edges', path: 'straight', size: { attribute: 'size' } },
26
+ nodes: {
27
+ depth: 'nodes',
28
+ label: { attribute: 'label' },
29
+ labelVisibility: 'hidden',
30
+ size: { attribute: 'size' },
31
+ x: { attribute: 'x' },
32
+ y: { attribute: 'y' },
33
+ },
34
+ },
35
+ };
36
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix-graph",
3
3
  "description": "Connects the Helix Graph frame to a graph renderer: Sigma.js, cosmos.gl, three.js, or a driver of your own.",
4
- "version": "0.213.0",
4
+ "version": "0.215.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -60,12 +60,16 @@
60
60
  },
61
61
  "bugs": "https://github.com/project-fusion/fusion/issues",
62
62
  "dependencies": {
63
- "@fusion.dev/helix-core": "0.213.0"
63
+ "@fusion.dev/helix-core": "0.215.0"
64
64
  },
65
65
  "devDependencies": {
66
- "@cosmos.gl/graph": "^3.4.2",
67
- "@fusion.dev/helix-headless": "0.213.0",
68
- "@fusion.dev/helix-testing": "0.213.0",
66
+ "@cosmos.gl/graph": "^3.5.1",
67
+ "@fusion.dev/helix-headless": "0.215.0",
68
+ "@fusion.dev/helix-testing": "0.215.0",
69
+ "@luma.gl/core": "~9.3.6",
70
+ "@luma.gl/engine": "~9.3.6",
71
+ "@luma.gl/shadertools": "~9.3.6",
72
+ "@luma.gl/webgl": "~9.3.6",
69
73
  "@types/bun": "^1.4.2",
70
74
  "@types/three": "^0.186.0",
71
75
  "d3-force-3d": "^3.0.6",
@@ -73,7 +77,7 @@
73
77
  "graphology-layout-forceatlas2": "^0.10.1",
74
78
  "graphology-types": "^0.24.8",
75
79
  "meshoptimizer": "^1.3.0",
76
- "sigma": "^3.0.3",
80
+ "sigma": "^4.0.0",
77
81
  "three": "^0.186.1"
78
82
  },
79
83
  "homepage": "https://fusion.dev",
@@ -90,7 +94,7 @@
90
94
  "peerDependencies": {
91
95
  "@cosmos.gl/graph": "^3.4.0",
92
96
  "d3-force-3d": "^3.0.0",
93
- "sigma": "^3.0.0",
97
+ "sigma": "^4.0.0",
94
98
  "three": ">=0.160.0 <0.200.0"
95
99
  },
96
100
  "peerDependenciesMeta": {