feat: force2 from graphin-force; feat: preset for layout; feat: tweak… (#3853)

* 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
This commit is contained in:
Yanyan Wang 2022-08-19 17:40:10 +08:00 committed by GitHub
parent 7b48f6827a
commit 1a52e3da05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 146 additions and 47 deletions

View File

@ -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;

View File

@ -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",

View File

@ -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',

View File

@ -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));

View File

@ -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);

View File

@ -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))) {

View File

@ -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": {

View File

@ -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",

View File

@ -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';

View File

@ -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",

View File

@ -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;

View File

@ -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',

View File

@ -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');
// 增量情况下(上一次的布局与当前布局一致),使用 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));
});

View File

@ -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);

View File

@ -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,
);
});
});

View File

@ -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",