feat: dijkstra shortest path length algorithm; fix: integrate getSourceNeighbors and getTargetNeighbors to getNeighbors(node, type)

This commit is contained in:
Yanyan-Wang 2020-07-06 10:41:23 +08:00 committed by Yanyan Wang
parent 684cd874ec
commit b1d494610f
13 changed files with 146 additions and 158 deletions

View File

@ -5,9 +5,11 @@
- fix: dulplicated edges in nodeselectchange event of brush-select;
- fix: triple click and drag canvas problem;
- fix: sync the minZoom and maxZoom in drag-canvas and graph;
- fix: integrate getSourceNeighbors and getTargetNeighbors to getNeighbors(node, type);
- feat: initial x and y for combo data;
- feat: dagre layout supports sortByCombo;
- feat: allow user to disable relayout in collapse-expand-combo behavior.
- feat: allow user to disable relayout in collapse-expand-combo behavior;
- feat: dijkstra shortest path lenght algorithm.
#### 3.5.9
- fix: multiple animate update shape for combo;

View File

@ -50,7 +50,7 @@
"site:deploy": "npm run site:build && gh-pages -d public",
"start": "npm run site:develop",
"test": "jest",
"test-live": "DEBUG_MODE=1 jest --watch ./tests/unit/graph/svg-spec.ts",
"test-live": "DEBUG_MODE=1 jest --watch ./tests/unit/graph/tree-graph-spec.ts",
"lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx",
"watch": "father build -w",
"cdn": "antv-bin upload -n @antv/g6"

View File

@ -61,7 +61,7 @@ const breadthFirstSearch = (graph: IGraph, startNodeId: string, originalCallback
})
// 将所有邻居添加到队列中以便遍历
graph.getSourceNeighbors(currentNode).forEach((nextNode: INode) => {
graph.getNeighbors(currentNode, 'target').forEach((nextNode: INode) => {
if (callbacks.allowTraversal({
previous: previousNode,
current: currentNode,

View File

@ -39,7 +39,7 @@ function depthFirstSearchRecursive(graph: IGraph, currentNode: INode, previousNo
previous: previousNode
});
graph.getSourceNeighbors(currentNode).forEach((nextNode) => {
graph.getNeighbors(currentNode, 'target').forEach((nextNode) => {
if (callbacks.allowTraversal({
previous: previousNode,
current: currentNode,

48
src/algorithm/dijkstra.ts Normal file
View File

@ -0,0 +1,48 @@
import { IGraph } from '../interface/graph';
const dijkstra = (graph: IGraph, source: string, directed?: boolean, weightPropertyName?: string) => {
const nodes = graph.getNodes();
const nodeIdxMap = {};
let i, v;
const marks = [];
const D = [];
nodes.forEach((node, i) => {
const id = node.getID();
nodeIdxMap[id] = i;
D[i] = Infinity;
if (id === source) D[i] = 0;
});
const nodeNum = nodes.length;
for (i = 0; i < nodeNum; i++) { //Process the vertices
v = minVertex(D, nodeNum, marks);
if (D[v] === Infinity) return; //Unreachable vertices
marks[i] = true;
let relatedEdges = [];
if (!directed) relatedEdges = nodes[v].getOutEdges();
else relatedEdges = nodes[v].getEdges();
relatedEdges.forEach(e => {
const w = nodeIdxMap[e.getTarget().getID()];
const weight = (weightPropertyName && e.getModel()[weightPropertyName]) ? e.getModel()[weightPropertyName] : 1;
if (D[w] > (D[v] + weight)) D[w] = D[v] + weight;
});
}
return D
}
function minVertex(D: number[], nodeNum: number, marks: boolean[]) {//找出最小的点
let i, v;
for (i = 0; i < nodeNum; i++) { //找没有被访问的点
if (marks[i] == false) v = i; break;
}
for (i++; i < nodeNum; i++) { //找比上面还小的未被访问的点注意此时的i++
if (!marks[i] && D[i] < D[v]) v = i;
return v;
}
}
export default dijkstra;

View File

@ -9,7 +9,7 @@ export default {
* /
*/
trigger: DEFAULT_TRIGGER,
onChange() {},
onChange() { },
};
},
getEvents(): { [key in G6Event]?: string } {
@ -22,6 +22,7 @@ export default {
// eslint-disable-next-line no-console
console.warn("Behavior collapse-expand 的 trigger 参数不合法,请输入 'click' 或 'dblclick'");
}
console.log('triggertrigger', trigger);
return {
[`node:${trigger}`]: 'onNodeClick',
};

View File

@ -2823,43 +2823,14 @@ export default class Graph extends EventEmitter implements IGraph {
* @returns {INode[]}
* @memberof IGraph
*/
public getNeighbors(node: string | INode): INode[] {
public getNeighbors(node: string | INode, type?: 'source' | 'target' | undefined): INode[] {
let item = node as INode
if (isString(node)) {
item = this.findById(node) as INode
}
return item.getNeighbors()
return item.getNeighbors(type)
}
/**
* node
*
* @param {(string | INode)} node ID
* @returns {INode[]}
* @memberof IGraph
*/
public getSourceNeighbors(node: string | INode): INode[] {
let item = node as INode
if (isString(node)) {
item = this.findById(node) as INode
}
return item.getSourceNeighbors()
}
/**
* node
*
* @param {(string | INode)} node ID
* @returns {INode[]}
* @memberof IGraph
*/
public getTargetNeighbors(node: string | INode): INode[] {
let item = node as INode
if (isString(node)) {
item = this.findById(node) as INode
}
return item.getTargetNeighbors()
}
/**

View File

@ -229,24 +229,6 @@ export interface IGraph extends EventEmitter {
*/
getCombos(): ICombo[];
/**
* node
*
* @param {(string | INode)} node ID
* @returns {INode[]}
* @memberof IGraph
*/
getSourceNeighbors(node: string | INode): INode[];
/**
* node
*
* @param {(string | INode)} node ID
* @returns {INode[]}
* @memberof IGraph
*/
getTargetNeighbors(node: string | INode): INode[];
/**
*
*
@ -254,7 +236,7 @@ export interface IGraph extends EventEmitter {
* @returns {INode[]}
* @memberof IGraph
*/
getNeighbors(node: string | INode): INode[];
getNeighbors(node: string | INode, type?: 'source' | 'target' | undefined): INode[];
/**
* combo

View File

@ -301,23 +301,7 @@ export interface INode extends IItemBase {
* @returns {INode[]}
* @memberof INode
*/
getNeighbors(): INode[];
/**
* node
*
* @returns {INode[]}
* @memberof INode
*/
getSourceNeighbors(): INode[];
/**
* node
*
* @returns {INode[]}
* @memberof INode
*/
getTargetNeighbors(): INode[];
getNeighbors(type?: 'source' | 'target' | undefined): INode[];
}
export interface ICombo extends INode {

View File

@ -6,7 +6,7 @@ import { IPoint, IShapeBase, NodeConfig } from '../types';
import {
distance,
getCircleIntersectByPoint,
getEllipseIntersectByPoint,
getEllipseIntersectByPoint,
getRectIntersectByPoint,
} from '../util/math';
import Item from './item';
@ -68,48 +68,30 @@ export default class Node extends Item implements INode {
* @returns {INode[]}
* @memberof Node
*/
public getNeighbors(): INode[] {
public getNeighbors(type?: 'target' | 'source' | undefined): INode[] {
const edges = this.get('edges') as IEdge[]
if (type === 'target') {
// 当前节点为 source它所指向的目标节点
const neighhborsConverter = (edge: IEdge) => {
return edge.getSource() === this;
}
return edges.filter(neighhborsConverter).map(edge => edge.getTarget());
} else if (type === 'source') {
// 当前节点为 target它所指向的源节点
const neighhborsConverter = (edge: IEdge) => {
return edge.getTarget() === this
}
return edges.filter(neighhborsConverter).map(edge => edge.getSource())
}
// 若未指定 type ,则返回所有邻居
const neighhborsConverter = (edge: IEdge) => {
return edge.getSource() === this ? edge.getTarget() : edge.getSource()
}
return edges.map(neighhborsConverter)
}
/**
* source的邻居节点
*
* @returns {INode[]}
* @memberof Node
*/
public getSourceNeighbors(): INode[] {
const edges = this.get('edges') as IEdge[]
const neighhborsConverter = (edge: IEdge) => {
return edge.getSource() === this
}
return edges.filter(neighhborsConverter).map(edge => edge.getTarget())
}
/**
* target的邻居节点
*
* @returns {INode[]}
* @memberof Node
*/
public getTargetNeighbors(): INode[] {
const edges = this.get('edges') as IEdge[]
const neighhborsConverter = (edge: IEdge) => {
return edge.getTarget() === this
}
return edges.filter(neighhborsConverter).map(edge => edge.getSource())
}
/**
*
* @param {Number} index

View File

@ -128,6 +128,20 @@ export default class DagreLayout extends BaseLayout {
Object.keys(levels).forEach(key => {
const levelNodes = levels[key].nodes;
const nodesNum = levelNodes.length;
const comboCenters = {};
levelNodes.forEach(lnode => {
const lnodeCombo = lnode.comboId;
if (!comboCenters[lnodeCombo]) comboCenters[lnodeCombo] = { x: 0, y: 0, count: 0 };
comboCenters[lnodeCombo].x += lnode.x;
comboCenters[lnodeCombo].y += lnode.y;
comboCenters[lnodeCombo].count++;
});
Object.keys(comboCenters).forEach(ckey => {
comboCenters[ckey].x /= comboCenters[ckey].count;
comboCenters[ckey].y /= comboCenters[ckey].count;
});
if (nodesNum === 1) return;
const sortedByX = levelNodes.sort((a, b) => { return a.x - b.x });
const minX = sortedByX[0].x;

View File

@ -60,7 +60,7 @@ describe('graph', () => {
it('invalid container', () => {
expect(() => {
// eslint-disable-next-line no-new
new Graph({} as any);
new Graph({} as any);
}).toThrowError('invalid container');
});
@ -1461,7 +1461,7 @@ describe('auto rotate label on edge', () => {
graph.on('canvas:click', evt => {
graph.downloadFullImage('graph', {
backgroundColor: '#fff',
padding: [ 40, 10, 10, 10 ]
padding: [40, 10, 10, 10]
});
});
});
@ -1536,21 +1536,21 @@ describe('node Neighbors', () => {
graph.render()
it('getSourceNeighbors', () => {
const neighbors = graph.getSourceNeighbors('B')
const neighbors = graph.getNeighbors('B', 'target')
expect(neighbors.length).toBe(1)
expect(neighbors[0].getID()).toEqual('C')
const neighborE = graph.getSourceNeighbors('A')
const neighborE = graph.getNeighbors('A', 'target')
expect(neighborE.length).toBe(3)
expect(neighborE[0].getID()).toEqual('B')
})
it('getTargetNeighbors', () => {
const neighbors = graph.getTargetNeighbors('B')
const neighbors = graph.getNeighbors('B', 'source')
expect(neighbors.length).toBe(1)
expect(neighbors[0].getID()).toEqual('A')
const neighborE = graph.getTargetNeighbors('E')
const neighborE = graph.getNeighbors('E', 'source')
expect(neighborE.length).toBe(1)
expect(neighborE[0].getID()).toEqual('A')
})
@ -1619,7 +1619,7 @@ describe('redo stack & undo stack', () => {
// update 后undo stack 中有 2 条数据,一条 render一条 update
graph.update('node1', {
x: 120,
x: 120,
y: 200
})
@ -1635,7 +1635,7 @@ describe('redo stack & undo stack', () => {
// 执行 update 后undo stack 中有3条数据
graph.update('node2', {
x: 120,
x: 120,
y: 350
})
@ -1696,7 +1696,7 @@ describe('redo stack & undo stack', () => {
let stackData = graph.getStackData()
let undoStack = stackData.undoStack
let redoStack = stackData.redoStack
expect(undoStack.length).toBe(0)
expect(redoStack.length).toBe(0)
})

View File

@ -121,7 +121,7 @@ describe('tree graph without animate', () => {
type: 'rect',
children: [{ x: 150, y: 150, id: 'SubTreeNode3.1.1' }],
};
graph.on('afteraddchild', function(e) {
graph.on('afteraddchild', function (e) {
expect(e.item.getModel().id === 'SubTreeNode3.1' || e.item.getModel().id === 'SubTreeNode3.1.1').toBe(true);
expect(e.item.get('parent').getModel().id === 'SubTreeNode3' || e.item.get('parent').getModel().id === 'SubTreeNode3.1').toBe(true);
expect(e.parent.getModel().id === 'SubTreeNode3' || e.parent.getModel().id === 'SubTreeNode3.1').toBe(true);
@ -535,11 +535,54 @@ describe('tree graph with animate', () => {
expect(edge.get('source')).toEqual(graph3.findById('SubTreeNode4'));
expect(edge.get('target')).toEqual(graph3.findById('SubTreeNode4.1'));
});
graph3.changeData(data3);
expect(graph3.save()).toEqual(data3);
});
it('collapse & expand with parameter trigger=dblclick', done => {
graph3.off();
graph3.moveTo(100, 150);
const parent = graph3.findById('SubTreeNode1');
let child = graph3.findById('SubTreeNode1.1');
let collapsed = undefined;
graph3.on('afteranimate', () => {
if (collapsed === undefined) return;
if (collapsed) {
expect(parent.getModel().collapsed).toBe(true);
expect(child.destroyed).toBe(true);
} else {
child = graph3.findById('SubTreeNode1.1');
expect(parent.getModel().collapsed).toBe(false);
expect(child.get('model').x).not.toEqual(parent.get('model').x);
expect(!!child.getModel().collapsed).toBe(false);
expect(child.get('model').y).not.toEqual(parent.get('model').y);
// done();
}
});
graph3.addBehaviors(
[
{
type: 'collapse-expand',
trigger: 'dblclick',
},
],
'default',
);
timerOut(() => {
graph3.emit('node:dblclick', { item: parent });
collapsed = true;
}, 600);
timerOut(() => {
collapsed = false;
graph3.emit('node:dblclick', { item: parent });
done();
}, 1200);
});
it('collapse & expand', () => {
graph3.off();
@ -567,45 +610,6 @@ describe('tree graph with animate', () => {
graph3.emit('node:click', { item: parent });
}, 600);
});
it('collapse & expand with parameter trigger=dblclick', () => {
graph3.off();
graph3.moveTo(100, 150);
const parent = graph3.findById('SubTreeNode1');
let child = graph3.findById('SubTreeNode1.1');
let collapsed = true;
graph3.on('afteranimate', () => {
if (collapsed) {
console.log(parent.getModel().collapsed, child.destroyed)
expect(parent.getModel().collapsed).toBe(true);
expect(child.destroyed).toBe(true);
} else {
child = graph3.findById('SubTreeNode1.1');
expect(parent.getModel().collapsed).toBe(false);
expect(child.get('model').x).not.toEqual(parent.get('model').x);
expect(!!child.getModel().collapsed).toBe(false);
expect(child.get('model').y).not.toEqual(parent.get('model').y);
// done();
}
});
graph3.addBehaviors(
[
{
type: 'collapse-expand',
trigger: 'dblclick',
},
],
'default',
);
graph3.emit('node:dblclick', { item: parent });
timerOut(() => {
collapsed = false;
graph3.emit('node:dblclick', { item: parent });
}, 600);
});
// it('test', () => {
// const data = {