Skip to content

Commit 305a20a

Browse files
authored
fix: louvain data pollution (#76)
* fix: louvain data pollution * chore: refine
1 parent f6a34cd commit 305a20a

File tree

8 files changed

+587
-430
lines changed

8 files changed

+587
-430
lines changed

__tests__/unit/louvain.spec.ts

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,68 @@
1-
import { Graph } from "@antv/graphlib";
2-
import { louvain, iLouvain } from "../../packages/graph/src";
3-
import * as propertiesGraphData from "../data/cluster-origin-properties-data.json";
1+
import { Graph } from '@antv/graphlib';
2+
import { louvain, iLouvain } from '../../packages/graph/src';
3+
import * as propertiesGraphData from '../data/cluster-origin-properties-data.json';
44

55
describe('Louvain', () => {
66
it('simple louvain', () => {
77
const graph = new Graph<any, any>({
88
nodes: [
9-
{ id: '0', data: {} }, { id: '1', data: {} }, { id: '2', data: {} }, { id: '3', data: {} }, { id: '4', data: {} },
10-
{ id: '5', data: {} }, { id: '6', data: {} }, { id: '7', data: {} }, { id: '8', data: {} }, { id: '9', data: {} },
11-
{ id: '10', data: {} }, { id: '11', data: {} }, { id: '12', data: {} }, { id: '13', data: {} }, { id: '14', data: {} },
9+
{ id: '0', data: {} },
10+
{ id: '1', data: {} },
11+
{ id: '2', data: {} },
12+
{ id: '3', data: {} },
13+
{ id: '4', data: {} },
14+
{ id: '5', data: {} },
15+
{ id: '6', data: {} },
16+
{ id: '7', data: {} },
17+
{ id: '8', data: {} },
18+
{ id: '9', data: {} },
19+
{ id: '10', data: {} },
20+
{ id: '11', data: {} },
21+
{ id: '12', data: {} },
22+
{ id: '13', data: {} },
23+
{ id: '14', data: {} },
1224
],
1325
edges: [
14-
{ id: 'e1', source: '0', target: '1', data: {} }, { id: 'e2', source: '0', target: '2', data: {} }, { id: 'e3', source: '0', target: '3', data: {} }, { id: 'e4', source: '0', target: '4', data: {} },
15-
{ id: 'e5', source: '1', target: '2', data: {} }, { id: 'e6', source: '1', target: '3', data: {} }, { id: 'e7', source: '1', target: '4', data: {} },
16-
{ id: 'e8', source: '2', target: '3', data: {} }, { id: 'e9', source: '2', target: '4', data: {} },
26+
{ id: 'e1', source: '0', target: '1', data: {} },
27+
{ id: 'e2', source: '0', target: '2', data: {} },
28+
{ id: 'e3', source: '0', target: '3', data: {} },
29+
{ id: 'e4', source: '0', target: '4', data: {} },
30+
{ id: 'e5', source: '1', target: '2', data: {} },
31+
{ id: 'e6', source: '1', target: '3', data: {} },
32+
{ id: 'e7', source: '1', target: '4', data: {} },
33+
{ id: 'e8', source: '2', target: '3', data: {} },
34+
{ id: 'e9', source: '2', target: '4', data: {} },
1735
{ id: 'e10', source: '3', target: '4', data: {} },
1836
{ id: 'e11', source: '0', target: '0', data: {} },
1937
{ id: 'e12', source: '0', target: '0', data: {} },
2038
{ id: 'e13', source: '0', target: '0', data: {} },
21-
22-
{ id: 'e14', source: '5', target: '6', data: {weight: 5} }, { id: 'e15', source: '5', target: '7', data: {} }, { id: 'e16', source: '5', target: '8', data: {} }, { id: 'e17', source: '5', target: '9', data: {} },
23-
{ id: 'e18', source: '6', target: '7', data: {} }, { id: 'e19', source: '6', target: '8', data: {} }, { id: 'e20', source: '6', target: '9', data: {} },
24-
{ id: 'e21', source: '7', target: '8', data: {} }, { id: 'e22', source: '7', target: '9', data: {} },
25-
{ id: 'e23',source: '8', target: '9', data: {} },
26-
27-
{ id: 'e24',source: '10', target: '11', data: {} }, { id: 'e25',source: '10', target: '12', data: {} }, { id: 'e26',source: '10', target: '13', data: {} }, { id: 'e27',source: '10', target: '14', data: {} },
28-
{ id: 'e28',source: '11', target: '12', data: {} }, { id: 'e29',source: '11', target: '13', data: {} }, { id: 'e30',source: '11', target: '14', data: {} },
29-
{ id: 'e31',source: '12', target: '13', data: {} }, { id: 'e32',source: '12', target: '14', data: {} },
30-
{ id: 'e33',source: '13', target: '14', data: { weight: 5 } },
31-
32-
{ id: 'e34',source: '0', target: '5', data: {}},
33-
{ id: 'e35',source: '5', target: '10', data: {} },
34-
{ id: 'e36',source: '10', target: '0', data: {} },
35-
{ id: 'e37',source: '10', target: '0', data: {} },
39+
40+
{ id: 'e14', source: '5', target: '6', data: { weight: 5 } },
41+
{ id: 'e15', source: '5', target: '7', data: {} },
42+
{ id: 'e16', source: '5', target: '8', data: {} },
43+
{ id: 'e17', source: '5', target: '9', data: {} },
44+
{ id: 'e18', source: '6', target: '7', data: {} },
45+
{ id: 'e19', source: '6', target: '8', data: {} },
46+
{ id: 'e20', source: '6', target: '9', data: {} },
47+
{ id: 'e21', source: '7', target: '8', data: {} },
48+
{ id: 'e22', source: '7', target: '9', data: {} },
49+
{ id: 'e23', source: '8', target: '9', data: {} },
50+
51+
{ id: 'e24', source: '10', target: '11', data: {} },
52+
{ id: 'e25', source: '10', target: '12', data: {} },
53+
{ id: 'e26', source: '10', target: '13', data: {} },
54+
{ id: 'e27', source: '10', target: '14', data: {} },
55+
{ id: 'e28', source: '11', target: '12', data: {} },
56+
{ id: 'e29', source: '11', target: '13', data: {} },
57+
{ id: 'e30', source: '11', target: '14', data: {} },
58+
{ id: 'e31', source: '12', target: '13', data: {} },
59+
{ id: 'e32', source: '12', target: '14', data: {} },
60+
{ id: 'e33', source: '13', target: '14', data: { weight: 5 } },
61+
62+
{ id: 'e34', source: '0', target: '5', data: {} },
63+
{ id: 'e35', source: '5', target: '10', data: {} },
64+
{ id: 'e36', source: '10', target: '0', data: {} },
65+
{ id: 'e37', source: '10', target: '0', data: {} },
3666
],
3767
});
3868
const clusteredData = louvain(graph, false, 'weight');
@@ -64,4 +94,4 @@ describe('Louvain', () => {
6494
expect(clusteredData.clusters[2].sumTot).toBe(4);
6595
expect(clusteredData.clusterEdges.length).toBe(7);
6696
});
67-
});
97+
});

__tests__/utils/data.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1-
import { NodeID, INode, IEdge } from "../../packages/graph/src/types";
1+
import { ID } from '@antv/graphlib';
2+
import { INode, IEdge } from '../../packages/graph/src/types';
23
/**
34
* Convert the old version of the data format to the new version
45
* @param data old data
56
* @return {{nodes:INode[],edges:IEdge[]}} new data
67
*/
7-
export const dataTransformer = (data: { nodes: { id: NodeID, [key: string]: any }[], edges: { source: NodeID, target: NodeID, [key: string]: any }[] }): { nodes: INode[], edges: IEdge[] } => {
8-
const { nodes, edges } = data;
9-
return {
10-
nodes: nodes.map((n) => {
11-
const { id, ...rest } = n;
12-
return { id, data: rest ? rest : {} };
13-
}),
14-
edges: edges.map((e, i) => {
15-
const { id, source, target, ...rest } = e;
16-
return { id: id ? id : `edge-${i}`, target, source, data: rest };
17-
}),
18-
};
8+
export const dataTransformer = (data: {
9+
nodes: { id: ID; [key: string]: any }[];
10+
edges: { source: ID; target: ID; [key: string]: any }[];
11+
}): { nodes: INode[]; edges: IEdge[] } => {
12+
const { nodes, edges } = data;
13+
return {
14+
nodes: nodes.map((n) => {
15+
const { id, ...rest } = n;
16+
return { id, data: rest ? rest : {} };
17+
}),
18+
edges: edges.map((e, i) => {
19+
const { id, source, target, ...rest } = e;
20+
return { id: id ? id : `edge-${i}`, target, source, data: rest };
21+
}),
22+
};
1923
};

packages/graph/src/bfs.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { ID } from '@antv/graphlib';
12
import Queue from './structs/queue';
2-
import { Graph, IAlgorithmCallbacks, NodeID } from './types';
3+
import { Graph, IAlgorithmCallbacks } from './types';
34

45
/**
56
* @param startNodeId The ID of the bfs traverse starting node.
@@ -8,11 +9,14 @@ import { Graph, IAlgorithmCallbacks, NodeID } from './types';
89
- enterNode: Called when BFS visits a node.
910
- leaveNode: Called after BFS visits the node.
1011
*/
11-
function initCallbacks(callbacks: IAlgorithmCallbacks = {} as IAlgorithmCallbacks) {
12+
function initCallbacks(
13+
callbacks: IAlgorithmCallbacks = {} as IAlgorithmCallbacks
14+
) {
1215
const initiatedCallback = callbacks;
13-
const stubCallback = () => { };
16+
const stubCallback = () => {};
1417
const allowTraversalCallback = () => true;
15-
initiatedCallback.allowTraversal = callbacks.allowTraversal || allowTraversalCallback;
18+
initiatedCallback.allowTraversal =
19+
callbacks.allowTraversal || allowTraversalCallback;
1620
initiatedCallback.enter = callbacks.enter || stubCallback;
1721
initiatedCallback.leave = callbacks.leave || stubCallback;
1822
return initiatedCallback;
@@ -26,19 +30,19 @@ Performs breadth-first search (BFS) traversal on a graph.
2630
*/
2731
export const breadthFirstSearch = (
2832
graph: Graph,
29-
startNodeId: NodeID,
30-
originalCallbacks?: IAlgorithmCallbacks,
33+
startNodeId: ID,
34+
originalCallbacks?: IAlgorithmCallbacks
3135
) => {
32-
const visit = new Set<NodeID>();
36+
const visit = new Set<ID>();
3337
const callbacks = initCallbacks(originalCallbacks);
34-
const nodeQueue = new Queue<NodeID>();
38+
const nodeQueue = new Queue<ID>();
3539
// init Queue. Enqueue node ID.
3640
nodeQueue.enqueue(startNodeId);
3741
visit.add(startNodeId);
38-
let previousNodeId: NodeID = '';
42+
let previousNodeId: ID = '';
3943
// 遍历队列中的所有顶点
4044
while (!nodeQueue.isEmpty()) {
41-
const currentNodeId: NodeID = nodeQueue.dequeue();
45+
const currentNodeId: ID = nodeQueue.dequeue();
4246
callbacks.enter({
4347
current: currentNodeId,
4448
previous: previousNodeId,
@@ -52,7 +56,8 @@ export const breadthFirstSearch = (
5256
previous: previousNodeId,
5357
current: currentNodeId,
5458
next: nextNodeId,
55-
}) && !visit.has(nextNodeId)
59+
}) &&
60+
!visit.has(nextNodeId)
5661
) {
5762
visit.add(nextNodeId);
5863
nodeQueue.enqueue(nextNodeId);

packages/graph/src/connected-component.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { Graph, INode, NodeID } from './types';
1+
import { ID } from '@antv/graphlib';
2+
import { Graph, INode } from './types';
23
/**
34
* Generate all connected components for an undirected graph
45
* @param graph
56
*/
67
export const detectConnectedComponents = (graph: Graph): INode[][] => {
78
const nodes = graph.getAllNodes();
89
const allComponents: INode[][] = [];
9-
const visited: { [key: NodeID]: boolean } = {};
10+
const visited: { [key: ID]: boolean } = {};
1011
const nodeStack: INode[] = [];
1112
const getComponent = (node: INode) => {
1213
nodeStack.push(node);
@@ -49,9 +50,9 @@ export const detectStrongConnectComponents = (graph: Graph): INode[][] => {
4950
const nodes = graph.getAllNodes();
5051
const nodeStack: INode[] = [];
5152
// Assist to determine whether it is already in the stack to reduce the search overhead
52-
const inStack: { [key: NodeID]: boolean } = {};
53-
const indices: { [key: NodeID]: number } = {};
54-
const lowLink: { [key: NodeID]: number } = {};
53+
const inStack: { [key: ID]: boolean } = {};
54+
const indices: { [key: ID]: number } = {};
55+
const lowLink: { [key: ID]: number } = {};
5556
const allComponents: INode[][] = [];
5657
let index = 0;
5758
const getComponent = (node: INode) => {
@@ -61,7 +62,7 @@ export const detectStrongConnectComponents = (graph: Graph): INode[][] => {
6162
index += 1;
6263
nodeStack.push(node);
6364
inStack[node.id] = true;
64-
const relatedEdges = graph.getRelatedEdges(node.id, "out");
65+
const relatedEdges = graph.getRelatedEdges(node.id, 'out');
6566
for (let i = 0; i < relatedEdges.length; i++) {
6667
const targetNodeID = relatedEdges[i].target;
6768
if (!indices[targetNodeID] && indices[targetNodeID] !== 0) {
@@ -98,7 +99,10 @@ export const detectStrongConnectComponents = (graph: Graph): INode[][] => {
9899
return allComponents;
99100
};
100101

101-
export function getConnectedComponents(graph: Graph, directed?: boolean): INode[][] {
102+
export function getConnectedComponents(
103+
graph: Graph,
104+
directed?: boolean
105+
): INode[][] {
102106
if (directed) return detectStrongConnectComponents(graph);
103107
return detectConnectedComponents(graph);
104108
}

0 commit comments

Comments
 (0)