fix: custom layout warning and layout failed problem; (#3758)

* fix: custom layout warning and layout failed problem; fix: upgrade layout to fix DagreLayoutOptions type error; fix: upgrade layout to fix comboCombined with original node infomations problem;

* chore: tests refine

* Truncate node labels (#3690)

* Added maxLength property to LabelStyle

* Added unit test for label max length

* Added truncateLabelByLength util function

* Added safety checks for negative and non-number maxLength values

* Moved maxLength prop to labelCfg

* feat: maxLength for labelCfg;

* chore: upgrade version nums

Co-authored-by: Tony <51513780+TonyN96@users.noreply.github.com>
This commit is contained in:
Yanyan Wang 2022-06-28 14:44:14 +08:00 committed by GitHub
parent 778720cc89
commit 4512b10bcb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 598 additions and 43 deletions

View File

@ -1,5 +1,12 @@
# ChangeLog
#### 4.6.10
- feat: maxLength for labelCfg;
- fix: custom layout warning and layout failed problem;
- fix: upgrade layout to fix DagreLayoutOptions type error;
- fix: upgrade layout to fix comboCombined with original node infomations problem;
#### 4.6.8
- fix: spelling error for 'nodeselectChange', closes: #3736;

View File

@ -1,6 +1,6 @@
{
"name": "@antv/g6-core",
"version": "0.6.8",
"version": "0.6.10",
"description": "A Graph Visualization Framework in JavaScript",
"keywords": [
"antv",

View File

@ -10,6 +10,7 @@ import { formatPadding } from '../util/base';
import Global from '../global';
import Shape from './shape';
import { shapeBase } from './shapeBase';
import { truncateLabelByLength } from '../util/graphic';
const singleNode: ShapeOptions = {
itemType: 'node',
@ -48,11 +49,19 @@ const singleNode: ShapeOptions = {
},
// 私有方法,不希望扩展的节点复写这个方法
getLabelStyleByPosition(cfg: NodeConfig, labelCfg: ILabelConfig): LabelStyle {
const labelMaxLength = labelCfg.maxLength;
let text = cfg!.label as string;
if (labelMaxLength) {
text = truncateLabelByLength(text, labelMaxLength);
}
const labelPosition = labelCfg.position || this.labelPosition;
// 默认的位置(最可能的情形),所以放在最上面
if (labelPosition === 'center') {
return { x: 0, y: 0, text: cfg!.label as string, textBaseline: 'middle', textAlign: 'center' };
return { x: 0, y: 0, text, textBaseline: 'middle', textAlign: 'center' };
}
let { offset } = labelCfg;
@ -98,7 +107,7 @@ const singleNode: ShapeOptions = {
};
break;
}
style.text = cfg.label;
style.text = text;
return style;
},
getLabelBgStyleByPosition(

View File

@ -64,7 +64,7 @@ const colorSet = {
};
export default {
version: '0.6.8',
version: '0.6.10',
rootContainerClassName: 'root-container',
nodeContainerClassName: 'node-container',
edgeContainerClassName: 'edge-container',

View File

@ -211,7 +211,7 @@ export default abstract class LayoutController {
let start = Promise.resolve();
layoutMethods?.forEach((layoutMethod: any, index: number) => {
const currentCfg = layoutCfg[index];
const currentCfg = layoutCfg[index] || layoutCfg;
start = start.then(() => this.reLayoutMethod(layoutMethod, currentCfg));
});

View File

@ -21,6 +21,7 @@ import {
IG6GraphEvent,
IPoint,
FitViewRules,
G6Event
} from '../types';
import { IEdge, INode, ICombo } from './item';
import Hull from '../item/hull';
@ -625,12 +626,12 @@ export interface IAbstractGraph extends EventEmitter {
/**
*
*/
on: <T = IG6GraphEvent>(eventName: string, callback: (e: T) => void, once?: boolean) => this;
on: <T = IG6GraphEvent>(eventName: G6Event, callback: (e: T) => void, once?: boolean) => this;
/**
*
*/
off: <T = IG6GraphEvent>(eventName: string, callback: (e: T) => void, once?: boolean) => this;
off: <T = IG6GraphEvent>(eventName: G6Event, callback: (e: T) => void, once?: boolean) => this;
/**

View File

@ -8,6 +8,7 @@ export type ILabelConfig = Partial<{
refY: number;
autoRotate: boolean;
style: LabelStyle;
maxLength?: number;
}>;
export type ShapeDefine = string | ((cfg: ModelConfig) => string);

View File

@ -185,7 +185,7 @@ export type MobileInteractionEventType = MobileInteractionEvent;
* @example
* https://g6.antv.vision/en/docs/api/Event#combo-interaction-event
*/
export type G6Event = NodeEventType | EdgeEventType | ComboEventType | CanvasEventType | GraphTimingEventType | MobileInteractionEventType | CommonInteractionEvent | CommonInteractionEvent;
export type G6Event = NodeEventType | EdgeEventType | ComboEventType | CanvasEventType | GraphTimingEventType | MobileInteractionEventType | CommonInteractionEvent | CommonInteractionEvent | (string & {});
export interface IG6GraphEvent extends GraphEvent {
item: Item | null;

View File

@ -375,6 +375,13 @@ export const getTextSize = (text: string, fontSize: number) => {
return [width, fontSize];
};
export const truncateLabelByLength = (text: string, length: number) => {
if (typeof length !== 'number' || length <= 0 || length >= text.length) {
return text;
}
return text.substring(0, length) + '...';
}
/**
* construct the trees from combos data
* @param array the combos array

View File

@ -1,4 +1,4 @@
import { clone, groupBy } from '@antv/util';
import { clone, groupBy, isNumberEqual } from '@antv/util';
import Graph from '../implement-graph';
const div = document.createElement('div');
@ -477,11 +477,11 @@ describe('hierarchy data 1: combo A has one child: an empty combo B', () => {
comboB = graph.findById('B');
comboBModel = comboB.getModel();
expect(comboBModel.x).toBe(100);
expect(comboBModel.y).toBe(200);
expect(isNumberEqual(comboBModel.x, 100, 1)).toBe(true);
expect(isNumberEqual(comboBModel.y, 200, 1)).toBe(true);
// A has no position, follows the child
expect(comboAModel.x).toBe(100);
expect(comboAModel.y).toBe(200);
expect(isNumberEqual(comboAModel.x, 100, 1)).toBe(true);
expect(isNumberEqual(comboAModel.y, 200, 1)).toBe(true);
});
it('parent combo collapsed with pos, child combo with pos', (done) => {
const testData = clone(data);

View File

@ -247,6 +247,27 @@ describe('shape node test', () => {
expect(shape.attr('lineWidth')).toBe(1);
});
it('max label length', () => {
const mockLabel = 'This is a test label';
const group = canvas.addGroup();
factory.draw(
'simple-circle',
{
size: 20,
color: 'blue',
label: mockLabel,
labelCfg: {
maxLength: 5,
},
},
group,
);
canvas.draw();
const label = group.get('children')[1];
expect(label.attr('text')).toBe(mockLabel.substring(0, 5) + '...');
});
it('clear', () => {
canvas.destroy();
});

View File

@ -1,4 +1,4 @@
import { traverseTree, traverseTreeUp, getTextSize } from '../../../src/util/graphic';
import { traverseTree, traverseTreeUp, getTextSize, truncateLabelByLength } from '../../../src/util/graphic';
interface TreeNode {
id: string;
@ -89,4 +89,22 @@ describe('graphic unit test', () => {
result = getTextSize('体验技术', 14);
expect(result).toEqual([56, 14]);
});
it('truncateLabelByLength' ,() => {
const label = 'This is a test label';
let length = 5;
let result = truncateLabelByLength(label, length);
expect(result).toEqual(label.substring(0, 5) + '...');
length = 100;
result = truncateLabelByLength(label, length);
expect(result).toEqual(label);
length = -1;
result = truncateLabelByLength(label, length);
expect(result).toEqual(label);
});
});

View File

@ -1,6 +1,6 @@
{
"name": "@antv/g6-element",
"version": "0.6.8",
"version": "0.6.10",
"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.8",
"@antv/g6-core": "0.6.10",
"@antv/util": "~2.0.5"
},
"devDependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "@antv/g6",
"version": "4.6.8",
"version": "4.6.10",
"description": "A Graph Visualization Framework in JavaScript",
"keywords": [
"antv",
@ -66,7 +66,7 @@
]
},
"dependencies": {
"@antv/g6-pc": "0.6.8"
"@antv/g6-pc": "0.6.10"
},
"devDependencies": {
"@babel/core": "^7.7.7",

View File

@ -1,7 +1,7 @@
import G6 from '@antv/g6-pc';
G6.version = '4.6.8';
G6.version = '4.6.10';
export * from '@antv/g6-pc';
export default G6;
export const version = '4.6.8';
export const version = '4.6.10';

View File

@ -1,6 +1,6 @@
{
"name": "@antv/g6-pc",
"version": "0.6.8",
"version": "0.6.10",
"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.8",
"@antv/g6-element": "0.6.8",
"@antv/g6-plugin": "0.6.8",
"@antv/g6-core": "0.6.10",
"@antv/g6-element": "0.6.10",
"@antv/g6-plugin": "0.6.10",
"@antv/hierarchy": "^0.6.7",
"@antv/layout": "^0.2.1",
"@antv/layout": "^0.2.5",
"@antv/matrix-util": "^3.1.0-beta.3",
"@antv/path-util": "^2.0.3",
"@antv/util": "~2.0.5",

View File

@ -7,7 +7,7 @@ const textColor = 'rgb(0, 0, 0)';
const colorSet = getColorsWithSubjectColor(subjectColor, backColor);
export default {
version: '0.6.8',
version: '0.6.10',
rootContainerClassName: 'root-container',
nodeContainerClassName: 'node-container',
edgeContainerClassName: 'edge-container',

View File

@ -136,12 +136,12 @@ describe('text background label', () => {
}, 30);
}, 100)
});
it('text background with autoRotate false and clearItemStates', () => {
it('text background with autoRotate false and clearItemStates', (done) => {
let edge = graph.getEdges()[0];
let labelBgShape = edge.getContainer().get('children')[1];
let { x, y } = labelBgShape.attr();
expect(labelBgShape.attr().x).toBe(176.85302734375);
expect(labelBgShape.attr().y).toBe(116);
expect(x).toBe(176.85302734375);
expect(y).toBe(116);
graph.updateItem(graph.getNodes()[0], {
x: graph.getNodes()[0].getModel().x + 100,
@ -150,8 +150,11 @@ describe('text background label', () => {
graph.clearItemStates(edge, ['active']);
labelBgShape = edge.getContainer().get('children')[1];
const { x: newX, y: newY } = labelBgShape.attr();
expect(numberEqual(newX, 226, 2)).toBe(true);
expect(numberEqual(newY, 166, 2)).toBe(true);
setTimeout(() => {
const { x: newX, y: newY } = labelBgShape.attr();
expect(numberEqual(newX, 226, 2)).toBe(true);
expect(numberEqual(newY, 166, 2)).toBe(true);
done()
}, 16);
});
});

View File

@ -1,6 +1,6 @@
{
"name": "@antv/g6-plugin",
"version": "0.6.8",
"version": "0.6.10",
"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.8",
"@antv/g6-element": "0.6.8",
"@antv/g6-core": "0.6.10",
"@antv/g6-element": "0.6.10",
"@antv/matrix-util": "^3.1.0-beta.3",
"@antv/scale": "^0.3.4",
"@antv/util": "^2.0.9",

View File

@ -2,4 +2,6 @@
title: API
---
`markdown:docs/api/graphLayout/comboCombined.en.md`
`markdown:docs/api/graphLayout/comboForce.en.md`

View File

@ -2,4 +2,6 @@
title: API
---
`markdown:docs/api/graphLayout/comboCombined.zh.md`
`markdown:docs/api/graphLayout/comboForce.zh.md`

View File

@ -0,0 +1,472 @@
import G6 from '@antv/g6';
const data = {
nodes: [
{
id: '0',
comboId: 'a',
},
{
id: '1',
comboId: 'a',
},
{
id: '2',
comboId: 'a',
},
{
id: '3',
comboId: 'a',
},
{
id: '4',
comboId: 'a',
},
{
id: '5',
comboId: 'a',
},
{
id: '6',
comboId: 'a',
},
{
id: '7',
comboId: 'a',
},
{
id: '8',
comboId: 'a',
},
{
id: '9',
comboId: 'a',
},
{
id: '10',
comboId: 'a',
},
{
id: '11',
comboId: 'a',
},
{
id: '12',
comboId: 'a',
},
{
id: '13',
comboId: 'a',
},
{
id: '14',
comboId: 'a',
},
{
id: '15',
comboId: 'a',
},
{
id: '16',
comboId: 'b',
},
{
id: '17',
comboId: 'b',
},
{
id: '18',
comboId: 'b',
},
{
id: '19',
comboId: 'b',
},
{
id: '20',
},
{
id: '21',
},
{
id: '22',
},
{
id: '23',
comboId: 'c',
},
{
id: '24',
comboId: 'a',
},
{
id: '25',
},
{
id: '26',
},
{
id: '27',
comboId: 'c',
},
{
id: '28',
comboId: 'c',
},
{
id: '29',
comboId: 'c',
},
{
id: '30',
comboId: 'c',
},
{
id: '31',
comboId: 'c',
},
{
id: '32',
comboId: 'd',
},
{
id: '33',
comboId: 'd',
},
],
edges: [
{
source: 'a',
target: 'b',
label: 'Combo A - Combo B',
size: 3,
labelCfg: {
autoRotate: true,
style: {
stroke: '#fff',
lineWidth: 5,
fontSize: 20,
},
},
style: {
stroke: 'red',
},
},
{
source: 'a',
target: '33',
label: 'Combo-Node',
size: 3,
labelCfg: {
autoRotate: true,
style: {
stroke: '#fff',
lineWidth: 5,
fontSize: 20,
},
},
style: {
stroke: 'blue',
},
},
{
source: '0',
target: '1',
},
{
source: '0',
target: '2',
},
{
source: '0',
target: '3',
},
{
source: '0',
target: '4',
},
{
source: '0',
target: '5',
},
{
source: '0',
target: '7',
},
{
source: '0',
target: '8',
},
{
source: '0',
target: '9',
},
{
source: '0',
target: '10',
},
{
source: '0',
target: '11',
},
{
source: '0',
target: '13',
},
{
source: '0',
target: '14',
},
{
source: '0',
target: '15',
},
{
source: '0',
target: '16',
},
{
source: '2',
target: '3',
},
{
source: '4',
target: '5',
},
{
source: '4',
target: '6',
},
{
source: '5',
target: '6',
},
{
source: '7',
target: '13',
},
{
source: '8',
target: '14',
},
{
source: '9',
target: '10',
},
{
source: '10',
target: '22',
},
{
source: '10',
target: '14',
},
{
source: '10',
target: '12',
},
{
source: '10',
target: '24',
},
{
source: '10',
target: '21',
},
{
source: '10',
target: '20',
},
{
source: '11',
target: '24',
},
{
source: '11',
target: '22',
},
{
source: '11',
target: '14',
},
{
source: '12',
target: '13',
},
{
source: '16',
target: '17',
},
{
source: '16',
target: '18',
},
{
source: '16',
target: '21',
},
{
source: '16',
target: '22',
},
{
source: '17',
target: '18',
},
{
source: '17',
target: '20',
},
{
source: '18',
target: '19',
},
{
source: '19',
target: '20',
},
{
source: '19',
target: '33',
},
{
source: '19',
target: '22',
},
{
source: '19',
target: '23',
},
{
source: '20',
target: '21',
},
{
source: '21',
target: '22',
},
{
source: '22',
target: '24',
},
{
source: '22',
target: '25',
},
{
source: '22',
target: '26',
},
{
source: '22',
target: '23',
},
{
source: '22',
target: '28',
},
{
source: '22',
target: '30',
},
{
source: '22',
target: '31',
},
{
source: '22',
target: '32',
},
{
source: '22',
target: '33',
},
{
source: '23',
target: '28',
},
{
source: '23',
target: '27',
},
{
source: '23',
target: '29',
},
{
source: '23',
target: '30',
},
{
source: '23',
target: '31',
},
{
source: '23',
target: '33',
},
{
source: '32',
target: '33',
},
],
combos: [
{
id: 'a',
label: 'Combo A',
},
{
id: 'b',
label: 'Combo B',
},
{
id: 'c',
label: 'Combo D',
},
{
id: 'd',
label: 'Combo D',
parentId: 'b',
},
],
};
const container = document.getElementById('container');
const width = container.scrollWidth;
const height = container.scrollHeight || 500;
const graph = new G6.Graph({
container: 'container',
width,
height,
fitView: true,
fitViewPadding: 50,
minZoom: 0.00000001,
layout: {
type: 'comboCombined',
},
defaultNode: {
size: 15,
color: '#5B8FF9',
style: {
lineWidth: 2,
fill: '#C6E5FF',
},
},
defaultEdge: {
size: 2,
color: '#e2e2e2',
},
modes: {
default: ['drag-combo', 'drag-node', 'drag-canvas', 'zoom-canvas', 'collapse-expand-combo'],
},
});
graph.data(data);
graph.render();
if (typeof window !== 'undefined')
window.onresize = () => {
if (!graph || graph.get('destroyed')) return;
if (!container || !container.scrollWidth || !container.scrollHeight) return;
graph.changeSize(container.scrollWidth, container.scrollHeight);
};

View File

@ -4,6 +4,14 @@
"en": "Category"
},
"demos": [
{
"filename": "comboCombined.js",
"title": {
"zh": "ComboCombined 力导向布局",
"en": "Combo Combined Layout"
},
"screenshot": "https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*GK34T7F1_CYAAAAAAAAAAABkARQnAQ"
},
{
"filename": "basicComboForce.js",
"title": {

View File

@ -1,10 +1,12 @@
---
title: Combo Force Layout
title: Combo Related Layout
order: 10
---
_It is a new feature of V3.5._ Combo Force is designed for the graph with combos based on classical force directed layout algorith. It modifies the forces between nodes according to their combo infomation to achieve a final result with clustering nodes inside each combo and no overlappings.
_New feature of V4.6_ Designed for graph with combos. Support configuring the layout for items inside a combo and the layout for the outer combos and nodes.
_New feature of V3.5._ Combo Force is designed for the graph with combos based on classical force directed layout algorith. It modifies the forces between nodes according to their combo infomation to achieve a final result with clustering nodes inside each combo and no overlappings.
## Usage
We suggest to use `'comboForce'` for the graph with combos. Other layouts in G6 are also availabel, but they do not consider the combo infomation.
We suggest to use `'comboCombined'` or `'comboForce'` for the graph with combos. Other layouts in G6 are also availabel, but they do not consider the combo infomation.

View File

@ -1,10 +1,12 @@
---
title: Combo 力导布局
title: Combo 相关布局
order: 10
---
*V4.6.0 新增功能。*是 G6 自研的、适用于带有 combo 的图,可自由组合内外布局,默认情况下,内部使用同心圆布局,外部使用力导向布局,可以有较好的效果,推荐有 combo 的图使用该布局。
*V3.5 新增功能。*Combo Force 是基于力导向的专用于带有 combo 的图的布局算法。通过自研改造经典力导向算法,将根据节点的 combo 信息,施加不同的力以达到同 combo 节点尽可能聚集,不同 combo 之间尽可能无重叠的布局。
## 使用指南
在有 Combo 的图上,推荐使用 `'comboForce'` 布局。其他内置布局将不会考虑 Combo 信息对布局的影响。
在有 Combo 的图上,推荐使用 `'comboCombined'` 或 `'comboForce'` 布局。其他内置布局将不会考虑 Combo 信息对布局的影响。

View File

@ -1,7 +1,7 @@
{
"private": true,
"name": "@antv/g6-site",
"version": "4.6.8",
"version": "4.6.10",
"description": "G6 sites deployed on gh-pages",
"keywords": [
"antv",
@ -36,7 +36,7 @@
"dependencies": {
"@ant-design/icons": "^4.0.6",
"@antv/chart-node-g6": "^0.0.3",
"@antv/g6": "4.6.8",
"@antv/g6": "4.6.10",
"@antv/gatsby-theme-antv": "1.1.15",
"@antv/util": "^2.0.9",
"@antv/vis-predict-engine": "^0.1.1",