@simplex-viz/svg 0.1.1 → 0.1.2

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.
Files changed (2) hide show
  1. package/README.md +252 -10
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,9 +1,22 @@
1
1
  # @simplex-viz/svg
2
2
 
3
- Framework-agnostic SVG rendering engine and pluggable `SvgNodeView` architecture for `@simplex-viz/core`. Zero dependency on React—embeds cleanly into Vanilla TypeScript, Vue, Angular, Svelte, and Web Components.
3
+ Framework-agnostic SVG rendering engine and pluggable `SvgNodeView` architecture for `@simplex-viz/core`. Zero dependency on React—embeds cleanly into Vanilla TypeScript, Vue, Angular, Svelte, Solid, and Web Components.
4
4
 
5
5
  - **Live Demo:** <https://hackenberg.tech/simplex-viz/>
6
6
  - **Source & Monorepo:** <https://github.com/ghackenberg/simplex-viz/tree/main/packages/svg>
7
+ - **Companion Packages:** [`@simplex-viz/core`](https://www.npmjs.com/package/@simplex-viz/core) · [`@simplex-viz/react`](https://www.npmjs.com/package/@simplex-viz/react)
8
+
9
+ ---
10
+
11
+ ## Features
12
+
13
+ - **Framework-agnostic SVG scene graph**: Keyed DOM reconciliation for background mesh triangles, highlighted edges, convex hull debug wireframes, cluster nodes, and content nodes.
14
+ - **Automatic offscreen `getBBox()` hull measurement**: Omit `measureConvexHull` on any `SvgNodeView` and `@simplex-viz/svg` automatically renders it in a hidden measurement bin, reads `getBBox()`, and caches a tight convex polygon for overlap-free layout.
15
+ - **Smooth 60 fps transitions**: Critically damped spring motion + specialized parametric `morph()` or automatic dual-layer crossfade when switching visual presets or mutating items.
16
+ - **Centralized root-SVG interactions**: Attaches pointer, click, right-click, wheel, drag-to-pan, and `ResizeObserver` listeners solely on the root `<svg>` element and delegates hit-testing to `@simplex-viz/core`'s exact convex geometry solver.
17
+ - **Four built-in view plugins**: Ready-to-use views for geometric shape morphing, multi-sector topic glyphs, vertex anchors, and dynamic text badges.
18
+
19
+ ---
7
20
 
8
21
  ## Installation
9
22
 
@@ -11,26 +24,255 @@ Framework-agnostic SVG rendering engine and pluggable `SvgNodeView` architecture
11
24
  npm install @simplex-viz/core @simplex-viz/svg
12
25
  ```
13
26
 
14
- ## Included `SvgNodeView` Plugins
27
+ ---
15
28
 
16
- - `GeometricShapeSvgView` — Circle, rounded diamond, hexagon, and 5-point star with 64-point parametric SVG path morphing.
17
- - `MultiSectorGlyphSvgView` — Multi-colored topic weight donut glyphs representing exact barycentric weights.
18
- - `BadgeClusterSvgView` — Dynamic text badges with automatic offscreen `getBBox()` convex hull measurement.
19
- - `ClusterAnchorSvgView` — Calibrated double-ring vertex anchors with code & member count.
20
-
21
- ## Quickstart (Vanilla TS)
29
+ ## Quickstart (Vanilla TypeScript)
22
30
 
23
31
  ```ts
24
32
  import {
25
33
  SimplexSvgController,
26
34
  GeometricShapeSvgView,
27
35
  ClusterAnchorSvgView,
36
+ type GeometricShapeVisual,
37
+ type ClusterAnchorVisual,
28
38
  } from '@simplex-viz/svg';
39
+ import type { VisualResolver } from '@simplex-viz/core';
40
+
41
+ interface Bookmark {
42
+ readonly url: string;
43
+ readonly title: string;
44
+ readonly topics: readonly string[];
45
+ readonly minutes: number;
46
+ }
47
+
48
+ const visualResolver: VisualResolver<
49
+ Bookmark,
50
+ GeometricShapeVisual,
51
+ ClusterAnchorVisual
52
+ > = {
53
+ resolveContentVisual: (bookmark, ctx) => ({
54
+ shape: bookmark.minutes > 10 ? 'star' : 'circle',
55
+ radius: 10,
56
+ fillHex: ctx.mixedColorHex,
57
+ strokeHex: '#07090E',
58
+ strokeWidth: 1.4,
59
+ }),
60
+ resolveClusterVisual: (cluster) => ({
61
+ code: cluster.code,
62
+ label: cluster.defaultLabel,
63
+ memberCount: cluster.members.length,
64
+ colorHex: cluster.colorHex,
65
+ radius: 22,
66
+ }),
67
+ getVisualCacheKey: (visual) =>
68
+ 'shape' in visual
69
+ ? `${visual.shape}:${visual.radius}:${visual.fillHex}`
70
+ : `${visual.code}:${visual.memberCount}:${visual.colorHex}`,
71
+ };
29
72
 
30
- const controller = new SimplexSvgController(containerEl, {
31
- items,
73
+ const container = document.getElementById('graph')!;
74
+
75
+ const controller = new SimplexSvgController(container, {
76
+ items: myBookmarks,
77
+ getId: (b) => b.url,
78
+ getTags: (b) => b.topics,
32
79
  visualResolver,
33
80
  contentView: new GeometricShapeSvgView(),
34
81
  clusterView: new ClusterAnchorSvgView(),
82
+ meshOptions: {
83
+ innerPadding: 48,
84
+ minSideLength: 140,
85
+ maxSideLength: 280,
86
+ maxClusters: 10,
87
+ },
88
+ layoutOptions: {
89
+ clusterGapPx: 14,
90
+ itemGapPx: 5,
91
+ },
92
+ interactions: {
93
+ onContentHover: (ev) => {
94
+ if (ev) console.log('Hovered item:', ev.item.title, ev.screenPosition);
95
+ },
96
+ onLeftClick: (target) => {
97
+ if (target.kind === 'content') {
98
+ console.log('Clicked:', target.item.title);
99
+ } else if (target.kind === 'cluster') {
100
+ controller.setFocusedClusterUid(target.cluster.clusterUid);
101
+ } else {
102
+ controller.setFocusedClusterUid(null);
103
+ }
104
+ },
105
+ onRightClick: (target) => {
106
+ console.log('Context menu target:', target.kind);
107
+ return true; // Suppress browser default context menu
108
+ },
109
+ onWheel: 'zoom-to-cursor',
110
+ },
111
+ onMetricsChange: (metrics) => {
112
+ console.log(`Clusters: ${metrics.clusterCount}, Overlaps: ${metrics.overlapCount}`);
113
+ },
114
+ });
115
+
116
+ // Live mutations & camera controls
117
+ controller.addItems({
118
+ url: 'https://example.com/oklab',
119
+ title: 'Perceptual Color Mixing',
120
+ topics: ['oklab', 'color-science'],
121
+ minutes: 12,
35
122
  });
123
+ controller.removeItems('https://example.com/old');
124
+ controller.camera.reset();
125
+
126
+ // Clean up DOM, ResizeObserver, and animation loop on unmount
127
+ // controller.destroy();
36
128
  ```
129
+
130
+ ---
131
+
132
+ ## Built-In `SvgNodeView` Plugins
133
+
134
+ ### 1. `GeometricShapeSvgView`
135
+ Renders `'circle' | 'diamond' | 'star' | 'hexagon'` with analytical convex hulls and **64-point parametric SVG path morphing** between shapes and colors.
136
+
137
+ ```ts
138
+ import type { GeometricShapeVisual } from '@simplex-viz/svg';
139
+
140
+ // Descriptor shape:
141
+ const visual: GeometricShapeVisual = {
142
+ shape: 'hexagon', // 'circle' | 'diamond' | 'star' | 'hexagon'
143
+ radius: 11,
144
+ fillHex: ctx.mixedColorHex,
145
+ strokeHex: '#07090E',
146
+ strokeWidth: 1.4,
147
+ };
148
+ ```
149
+
150
+ ### 2. `MultiSectorGlyphSvgView`
151
+ Renders a multi-colored donut glyph whose sector arc angles match an item's exact barycentric weights across its active simplex unit (vertex, edge, or triangle).
152
+
153
+ ```ts
154
+ import type { MultiSectorGlyphVisual } from '@simplex-viz/svg';
155
+
156
+ const visual: MultiSectorGlyphVisual = {
157
+ outerRadius: 12,
158
+ innerRadius: 5,
159
+ sectors: ctx.unitClusters.map((uc) => ({
160
+ colorHex: uc.cluster.colorHex,
161
+ weight: uc.weight,
162
+ })),
163
+ centerColorHex: ctx.mixedColorHex,
164
+ };
165
+ ```
166
+
167
+ ### 3. `BadgeClusterSvgView`
168
+ Renders dynamic pill/card cluster badges sized to their title and metadata line. Intentionally omits `measureConvexHull` so `@simplex-viz/svg` measures the rendered SVG element via `getBBox()`.
169
+
170
+ ```ts
171
+ import type { BadgeClusterVisual } from '@simplex-viz/svg';
172
+
173
+ const visual: BadgeClusterVisual = {
174
+ code: cluster.code,
175
+ title: cluster.defaultLabel,
176
+ metaLine: `${cluster.members.length} items`,
177
+ colorHex: cluster.colorHex,
178
+ };
179
+ ```
180
+
181
+ ### 4. `ClusterAnchorSvgView`
182
+ Renders calibrated double-ring vertex anchors showing the cluster code and member count.
183
+
184
+ ```ts
185
+ import type { ClusterAnchorVisual } from '@simplex-viz/svg';
186
+
187
+ const visual: ClusterAnchorVisual = {
188
+ code: cluster.code,
189
+ label: cluster.defaultLabel,
190
+ memberCount: cluster.members.length,
191
+ colorHex: cluster.colorHex,
192
+ radius: 23,
193
+ };
194
+ ```
195
+
196
+ ---
197
+
198
+ ## Writing Custom `SvgNodeView` Plugins
199
+
200
+ Implement `SvgNodeView<TVisual>` centered around local origin `(0, 0)`. `SvgRendererAdapter` handles world positioning `(x, y)`, solver scale `α`, opacity, and crossfading on the parent `<g>`.
201
+
202
+ ```ts
203
+ import type { SvgNodeView } from '@simplex-viz/svg';
204
+ import { rectToConvexPolygon } from '@simplex-viz/core';
205
+
206
+ interface CardVisual {
207
+ readonly title: string;
208
+ readonly colorHex: string;
209
+ }
210
+
211
+ const SVG_NS = 'http://www.w3.org/2000/svg';
212
+
213
+ export const CustomCardSvgView: SvgNodeView<CardVisual> = {
214
+ // Optional: omit to measure automatically via offscreen getBBox()
215
+ measureConvexHull: (visual) =>
216
+ rectToConvexPolygon(Math.max(80, visual.title.length * 7 + 20), 28, 6),
217
+
218
+ mountOrUpdate: (groupEl, visual, state) => {
219
+ const doc = groupEl.ownerDocument;
220
+ while (groupEl.firstChild) groupEl.removeChild(groupEl.firstChild);
221
+
222
+ const width = Math.max(80, visual.title.length * 7 + 20);
223
+ const rect = doc.createElementNS(SVG_NS, 'rect');
224
+ rect.setAttribute('x', String(-width / 2));
225
+ rect.setAttribute('y', '-14');
226
+ rect.setAttribute('width', String(width));
227
+ rect.setAttribute('height', '28');
228
+ rect.setAttribute('rx', '6');
229
+ rect.setAttribute('fill', '#0d1320');
230
+ rect.setAttribute(
231
+ 'stroke',
232
+ state.interaction.isHovered ? '#f8fafc' : visual.colorHex,
233
+ );
234
+ rect.setAttribute('stroke-width', state.interaction.isHovered ? '2' : '1.2');
235
+
236
+ const text = doc.createElementNS(SVG_NS, 'text');
237
+ text.setAttribute('text-anchor', 'middle');
238
+ text.setAttribute('y', '4');
239
+ text.setAttribute('fill', '#f8fafc');
240
+ text.setAttribute('font-size', '11');
241
+ text.textContent = visual.title;
242
+
243
+ groupEl.appendChild(rect);
244
+ groupEl.appendChild(text);
245
+ },
246
+
247
+ // Optional: omit to use universal dual-layer crossfade
248
+ // morph: (groupEl, from, to, progress, state) => false,
249
+ };
250
+ ```
251
+
252
+ ---
253
+
254
+ ## `SimplexSvgController` API Summary
255
+
256
+ | Method / Property | Description |
257
+ | --- | --- |
258
+ | `addItems(items)` | Appends one or more items and triggers an interruptible transition. |
259
+ | `removeItems(ids)` | Removes items by ID and smoothly fades out exiting nodes. |
260
+ | `setItems(items)` | Replaces the entire corpus while preserving matching clusters & items. |
261
+ | `setViews({ contentView, clusterView })` | Hot-swaps `SvgNodeView` plugins at runtime. |
262
+ | `setVisualResolver(resolver)` | Updates descriptor resolution and crossfades smoothly. |
263
+ | `setSizeMultiplier(multiplier)` | Uniformly scales node sizes (`0.35` – `3.0`) and re-solves zero overlap. |
264
+ | `setShowHulls(show)` | Toggles convex bounding hull wireframe overlays. |
265
+ | `setPinnedIds(ids)` / `setHighlightedIds(ids)` | Controls pinned and highlighted visual emphasis states. |
266
+ | `setFocusedClusterUid(uid \| null)` | Isolates a cluster and its members, dimming unrelated nodes. |
267
+ | `setInteractions(handlers)` | Replaces pointer, click, and wheel callbacks dynamically. |
268
+ | `camera` | Exposes `CameraController` (`zoomToCursor`, `panBy`, `reset`, `getState`, `setState`). |
269
+ | `getMetrics()` / `getClusters()` / `getItems()` | Reads live solver telemetry, clusters, and items. |
270
+ | `getSvgElement()` | Returns the managed root `<svg>` element. |
271
+ | `stepForTest(dtMs)` | Deterministically advances simulation time in unit/DOM tests. |
272
+ | `destroy()` | Cancels `requestAnimationFrame`, disconnects `ResizeObserver`, and removes the `<svg>`. |
273
+
274
+ ---
275
+
276
+ ## License
277
+
278
+ MIT — see [LICENSE](https://github.com/ghackenberg/simplex-viz/blob/main/LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplex-viz/svg",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Framework-agnostic SVG rendering engine, offscreen getBBox() hull measurer, and pluggable SvgNodeView system for @simplex-viz/core.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  "LICENSE"
21
21
  ],
22
22
  "dependencies": {
23
- "@simplex-viz/core": "^0.1.1"
23
+ "@simplex-viz/core": "^0.1.2"
24
24
  },
25
25
  "keywords": [
26
26
  "simplex",