ngraph.svg 0.0.17 → 0.10.0
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/LICENSE +1 -1
- package/README.md +359 -5
- package/package.json +31 -24
- package/src/DrawContext.js +123 -0
- package/src/collections/EdgeCollection.js +715 -0
- package/src/collections/NodeCollection.js +1203 -0
- package/src/controls/createKineticAnimation.js +155 -0
- package/src/controls/createMouseController.js +98 -0
- package/src/controls/createPanZoom.js +212 -0
- package/src/controls/createTouchController.js +188 -0
- package/src/createScene.js +273 -0
- package/src/index.js +49 -0
- package/src/intersectShape.js +67 -0
- package/src/layout/ForceLayoutAdapter.js +757 -0
- package/src/layout/computeLayers.js +65 -0
- package/src/layout/computeStressedNodes.js +45 -0
- package/src/removeOverlaps.js +179 -0
- package/example/basic/bundle.js +0 -5915
- package/example/basic/index.html +0 -18
- package/example/basic/index.js +0 -5
- package/example/customNode/bundle.js +0 -5934
- package/example/customNode/index.html +0 -18
- package/example/customNode/index.js +0 -24
- package/example/customSpringLength/bundle.js +0 -5953
- package/example/customSpringLength/index.html +0 -18
- package/example/customSpringLength/index.js +0 -43
- package/index.js +0 -376
- package/lib/defaultLayout.js +0 -21
- package/lib/defaultUI.js +0 -31
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,15 +1,369 @@
|
|
|
1
1
|
# ngraph.svg
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
SVG-based graph visualization library with adaptive rendering. Designed for rendering large graphs with zoom-dependent detail levels, collision-based importance, and viewport culling.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Features
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **Adaptive rendering** — nodes show different detail levels based on zoom (dots → labels → cards)
|
|
8
|
+
- **Collision-based importance** — more important nodes get richer detail at lower zoom levels
|
|
9
|
+
- **Property functions** — style any property with a literal, `d => val`, or `(d, ctx) => val`
|
|
10
|
+
- **Performance** — element pooling, viewport culling via R-tree, batched DOM updates
|
|
11
|
+
- **Force-directed layout** — integrated with ngraph.forcelayout, with layered orchestration and stress refinement
|
|
12
|
+
- **Interactive controls** — pan, zoom, kinetic scrolling on mouse and touch
|
|
13
|
+
- **Directed edges** — arrow markers with automatic endpoint calculation on node boundaries
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
10
18
|
npm install ngraph.svg
|
|
11
19
|
```
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import createGraph from 'ngraph.graph';
|
|
25
|
+
import {
|
|
26
|
+
createScene, NodeCollection, EdgeCollection, ForceLayoutAdapter
|
|
27
|
+
} from 'ngraph.svg';
|
|
28
|
+
|
|
29
|
+
// 1. Create a graph
|
|
30
|
+
const graph = createGraph();
|
|
31
|
+
graph.addNode('a', { label: 'Node A' });
|
|
32
|
+
graph.addNode('b', { label: 'Node B' });
|
|
33
|
+
graph.addLink('a', 'b');
|
|
34
|
+
|
|
35
|
+
// 2. Create the scene
|
|
36
|
+
const scene = createScene(document.getElementById('container'), {
|
|
37
|
+
panZoom: { minZoom: 0.1, maxZoom: 50 }
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// 3. Create collections (graph binding auto-syncs nodes/edges)
|
|
41
|
+
const nodeCol = new NodeCollection({
|
|
42
|
+
graph,
|
|
43
|
+
data: (graphNode) => ({
|
|
44
|
+
label: graphNode.data?.label || graphNode.id,
|
|
45
|
+
}),
|
|
46
|
+
maxScale: 2,
|
|
47
|
+
levels: [
|
|
48
|
+
{ type: 'circle', radius: 2, fill: '#4a90d9' },
|
|
49
|
+
{ minZoom: 2,
|
|
50
|
+
layers: [
|
|
51
|
+
{ type: 'circle', radius: 4, fill: '#4a90d9' },
|
|
52
|
+
{ type: 'text', text: d => d.label, fontSize: 10, fill: '#fff',
|
|
53
|
+
anchor: 'top', offset: [0, -8] },
|
|
54
|
+
] },
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const edgeCol = new EdgeCollection({
|
|
59
|
+
graph,
|
|
60
|
+
nodeCollection: nodeCol,
|
|
61
|
+
directed: true,
|
|
62
|
+
color: '#666',
|
|
63
|
+
width: 1,
|
|
64
|
+
opacity: 0.5,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
scene.addCollection(edgeCol);
|
|
68
|
+
scene.addCollection(nodeCol);
|
|
69
|
+
|
|
70
|
+
// 4. Layout
|
|
71
|
+
const layout = new ForceLayoutAdapter(graph, { springLength: 50 });
|
|
72
|
+
layout.onUpdate((positions) => {
|
|
73
|
+
nodeCol.syncPositions(positions);
|
|
74
|
+
edgeCol.syncPositions(positions);
|
|
75
|
+
scene.requestRender();
|
|
76
|
+
});
|
|
77
|
+
layout.start();
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## API Reference
|
|
81
|
+
|
|
82
|
+
### createScene(container, options)
|
|
83
|
+
|
|
84
|
+
Creates an SVG scene for graph visualization.
|
|
85
|
+
|
|
86
|
+
**Parameters:**
|
|
87
|
+
- `container` — DOM element to attach the scene to
|
|
88
|
+
- `options.viewBox` — initial bounds `{ left, top, right, bottom }` (default: -100 to 100)
|
|
89
|
+
- `options.panZoom` — pan/zoom config `{ minZoom, maxZoom, enabled }` (default: 0.1–20)
|
|
90
|
+
|
|
91
|
+
**Returns:** Scene object with properties and methods:
|
|
92
|
+
- `svg` — the SVG root element
|
|
93
|
+
- `root` — the transform group containing all scene content
|
|
94
|
+
- `drawContext` — current DrawContext
|
|
95
|
+
- `addCollection(collection)` / `removeCollection(collection)` — manage collections
|
|
96
|
+
- `on(event, callback)` / `off(event, callback)` — events: `render`, `transform`, `resize`
|
|
97
|
+
- `requestRender()` — request render on next animation frame
|
|
98
|
+
- `getPanZoom()` — get the pan/zoom controller
|
|
99
|
+
- `flyTo(x, y, scale, duration)` — animated camera movement (returns Promise)
|
|
100
|
+
- `fitToView(bounds, padding)` — fit content to viewport
|
|
101
|
+
- `dispose()` — clean up resources
|
|
102
|
+
|
|
103
|
+
### NodeCollection
|
|
104
|
+
|
|
105
|
+
Manages batched node rendering with a MapLibre-inspired styling API.
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
const nodes = new NodeCollection({
|
|
109
|
+
graph, // ngraph instance (auto-binds nodes)
|
|
110
|
+
data: (graphNode) => ({...}), // extract data from graph node
|
|
111
|
+
maxScale: 2, // counter-scaling cap
|
|
112
|
+
|
|
113
|
+
levels: [
|
|
114
|
+
// Level 0: always visible, no importance gating
|
|
115
|
+
{ type: 'circle', radius: 2, fill: '#CFCCDF' },
|
|
116
|
+
|
|
117
|
+
// Level 1: importance-gated, any zoom
|
|
118
|
+
{ importance: d => d.importance,
|
|
119
|
+
layers: [
|
|
120
|
+
{ type: 'circle', radius: 3, fill: '#CFCCDF' },
|
|
121
|
+
{ type: 'text', text: d => d.name, fontSize: 10, fill: '#CFCCDF',
|
|
122
|
+
anchor: 'top', offset: [0, -8] },
|
|
123
|
+
] },
|
|
124
|
+
|
|
125
|
+
// Level 2: importance-gated, zoom >= 3.5
|
|
126
|
+
{ minZoom: 3.5, importance: d => d.importance,
|
|
127
|
+
layers: [
|
|
128
|
+
{ type: 'circle', radius: 4, fill: '#CFCCDF' },
|
|
129
|
+
{ type: 'text', text: d => d.name, fontSize: 11, fill: '#CFCCDF',
|
|
130
|
+
anchor: 'top', offset: [0, -10] },
|
|
131
|
+
{ type: 'text', text: d => d.version, fontSize: 9, fill: '#888',
|
|
132
|
+
anchor: 'bottom', offset: [0, 16], visible: d => !!d.version },
|
|
133
|
+
] },
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Methods:**
|
|
139
|
+
- `add({ id, x, y, data })` — add a node, returns node handle
|
|
140
|
+
- `remove(nodeOrId)` — remove a node
|
|
141
|
+
- `get(id)` — get node by ID
|
|
142
|
+
- `setPosition(node, x, y)` — update position
|
|
143
|
+
- `syncPositions(positions)` — sync positions from a `Map<id, {x, y}>`
|
|
144
|
+
- `getNodeAt(screenX, screenY, drawContext)` — hit testing (returns node ID or null)
|
|
145
|
+
- `getNodeShape(nodeId)` — get current shape in world coordinates (for arrow intersection)
|
|
146
|
+
- `setState(nodeOrId, key, value)` — set state (affects `ctx` in property functions + CSS class)
|
|
147
|
+
- `getState(nodeOrId, key)` — get state value
|
|
148
|
+
- `clearState(key)` — remove a state key from all nodes
|
|
149
|
+
- `beginBatch()` / `endBatch()` — batch DOM updates
|
|
150
|
+
- `forEach(callback)` — iterate nodes
|
|
151
|
+
- `count` — number of nodes (getter)
|
|
152
|
+
- `clear()` — remove all nodes
|
|
153
|
+
- `dispose()` — clean up resources
|
|
154
|
+
|
|
155
|
+
### EdgeCollection
|
|
156
|
+
|
|
157
|
+
Manages batched edge/line rendering.
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
const edges = new EdgeCollection({
|
|
161
|
+
graph, // ngraph instance (auto-binds edges)
|
|
162
|
+
nodeCollection: nodeCol, // for directed arrow endpoint calculation
|
|
163
|
+
directed: true, // add arrowhead markers
|
|
164
|
+
color: '#666', // literal, d => val, or (d, ctx) => val
|
|
165
|
+
width: 1,
|
|
166
|
+
opacity: 0.5,
|
|
167
|
+
arrowLength: 10, // screen pixels
|
|
168
|
+
arrowWidth: 5, // screen pixels
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Methods:**
|
|
173
|
+
- `add({ id, fromX, fromY, toX, toY, data })` — add an edge, returns edge handle
|
|
174
|
+
- `remove(edgeOrId)` — remove an edge
|
|
175
|
+
- `get(id)` — get edge by ID
|
|
176
|
+
- `setEndpoints(edge, fromX, fromY, toX, toY)` — update geometry
|
|
177
|
+
- `syncPositions(positions)` — sync endpoints from a `Map<id, {x, y}>` (handles directed arrow offsets)
|
|
178
|
+
- `setState(edgeOrId, key, value)` — set state
|
|
179
|
+
- `getState(edgeOrId, key)` — get state
|
|
180
|
+
- `clearState(key)` — clear state from all edges
|
|
181
|
+
- `beginBatch()` / `endBatch()` — batch DOM updates
|
|
182
|
+
- `forEach(callback)` — iterate edges
|
|
183
|
+
- `count` — number of edges (getter)
|
|
184
|
+
- `clear()` — remove all edges
|
|
185
|
+
- `dispose()` — clean up resources
|
|
186
|
+
|
|
187
|
+
### Property Functions
|
|
188
|
+
|
|
189
|
+
Every visual property (fill, radius, fontSize, opacity, etc.) can be:
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
fill: '#CFCCDF' // literal
|
|
193
|
+
fill: d => d.color // data-driven
|
|
194
|
+
fill: (d, ctx) => ctx.highlighted ? '#fff' : d.color // data + state
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The `ctx` object contains:
|
|
198
|
+
- `zoom` — current zoom level
|
|
199
|
+
- Any state keys set via `setState()` (e.g. `ctx.highlighted`, `ctx.dimmed`)
|
|
200
|
+
|
|
201
|
+
### Levels
|
|
202
|
+
|
|
203
|
+
Levels define zoom-dependent rendering with collision-based importance. They are ordered by `minZoom` (lowest first). At any given zoom, the highest applicable level is the candidate.
|
|
204
|
+
|
|
205
|
+
**Key properties:**
|
|
206
|
+
- `minZoom` — minimum zoom to show this level (default: 0)
|
|
207
|
+
- `maxZoom` — maximum zoom (optional)
|
|
208
|
+
- `importance` — function `d => 0..1` for collision gating. Omit for always-visible levels.
|
|
209
|
+
- `layers` — array of layer definitions
|
|
210
|
+
- `hitArea` — custom hit area for `render`-type layers: `{ type: 'rect', width, height }`
|
|
211
|
+
|
|
212
|
+
**How collision works:** Nodes with `importance` compete for screen space. More important nodes always win. If a node collides at level N, it falls back to level N-1, and so on down to level 0 (which has no importance gating and always renders).
|
|
213
|
+
|
|
214
|
+
**Shorthand** — a level with a single shape can omit the `layers` wrapper:
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
// These are equivalent:
|
|
218
|
+
{ type: 'circle', radius: 2, fill: '#CCC' }
|
|
219
|
+
{ layers: [{ type: 'circle', radius: 2, fill: '#CCC' }] }
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Level transitions** — when a node changes level, it cross-fades over ~150ms.
|
|
223
|
+
|
|
224
|
+
### Layer Types
|
|
225
|
+
|
|
226
|
+
| Type | Purpose | Key Properties |
|
|
227
|
+
|---|---|---|
|
|
228
|
+
| `circle` | Filled circle | `radius`, `fill`, `stroke`, `strokeWidth`, `opacity`, `filter` |
|
|
229
|
+
| `rect` | Rectangle | `width`, `height`, `rx`, `ry`, `fill`, `stroke`, `strokeWidth`, `opacity`, `filter` |
|
|
230
|
+
| `text` | Text label | `text`, `fontSize`, `fill`, `fontFamily`, `fontWeight`, `anchor`, `offset`, `maxWidth`, `opacity` |
|
|
231
|
+
| `render` | Custom SVG | `render: (data, ctx) => svgString` |
|
|
232
|
+
|
|
233
|
+
All layer types support `visible: d => boolean` for conditional rendering.
|
|
234
|
+
|
|
235
|
+
**Text anchoring:**
|
|
236
|
+
```js
|
|
237
|
+
{ type: 'text', text: d => d.name, fontSize: 10,
|
|
238
|
+
anchor: 'top', // 'top' | 'bottom' | 'left' | 'right' | 'center'
|
|
239
|
+
offset: [0, -6] } // [dx, dy] in counter-scaled pixels
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Text word-wrap:**
|
|
243
|
+
```js
|
|
244
|
+
{ type: 'text', text: d => d.description, fontSize: 8,
|
|
245
|
+
maxWidth: 120 } // wraps into multiple lines using <tspan> elements
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### ForceLayoutAdapter
|
|
249
|
+
|
|
250
|
+
Wraps ngraph.forcelayout with animation loop, position smoothing, layered orchestration, and stress refinement.
|
|
251
|
+
|
|
252
|
+
```js
|
|
253
|
+
const layout = new ForceLayoutAdapter(graph, {
|
|
254
|
+
springLength: 30,
|
|
255
|
+
springCoefficient: 0.0008,
|
|
256
|
+
gravity: -2.0,
|
|
257
|
+
dragCoefficient: 0.04,
|
|
258
|
+
theta: 0.8,
|
|
259
|
+
timeStep: 20,
|
|
260
|
+
smoothing: 0.15, // position interpolation (0 = very smooth, 1 = raw)
|
|
261
|
+
maxSpeed: 50, // max displacement per frame
|
|
262
|
+
energyThreshold: 0.003, // convergence threshold
|
|
263
|
+
stableFramesRequired: 10, // frames below threshold to confirm stability
|
|
264
|
+
layeredLayout: true, // enable orchestrated layout (default)
|
|
265
|
+
stressThreshold: 0.3, // edge stretch threshold for refinement
|
|
266
|
+
maxStressIterations: 200,
|
|
267
|
+
getNodeSize: (nodeId) => 10, // for size-aware spacing
|
|
268
|
+
nodePadding: 4, // padding between nodes
|
|
269
|
+
onStabilized: () => {}, // called when layout converges
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Methods:**
|
|
274
|
+
- `start()` / `stop()` — control animation loop
|
|
275
|
+
- `step()` — single layout iteration (async)
|
|
276
|
+
- `stabilize(maxIterations)` — run until stable (async)
|
|
277
|
+
- `getNodePosition(nodeId)` — get position (async)
|
|
278
|
+
- `setNodePosition(nodeId, x, y)` — set position (async)
|
|
279
|
+
- `pinNode(nodeId)` / `unpinNode(nodeId)` — lock/unlock position (async)
|
|
280
|
+
- `getPositions()` — get all positions as `Map<id, {x, y}>`
|
|
281
|
+
- `getBounds()` — get bounding box (async)
|
|
282
|
+
- `isStabilized()` / `isRunning()` — query state
|
|
283
|
+
- `onUpdate(callback)` — listen for position updates (called each frame with positions Map)
|
|
284
|
+
- `dispose()` — clean up
|
|
285
|
+
|
|
286
|
+
**Layered orchestration** (enabled by default):
|
|
287
|
+
1. Computes onion layers via iterative leaf peeling
|
|
288
|
+
2. Pins all nodes, unpins only the structural core
|
|
289
|
+
3. After core converges, progressively unpins outer layers
|
|
290
|
+
4. Detects and refines stretched edges (stress passes)
|
|
291
|
+
|
|
292
|
+
The layout automatically restarts when nodes/edges are added to the graph.
|
|
293
|
+
|
|
294
|
+
### State System
|
|
295
|
+
|
|
296
|
+
Both NodeCollection and EdgeCollection share the same state API. State keys appear as:
|
|
297
|
+
1. Properties on the `ctx` object passed to property functions
|
|
298
|
+
2. CSS classes on the SVG element (for external styling)
|
|
299
|
+
|
|
300
|
+
```js
|
|
301
|
+
// Set state
|
|
302
|
+
nodeCol.setState('node-1', 'highlighted', true);
|
|
303
|
+
edgeCol.setState('edge-1', 'dimmed', true);
|
|
304
|
+
|
|
305
|
+
// Use state in property functions
|
|
306
|
+
fill: (d, ctx) => ctx.highlighted ? '#fff' : '#ccc'
|
|
307
|
+
opacity: (d, ctx) => ctx.dimmed ? 0.2 : 1
|
|
308
|
+
|
|
309
|
+
// Clear state from all nodes/edges
|
|
310
|
+
nodeCol.clearState('highlighted');
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Graph Binding
|
|
314
|
+
|
|
315
|
+
When a `graph` option is passed to NodeCollection or EdgeCollection, the collection automatically syncs with the graph — adding/removing visual elements as nodes/edges are added/removed.
|
|
316
|
+
|
|
317
|
+
NodeCollection also requires a `data` callback when using graph binding:
|
|
318
|
+
|
|
319
|
+
```js
|
|
320
|
+
const nodeCol = new NodeCollection({
|
|
321
|
+
graph,
|
|
322
|
+
data: (graphNode) => ({
|
|
323
|
+
name: graphNode.data?.name || graphNode.id,
|
|
324
|
+
color: graphNode.data?.color || '#4a90d9',
|
|
325
|
+
}),
|
|
326
|
+
levels: [...],
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
EdgeCollection stores `fromId` and `toId` from the graph link in each edge's data automatically.
|
|
331
|
+
|
|
332
|
+
### Other Exports
|
|
333
|
+
|
|
334
|
+
**DrawContext** — transform/viewport state passed to all renderers:
|
|
335
|
+
- `screenToScene(screenX, screenY)` / `sceneToScreen(sceneX, sceneY)` — coordinate conversion
|
|
336
|
+
- `isVisible(x, y, radius)` — check viewport visibility
|
|
337
|
+
- `getVisibleBounds()` — visible area in scene coordinates
|
|
338
|
+
- `getNodeScreenSize(worldSize)` — world-to-screen size conversion
|
|
339
|
+
|
|
340
|
+
**removeOverlaps(rects, originalPositions, options)** — R-tree based overlap removal for post-layout node positioning. Two-phase: separation, then relaxation toward original positions.
|
|
341
|
+
|
|
342
|
+
**separatePair(a, b, padding)** — separate two overlapping rectangles.
|
|
343
|
+
|
|
344
|
+
**intersectCircle / intersectRect / intersectShape** — shape boundary intersection for directed edge arrow placement.
|
|
345
|
+
|
|
346
|
+
**computeLayers(graph)** — iterative leaf peeling (onion decomposition). Returns `{ layerMap, maxLayer }`.
|
|
347
|
+
|
|
348
|
+
**computeStressedNodes(graph, layout, threshold)** — find edges deviating from ideal length. Returns `Set<nodeId>`.
|
|
349
|
+
|
|
350
|
+
## Demos
|
|
351
|
+
|
|
352
|
+
Run the development server:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
npm run dev
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
- `demo/index.html` — random graph with adaptive detail levels
|
|
359
|
+
- `demo/chinese-vocab.html` — Chinese vocabulary hierarchy visualization
|
|
360
|
+
|
|
361
|
+
## Dependencies
|
|
362
|
+
|
|
363
|
+
- [ngraph.graph](https://github.com/anvaka/ngraph.graph) — graph data structure
|
|
364
|
+
- [ngraph.forcelayout](https://github.com/anvaka/ngraph.forcelayout) — force-directed layout
|
|
365
|
+
- [rbush](https://github.com/mourner/rbush) — R-tree spatial index
|
|
366
|
+
|
|
367
|
+
## License
|
|
14
368
|
|
|
15
369
|
MIT
|
package/package.json
CHANGED
|
@@ -1,33 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ngraph.svg",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "SVG-based graph visualization library with adaptive rendering",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"module": "./src/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./src/index.js",
|
|
11
|
+
"default": "./src/index.js"
|
|
12
|
+
}
|
|
9
13
|
},
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"vivagraph",
|
|
14
|
-
"ngraph"
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src"
|
|
15
17
|
],
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
"url": "https://github.com/anvaka/ngraph.svg"
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "vite",
|
|
20
|
+
"build": "vite build",
|
|
21
|
+
"preview": "vite preview"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
|
-
"
|
|
24
|
-
"ngraph.
|
|
25
|
-
"
|
|
26
|
-
"simplesvg": "0.1.0",
|
|
27
|
-
"ngraph.merge": "0.0.1",
|
|
28
|
-
"wheel": "0.0.1"
|
|
24
|
+
"ngraph.forcelayout": "^3.3.1",
|
|
25
|
+
"ngraph.graph": "^20.1.1",
|
|
26
|
+
"rbush": "^3.0.1"
|
|
29
27
|
},
|
|
30
28
|
"devDependencies": {
|
|
31
|
-
"
|
|
32
|
-
}
|
|
29
|
+
"vite": "^6.0.11"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"graph",
|
|
33
|
+
"visualization",
|
|
34
|
+
"svg",
|
|
35
|
+
"ngraph",
|
|
36
|
+
"adaptive"
|
|
37
|
+
],
|
|
38
|
+
"author": "",
|
|
39
|
+
"license": "MIT"
|
|
33
40
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DrawContext holds transform/viewport state passed to all renderers.
|
|
3
|
+
* It provides helpers for adaptive rendering calculations.
|
|
4
|
+
*/
|
|
5
|
+
export default class DrawContext {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.viewBox = { left: -100, top: -100, right: 100, bottom: 100 };
|
|
8
|
+
this.transform = { scale: 1, x: 0, y: 0 };
|
|
9
|
+
this.width = 0;
|
|
10
|
+
this.height = 0;
|
|
11
|
+
this.pixelRatio = typeof window !== 'undefined' ? window.devicePixelRatio : 1;
|
|
12
|
+
|
|
13
|
+
// Cached visible bounds to avoid per-node allocations
|
|
14
|
+
this._visibleBounds = { left: 0, top: 0, right: 0, bottom: 0 };
|
|
15
|
+
this._visibleBoundsDirty = true;
|
|
16
|
+
|
|
17
|
+
// Preallocated points to avoid per-call allocations
|
|
18
|
+
this._reusableScenePoint = { x: 0, y: 0 };
|
|
19
|
+
this._reusableScreenPoint = { x: 0, y: 0 };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Update the viewport dimensions
|
|
24
|
+
*/
|
|
25
|
+
setSize(width, height) {
|
|
26
|
+
this.width = width;
|
|
27
|
+
this.height = height;
|
|
28
|
+
this._visibleBoundsDirty = true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Update the viewBox bounds
|
|
33
|
+
*/
|
|
34
|
+
setViewBox(left, top, right, bottom) {
|
|
35
|
+
this.viewBox.left = left;
|
|
36
|
+
this.viewBox.top = top;
|
|
37
|
+
this.viewBox.right = right;
|
|
38
|
+
this.viewBox.bottom = bottom;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Update the transform state
|
|
43
|
+
*/
|
|
44
|
+
setTransform(scale, x, y) {
|
|
45
|
+
this.transform.scale = scale;
|
|
46
|
+
this.transform.x = x;
|
|
47
|
+
this.transform.y = y;
|
|
48
|
+
this._visibleBoundsDirty = true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Calculate how many pixels a node of given size occupies on screen.
|
|
53
|
+
* This is the core metric for adaptive rendering decisions.
|
|
54
|
+
*/
|
|
55
|
+
getNodeScreenSize(nodeSize) {
|
|
56
|
+
return nodeSize * this.transform.scale;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Convert screen coordinates to scene coordinates
|
|
61
|
+
*/
|
|
62
|
+
screenToScene(screenX, screenY) {
|
|
63
|
+
const p = this._reusableScenePoint;
|
|
64
|
+
p.x = (screenX - this.transform.x) / this.transform.scale;
|
|
65
|
+
p.y = (screenY - this.transform.y) / this.transform.scale;
|
|
66
|
+
return p;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Convert scene coordinates to screen coordinates
|
|
71
|
+
*/
|
|
72
|
+
sceneToScreen(sceneX, sceneY) {
|
|
73
|
+
const p = this._reusableScreenPoint;
|
|
74
|
+
p.x = sceneX * this.transform.scale + this.transform.x;
|
|
75
|
+
p.y = sceneY * this.transform.scale + this.transform.y;
|
|
76
|
+
return p;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get the visible bounds in scene coordinates
|
|
81
|
+
*/
|
|
82
|
+
/**
|
|
83
|
+
* Returns the cached visible bounds object. Callers must not modify it.
|
|
84
|
+
*/
|
|
85
|
+
getVisibleBounds() {
|
|
86
|
+
this._updateVisibleBounds();
|
|
87
|
+
return this._visibleBounds;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_updateVisibleBounds() {
|
|
91
|
+
if (this._visibleBoundsDirty) {
|
|
92
|
+
this._visibleBoundsDirty = false;
|
|
93
|
+
const s = this.transform.scale;
|
|
94
|
+
const tx = this.transform.x;
|
|
95
|
+
const ty = this.transform.y;
|
|
96
|
+
this._visibleBounds.left = (0 - tx) / s;
|
|
97
|
+
this._visibleBounds.top = (0 - ty) / s;
|
|
98
|
+
this._visibleBounds.right = (this.width - tx) / s;
|
|
99
|
+
this._visibleBounds.bottom = (this.height - ty) / s;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if a point with given radius is visible in the viewport
|
|
105
|
+
*/
|
|
106
|
+
isVisible(x, y, radius = 0) {
|
|
107
|
+
this._updateVisibleBounds();
|
|
108
|
+
const bounds = this._visibleBounds;
|
|
109
|
+
return (
|
|
110
|
+
x + radius >= bounds.left &&
|
|
111
|
+
x - radius <= bounds.right &&
|
|
112
|
+
y + radius >= bounds.top &&
|
|
113
|
+
y - radius <= bounds.bottom
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get the SVG transform string for the current state
|
|
119
|
+
*/
|
|
120
|
+
getTransformString() {
|
|
121
|
+
return `translate(${this.transform.x}, ${this.transform.y}) scale(${this.transform.scale})`;
|
|
122
|
+
}
|
|
123
|
+
}
|