@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 +177 -27
- package/dist/index.cjs +13 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @3plate/graph-core
|
|
2
2
|
|
|
3
|
-
|
|
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 {
|
|
14
|
+
import { graph } from '@3plate/graph-core'
|
|
15
15
|
|
|
16
|
-
const
|
|
16
|
+
const api = await graph({
|
|
17
|
+
root: 'my-graph', // ID of the container element
|
|
17
18
|
nodes: [
|
|
18
|
-
{ id: '
|
|
19
|
-
{ id: '
|
|
19
|
+
{ id: 'a', title: 'Start' },
|
|
20
|
+
{ id: 'b', title: 'Process' },
|
|
21
|
+
{ id: 'c', title: 'End' },
|
|
20
22
|
],
|
|
21
|
-
edges: [
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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()) {
|