diff --git a/packages/graphin-components/package.json b/packages/graphin-components/package.json index f486f7144..0ede16d52 100644 --- a/packages/graphin-components/package.json +++ b/packages/graphin-components/package.json @@ -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", diff --git a/packages/graphin-components/src/index.ts b/packages/graphin-components/src/index.ts index bd72bf41f..48d232b26 100644 --- a/packages/graphin-components/src/index.ts +++ b/packages/graphin-components/src/index.ts @@ -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'; diff --git a/packages/graphin/package.json b/packages/graphin/package.json index 1bad4d201..f75efcf4a 100644 --- a/packages/graphin/package.json +++ b/packages/graphin/package.json @@ -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", @@ -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" @@ -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", diff --git a/packages/graphin/src/layout/force/ForceLayout.ts b/packages/graphin/src/layout/force/ForceLayout.ts index a286e6c81..ad49da831 100644 --- a/packages/graphin/src/layout/force/ForceLayout.ts +++ b/packages/graphin/src/layout/force/ForceLayout.ts @@ -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; @@ -39,9 +40,7 @@ export interface ForceProps { /** 其他节点的施加力的因子 */ others?: number; /** 向心力的中心点,默认为画布的中心 */ - center?: ( - node: NodeType, - ) => { + center?: (node: NodeType) => { x: number; y: number; }; @@ -406,7 +405,8 @@ class ForceLayout { }; tick = (interval: number) => { - this.updateCoulombsLaw(); + // this.updateCoulombsLaw(); + this.updateCoulombsLawOptimized(); this.updateHookesLaw(); this.attractToCentre(); this.updateVelocity(interval); @@ -414,6 +414,23 @@ class ForceLayout { }; /** 布局算法 */ + 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; diff --git a/packages/graphin/src/layout/force/ForceNBody.ts b/packages/graphin/src/layout/force/ForceNBody.ts new file mode 100644 index 000000000..4d7fdc48f --- /dev/null +++ b/packages/graphin/src/layout/force/ForceNBody.ts @@ -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); +}