From a67c1c4b56b62b7b4cda00c127f3b7cd36083dc6 Mon Sep 17 00:00:00 2001 From: Yanyan-Wang Date: Fri, 24 Apr 2020 15:28:03 +0800 Subject: [PATCH] feat: collapse expand combo to add virtual edges to represent the edges between collapsed combos. --- src/behavior/collapse-expand-combo.ts | 3 +- src/global.ts | 1 + src/graph/controller/item.ts | 24 +- src/graph/controller/layout.ts | 6 +- src/graph/graph.ts | 232 +++++++++++++++--- src/interface/item.ts | 8 +- src/item/combo.ts | 54 +++- src/item/edge.ts | 10 +- src/item/item.ts | 1 + src/item/node.ts | 2 +- src/layout/comboForce.ts | 66 +++-- src/shape/combo.ts | 6 +- src/shape/combos/circle.ts | 8 +- src/shape/combos/rect.ts | 22 +- src/types/index.ts | 2 +- src/util/graphic.ts | 6 +- stories/Case/component/tutorial.tsx | 4 + stories/Combo/combo.stories.tsx | 12 +- .../Combo/component/collapse-expand-vedge.tsx | 177 +++++++++++++ .../component/combo-collapse-expand-tree.tsx | 141 +++++++++++ .../combo-layout-collapse-expand.tsx | 84 ++++--- .../Layout/component/combo-force-layout.tsx | 7 - tests/unit/shape/combo-spec.ts | 8 +- 23 files changed, 749 insertions(+), 135 deletions(-) create mode 100644 stories/Combo/component/collapse-expand-vedge.tsx create mode 100644 stories/Combo/component/combo-collapse-expand-tree.tsx diff --git a/src/behavior/collapse-expand-combo.ts b/src/behavior/collapse-expand-combo.ts index 8be10bf819..86e00b215f 100644 --- a/src/behavior/collapse-expand-combo.ts +++ b/src/behavior/collapse-expand-combo.ts @@ -40,6 +40,7 @@ export default { return; } graph.collapseExpandCombo(comboId); - graph.layout(); + if (graph.get('layoutCfg')) graph.layout(); + else graph.refreshPositions(); }, }; diff --git a/src/global.ts b/src/global.ts index fe6734f145..4110e17d34 100644 --- a/src/global.ts +++ b/src/global.ts @@ -64,6 +64,7 @@ export default { }, size: [20, 5], color: '#A3B1BF', + padding: [25, 20, 15, 20] }, // 节点应用状态后的样式,默认仅提供 active 和 selected 用户可以自己扩展 nodeStateStyle: {}, diff --git a/src/graph/controller/item.ts b/src/graph/controller/item.ts index b41068abdf..9e670cdc66 100644 --- a/src/graph/controller/item.ts +++ b/src/graph/controller/item.ts @@ -17,6 +17,7 @@ import { traverseTreeUp, traverseTree, getComboBBox } from '../../util/graphic'; const NODE = 'node'; const EDGE = 'edge'; +const VEDGE = 'vedge'; const COMBO = 'combo'; const CFG_PREFIX = 'default'; const MAPPER_SUFFIX = 'Mapper'; @@ -46,11 +47,12 @@ export default class ItemController { public addItem(type: ITEM_TYPE, model: ModelConfig) { const { graph } = this; const parent: Group = graph.get(`${type}Group`) || graph.get('group'); - const upperType = upperFirst(type); + const vType = type === VEDGE ? EDGE : type; + const upperType = upperFirst(vType); let item: Item | null = null; // 获取 this.get('styles') 中的值 - let styles = graph.get(type + upperFirst(STATE_SUFFIX)) || {}; + let styles = graph.get(vType + upperFirst(STATE_SUFFIX)) || {}; const defaultModel = graph.get(CFG_PREFIX + upperType); if (model[STATE_SUFFIX]) { @@ -58,7 +60,7 @@ export default class ItemController { styles = model[STATE_SUFFIX]; } - const mapper = graph.get(type + MAPPER_SUFFIX); + const mapper = graph.get(vType + MAPPER_SUFFIX); if (mapper) { const mappedModel = mapper(model); if (mappedModel[STATE_SUFFIX]) { @@ -88,7 +90,7 @@ export default class ItemController { graph.emit('beforeadditem', { type, model }); - if (type === EDGE) { + if (type === EDGE || type === VEDGE) { let source: Id; let target: Id; source = (model as EdgeConfig).source; // eslint-disable-line prefer-destructuring @@ -96,9 +98,11 @@ export default class ItemController { if (source && isString(source)) { source = graph.findById(source); + if (source.getType() === 'combo') model.isComboEdge = true; } if (target && isString(target)) { target = graph.findById(target); + if (target.getType() === 'combo') model.isComboEdge = true; } if (!source || !target) { @@ -123,6 +127,7 @@ export default class ItemController { } else if (type === COMBO) { const children: ComboTree[] = (model as ComboConfig).children; + const comboBBox = getComboBBox(children, graph); model.x = comboBBox.x || Math.random() * 100; model.y = comboBBox.y || Math.random() * 100; @@ -252,7 +257,6 @@ export default class ItemController { if (!combo || combo.destroyed) { return; } - const comboBBox = getComboBBox(children, graph); combo.set('bbox', comboBBox); @@ -260,6 +264,7 @@ export default class ItemController { x: comboBBox.x, y: comboBBox.y }); + } /** @@ -322,9 +327,14 @@ export default class ItemController { graph.emit('beforeremoveitem', { item }); const type = item.getType(); - const items = graph.get(`${item.getType()}s`); + const items = graph.get(`${type}s`); const index = items.indexOf(item); - items.splice(index, 1); + if (index > -1) items.splice(index, 1); + if (type === EDGE) { + const vitems = graph.get(`v${type}s`); + const vindex = vitems.indexOf(item); + if (vindex > -1) vitems.splice(vindex, 1); + } const itemId: string = item.get('id'); const itemMap: NodeMap = graph.get('itemMap'); diff --git a/src/graph/controller/layout.ts b/src/graph/controller/layout.ts index cf3b6359e1..76c951c6e8 100644 --- a/src/graph/controller/layout.ts +++ b/src/graph/controller/layout.ts @@ -366,12 +366,12 @@ export default class LayoutController { nodes.push(model); }); edgeItems.forEach(edgeItem => { - if (!edgeItem.isVisible()) return; + if (edgeItem.destroyed || !edgeItem.isVisible()) return; const model = edgeItem.getModel(); - edges.push(model); + if (!model.isComboEdge) edges.push(model); }); comboItems.forEach(comboItem => { - if (!comboItem.isVisible()) return; + if (comboItem.destroyed || !comboItem.isVisible()) return; const model = comboItem.getModel(); combos.push(model); }); diff --git a/src/graph/graph.ts b/src/graph/graph.ts index 8ed5245342..252abdd805 100644 --- a/src/graph/graph.ts +++ b/src/graph/graph.ts @@ -70,6 +70,8 @@ export interface PrivateGraphOption extends GraphOptions { edges: EdgeConfig[]; + vedges: EdgeConfig[]; + groups: GroupConfig[]; combos: ComboConfig[]; @@ -299,6 +301,10 @@ export default class Graph extends EventEmitter implements IGraph { * store all the combo instances */ combos: [], + /** + * store all the edge instances which are virtual edges related to collapsed combo + */ + vedges: [], /** * all the instances indexed by id */ @@ -844,8 +850,8 @@ export default class Graph extends EventEmitter implements IGraph { found = true; const newCombo: ComboTree = { id: model.id as string, - depth: child.depth + 1, - ...model as ComboTree + depth: child.depth + 2, + ...model } if (child.children) child.children.push(newCombo); else child.children = [newCombo]; @@ -891,7 +897,7 @@ export default class Graph extends EventEmitter implements IGraph { const combos = this.get('combos'); if (combos && combos.length > 0) { - this.sortCombos(this.save() as GraphData); + this.sortCombos(); } this.autoPaint(); return item; @@ -972,9 +978,6 @@ export default class Graph extends EventEmitter implements IGraph { self.add('node', node); }); - each(edges, (edge: EdgeConfig) => { - self.add('edge', edge); - }); // process the data to tree structure if (combos && combos.length !== 0) { @@ -984,6 +987,10 @@ export default class Graph extends EventEmitter implements IGraph { self.addCombos(combos); } + each(edges, (edge: EdgeConfig) => { + self.add('edge', edge); + }); + // layout const layoutController = self.get('layoutController'); if (!layoutController.layout(success)) { @@ -999,7 +1006,7 @@ export default class Graph extends EventEmitter implements IGraph { if (!this.get('groupByTypes')) { if (combos && combos.length !== 0) { - this.sortCombos(data); + this.sortCombos(); } else { // 为提升性能,选择数量少的进行操作 if (data.nodes && data.edges && data.nodes.length < data.edges.length) { @@ -1122,6 +1129,15 @@ export default class Graph extends EventEmitter implements IGraph { } }); + // clear the destroyed combos here to avoid removing sub nodes before removing the parent combo + const comboItems = this.getCombos(); + const combosLength = comboItems.length; + for (let i = combosLength - 1; i >= 0; i--) { + if (comboItems[i].destroyed) { + comboItems.splice(i, 1); + } + } + // process the data to tree structure const combosData = (data as GraphData).combos; if (combosData) { @@ -1129,9 +1145,9 @@ export default class Graph extends EventEmitter implements IGraph { this.set('comboTrees', comboTrees); // add combos self.addCombos(combosData); - if (!this.get('groupByTypes')) this.sortCombos(data as GraphData); - } + if (!this.get('groupByTypes')) this.sortCombos(); + } this.set({ nodes: items.nodes, edges: items.edges }); @@ -1231,7 +1247,7 @@ export default class Graph extends EventEmitter implements IGraph { return true; }); }); - self.sortCombos(self.get('data')); + self.sortCombos(); } /** @@ -1376,6 +1392,7 @@ export default class Graph extends EventEmitter implements IGraph { } else { const nodes: INode[] = self.get('nodes'); const edges: IEdge[] = self.get('edges'); + const vedges: IEdge[] = self.get('edges'); each(nodes, (node: INode) => { node.refresh(); @@ -1384,6 +1401,10 @@ export default class Graph extends EventEmitter implements IGraph { each(edges, (edge: IEdge) => { edge.refresh(); }); + + each(vedges, (vedge: IEdge) => { + vedge.refresh(); + }); } @@ -1534,6 +1555,7 @@ export default class Graph extends EventEmitter implements IGraph { const nodes: INode[] = self.get('nodes'); const edges: IEdge[] = self.get('edges'); + const vedges: IEdge[] = self.get('vedges'); const combos: ICombo[] = self.get('combos') let model: NodeConfig; @@ -1549,17 +1571,21 @@ export default class Graph extends EventEmitter implements IGraph { updatedNodes[model.id] = true; }); + if (combos && combos.length !== 0) { + self.updateCombos(); + } + each(edges, (edge: IEdge) => { const sourceModel = edge.getSource().getModel(); const targetModel = edge.getTarget().getModel(); - if (updatedNodes[sourceModel.id as string] || updatedNodes[targetModel.id as string]) { + if (updatedNodes[sourceModel.id as string] || updatedNodes[targetModel.id as string] || edge.getModel().isComboEdge) { edge.refresh(); } }); - if (combos && combos.length !== 0) { - self.updateCombos(); - } + each(vedges, (vedge: IEdge) => { + vedge.refresh(); + }); self.emit('aftergraphrefreshposition'); self.autoPaint(); @@ -1852,6 +1878,7 @@ export default class Graph extends EventEmitter implements IGraph { public layout(): void { const layoutController = this.get('layoutController'); const layoutCfg = this.get('layout'); + if (!layoutCfg) return; if (layoutCfg.workerEnabled) { // 如果使用web worker布局 @@ -1874,6 +1901,83 @@ export default class Graph extends EventEmitter implements IGraph { combo = this.findById(combo) as ICombo; } const comboModel = combo.getModel(); + + // add virtual edges + const edges = this.getEdges().concat(this.get('vedges')); + const cnodes = combo.getNodes(); + const ccombos = combo.getCombos(); + + const processedNodes = {}; + const addedVEdges = []; + edges.forEach(edge => { + const source = edge.getSource(); + const target = edge.getTarget(); + if (((cnodes.includes(source) || ccombos.includes(source)) + && (!cnodes.includes(target) && !ccombos.includes(target))) + || (source.getModel().id === comboModel.id)) { + const edgeModel = edge.getModel(); + if (edgeModel.isVEdge) { + this.removeItem(edge); + return; + } + let targetModel = target.getModel(); + if (!target.isVisible()) { + targetModel = this.findById((targetModel.parentId as string) || (targetModel.comboId as string)).getModel(); + } + const targetId = targetModel.id; + + if (processedNodes[targetId]) { + processedNodes[targetId] += (edgeModel.size || 1); + return; + } + // the source is in the combo, the target is not + const vedge = this.addItem('vedge', { + source: comboModel.id, + target: targetId, + isVEdge: true, + }); + processedNodes[targetId] = edgeModel.size || 1; + addedVEdges.push(vedge); + } else if (((!cnodes.includes(source) && !ccombos.includes(source)) + && (cnodes.includes(target) || ccombos.includes(target))) + || (target.getModel().id === comboModel.id)) { + const edgeModel = edge.getModel(); + if (edgeModel.isVEdge) { + this.removeItem(edge); + return; + } + let sourceModel = source.getModel(); + if (!target.isVisible()) { + sourceModel = this.findById((sourceModel.parentId as string) || (sourceModel.comboId as string)).getModel(); + } + const sourceId = sourceModel.id; + if (processedNodes[sourceId]) { + processedNodes[sourceId] += (edgeModel.size || 1); + return; + } + // the target is in the combo, the source is not + const vedge = this.addItem('vedge', { + target: comboModel.id, + source: sourceId, + isVEdge: true + }); + processedNodes[sourceId] = edgeModel.size || 1; + addedVEdges.push(vedge); + } + }); + addedVEdges.forEach(vedge => { + const vedgeModel = vedge.getModel(); + if (vedgeModel.source === comboModel.id) { + this.updateItem(vedge, { + size: processedNodes[vedge.getTarget().get('id')] + }) + } else if (vedgeModel.target === comboModel.id) { + this.updateItem(vedge, { + size: processedNodes[vedge.getSource().get('id')] + }) + } + }); + const itemController: ItemController = this.get('itemController'); itemController.collapseCombo(combo); // update combo size @@ -1889,24 +1993,81 @@ export default class Graph extends EventEmitter implements IGraph { if (isString(combo)) { combo = this.findById(combo) as ICombo; } + const comboModel = combo.getModel(); + + // add virtual edges + const edges = this.getEdges().concat(this.get('vedges')); + const cnodes = combo.getNodes(); + const ccombos = combo.getCombos(); + + const processedNodes = {}; + const addedVEdges = {}; + edges.forEach(edge => { + const source = edge.getSource(); + const target = edge.getTarget(); + const sourceId = source.get('id'); + const targetId = target.get('id'); + if (((cnodes.includes(source) || ccombos.includes(source)) + && (!cnodes.includes(target) && !ccombos.includes(target))) + || sourceId === comboModel.id) { + if (edge.getModel().isVEdge) { + this.removeItem(edge); + return; + } + // the source is in the combo, the target is not + if (!target.isVisible()) { + const oppsiteComboId: string = (target.getModel().comboId as string) || (target.getModel().parentId as string); + if (oppsiteComboId) { + const vedgeId = `${sourceId}-${oppsiteComboId}`; + if (processedNodes[vedgeId]) { + processedNodes[vedgeId] += (edge.getModel().size || 1); + this.updateItem(addedVEdges[vedgeId], { + size: processedNodes[vedgeId] + }) + return; + } + const vedge = this.addItem('vedge', { + source: sourceId, + target: oppsiteComboId, + isVEdge: true + }); + processedNodes[vedgeId] = edge.getModel().size || 1; + addedVEdges[vedgeId] = vedge; + } + } + } else if (((!cnodes.includes(source) && !ccombos.includes(source)) + && (cnodes.includes(target) || ccombos.includes(target))) + || targetId === comboModel.id) { + if (edge.getModel().isVEdge) { + this.removeItem(edge); + return; + } + // the target is in the combo, the source is not + if (!source.isVisible()) { + const oppsiteComboId: string = (source.getModel().comboId as string) || (target.getModel().parentId as string); + if (oppsiteComboId) { + const vedgeId = `${oppsiteComboId}-${targetId}`; + if (processedNodes[vedgeId]) { + processedNodes[vedgeId] += (edge.getModel().size || 1); + this.updateItem(addedVEdges[vedgeId], { + size: processedNodes[vedgeId] + }) + return; + } + const vedge = this.addItem('vedge', { + target: targetId, + source: oppsiteComboId, + isVEdge: true + }); + processedNodes[vedgeId] = edge.getModel().size || 1; + addedVEdges[vedgeId] = vedge; + } + } + } + }); + const itemController: ItemController = this.get('itemController'); itemController.expandCombo(combo); - - const comboModel = combo.getModel(); - // find the children from comboTrees - const comboTrees = this.get('comboTrees'); - let children = []; - comboTrees.forEach((ctree: ComboTree) => { - let found = false; - traverseTreeUp(ctree, child => { - if (comboModel.id === child.id) { - children = child.children; - } - return true; - }); - }); - // update combo size - // itemController.updateCombo(combo, children); comboModel.collapsed = false; } @@ -1972,7 +2133,7 @@ export default class Graph extends EventEmitter implements IGraph { * 根据 comboTree 结构整理 Combo 相关的图形绘制层级,包括 Combo 本身、节点、边 * @param {GraphData} data 数据 */ - private sortCombos(data: GraphData) { + private sortCombos() { const depthMap = []; const dataDepthMap = {}; const comboTrees = this.get('comboTrees'); @@ -1984,10 +2145,11 @@ export default class Graph extends EventEmitter implements IGraph { return true; }); }); - const edges = data.edges; - edges && edges.forEach(edge => { - const sourceDepth: number = dataDepthMap[edge.source] || 0; - const targetDepth: number = dataDepthMap[edge.target] || 0; + const edges = this.getEdges().concat(this.get('vedges')); + edges && edges.forEach(edgeItem => { + const edge = edgeItem.getModel(); + const sourceDepth: number = dataDepthMap[edge.source as string] || 0; + const targetDepth: number = dataDepthMap[edge.target as string] || 0; const depth = Math.max(sourceDepth, targetDepth); if (depthMap[depth]) depthMap[depth].push(edge.id); else depthMap[depth] = [edge.id]; diff --git a/src/interface/item.ts b/src/interface/item.ts index 042e8b3325..368519dfa1 100644 --- a/src/interface/item.ts +++ b/src/interface/item.ts @@ -227,10 +227,10 @@ export interface IItemBase { } export interface IEdge extends IItemBase { - setSource(source: INode): void; - setTarget(target: INode): void; - getSource(): INode; - getTarget(): INode; + setSource(source: INode | ICombo): void; + setTarget(target: INode | ICombo): void; + getSource(): INode | ICombo; + getTarget(): INode | ICombo; } export interface INode extends IItemBase { diff --git a/src/item/combo.ts b/src/item/combo.ts index 7ac5fd73e3..92ff440579 100644 --- a/src/item/combo.ts +++ b/src/item/combo.ts @@ -4,11 +4,14 @@ import Node from './node'; import { ComboConfig, IBBox, IShapeBase } from '../types'; import Global from '../global'; import { getBBox } from '../util/graphic'; - +import isNumber from '@antv/util/lib/is-number'; +import { IItemBaseConfig } from '../interface/item'; const CACHE_BBOX = 'bboxCache'; const CACHE_CANVAS_BBOX = 'bboxCanvasCache'; const CACHE_SIZE = 'sizeCache'; +const CACHE_ANCHOR_POINTS = 'anchorPointsCache'; + export default class Combo extends Node implements ICombo { public getDefaultCfg() { @@ -26,14 +29,23 @@ export default class Combo extends Node implements ICombo { if (styles) { // merge graph的item样式与数据模型中的样式 const newModel = model; - const itemType = this.getType(); const size = { r: Math.hypot(bbox.height, bbox.width) / 2 || Global.defaultCombo.size[0] / 2, width: bbox.width || Global.defaultCombo.size[0], height: bbox.height || Global.defaultCombo.size[1] }; - this.set(CACHE_SIZE, size); newModel.style = Object.assign({}, styles, model.style, size); + let padding = model.padding || Global.defaultCombo.padding + if (isNumber(padding)) { + size.r += padding; + size.width += padding * 2; + size.height += padding * 2; + } else { + size.r += padding[0]; + size.width += (padding[1] + padding[3]) || padding[1] * 2; + size.height += (padding[0] + padding[2]) || padding[0] * 2; + } + this.set(CACHE_SIZE, size); return newModel; } return model; @@ -202,10 +214,44 @@ export default class Combo extends Node implements ICombo { public isOnlyMove(cfg?: ComboConfig): boolean { return false; } - + + + /** + * 获取 item 的包围盒,这个包围盒是相对于 item 自己,不会将 matrix 计算在内 + * @return {Object} 包含 x,y,width,height, centerX, centerY + */ + public getBBox(): IBBox { + this.set(CACHE_CANVAS_BBOX, null); + let bbox: IBBox = this.calculateCanvasBBox(); + // 计算 bbox 开销有些大,缓存 + // let bbox: IBBox = this.get(CACHE_BBOX); + // if (!bbox) { + // bbox = this.getCanvasBBox(); + // this.set(CACHE_BBOX, bbox); + // } + return bbox; + } public clearCache() { this.set(CACHE_BBOX, null); // 清理缓存的 bbox this.set(CACHE_CANVAS_BBOX, null); + this.set(CACHE_ANCHOR_POINTS, null); } + + public destroy() { + if (!this.destroyed) { + const animate = this.get('animate'); + const group: Group = this.get('group'); + if (animate) { + group.stopAnimate(); + } + this.clearCache(); + this.set(CACHE_SIZE, null); + this.set('bbox', null); + group.remove(); + (this._cfg as IItemBaseConfig | null) = null; + this.destroyed = true; + } + } + } \ No newline at end of file diff --git a/src/item/edge.ts b/src/item/edge.ts index 3292a5c56f..ad5beefdad 100644 --- a/src/item/edge.ts +++ b/src/item/edge.ts @@ -1,6 +1,6 @@ import isNil from '@antv/util/lib/is-nil'; import isPlainObject from '@antv/util/lib/is-plain-object'; -import { IEdge, INode } from '../interface/item'; +import { IEdge, INode, ICombo } from '../interface/item'; import { EdgeConfig, IPoint, NodeConfig, SourceTarget, Indexable, ModelConfig } from '../types'; import Item from './item'; import Node from './node'; @@ -174,21 +174,21 @@ export default class Edge extends Item implements IEdge { return out; } - public setSource(source: INode) { + public setSource(source: INode | ICombo) { this.setEnd('source', source); this.set('source', source); } - public setTarget(target: INode) { + public setTarget(target: INode | ICombo) { this.setEnd('target', target); this.set('target', target); } - public getSource(): INode { + public getSource(): INode | ICombo { return this.get('source'); } - public getTarget(): INode { + public getTarget(): INode | ICombo { return this.get('target'); } diff --git a/src/item/item.ts b/src/item/item.ts index b893252f04..49cbecdd82 100644 --- a/src/item/item.ts +++ b/src/item/item.ts @@ -699,6 +699,7 @@ export default class ItemBase implements IItemBase { if (animate) { group.stopAnimate(); } + this.clearCache(); group.remove(); (this._cfg as IItemBaseConfig | null) = null; this.destroyed = true; diff --git a/src/item/node.ts b/src/item/node.ts index 3a467ee7d2..ac88a0eb25 100644 --- a/src/item/node.ts +++ b/src/item/node.ts @@ -15,7 +15,7 @@ const CACHE_ANCHOR_POINTS = 'anchorPointsCache'; const CACHE_BBOX = 'bboxCache'; export default class Node extends Item implements INode { - private getNearestPoint(points: IPoint[], curPoint: IPoint): IPoint { + public getNearestPoint(points: IPoint[], curPoint: IPoint): IPoint { let index = 0; let nearestPoint = points[0]; let minDistance = distance(points[0], curPoint); diff --git a/src/layout/comboForce.ts b/src/layout/comboForce.ts index 7f61410f02..14829ed2aa 100644 --- a/src/layout/comboForce.ts +++ b/src/layout/comboForce.ts @@ -5,9 +5,10 @@ import { EdgeConfig, IPointTuple, NodeConfig, NodeIdxMap, ComboTree, ComboConfig } from '../types'; import { BaseLayout } from './layout'; -import { isNumber, isArray, isFunction, clone } from '@antv/util'; +import { isNumber, isArray, isFunction, clone, isNil } from '@antv/util'; import { Point } from '@antv/g-base'; import { traverseTreeUp } from '../util/graphic'; +import Global from '../global'; type Node = NodeConfig & { depth: number; @@ -44,7 +45,7 @@ export default class ComboForce extends BaseLayout { /** 群组中心力大小 */ public comboGravity: number = 10; /** 默认边长度 */ - public linkDistance: number | ((d?: unknown) => number) = 50; + public linkDistance: number | ((d?: unknown) => number) = 10; /** 每次迭代位移的衰减相关参数 */ public alpha: number = 1; public alphaMin: number = 0.001; @@ -63,7 +64,7 @@ export default class ComboForce extends BaseLayout { /** 是否开启 Combo 之间的防止重叠 */ public preventComboOverlap: boolean = false; /** 防止重叠的碰撞力大小 */ - public collideStrength: number = 0.2; + public collideStrength: number | undefined = undefined; /** 防止重叠的碰撞力大小 */ public nodeCollideStrength: number | undefined = undefined; /** 防止重叠的碰撞力大小 */ @@ -82,9 +83,10 @@ export default class ComboForce extends BaseLayout { public optimizeRangeFactor: number = 1; /** 每次迭代的回调函数 */ public tick: () => void = () => { }; - /** 根据边两端节点层级差距的调整函数 */ - public depthAttractiveForceScale: number = 0.3; // ((d?: unknown) => number); - public depthRepulsiveForceScale: number = 2; // ((d?: unknown) => number); + /** 根据边两端节点层级差距的调整引力的因子,[0, 1] 代表层级差距越大,引力越小 */ + public depthAttractiveForceScale: number = 0.5; + /** 根据边两端节点层级差距的调整斥力的因子,[1, Infinity] 代表层级差距越大,斥力越大 */ + public depthRepulsiveForceScale: number = 2; /** 内部计算参数 */ public nodes: Node[] = []; @@ -105,18 +107,24 @@ export default class ComboForce extends BaseLayout { public getDefaultCfg() { return { - maxIteration: 1000, + maxIteration: 100, center: [0, 0], gravity: 10, speed: 1, - comboGravity: 10, + comboGravity: 30, preventOverlap: false, + preventComboOverlap: true, + preventNodeOverlap: true, nodeSpacing: undefined, - collideStrength: 0.2, - nodeCollideStrength: undefined, - comboCollideStrength: undefined, - comboSpacing: 5, - comboPadding: 10 + collideStrength: undefined, + nodeCollideStrength: 0.5, + comboCollideStrength: 0.5, + comboSpacing: 20, + comboPadding: 10, + linkStrength: 0.2, + nodeStrength: 30, + linkDistance: 10, + }; } /** @@ -125,7 +133,6 @@ export default class ComboForce extends BaseLayout { public execute() { const self = this; const nodes = self.nodes; - const combos = self.combos; const center = self.center; self.comboTree = { id: 'comboTreeRoot', @@ -238,14 +245,14 @@ export default class ComboForce extends BaseLayout { self.comboMap = self.getComboMap(); const preventOverlap = self.preventOverlap; - if (preventOverlap) { - self.preventComboOverlap = true; - self.preventNodeOverlap = true; - } + self.preventComboOverlap = self.preventComboOverlap || preventOverlap; + self.preventNodeOverlap = self.preventNodeOverlap || preventOverlap; const collideStrength = self.collideStrength; - if (!self.comboCollideStrength) self.comboCollideStrength = collideStrength; - if (!self.nodeCollideStrength) self.nodeCollideStrength = collideStrength; + if (!isNil(collideStrength)) { + self.comboCollideStrength = collideStrength; + self.nodeCollideStrength = collideStrength; + } // get edge bias for (let i = 0; i < edges.length; ++i) { @@ -334,7 +341,7 @@ export default class ComboForce extends BaseLayout { let linkDistance = this.linkDistance; let linkDistanceFunc; if (!linkDistance) { - linkDistance = 50; + linkDistance = 10; } if (isNumber(linkDistance)) { linkDistanceFunc = d => { @@ -595,7 +602,11 @@ export default class ComboForce extends BaseLayout { if (c.maxX < nodeMaxX) c.maxX = nodeMaxX; if (c.maxY < nodeMaxY) c.maxY = nodeMaxY; }); - c.r = Math.max(c.maxX - c.minX, c.maxY - c.minY) / 2 + comboSpacing(c) / 2 + comboPadding(c); + let minSize = self.oriComboMap[treeNode.id].size || Global.defaultCombo.size; + if (isArray(minSize)) minSize = minSize[0]; + const maxLength = Math.max(c.maxX - c.minX, c.maxY - c.minY, minSize as number); + c.r = maxLength / 2 + comboSpacing(c) / 2 + comboPadding(c); + return true; }); }); @@ -614,6 +625,7 @@ export default class ComboForce extends BaseLayout { traverseTreeUp(comboTree, treeNode => { if (!comboMap[treeNode.id] && !nodeMap[treeNode.id] && treeNode.id !== 'comboTreeRoot') return; // means it is hidden const children = treeNode.children; + // 同个子树下的子 combo 间两两对比 if (children && children.length > 1) { children.forEach((v, i) => { if (v.itemType === 'node') return; @@ -642,6 +654,7 @@ export default class ComboForce extends BaseLayout { const yl = vy * ll; let rratio = ru2 / (rv2 + ru2); let irratio = 1 - rratio; + // 两兄弟 combo 的子节点上施加斥力 vnodes.forEach(vn => { if (vn.itemType !== 'node') return; if (!nodeMap[vn.id]) return; // means it is hidden @@ -704,9 +717,9 @@ export default class ComboForce extends BaseLayout { let { vx, vy } = vecMap[`${v.id}-${u.id}`]; - const depthDiff = (Math.abs(u.depth - v.depth) + 1) || 1; + let depthDiff = (Math.abs(u.depth - v.depth) + 1) || 1; + if (u.comboId !== v.comboId) depthDiff++; let depthParam = depthDiff ? Math.pow(scale, depthDiff) : 1; - // let depthParam = depthDiff ? scale * depthDiff : 1; const params = nodeStrength(u) * alpha / vl * depthParam; displacements[i].x += vx * params; @@ -763,13 +776,16 @@ export default class ComboForce extends BaseLayout { const vecX = vx * l; const vecY = vy * l; const b = bias[i]; - const depthDiff = Math.abs(u.depth - v.depth); + + let depthDiff = Math.abs(u.depth - v.depth); + if (u.comboId === v.comboId) depthDiff / 2; let depthParam = depthDiff ? Math.pow(scale, depthDiff) : 1; if (u.comboId !== v.comboId && depthParam === 1) { depthParam = scale; } else if (u.comboId === v.comboId) { depthParam = 1; } + // depthParam = 1; displacements[vIndex].x -= vecX * b * depthParam; displacements[vIndex].y -= vecY * b * depthParam; displacements[uIndex].x += vecX * (1 - b) * depthParam; diff --git a/src/shape/combo.ts b/src/shape/combo.ts index 29df7661d5..5481a0da73 100644 --- a/src/shape/combo.ts +++ b/src/shape/combo.ts @@ -6,7 +6,7 @@ import GGroup from '@antv/g-canvas/lib/group'; import { IShape } from '@antv/g-canvas/lib/interfaces'; import { isArray, isNil, clone, isNumber } from '@antv/util'; import { ILabelConfig, ShapeOptions } from '../interface/shape'; -import { Item, LabelStyle, NodeConfig, ModelConfig } from '../types'; +import { Item, LabelStyle, NodeConfig, ModelConfig, ShapeStyle } from '../types'; import Global from '../global'; import Shape from './shape'; import { shapeBase } from './shapeBase'; @@ -121,12 +121,12 @@ const singleCombo: ShapeOptions = { }); return shape; }, - updateShape(cfg: NodeConfig, item: Item, keyShapeStyle: object) { + updateShape(cfg: NodeConfig, item: Item, keyShapeStyle: ShapeStyle) { const keyShape = item.get('keyShape'); const animate = this.options.animate; if (animate && keyShape.animate) { keyShape.animate(keyShapeStyle, { - duration: 280, + duration: 200, easing: 'easeLinear', }) } else { diff --git a/src/shape/combos/circle.ts b/src/shape/combos/circle.ts index c90ef786bc..e77ff1d3dc 100644 --- a/src/shape/combos/circle.ts +++ b/src/shape/combos/circle.ts @@ -14,7 +14,7 @@ Shape.registerCombo( // 自定义节点时的配置 options: { size: [Global.defaultCombo.size[0], Global.defaultCombo.size[0]], - padding: 20, + padding: Global.defaultCombo.padding[0], animate: true, style: { stroke: Global.defaultCombo.style.stroke, @@ -87,6 +87,12 @@ Shape.registerCombo( const cfgStyle = clone(cfg.style); const r = Math.max(cfgStyle.r, size[0] / 2) || size[0] / 2;; cfgStyle.r = r + padding; + + const itemCacheSize = item.get('sizeCache'); + if (itemCacheSize) { + itemCacheSize.r = cfgStyle.r; + } + // 下面这些属性需要覆盖默认样式与目前样式,但若在 cfg 中有指定则应该被 cfg 的相应配置覆盖。 const strokeStyle = { stroke: cfg.color diff --git a/src/shape/combos/rect.ts b/src/shape/combos/rect.ts index d0696c4065..f75729cee5 100644 --- a/src/shape/combos/rect.ts +++ b/src/shape/combos/rect.ts @@ -275,8 +275,16 @@ Shape.registerCombo( const cfgStyle = clone(cfg.style); const width = (Math.max(cfgStyle.width, size[0]) || size[0]); const height = (Math.max(cfgStyle.height, size[1]) || size[1]); + cfgStyle.width = width + padding[1] + padding[3]; cfgStyle.height = height + padding[0] + padding[2]; + + const itemCacheSize = item.get('sizeCache'); + if (itemCacheSize) { + itemCacheSize.width = cfgStyle.width; + itemCacheSize.height = cfgStyle.height; + } + cfgStyle.x = -width / 2 - padding[3]; cfgStyle.y = -height / 2 - padding[0]; // 下面这些属性需要覆盖默认样式与目前样式,但若在 cfg 中有指定则应该被 cfg 的相应配置覆盖。 @@ -298,9 +306,17 @@ Shape.registerCombo( }, updateShape(cfg: ComboConfig, item: Item, keyShapeStyle: object) { const keyShape = item.get('keyShape'); - keyShape.attr({ - ...keyShapeStyle, - }); + const animate = this.options.animate; + if (animate && keyShape.animate) { + keyShape.animate(keyShapeStyle, { + duration: 200, + easing: 'easeLinear', + }) + } else { + keyShape.attr({ + ...keyShapeStyle, + }); + } (this as any).updateLabel(cfg, item); (this as any).updateCollapseIcon(cfg, item, keyShapeStyle) diff --git a/src/types/index.ts b/src/types/index.ts index 1961d0193a..f2fb1f93b5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -598,7 +598,7 @@ export interface IG6GraphEvent extends GraphEvent { // Node Edge Combo 实例 export type Item = INode | IEdge | ICombo; -export type ITEM_TYPE = 'node' | 'edge' | 'combo' | 'group'; +export type ITEM_TYPE = 'node' | 'edge' | 'combo' | 'group' | 'vedge'; export type NodeIdxMap = { [key: string]: number; diff --git a/src/util/graphic.ts b/src/util/graphic.ts index 8f97deef81..3be4dd0c23 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -506,13 +506,15 @@ export const plainCombosToTrees = (array: ComboConfig[], nodes?: INode[]) => { tree.depth = 0; traverse(tree, child => { let parent; - if (addedMap[child.id]['itemType'] === 'node') { + const itemType = addedMap[child.id]['itemType']; + if (itemType === 'node') { parent = addedMap[child['comboId'] as string]; } else { parent = addedMap[child.parentId]; } if (parent) { - child.depth = parent.depth + 1; + if (itemType === 'node') child.depth = parent.depth + 1; + else child.depth = parent.depth + 2; } else { child.depth = 0; } diff --git a/stories/Case/component/tutorial.tsx b/stories/Case/component/tutorial.tsx index 5c3a8aed65..fa178e1e90 100644 --- a/stories/Case/component/tutorial.tsx +++ b/stories/Case/component/tutorial.tsx @@ -600,6 +600,10 @@ const Tutorial = () => { 'https://gw.alipayobjects.com/os/basement_prod/6cae02ab-4c29-44b2-b1fd-4005688febcb.json', ); const data = await response.json(); +<<<<<<< HEAD +======= + console.log(data); +>>>>>>> feat: collapse expand combo to add virtual edges to represent the edges between collapsed combos. const nodes = data.nodes; const edges = data.edges; nodes.forEach(node => { diff --git a/stories/Combo/combo.stories.tsx b/stories/Combo/combo.stories.tsx index becb726fe8..b46c1ef79f 100644 --- a/stories/Combo/combo.stories.tsx +++ b/stories/Combo/combo.stories.tsx @@ -3,8 +3,10 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; import DefaultCombo from './component/default-combo'; import RegisterCombo from './component/register-combo'; -import CollapseExpand from './component/collapse-expand-combo' -import ComboLayoutCollapseExpand from './component/combo-layout-collapse-expand' +import CollapseExpand from './component/collapse-expand-combo'; +import ComboLayoutCollapseExpand from './component/combo-layout-collapse-expand'; +import CollapseExpandVEdge from './component/collapse-expand-vedge'; +import ComboCollapseExpandTree from './component/combo-collapse-expand-tree'; export default { title: 'Combo' }; @@ -20,4 +22,10 @@ storiesOf('Combo', module) )) .add('force + collapse expand', () => ( + )) + .add('collapse expand vedge', () => ( + + )) + .add('collapse expand tree', () => ( + )); diff --git a/stories/Combo/component/collapse-expand-vedge.tsx b/stories/Combo/component/collapse-expand-vedge.tsx new file mode 100644 index 0000000000..42249a0111 --- /dev/null +++ b/stories/Combo/component/collapse-expand-vedge.tsx @@ -0,0 +1,177 @@ +import React, { useEffect } from 'react'; +import G6 from '../../../src'; +import { IGraph } from '../../../src/interface/graph'; + +let graph: IGraph = null; + +const colors = { + a: '#BDD2FD', + b: '#BDEFDB', + c: '#C2C8D5', + d: '#FBE5A2', + e: '#F6C3B7', + f: '#B6E3F5', + g: '#D3C6EA', + h: '#FFD8B8', + i: '#AAD8D8', + j: '#FFD6E7', +}; + + +const testData = { + nodes: [ + { + id: '0', + label: '0', + comboId: 'a', + x: 100, + y: 100 + }, + { + id: '1', + label: '1', + comboId: 'a', + x: 150, + y: 140 + }, + { + id: '2', + label: '2', + comboId: 'b', + x: 300, + y: 200 + }, + { + id: '3', + label: '3', + comboId: 'b', + x: 370, + y: 260 + }, + { + id: '4', + label: '4', + comboId: 'c', + x: 360, + y: 510 + }, + // { + // id: '5', + // label: '5', + // comboId: 'd', + // x: 420, + // y: 510 + // }, + ], + edges: [ + // { + // source: 'a', + // target: 'b', + // size: 3, + // style: { + // stroke: 'red' + // } + // }, + // { + // source: 'b', + // target: '1', + // size: 3, + // style: { + // stroke: 'blue' + // } + // }, + { + source: '0', + target: '1', + }, + { + source: '0', + target: '2', + }, + { + source: '0', + target: '3', + }, + // { + // source: '3', + // target: '5', + // }, + { + source: '4', + target: '1', + } + ], + combos: [{ + id: 'a', + label: 'combo a' + }, { + id: 'b', + label: 'combo b' + }, { + id: 'c', + label: 'combo c' + }, + // { + // id: 'd', + // label: 'combo d', + // parentId: 'c' + // } + ] +}; + +const CollapseExpandVEdge = () => { + const container = React.useRef(); + useEffect(() => { + if (!graph) { + graph = new G6.Graph({ + container: container.current as string | HTMLElement, + width: 1000, + height: 800, + fitView: true, + modes: { + default: ['drag-canvas', 'drag-node', 'zoom-canvas', 'collapse-expand-combo'], + }, + defaultEdge: { + size: 1, + color: '#666', + }, + defaultCombo: { + type: 'circle', + //padding: 1 + }, + groupByTypes: false, + //animate: true + }); + + graph.node(node => { + const color = colors[node.comboId as string]; + return { + size: 20, + style: { + lineWidth: 2, + stroke: '#ccc', + fill: color, + }, + } + }); + graph.combo(combo => { + const color = colors[combo.id as string]; + return { + // size: 80, + style: { + lineWidth: 2, + stroke: color, + fillOpacity: 0.8 + }, + } + }); + + + graph.data(testData);//testData_pos + graph.render(); + } + }); + return
; +}; + +export default CollapseExpandVEdge; diff --git a/stories/Combo/component/combo-collapse-expand-tree.tsx b/stories/Combo/component/combo-collapse-expand-tree.tsx new file mode 100644 index 0000000000..3e57b36975 --- /dev/null +++ b/stories/Combo/component/combo-collapse-expand-tree.tsx @@ -0,0 +1,141 @@ +import React, { useEffect } from 'react'; +import G6 from '../../../src'; +import { IGraph } from '../../../src/interface/graph'; + +let graph: IGraph = null; + +const colors = { + a: '#BDD2FD', + b: '#BDEFDB', + c: '#C2C8D5', + d: '#FBE5A2', + e: '#F6C3B7', + f: '#B6E3F5', + g: '#D3C6EA', + h: '#FFD8B8', + i: '#AAD8D8', + j: '#FFD6E7', +}; +const data = { + "id": "Modeling Methods", + "children": [ + { + "id": "Classification", + comboId: 'a', + "children": [ + { "id": "Logistic regression", comboId: 'a', }, + { "id": "Linear discriminant analysis", comboId: 'a' }, + { "id": "Rules", comboId: 'a' }, + { "id": "Decision trees", comboId: 'a' }, + { "id": "Naive Bayes", comboId: 'a' }, + { "id": "K nearest neighbor", comboId: 'a' }, + { "id": "Probabilistic neural network", comboId: 'a' }, + { "id": "Support vector machine", comboId: 'a' } + ] + }, + { + "id": "Consensus", + "children": [ + { + "id": "Models diversity", + comboId: 'a', + "children": [ + { "id": "Different initializations", comboId: 'a' }, + { "id": "Different parameter choices", comboId: 'a' }, + { "id": "Different architectures", comboId: 'a' }, + { "id": "Different modeling methods", comboId: 'a' }, + { "id": "Different training sets", comboId: 'a' }, + { "id": "Different feature sets", comboId: 'a' } + ] + }, + { + "id": "Methods", + comboId: 'b', + "children": [ + { "id": "Classifier selection", comboId: 'b' }, + { "id": "Classifier fusion", comboId: 'b' } + ] + }, + { + "id": "Common", + comboId: 'c', + "children": [ + { "id": "Bagging", comboId: 'c' }, + { "id": "Boosting", comboId: 'c' }, + { "id": "AdaBoost", comboId: 'c' } + ] + } + ] + }, + { + "id": "Regression", + comboId: 'd', + "children": [ + { "id": "Multiple linear regression", comboId: 'd' }, + { "id": "Partial least squares", comboId: 'd' }, + { "id": "Multi-layer feedforward neural network" }, + { "id": "General regression neural network" }, + { "id": "Support vector regression" } + ] + } + ], + combos: [] +}; + +const ComboCollapseExpandTree = () => { + const container = React.useRef(); + useEffect(() => { + if (!graph) { + graph = new G6.TreeGraph({ + container: container.current as string | HTMLElement, + width: 1000, + height: 800, + fitView: true, + modes: { + default: ['drag-canvas', 'drag-node', 'zoom-canvas', 'collapse-expand-combo'], + }, + layout: { + type: 'compactBox', + }, + defaultEdge: { + size: 1, + color: '#666', + }, + defaultCombo: { + type: 'circle', + padding: 50 + }, + groupByTypes: false, + //animate: true + }); + + graph.combo(combo => { + const color = colors[combo.id as string]; + return { + // size: 80, + style: { + lineWidth: 2, + stroke: color, + fillOpacity: 0.8 + }, + } + }); + + + data.combos = [{ + id: 'a' + }, { + id: 'b' + }, { + id: 'c' + }, { + id: 'd' + }]; + graph.data(data); + graph.render(); + } + }); + return
; +}; + +export default ComboCollapseExpandTree; diff --git a/stories/Combo/component/combo-layout-collapse-expand.tsx b/stories/Combo/component/combo-layout-collapse-expand.tsx index 4cbadaba1d..37b339a550 100644 --- a/stories/Combo/component/combo-layout-collapse-expand.tsx +++ b/stories/Combo/component/combo-layout-collapse-expand.tsx @@ -628,6 +628,22 @@ const testData = { }, ], edges: [ + { + source: 'a', + target: 'b', + size: 3, + style: { + stroke: 'red' + } + }, + { + source: 'a', + target: '33', + size: 3, + style: { + stroke: 'blue' + } + }, { source: '0', target: '1', @@ -922,6 +938,8 @@ const testData2 = { target: 'node0' }] } + +const testData_pos = {"nodes":[{"id":"0","x":519.9756011152062,"y":312.3748588848735,"comboId":"a","label":"0"},{"id":"1","x":516.9130868036522,"y":300.01298088318964,"comboId":"a","label":"1"},{"id":"2","x":532.135761126721,"y":292.4828444555691,"comboId":"a","label":"2"},{"id":"3","x":503.08139107396323,"y":274.4859385079485,"comboId":"a","label":"3"},{"id":"4","x":515.7858143951453,"y":288.9459640448524,"comboId":"a","label":"4"},{"id":"5","x":523.1699237148887,"y":271.80995614771984,"comboId":"a","label":"5"},{"id":"6","x":542.5161585695183,"y":269.44295161957444,"comboId":"a","label":"6"},{"id":"7","x":540.3068131495662,"y":312.3305908975899,"comboId":"a","label":"7"},{"id":"8","x":540.1624078071186,"y":332.7802388427267,"comboId":"a","label":"8"},{"id":"9","x":497.2349390488828,"y":294.6747309770873,"comboId":"a","label":"9"},{"id":"10","x":477.27853085824415,"y":310.52154966368073,"comboId":"a","label":"10"},{"id":"11","x":498.2935956770408,"y":315.7336916123234,"comboId":"a","label":"11"},{"id":"12","x":511.8111914629553,"y":331.4660202536512,"comboId":"a","label":"12"},{"id":"13","x":719.9144466443125,"y":657.5827638668379,"comboId":"b","label":"13"},{"id":"14","x":705.1704215932026,"y":623.1349052942683,"comboId":"b","label":"14"},{"id":"15","x":748.888997314242,"y":693.5395656445546,"comboId":"b","label":"15"},{"id":"16","x":691.790150377325,"y":647.2895235684261,"comboId":"b","label":"16"},{"id":"17","x":700.453909738154,"y":666.3168884837505,"comboId":"b","label":"17"},{"id":"18","x":377.3258580235555,"y":373.66554381437624,"comboId":"c","label":"18"},{"id":"19","x":361.8579868952546,"y":350.20312804345934,"comboId":"c","label":"19"},{"id":"20","x":380.71103876474103,"y":332.78105195763504,"comboId":"c","label":"20"},{"id":"21","x":369.1201243167846,"y":332.02692181101764,"comboId":"c","label":"21"},{"id":"22","x":377.8110758618865,"y":350.0158128077823,"comboId":"c","label":"22"},{"id":"23","x":359.4471586647803,"y":335.9621566320535,"comboId":"c","label":"23"},{"id":"24","x":372.74841352346505,"y":314.6483478876071,"comboId":"c","label":"24"},{"id":"25","x":352.7557971300988,"y":331.4794537088405,"comboId":"c","label":"25"},{"id":"26","x":344.9855032420906,"y":350.16490710174355,"comboId":"c","label":"26"},{"id":"27","x":347.46959717648184,"y":313.6154232044856,"comboId":"c","label":"27"},{"id":"28","x":361.4870734755764,"y":322.1957223205074,"comboId":"c","label":"28"},{"id":"29","x":329.63246460052113,"y":320.22474499903063,"comboId":"c","label":"29"},{"id":"30","x":339.92007768802193,"y":335.1429986552043,"comboId":"c","label":"30"},{"id":"31","x":680.1030572348665,"y":679.0745385883383,"comboId":"d","label":"31"},{"id":"32","x":701.6575625058663,"y":703.0463672244064,"comboId":"d","label":"32"},{"id":"33","x":658.0840704258677,"y":660.8269175948863,"comboId":"d","label":"33"}],"combos":[{"id":"a","label":"combo a"},{"id":"b","label":"combo b"},{"id":"c","label":"combo c"},{"id":"d","parentId":"b","label":"combo d"}]} const ComboLayoutCollapseExpand = () => { const container = React.useRef(); useEffect(() => { @@ -929,36 +947,24 @@ const ComboLayoutCollapseExpand = () => { graph = new G6.Graph({ container: container.current as string | HTMLElement, width: 1000, - height: 800, - // fitView: true, + height: 500, + fitView: true, modes: { default: ['drag-canvas', 'drag-node', 'zoom-canvas', 'collapse-expand-combo'], }, layout: { - type: 'comboForce', - linkDistance: 10, - comboGravity: 30, - nodeSpacing: 1, - nodeStrength: 30, - linkStrength: 0.1, - preventNodeOverlap: true, - preventComboOverlap: true, - // preventOverlap: true, - comboCollideStrength: 0.5, - nodeCollideStrength: 0.1, - maxIteration: 100, - comboPadding: 10, - comboSpacing: 30 + type: 'comboForce' }, defaultEdge: { - size: 3, + size: 1, color: '#666', }, defaultCombo: { - type: 'rect' + type: 'circle', + padding: 10 }, groupByTypes: false, - animate: true + // animate: true }); graph.node(node => { @@ -975,8 +981,7 @@ const ComboLayoutCollapseExpand = () => { graph.combo(combo => { const color = colors[combo.id as string]; return { - size: 20, - // padding: 5, + // size: 80, style: { lineWidth: 2, stroke: color, @@ -985,14 +990,35 @@ const ComboLayoutCollapseExpand = () => { } }); - fetch( - 'https://gw.alipayobjects.com/os/basement_prod/7bacd7d1-4119-4ac1-8be3-4c4b9bcbc25f.json', - ) - .then(res => res.json()) - .then(data => { - graph.data(testData); - graph.render(); - }); + graph.data(testData);//testData_pos + graph.render(); + // const outputData = { + // nodes: [], + // combos: [] + // }; + // graph.getNodes().forEach((n: any) => { + // const node = n.getModel(); + // console.log(node.x, node.y) + // outputData.nodes.push({ + // id: node.id, + // x: node.x, + // y: node.y, + // comboId: node.comboId, + // label: node.label + // }); + // }) + // testData.combos.forEach((combo: any) => { + // outputData.combos.push({ + // id: combo.id, + // parentId: combo.parentId, + // label: combo.label + // }); + // }); + // console.log(JSON.stringify(outputData)); + + // graph.on('canvas:click', e => { + // graph.changeData(testData_pos); + // }); } }); return
; diff --git a/stories/Layout/component/combo-force-layout.tsx b/stories/Layout/component/combo-force-layout.tsx index 4903d83923..c612529ffa 100644 --- a/stories/Layout/component/combo-force-layout.tsx +++ b/stories/Layout/component/combo-force-layout.tsx @@ -497,13 +497,6 @@ const ComboForceLayout = () => { modes: { default: ['drag-canvas', 'drag-node', 'zoom-canvas'], }, - layout: { - type: 'comboForce', - linkDistance: 1000, - fitView: true, - modes: { - default: ['drag-canvas', 'drag-node', 'zoom-canvas'], - }, layout: { type: 'comboForce', linkDistance: 100, diff --git a/tests/unit/shape/combo-spec.ts b/tests/unit/shape/combo-spec.ts index a1306bc333..da5d44516c 100644 --- a/tests/unit/shape/combo-spec.ts +++ b/tests/unit/shape/combo-spec.ts @@ -51,7 +51,7 @@ describe('combo node test', () => { group, ); canvas.draw(); - expect(shape.attr('r')).toBe(40); + expect(shape.attr('r')).toBe(45); // size / 2 + padding expect(group.getCount()).toBe(1); }); @@ -156,7 +156,11 @@ describe('combo node test', () => { fill: 'steelblue' } }); - expect(shape.attr('fill')).toBe('steelblue'); + // since the update is animated, check it after 300ms + setTimeout(() => { + expect(shape.attr('fill')).toBe('steelblue'); + }, 300); + }); it('active', () => {