From 76af4f59f2b4c8dcf5f9b2af44557b63ac19bc55 Mon Sep 17 00:00:00 2001 From: Yanyan-Wang Date: Tue, 23 Jun 2020 19:26:18 +0800 Subject: [PATCH] feat: adjacency matrix in Algorithm. feat: floydShall shortest path in Algorithm. --- package.json | 2 +- src/algorithm/adjacent-matrix.ts | 40 +++ src/algorithm/floydWarshall.ts | 41 +++ src/algorithm/index.ts | 4 + src/graph/graph.ts | 58 +++- stories/Combo/combo.stories.tsx | 3 + stories/Combo/component/edges2.tsx | 97 +++++++ tests/unit/algorithm/adjacent-matrix-spec.ts | 234 ++++++++++++++++ .../algorithm/shortestpath-matrix-spec.ts | 264 ++++++++++++++++++ 9 files changed, 737 insertions(+), 6 deletions(-) create mode 100644 src/algorithm/adjacent-matrix.ts create mode 100644 src/algorithm/floydWarshall.ts create mode 100644 stories/Combo/component/edges2.tsx create mode 100644 tests/unit/algorithm/adjacent-matrix-spec.ts create mode 100644 tests/unit/algorithm/shortestpath-matrix-spec.ts diff --git a/package.json b/package.json index 85a025a039..a87e0c4922 100644 --- a/package.json +++ b/package.json @@ -126,4 +126,4 @@ "webpack-cli": "^3.3.10", "worker-loader": "^2.0.0" } -} +} \ No newline at end of file diff --git a/src/algorithm/adjacent-matrix.ts b/src/algorithm/adjacent-matrix.ts new file mode 100644 index 0000000000..42fd00e209 --- /dev/null +++ b/src/algorithm/adjacent-matrix.ts @@ -0,0 +1,40 @@ +import { IGraph } from '../interface/graph'; +import { Matrix } from '../types'; + +const adjMatrix = (graph: IGraph, directed?: boolean) => { + + const nodes = graph.getNodes(); + const edges = graph.getEdges(); + const matrix: Matrix[] = []; + // map node with index in data.nodes + const nodeMap: { + [key: string]: number; + } = {}; + + if (!nodes) { + throw new Error('invalid nodes data!'); + } + if (nodes) { + nodes.forEach((node, i) => { + nodeMap[node.getID()] = i; + const row: number[] = []; + matrix.push(row); + }); + } + + if (edges) { + edges.forEach(e => { + const model = e.getModel(); + const { source, target } = model; + const sIndex = nodeMap[source as string]; + const tIndex = nodeMap[target as string]; + matrix[sIndex][tIndex] = 1; + if (!directed) { + matrix[tIndex][sIndex] = 1; + } + }); + } + return matrix +} + +export default adjMatrix diff --git a/src/algorithm/floydWarshall.ts b/src/algorithm/floydWarshall.ts new file mode 100644 index 0000000000..7285212836 --- /dev/null +++ b/src/algorithm/floydWarshall.ts @@ -0,0 +1,41 @@ +import { IGraph } from '../interface/graph'; +import adjMatrix from './adjacent-matrix'; +import { Matrix } from '../types'; + + +const floydWarshall = (graph: IGraph, directed?: boolean) => { + + let adjacentMatrix = graph.get('adjMatrix'); + if (!adjacentMatrix) { + adjacentMatrix = adjMatrix(graph, directed); + } + + + const dist: Matrix[] = []; + const size = adjacentMatrix.length; + for (let i = 0; i < size; i += 1) { + dist[i] = []; + for (let j = 0; j < size; j += 1) { + if (i === j) { + dist[i][j] = 0; + } else if (adjacentMatrix[i][j] === 0 || !adjacentMatrix[i][j]) { + dist[i][j] = Infinity; + } else { + dist[i][j] = adjacentMatrix[i][j]; + } + } + } + // floyd + for (let k = 0; k < size; k += 1) { + for (let i = 0; i < size; i += 1) { + for (let j = 0; j < size; j += 1) { + if (dist[i][j] > dist[i][k] + dist[k][j]) { + dist[i][j] = dist[i][k] + dist[k][j]; + } + } + } + } + return dist; +} + +export default floydWarshall diff --git a/src/algorithm/index.ts b/src/algorithm/index.ts index 4c666b9d79..f07d0fb086 100644 --- a/src/algorithm/index.ts +++ b/src/algorithm/index.ts @@ -1,3 +1,7 @@ export { default as depthFirstSearch } from './dfs' export { default as breadthFirstSearch } from './bfs' export { default as detectDirectedCycle } from './detect-cycle' +export { default as degree } from './degree' +export { default as adjMatrix } from './adjacent-matrix' +export { default as floydWarshall } from './floydWarshall' + diff --git a/src/graph/graph.ts b/src/graph/graph.ts index 82e224e576..dd6c7643a4 100644 --- a/src/graph/graph.ts +++ b/src/graph/graph.ts @@ -46,6 +46,8 @@ import createDom from '@antv/dom-util/lib/create-dom'; import { plainCombosToTrees, traverseTree, reconstructTree, traverseTreeUp } from '../util/graphic'; import degree from '../algorithm/degree'; import Stack from '../algorithm/structs/stack' +import adjMatrix from '../algorithm/adjacent-matrix'; +import floydWarshall from '../algorithm/floydWarshall' const NODE = 'node'; const SVG = 'svg'; @@ -2370,8 +2372,13 @@ export default class Graph extends EventEmitter implements IGraph { console.warn('The combo to be collapsed does not exist!'); return; } + const comboModel = combo.getModel(); + const itemController: ItemController = this.get('itemController'); + itemController.collapseCombo(combo); + comboModel.collapsed = true; + // add virtual edges const edges = this.getEdges().concat(this.get('vedges')); @@ -2414,6 +2421,7 @@ export default class Graph extends EventEmitter implements IGraph { const edgeWeightMap = {}; const addedVEdges = []; edges.forEach(edge => { + if (edge.isVisible() && !edge.getModel().isVEdge) return; let source = edge.getSource(); let target = edge.getTarget(); if (((cnodes.includes(source) || ccombos.includes(source)) @@ -2485,9 +2493,6 @@ export default class Graph extends EventEmitter implements IGraph { }, false) }); - const itemController: ItemController = this.get('itemController'); - itemController.collapseCombo(combo); - comboModel.collapsed = true; } /** @@ -2548,6 +2553,7 @@ export default class Graph extends EventEmitter implements IGraph { const edgeWeightMap = {}; const addedVEdges = {}; edges.forEach(edge => { + if (edge.isVisible() && !edge.getModel().isVEdge) return; let source = edge.getSource(); let target = edge.getTarget(); let sourceId = source.get('id'); @@ -2907,7 +2913,7 @@ export default class Graph extends EventEmitter implements IGraph { console.warn('请先启用 undo & redo 功能,在实例化 Graph 时候配置 enabledStack: true !') return } - + const stackData = data ? clone(data) : clone(this.save()) if (stackType === 'redo') { @@ -2926,6 +2932,48 @@ export default class Graph extends EventEmitter implements IGraph { undoStack: this.undoStack, redoStack: this.redoStack }) + + + /** + * 获取邻接矩阵 + * + * @param {boolean} cache 是否使用缓存的 + * @param {boolean} directed 是否是有向图,默认取 graph.directed + * @returns {Matrix} 邻接矩阵 + * @memberof IGraph + */ + public getAdjMatrix(cache: boolean = true, directed?: boolean): Number | Object { + if (directed === undefined) directed = this.get('directed'); + let currentAdjMatrix = this.get('adjMatrix'); + if (!currentAdjMatrix || !cache) { + currentAdjMatrix = adjMatrix(this, directed); + this.set('adjMatrix', currentAdjMatrix); + } + return currentAdjMatrix; + } + + + /** + * 获取最短路径矩阵 + * + * @param {boolean} cache 是否使用缓存的 + * @param {boolean} directed 是否是有向图,默认取 graph.directed + * @returns {Matrix} 最短路径矩阵 + * @memberof IGraph + */ + public getShortestPathMatrix(cache: boolean = true, directed?: boolean): Number | Object { + if (directed === undefined) directed = this.get('directed'); + let currentAdjMatrix = this.get('adjMatrix'); + let currentShourtestPathMatrix = this.get('shortestPathMatrix'); + if (!currentAdjMatrix || !cache) { + currentAdjMatrix = adjMatrix(this, directed); + this.set('adjMatrix', currentAdjMatrix); + } + if (!currentShourtestPathMatrix || !cache) { + currentShourtestPathMatrix = floydWarshall(this, directed); + this.set('shortestPathMatrix', currentShourtestPathMatrix); + } + return currentShourtestPathMatrix; } /** @@ -2936,7 +2984,7 @@ export default class Graph extends EventEmitter implements IGraph { // 清空栈数据 this.clearStack(); - + each(this.get('plugins'), plugin => { plugin.destroyPlugin(); }); diff --git a/stories/Combo/combo.stories.tsx b/stories/Combo/combo.stories.tsx index 0b0e4aa588..c47841f597 100644 --- a/stories/Combo/combo.stories.tsx +++ b/stories/Combo/combo.stories.tsx @@ -13,6 +13,7 @@ import RegisterRectCombo from './component/register-rect-combo'; import RegisterCircleCombo from './component/register-circle-combo'; import Edges from './component/edges'; import DagreCombo from './component/dagre-combo'; +import Edges2 from './component/edges2'; export default { title: 'Combo' }; @@ -48,4 +49,6 @@ storiesOf('Combo', module) )).add('dagre combo', () => ( + )).add('edges2 ', () => ( + )); diff --git a/stories/Combo/component/edges2.tsx b/stories/Combo/component/edges2.tsx new file mode 100644 index 0000000000..3b5acaf8f6 --- /dev/null +++ b/stories/Combo/component/edges2.tsx @@ -0,0 +1,97 @@ +import React, { useEffect } from 'react'; +import G6 from '../../../src'; +import { IGraph } from '../../../src/interface/graph'; +import { GraphData } from '../../../src/types'; + +let graph: IGraph = null; + +const data: GraphData = { + nodes: [{ + id: '1', + comboId: '分组1', + x: 100, + y: 100 + }, { + id: '2', + comboId: '分组1', + x: 200, + y: 100 + }, { + id: '4', + comboId: '分组2', + x: 400, + y: 100 + } + ], + edges: [ + { + source: '分组1', + target: '分组2', + } + ], + combos: [ + { + id: '分组1', + label: '分组1', + //collapsed: true + }, + { + id: '分组2', + label: '分组2', + //collapsed: true, + } + ] +}; + +const Edges2 = () => { + const container = React.useRef(); + useEffect(() => { + if (!graph) { + const graph = new G6.Graph({ + container: container.current as string | HTMLElement, + width: 1000, + height: 800, + groupByTypes: false, + defaultEdge: { + style: { + endArrow: true + } + }, + defaultCombo: { + type: 'rect', + size: [50, 60], // The minimum size of the Combo + padding: [20, 10, 10, 20], + style: { + lineWidth: 3, + }, + anchorPoints: [ + [0.5, 1], + [0.5, 0], + ], + labelCfg: { + refY: 10, + refX: 20, + position: 'top', + }, + }, + modes: { + default: ['drag-canvas', 'drag-node', + { + type: 'drag-combo', + enableDelegate: true //拖动时禁止合并 + } + ], + }, + }); + graph.data(data); + graph.render(); + graph.on('combo:click', function (e) { + graph.collapseExpandCombo(e.item); + graph.refreshPositions(); + }); + } + }); + return
; +}; + +export default Edges2; diff --git a/tests/unit/algorithm/adjacent-matrix-spec.ts b/tests/unit/algorithm/adjacent-matrix-spec.ts new file mode 100644 index 0000000000..7ed2eeeb3f --- /dev/null +++ b/tests/unit/algorithm/adjacent-matrix-spec.ts @@ -0,0 +1,234 @@ +import G6, { Algorithm } from '../../../src'; +const { adjMatrix } = Algorithm; + +const div = document.createElement('div'); +div.id = 'container'; +document.body.appendChild(div); + +const data = { + nodes: [ + { + id: 'A', + label: '0' + }, + { + id: 'B', + label: '1' + }, + { + id: 'C', + label: '2' + }, + { + id: 'D', + label: '3' + }, + { + id: 'E', + label: '4' + }, + { + id: 'F', + label: '5' + }, + { + id: 'G', + label: '6' + }, + { + id: 'H', + label: '7' + }, + ], + edges: [ + { + source: 'A', + target: 'B', + }, + { + source: 'B', + target: 'C', + }, + { + source: 'C', + target: 'G', + }, + { + source: 'A', + target: 'D', + }, + { + source: 'A', + target: 'E', + }, + { + source: 'E', + target: 'F', + }, + { + source: 'F', + target: 'D', + }, + ] +} + +describe('Adjacency Matrix on graph', () => { + const graph = new G6.Graph({ + container: 'container', + width: 500, + height: 500, + defaultEdge: { + style: { + endArrow: true + } + } + }) + + graph.data(data) + graph.render() + + it('get graph adjacency matrix', () => { + const matrix = graph.getAdjMatrix(); + expect(Object.keys(matrix).length).toBe(8); + const node0Adj = matrix[0]; + expect(node0Adj.length).toBe(5); + expect(node0Adj[0]).toBe(undefined); + expect(node0Adj[1]).toBe(1); + expect(node0Adj[2]).toBe(undefined); + expect(node0Adj[3]).toBe(1); + expect(node0Adj[4]).toBe(1); + }); + + it('add items and cache', () => { + graph.addItem('node', { + id: 'I', + label: '8' + }); + graph.addItem('edge', { + source: 'I', + target: 'A' + }); + graph.addItem('edge', { + source: 'C', + target: 'A' + }); + // use the cache + const cachedMatrix = graph.getAdjMatrix(); + expect(Object.keys(cachedMatrix).length).toBe(8); + const cachedNode0Adj = cachedMatrix[0]; + expect(cachedNode0Adj.length).toBe(5); + expect(cachedNode0Adj[0]).toBe(undefined); + expect(cachedNode0Adj[1]).toBe(1); + expect(cachedNode0Adj[2]).toBe(undefined); + expect(cachedNode0Adj[3]).toBe(1); + expect(cachedNode0Adj[4]).toBe(1); + + // do not use the cache + const matrix = graph.getAdjMatrix(false); + expect(Object.keys(matrix).length).toBe(9); + const node0Adj = matrix[0]; + expect(node0Adj.length).toBe(9); + expect(node0Adj[0]).toBe(undefined); + expect(node0Adj[1]).toBe(1); + expect(node0Adj[2]).toBe(1); + expect(node0Adj[3]).toBe(1); + expect(node0Adj[4]).toBe(1); + expect(node0Adj[5]).toBe(undefined); + expect(node0Adj[6]).toBe(undefined); + expect(node0Adj[7]).toBe(undefined); + expect(node0Adj[8]).toBe(1); + }) + + it('directed', () => { + // do not use the cache and directed + const matrix = graph.getAdjMatrix(false, true); + expect(Object.keys(matrix).length).toBe(9); + const node0Adj = matrix[0]; + expect(node0Adj.length).toBe(5); + expect(node0Adj[0]).toBe(undefined); + expect(node0Adj[1]).toBe(1); + expect(node0Adj[2]).toBe(undefined); + expect(node0Adj[3]).toBe(1); + expect(node0Adj[4]).toBe(1); + const node8Adj = matrix[8]; + expect(node8Adj.length).toBe(1); + expect(node8Adj[0]).toBe(1); + graph.destroy(); + }); +}); + + + +describe('Adjacency Matrix by Algorithm', () => { + const graph = new G6.Graph({ + container: 'container', + width: 500, + height: 500, + defaultEdge: { + style: { + endArrow: true + } + } + }) + + graph.data(data) + graph.render() + + it('get graph adjacency matrix', () => { + const matrix = adjMatrix(graph); + expect(Object.keys(matrix).length).toBe(8); + const node0Adj = matrix[0]; + expect(node0Adj.length).toBe(5); + expect(node0Adj[0]).toBe(undefined); + expect(node0Adj[1]).toBe(1); + expect(node0Adj[2]).toBe(undefined); + expect(node0Adj[3]).toBe(1); + expect(node0Adj[4]).toBe(1); + }); + + it('add items and cache', () => { + graph.addItem('node', { + id: 'I', + label: '8' + }); + graph.addItem('edge', { + source: 'I', + target: 'A' + }); + graph.addItem('edge', { + source: 'C', + target: 'A' + }); + + const matrix = adjMatrix(graph); + expect(Object.keys(matrix).length).toBe(9); + const node0Adj = matrix[0]; + expect(node0Adj.length).toBe(9); + expect(node0Adj[0]).toBe(undefined); + expect(node0Adj[1]).toBe(1); + expect(node0Adj[2]).toBe(1); + expect(node0Adj[3]).toBe(1); + expect(node0Adj[4]).toBe(1); + expect(node0Adj[5]).toBe(undefined); + expect(node0Adj[6]).toBe(undefined); + expect(node0Adj[7]).toBe(undefined); + expect(node0Adj[8]).toBe(1); + }) + + it('directed', () => { + // directed + const matrix = adjMatrix(graph, true); + expect(Object.keys(matrix).length).toBe(9); + const node0Adj = matrix[0]; + expect(node0Adj.length).toBe(5); + expect(node0Adj[0]).toBe(undefined); + expect(node0Adj[1]).toBe(1); + expect(node0Adj[2]).toBe(undefined); + expect(node0Adj[3]).toBe(1); + expect(node0Adj[4]).toBe(1); + const node8Adj = matrix[8]; + expect(node8Adj.length).toBe(1); + expect(node8Adj[0]).toBe(1); + graph.destroy(); + }); +}); \ No newline at end of file diff --git a/tests/unit/algorithm/shortestpath-matrix-spec.ts b/tests/unit/algorithm/shortestpath-matrix-spec.ts new file mode 100644 index 0000000000..9756686747 --- /dev/null +++ b/tests/unit/algorithm/shortestpath-matrix-spec.ts @@ -0,0 +1,264 @@ +import G6, { Algorithm } from '../../../src'; +const { floydWarshall } = Algorithm; + +const div = document.createElement('div'); +div.id = 'container'; +document.body.appendChild(div); + +const data = { + nodes: [ + { + id: 'A', + label: '0' + }, + { + id: 'B', + label: '1' + }, + { + id: 'C', + label: '2' + }, + { + id: 'D', + label: '3' + }, + { + id: 'E', + label: '4' + }, + { + id: 'F', + label: '5' + }, + { + id: 'G', + label: '6' + }, + { + id: 'H', + label: '7' + }, + ], + edges: [ + { + source: 'A', + target: 'B', + }, + { + source: 'B', + target: 'C', + }, + { + source: 'C', + target: 'G', + }, + { + source: 'A', + target: 'D', + }, + { + source: 'A', + target: 'E', + }, + { + source: 'E', + target: 'F', + }, + { + source: 'F', + target: 'D', + }, + ] +} + +describe('Shortest Path Matrix on graph', () => { + const graph = new G6.Graph({ + container: 'container', + width: 500, + height: 500, + defaultEdge: { + style: { + endArrow: true + } + } + }) + + graph.data(data) + graph.render() + + it('get graph shortest path matrix', () => { + const matrix = graph.getShortestPathMatrix(); + console.log(matrix); + expect(Object.keys(matrix).length).toBe(8); + const node0 = matrix[0]; + expect(node0.length).toBe(8); + expect(node0[0]).toBe(0); + expect(node0[1]).toBe(1); + expect(node0[2]).toBe(2); + expect(node0[3]).toBe(1); + expect(node0[4]).toBe(1); + expect(node0[5]).toBe(2); + expect(node0[6]).toBe(3); + expect(node0[7]).toBe(Infinity); + expect(matrix[1][7]).toBe(Infinity); + expect(matrix[2][7]).toBe(Infinity); + expect(matrix[3][7]).toBe(Infinity); + }); + + it('add items and cache', () => { + graph.addItem('node', { + id: 'I', + label: '8' + }); + graph.addItem('edge', { + source: 'I', + target: 'A' + }); + graph.addItem('edge', { + source: 'C', + target: 'A' + }); + // use the cache + const cachedMatrix = graph.getShortestPathMatrix(); + expect(Object.keys(cachedMatrix).length).toBe(8); + const cachedNode0 = cachedMatrix[0]; + expect(cachedNode0.length).toBe(8); + expect(cachedNode0[0]).toBe(0); + expect(cachedNode0[1]).toBe(1); + expect(cachedNode0[2]).toBe(2); + expect(cachedNode0[3]).toBe(1); + expect(cachedNode0[4]).toBe(1); + expect(cachedNode0[5]).toBe(2); + expect(cachedNode0[6]).toBe(3); + expect(cachedNode0[7]).toBe(Infinity); + expect(cachedMatrix[1][7]).toBe(Infinity); + expect(cachedMatrix[2][7]).toBe(Infinity); + expect(cachedMatrix[3][7]).toBe(Infinity); + + // do not use the cache + const matrix = graph.getShortestPathMatrix(false); + expect(Object.keys(matrix).length).toBe(9); + console.log(matrix); + const node0 = matrix[0]; + expect(node0.length).toBe(9); + expect(node0[0]).toBe(0); + expect(node0[1]).toBe(1); + expect(node0[2]).toBe(1); + expect(node0[3]).toBe(1); + expect(node0[4]).toBe(1); + expect(node0[5]).toBe(2); + expect(node0[6]).toBe(2); + expect(node0[7]).toBe(Infinity); + expect(node0[8]).toBe(1); + }) + + it('directed', () => { + // do not use the cache and directed + const matrix = graph.getShortestPathMatrix(false, true); + expect(Object.keys(matrix).length).toBe(9); + console.log(matrix) + const node0 = matrix[0]; + expect(node0.length).toBe(9); + expect(node0[0]).toBe(0); + expect(node0[1]).toBe(1); + expect(node0[2]).toBe(2); + expect(node0[3]).toBe(1); + expect(node0[4]).toBe(1); + expect(node0[5]).toBe(2); + expect(node0[6]).toBe(3); + expect(node0[7]).toBe(Infinity); + expect(node0[8]).toBe(Infinity); + const node8 = matrix[8]; + expect(node8.length).toBe(9); + expect(node8[0]).toBe(1); + graph.destroy(); + }); +}); + + + +describe('Adjacency Matrix by Algorithm', () => { + const graph = new G6.Graph({ + container: 'container', + width: 500, + height: 500, + defaultEdge: { + style: { + endArrow: true + } + } + }) + + graph.data(data) + graph.render() + + it('get graph shortestpath matrix', () => { + const matrix = floydWarshall(graph); + expect(Object.keys(matrix).length).toBe(8); + const node0 = matrix[0]; + expect(node0.length).toBe(8); + expect(node0[0]).toBe(0); + expect(node0[1]).toBe(1); + expect(node0[2]).toBe(2); + expect(node0[3]).toBe(1); + expect(node0[4]).toBe(1); + expect(node0[5]).toBe(2); + expect(node0[6]).toBe(3); + expect(node0[7]).toBe(Infinity); + expect(matrix[1][7]).toBe(Infinity); + expect(matrix[2][7]).toBe(Infinity); + expect(matrix[3][7]).toBe(Infinity); + }); + + it('add items and cache', () => { + graph.addItem('node', { + id: 'I', + label: '8' + }); + graph.addItem('edge', { + source: 'I', + target: 'A' + }); + graph.addItem('edge', { + source: 'C', + target: 'A' + }); + + const matrix = floydWarshall(graph); + expect(Object.keys(matrix).length).toBe(9); + console.log(matrix); + const node0 = matrix[0]; + expect(node0.length).toBe(9); + expect(node0[0]).toBe(0); + expect(node0[1]).toBe(1); + expect(node0[2]).toBe(1); + expect(node0[3]).toBe(1); + expect(node0[4]).toBe(1); + expect(node0[5]).toBe(2); + expect(node0[6]).toBe(2); + expect(node0[7]).toBe(Infinity); + expect(node0[8]).toBe(1); + }) + + it('directed', () => { + // directed + const matrix = floydWarshall(graph, true); + expect(Object.keys(matrix).length).toBe(9); + const node0 = matrix[0]; + expect(node0.length).toBe(9); + expect(node0[0]).toBe(0); + expect(node0[1]).toBe(1); + expect(node0[2]).toBe(2); + expect(node0[3]).toBe(1); + expect(node0[4]).toBe(1); + expect(node0[5]).toBe(2); + expect(node0[6]).toBe(3); + expect(node0[7]).toBe(Infinity); + expect(node0[8]).toBe(Infinity); + const node8 = matrix[8]; + expect(node8.length).toBe(9); + expect(node8[0]).toBe(1); + graph.destroy(); + }); +}); \ No newline at end of file