@fusion.dev/helix-docs 0.128.0 → 0.128.1

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.
@@ -0,0 +1,346 @@
1
+ ---
2
+ description: Add interactive graphs to your site, styled with your theme and drawn by Sigma.js, cosmos.gl, or three.js.
3
+ section: Essentials
4
+ title: Graphs
5
+ ---
6
+
7
+ # Graphs
8
+
9
+ Helix Graph puts a frame around a graph drawn by Sigma.js, cosmos.gl, three.js,
10
+ or a renderer of your own. The frame adds a searchable list of every note, zoom
11
+ and fullscreen buttons, and keyboard controls. You give the frame notes and
12
+ links, and `@fusion.dev/helix-graph` draws them with the renderer, colored by
13
+ your theme.
14
+
15
+ ## Add a graph
16
+
17
+ Install Helix, the graph connector, and Sigma.js with its layout:
18
+
19
+ ```sh
20
+ bun add @fusion.dev/helix @fusion.dev/helix-graph sigma graphology graphology-layout-forceatlas2
21
+ ```
22
+
23
+ Add the frame, with an element inside it for the renderer to draw in:
24
+
25
+ ```html
26
+ <helix-graph label="Research notes">
27
+ <div></div>
28
+ </helix-graph>
29
+ ```
30
+
31
+ Give the frame its notes and links, make the renderer, and connect the two:
32
+
33
+ ```js
34
+ import '@fusion.dev/helix'
35
+ import { connectGraph } from '@fusion.dev/helix-graph'
36
+ import { sigmaDriver } from '@fusion.dev/helix-graph/sigma'
37
+ import Graph from 'graphology'
38
+ import forceAtlas2 from 'graphology-layout-forceatlas2'
39
+ import FA2Layout from 'graphology-layout-forceatlas2/worker'
40
+ import Sigma from 'sigma'
41
+
42
+ const frame = document.querySelector('helix-graph')
43
+ frame.nodes = [
44
+ { id: 'roadmap', label: 'Roadmap', group: 'Product' },
45
+ { id: 'pricing', label: 'Pricing', group: 'Sales' },
46
+ ]
47
+ frame.links = [{ source: 'pricing', target: 'roadmap' }]
48
+
49
+ const sigma = new Sigma(new Graph(), frame.querySelector('div'))
50
+ const connection = connectGraph(
51
+ frame,
52
+ sigmaDriver(sigma, {
53
+ layout: (graph) => new FA2Layout(graph, { settings: forceAtlas2.inferSettings(graph) }),
54
+ }),
55
+ )
56
+ ```
57
+
58
+ `nodes` and `links` are properties. Set them in JavaScript, since a graph of
59
+ thousands of notes written as markup would be thousands of elements.
60
+
61
+ ## What the connector does
62
+
63
+ `connectGraph` links the frame to the renderer:
64
+
65
+ - It draws the notes and links, and lays out the notes that have no place.
66
+ - It colors each group from your theme's series colors, and the names in your
67
+ theme's typeface. It recolors the graph when your theme or mode changes.
68
+ After you replace a stylesheet, which changes no attribute, call
69
+ `connection.restyle()`.
70
+ - The note you select in the list is the note selected in the drawing, and the
71
+ other way round. The selected note's links stand out, and the rest of the
72
+ graph fades.
73
+ - The frame's zoom buttons and keys move the drawing.
74
+ - `connection.relayout()` lays the whole graph out again.
75
+ - `connection.disconnect()` puts back everything `connectGraph` changed.
76
+
77
+ ## Choose a renderer
78
+
79
+ | Renderer | Use it for | Install | Driver |
80
+ |---|---|---|---|
81
+ | Sigma.js | Graphs of thousands of notes | `sigma graphology graphology-layout-forceatlas2` | `sigmaDriver` from `@fusion.dev/helix-graph/sigma` |
82
+ | cosmos.gl | Graphs of tens of thousands of notes or more, laid out on the GPU | `@cosmos.gl/graph` | `cosmosDriver` from `@fusion.dev/helix-graph/cosmos` |
83
+ | three.js | Graphs in 3D, and graphs on the surface of a 3D model | `three d3-force-3d` | `threeDriver` from `@fusion.dev/helix-graph/three` |
84
+
85
+ None of the renderers is bundled with Helix, so you can use the version you
86
+ already have.
87
+
88
+ With cosmos.gl, make the graph in the frame's box and pass the box as well:
89
+
90
+ ```js
91
+ import { Graph } from '@cosmos.gl/graph'
92
+ import { cosmosDriver } from '@fusion.dev/helix-graph/cosmos'
93
+
94
+ const box = frame.querySelector('div')
95
+ const graph = new Graph(box, { attribution: '', enableSimulation: true, scalePointsOnZoom: false })
96
+ const connection = connectGraph(frame, cosmosDriver(graph, box))
97
+ ```
98
+
99
+ With three.js, pass the box, and the driver makes the renderer:
100
+
101
+ ```js
102
+ import { threeDriver } from '@fusion.dev/helix-graph/three'
103
+
104
+ const connection = connectGraph(frame, threeDriver(frame.querySelector('div')))
105
+ ```
106
+
107
+ Drag to turn a 3D graph, and use the wheel or a pinch to zoom. It turns slowly
108
+ on its own until someone handles it, unless they asked for reduced motion. Set
109
+ `autoRotate: false` to keep it still.
110
+
111
+ ## Free the renderer
112
+
113
+ Call `connection.disconnect()` when the graph leaves the page, then free the
114
+ renderer you made:
115
+
116
+ ```js
117
+ connection.disconnect()
118
+ sigma.kill() // Sigma.js
119
+ graph.destroy() // cosmos.gl
120
+ ```
121
+
122
+ `threeDriver` makes its own renderer, and `disconnect()` frees it. A graph you
123
+ remove from the page without disconnecting keeps its renderer running.
124
+
125
+ ## Keep the layout
126
+
127
+ A layout places every note that has no `x` and `y`, and moves only those. When
128
+ it settles, the frame sends `helix-graph-layout` with where each note ended.
129
+ Save the positions and pass them back as each note's `x` and `y`, and the graph
130
+ opens in the same places next time, with no layout to wait for:
131
+
132
+ ```js
133
+ frame.addEventListener('helix-graph-layout', (event) => {
134
+ localStorage.setItem('positions', JSON.stringify(event.detail.positions))
135
+ })
136
+ ```
137
+
138
+ ## Update the graph as your data changes
139
+
140
+ Give the frame new arrays whenever your data changes. The connector compares
141
+ them with what is drawn, by each note's `id` and each link's two ends, and
142
+ draws only what changed. New notes grow in beside their neighbors, and the rest
143
+ stay where they are:
144
+
145
+ ```js
146
+ socket.addEventListener('message', ({ data }) => {
147
+ const note = JSON.parse(data)
148
+ frame.nodes = [...frame.nodes, note]
149
+ frame.links = [...frame.links, { source: note.id, target: note.parent }]
150
+ })
151
+ ```
152
+
153
+ Call `signal()` to show something passing between two notes, such as a message
154
+ or a payment. A comet travels along the link between them, in the first note's
155
+ color turning to the second's, or in the color of a tone you give it:
156
+
157
+ ```js
158
+ frame.signal('checkout', 'payments', { tone: 'success' })
159
+ ```
160
+
161
+ When someone asked for reduced motion, the link lights up for a moment
162
+ instead. Screen readers do not announce signals, so say anything a reader needs
163
+ to know in your page's own words as well.
164
+
165
+ ## Style the notes
166
+
167
+ Each note takes these fields, as Helix's other components do:
168
+
169
+ | Field | Values | What it does |
170
+ |---|---|---|
171
+ | `group` | Any name | Colors the note from your theme's series. Each group's name is drawn over its notes |
172
+ | `shape` | `circle`, `square`, `diamond`, `ring` | Says what kind of note it is at a glance |
173
+ | `emphasis` | `solid`, `outlined`, `subtle` | Fills the note, rings it, or tints it |
174
+ | `material` | `glass`, `frosted` | Draws the note as a shaded sphere or as frosted glass |
175
+ | `tone` | `primary`, `accent`, `info`, `success`, `warning`, `danger`, `neutral` | Colors the note by what it means, over its group's color |
176
+ | `size` | A number | How large the note is. A note without one is as large as it is connected |
177
+
178
+ Set a default for every note with the driver's options, such as
179
+ `sigmaDriver(sigma, { material: 'glass', shape: 'circle' })`. cosmos.gl draws a
180
+ note's `shape` and not its `emphasis` or `material`.
181
+
182
+ ## Select a note
183
+
184
+ Pressing a note selects it, and pressing where no note is clears the selection.
185
+ The frame sends `helix-activate` with the note's `id`, and its `active`
186
+ property holds the selected note. Set `active` to select a note from your page,
187
+ and the frame brings it into view with its neighbors. Start with one selected
188
+ with `default-active`:
189
+
190
+ ```html
191
+ <helix-graph default-active="roadmap" label="Research notes">
192
+ <div></div>
193
+ </helix-graph>
194
+ ```
195
+
196
+ To keep the selection in your own state, cancel `helix-before-activate` and set
197
+ `active` yourself.
198
+
199
+ Set `depth` to show only the notes within that many links of the selected one,
200
+ for a graph too large to read at once:
201
+
202
+ ```html
203
+ <helix-graph default-active="roadmap" depth="2" label="Research notes">
204
+ <div></div>
205
+ </helix-graph>
206
+ ```
207
+
208
+ ## Draw the notes in a shape
209
+
210
+ Pass a `stencil` and the connector places the notes in its shape from the first
211
+ frame, with no layout. Each group takes a region of the shape. Give it an SVG
212
+ path and the size of the box it is drawn in:
213
+
214
+ ```js
215
+ connectGraph(frame, driver, {
216
+ stencil: { path: outline.getAttribute('d') ?? '', width: 220, height: 170 },
217
+ })
218
+ ```
219
+
220
+ `@fusion.dev/helix-graph/shapes` makes shapes from what you have:
221
+
222
+ | Function | Makes |
223
+ |---|---|
224
+ | `shapeFromSvg(svg)` | A shape from an SVG's paths, rectangles, circles, ellipses, and polygons |
225
+ | `shapeFromImage(image)` | A shape from an image's opaque pixels |
226
+ | `shapeFromPath(path, width, height)` | A shape from one SVG path |
227
+ | `inflate(shape, { depth })` | A solid from a flat shape, puffed out as a cushion is |
228
+ | `onSphere(shape)` | A flat map wrapped around a globe |
229
+ | `sphere()`, `torus()`, `cuboid()` | Solids |
230
+
231
+ With three.js, place the notes on the surface of a 3D model.
232
+ `shapeFromModel` reads a `.glb` or `.gltf` file, and is in its own entry,
233
+ `@fusion.dev/helix-graph/model`, since it loads three.js:
234
+
235
+ ```js
236
+ import { shapeFromModel } from '@fusion.dev/helix-graph/model'
237
+
238
+ const brain = await shapeFromModel('/models/brain.glb')
239
+ connectGraph(frame, threeDriver(box), {
240
+ stencil: {
241
+ ...brain,
242
+ place: 'surface',
243
+ regions: { Vision: 'occipital', Hearing: 'temporal' },
244
+ },
245
+ })
246
+ ```
247
+
248
+ - `place: 'surface'` puts the notes in a thin shell just inside the surface,
249
+ and draws the model under them. `place: 'fill'`, the default, puts them all
250
+ through it.
251
+ - `regions` sends each group to a named part of the shape. A model's parts are
252
+ its meshes, and an SVG's are its shapes with an `id`. A part the shape does
253
+ not have is reported in the console, with the parts it has.
254
+ - In TypeScript, write `place: 'surface' as const`, so the object keeps the
255
+ type the stencil takes.
256
+
257
+ ## Share the scroll with the page
258
+
259
+ By default a graph takes every gesture over it: the wheel zooms it, and one
260
+ finger moves it. A graph embedded in a page takes the wheel from a reader who
261
+ is scrolling past it. Set `gestures` on the frame to share them:
262
+
263
+ ```html
264
+ <helix-graph gestures="cooperative" label="Research notes">
265
+ <div></div>
266
+ </helix-graph>
267
+ ```
268
+
269
+ - `cooperative` leaves the wheel and one finger to the page. The graph takes
270
+ Ctrl or ⌘ with the wheel, and two fingers, and says so when a gesture passes
271
+ to the page. Use it for a graph embedded in a page.
272
+ - `activate` takes nothing until the graph is pressed, and gives the gestures
273
+ back on Escape, when the pointer leaves, or on a press elsewhere.
274
+ - `always`, the default, takes every gesture.
275
+
276
+ The keyboard and the frame's buttons work the same in every mode.
277
+
278
+ ## Show the graph large
279
+
280
+ Set `no-panel` to leave the search and the list out of the frame, so the
281
+ drawing takes all of it, as at the top of a page. A Notes button among the
282
+ controls opens them over the drawing:
283
+
284
+ ```html
285
+ <helix-graph gestures="cooperative" label="Research notes" no-panel>
286
+ <div></div>
287
+ </helix-graph>
288
+ ```
289
+
290
+ Set the frame's height with `--graph-block-size`, and the list's width with
291
+ `--graph-panel-size`.
292
+
293
+ ## Show the graph once it is drawn
294
+
295
+ When the layout settles, the frame sends `helix-graph-ready` and matches
296
+ `:state(ready)`. Its `ready` property is `true` from then on. To show a graph
297
+ only once it is laid out, fade it in on the state:
298
+
299
+ ```css
300
+ helix-graph > div {
301
+ opacity: 0;
302
+ transition: opacity 0.3s;
303
+ }
304
+
305
+ helix-graph:state(ready) > div {
306
+ opacity: 1;
307
+ }
308
+ ```
309
+
310
+ ## Keyboard and screen readers
311
+
312
+ - The frame is one tab stop. Arrow keys move the drawing, and the plus and
313
+ minus keys zoom it. Set `controls="pan zoom fullscreen"` to add buttons that
314
+ move it.
315
+ - The list is a listbox, named by `notes-label`. Arrow keys, Home, End, Page
316
+ Up, and Page Down move through it, and Enter or Space selects a note.
317
+ - In the search field, Down moves into the list, and Escape empties the search.
318
+ With `no-panel`, Escape on an empty search closes the list.
319
+ - Screen readers read the selected note first, then how each other note links
320
+ to it, such as "Linked from Roadmap", and the count as a search narrows it.
321
+ - Change the words with `search-label`, `count-label`, `selected-label`,
322
+ `links-to-label`, `linked-from-label`, and `linked-both-label`.
323
+
324
+ ## Write a driver
325
+
326
+ To use another renderer, write a driver: an object with four methods.
327
+
328
+ - `draw({ nodes, links })` draws the graph, each note at its `x` and `y`.
329
+ - `look(looks)` says how each note and link is drawn, from the theme and the
330
+ selection.
331
+ - `panBy(x, y, animate)` moves the view by a number of pixels.
332
+ - `zoomBy(direction, animate)` zooms in for `1` and out for `-1`.
333
+
334
+ Each optional method turns on a feature:
335
+
336
+ | Method | Turns on |
337
+ |---|---|
338
+ | `update` | Drawing only what changed, for live data |
339
+ | `settle` | Laying out notes that have no place |
340
+ | `onPress` | Selecting a note by pressing it in the drawing |
341
+ | `center`, `fit` | Bringing a selected note and its neighbors into view |
342
+ | `signal` | Signals between notes |
343
+ | `groups` | Each group's name over its notes |
344
+ | `gestures`, `focusTarget` | Sharing gestures with the page, and the frame's keys |
345
+ | `shape` | Drawing a stencil's shape |
346
+ | `stop` | Removing what the driver added when the graph disconnects |
@@ -63,6 +63,7 @@ each framework.
63
63
  | `@fusion.dev/helix-theme` | You are generating or validating a theme |
64
64
  | `@fusion.dev/helix-icons` | You are drawing an icon outside a component |
65
65
  | `@fusion.dev/helix-map` | You are drawing a map with MapLibre, Leaflet, or a renderer of your own. See [Maps](./maps) |
66
+ | `@fusion.dev/helix-graph` | You are drawing a graph with Sigma.js, cosmos.gl, three.js, or a renderer of your own. See [Graphs](./graphs) |
66
67
  | `@fusion.dev/helix-core` | You are building a new Helix component |
67
68
 
68
69
  React uses the elements directly. Helix ships a `jsx.d.ts` that types every