Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 55 additions & 25 deletions __tests__/unit/louvain.spec.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,68 @@
import { Graph } from "@antv/graphlib";
import { louvain, iLouvain } from "../../packages/graph/src";
import * as propertiesGraphData from "../data/cluster-origin-properties-data.json";
import { Graph } from '@antv/graphlib';
import { louvain, iLouvain } from '../../packages/graph/src';
import * as propertiesGraphData from '../data/cluster-origin-properties-data.json';

describe('Louvain', () => {
it('simple louvain', () => {
const graph = new Graph<any, any>({
nodes: [
{ id: '0', data: {} }, { id: '1', data: {} }, { id: '2', data: {} }, { id: '3', data: {} }, { id: '4', data: {} },
{ id: '5', data: {} }, { id: '6', data: {} }, { id: '7', data: {} }, { id: '8', data: {} }, { id: '9', data: {} },
{ id: '10', data: {} }, { id: '11', data: {} }, { id: '12', data: {} }, { id: '13', data: {} }, { id: '14', data: {} },
{ id: '0', data: {} },
{ id: '1', data: {} },
{ id: '2', data: {} },
{ id: '3', data: {} },
{ id: '4', data: {} },
{ id: '5', data: {} },
{ id: '6', data: {} },
{ id: '7', data: {} },
{ id: '8', data: {} },
{ id: '9', data: {} },
{ id: '10', data: {} },
{ id: '11', data: {} },
{ id: '12', data: {} },
{ id: '13', data: {} },
{ id: '14', data: {} },
],
edges: [
{ 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: {} },
{ id: 'e5', source: '1', target: '2', data: {} }, { id: 'e6', source: '1', target: '3', data: {} }, { id: 'e7', source: '1', target: '4', data: {} },
{ id: 'e8', source: '2', target: '3', data: {} }, { id: 'e9', source: '2', target: '4', data: {} },
{ 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: {} },
{ id: 'e5', source: '1', target: '2', data: {} },
{ id: 'e6', source: '1', target: '3', data: {} },
{ id: 'e7', source: '1', target: '4', data: {} },
{ id: 'e8', source: '2', target: '3', data: {} },
{ id: 'e9', source: '2', target: '4', data: {} },
{ id: 'e10', source: '3', target: '4', data: {} },
{ id: 'e11', source: '0', target: '0', data: {} },
{ id: 'e12', source: '0', target: '0', data: {} },
{ id: 'e13', source: '0', target: '0', data: {} },

{ 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: {} },
{ id: 'e18', source: '6', target: '7', data: {} }, { id: 'e19', source: '6', target: '8', data: {} }, { id: 'e20', source: '6', target: '9', data: {} },
{ id: 'e21', source: '7', target: '8', data: {} }, { id: 'e22', source: '7', target: '9', data: {} },
{ id: 'e23',source: '8', target: '9', data: {} },

{ 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: {} },
{ id: 'e28',source: '11', target: '12', data: {} }, { id: 'e29',source: '11', target: '13', data: {} }, { id: 'e30',source: '11', target: '14', data: {} },
{ id: 'e31',source: '12', target: '13', data: {} }, { id: 'e32',source: '12', target: '14', data: {} },
{ id: 'e33',source: '13', target: '14', data: { weight: 5 } },

{ id: 'e34',source: '0', target: '5', data: {}},
{ id: 'e35',source: '5', target: '10', data: {} },
{ id: 'e36',source: '10', target: '0', data: {} },
{ id: 'e37',source: '10', target: '0', data: {} },

{ 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: {} },
{ id: 'e18', source: '6', target: '7', data: {} },
{ id: 'e19', source: '6', target: '8', data: {} },
{ id: 'e20', source: '6', target: '9', data: {} },
{ id: 'e21', source: '7', target: '8', data: {} },
{ id: 'e22', source: '7', target: '9', data: {} },
{ id: 'e23', source: '8', target: '9', data: {} },

{ 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: {} },
{ id: 'e28', source: '11', target: '12', data: {} },
{ id: 'e29', source: '11', target: '13', data: {} },
{ id: 'e30', source: '11', target: '14', data: {} },
{ id: 'e31', source: '12', target: '13', data: {} },
{ id: 'e32', source: '12', target: '14', data: {} },
{ id: 'e33', source: '13', target: '14', data: { weight: 5 } },

{ id: 'e34', source: '0', target: '5', data: {} },
{ id: 'e35', source: '5', target: '10', data: {} },
{ id: 'e36', source: '10', target: '0', data: {} },
{ id: 'e37', source: '10', target: '0', data: {} },
],
});
const clusteredData = louvain(graph, false, 'weight');
Expand Down Expand Up @@ -64,4 +94,4 @@ describe('Louvain', () => {
expect(clusteredData.clusters[2].sumTot).toBe(4);
expect(clusteredData.clusterEdges.length).toBe(7);
});
});
});
30 changes: 17 additions & 13 deletions __tests__/utils/data.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { NodeID, INode, IEdge } from "../../packages/graph/src/types";
import { ID } from '@antv/graphlib';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

其他都是同一 ID 从 graphlib 导出

import { INode, IEdge } from '../../packages/graph/src/types';
/**
* Convert the old version of the data format to the new version
* @param data old data
* @return {{nodes:INode[],edges:IEdge[]}} new data
*/
export const dataTransformer = (data: { nodes: { id: NodeID, [key: string]: any }[], edges: { source: NodeID, target: NodeID, [key: string]: any }[] }): { nodes: INode[], edges: IEdge[] } => {
const { nodes, edges } = data;
return {
nodes: nodes.map((n) => {
const { id, ...rest } = n;
return { id, data: rest ? rest : {} };
}),
edges: edges.map((e, i) => {
const { id, source, target, ...rest } = e;
return { id: id ? id : `edge-${i}`, target, source, data: rest };
}),
};
export const dataTransformer = (data: {
nodes: { id: ID; [key: string]: any }[];
edges: { source: ID; target: ID; [key: string]: any }[];
}): { nodes: INode[]; edges: IEdge[] } => {
const { nodes, edges } = data;
return {
nodes: nodes.map((n) => {
const { id, ...rest } = n;
return { id, data: rest ? rest : {} };
}),
edges: edges.map((e, i) => {
const { id, source, target, ...rest } = e;
return { id: id ? id : `edge-${i}`, target, source, data: rest };
}),
};
};
27 changes: 16 additions & 11 deletions packages/graph/src/bfs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ID } from '@antv/graphlib';
import Queue from './structs/queue';
import { Graph, IAlgorithmCallbacks, NodeID } from './types';
import { Graph, IAlgorithmCallbacks } from './types';

/**
* @param startNodeId The ID of the bfs traverse starting node.
Expand All @@ -8,11 +9,14 @@ import { Graph, IAlgorithmCallbacks, NodeID } from './types';
- enterNode: Called when BFS visits a node.
- leaveNode: Called after BFS visits the node.
*/
function initCallbacks(callbacks: IAlgorithmCallbacks = {} as IAlgorithmCallbacks) {
function initCallbacks(
callbacks: IAlgorithmCallbacks = {} as IAlgorithmCallbacks
) {
const initiatedCallback = callbacks;
const stubCallback = () => { };
const stubCallback = () => {};
const allowTraversalCallback = () => true;
initiatedCallback.allowTraversal = callbacks.allowTraversal || allowTraversalCallback;
initiatedCallback.allowTraversal =
callbacks.allowTraversal || allowTraversalCallback;
initiatedCallback.enter = callbacks.enter || stubCallback;
initiatedCallback.leave = callbacks.leave || stubCallback;
return initiatedCallback;
Expand All @@ -26,19 +30,19 @@ Performs breadth-first search (BFS) traversal on a graph.
*/
export const breadthFirstSearch = (
graph: Graph,
startNodeId: NodeID,
originalCallbacks?: IAlgorithmCallbacks,
startNodeId: ID,
originalCallbacks?: IAlgorithmCallbacks
) => {
const visit = new Set<NodeID>();
const visit = new Set<ID>();
const callbacks = initCallbacks(originalCallbacks);
const nodeQueue = new Queue<NodeID>();
const nodeQueue = new Queue<ID>();
// init Queue. Enqueue node ID.
nodeQueue.enqueue(startNodeId);
visit.add(startNodeId);
let previousNodeId: NodeID = '';
let previousNodeId: ID = '';
// 遍历队列中的所有顶点
while (!nodeQueue.isEmpty()) {
const currentNodeId: NodeID = nodeQueue.dequeue();
const currentNodeId: ID = nodeQueue.dequeue();
callbacks.enter({
current: currentNodeId,
previous: previousNodeId,
Expand All @@ -52,7 +56,8 @@ export const breadthFirstSearch = (
previous: previousNodeId,
current: currentNodeId,
next: nextNodeId,
}) && !visit.has(nextNodeId)
}) &&
!visit.has(nextNodeId)
) {
visit.add(nextNodeId);
nodeQueue.enqueue(nextNodeId);
Expand Down
18 changes: 11 additions & 7 deletions packages/graph/src/connected-component.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Graph, INode, NodeID } from './types';
import { ID } from '@antv/graphlib';
import { Graph, INode } from './types';
/**
* Generate all connected components for an undirected graph
* @param graph
*/
export const detectConnectedComponents = (graph: Graph): INode[][] => {
const nodes = graph.getAllNodes();
const allComponents: INode[][] = [];
const visited: { [key: NodeID]: boolean } = {};
const visited: { [key: ID]: boolean } = {};
const nodeStack: INode[] = [];
const getComponent = (node: INode) => {
nodeStack.push(node);
Expand Down Expand Up @@ -49,9 +50,9 @@ export const detectStrongConnectComponents = (graph: Graph): INode[][] => {
const nodes = graph.getAllNodes();
const nodeStack: INode[] = [];
// Assist to determine whether it is already in the stack to reduce the search overhead
const inStack: { [key: NodeID]: boolean } = {};
const indices: { [key: NodeID]: number } = {};
const lowLink: { [key: NodeID]: number } = {};
const inStack: { [key: ID]: boolean } = {};
const indices: { [key: ID]: number } = {};
const lowLink: { [key: ID]: number } = {};
const allComponents: INode[][] = [];
let index = 0;
const getComponent = (node: INode) => {
Expand All @@ -61,7 +62,7 @@ export const detectStrongConnectComponents = (graph: Graph): INode[][] => {
index += 1;
nodeStack.push(node);
inStack[node.id] = true;
const relatedEdges = graph.getRelatedEdges(node.id, "out");
const relatedEdges = graph.getRelatedEdges(node.id, 'out');
for (let i = 0; i < relatedEdges.length; i++) {
const targetNodeID = relatedEdges[i].target;
if (!indices[targetNodeID] && indices[targetNodeID] !== 0) {
Expand Down Expand Up @@ -98,7 +99,10 @@ export const detectStrongConnectComponents = (graph: Graph): INode[][] => {
return allComponents;
};

export function getConnectedComponents(graph: Graph, directed?: boolean): INode[][] {
export function getConnectedComponents(
graph: Graph,
directed?: boolean
): INode[][] {
if (directed) return detectStrongConnectComponents(graph);
return detectConnectedComponents(graph);
}
Loading