polly-graph 0.1.6 → 0.1.8
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 +308 -30
- package/dist/index.cjs +2915 -632
- package/dist/index.css +9 -5
- package/dist/index.d.cts +487 -8
- package/dist/index.d.ts +487 -8
- package/dist/index.js +2909 -622
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
# polly-graph
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A framework-independent TypeScript-based D3 graph visualization SDK that provides a comprehensive, reusable solution for creating interactive network graphs. Designed to work seamlessly across React, Angular, Vue, Svelte, or vanilla JavaScript applications.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
* **
|
|
9
|
-
* **
|
|
10
|
-
* **
|
|
7
|
+
### Core Architecture
|
|
8
|
+
* **Framework Independent**: Works with React, Vue, Angular, Svelte, or vanilla JavaScript
|
|
9
|
+
* **Managed Root Architecture**: Provide one host element; the SDK internally manages the SVG canvas and HTML UI overlay
|
|
10
|
+
* **TypeScript First**: Complete type safety with zero `any` or `unknown` usage in core modules
|
|
11
|
+
* **Modular Design**: Clean separation with GraphManager, RenderPipeline, and InteractionManager
|
|
12
|
+
|
|
13
|
+
### Interactions & Behavior
|
|
14
|
+
* **Advanced Selection System**: Proper layer hierarchy ensures selected links appear above unselected nodes
|
|
15
|
+
* **Smart Hover States**: Conflict resolution between hover and selection with visual feedback
|
|
16
|
+
* **Enhanced Zoom Range**: 0.01x to 10x zoom support for large graphs
|
|
17
|
+
* **Intelligent Drag**: Boundary-aware dragging with force continuation outside canvas
|
|
18
|
+
* **Touch-Friendly**: Responsive interactions across desktop and mobile
|
|
19
|
+
|
|
20
|
+
### Styling & Customization
|
|
21
|
+
* **Declarative Styling**: Fully customizable node and link aesthetics via style objects
|
|
22
|
+
* **Smart Positioning**: Controls and legends use corner anchoring with CSS variable overrides
|
|
23
|
+
* **Animated UI**: Legends feature directional retraction animations
|
|
24
|
+
* **Adaptive Forces**: Simulation parameters automatically adjust based on graph size
|
|
11
25
|
|
|
12
26
|
---
|
|
13
27
|
|
|
@@ -35,31 +49,61 @@ const viewport = document.getElementById('graph-viewport') as HTMLElement;
|
|
|
35
49
|
const graph = createGraph({
|
|
36
50
|
container: viewport,
|
|
37
51
|
nodes: [
|
|
38
|
-
{
|
|
39
|
-
id: 'n1',
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
{
|
|
53
|
+
id: 'n1',
|
|
54
|
+
type: 'service',
|
|
55
|
+
label: 'Core Service',
|
|
56
|
+
tooltip: 'Primary application service',
|
|
57
|
+
style: {
|
|
58
|
+
radius: 30,
|
|
59
|
+
fill: '#7c3aed',
|
|
60
|
+
stroke: '#5b21b6',
|
|
45
61
|
strokeWidth: 2,
|
|
46
|
-
textColor: '#ffffff'
|
|
47
|
-
|
|
48
|
-
|
|
62
|
+
textColor: '#ffffff'
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'n2',
|
|
67
|
+
type: 'database',
|
|
68
|
+
label: 'Database',
|
|
69
|
+
style: { radius: 25, fill: '#dc2626' }
|
|
49
70
|
}
|
|
50
71
|
],
|
|
51
72
|
links: [
|
|
52
|
-
{
|
|
53
|
-
source: 'n1',
|
|
54
|
-
target: 'n2',
|
|
55
|
-
|
|
73
|
+
{
|
|
74
|
+
source: 'n1',
|
|
75
|
+
target: 'n2',
|
|
76
|
+
label: 'connects to',
|
|
77
|
+
style: {
|
|
78
|
+
stroke: '#94a3b8',
|
|
79
|
+
strokeWidth: 2,
|
|
80
|
+
opacity: 0.8,
|
|
81
|
+
arrow: { enabled: true, size: 8 }
|
|
82
|
+
}
|
|
56
83
|
}
|
|
57
84
|
],
|
|
85
|
+
interaction: {
|
|
86
|
+
drag: { enabled: true },
|
|
87
|
+
hover: {
|
|
88
|
+
enabled: true,
|
|
89
|
+
tooltip: { enabled: true, theme: 'dark' }
|
|
90
|
+
},
|
|
91
|
+
selection: { enabled: true }
|
|
92
|
+
},
|
|
58
93
|
controls: { enabled: true, position: 'top-right' },
|
|
59
|
-
legend: { enabled: true, position: 'bottom-left' }
|
|
94
|
+
legend: { enabled: true, position: 'bottom-left', collapsible: true }
|
|
60
95
|
});
|
|
61
96
|
|
|
97
|
+
// Render the graph
|
|
62
98
|
graph.render();
|
|
99
|
+
|
|
100
|
+
// Event handling
|
|
101
|
+
graph.on('nodeSelect', (node, element) => {
|
|
102
|
+
console.log('Selected node:', node.label);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Cleanup when done
|
|
106
|
+
// graph.destroy();
|
|
63
107
|
```
|
|
64
108
|
|
|
65
109
|
---
|
|
@@ -77,13 +121,24 @@ Every node can have a unique appearance defined in its `style` object.
|
|
|
77
121
|
| `textColor` | `string` | Label color. |
|
|
78
122
|
|
|
79
123
|
### Link Styles
|
|
80
|
-
Links support custom coloring, thickness, and arrow markers.
|
|
124
|
+
Links support custom coloring, thickness, labels, and arrow markers.
|
|
81
125
|
```ts
|
|
82
126
|
style: {
|
|
83
127
|
stroke: '#cbd5e1',
|
|
84
128
|
strokeWidth: 1.5,
|
|
85
129
|
opacity: 0.8,
|
|
86
|
-
|
|
130
|
+
dashArray: '5,5', // Dashed lines
|
|
131
|
+
arrow: {
|
|
132
|
+
enabled: true,
|
|
133
|
+
size: 8,
|
|
134
|
+
fill: '#64748b'
|
|
135
|
+
},
|
|
136
|
+
label: {
|
|
137
|
+
enabled: true,
|
|
138
|
+
visibility: 'hover', // 'always' | 'hover' | 'selection'
|
|
139
|
+
backgroundFill: '#ffffff',
|
|
140
|
+
textColor: '#374151'
|
|
141
|
+
}
|
|
87
142
|
}
|
|
88
143
|
```
|
|
89
144
|
|
|
@@ -121,16 +176,239 @@ The Legend component is aware of its position.
|
|
|
121
176
|
|
|
122
177
|
## Framework Integration
|
|
123
178
|
|
|
124
|
-
###
|
|
125
|
-
|
|
126
|
-
|
|
179
|
+
### React
|
|
180
|
+
|
|
181
|
+
#### Basic Component
|
|
182
|
+
```tsx
|
|
183
|
+
import { useEffect, useRef } from 'react';
|
|
184
|
+
import { createGraph, GraphInstance } from 'polly-graph';
|
|
185
|
+
|
|
186
|
+
function GraphComponent({ nodes, links }) {
|
|
187
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
188
|
+
const graphRef = useRef<GraphInstance | null>(null);
|
|
189
|
+
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (containerRef.current) {
|
|
192
|
+
graphRef.current = createGraph({
|
|
193
|
+
container: containerRef.current,
|
|
194
|
+
nodes,
|
|
195
|
+
links,
|
|
196
|
+
controls: { enabled: true, position: 'top-right' },
|
|
197
|
+
legend: { enabled: true, position: 'bottom-left' }
|
|
198
|
+
});
|
|
199
|
+
graphRef.current.render();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return () => {
|
|
203
|
+
graphRef.current?.destroy();
|
|
204
|
+
};
|
|
205
|
+
}, [nodes, links]);
|
|
206
|
+
|
|
207
|
+
return <div ref={containerRef} style={{ width: '100%', height: '600px' }} />;
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### Custom Hook
|
|
212
|
+
```tsx
|
|
213
|
+
import { useEffect, useRef, useCallback } from 'react';
|
|
214
|
+
import { createGraph, GraphInstance, GraphConfig } from 'polly-graph';
|
|
215
|
+
|
|
216
|
+
function usePollyGraph(config: Omit<GraphConfig, 'container'>) {
|
|
217
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
218
|
+
const graphRef = useRef<GraphInstance | null>(null);
|
|
219
|
+
|
|
220
|
+
const initialize = useCallback(() => {
|
|
221
|
+
if (containerRef.current && !graphRef.current) {
|
|
222
|
+
graphRef.current = createGraph({
|
|
223
|
+
...config,
|
|
224
|
+
container: containerRef.current
|
|
225
|
+
});
|
|
226
|
+
graphRef.current.render();
|
|
227
|
+
}
|
|
228
|
+
}, [config]);
|
|
229
|
+
|
|
230
|
+
const destroy = useCallback(() => {
|
|
231
|
+
graphRef.current?.destroy();
|
|
232
|
+
graphRef.current = null;
|
|
233
|
+
}, []);
|
|
234
|
+
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
initialize();
|
|
237
|
+
return destroy;
|
|
238
|
+
}, [initialize, destroy]);
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
containerRef,
|
|
242
|
+
graph: graphRef.current,
|
|
243
|
+
reinitialize: () => {
|
|
244
|
+
destroy();
|
|
245
|
+
initialize();
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Usage
|
|
251
|
+
function App() {
|
|
252
|
+
const { containerRef } = usePollyGraph({
|
|
253
|
+
nodes: [...],
|
|
254
|
+
links: [...],
|
|
255
|
+
controls: { enabled: true }
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return <div ref={containerRef} style={{ width: '100%', height: '600px' }} />;
|
|
259
|
+
}
|
|
260
|
+
```
|
|
127
261
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
262
|
+
#### With Event Handling
|
|
263
|
+
```tsx
|
|
264
|
+
import { useEffect, useRef, useState } from 'react';
|
|
265
|
+
import { createGraph, GraphInstance, GraphNode, GraphLink } from 'polly-graph';
|
|
266
|
+
|
|
267
|
+
function InteractiveGraph({ nodes, links }) {
|
|
268
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
269
|
+
const graphRef = useRef<GraphInstance | null>(null);
|
|
270
|
+
const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null);
|
|
271
|
+
const [selectedLink, setSelectedLink] = useState<GraphLink | null>(null);
|
|
272
|
+
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
if (containerRef.current) {
|
|
275
|
+
graphRef.current = createGraph({
|
|
276
|
+
container: containerRef.current,
|
|
277
|
+
nodes,
|
|
278
|
+
links,
|
|
279
|
+
interaction: {
|
|
280
|
+
selection: { enabled: true },
|
|
281
|
+
hover: { enabled: true, tooltip: { enabled: true } }
|
|
282
|
+
},
|
|
283
|
+
controls: { enabled: true }
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Event listeners
|
|
287
|
+
const unsubscribeNode = graphRef.current.on('nodeSelect', (node) => {
|
|
288
|
+
setSelectedNode(node);
|
|
289
|
+
setSelectedLink(null);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const unsubscribeNodeDeselect = graphRef.current.on('nodeDeselect', () => {
|
|
293
|
+
setSelectedNode(null);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const unsubscribeLink = graphRef.current.on('linkSelect', (link) => {
|
|
297
|
+
setSelectedLink(link);
|
|
298
|
+
setSelectedNode(null);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const unsubscribeLinkDeselect = graphRef.current.on('linkDeselect', () => {
|
|
302
|
+
setSelectedLink(null);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
graphRef.current.render();
|
|
306
|
+
|
|
307
|
+
return () => {
|
|
308
|
+
unsubscribeNode();
|
|
309
|
+
unsubscribeNodeDeselect();
|
|
310
|
+
unsubscribeLink();
|
|
311
|
+
unsubscribeLinkDeselect();
|
|
312
|
+
graphRef.current?.destroy();
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}, [nodes, links]);
|
|
316
|
+
|
|
317
|
+
const handleClearSelection = () => {
|
|
318
|
+
graphRef.current?.clearSelection();
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const handleFitView = () => {
|
|
322
|
+
graphRef.current?.fitView();
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
327
|
+
<div style={{ padding: '1rem', borderBottom: '1px solid #e2e8f0' }}>
|
|
328
|
+
<button onClick={handleClearSelection}>Clear Selection</button>
|
|
329
|
+
<button onClick={handleFitView} style={{ marginLeft: '8px' }}>Fit View</button>
|
|
330
|
+
{selectedNode && (
|
|
331
|
+
<span style={{ marginLeft: '16px' }}>
|
|
332
|
+
Selected: {selectedNode.label || selectedNode.id}
|
|
333
|
+
</span>
|
|
334
|
+
)}
|
|
335
|
+
{selectedLink && (
|
|
336
|
+
<span style={{ marginLeft: '16px' }}>
|
|
337
|
+
Selected Link: {typeof selectedLink.source === 'string'
|
|
338
|
+
? selectedLink.source
|
|
339
|
+
: selectedLink.source.id} → {typeof selectedLink.target === 'string'
|
|
340
|
+
? selectedLink.target
|
|
341
|
+
: selectedLink.target.id}
|
|
342
|
+
</span>
|
|
343
|
+
)}
|
|
344
|
+
</div>
|
|
345
|
+
<div ref={containerRef} style={{ flex: 1, position: 'relative' }} />
|
|
346
|
+
</div>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Vue 3
|
|
352
|
+
```vue
|
|
353
|
+
<template>
|
|
354
|
+
<div ref="container" class="graph-container"></div>
|
|
355
|
+
</template>
|
|
356
|
+
|
|
357
|
+
<script setup>
|
|
358
|
+
import { ref, onMounted, onUnmounted } from 'vue';
|
|
359
|
+
import { createGraph } from 'polly-graph';
|
|
360
|
+
|
|
361
|
+
const container = ref(null);
|
|
362
|
+
let graph = null;
|
|
363
|
+
|
|
364
|
+
onMounted(() => {
|
|
365
|
+
graph = createGraph({
|
|
366
|
+
container: container.value,
|
|
367
|
+
nodes: [...],
|
|
368
|
+
links: [...]
|
|
132
369
|
});
|
|
133
|
-
|
|
370
|
+
graph.render();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
onUnmounted(() => {
|
|
374
|
+
graph?.destroy();
|
|
375
|
+
});
|
|
376
|
+
</script>
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Angular 18+ (Signal-based)
|
|
380
|
+
```ts
|
|
381
|
+
import { Component, ElementRef, viewChild, DestroyRef, effect } from '@angular/core';
|
|
382
|
+
import { inject } from '@angular/core';
|
|
383
|
+
import { createGraph, GraphInstance } from 'polly-graph';
|
|
384
|
+
|
|
385
|
+
@Component({
|
|
386
|
+
selector: 'app-graph',
|
|
387
|
+
template: '<div #viewport class="graph-container"></div>'
|
|
388
|
+
})
|
|
389
|
+
export class GraphComponent {
|
|
390
|
+
private readonly destroyRef = inject(DestroyRef);
|
|
391
|
+
private readonly viewport = viewChild.required<ElementRef>('viewport');
|
|
392
|
+
|
|
393
|
+
private graph: GraphInstance | null = null;
|
|
394
|
+
|
|
395
|
+
constructor() {
|
|
396
|
+
effect(() => {
|
|
397
|
+
const container = this.viewport()?.nativeElement;
|
|
398
|
+
if (container) {
|
|
399
|
+
this.graph = createGraph({
|
|
400
|
+
container,
|
|
401
|
+
nodes: [...],
|
|
402
|
+
links: [...]
|
|
403
|
+
});
|
|
404
|
+
this.graph.render();
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
this.destroyRef.onDestroy(() => {
|
|
409
|
+
this.graph?.destroy();
|
|
410
|
+
});
|
|
411
|
+
}
|
|
134
412
|
}
|
|
135
413
|
```
|
|
136
414
|
|