Skip to content

Commit

Permalink
Merge pull request #278 from antvis/force-layout-optimization
Browse files Browse the repository at this point in the history
perf: ⚡️ new force computing method for force directed layout
  • Loading branch information
pomelo-nwu authored Aug 9, 2021
2 parents 46293b2 + c259e8f commit 56935ca
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 9 deletions.
2 changes: 1 addition & 1 deletion packages/graphin-components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@antv/graphin-components",
"version": "2.2.0",
"version": "2.3.0",
"description": "Components for graphin",
"main": "lib/index.js",
"module": "es/index.js",
Expand Down
3 changes: 2 additions & 1 deletion packages/graphin-components/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
export { default as Combo } from './Combo';
export { default as ContextMenu } from './ContextMenu';
export { default as CreateEdge } from './CreateEdge';
export { default as EdgeBundling } from './EdgeBundling';
export { default as FishEye } from './FishEye';
export { default as Grid } from './Grid';
export { default as Hull, HullCfg } from './Hull';
export { default as LayoutSelector } from './LayoutSelector';
export { default as Legend } from './Legend';
export { default as MiniMap } from './MiniMap';
export { default as Toolbar } from './Toolbar';
export { default as Tooltip } from './Tooltip';
export { default as Grid } from './Grid';
export * from './typing';
export { default as VisSettingPanel } from './VisSettingPanel';
8 changes: 5 additions & 3 deletions packages/graphin/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@antv/graphin",
"version": "2.2.0",
"version": "2.3.0",
"description": "the react toolkit for graph analysis based on g6",
"main": "lib/index.js",
"module": "es/index.js",
Expand Down Expand Up @@ -40,7 +40,8 @@
"typescript": "^4.1.3",
"webpack": "^4.41.5",
"webpack-bundle-analyzer": "^3.6.0",
"webpack-cli": "^3.3.10"
"webpack-cli": "^3.3.10",
"@types/d3-quadtree": "^3.0.2"
},
"sideEffects": [
"*.css"
Expand All @@ -50,7 +51,8 @@
"dependencies": {
"@antv/g6": "4.3.4",
"@antv/util": "^2.0.10",
"lodash-es": "^4.17.21"
"lodash-es": "^4.17.21",
"d3-quadtree": "^3.0.1"
},
"peerDependencies": {
"react": ">=16.9.0",
Expand Down
25 changes: 21 additions & 4 deletions packages/graphin/src/layout/force/ForceLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Spring from './Spring';
import { getDegree } from '../utils/graph';
import { GraphinData as Data, IUserNode as NodeType } from '../../typings/type';
import { Item, Graph } from '@antv/g6/';
import { forceNBody } from './ForceNBody';

type ForceNodeType = Node;

Expand Down Expand Up @@ -39,9 +40,7 @@ export interface ForceProps {
/** 其他节点的施加力的因子 */
others?: number;
/** 向心力的中心点,默认为画布的中心 */
center?: (
node: NodeType,
) => {
center?: (node: NodeType) => {
x: number;
y: number;
};
Expand Down Expand Up @@ -406,14 +405,32 @@ class ForceLayout {
};

tick = (interval: number) => {
this.updateCoulombsLaw();
// this.updateCoulombsLaw();
this.updateCoulombsLawOptimized();
this.updateHookesLaw();
this.attractToCentre();
this.updateVelocity(interval);
this.updatePosition(interval);
};

/** 布局算法 */
updateCoulombsLawOptimized = () => {
// 用force-n-body结合 Barnes-Hut approximation 优化的方法
const { coulombDisScale } = this.props;
const { repulsion } = this.props;
const nodes = this.nodes.map(n => {
const point = this.nodePoints.get(n.id).p;
return {
x: point.x,
y: point.y,
};
});
const forces = forceNBody(nodes, coulombDisScale, repulsion);
this.nodes.forEach((node, i) => {
this.nodePoints.get(node.id).updateAcc(new Vector(forces[i].vx, forces[i].vy));
});
};

updateCoulombsLaw = () => {
const len = this.nodes.length;

Expand Down
120 changes: 120 additions & 0 deletions packages/graphin/src/layout/force/ForceNBody.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { quadtree } from 'd3-quadtree';

const theta2 = 0.81; // Barnes-Hut approximation threshold

interface Node {
x: number;
y: number;
}

interface InternalNode {
x: number;
y: number;
vx: number;
vy: number;
}

export function forceNBodyBruteForce(nodes: Node[], coulombDisScale: number, repulsion: number) {
return nodes.map((a, i) => {
const v = { vx: 0, vy: 0 };

nodes.forEach((b, j) => {
if (i === j) return;
const dx = a.x - b.x;
const dy = a.y - b.y;
const len = Math.sqrt(dx * dx + dy * dy);
const dis = len * coulombDisScale;
const force = repulsion / (dis * dis) || 0;

v.vx += (dx / len || 0) * force;
v.vy += (dy / len || 0) * force;
});

return v;
});
}

export function forceNBody(nodes: Node[], coulombDisScale: number, repulsion: number) {
const weight = repulsion / (coulombDisScale * coulombDisScale);
const data = nodes.map((n, i) => ({
index: i,
...n,
vx: 0,
vy: 0,
weight,
}));

const tree = quadtree(
data,
d => d.x,
d => d.y,
).visitAfter(accumulate); // init internal node

data.forEach(n => {
computeForce(n, tree);
});

return data.map(n => ({
vx: n.vx,
vy: n.vy,
}));
}

// @ts-ignore
function accumulate(quad) {
let accWeight = 0;
let accX = 0;
let accY = 0;

if (quad.length) {
// internal node, accumulate 4 child quads
for (let i = 0; i < 4; i++) {
const q = quad[i];
if (q && q.weight) {
accWeight += q.weight;
accX += q.x * q.weight;
accY += q.y * q.weight;
}
}
quad.x = accX / accWeight;
quad.y = accY / accWeight;
quad.weight = accWeight;
} else {
// leaf node
const q = quad;
quad.x = q.data.x;
quad.y = q.data.y;
quad.weight = q.data.weight;
}
}

// @ts-ignore
function computeForce(node: InternalNode, tree) {
// @ts-ignore
const apply = (quad, x1: number, y1: number, x2: number, y2: number) => {
const dx = node.x - quad.x;
const dy = node.y - quad.y;
const width = x2 - x1;
const len2 = dx * dx + dy * dy;
const len = Math.sqrt(len2);

// far node, apply Barnes-Hut approximation
if ((width * width) / theta2 < len2) {
node.vx += ((dx / len) * quad.weight) / len2;
node.vy += ((dy / len) * quad.weight) / len2;

return true;
}
// near quad, compute force directly
if (quad.length) return false; // internal node, visit children

// leaf node

if (quad.data !== node) {
node.vx += ((dx / len) * quad.data.weight) / len2;
node.vy += ((dy / len) * quad.data.weight) / len2;
}
};

tree.visit(apply);
}

0 comments on commit 56935ca

Please sign in to comment.