@sandeepsj0000/react-graph-visualizer 1.0.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/README.md +646 -0
- package/dist/GraphVisualizer.d.ts +3 -0
- package/dist/exportUtils.d.ts +14 -0
- package/dist/graphEngine.d.ts +96 -0
- package/dist/index.cjs.js +5513 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.esm.js +5503 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/types.d.ts +94 -0
- package/dist/useGraphEngine.d.ts +82 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
# react-graph-visualizer
|
|
2
|
+
|
|
3
|
+
Interactive React component **and** headless engine for graph visualization, node selection, path finding, and data export. Supports directed and undirected graphs.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Install](#install)
|
|
10
|
+
- [Quick Start — Visual Mode](#quick-start--visual-mode)
|
|
11
|
+
- [Quick Start — Headless Mode](#quick-start--headless-mode)
|
|
12
|
+
- [Data Format](#data-format)
|
|
13
|
+
- [Visual Component API](#visual-component-api)
|
|
14
|
+
- [Props](#props)
|
|
15
|
+
- [Interaction Guide](#interaction-guide)
|
|
16
|
+
- [Selection Modes](#selection-modes)
|
|
17
|
+
- [Headless APIs](#headless-apis)
|
|
18
|
+
- [GraphEngine (class)](#graphengine-class)
|
|
19
|
+
- [useGraphEngine (React hook)](#usegraphengine-react-hook)
|
|
20
|
+
- [Export & Serialization](#export--serialization)
|
|
21
|
+
- [Type Reference](#type-reference)
|
|
22
|
+
- [Examples](#examples)
|
|
23
|
+
- [Running the Demo](#running-the-demo)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install react-graph-visualizer
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Peer dependencies: `react >= 17`, `react-dom >= 17`.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Quick Start — Visual Mode
|
|
38
|
+
|
|
39
|
+
Render an interactive force-directed graph with built-in selection UI, sidebar, and export buttons.
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { GraphVisualizer } from "react-graph-visualizer";
|
|
43
|
+
|
|
44
|
+
const data = {
|
|
45
|
+
directed: false,
|
|
46
|
+
nodes: [
|
|
47
|
+
{ id: "a", label: "Alice" },
|
|
48
|
+
{ id: "b", label: "Bob" },
|
|
49
|
+
{ id: "c", label: "Charlie" },
|
|
50
|
+
{ id: "d", label: "Diana" },
|
|
51
|
+
],
|
|
52
|
+
edges: [
|
|
53
|
+
{ source: "a", target: "b" },
|
|
54
|
+
{ source: "b", target: "c" },
|
|
55
|
+
{ source: "c", target: "d" },
|
|
56
|
+
{ source: "a", target: "d" },
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function App() {
|
|
61
|
+
return (
|
|
62
|
+
<GraphVisualizer
|
|
63
|
+
data={data}
|
|
64
|
+
width={900}
|
|
65
|
+
height={600}
|
|
66
|
+
onSelectionChange={(selected, connected) => {
|
|
67
|
+
console.log("Selected:", selected, "Connected:", connected);
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Quick Start — Headless Mode
|
|
77
|
+
|
|
78
|
+
Use `GraphEngine` directly when you don't need any UI — works in Node.js, tests, scripts, or server-side code.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { GraphEngine } from "react-graph-visualizer";
|
|
82
|
+
|
|
83
|
+
const engine = new GraphEngine({
|
|
84
|
+
directed: true,
|
|
85
|
+
nodes: [
|
|
86
|
+
{ id: "api", label: "API Gateway" },
|
|
87
|
+
{ id: "auth", label: "Auth Service" },
|
|
88
|
+
{ id: "db", label: "Database" },
|
|
89
|
+
{ id: "cache", label: "Cache" },
|
|
90
|
+
],
|
|
91
|
+
edges: [
|
|
92
|
+
{ source: "api", target: "auth" },
|
|
93
|
+
{ source: "auth", target: "db" },
|
|
94
|
+
{ source: "auth", target: "cache" },
|
|
95
|
+
{ source: "api", target: "db" },
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Connected nodes
|
|
100
|
+
engine.getConnectedNodes("auth");
|
|
101
|
+
// => ["db", "cache", "api"]
|
|
102
|
+
|
|
103
|
+
// Inverted selection — everything NOT connected to "auth"
|
|
104
|
+
engine.getInvertedSelection("auth");
|
|
105
|
+
// => [] (all nodes happen to be connected in this small graph)
|
|
106
|
+
|
|
107
|
+
// All paths from api to db
|
|
108
|
+
engine.findAllPaths("api", "db");
|
|
109
|
+
// => [
|
|
110
|
+
// { path: ["api", "auth", "db"], labels: ["API Gateway", "Auth Service", "Database"] },
|
|
111
|
+
// { path: ["api", "db"], labels: ["API Gateway", "Database"] }
|
|
112
|
+
// ]
|
|
113
|
+
|
|
114
|
+
// Shortest path
|
|
115
|
+
engine.findShortestPath("api", "db");
|
|
116
|
+
// => { path: ["api", "db"], labels: ["API Gateway", "Database"] }
|
|
117
|
+
|
|
118
|
+
// Graph stats
|
|
119
|
+
engine.getStats();
|
|
120
|
+
// => { nodeCount: 4, edgeCount: 4, directed: true, density: 0.3333, ... }
|
|
121
|
+
|
|
122
|
+
// Serialize results to CSV string (no DOM needed)
|
|
123
|
+
const csv = GraphEngine.toCSV(engine.getConnectedNodesWithLabels("auth"));
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Or use the **React hook** `useGraphEngine` for headless usage inside React apps (manages selection state for you):
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
import { useGraphEngine } from "react-graph-visualizer";
|
|
130
|
+
|
|
131
|
+
function MyCustomUI({ data }) {
|
|
132
|
+
const graph = useGraphEngine(data);
|
|
133
|
+
|
|
134
|
+
// Select a node programmatically
|
|
135
|
+
graph.selectNode("auth");
|
|
136
|
+
|
|
137
|
+
// Reactive results
|
|
138
|
+
console.log(graph.connectedNodes); // [{ id: "db", label: "Database" }, ...]
|
|
139
|
+
console.log(graph.invertedNodes); // nodes NOT connected
|
|
140
|
+
console.log(graph.paths); // auto-computed when 2+ selected
|
|
141
|
+
|
|
142
|
+
// Serialize current results
|
|
143
|
+
const csv = graph.toCSV();
|
|
144
|
+
const json = graph.pathsToJSON();
|
|
145
|
+
|
|
146
|
+
return <div>{/* build your own UI */}</div>;
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Data Format
|
|
153
|
+
|
|
154
|
+
### `GraphData`
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
interface GraphData {
|
|
158
|
+
nodes: GraphNode[];
|
|
159
|
+
edges: GraphEdge[];
|
|
160
|
+
directed?: boolean; // default: false
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### `GraphNode`
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
interface GraphNode {
|
|
168
|
+
id: string; // unique identifier
|
|
169
|
+
label?: string; // display name (defaults to id)
|
|
170
|
+
color?: string; // per-node color override
|
|
171
|
+
size?: number; // per-node radius override
|
|
172
|
+
metadata?: Record<string, unknown>; // arbitrary data attached to node
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### `GraphEdge`
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
interface GraphEdge {
|
|
180
|
+
source: string; // source node id
|
|
181
|
+
target: string; // target node id
|
|
182
|
+
label?: string; // edge label
|
|
183
|
+
weight?: number; // edge weight
|
|
184
|
+
metadata?: Record<string, unknown>; // arbitrary data attached to edge
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Visual Component API
|
|
191
|
+
|
|
192
|
+
### Props
|
|
193
|
+
|
|
194
|
+
| Prop | Type | Default | Description |
|
|
195
|
+
|------|------|---------|-------------|
|
|
196
|
+
| `data` | `GraphData` | **required** | Graph nodes and edges |
|
|
197
|
+
| `width` | `number` | `900` | SVG canvas width in pixels |
|
|
198
|
+
| `height` | `number` | `600` | SVG canvas height in pixels |
|
|
199
|
+
| `onSelectionChange` | `(selectedIds: string[], connectedIds: string[]) => void` | — | Fires when selection changes |
|
|
200
|
+
| `nodeColor` | `string` | `#4f46e5` | Default node fill color |
|
|
201
|
+
| `highlightColor` | `string` | `#f59e0b` | Color for selected / connected nodes |
|
|
202
|
+
| `dimColor` | `string` | `#d1d5db` | Color for non-relevant nodes |
|
|
203
|
+
| `invertColor` | `string` | `#ef4444` | Color for inverted selection nodes |
|
|
204
|
+
| `nodeRadius` | `number` | `20` | Default circle radius |
|
|
205
|
+
| `showEdgeLabels` | `boolean` | `false` | Render edge labels on the graph |
|
|
206
|
+
| `showArrows` | `boolean` | `true` | Show arrowheads on directed edges |
|
|
207
|
+
| `className` | `string` | — | CSS class applied to the outer container |
|
|
208
|
+
|
|
209
|
+
### Interaction Guide
|
|
210
|
+
|
|
211
|
+
| Action | Effect |
|
|
212
|
+
|--------|--------|
|
|
213
|
+
| **Click** a node | Select it (replaces any previous selection) |
|
|
214
|
+
| **Ctrl/Cmd + Click** a node | Toggle it into/out of a multi-selection |
|
|
215
|
+
| **Click** the background | Clear all selections |
|
|
216
|
+
| **Drag** a node | Reposition it in the force layout |
|
|
217
|
+
| **Scroll wheel** | Zoom in/out |
|
|
218
|
+
| **Click + drag** background | Pan the viewport |
|
|
219
|
+
|
|
220
|
+
### Selection Modes
|
|
221
|
+
|
|
222
|
+
The sidebar provides three toggle modes:
|
|
223
|
+
|
|
224
|
+
| Mode | Behavior |
|
|
225
|
+
|------|----------|
|
|
226
|
+
| **Connected** | Highlights direct neighbors of the selected node. Lists them in the sidebar. |
|
|
227
|
+
| **Inverted** | Highlights every node that is NOT a direct neighbor (selection inverter). |
|
|
228
|
+
| **Paths** | When 2+ nodes are selected, finds and lists all paths connecting them. Highlights path edges. |
|
|
229
|
+
|
|
230
|
+
The sidebar includes **Export** buttons to download results as CSV/JSON and the graph image as SVG/PNG.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Headless APIs
|
|
235
|
+
|
|
236
|
+
### `GraphEngine` (class)
|
|
237
|
+
|
|
238
|
+
Import: `import { GraphEngine } from "react-graph-visualizer"`
|
|
239
|
+
|
|
240
|
+
No React or DOM dependency. Works in Node.js, tests, server-side, scripts — anywhere.
|
|
241
|
+
|
|
242
|
+
#### Constructor
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
const engine = new GraphEngine(data: GraphData);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
#### Node Accessors
|
|
249
|
+
|
|
250
|
+
| Method | Returns | Description |
|
|
251
|
+
|--------|---------|-------------|
|
|
252
|
+
| `getNode(id)` | `GraphNode \| undefined` | Get full node object |
|
|
253
|
+
| `getLabel(id)` | `string` | Display label |
|
|
254
|
+
| `getNodeIds()` | `string[]` | All node ids |
|
|
255
|
+
| `getNodes()` | `GraphNode[]` | All node objects |
|
|
256
|
+
| `getEdges()` | `GraphEdge[]` | All edge objects |
|
|
257
|
+
| `hasNode(id)` | `boolean` | Check existence |
|
|
258
|
+
| `hasEdge(source, target)` | `boolean` | Check if edge exists |
|
|
259
|
+
| `getNodeInfo(id)` | `NodeInfo \| null` | Rich info: label, degrees, all neighbors |
|
|
260
|
+
|
|
261
|
+
#### Neighbor Queries
|
|
262
|
+
|
|
263
|
+
| Method | Returns | Description |
|
|
264
|
+
|--------|---------|-------------|
|
|
265
|
+
| `getConnectedNodes(id)` | `string[]` | All direct neighbors (in + out) |
|
|
266
|
+
| `getConnectedNodesWithLabels(id)` | `{id, label}[]` | Same, with labels |
|
|
267
|
+
| `getOutgoingNeighbors(id)` | `string[]` | Only outgoing (directed) |
|
|
268
|
+
| `getIncomingNeighbors(id)` | `string[]` | Only incoming (directed) |
|
|
269
|
+
|
|
270
|
+
#### Degree
|
|
271
|
+
|
|
272
|
+
| Method | Returns | Description |
|
|
273
|
+
|--------|---------|-------------|
|
|
274
|
+
| `getDegree(id)` | `number` | Total unique neighbors |
|
|
275
|
+
| `getOutDegree(id)` | `number` | Outgoing edge count |
|
|
276
|
+
| `getInDegree(id)` | `number` | Incoming edge count |
|
|
277
|
+
|
|
278
|
+
#### Selection Inverter
|
|
279
|
+
|
|
280
|
+
| Method | Returns | Description |
|
|
281
|
+
|--------|---------|-------------|
|
|
282
|
+
| `getInvertedSelection(id)` | `string[]` | Nodes NOT connected to `id` |
|
|
283
|
+
| `getInvertedSelectionWithLabels(id)` | `{id, label}[]` | Same, with labels |
|
|
284
|
+
|
|
285
|
+
#### Path Finding
|
|
286
|
+
|
|
287
|
+
| Method | Returns | Description |
|
|
288
|
+
|--------|---------|-------------|
|
|
289
|
+
| `findAllPaths(from, to, maxDepth?)` | `PathResult[]` | All paths via DFS (default depth limit: 10) |
|
|
290
|
+
| `findShortestPath(from, to)` | `PathResult \| null` | Shortest path via BFS |
|
|
291
|
+
| `findAllPathsBetweenMultiple(ids)` | `PathResult[]` | All pairwise paths between multiple nodes |
|
|
292
|
+
| `areConnected(from, to)` | `boolean` | Reachability check |
|
|
293
|
+
|
|
294
|
+
#### Edge Queries
|
|
295
|
+
|
|
296
|
+
| Method | Returns | Description |
|
|
297
|
+
|--------|---------|-------------|
|
|
298
|
+
| `getEdgesOfNode(id)` | `GraphEdge[]` | All edges touching a node |
|
|
299
|
+
| `getEdgesForNodes(idSet)` | `GraphEdge[]` | Edges where both endpoints are in the set |
|
|
300
|
+
| `getEdgesOnPaths(paths)` | `Set<string>` | Edge keys (`"a->b"`) on given paths |
|
|
301
|
+
|
|
302
|
+
#### Subgraph Extraction
|
|
303
|
+
|
|
304
|
+
| Method | Returns | Description |
|
|
305
|
+
|--------|---------|-------------|
|
|
306
|
+
| `getSubgraph(ids)` | `SubgraphResult` | Subgraph of specified nodes + their internal edges |
|
|
307
|
+
| `getNeighborhoodSubgraph(id)` | `SubgraphResult` | Node + all neighbors + connecting edges |
|
|
308
|
+
|
|
309
|
+
#### Graph Analytics
|
|
310
|
+
|
|
311
|
+
| Method | Returns | Description |
|
|
312
|
+
|--------|---------|-------------|
|
|
313
|
+
| `getStats()` | `GraphStats` | Node/edge count, density, avg/max/min degree, components, isolated nodes |
|
|
314
|
+
| `getNodesByDegree()` | `{id, label, degree}[]` | All nodes sorted by degree (descending) |
|
|
315
|
+
|
|
316
|
+
#### Serialization (string output, no DOM)
|
|
317
|
+
|
|
318
|
+
| Method | Returns | Description |
|
|
319
|
+
|--------|---------|-------------|
|
|
320
|
+
| `GraphEngine.toCSV(nodes)` | `string` | CSV of `{id, label}[]` |
|
|
321
|
+
| `GraphEngine.toJSON(nodes)` | `string` | JSON of `{id, label}[]` |
|
|
322
|
+
| `GraphEngine.pathsToCSV(paths)` | `string` | CSV of `PathResult[]` |
|
|
323
|
+
| `GraphEngine.pathsToJSON(paths)` | `string` | JSON of `PathResult[]` |
|
|
324
|
+
| `engine.toGraphJSON()` | `string` | Full graph data as JSON |
|
|
325
|
+
|
|
326
|
+
#### Meta
|
|
327
|
+
|
|
328
|
+
| Method | Returns | Description |
|
|
329
|
+
|--------|---------|-------------|
|
|
330
|
+
| `isDirected()` | `boolean` | Whether graph is directed |
|
|
331
|
+
| `nodeCount()` | `number` | Number of nodes |
|
|
332
|
+
| `edgeCount()` | `number` | Number of edges |
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
### `useGraphEngine` (React hook)
|
|
337
|
+
|
|
338
|
+
Import: `import { useGraphEngine } from "react-graph-visualizer"`
|
|
339
|
+
|
|
340
|
+
A React hook that wraps `GraphEngine` with reactive selection state. Use it to build your own custom UI or to drive headless logic inside a React app.
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
const graph = useGraphEngine(data: GraphData): UseGraphEngineReturn;
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
#### Selection State
|
|
347
|
+
|
|
348
|
+
| Property / Method | Type | Description |
|
|
349
|
+
|-------------------|------|-------------|
|
|
350
|
+
| `selectedNodes` | `string[]` | Currently selected node ids |
|
|
351
|
+
| `selectNode(id)` | `void` | Select a single node (clears previous) |
|
|
352
|
+
| `toggleNode(id)` | `void` | Add/remove node from multi-selection |
|
|
353
|
+
| `selectNodes(ids)` | `void` | Select multiple nodes at once |
|
|
354
|
+
| `clearSelection()` | `void` | Deselect everything |
|
|
355
|
+
|
|
356
|
+
#### Reactive Results (auto-update when selection changes)
|
|
357
|
+
|
|
358
|
+
| Property | Type | Description |
|
|
359
|
+
|----------|------|-------------|
|
|
360
|
+
| `connectedNodes` | `{id, label}[]` | Neighbors of all selected nodes |
|
|
361
|
+
| `invertedNodes` | `{id, label}[]` | Non-neighbors (when 1 node selected) |
|
|
362
|
+
| `paths` | `PathResult[]` | All paths between selected nodes (when 2+ selected) |
|
|
363
|
+
|
|
364
|
+
#### Direct Query Methods
|
|
365
|
+
|
|
366
|
+
Same as `GraphEngine` methods — `getNodeInfo`, `findAllPaths`, `findShortestPath`, `findAllPathsBetweenMultiple`, `areConnected`, `getInvertedSelection`, `getConnectedNodes`, `getSubgraph`, `getNeighborhoodSubgraph`, `getNodesByDegree`, `getStats`.
|
|
367
|
+
|
|
368
|
+
#### Serialization
|
|
369
|
+
|
|
370
|
+
| Method | Description |
|
|
371
|
+
|--------|-------------|
|
|
372
|
+
| `toCSV(nodes?)` | CSV string of provided nodes or current `connectedNodes` |
|
|
373
|
+
| `toJSON(nodes?)` | JSON string of provided nodes or current `connectedNodes` |
|
|
374
|
+
| `pathsToCSV(paths?)` | CSV string of provided paths or current `paths` |
|
|
375
|
+
| `pathsToJSON(paths?)` | JSON string of provided paths or current `paths` |
|
|
376
|
+
| `toGraphJSON()` | Full graph data as JSON string |
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## Export & Serialization
|
|
381
|
+
|
|
382
|
+
### Headless (string output — no DOM required)
|
|
383
|
+
|
|
384
|
+
Use `GraphEngine` static methods to get raw strings you can write to a file, send over an API, or process further:
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
import { GraphEngine } from "react-graph-visualizer";
|
|
388
|
+
|
|
389
|
+
const engine = new GraphEngine(data);
|
|
390
|
+
|
|
391
|
+
// Get CSV string of connected nodes
|
|
392
|
+
const csv = GraphEngine.toCSV(engine.getConnectedNodesWithLabels("a"));
|
|
393
|
+
// => "ID,Label\n\"a\",\"Alice\"\n..."
|
|
394
|
+
|
|
395
|
+
// Get JSON string of all paths
|
|
396
|
+
const json = GraphEngine.pathsToJSON(engine.findAllPaths("a", "d"));
|
|
397
|
+
|
|
398
|
+
// Full graph as JSON
|
|
399
|
+
const graphJson = engine.toGraphJSON();
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Browser (triggers file download — requires DOM)
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
import {
|
|
406
|
+
exportNodeListAsCSV,
|
|
407
|
+
exportNodeListAsJSON,
|
|
408
|
+
exportPathsAsCSV,
|
|
409
|
+
exportPathsAsJSON,
|
|
410
|
+
exportSVG,
|
|
411
|
+
exportPNG,
|
|
412
|
+
} from "react-graph-visualizer";
|
|
413
|
+
|
|
414
|
+
// Download a node list
|
|
415
|
+
exportNodeListAsCSV([{ id: "a", label: "Alice" }], "my-nodes.csv");
|
|
416
|
+
|
|
417
|
+
// Download paths
|
|
418
|
+
exportPathsAsJSON(paths, "routes.json");
|
|
419
|
+
|
|
420
|
+
// Download graph image (pass the SVG DOM element)
|
|
421
|
+
exportSVG(svgElement, "graph.svg");
|
|
422
|
+
exportPNG(svgElement, "graph.png", 2); // 2x scale
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## Type Reference
|
|
428
|
+
|
|
429
|
+
### `PathResult`
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
interface PathResult {
|
|
433
|
+
path: string[]; // ordered node ids
|
|
434
|
+
labels: string[]; // ordered node labels
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### `NodeInfo`
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
interface NodeInfo {
|
|
442
|
+
id: string;
|
|
443
|
+
label: string;
|
|
444
|
+
metadata?: Record<string, unknown>;
|
|
445
|
+
outDegree: number;
|
|
446
|
+
inDegree: number;
|
|
447
|
+
degree: number;
|
|
448
|
+
outgoingNeighbors: { id: string; label: string }[];
|
|
449
|
+
incomingNeighbors: { id: string; label: string }[];
|
|
450
|
+
allNeighbors: { id: string; label: string }[];
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### `GraphStats`
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
interface GraphStats {
|
|
458
|
+
nodeCount: number;
|
|
459
|
+
edgeCount: number;
|
|
460
|
+
directed: boolean;
|
|
461
|
+
density: number; // 0-1
|
|
462
|
+
avgDegree: number;
|
|
463
|
+
maxDegree: number;
|
|
464
|
+
minDegree: number;
|
|
465
|
+
connectedComponents: number; // weakly connected components
|
|
466
|
+
isolatedNodes: string[]; // nodes with 0 connections
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### `SubgraphResult`
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
interface SubgraphResult {
|
|
474
|
+
nodes: GraphNode[];
|
|
475
|
+
edges: GraphEdge[];
|
|
476
|
+
directed: boolean;
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Examples
|
|
483
|
+
|
|
484
|
+
### Headless: Find all paths and export to CSV (Node.js / script)
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
import { GraphEngine } from "react-graph-visualizer";
|
|
488
|
+
import { writeFileSync } from "fs";
|
|
489
|
+
|
|
490
|
+
const engine = new GraphEngine({
|
|
491
|
+
directed: false,
|
|
492
|
+
nodes: [
|
|
493
|
+
{ id: "1", label: "Start" },
|
|
494
|
+
{ id: "2", label: "Mid A" },
|
|
495
|
+
{ id: "3", label: "Mid B" },
|
|
496
|
+
{ id: "4", label: "End" },
|
|
497
|
+
],
|
|
498
|
+
edges: [
|
|
499
|
+
{ source: "1", target: "2" },
|
|
500
|
+
{ source: "1", target: "3" },
|
|
501
|
+
{ source: "2", target: "4" },
|
|
502
|
+
{ source: "3", target: "4" },
|
|
503
|
+
{ source: "2", target: "3" },
|
|
504
|
+
],
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const paths = engine.findAllPaths("1", "4");
|
|
508
|
+
console.log(`Found ${paths.length} paths`);
|
|
509
|
+
|
|
510
|
+
// Write to file
|
|
511
|
+
writeFileSync("paths.csv", GraphEngine.pathsToCSV(paths));
|
|
512
|
+
writeFileSync("paths.json", GraphEngine.pathsToJSON(paths));
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Headless: Graph analytics
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
const engine = new GraphEngine(data);
|
|
519
|
+
|
|
520
|
+
// Who are the most connected nodes?
|
|
521
|
+
const ranked = engine.getNodesByDegree();
|
|
522
|
+
console.log("Top nodes:", ranked.slice(0, 5));
|
|
523
|
+
|
|
524
|
+
// Overall graph health
|
|
525
|
+
const stats = engine.getStats();
|
|
526
|
+
console.log(`Density: ${stats.density}, Components: ${stats.connectedComponents}`);
|
|
527
|
+
console.log(`Isolated nodes: ${stats.isolatedNodes.join(", ") || "none"}`);
|
|
528
|
+
|
|
529
|
+
// Can user A reach user D?
|
|
530
|
+
console.log(engine.areConnected("a", "d")); // true/false
|
|
531
|
+
|
|
532
|
+
// What's the shortest route?
|
|
533
|
+
const shortest = engine.findShortestPath("a", "d");
|
|
534
|
+
console.log(shortest?.labels.join(" → "));
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### Headless: Selection inverter
|
|
538
|
+
|
|
539
|
+
```ts
|
|
540
|
+
const engine = new GraphEngine(data);
|
|
541
|
+
|
|
542
|
+
// "Show me everything NOT connected to node X"
|
|
543
|
+
const isolated = engine.getInvertedSelectionWithLabels("api");
|
|
544
|
+
console.log("Disconnected from API:", isolated);
|
|
545
|
+
const csv = GraphEngine.toCSV(isolated);
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### React hook: Custom UI with headless engine
|
|
549
|
+
|
|
550
|
+
```tsx
|
|
551
|
+
import { useGraphEngine } from "react-graph-visualizer";
|
|
552
|
+
|
|
553
|
+
function DependencyExplorer({ data }) {
|
|
554
|
+
const graph = useGraphEngine(data);
|
|
555
|
+
|
|
556
|
+
return (
|
|
557
|
+
<div>
|
|
558
|
+
<h3>Services</h3>
|
|
559
|
+
<ul>
|
|
560
|
+
{graph.engine.getNodes().map((node) => (
|
|
561
|
+
<li
|
|
562
|
+
key={node.id}
|
|
563
|
+
onClick={() => graph.selectNode(node.id)}
|
|
564
|
+
style={{
|
|
565
|
+
fontWeight: graph.selectedNodes.includes(node.id)
|
|
566
|
+
? "bold"
|
|
567
|
+
: "normal",
|
|
568
|
+
}}
|
|
569
|
+
>
|
|
570
|
+
{node.label ?? node.id} (degree: {graph.engine.getDegree(node.id)})
|
|
571
|
+
</li>
|
|
572
|
+
))}
|
|
573
|
+
</ul>
|
|
574
|
+
|
|
575
|
+
{graph.connectedNodes.length > 0 && (
|
|
576
|
+
<div>
|
|
577
|
+
<h4>Connected to selection:</h4>
|
|
578
|
+
<ul>
|
|
579
|
+
{graph.connectedNodes.map((n) => (
|
|
580
|
+
<li key={n.id}>{n.label}</li>
|
|
581
|
+
))}
|
|
582
|
+
</ul>
|
|
583
|
+
<button onClick={() => console.log(graph.toCSV())}>
|
|
584
|
+
Copy as CSV
|
|
585
|
+
</button>
|
|
586
|
+
</div>
|
|
587
|
+
)}
|
|
588
|
+
|
|
589
|
+
{graph.paths.length > 0 && (
|
|
590
|
+
<div>
|
|
591
|
+
<h4>Paths ({graph.paths.length}):</h4>
|
|
592
|
+
{graph.paths.map((p, i) => (
|
|
593
|
+
<div key={i}>{p.labels.join(" → ")}</div>
|
|
594
|
+
))}
|
|
595
|
+
<button onClick={() => console.log(graph.pathsToCSV())}>
|
|
596
|
+
Copy paths as CSV
|
|
597
|
+
</button>
|
|
598
|
+
</div>
|
|
599
|
+
)}
|
|
600
|
+
</div>
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
### Visual + headless combined
|
|
606
|
+
|
|
607
|
+
```tsx
|
|
608
|
+
import { GraphVisualizer, useGraphEngine } from "react-graph-visualizer";
|
|
609
|
+
|
|
610
|
+
function App() {
|
|
611
|
+
const graph = useGraphEngine(data);
|
|
612
|
+
|
|
613
|
+
return (
|
|
614
|
+
<div>
|
|
615
|
+
{/* Visual graph */}
|
|
616
|
+
<GraphVisualizer
|
|
617
|
+
data={data}
|
|
618
|
+
onSelectionChange={(sel) => graph.selectNodes(sel)}
|
|
619
|
+
/>
|
|
620
|
+
|
|
621
|
+
{/* Programmatic access alongside the visual */}
|
|
622
|
+
<pre>{graph.toGraphJSON()}</pre>
|
|
623
|
+
<pre>Stats: {JSON.stringify(graph.getStats(), null, 2)}</pre>
|
|
624
|
+
</div>
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
---
|
|
630
|
+
|
|
631
|
+
## Running the Demo
|
|
632
|
+
|
|
633
|
+
```bash
|
|
634
|
+
git clone <repo>
|
|
635
|
+
cd visualize-graph
|
|
636
|
+
npm install
|
|
637
|
+
npm run demo
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
Opens at `http://localhost:5173` with two sample graphs (social network + microservices).
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## License
|
|
645
|
+
|
|
646
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PathResult } from "./types";
|
|
2
|
+
export declare function downloadFile(content: string, filename: string, mime: string): void;
|
|
3
|
+
export declare function exportNodeListAsCSV(nodes: {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}[], filename?: string): void;
|
|
7
|
+
export declare function exportNodeListAsJSON(nodes: {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
}[], filename?: string): void;
|
|
11
|
+
export declare function exportPathsAsCSV(paths: PathResult[], filename?: string): void;
|
|
12
|
+
export declare function exportPathsAsJSON(paths: PathResult[], filename?: string): void;
|
|
13
|
+
export declare function exportSVG(svgElement: SVGSVGElement, filename?: string): void;
|
|
14
|
+
export declare function exportPNG(svgElement: SVGSVGElement, filename?: string, scale?: number): void;
|