mirror of
https://gitee.com/antv/g6.git
synced 2024-12-04 12:49:04 +08:00
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:
parent
7b48f6827a
commit
1a52e3da05
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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',
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
|
@ -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))) {
|
||||
|
@ -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": {
|
||||
|
@ -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",
|
||||
|
@ -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';
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -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));
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user