@cocorof/graphier 0.1.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 +139 -0
- package/dist/analysis.cjs +126 -0
- package/dist/analysis.cjs.map +1 -0
- package/dist/analysis.d.ts +126 -0
- package/dist/analysis.js +126 -0
- package/dist/analysis.js.map +1 -0
- package/dist/index.cjs +5152 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +310 -0
- package/dist/index.js +5136 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Graphier
|
|
2
|
+
|
|
3
|
+
High-performance 3D/2D graph renderer for React — powered by Three.js and d3-force-3d.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **2 GPU draw calls** — InstancedMesh (nodes) + LineSegments (edges) for maximum performance
|
|
10
|
+
- **Web Worker layout** — Force-directed simulation off the main thread (d3-force-3d)
|
|
11
|
+
- **Custom GLSL shaders** — Fresnel rim glow + subsurface scatter on every node
|
|
12
|
+
- **Post-processing** — UnrealBloomPass glow with adaptive resolution
|
|
13
|
+
- **Auto-adaptive** — Layout, LOD, bloom, and fog scale automatically with graph size
|
|
14
|
+
- **360 keyboard camera** — Full spherical rotation with quaternion-based controls, no gimbal lock
|
|
15
|
+
- **Incremental updates** — `appendData()` adds nodes/links without full rebuild
|
|
16
|
+
- **Position preservation** — Existing node positions survive data changes
|
|
17
|
+
- **Theme system** — 3 built-in presets (celestial, neon, minimal) + fully customizable
|
|
18
|
+
- **Node Detail Panel** — Modal with navigation history, connections list, and 3D subgraph
|
|
19
|
+
- **2D Subgraph View** — Lightweight canvas-based neighborhood renderer
|
|
20
|
+
- **Graph Analysis** — Tree-shakeable analytics module (`graphier/analysis`), zero Three.js dependency
|
|
21
|
+
- **TypeScript** — Full type safety with exported types
|
|
22
|
+
- **Dual ESM/CJS** — Works in all bundler configurations
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install graphier three react react-dom
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Peer dependencies:** `react >= 18`, `react-dom >= 18`, `three >= 0.150`
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { NetworkGraph3D } from "graphier";
|
|
36
|
+
|
|
37
|
+
const data = {
|
|
38
|
+
nodes: [
|
|
39
|
+
{ id: "alice", type: "person", label: "Alice", val: 10 },
|
|
40
|
+
{ id: "bob", type: "person", label: "Bob", val: 5 },
|
|
41
|
+
{ id: "project-x", type: "repo", label: "Project X", val: 20 },
|
|
42
|
+
],
|
|
43
|
+
links: [
|
|
44
|
+
{ source: "alice", target: "project-x", type: "owns" },
|
|
45
|
+
{ source: "bob", target: "project-x", type: "contributes" },
|
|
46
|
+
{ source: "alice", target: "bob", type: "follows" },
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default function App() {
|
|
51
|
+
return (
|
|
52
|
+
<div style={{ width: "100vw", height: "100vh" }}>
|
|
53
|
+
<NetworkGraph3D data={data} />
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Ref API
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
const graphRef = useRef<NetworkGraph3DRef>(null);
|
|
63
|
+
|
|
64
|
+
graphRef.current?.focusNode("alice", 1200);
|
|
65
|
+
graphRef.current?.zoomToFit(800, 100);
|
|
66
|
+
graphRef.current?.zoomIn();
|
|
67
|
+
graphRef.current?.zoomOut();
|
|
68
|
+
graphRef.current?.appendData(newNodes, newLinks);
|
|
69
|
+
graphRef.current?.screenshot();
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Theme & Style
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
<NetworkGraph3D
|
|
76
|
+
data={data}
|
|
77
|
+
theme="celestial" // or "neon", "minimal", or a ThemeConfig object
|
|
78
|
+
style={{
|
|
79
|
+
bloomStrength: 0.7,
|
|
80
|
+
fogDensity: 0.0004,
|
|
81
|
+
nodeMinSize: 2,
|
|
82
|
+
nodeMaxSize: 18,
|
|
83
|
+
showLabels: true,
|
|
84
|
+
maxLabels: 200,
|
|
85
|
+
}}
|
|
86
|
+
/>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Analysis Module
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { analyzeGraph } from "graphier/analysis";
|
|
93
|
+
|
|
94
|
+
const stats = analyzeGraph(data);
|
|
95
|
+
// stats.nodeCount, stats.density, stats.avgDegree, stats.topByDegree(10), ...
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Keyboard Controls
|
|
99
|
+
|
|
100
|
+
| Key | Action |
|
|
101
|
+
|-----|--------|
|
|
102
|
+
| `Z` / `X` | Zoom in / out |
|
|
103
|
+
| Arrow keys | 360 camera rotation |
|
|
104
|
+
| `Escape` | Deselect |
|
|
105
|
+
|
|
106
|
+
## Documentation
|
|
107
|
+
|
|
108
|
+
- [USAGE.md](./USAGE.md) — Full English documentation
|
|
109
|
+
- [USAGE.ko.md](./USAGE.ko.md) — 한국어 문서
|
|
110
|
+
|
|
111
|
+
## Next.js / SSR
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
// app/page.tsx
|
|
115
|
+
"use client";
|
|
116
|
+
import dynamic from "next/dynamic";
|
|
117
|
+
const GraphView = dynamic(() => import("./GraphView"), { ssr: false });
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
// next.config.js
|
|
122
|
+
const nextConfig = { transpilePackages: ["graphier"] };
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Architecture
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
NetworkGraph3D
|
|
129
|
+
├── InstancedMesh (all nodes → 1 draw call)
|
|
130
|
+
├── LineSegments (all edges → 1 draw call)
|
|
131
|
+
├── Web Worker (d3-force-3d layout, off main thread)
|
|
132
|
+
├── UnrealBloomPass (post-processing glow)
|
|
133
|
+
├── Sprite labels (distance-culled, texture-cached)
|
|
134
|
+
└── Keyboard controls (quaternion-based 360 rotation)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
function computeDegreeMap(data) {
|
|
4
|
+
const degree = {};
|
|
5
|
+
for (const n of data.nodes) degree[n.id] = 0;
|
|
6
|
+
for (const l of data.links) {
|
|
7
|
+
const s = typeof l.source === "object" ? l.source.id : l.source;
|
|
8
|
+
const t = typeof l.target === "object" ? l.target.id : l.target;
|
|
9
|
+
if (degree[s] !== void 0) degree[s]++;
|
|
10
|
+
if (degree[t] !== void 0) degree[t]++;
|
|
11
|
+
}
|
|
12
|
+
return degree;
|
|
13
|
+
}
|
|
14
|
+
function computeDegreeStats(degreeMap) {
|
|
15
|
+
const values = Object.values(degreeMap);
|
|
16
|
+
if (values.length === 0) {
|
|
17
|
+
return { min: 0, max: 0, avg: 0, total: 0 };
|
|
18
|
+
}
|
|
19
|
+
const total = values.reduce((a, b) => a + b, 0);
|
|
20
|
+
return {
|
|
21
|
+
min: Math.min(...values),
|
|
22
|
+
max: Math.max(...values),
|
|
23
|
+
avg: total / values.length,
|
|
24
|
+
total
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function topByDegree(nodes, degreeMap, n) {
|
|
28
|
+
return nodes.map((node) => ({ ...node, degree: degreeMap[node.id] ?? 0 })).sort((a, b) => b.degree - a.degree).slice(0, n);
|
|
29
|
+
}
|
|
30
|
+
function computeDensity(nodeCount, linkCount) {
|
|
31
|
+
if (nodeCount <= 1) return 0;
|
|
32
|
+
return 2 * linkCount / (nodeCount * (nodeCount - 1));
|
|
33
|
+
}
|
|
34
|
+
function computeLinkTypeBreakdown(links) {
|
|
35
|
+
const counts = {};
|
|
36
|
+
for (const l of links) {
|
|
37
|
+
const type = l.type ?? "default";
|
|
38
|
+
counts[type] = (counts[type] ?? 0) + 1;
|
|
39
|
+
}
|
|
40
|
+
return counts;
|
|
41
|
+
}
|
|
42
|
+
function computeNodeLinkCounts(data, linkTypes) {
|
|
43
|
+
const counts = /* @__PURE__ */ new Map();
|
|
44
|
+
for (const n of data.nodes) counts.set(n.id, 0);
|
|
45
|
+
const typeSet = new Set(linkTypes);
|
|
46
|
+
for (const l of data.links) {
|
|
47
|
+
if (!typeSet.has(l.type ?? "default")) continue;
|
|
48
|
+
const s = typeof l.source === "object" ? l.source.id : l.source;
|
|
49
|
+
const t = typeof l.target === "object" ? l.target.id : l.target;
|
|
50
|
+
if (counts.has(s)) counts.set(s, counts.get(s) + 1);
|
|
51
|
+
if (counts.has(t)) counts.set(t, counts.get(t) + 1);
|
|
52
|
+
}
|
|
53
|
+
return counts;
|
|
54
|
+
}
|
|
55
|
+
function findHubs(nodes, degreeMap, minDegree) {
|
|
56
|
+
return nodes.map((n) => ({ ...n, degree: degreeMap[n.id] ?? 0 })).filter((n) => n.degree >= minDegree).sort((a, b) => b.degree - a.degree);
|
|
57
|
+
}
|
|
58
|
+
function groupByType(nodes) {
|
|
59
|
+
const groups = {};
|
|
60
|
+
for (const n of nodes) {
|
|
61
|
+
const type = n.type ?? "default";
|
|
62
|
+
groups[type] = (groups[type] ?? 0) + 1;
|
|
63
|
+
}
|
|
64
|
+
return groups;
|
|
65
|
+
}
|
|
66
|
+
function buildAdjacencyMap(links) {
|
|
67
|
+
const adj = /* @__PURE__ */ new Map();
|
|
68
|
+
for (const l of links) {
|
|
69
|
+
const s = typeof l.source === "object" ? l.source.id : l.source;
|
|
70
|
+
const t = typeof l.target === "object" ? l.target.id : l.target;
|
|
71
|
+
if (!adj.has(s)) adj.set(s, []);
|
|
72
|
+
if (!adj.has(t)) adj.set(t, []);
|
|
73
|
+
adj.get(s).push(t);
|
|
74
|
+
adj.get(t).push(s);
|
|
75
|
+
}
|
|
76
|
+
return adj;
|
|
77
|
+
}
|
|
78
|
+
function getNeighbors(adjacencyMap, rootId, maxHops) {
|
|
79
|
+
const visited = /* @__PURE__ */ new Map();
|
|
80
|
+
visited.set(rootId, 0);
|
|
81
|
+
const queue = [rootId];
|
|
82
|
+
while (queue.length > 0) {
|
|
83
|
+
const current = queue.shift();
|
|
84
|
+
const dist = visited.get(current);
|
|
85
|
+
if (dist >= maxHops) continue;
|
|
86
|
+
for (const nb of adjacencyMap.get(current) ?? []) {
|
|
87
|
+
if (!visited.has(nb)) {
|
|
88
|
+
visited.set(nb, dist + 1);
|
|
89
|
+
queue.push(nb);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return visited;
|
|
94
|
+
}
|
|
95
|
+
function analyzeGraph(data) {
|
|
96
|
+
const degreeMap = computeDegreeMap(data);
|
|
97
|
+
const degreeStats = computeDegreeStats(degreeMap);
|
|
98
|
+
const density = computeDensity(data.nodes.length, data.links.length);
|
|
99
|
+
const nodesByType = groupByType(data.nodes);
|
|
100
|
+
const linksByType = computeLinkTypeBreakdown(data.links);
|
|
101
|
+
return {
|
|
102
|
+
nodeCount: data.nodes.length,
|
|
103
|
+
linkCount: data.links.length,
|
|
104
|
+
density,
|
|
105
|
+
avgDegree: degreeStats.avg,
|
|
106
|
+
maxDegree: degreeStats.max,
|
|
107
|
+
minDegree: degreeStats.min,
|
|
108
|
+
totalDegree: degreeStats.total,
|
|
109
|
+
degreeMap,
|
|
110
|
+
nodesByType,
|
|
111
|
+
linksByType,
|
|
112
|
+
topByDegree: (n) => topByDegree(data.nodes, degreeMap, n)
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
exports.analyzeGraph = analyzeGraph;
|
|
116
|
+
exports.buildAdjacencyMap = buildAdjacencyMap;
|
|
117
|
+
exports.computeDegreeMap = computeDegreeMap;
|
|
118
|
+
exports.computeDegreeStats = computeDegreeStats;
|
|
119
|
+
exports.computeDensity = computeDensity;
|
|
120
|
+
exports.computeLinkTypeBreakdown = computeLinkTypeBreakdown;
|
|
121
|
+
exports.computeNodeLinkCounts = computeNodeLinkCounts;
|
|
122
|
+
exports.findHubs = findHubs;
|
|
123
|
+
exports.getNeighbors = getNeighbors;
|
|
124
|
+
exports.groupByType = groupByType;
|
|
125
|
+
exports.topByDegree = topByDegree;
|
|
126
|
+
//# sourceMappingURL=analysis.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analysis.cjs","sources":["../src/analysis/degree.ts","../src/analysis/density.ts","../src/analysis/centrality.ts","../src/analysis/adjacency.ts","../src/analysis/index.ts"],"sourcesContent":["/**\n * Degree computation: degree map, distribution stats.\n */\nimport type { GraphData, GraphNode } from \"../types\";\n\nexport interface DegreeMap {\n [nodeId: string]: number;\n}\n\n/** Compute degree for every node */\nexport function computeDegreeMap(data: GraphData): DegreeMap {\n const degree: DegreeMap = {};\n for (const n of data.nodes) degree[n.id] = 0;\n for (const l of data.links) {\n const s = typeof l.source === \"object\" ? (l.source as any).id : l.source;\n const t = typeof l.target === \"object\" ? (l.target as any).id : l.target;\n if (degree[s] !== undefined) degree[s]++;\n if (degree[t] !== undefined) degree[t]++;\n }\n return degree;\n}\n\nexport interface DegreeStats {\n min: number;\n max: number;\n avg: number;\n total: number;\n}\n\n/** Compute degree distribution statistics */\nexport function computeDegreeStats(degreeMap: DegreeMap): DegreeStats {\n const values = Object.values(degreeMap);\n if (values.length === 0) {\n return { min: 0, max: 0, avg: 0, total: 0 };\n }\n const total = values.reduce((a, b) => a + b, 0);\n return {\n min: Math.min(...values),\n max: Math.max(...values),\n avg: total / values.length,\n total,\n };\n}\n\n/** Get top N nodes by degree */\nexport function topByDegree(\n nodes: GraphNode[],\n degreeMap: DegreeMap,\n n: number\n): Array<GraphNode & { degree: number }> {\n return nodes\n .map((node) => ({ ...node, degree: degreeMap[node.id] ?? 0 }))\n .sort((a, b) => b.degree - a.degree)\n .slice(0, n);\n}\n","/**\n * Network density computation.\n */\n\n/** Compute graph density: 2|E| / (|N| * (|N| - 1)) for undirected graphs */\nexport function computeDensity(nodeCount: number, linkCount: number): number {\n if (nodeCount <= 1) return 0;\n return (2 * linkCount) / (nodeCount * (nodeCount - 1));\n}\n","/**\n * Centrality and hub detection.\n */\nimport type { GraphData, GraphNode, GraphLink } from \"../types\";\nimport { computeDegreeMap, type DegreeMap } from \"./degree\";\n\nexport interface LinkTypeBreakdown {\n [linkType: string]: number;\n}\n\n/** Count links by type */\nexport function computeLinkTypeBreakdown(links: GraphLink[]): LinkTypeBreakdown {\n const counts: LinkTypeBreakdown = {};\n for (const l of links) {\n const type = l.type ?? \"default\";\n counts[type] = (counts[type] ?? 0) + 1;\n }\n return counts;\n}\n\n/** Count how many links of specific types each node has */\nexport function computeNodeLinkCounts(\n data: GraphData,\n linkTypes: string[]\n): Map<string, number> {\n const counts = new Map<string, number>();\n for (const n of data.nodes) counts.set(n.id, 0);\n\n const typeSet = new Set(linkTypes);\n for (const l of data.links) {\n if (!typeSet.has(l.type ?? \"default\")) continue;\n const s = typeof l.source === \"object\" ? (l.source as any).id : l.source;\n const t = typeof l.target === \"object\" ? (l.target as any).id : l.target;\n if (counts.has(s)) counts.set(s, counts.get(s)! + 1);\n if (counts.has(t)) counts.set(t, counts.get(t)! + 1);\n }\n return counts;\n}\n\n/**\n * Find hub nodes — nodes with degree above a threshold.\n * Returns nodes sorted by degree descending.\n */\nexport function findHubs(\n nodes: GraphNode[],\n degreeMap: DegreeMap,\n minDegree: number\n): Array<GraphNode & { degree: number }> {\n return nodes\n .map((n) => ({ ...n, degree: degreeMap[n.id] ?? 0 }))\n .filter((n) => n.degree >= minDegree)\n .sort((a, b) => b.degree - a.degree);\n}\n\n/** Group nodes by type with counts */\nexport function groupByType(\n nodes: GraphNode[]\n): Record<string, number> {\n const groups: Record<string, number> = {};\n for (const n of nodes) {\n const type = n.type ?? \"default\";\n groups[type] = (groups[type] ?? 0) + 1;\n }\n return groups;\n}\n","/**\n * Adjacency list construction and N-hop neighbor search.\n */\nimport type { GraphLink } from \"../types\";\n\n/** Build an adjacency map from a list of links */\nexport function buildAdjacencyMap(\n links: GraphLink[]\n): Map<string, string[]> {\n const adj = new Map<string, string[]>();\n for (const l of links) {\n const s = typeof l.source === \"object\" ? (l.source as any).id : l.source;\n const t = typeof l.target === \"object\" ? (l.target as any).id : l.target;\n if (!adj.has(s)) adj.set(s, []);\n if (!adj.has(t)) adj.set(t, []);\n adj.get(s)!.push(t);\n adj.get(t)!.push(s);\n }\n return adj;\n}\n\n/**\n * Find all nodes within N hops of a root node.\n * Returns a Map of node ID → hop distance.\n */\nexport function getNeighbors(\n adjacencyMap: Map<string, string[]>,\n rootId: string,\n maxHops: number\n): Map<string, number> {\n const visited = new Map<string, number>();\n visited.set(rootId, 0);\n const queue = [rootId];\n while (queue.length > 0) {\n const current = queue.shift()!;\n const dist = visited.get(current)!;\n if (dist >= maxHops) continue;\n for (const nb of adjacencyMap.get(current) ?? []) {\n if (!visited.has(nb)) {\n visited.set(nb, dist + 1);\n queue.push(nb);\n }\n }\n }\n return visited;\n}\n","/**\n * Graph analysis module — tree-shakeable, no Three.js dependency.\n *\n * Usage:\n * import { analyzeGraph, buildAdjacencyMap } from 'graphier/analysis';\n */\nimport type { GraphData, GraphNode } from \"../types\";\nimport { computeDegreeMap, computeDegreeStats, topByDegree } from \"./degree\";\nimport { computeDensity } from \"./density\";\nimport {\n computeLinkTypeBreakdown,\n groupByType,\n} from \"./centrality\";\nimport { buildAdjacencyMap, getNeighbors } from \"./adjacency\";\n\nexport interface GraphAnalytics {\n nodeCount: number;\n linkCount: number;\n density: number;\n avgDegree: number;\n maxDegree: number;\n minDegree: number;\n totalDegree: number;\n degreeMap: Record<string, number>;\n nodesByType: Record<string, number>;\n linksByType: Record<string, number>;\n /** Get top N nodes by degree */\n topByDegree(n: number): Array<GraphNode & { degree: number }>;\n}\n\n/**\n * Analyze a graph and return comprehensive statistics.\n *\n * @example\n * ```ts\n * const stats = analyzeGraph(data);\n * console.log(`Density: ${stats.density}`);\n * console.log(`Top hubs:`, stats.topByDegree(10));\n * ```\n */\nexport function analyzeGraph(data: GraphData): GraphAnalytics {\n const degreeMap = computeDegreeMap(data);\n const degreeStats = computeDegreeStats(degreeMap);\n const density = computeDensity(data.nodes.length, data.links.length);\n const nodesByType = groupByType(data.nodes);\n const linksByType = computeLinkTypeBreakdown(data.links);\n\n return {\n nodeCount: data.nodes.length,\n linkCount: data.links.length,\n density,\n avgDegree: degreeStats.avg,\n maxDegree: degreeStats.max,\n minDegree: degreeStats.min,\n totalDegree: degreeStats.total,\n degreeMap,\n nodesByType,\n linksByType,\n topByDegree: (n: number) => topByDegree(data.nodes, degreeMap, n),\n };\n}\n\n// Re-export individual utilities\nexport { buildAdjacencyMap, getNeighbors } from \"./adjacency\";\nexport { computeDegreeMap, computeDegreeStats, topByDegree } from \"./degree\";\nexport { computeDensity } from \"./density\";\nexport {\n computeLinkTypeBreakdown,\n computeNodeLinkCounts,\n findHubs,\n groupByType,\n} from \"./centrality\";\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyze a graph and return comprehensive statistics.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* const stats = analyzeGraph(data);
|
|
7
|
+
* console.log(`Density: ${stats.density}`);
|
|
8
|
+
* console.log(`Top hubs:`, stats.topByDegree(10));
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export declare function analyzeGraph(data: GraphData): GraphAnalytics;
|
|
12
|
+
|
|
13
|
+
/** Build an adjacency map from a list of links */
|
|
14
|
+
export declare function buildAdjacencyMap(links: GraphLink[]): Map<string, string[]>;
|
|
15
|
+
|
|
16
|
+
/** Compute degree for every node */
|
|
17
|
+
export declare function computeDegreeMap(data: GraphData): DegreeMap;
|
|
18
|
+
|
|
19
|
+
/** Compute degree distribution statistics */
|
|
20
|
+
export declare function computeDegreeStats(degreeMap: DegreeMap): DegreeStats;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Network density computation.
|
|
24
|
+
*/
|
|
25
|
+
/** Compute graph density: 2|E| / (|N| * (|N| - 1)) for undirected graphs */
|
|
26
|
+
export declare function computeDensity(nodeCount: number, linkCount: number): number;
|
|
27
|
+
|
|
28
|
+
/** Count links by type */
|
|
29
|
+
export declare function computeLinkTypeBreakdown(links: GraphLink[]): LinkTypeBreakdown;
|
|
30
|
+
|
|
31
|
+
/** Count how many links of specific types each node has */
|
|
32
|
+
export declare function computeNodeLinkCounts(data: GraphData, linkTypes: string[]): Map<string, number>;
|
|
33
|
+
|
|
34
|
+
declare interface DegreeMap {
|
|
35
|
+
[nodeId: string]: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
declare interface DegreeStats {
|
|
39
|
+
min: number;
|
|
40
|
+
max: number;
|
|
41
|
+
avg: number;
|
|
42
|
+
total: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Find hub nodes — nodes with degree above a threshold.
|
|
47
|
+
* Returns nodes sorted by degree descending.
|
|
48
|
+
*/
|
|
49
|
+
export declare function findHubs(nodes: GraphNode[], degreeMap: DegreeMap, minDegree: number): Array<GraphNode & {
|
|
50
|
+
degree: number;
|
|
51
|
+
}>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Find all nodes within N hops of a root node.
|
|
55
|
+
* Returns a Map of node ID → hop distance.
|
|
56
|
+
*/
|
|
57
|
+
export declare function getNeighbors(adjacencyMap: Map<string, string[]>, rootId: string, maxHops: number): Map<string, number>;
|
|
58
|
+
|
|
59
|
+
export declare interface GraphAnalytics {
|
|
60
|
+
nodeCount: number;
|
|
61
|
+
linkCount: number;
|
|
62
|
+
density: number;
|
|
63
|
+
avgDegree: number;
|
|
64
|
+
maxDegree: number;
|
|
65
|
+
minDegree: number;
|
|
66
|
+
totalDegree: number;
|
|
67
|
+
degreeMap: Record<string, number>;
|
|
68
|
+
nodesByType: Record<string, number>;
|
|
69
|
+
linksByType: Record<string, number>;
|
|
70
|
+
/** Get top N nodes by degree */
|
|
71
|
+
topByDegree(n: number): Array<GraphNode & {
|
|
72
|
+
degree: number;
|
|
73
|
+
}>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
declare interface GraphData {
|
|
77
|
+
nodes: GraphNode[];
|
|
78
|
+
links: GraphLink[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
declare interface GraphLink {
|
|
82
|
+
/** Source node id */
|
|
83
|
+
source: string;
|
|
84
|
+
/** Target node id */
|
|
85
|
+
target: string;
|
|
86
|
+
/** Relationship type — maps to theme colors (e.g. "follows", "owns") */
|
|
87
|
+
type?: string;
|
|
88
|
+
/** Edge weight (default: 1) */
|
|
89
|
+
weight?: number;
|
|
90
|
+
/** User-defined extra data */
|
|
91
|
+
[key: string]: unknown;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Core graph data types — domain-agnostic.
|
|
96
|
+
* Users provide any `type` string ("person", "server", "repo", etc.)
|
|
97
|
+
* and the theme system maps them to colors automatically.
|
|
98
|
+
*/
|
|
99
|
+
declare interface GraphNode {
|
|
100
|
+
/** Unique identifier */
|
|
101
|
+
id: string;
|
|
102
|
+
/** Node category — maps to theme colors (e.g. "person", "repo") */
|
|
103
|
+
type?: string;
|
|
104
|
+
/** Display label (falls back to id) */
|
|
105
|
+
label?: string;
|
|
106
|
+
/** Size weight — higher values render larger (default: 1) */
|
|
107
|
+
val?: number;
|
|
108
|
+
/** Grouping key for color auto-assignment (alternative to type) */
|
|
109
|
+
group?: string;
|
|
110
|
+
/** User-defined extra data (accessible in callbacks) */
|
|
111
|
+
[key: string]: unknown;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Group nodes by type with counts */
|
|
115
|
+
export declare function groupByType(nodes: GraphNode[]): Record<string, number>;
|
|
116
|
+
|
|
117
|
+
declare interface LinkTypeBreakdown {
|
|
118
|
+
[linkType: string]: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Get top N nodes by degree */
|
|
122
|
+
export declare function topByDegree(nodes: GraphNode[], degreeMap: DegreeMap, n: number): Array<GraphNode & {
|
|
123
|
+
degree: number;
|
|
124
|
+
}>;
|
|
125
|
+
|
|
126
|
+
export { }
|
package/dist/analysis.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
function computeDegreeMap(data) {
|
|
2
|
+
const degree = {};
|
|
3
|
+
for (const n of data.nodes) degree[n.id] = 0;
|
|
4
|
+
for (const l of data.links) {
|
|
5
|
+
const s = typeof l.source === "object" ? l.source.id : l.source;
|
|
6
|
+
const t = typeof l.target === "object" ? l.target.id : l.target;
|
|
7
|
+
if (degree[s] !== void 0) degree[s]++;
|
|
8
|
+
if (degree[t] !== void 0) degree[t]++;
|
|
9
|
+
}
|
|
10
|
+
return degree;
|
|
11
|
+
}
|
|
12
|
+
function computeDegreeStats(degreeMap) {
|
|
13
|
+
const values = Object.values(degreeMap);
|
|
14
|
+
if (values.length === 0) {
|
|
15
|
+
return { min: 0, max: 0, avg: 0, total: 0 };
|
|
16
|
+
}
|
|
17
|
+
const total = values.reduce((a, b) => a + b, 0);
|
|
18
|
+
return {
|
|
19
|
+
min: Math.min(...values),
|
|
20
|
+
max: Math.max(...values),
|
|
21
|
+
avg: total / values.length,
|
|
22
|
+
total
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function topByDegree(nodes, degreeMap, n) {
|
|
26
|
+
return nodes.map((node) => ({ ...node, degree: degreeMap[node.id] ?? 0 })).sort((a, b) => b.degree - a.degree).slice(0, n);
|
|
27
|
+
}
|
|
28
|
+
function computeDensity(nodeCount, linkCount) {
|
|
29
|
+
if (nodeCount <= 1) return 0;
|
|
30
|
+
return 2 * linkCount / (nodeCount * (nodeCount - 1));
|
|
31
|
+
}
|
|
32
|
+
function computeLinkTypeBreakdown(links) {
|
|
33
|
+
const counts = {};
|
|
34
|
+
for (const l of links) {
|
|
35
|
+
const type = l.type ?? "default";
|
|
36
|
+
counts[type] = (counts[type] ?? 0) + 1;
|
|
37
|
+
}
|
|
38
|
+
return counts;
|
|
39
|
+
}
|
|
40
|
+
function computeNodeLinkCounts(data, linkTypes) {
|
|
41
|
+
const counts = /* @__PURE__ */ new Map();
|
|
42
|
+
for (const n of data.nodes) counts.set(n.id, 0);
|
|
43
|
+
const typeSet = new Set(linkTypes);
|
|
44
|
+
for (const l of data.links) {
|
|
45
|
+
if (!typeSet.has(l.type ?? "default")) continue;
|
|
46
|
+
const s = typeof l.source === "object" ? l.source.id : l.source;
|
|
47
|
+
const t = typeof l.target === "object" ? l.target.id : l.target;
|
|
48
|
+
if (counts.has(s)) counts.set(s, counts.get(s) + 1);
|
|
49
|
+
if (counts.has(t)) counts.set(t, counts.get(t) + 1);
|
|
50
|
+
}
|
|
51
|
+
return counts;
|
|
52
|
+
}
|
|
53
|
+
function findHubs(nodes, degreeMap, minDegree) {
|
|
54
|
+
return nodes.map((n) => ({ ...n, degree: degreeMap[n.id] ?? 0 })).filter((n) => n.degree >= minDegree).sort((a, b) => b.degree - a.degree);
|
|
55
|
+
}
|
|
56
|
+
function groupByType(nodes) {
|
|
57
|
+
const groups = {};
|
|
58
|
+
for (const n of nodes) {
|
|
59
|
+
const type = n.type ?? "default";
|
|
60
|
+
groups[type] = (groups[type] ?? 0) + 1;
|
|
61
|
+
}
|
|
62
|
+
return groups;
|
|
63
|
+
}
|
|
64
|
+
function buildAdjacencyMap(links) {
|
|
65
|
+
const adj = /* @__PURE__ */ new Map();
|
|
66
|
+
for (const l of links) {
|
|
67
|
+
const s = typeof l.source === "object" ? l.source.id : l.source;
|
|
68
|
+
const t = typeof l.target === "object" ? l.target.id : l.target;
|
|
69
|
+
if (!adj.has(s)) adj.set(s, []);
|
|
70
|
+
if (!adj.has(t)) adj.set(t, []);
|
|
71
|
+
adj.get(s).push(t);
|
|
72
|
+
adj.get(t).push(s);
|
|
73
|
+
}
|
|
74
|
+
return adj;
|
|
75
|
+
}
|
|
76
|
+
function getNeighbors(adjacencyMap, rootId, maxHops) {
|
|
77
|
+
const visited = /* @__PURE__ */ new Map();
|
|
78
|
+
visited.set(rootId, 0);
|
|
79
|
+
const queue = [rootId];
|
|
80
|
+
while (queue.length > 0) {
|
|
81
|
+
const current = queue.shift();
|
|
82
|
+
const dist = visited.get(current);
|
|
83
|
+
if (dist >= maxHops) continue;
|
|
84
|
+
for (const nb of adjacencyMap.get(current) ?? []) {
|
|
85
|
+
if (!visited.has(nb)) {
|
|
86
|
+
visited.set(nb, dist + 1);
|
|
87
|
+
queue.push(nb);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return visited;
|
|
92
|
+
}
|
|
93
|
+
function analyzeGraph(data) {
|
|
94
|
+
const degreeMap = computeDegreeMap(data);
|
|
95
|
+
const degreeStats = computeDegreeStats(degreeMap);
|
|
96
|
+
const density = computeDensity(data.nodes.length, data.links.length);
|
|
97
|
+
const nodesByType = groupByType(data.nodes);
|
|
98
|
+
const linksByType = computeLinkTypeBreakdown(data.links);
|
|
99
|
+
return {
|
|
100
|
+
nodeCount: data.nodes.length,
|
|
101
|
+
linkCount: data.links.length,
|
|
102
|
+
density,
|
|
103
|
+
avgDegree: degreeStats.avg,
|
|
104
|
+
maxDegree: degreeStats.max,
|
|
105
|
+
minDegree: degreeStats.min,
|
|
106
|
+
totalDegree: degreeStats.total,
|
|
107
|
+
degreeMap,
|
|
108
|
+
nodesByType,
|
|
109
|
+
linksByType,
|
|
110
|
+
topByDegree: (n) => topByDegree(data.nodes, degreeMap, n)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export {
|
|
114
|
+
analyzeGraph,
|
|
115
|
+
buildAdjacencyMap,
|
|
116
|
+
computeDegreeMap,
|
|
117
|
+
computeDegreeStats,
|
|
118
|
+
computeDensity,
|
|
119
|
+
computeLinkTypeBreakdown,
|
|
120
|
+
computeNodeLinkCounts,
|
|
121
|
+
findHubs,
|
|
122
|
+
getNeighbors,
|
|
123
|
+
groupByType,
|
|
124
|
+
topByDegree
|
|
125
|
+
};
|
|
126
|
+
//# sourceMappingURL=analysis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analysis.js","sources":["../src/analysis/degree.ts","../src/analysis/density.ts","../src/analysis/centrality.ts","../src/analysis/adjacency.ts","../src/analysis/index.ts"],"sourcesContent":["/**\n * Degree computation: degree map, distribution stats.\n */\nimport type { GraphData, GraphNode } from \"../types\";\n\nexport interface DegreeMap {\n [nodeId: string]: number;\n}\n\n/** Compute degree for every node */\nexport function computeDegreeMap(data: GraphData): DegreeMap {\n const degree: DegreeMap = {};\n for (const n of data.nodes) degree[n.id] = 0;\n for (const l of data.links) {\n const s = typeof l.source === \"object\" ? (l.source as any).id : l.source;\n const t = typeof l.target === \"object\" ? (l.target as any).id : l.target;\n if (degree[s] !== undefined) degree[s]++;\n if (degree[t] !== undefined) degree[t]++;\n }\n return degree;\n}\n\nexport interface DegreeStats {\n min: number;\n max: number;\n avg: number;\n total: number;\n}\n\n/** Compute degree distribution statistics */\nexport function computeDegreeStats(degreeMap: DegreeMap): DegreeStats {\n const values = Object.values(degreeMap);\n if (values.length === 0) {\n return { min: 0, max: 0, avg: 0, total: 0 };\n }\n const total = values.reduce((a, b) => a + b, 0);\n return {\n min: Math.min(...values),\n max: Math.max(...values),\n avg: total / values.length,\n total,\n };\n}\n\n/** Get top N nodes by degree */\nexport function topByDegree(\n nodes: GraphNode[],\n degreeMap: DegreeMap,\n n: number\n): Array<GraphNode & { degree: number }> {\n return nodes\n .map((node) => ({ ...node, degree: degreeMap[node.id] ?? 0 }))\n .sort((a, b) => b.degree - a.degree)\n .slice(0, n);\n}\n","/**\n * Network density computation.\n */\n\n/** Compute graph density: 2|E| / (|N| * (|N| - 1)) for undirected graphs */\nexport function computeDensity(nodeCount: number, linkCount: number): number {\n if (nodeCount <= 1) return 0;\n return (2 * linkCount) / (nodeCount * (nodeCount - 1));\n}\n","/**\n * Centrality and hub detection.\n */\nimport type { GraphData, GraphNode, GraphLink } from \"../types\";\nimport { computeDegreeMap, type DegreeMap } from \"./degree\";\n\nexport interface LinkTypeBreakdown {\n [linkType: string]: number;\n}\n\n/** Count links by type */\nexport function computeLinkTypeBreakdown(links: GraphLink[]): LinkTypeBreakdown {\n const counts: LinkTypeBreakdown = {};\n for (const l of links) {\n const type = l.type ?? \"default\";\n counts[type] = (counts[type] ?? 0) + 1;\n }\n return counts;\n}\n\n/** Count how many links of specific types each node has */\nexport function computeNodeLinkCounts(\n data: GraphData,\n linkTypes: string[]\n): Map<string, number> {\n const counts = new Map<string, number>();\n for (const n of data.nodes) counts.set(n.id, 0);\n\n const typeSet = new Set(linkTypes);\n for (const l of data.links) {\n if (!typeSet.has(l.type ?? \"default\")) continue;\n const s = typeof l.source === \"object\" ? (l.source as any).id : l.source;\n const t = typeof l.target === \"object\" ? (l.target as any).id : l.target;\n if (counts.has(s)) counts.set(s, counts.get(s)! + 1);\n if (counts.has(t)) counts.set(t, counts.get(t)! + 1);\n }\n return counts;\n}\n\n/**\n * Find hub nodes — nodes with degree above a threshold.\n * Returns nodes sorted by degree descending.\n */\nexport function findHubs(\n nodes: GraphNode[],\n degreeMap: DegreeMap,\n minDegree: number\n): Array<GraphNode & { degree: number }> {\n return nodes\n .map((n) => ({ ...n, degree: degreeMap[n.id] ?? 0 }))\n .filter((n) => n.degree >= minDegree)\n .sort((a, b) => b.degree - a.degree);\n}\n\n/** Group nodes by type with counts */\nexport function groupByType(\n nodes: GraphNode[]\n): Record<string, number> {\n const groups: Record<string, number> = {};\n for (const n of nodes) {\n const type = n.type ?? \"default\";\n groups[type] = (groups[type] ?? 0) + 1;\n }\n return groups;\n}\n","/**\n * Adjacency list construction and N-hop neighbor search.\n */\nimport type { GraphLink } from \"../types\";\n\n/** Build an adjacency map from a list of links */\nexport function buildAdjacencyMap(\n links: GraphLink[]\n): Map<string, string[]> {\n const adj = new Map<string, string[]>();\n for (const l of links) {\n const s = typeof l.source === \"object\" ? (l.source as any).id : l.source;\n const t = typeof l.target === \"object\" ? (l.target as any).id : l.target;\n if (!adj.has(s)) adj.set(s, []);\n if (!adj.has(t)) adj.set(t, []);\n adj.get(s)!.push(t);\n adj.get(t)!.push(s);\n }\n return adj;\n}\n\n/**\n * Find all nodes within N hops of a root node.\n * Returns a Map of node ID → hop distance.\n */\nexport function getNeighbors(\n adjacencyMap: Map<string, string[]>,\n rootId: string,\n maxHops: number\n): Map<string, number> {\n const visited = new Map<string, number>();\n visited.set(rootId, 0);\n const queue = [rootId];\n while (queue.length > 0) {\n const current = queue.shift()!;\n const dist = visited.get(current)!;\n if (dist >= maxHops) continue;\n for (const nb of adjacencyMap.get(current) ?? []) {\n if (!visited.has(nb)) {\n visited.set(nb, dist + 1);\n queue.push(nb);\n }\n }\n }\n return visited;\n}\n","/**\n * Graph analysis module — tree-shakeable, no Three.js dependency.\n *\n * Usage:\n * import { analyzeGraph, buildAdjacencyMap } from 'graphier/analysis';\n */\nimport type { GraphData, GraphNode } from \"../types\";\nimport { computeDegreeMap, computeDegreeStats, topByDegree } from \"./degree\";\nimport { computeDensity } from \"./density\";\nimport {\n computeLinkTypeBreakdown,\n groupByType,\n} from \"./centrality\";\nimport { buildAdjacencyMap, getNeighbors } from \"./adjacency\";\n\nexport interface GraphAnalytics {\n nodeCount: number;\n linkCount: number;\n density: number;\n avgDegree: number;\n maxDegree: number;\n minDegree: number;\n totalDegree: number;\n degreeMap: Record<string, number>;\n nodesByType: Record<string, number>;\n linksByType: Record<string, number>;\n /** Get top N nodes by degree */\n topByDegree(n: number): Array<GraphNode & { degree: number }>;\n}\n\n/**\n * Analyze a graph and return comprehensive statistics.\n *\n * @example\n * ```ts\n * const stats = analyzeGraph(data);\n * console.log(`Density: ${stats.density}`);\n * console.log(`Top hubs:`, stats.topByDegree(10));\n * ```\n */\nexport function analyzeGraph(data: GraphData): GraphAnalytics {\n const degreeMap = computeDegreeMap(data);\n const degreeStats = computeDegreeStats(degreeMap);\n const density = computeDensity(data.nodes.length, data.links.length);\n const nodesByType = groupByType(data.nodes);\n const linksByType = computeLinkTypeBreakdown(data.links);\n\n return {\n nodeCount: data.nodes.length,\n linkCount: data.links.length,\n density,\n avgDegree: degreeStats.avg,\n maxDegree: degreeStats.max,\n minDegree: degreeStats.min,\n totalDegree: degreeStats.total,\n degreeMap,\n nodesByType,\n linksByType,\n topByDegree: (n: number) => topByDegree(data.nodes, degreeMap, n),\n };\n}\n\n// Re-export individual utilities\nexport { buildAdjacencyMap, getNeighbors } from \"./adjacency\";\nexport { computeDegreeMap, computeDegreeStats, topByDegree } from \"./degree\";\nexport { computeDensity } from \"./density\";\nexport {\n computeLinkTypeBreakdown,\n computeNodeLinkCounts,\n findHubs,\n groupByType,\n} from \"./centrality\";\n"],"names":[],"mappings":"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"}
|