@3plate/graph-core 0.1.15 → 0.1.16

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @3plate/graph-core
2
2
 
3
- Graph library with stable layout and incremental updates.
3
+ Framework-agnostic core of [@3plate/graph](../../README.md) — a graph visualization library with stable layouts and incremental updates.
4
4
 
5
5
  ## Installation
6
6
 
@@ -11,32 +11,182 @@ npm install @3plate/graph-core
11
11
  ## Usage
12
12
 
13
13
  ```typescript
14
- import { createGraph } from '@3plate/graph-core'
14
+ import { graph } from '@3plate/graph-core'
15
15
 
16
- const graph = createGraph({
16
+ const api = await graph({
17
+ root: 'my-graph', // ID of the container element
17
18
  nodes: [
18
- { id: '1', label: 'Node 1' },
19
- { id: '2', label: 'Node 2' },
19
+ { id: 'a', title: 'Start' },
20
+ { id: 'b', title: 'Process' },
21
+ { id: 'c', title: 'End' },
20
22
  ],
21
- edges: [{ id: 'e1', source: '1', target: '2' }],
22
- })
23
- ```
24
-
25
- ## Features
26
-
27
- - Custom node rendering
28
- - Automatic graph layout
29
- - Stable layouts
30
- - Multiple orientations
31
- - Interactive features
32
- - Cycle detection
33
- - Edge styling
34
- - Pan and zoom
35
- - Mini-map
36
- - Animations
37
- - SVG for crisp details
38
- - Fast rendering
39
- - Builder mode
40
- - Incremental updates
41
- - Railroad-style edges
42
- - Configurable layout
23
+ edges: [
24
+ { source: 'a', target: 'b' },
25
+ { source: 'b', target: 'c' },
26
+ ],
27
+ })
28
+
29
+ // Incrementally update the graph
30
+ api.update(u => {
31
+ u.addNodes({ id: 'd', title: 'New Step' })
32
+ u.addEdges({ source: 'b', target: 'd' })
33
+ })
34
+ ```
35
+
36
+ ## Custom Node Rendering
37
+
38
+ Provide `renderNode` to control what appears inside each node. It receives your node data and must return an `HTMLElement`:
39
+
40
+ ```typescript
41
+ await graph({
42
+ root: 'my-graph',
43
+ nodes: myNodes,
44
+ edges: myEdges,
45
+ options: {
46
+ canvas: {
47
+ renderNode: (node) => {
48
+ const el = document.createElement('div')
49
+ el.innerHTML = `<strong>${node.title}</strong><p>${node.description}</p>`
50
+ return el
51
+ },
52
+ },
53
+ },
54
+ })
55
+ ```
56
+
57
+ ### Using a Framework Renderer (React, Vue, etc.)
58
+
59
+ To render framework components inside nodes, use `mountNode` alongside `renderNode`. `mountNode` is called after the node element is in the DOM — use it to mount your component and return a cleanup function:
60
+
61
+ ```typescript
62
+ import { createRoot, flushSync } from 'react-dom/client' // or any framework
63
+
64
+ await graph({
65
+ root: 'my-graph',
66
+ nodes: myNodes,
67
+ edges: myEdges,
68
+ options: {
69
+ canvas: {
70
+ // renderNode returns a placeholder element
71
+ renderNode: () => document.createElement('div'),
72
+
73
+ // mountNode renders into it synchronously so the graph can measure it
74
+ mountNode: (node, el) => {
75
+ const root = createRoot(el)
76
+ flushSync(() => root.render(<MyNodeCard node={node} />))
77
+ return () => root.unmount()
78
+ },
79
+ },
80
+ },
81
+ })
82
+ ```
83
+
84
+ > **Note:** Use `flushSync` (or your framework's equivalent synchronous flush) so the node has real dimensions when the layout engine measures it. The returned cleanup function is called automatically when the node is removed.
85
+
86
+ If you're using React, the `@3plate/graph-react` wrapper handles all of this for you — `renderNode` can return JSX directly.
87
+
88
+ ## Options
89
+
90
+ ### Graph layout
91
+
92
+ ```typescript
93
+ options: {
94
+ graph: {
95
+ orientation: 'TB', // 'TB' | 'BT' | 'LR' | 'RL' (default: 'TB')
96
+ nodeMargin: 15, // Space between nodes in the same layer
97
+ layerMargin: 5, // Space between layers
98
+ edgeSpacing: 10, // Space between parallel edges
99
+ turnRadius: 10, // Rounded corner radius for edge bends
100
+ nodeAlign: 'natural', // 'natural' | 'top' | 'bottom' | 'left' | 'right'
101
+ },
102
+ }
103
+ ```
104
+
105
+ ### Canvas
106
+
107
+ ```typescript
108
+ options: {
109
+ canvas: {
110
+ width: '100%', // Canvas width (default: '100%')
111
+ height: '100%', // Canvas height (default: '100%')
112
+ padding: 20, // Viewport padding in px (default: 20)
113
+ editable: false, // Enable interactive edit mode (default: false)
114
+ panZoom: true, // Enable pan and zoom (default: true)
115
+ colorMode: 'system', // 'light' | 'dark' | 'system' (default: 'system')
116
+ renderNode, // (node, props?) => HTMLElement
117
+ mountNode, // (node, el) => (() => void) | void
118
+ theme: {}, // Global theme overrides
119
+ nodeTypes: {}, // Per-type theme overrides
120
+ edgeTypes: {}, // Per-type edge color overrides
121
+ },
122
+ }
123
+ ```
124
+
125
+ ## API Methods
126
+
127
+ ```typescript
128
+ const api = await graph({ ... })
129
+
130
+ // Incremental updates
131
+ api.update(u => {
132
+ u.addNodes(node)
133
+ u.updateNodes(node)
134
+ u.deleteNodes(id)
135
+ u.addEdges(edge)
136
+ u.updateEdges(edge)
137
+ u.deleteEdges(id)
138
+ u.describe('Optional label for history')
139
+ })
140
+
141
+ // Replace the entire graph at once
142
+ api.replaceSnapshot(nodes, edges)
143
+
144
+ // History navigation
145
+ api.nav('first' | 'prev' | 'next' | 'last')
146
+
147
+ // Theming
148
+ api.setColorMode('light' | 'dark')
149
+ api.updateStyles({ theme, nodeTypes, edgeTypes })
150
+
151
+ // Cleanup
152
+ api.destroy()
153
+ ```
154
+
155
+ ## Events
156
+
157
+ ```typescript
158
+ await graph({
159
+ root: 'my-graph',
160
+ nodes: [...],
161
+ edges: [...],
162
+ events: {
163
+ nodeClick: (node) => { ... },
164
+ edgeClick: (edge) => { ... },
165
+ addNode: (props, done) => done({ id: generateId(), ...props }),
166
+ addEdge: (edge, done) => done(edge),
167
+ removeNode: (node, done) => done(true),
168
+ removeEdge: (edge, done) => done(true),
169
+ historyChange: (index, length) => { ... },
170
+ },
171
+ })
172
+ ```
173
+
174
+ ## Real-time Ingestion
175
+
176
+ ```typescript
177
+ // WebSocket
178
+ await graph({
179
+ root: 'my-graph',
180
+ ingestion: { type: 'websocket', url: 'ws://localhost:8787' },
181
+ })
182
+
183
+ // Polling a file or endpoint
184
+ await graph({
185
+ root: 'my-graph',
186
+ ingestion: { type: 'file', url: '/api/updates.ndjson', intervalMs: 1000 },
187
+ })
188
+ ```
189
+
190
+ ## License
191
+
192
+ **GNU General Public License v3.0**. Commercial licenses available — see the [root README](../../README.md) for details.
package/dist/index.cjs CHANGED
@@ -2014,6 +2014,8 @@ var Node2 = class {
2014
2014
  hovered;
2015
2015
  container;
2016
2016
  content;
2017
+ innerContent;
2018
+ cleanup;
2017
2019
  canvas;
2018
2020
  data;
2019
2021
  isDummy;
@@ -2028,10 +2030,13 @@ var Node2 = class {
2028
2030
  const size = canvas.dummyNodeSize;
2029
2031
  } else {
2030
2032
  const render = data.render ?? canvas.renderNode;
2031
- this.content = this.renderContent(render(data.data, data));
2033
+ const innerEl = render(data.data, data);
2034
+ this.innerContent = innerEl;
2035
+ this.content = this.renderContent(innerEl);
2032
2036
  }
2033
2037
  }
2034
2038
  remove() {
2039
+ this.cleanup?.();
2035
2040
  this.container.remove();
2036
2041
  }
2037
2042
  append() {
@@ -2972,6 +2977,13 @@ var Canvas = class {
2972
2977
  newNodes.set(data.data, node);
2973
2978
  this.measurement.appendChild(node.content);
2974
2979
  }
2980
+ if (this.mountNode) {
2981
+ for (const node of newNodes.values()) {
2982
+ if (node.innerContent) {
2983
+ node.cleanup = this.mountNode(node.data.data, node.innerContent) ?? void 0;
2984
+ }
2985
+ }
2986
+ }
2975
2987
  await new Promise(requestAnimationFrame);
2976
2988
  const isVertical = this.orientation === "TB" || this.orientation === "BT";
2977
2989
  for (const node of newNodes.values()) {