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 CHANGED
@@ -1,13 +1,27 @@
1
1
  # polly-graph
2
2
 
3
- Reusable D3-based graph visualization SDK with configurable nodes, links, labels, interactions, and layout behaviors.
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
- * **Managed Root Architecture**: Provide one host element; the SDK internally manages the SVG canvas and the HTML UI overlay.
8
- * **Smart Positioning**: Controls and legends use a class-based system (`pg-pos-top-right`) with CSS variable overrides for precision offsets.
9
- * **Declarative Styling**: Fully customizable node and link aesthetics via style objects.
10
- * **Animated UI**: Legends feature directional "retraction" animations that sync with their anchor position.
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
- label: 'Core Service',
41
- style: {
42
- radius: 30,
43
- fill: '#7c3aed',
44
- stroke: '#5b21b6',
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
- fontSize: 12
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
- style: { stroke: '#94a3b8', strokeWidth: 2, opacity: 0.6 }
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
- dashed: false // Coming soon
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
- ### Angular
125
- ```ts
126
- @ViewChild('viewport') viewport!: ElementRef;
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
- ngAfterViewInit() {
129
- this.graph = createGraph({
130
- container: this.viewport.nativeElement,
131
- // ... config
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
- this.graph.render();
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