@graphty/algorithms 1.5.0 → 1.7.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/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "@graphty/algorithms",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Graph algorithms library for browser environments implemented in TypeScript",
5
5
  "author": "Adam Powers <apowers@ato.ms>",
6
6
  "main": "dist/algorithms.js",
7
7
  "type": "module",
8
8
  "exports": {
9
9
  ".": {
10
+ "types": "./dist/algorithms.d.ts",
10
11
  "import": "./dist/algorithms.js",
11
- "types": "./dist/algorithms.d.ts"
12
+ "default": "./dist/algorithms.js"
12
13
  }
13
14
  },
14
15
  "types": "dist/algorithms.d.ts",
@@ -56,10 +57,35 @@
56
57
  }
57
58
  },
58
59
  "devDependencies": {
60
+ "@chromatic-com/storybook": "^4.0.0",
61
+ "@eslint/js": "^9.29.0",
62
+ "@semantic-release/changelog": "^6.0.3",
63
+ "@semantic-release/git": "^10.0.1",
64
+ "@storybook/addon-docs": "^9.0.11",
65
+ "@storybook/html-vite": "^9.0.11",
66
+ "@storybook/test": "^8.6.14",
59
67
  "@types/benchmark": "^2.1.5",
60
68
  "benchmark": "^2.1.4",
69
+ "chromatic": "^11.0.0",
70
+ "eslint": "^9.29.0",
71
+ "eslint-plugin-simple-import-sort": "^12.1.1",
61
72
  "gh-pages": "^6.3.0",
62
- "ts-node": "^10.9.2"
73
+ "globals": "^15.12.0",
74
+ "happy-dom": "^18.0.1",
75
+ "knip": "^5.61.3",
76
+ "playwright": "^1.53.0",
77
+ "semantic-release": "^25.0.2",
78
+ "storybook": "^9.0.11",
79
+ "ts-node": "^10.9.2",
80
+ "tsx": "^4.20.3",
81
+ "typedoc": "^0.28.15",
82
+ "typedoc-plugin-markdown": "^4.9.0",
83
+ "typedoc-vitepress-theme": "^1.1.2",
84
+ "typescript": "^5.8.3",
85
+ "typescript-eslint": "^8.34.1",
86
+ "vite": "^7.0.5",
87
+ "vitepress": "^1.6.3",
88
+ "vitest": "^3.2.4"
63
89
  },
64
90
  "dependencies": {
65
91
  "pupt": "^1.3.2",
@@ -112,6 +138,8 @@
112
138
  "docs:dev": "vitepress dev docs",
113
139
  "docs:watch": "npm run docs:api && (npm run docs:api:watch & npm run docs:dev & wait)",
114
140
  "docs:build": "npm run docs:api && vitepress build docs",
115
- "docs:preview": "vitepress preview docs"
141
+ "docs:preview": "vitepress preview docs",
142
+ "storybook": ". ../.env 2>/dev/null; storybook dev -p ${PORT:-6006} --host ${HOST:-localhost} ${HTTPS_CERT_PATH:+--https --ssl-cert $HTTPS_CERT_PATH --ssl-key $HTTPS_KEY_PATH} --no-open",
143
+ "build-storybook": "storybook build"
116
144
  }
117
145
  }
@@ -39,7 +39,7 @@ export function girvanNewman(graph: Graph, options: GirvanNewmanOptions = {}): C
39
39
  while (Array.from(workingGraph.edges()).length > 0 && iterations < maxIterations) {
40
40
  iterations++;
41
41
  // Calculate edge betweenness centrality
42
- const edgeBetweenness = calculateEdgeBetweenness(workingGraph);
42
+ const { betweenness: edgeBetweenness, edgeEndpoints } = calculateEdgeBetweenness(workingGraph);
43
43
 
44
44
  if (edgeBetweenness.size === 0) {
45
45
  break;
@@ -51,9 +51,10 @@ export function girvanNewman(graph: Graph, options: GirvanNewmanOptions = {}): C
51
51
 
52
52
  for (const [edgeKey, centrality] of edgeBetweenness) {
53
53
  if (Math.abs(centrality - maxBetweenness) < 1e-10) {
54
- const [source, target] = edgeKey.split("|");
55
- if (source && target) {
56
- edgesToRemove.push({ source, target });
54
+ // Use original edge endpoints to preserve node ID types (number vs string)
55
+ const endpoints = edgeEndpoints.get(edgeKey);
56
+ if (endpoints) {
57
+ edgesToRemove.push(endpoints);
57
58
  }
58
59
  }
59
60
  }
@@ -91,21 +92,31 @@ export function girvanNewman(graph: Graph, options: GirvanNewmanOptions = {}): C
91
92
  return dendrogram;
92
93
  }
93
94
 
95
+ /**
96
+ * Edge betweenness result containing centrality values and original edge endpoints
97
+ */
98
+ interface EdgeBetweennessResult {
99
+ betweenness: Map<string, number>;
100
+ edgeEndpoints: Map<string, { source: NodeId; target: NodeId }>;
101
+ }
102
+
94
103
  /**
95
104
  * Calculate edge betweenness centrality for all edges in the graph
96
105
  *
97
106
  * Edge betweenness is the fraction of shortest paths that pass through the edge.
98
107
  * We adapt node betweenness centrality calculation to work with edges.
99
108
  * @param graph - The input graph to analyze
100
- * @returns Map of edge keys to their betweenness centrality values
109
+ * @returns Object with betweenness map and edge endpoints map (preserving original ID types)
101
110
  */
102
- function calculateEdgeBetweenness(graph: Graph): Map<string, number> {
111
+ function calculateEdgeBetweenness(graph: Graph): EdgeBetweennessResult {
103
112
  const edgeBetweenness = new Map<string, number>();
113
+ const edgeEndpoints = new Map<string, { source: NodeId; target: NodeId }>();
104
114
 
105
- // Initialize all edges with 0 betweenness
115
+ // Initialize all edges with 0 betweenness and store original endpoints
106
116
  for (const edge of graph.edges()) {
107
117
  const edgeKey = getEdgeKey(edge.source, edge.target);
108
118
  edgeBetweenness.set(edgeKey, 0);
119
+ edgeEndpoints.set(edgeKey, { source: edge.source, target: edge.target });
109
120
  }
110
121
 
111
122
  // For each node as source, calculate shortest paths and accumulate edge betweenness
@@ -182,7 +193,7 @@ function calculateEdgeBetweenness(graph: Graph): Map<string, number> {
182
193
  }
183
194
  }
184
195
 
185
- return edgeBetweenness;
196
+ return { betweenness: edgeBetweenness, edgeEndpoints };
186
197
  }
187
198
 
188
199
  /**
@@ -111,7 +111,7 @@ function buildTransitionMatrix(graph: Graph, nodeIds: NodeId[], selfLoops: boole
111
111
  // Fill adjacency values
112
112
  for (let i = 0; i < n; i++) {
113
113
  const nodeId = nodeIds[i];
114
- if (!nodeId) {
114
+ if (nodeId === undefined) {
115
115
  continue;
116
116
  }
117
117
 
@@ -398,11 +398,16 @@ function extractClusters(
398
398
  }
399
399
  }
400
400
 
401
- // Assign nodes to clusters based on columns
402
- let clusterIndex = 0;
401
+ // Assign nodes (columns) to clusters based on which attractor (row) they flow to
402
+ // After MCL convergence, matrix[i][j] > 0 means node j belongs to attractor i's cluster
403
+ const attractorToCommunity = new Map<number, number[]>();
404
+
403
405
  for (let j = 0; j < n; j++) {
404
- // Find nodes that belong to this cluster (column)
405
- const clusterNodes: number[] = [];
406
+ // Find the attractor for this column (node j)
407
+ // Look for the row with the highest non-zero value in column j
408
+ let maxVal = 0;
409
+ let attractorRow = -1;
410
+
406
411
  for (let i = 0; i < n; i++) {
407
412
  const matrixRow = matrix[i];
408
413
  if (!matrixRow) {
@@ -410,34 +415,43 @@ function extractClusters(
410
415
  }
411
416
 
412
417
  const val = matrixRow[j];
413
- if (val !== undefined && val > 0 && !nodeToCluster.has(i)) {
414
- clusterNodes.push(i);
415
- nodeToCluster.set(i, clusterIndex);
418
+ if (val !== undefined && val > maxVal) {
419
+ maxVal = val;
420
+ attractorRow = i;
416
421
  }
417
422
  }
418
423
 
419
- if (clusterNodes.length > 0) {
420
- communities.push(
421
- clusterNodes
422
- .map((idx) => {
423
- const nodeId = nodeIds[idx];
424
- return nodeId;
425
- })
426
- .filter((node): node is NodeId => node !== undefined),
427
- );
428
- clusterIndex++;
424
+ if (attractorRow >= 0) {
425
+ // Node j belongs to the cluster of attractor at row attractorRow
426
+ let community = attractorToCommunity.get(attractorRow);
427
+ if (!community) {
428
+ community = [];
429
+ attractorToCommunity.set(attractorRow, community);
430
+ }
431
+ community.push(j);
432
+ nodeToCluster.set(j, attractorRow);
429
433
  }
430
434
  }
431
435
 
432
- // Handle isolated nodes
433
- for (let i = 0; i < n; i++) {
434
- if (!nodeToCluster.has(i)) {
435
- const nodeId = nodeIds[i];
436
+ // Convert attractor communities to node ID arrays
437
+ for (const [, memberIndices] of attractorToCommunity) {
438
+ communities.push(
439
+ memberIndices
440
+ .map((idx) => {
441
+ const nodeId = nodeIds[idx];
442
+ return nodeId;
443
+ })
444
+ .filter((node): node is NodeId => node !== undefined),
445
+ );
446
+ }
447
+
448
+ // Handle isolated nodes (nodes with all-zero columns)
449
+ for (let j = 0; j < n; j++) {
450
+ if (!nodeToCluster.has(j)) {
451
+ const nodeId = nodeIds[j];
436
452
  if (nodeId !== undefined) {
437
453
  communities.push([nodeId]);
438
454
  }
439
-
440
- nodeToCluster.set(i, clusterIndex++);
441
455
  }
442
456
  }
443
457