From 1a52e3da0596e5a5336eb722aefc512b0356e64b Mon Sep 17 00:00:00 2001 From: Yanyan Wang Date: Fri, 19 Aug 2022 17:40:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20force2=20from=20graphin-force;=20feat:?= =?UTF-8?q?=20preset=20for=20layout;=20feat:=20tweak=E2=80=A6=20(#3853)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: force2 from graphin-force; feat: preset for layout; feat: tweak incremental layout init for force like layouts; * chore: update tests * chore: upgrade version num * chore: onLayoutEnd with nodes param --- CHANGELOG.md | 6 ++ packages/core/package.json | 2 +- packages/core/src/global.ts | 2 +- packages/core/src/graph/controller/layout.ts | 21 ++++- packages/core/src/graph/controller/view.ts | 11 ++- packages/core/src/graph/graph.ts | 4 +- packages/element/package.json | 4 +- packages/g6/package.json | 4 +- packages/g6/src/index.ts | 4 +- packages/pc/package.json | 10 +- packages/pc/src/behavior/drag-combo.ts | 2 - packages/pc/src/global.ts | 2 +- packages/pc/src/graph/controller/layout.ts | 92 ++++++++++++++++--- packages/pc/src/layout/index.ts | 2 + .../unit/graph/updateLayout-align-spec.ts | 21 +++-- packages/plugin/package.json | 6 +- 16 files changed, 146 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9d2cf2265..d7ce7bc8d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # ChangeLog +#### 4.7.0-beta + +- feat: force2 from graphin-force; +- feat: preset for layout; +- feat: tweak incremental layout init for force like layouts; + #### 4.6.18 - feat: updateLayout from no pipes to pipes, closes: #3726; diff --git a/packages/core/package.json b/packages/core/package.json index c56c5e7ced..ad704800db 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g6-core", - "version": "0.6.17", + "version": "0.7.0-beta.2", "description": "A Graph Visualization Framework in JavaScript", "keywords": [ "antv", diff --git a/packages/core/src/global.ts b/packages/core/src/global.ts index 554726c19d..a9e60041d7 100644 --- a/packages/core/src/global.ts +++ b/packages/core/src/global.ts @@ -64,7 +64,7 @@ const colorSet = { }; export default { - version: '0.6.17', + version: '0.7.0-beta.1', rootContainerClassName: 'root-container', nodeContainerClassName: 'node-container', edgeContainerClassName: 'edge-container', diff --git a/packages/core/src/graph/controller/layout.ts b/packages/core/src/graph/controller/layout.ts index 5c43905ac1..5f5d20dba2 100644 --- a/packages/core/src/graph/controller/layout.ts +++ b/packages/core/src/graph/controller/layout.ts @@ -86,25 +86,30 @@ export default abstract class LayoutController { // 更换布局 public changeLayout(cfg) { - this.layoutCfg = cfg; - this.layoutType = cfg.type || this.layoutType; + const { disableTriggerLayout, ...otherCfgs } = cfg; + this.layoutCfg = otherCfgs; + this.layoutType = otherCfgs.type || this.layoutType; - this.destoryLayoutMethods(); + // 不触发重新布局,仅更新参数 + if (disableTriggerLayout) return; this.layout(); } // 更换数据 public changeData(success) { - this.destoryLayoutMethods(); this.layout(success); } - public destoryLayoutMethods() { + public destoryLayoutMethods(): string[] { const { layoutMethods } = this; + const destroyedLayoutTypes = []; layoutMethods?.forEach((layoutMethod) => { + const layoutType = layoutMethod.getType?.(); + if (layoutType) destroyedLayoutTypes.push(layoutType); layoutMethod.destroy(); }); this.layoutMethods = []; + return destroyedLayoutTypes; } // 销毁布局,不能使用 this.destroy,因为 controller 还需要被使用,只是把布局算法销毁 @@ -292,6 +297,8 @@ export default abstract class LayoutController { } } + public abstract initWithPreset(): boolean; + // 初始化节点到 center 附近 public initPositions(center, nodes): boolean { const { graph } = this; @@ -301,6 +308,10 @@ export default abstract class LayoutController { const nodeLength = nodes ? nodes.length : 0; if (!nodeLength) return; + const hasPreset = this.initWithPreset(); + + if (hasPreset) return false; + const width = graph.get('width') * 0.85; const height = graph.get('height') * 0.85; const horiNum = Math.ceil(Math.sqrt(nodeLength) * (width / height)); diff --git a/packages/core/src/graph/controller/view.ts b/packages/core/src/graph/controller/view.ts index cd98d1be03..df091c2a72 100644 --- a/packages/core/src/graph/controller/view.ts +++ b/packages/core/src/graph/controller/view.ts @@ -2,7 +2,7 @@ import { AbstractCanvas, BBox } from '@antv/g-base'; import { Point, IGroup } from '@antv/g-base'; import { isNumber, isString } from '@antv/util'; import { Item, Matrix, Padding, GraphAnimateConfig, IEdge, FitViewRules } from '../../types'; -import { formatPadding } from '../../util/base'; +import { formatPadding, isNaN } from '../../util/base'; import { applyMatrix, invertMatrix, lerpArray } from '../../util/math'; import { IAbstractGraph } from '../../interface/graph'; import { transform } from '@antv/matrix-util/lib/ext'; @@ -55,6 +55,7 @@ export default class ViewController { // Translate const vx = bbox.x + viewCenter.x - groupCenter.x - bbox.minX; const vy = bbox.y + viewCenter.y - groupCenter.y - bbox.minY; + if (isNaN(vx) || isNaN(vy)) return; const translatedMatrix = transform(matrix, [['t', vx, vy]]); // Zoom @@ -79,10 +80,13 @@ export default class ViewController { const animationConfig = getAnimateCfgWithCallback({ animateCfg, callback: () => { + group.setMatrix(zoomedMatrix); graph.emit('viewportchange', { action: 'translate', matrix: translatedMatrix }); graph.emit('viewportchange', { action: 'zoom', matrix: zoomedMatrix }); } }); + group.stopAnimate(); + group.setMatrix(startMatrix); group.animate((ratio: number) => { return { matrix: lerpArray(startMatrix, zoomedMatrix, ratio) }; }, animationConfig); @@ -118,7 +122,10 @@ export default class ViewController { if (animate) { this.animatedFitView(group, startMatrix, animateCfg, bbox, viewCenter, groupCenter, ratio); } else { - graph.translate(viewCenter.x - groupCenter.x, viewCenter.y - groupCenter.y); + const dx = viewCenter.x - groupCenter.x; + const dy = viewCenter.y - groupCenter.y; + if (isNaN(dx) || isNaN(dy)) return; + graph.translate(dx, dy); if (!graph.zoom(ratio, viewCenter)) { console.warn('zoom failed, ratio out of range, ratio: %f', ratio); diff --git a/packages/core/src/graph/graph.ts b/packages/core/src/graph/graph.ts index 2506e502fa..93d928a1cd 100644 --- a/packages/core/src/graph/graph.ts +++ b/packages/core/src/graph/graph.ts @@ -2439,7 +2439,7 @@ export default abstract class AbstractGraph extends EventEmitter implements IAbs * 若 cfg 含有 type 字段或为 String 类型,且与现有布局方法不同,则更换布局 * 若 cfg 不包括 type ,则保持原有布局方法,仅更新布局配置项 */ - public updateLayout(cfg: any, align?: 'center' | 'begin', alignPoint?: IPoint, stack: boolean = true): void { + public updateLayout(cfg: any = {}, align?: 'center' | 'begin', alignPoint?: IPoint, stack: boolean = true): void { const layoutController = this.get('layoutController'); if (isString(cfg)) { @@ -2458,7 +2458,7 @@ export default abstract class AbstractGraph extends EventEmitter implements IAbs // translate to point coordinate system toPoint = this.getPointByCanvas(toPoint.x, toPoint.y); - const forceTypes = ['force', 'gForce', 'fruchterman']; + const forceTypes = ['force', 'gForce', 'fruchterman', 'force2']; // if it is force layout, only center takes effect, and assign center force if (forceTypes.includes(cfg.type) || (!cfg.type && forceTypes.includes(layoutController?.layoutType))) { diff --git a/packages/element/package.json b/packages/element/package.json index 5eb816b04f..8149074fe9 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g6-element", - "version": "0.6.17", + "version": "0.7.0-beta.2", "description": "A Graph Visualization Framework in JavaScript", "keywords": [ "antv", @@ -61,7 +61,7 @@ }, "dependencies": { "@antv/g-base": "^0.5.1", - "@antv/g6-core": "0.6.17", + "@antv/g6-core": "0.7.0-beta.2", "@antv/util": "~2.0.5" }, "devDependencies": { diff --git a/packages/g6/package.json b/packages/g6/package.json index 6c985b9ba3..d1c1a0761f 100644 --- a/packages/g6/package.json +++ b/packages/g6/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g6", - "version": "4.6.17", + "version": "4.7.0-beta.2", "description": "A Graph Visualization Framework in JavaScript", "keywords": [ "antv", @@ -66,7 +66,7 @@ ] }, "dependencies": { - "@antv/g6-pc": "0.6.17" + "@antv/g6-pc": "0.7.0-beta.2" }, "devDependencies": { "@babel/core": "^7.7.7", diff --git a/packages/g6/src/index.ts b/packages/g6/src/index.ts index 258852b2b6..4cdd506061 100644 --- a/packages/g6/src/index.ts +++ b/packages/g6/src/index.ts @@ -1,7 +1,7 @@ import G6 from '@antv/g6-pc'; -G6.version = '4.6.17'; +G6.version = '4.7.0-beta.2'; export * from '@antv/g6-pc'; export default G6; -export const version = '4.6.17'; +export const version = '4.7.0-beta.2'; diff --git a/packages/pc/package.json b/packages/pc/package.json index 8368dccfcf..9736ad4bf0 100644 --- a/packages/pc/package.json +++ b/packages/pc/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g6-pc", - "version": "0.6.17", + "version": "0.7.0-beta.2", "description": "A Graph Visualization Framework in JavaScript", "keywords": [ "antv", @@ -75,11 +75,11 @@ "@antv/g-canvas": "^0.5.2", "@antv/g-math": "^0.1.1", "@antv/g-svg": "^0.5.1", - "@antv/g6-core": "0.6.17", - "@antv/g6-element": "0.6.17", - "@antv/g6-plugin": "0.6.17", + "@antv/g6-core": "0.7.0-beta.2", + "@antv/g6-element": "0.7.0-beta.2", + "@antv/g6-plugin": "0.7.0-beta.2", "@antv/hierarchy": "^0.6.7", - "@antv/layout": "^0.2.5", + "@antv/layout": "^0.3.0-beta.1", "@antv/matrix-util": "^3.1.0-beta.3", "@antv/path-util": "^2.0.3", "@antv/util": "~2.0.5", diff --git a/packages/pc/src/behavior/drag-combo.ts b/packages/pc/src/behavior/drag-combo.ts index 8551a78511..06d37948a1 100644 --- a/packages/pc/src/behavior/drag-combo.ts +++ b/packages/pc/src/behavior/drag-combo.ts @@ -198,7 +198,6 @@ export default { updatePositions(evt: IG6GraphEvent, restore: boolean) { // 当启用 delegate 时,拖动结束时需要更新 combo if (this.enableDelegate || restore) { - console.log('updatePositions', this.targets); each(this.targets, (item) => { this.updateCombo(item, evt, restore); }); @@ -424,7 +423,6 @@ export default { let x: number = evt.x - origin.x + this.point[itemId].x; let y: number = evt.y - origin.y + this.point[itemId].y; - console.log('restore', restore); if (restore) { x += origin.x - evt.x; y += origin.y - evt.y; diff --git a/packages/pc/src/global.ts b/packages/pc/src/global.ts index c888b2f493..0aa792c773 100644 --- a/packages/pc/src/global.ts +++ b/packages/pc/src/global.ts @@ -7,7 +7,7 @@ const textColor = 'rgb(0, 0, 0)'; const colorSet = getColorsWithSubjectColor(subjectColor, backColor); export default { - version: '0.6.17', + version: '0.7.0-beta.2', rootContainerClassName: 'root-container', nodeContainerClassName: 'node-container', edgeContainerClassName: 'edge-container', diff --git a/packages/pc/src/graph/controller/layout.ts b/packages/pc/src/graph/controller/layout.ts index 4d0f47d81e..2c2805a810 100644 --- a/packages/pc/src/graph/controller/layout.ts +++ b/packages/pc/src/graph/controller/layout.ts @@ -129,14 +129,14 @@ export default class LayoutController extends AbstractLayout { } } - const isForce = layoutType === 'force' || layoutType === 'g6force' || layoutType === 'gForce'; + const isForce = layoutType === 'force' || layoutType === 'g6force' || layoutType === 'gForce' || layoutType === 'force2'; if (isForce) { - const { onTick } = layoutCfg; + const { onTick, animate } = layoutCfg; const tick = () => { if (onTick) { onTick(); } - graph.refreshPositions(); + if (animate) graph.refreshPositions(); }; layoutCfg.tick = tick; } else if (layoutType === 'comboForce' || layoutType === 'comboCombined') { @@ -148,6 +148,10 @@ export default class LayoutController extends AbstractLayout { try { layoutMethod = new Layout[layoutType](layoutCfg); + if (this.layoutMethods[order]) { + this.layoutMethods[order].destroy() + } + this.layoutMethods[order] = layoutMethod; } catch (e) { console.warn(`The layout method: '${layoutType}' does not exist! Please specify it first.`); reject(); @@ -173,7 +177,6 @@ export default class LayoutController extends AbstractLayout { graph.emit('beforesublayout', { type: layoutType }); await layoutMethod.execute(); if (layoutMethod.isCustomLayout && layoutCfg.onLayoutEnd) layoutCfg.onLayoutEnd(); - this.layoutMethods[order] = layoutMethod; }); } @@ -222,16 +225,23 @@ export default class LayoutController extends AbstractLayout { this.layoutCfg, ); this.layoutCfg = layoutCfg; + let layoutType = layoutCfg.type; - this.destoryLayoutMethods(); + const preLayoutTypes = this.destoryLayoutMethods(); graph.emit('beforelayout'); - this.initPositions(layoutCfg.center, nodes); + + // 增量情况下(上一次的布局与当前布局一致),使用 treakInit + if (preLayoutTypes?.length && layoutType && preLayoutTypes?.length === 1 && preLayoutTypes[0] === layoutType) { + this.tweakInit(); + } else { + // 初始化位置,若配置了 preset,则使用 preset 的参数生成布局作为预布局,否则使用 grid + this.initPositions(layoutCfg.center, nodes); + } // init hidden nodes this.initPositions(layoutCfg.center, hiddenNodes); // 防止用户直接用 -gpu 结尾指定布局 - let layoutType = layoutCfg.type; if (layoutType && layoutType.split('-')[1] === 'gpu') { layoutType = layoutType.split('-')[0]; layoutCfg.gpuEnabled = true; @@ -258,7 +268,7 @@ export default class LayoutController extends AbstractLayout { layoutCfg.onAllLayoutEnd = async () => { // 执行用户自定义 onLayoutEnd if (onLayoutEnd) { - onLayoutEnd(); + onLayoutEnd(nodes); } // 更新节点位置 @@ -312,6 +322,57 @@ export default class LayoutController extends AbstractLayout { return false; } + /** + * 增量数据初始化位置 + */ + public tweakInit() { + const { data, graph } = this; + const { nodes, edges } = data; + if (!nodes?.length) return; + const positionMap = {}; + nodes.forEach(node => { + const { x, y } = node; + if (!isNaN(x) && !isNaN(y)) { + positionMap[node.id] = { x, y }; + // 有位置信息,则是原有节点,增加 mass + node.mass = node.mass || 2; + } + }); + edges.forEach(edge => { + const { source, target } = edge; + const sourcePosition = positionMap[source] + const targetPosition = positionMap[target] + if (!sourcePosition && targetPosition) { + positionMap[source] = { + x: targetPosition.x + (Math.random() - 0.5) * 80, + y: targetPosition.y + (Math.random() - 0.5) * 80 + } + } else if (!targetPosition && sourcePosition) { + positionMap[target] = { + x: sourcePosition.x + (Math.random() - 0.5) * 80, + y: sourcePosition.y + (Math.random() - 0.5) * 80 + } + } + }); + const width = graph.get('width'); + const height = graph.get('height'); + nodes.forEach(node => { + const position = positionMap[node.id] || { x: width / 2 + (Math.random() - 0.5) * 20, y: height / 2 + (Math.random() - 0.5) * 20 }; + node.x = position.x; + node.y = position.y; + }) + } + + public initWithPreset(): boolean { + const { layoutCfg, data } = this; + const { preset } = layoutCfg; + if (!preset?.type || !Layout[preset?.type]) return false; + const presetLayout = new Layout[preset?.type](preset); + presetLayout.layout(data); + delete layoutCfg.preset; + return true; + } + /** * layout with web worker * @param {object} data graph data @@ -475,9 +536,16 @@ export default class LayoutController extends AbstractLayout { // 更新布局参数 public updateLayoutCfg(cfg) { const { graph, layoutMethods } = this; - const layoutCfg = mix({}, this.layoutCfg, cfg); + // disableTriggerLayout 不触发重新布局,仅更新参数 + const { disableTriggerLayout, ...otherCfg } = cfg; + const layoutCfg = mix({}, this.layoutCfg, otherCfg); this.layoutCfg = layoutCfg; + // disableTriggerLayout 不触发重新布局,仅更新参数 + if (disableTriggerLayout) { + return; + } + if (!layoutMethods?.length) { this.layout(); return; @@ -485,7 +553,7 @@ export default class LayoutController extends AbstractLayout { this.data = this.setDataFromGraph(); this.stopWorker(); - if (cfg.workerEnabled && this.layoutWithWorker(this.data)) { + if (otherCfg.workerEnabled && this.layoutWithWorker(this.data)) { // 如果启用布局web worker并且浏览器支持web worker,用web worker布局。否则回退到不用web worker布局。 return; } @@ -497,9 +565,9 @@ export default class LayoutController extends AbstractLayout { if (layoutMethods?.length === 1) { hasLayout = true; start = start.then(async () => await this.updateLayoutMethod(layoutMethods[0], layoutCfg)); - } else { + } else if (layoutMethods?.length) { hasLayout = true; - layoutMethods?.forEach((layoutMethod, index) => { + layoutMethods.forEach((layoutMethod, index) => { const currentCfg = layoutCfg.pipes[index]; start = start.then(async () => await this.updateLayoutMethod(layoutMethod, currentCfg)); }); diff --git a/packages/pc/src/layout/index.ts b/packages/pc/src/layout/index.ts index 3ed2712260..7e74f19941 100644 --- a/packages/pc/src/layout/index.ts +++ b/packages/pc/src/layout/index.ts @@ -13,6 +13,7 @@ import { FruchtermanGPULayout, FruchtermanLayout, GForceLayout, + Force2Layout, GForceGPULayout, ComboForceLayout, ComboCombinedLayout, @@ -33,6 +34,7 @@ oRegisterLayout('mds', MDSLayout); oRegisterLayout('fruchterman', FruchtermanLayout); oRegisterLayout('fruchterman-gpu', FruchtermanGPULayout); oRegisterLayout('gForce', GForceLayout); +oRegisterLayout('force2', Force2Layout); oRegisterLayout('gForce-gpu', GForceGPULayout); oRegisterLayout('comboForce', ComboForceLayout); oRegisterLayout('comboCombined', ComboCombinedLayout); diff --git a/packages/pc/tests/unit/graph/updateLayout-align-spec.ts b/packages/pc/tests/unit/graph/updateLayout-align-spec.ts index a501539022..888121feb5 100644 --- a/packages/pc/tests/unit/graph/updateLayout-align-spec.ts +++ b/packages/pc/tests/unit/graph/updateLayout-align-spec.ts @@ -82,19 +82,26 @@ describe('graph', () => { ); }); it('force align center', (done) => { - setTimeout(() => { - const bbox = graph.getGroup().getCanvasBBox(); - console.log('bbox', bbox.x, bbox.y); - expect(Math.abs(bbox.x - 55.352659713113454) < 10).toBe(true); - expect(Math.abs(bbox.y - 163.01955290709174) < 10).toBe(true); + const canvasPoint = { x: 100, y: 200 } + const point = graph.getPointByCanvas(canvasPoint.x, canvasPoint.y) + graph.once('afterlayout', () => { + const meanCenter = { x: 0, y: 0 }; + graph.getNodes().forEach(node => { + meanCenter.x += node.getModel().x; + meanCenter.y += node.getModel().y; + }); + meanCenter.x /= graph.getNodes().length; + meanCenter.y /= graph.getNodes().length; + expect(Math.abs(meanCenter.x - point.x) < 10).toBe(true); + expect(Math.abs(meanCenter.y - point.y) < 10).toBe(true); done(); - }, 2000); + }) graph.updateLayout( { type: 'force', }, 'center', - { x: 100, y: 200 }, + canvasPoint, ); }); }); diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 8e50391193..82d20d08ea 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g6-plugin", - "version": "0.6.17", + "version": "0.7.0-beta.2", "description": "G6 Plugin", "main": "lib/index.js", "module": "es/index.js", @@ -22,8 +22,8 @@ "@antv/g-base": "^0.5.1", "@antv/g-canvas": "^0.5.2", "@antv/g-svg": "^0.5.2", - "@antv/g6-core": "0.6.17", - "@antv/g6-element": "0.6.17", + "@antv/g6-core": "0.7.0-beta.2", + "@antv/g6-element": "0.7.0-beta.2", "@antv/matrix-util": "^3.1.0-beta.3", "@antv/scale": "^0.3.4", "@antv/util": "^2.0.9",