@simplex-viz/react 0.1.2 → 0.1.3
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 +98 -64
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/ghackenberg/simplex-viz/main/docs/assets/logo-react.png" width="640" alt="SimplexViz / react — @simplex-viz/react" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# @simplex-viz/react
|
|
2
6
|
|
|
3
7
|
End-to-end type-safe React bindings, declarative JSX node renderers, and imperative controller hooks for `@simplex-viz/core` and `@simplex-viz/svg`.
|
|
@@ -6,7 +10,15 @@ End-to-end type-safe React bindings, declarative JSX node renderers, and imperat
|
|
|
6
10
|
- **Source & Monorepo:** <https://github.com/ghackenberg/simplex-viz/tree/main/packages/react>
|
|
7
11
|
- **Companion Packages:** [`@simplex-viz/core`](https://www.npmjs.com/package/@simplex-viz/core) · [`@simplex-viz/svg`](https://www.npmjs.com/package/@simplex-viz/svg)
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
## Declarative JSX Slot Architecture
|
|
14
|
+
|
|
15
|
+
In React, `<SimplexTopicGraph />` lets you drop custom JSX directly into cluster and content slots instead of hand-writing imperative SVG DOM code. The wireframe preview below illustrates how those two component slots populate a single visualization:
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<img src="https://raw.githubusercontent.com/ghackenberg/simplex-viz/main/docs/assets/preview-react.png" width="100%" alt="@simplex-viz/react preview showing minimalist renderClusterNode and renderContentNode JSX slot placeholders with automatic getBBox() collision sizing" />
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
*Figure: Architectural slot layout rendered with `@simplex-viz/react`. Dashed topic cards illustrate the `renderClusterNode` JSX slot positioned on a subtle 60° simplex mesh, while minimalist skeleton content cards represent `renderContentNode` slots complete with color accents, type/time metadata, and interactive selection highlights (amber for pinned, cyan for hovered). Every JSX element is measured automatically via offscreen `getBBox()` so custom React SVG cards remain strictly overlap-free.*
|
|
10
22
|
|
|
11
23
|
## Features
|
|
12
24
|
|
|
@@ -17,28 +29,21 @@ End-to-end type-safe React bindings, declarative JSX node renderers, and imperat
|
|
|
17
29
|
- **Full generic type inference**: Infers `<TItem, TContentVisual, TClusterVisual, TTagMeta, TClusterMeta>` end-to-end across `items`, `getId`, `getTags`, `deriveClusterMeta`, `visualResolver`, `renderContentNode`, `renderClusterNode`, hover events, and click targets.
|
|
18
30
|
- **`useSimplexGraphController`**: Imperative hook for live item mutations (`addItems`, `removeItems`, `setItems`), camera controls (`zoomBy`, `resetCamera`), and telemetry getters.
|
|
19
31
|
|
|
20
|
-
---
|
|
21
|
-
|
|
22
32
|
## Installation
|
|
23
33
|
|
|
34
|
+
Install the React wrapper together with its core engine and SVG scene-graph peer packages (`react >= 18.0.0` and `react-dom >= 18.0.0`):
|
|
35
|
+
|
|
24
36
|
```bash
|
|
25
37
|
npm install @simplex-viz/core @simplex-viz/svg @simplex-viz/react
|
|
26
38
|
```
|
|
27
39
|
|
|
28
|
-
|
|
40
|
+
## Quickstart: Declarative JSX Node Renderers
|
|
29
41
|
|
|
30
|
-
|
|
42
|
+
### Step 1 — Define Domain, Visual, and Cluster Metadata Types
|
|
31
43
|
|
|
32
|
-
|
|
44
|
+
Describe your domain item (`Article`), visual cache descriptors (`ArticleVisual`, `ClusterVisual`), and aggregate cluster stats (`ClusterStats`). TypeScript preserves these types across every callback prop on `<SimplexTopicGraph />`:
|
|
33
45
|
|
|
34
46
|
```tsx
|
|
35
|
-
import { useMemo, useState } from 'react';
|
|
36
|
-
import {
|
|
37
|
-
SimplexTopicGraph,
|
|
38
|
-
useSimplexGraphController,
|
|
39
|
-
} from '@simplex-viz/react';
|
|
40
|
-
import type { VisualResolver } from '@simplex-viz/core';
|
|
41
|
-
|
|
42
47
|
interface Article {
|
|
43
48
|
readonly slug: string;
|
|
44
49
|
readonly title: string;
|
|
@@ -64,6 +69,77 @@ interface ClusterStats {
|
|
|
64
69
|
readonly count: number;
|
|
65
70
|
readonly avgMinutes: number;
|
|
66
71
|
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Step 2 — Define Declarative JSX Node Renderers
|
|
75
|
+
|
|
76
|
+
Write standard SVG JSX centered around `(0, 0)` for individual items and topic clusters. `@simplex-viz/react` measures the resulting `<g>` subtree automatically via offscreen `getBBox()` whenever `getVisualCacheKey` changes, guaranteeing collision-free card placement:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import type {
|
|
80
|
+
ClusterDescriptor,
|
|
81
|
+
ContentLayoutContext,
|
|
82
|
+
NodeRenderState,
|
|
83
|
+
} from '@simplex-viz/core';
|
|
84
|
+
|
|
85
|
+
const renderArticleNode = (
|
|
86
|
+
article: Article,
|
|
87
|
+
ctx: ContentLayoutContext<Article, undefined, ClusterStats>,
|
|
88
|
+
state: NodeRenderState<ArticleVisual>,
|
|
89
|
+
) => (
|
|
90
|
+
<g>
|
|
91
|
+
<rect
|
|
92
|
+
x={-54}
|
|
93
|
+
y={-15}
|
|
94
|
+
width={108}
|
|
95
|
+
height={30}
|
|
96
|
+
rx={6}
|
|
97
|
+
fill="#0f172a"
|
|
98
|
+
stroke={state.interaction.isPinned ? '#f59e0b' : ctx.mixedColorHex}
|
|
99
|
+
strokeWidth={state.interaction.isHovered ? 2 : 1.2}
|
|
100
|
+
/>
|
|
101
|
+
<text textAnchor="middle" y={4} fill="#f8fafc" fontSize={9}>
|
|
102
|
+
{article.title.slice(0, 16)}
|
|
103
|
+
</text>
|
|
104
|
+
</g>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const renderTopicCluster = (
|
|
108
|
+
cluster: ClusterDescriptor<Article, undefined, ClusterStats>,
|
|
109
|
+
state: NodeRenderState<ClusterVisual>,
|
|
110
|
+
) => (
|
|
111
|
+
<g>
|
|
112
|
+
<rect
|
|
113
|
+
x={-68}
|
|
114
|
+
y={-20}
|
|
115
|
+
width={136}
|
|
116
|
+
height={40}
|
|
117
|
+
rx={8}
|
|
118
|
+
fill="#090d16"
|
|
119
|
+
stroke={state.interaction.isHovered ? '#f8fafc' : cluster.colorHex}
|
|
120
|
+
strokeWidth={1.6}
|
|
121
|
+
/>
|
|
122
|
+
<text textAnchor="middle" y={-2} fill="#f8fafc" fontSize={10} fontWeight={600}>
|
|
123
|
+
{`${cluster.code} · ${cluster.defaultLabel}`}
|
|
124
|
+
</text>
|
|
125
|
+
<text textAnchor="middle" y={12} fill="#94a3b8" fontSize={8.5}>
|
|
126
|
+
{`${cluster.meta.count} items · avg ${cluster.meta.avgMinutes.toFixed(0)}m`}
|
|
127
|
+
</text>
|
|
128
|
+
</g>
|
|
129
|
+
);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Step 3 — Initialize State, Controller Hook & Memoized `VisualResolver`
|
|
133
|
+
|
|
134
|
+
Inside your React component, set up selection state, create a typed controller handle with `useSimplexGraphController`, and memoize `visualResolver` with `useMemo` so re-renders never trigger unintended crossfade transitions:
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import { useMemo, useState } from 'react';
|
|
138
|
+
import {
|
|
139
|
+
SimplexTopicGraph,
|
|
140
|
+
useSimplexGraphController,
|
|
141
|
+
} from '@simplex-viz/react';
|
|
142
|
+
import type { VisualResolver } from '@simplex-viz/core';
|
|
67
143
|
|
|
68
144
|
export function KnowledgeAtlas({ articles }: { articles: readonly Article[] }) {
|
|
69
145
|
const [focusedClusterUid, setFocusedClusterUid] = useState<string | null>(null);
|
|
@@ -97,7 +173,13 @@ export function KnowledgeAtlas({ articles }: { articles: readonly Article[] }) {
|
|
|
97
173
|
}),
|
|
98
174
|
[],
|
|
99
175
|
);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Step 4 — Render `<SimplexTopicGraph />` with Camera Toolbar & Click Handling
|
|
179
|
+
|
|
180
|
+
Finally, wire the camera toolbar (`handle.zoomBy`, `handle.resetCamera`), pass the JSX renderers from Step 2, and handle left-clicks to toggle pinned items or isolate clicked topic clusters:
|
|
100
181
|
|
|
182
|
+
```tsx
|
|
101
183
|
return (
|
|
102
184
|
<div style={{ width: '100%', height: '640px', position: 'relative' }}>
|
|
103
185
|
<div style={{ position: 'absolute', top: 12, left: 12, zIndex: 10 }}>
|
|
@@ -120,48 +202,8 @@ export function KnowledgeAtlas({ articles }: { articles: readonly Article[] }) {
|
|
|
120
202
|
visualResolver={visualResolver}
|
|
121
203
|
pinnedIds={pinnedIds}
|
|
122
204
|
focusedClusterUid={focusedClusterUid}
|
|
123
|
-
renderContentNode={
|
|
124
|
-
|
|
125
|
-
<rect
|
|
126
|
-
x={-54}
|
|
127
|
-
y={-15}
|
|
128
|
-
width={108}
|
|
129
|
-
height={30}
|
|
130
|
-
rx={6}
|
|
131
|
-
fill="#0f172a"
|
|
132
|
-
stroke={state.interaction.isPinned ? '#f59e0b' : ctx.mixedColorHex}
|
|
133
|
-
strokeWidth={state.interaction.isHovered ? 2 : 1.2}
|
|
134
|
-
/>
|
|
135
|
-
<text
|
|
136
|
-
textAnchor="middle"
|
|
137
|
-
y={4}
|
|
138
|
-
fill="#f8fafc"
|
|
139
|
-
fontSize={9}
|
|
140
|
-
>
|
|
141
|
-
{article.title.slice(0, 16)}
|
|
142
|
-
</text>
|
|
143
|
-
</g>
|
|
144
|
-
)}
|
|
145
|
-
renderClusterNode={(cluster, state) => (
|
|
146
|
-
<g>
|
|
147
|
-
<rect
|
|
148
|
-
x={-68}
|
|
149
|
-
y={-20}
|
|
150
|
-
width={136}
|
|
151
|
-
height={40}
|
|
152
|
-
rx={8}
|
|
153
|
-
fill="#090d16"
|
|
154
|
-
stroke={state.interaction.isHovered ? '#f8fafc' : cluster.colorHex}
|
|
155
|
-
strokeWidth={1.6}
|
|
156
|
-
/>
|
|
157
|
-
<text textAnchor="middle" y={-2} fill="#f8fafc" fontSize={10} fontWeight={600}>
|
|
158
|
-
{`${cluster.code} · ${cluster.defaultLabel}`}
|
|
159
|
-
</text>
|
|
160
|
-
<text textAnchor="middle" y={12} fill="#94a3b8" fontSize={8.5}>
|
|
161
|
-
{`${cluster.meta.count} items · avg ${cluster.meta.avgMinutes.toFixed(0)}m`}
|
|
162
|
-
</text>
|
|
163
|
-
</g>
|
|
164
|
-
)}
|
|
205
|
+
renderContentNode={renderArticleNode}
|
|
206
|
+
renderClusterNode={renderTopicCluster}
|
|
165
207
|
onLeftClick={(target) => {
|
|
166
208
|
if (target.kind === 'content') {
|
|
167
209
|
setPinnedIds((prev) => {
|
|
@@ -184,11 +226,9 @@ export function KnowledgeAtlas({ articles }: { articles: readonly Article[] }) {
|
|
|
184
226
|
}
|
|
185
227
|
```
|
|
186
228
|
|
|
187
|
-
---
|
|
188
|
-
|
|
189
229
|
## Using Portable `SvgNodeView` Plugins (60 fps Without JSX Re-Renders)
|
|
190
230
|
|
|
191
|
-
For larger graphs or built-in shape morphing, pass `contentView` and `clusterView`
|
|
231
|
+
For larger graphs or built-in shape morphing without per-frame JSX overhead, instantiate `GeometricShapeSvgView` and `ClusterAnchorSvgView` once outside the component and pass them via `contentView` and `clusterView` instead of `renderContentNode` / `renderClusterNode`:
|
|
192
232
|
|
|
193
233
|
```tsx
|
|
194
234
|
import { SimplexTopicGraph } from '@simplex-viz/react';
|
|
@@ -216,8 +256,6 @@ export function FastGraph({ items, visualResolver }) {
|
|
|
216
256
|
|
|
217
257
|
> **Tip:** Memoize `visualResolver` (e.g., via `useMemo`) and keep `contentView` / `clusterView` instances stable outside render to avoid unnecessary crossfades. Changing `visualResolver` or switching view props intentionally triggers a smooth crossfade/morph transition.
|
|
218
258
|
|
|
219
|
-
---
|
|
220
|
-
|
|
221
259
|
## `<SimplexTopicGraph />` Props Reference
|
|
222
260
|
|
|
223
261
|
### Data, Typing & Metadata
|
|
@@ -264,8 +302,6 @@ export function FastGraph({ items, visualResolver }) {
|
|
|
264
302
|
| `onFpsUpdate` | `(fps: number) => void` |
|
|
265
303
|
| `controllerRef` | Ref populated with the underlying `SimplexSvgController` instance. |
|
|
266
304
|
|
|
267
|
-
---
|
|
268
|
-
|
|
269
305
|
## `useSimplexGraphController()` Hook
|
|
270
306
|
|
|
271
307
|
Returns `{ controllerRef, handle }`:
|
|
@@ -279,8 +315,6 @@ Returns `{ controllerRef, handle }`:
|
|
|
279
315
|
- `handle.getClusters()` — Returns current `ClusterDescriptor[]`.
|
|
280
316
|
- `handle.getRawController()` — Returns the underlying `SimplexSvgController | null`.
|
|
281
317
|
|
|
282
|
-
---
|
|
283
|
-
|
|
284
318
|
## License
|
|
285
319
|
|
|
286
320
|
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/react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "End-to-end type-safe React bindings, declarative JSX node renderers, and hooks for @simplex-viz/core and @simplex-viz/svg.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"LICENSE"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@simplex-viz/core": "^0.1.
|
|
24
|
-
"@simplex-viz/svg": "^0.1.
|
|
23
|
+
"@simplex-viz/core": "^0.1.3",
|
|
24
|
+
"@simplex-viz/svg": "^0.1.3"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"react": ">=18.0.0",
|