From 5dd6a79572fcdd79128f1fb0bc4bf8f69fd932e7 Mon Sep 17 00:00:00 2001 From: Yanyan Wang Date: Thu, 3 Mar 2022 20:37:15 +0800 Subject: [PATCH] feat: comboCombined Layout; feat: combo supports position configurations for any situations; 4.6.0-beta (#3510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add indented tree and brief node demos * feat: combo supports being assigned positions * fix: run layout promise only when the layout is configured; * chore: rebase and version number unpdate * chore: upgrade * chore: refine * chore: refine * fix: no animate when a node has no previous position * fix: fix the hidden combos not set correctly (#3509) close #3508 * chore: update version * chore: register dagreCompound layout (#3548) * fix: #3513 切换节点文字位置,文字位置出现错误 (#3531) Co-authored-by: daichaofan * chore: update versions Co-authored-by: simplejason Co-authored-by: daichaofan <50311886+daichaofan@users.noreply.github.com> Co-authored-by: daichaofan --- CHANGELOG.md | 7 + packages/core/package.json | 2 +- packages/core/src/element/node.ts | 6 +- packages/core/src/global.ts | 2 +- packages/core/src/graph/controller/item.ts | 118 +- packages/core/src/graph/controller/layout.ts | 26 +- packages/core/src/graph/graph.ts | 82 +- packages/core/src/interface/graph.ts | 4 +- packages/core/src/item/edge.ts | 6 +- packages/core/src/item/item.ts | 4 +- packages/core/src/util/graphic.ts | 6 +- .../unit/shape/combo-collapsed-pos-spec.ts | 1346 +++++++++++++++++ packages/element/package.json | 6 +- packages/g6/package.json | 6 +- packages/g6/src/index.ts | 4 +- packages/pc/package.json | 14 +- packages/pc/src/global.ts | 2 +- packages/pc/src/graph/controller/layout.ts | 61 +- packages/pc/src/layout/index.ts | 8 +- .../pc/src/layout/worker/layout.worker.ts | 8 +- .../unit/element/nodes/icon-iconfont-spec.ts | 3 +- .../tests/unit/layout/combo-combined-spec.ts | 298 ++++ packages/pc/tests/unit/layout/dagre-spec.ts | 11 +- .../tests/unit/layout/data/combo-test-data.ts | 13 + packages/plugin/package.json | 6 +- packages/plugin/src/minimap/index.ts | 12 +- .../case/treeDemos/demo/indentedTree.js | 4 +- 27 files changed, 1913 insertions(+), 152 deletions(-) create mode 100644 packages/core/tests/unit/shape/combo-collapsed-pos-spec.ts create mode 100644 packages/pc/tests/unit/layout/combo-combined-spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4005b3011a7..f00d41ba600 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # ChangeLog +#### 4.6.0-beta + +- feat: comboCombined Layout from @antv/layout; +- feat: combo supports position configurations for any situations; +- fix: run layout promise only when the layout is configured; + #### 4.5.5 - fix: tooltip with wrong duplicated child DOM nodes; @@ -23,6 +29,7 @@ - fix: edge label background with clearItemStates problem; - fix: edge label with autoRotate false and padding problem; - fix: changeData in the process of create-edge behavior, an error occurs, closes: #3384; +- fix: node update from no icon to iconfont icon failed; #### 4.5.1 diff --git a/packages/core/package.json b/packages/core/package.json index 249b15b527b..5e942219a4d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g6-core", - "version": "0.5.5", + "version": "0.6.0", "description": "A Graph Visualization Framework in JavaScript", "keywords": [ "antv", diff --git a/packages/core/src/element/node.ts b/packages/core/src/element/node.ts index f3f65788cd6..987d9396023 100644 --- a/packages/core/src/element/node.ts +++ b/packages/core/src/element/node.ts @@ -52,7 +52,7 @@ const singleNode: ShapeOptions = { // 默认的位置(最可能的情形),所以放在最上面 if (labelPosition === 'center') { - return { x: 0, y: 0, text: cfg!.label as string }; + return { x: 0, y: 0, text: cfg!.label as string, textBaseline: 'middle', textAlign: 'center' }; } let { offset } = labelCfg; @@ -70,6 +70,7 @@ const singleNode: ShapeOptions = { x: 0, y: -size[1] / 2 - (offset as number), textBaseline: 'bottom', // 文本在图形的上面 + textAlign: 'center', }; break; case 'bottom': @@ -77,12 +78,14 @@ const singleNode: ShapeOptions = { x: 0, y: size[1] / 2 + (offset as number), textBaseline: 'top', + textAlign: 'center', }; break; case 'left': style = { x: -size[0] / 2 - (offset as number), y: 0, + textBaseline: 'middle', textAlign: 'right', }; break; @@ -90,6 +93,7 @@ const singleNode: ShapeOptions = { style = { x: size[0] / 2 + (offset as number), y: 0, + textBaseline: 'middle', textAlign: 'left', }; break; diff --git a/packages/core/src/global.ts b/packages/core/src/global.ts index de2432b9df3..8fcd2fc402b 100644 --- a/packages/core/src/global.ts +++ b/packages/core/src/global.ts @@ -64,7 +64,7 @@ const colorSet = { }; export default { - version: '0.5.5', + version: '0.6.0', rootContainerClassName: 'root-container', nodeContainerClassName: 'node-container', edgeContainerClassName: 'edge-container', diff --git a/packages/core/src/graph/controller/item.ts b/packages/core/src/graph/controller/item.ts index 7a2a6471261..de645837f3c 100644 --- a/packages/core/src/graph/controller/item.ts +++ b/packages/core/src/graph/controller/item.ts @@ -149,10 +149,22 @@ export default class ItemController { const children: ComboTree[] = (model as ComboConfig).children; const comboBBox = getComboBBox(children, graph); - if (!isNaN(comboBBox.x)) model.x = comboBBox.x; - else if (isNaN(model.x)) model.x = Math.random() * 100; - if (!isNaN(comboBBox.y)) model.y = comboBBox.y; - else if (isNaN(model.y)) model.y = Math.random() * 100; + let bboxX, bboxY; + if (!isNaN(comboBBox.x)) bboxX = comboBBox.x; + else if (isNaN(model.x)) bboxX = Math.random() * 100; + if (!isNaN(comboBBox.y)) bboxY = comboBBox.y; + else if (isNaN(model.y)) bboxY = Math.random() * 100; + + if (isNaN(model.x) || isNaN(model.y)) { + model.x = bboxX; + model.y = bboxY; + } else { + // if there is x y in model, place the combo according to it and move its succeed items. that means, the priority of the combo's position is higher than succeed items' + const dx = model.x - bboxX; + const dy = model.y - bboxY; + // In the same time, adjust the children's positions + this.updateComboSucceeds(model.id, dx, dy, children); + } const comboGroup = parent.addGroup(); comboGroup.setZIndex((model as ComboConfig).depth as number); @@ -215,6 +227,7 @@ export default class ItemController { const mapper = graph.get(type + MAPPER_SUFFIX); const model = item.getModel(); + const { x: oriX, y: oriY } = model; const updateType = item.getUpdateType(cfg); @@ -263,30 +276,35 @@ export default class ItemController { (item as IEdge).setTarget(target); } item.update(cfg); - } - - // item.update(cfg); - - if (type === NODE || type === COMBO) { + } else if (type === NODE) { item.update(cfg, updateType); const edges: IEdge[] = (item as INode).getEdges(); const refreshEdge = updateType?.includes('bbox') || updateType === 'move'; - if (type === NODE) { - if (updateType === 'move') { - each(edges, (edge: IEdge) => { - this.edgeToBeUpdateMap[edge.getID()] = { - edge: edge, - updateType - }; - this.throttleRefresh(); - }); - } else if (refreshEdge) { - each(edges, (edge: IEdge) => { - edge.refresh(updateType); - }); - } + if (updateType === 'move') { + each(edges, (edge: IEdge) => { + this.edgeToBeUpdateMap[edge.getID()] = { + edge: edge, + updateType + }; + this.throttleRefresh(); + }); + } else if (refreshEdge) { + each(edges, (edge: IEdge) => { + edge.refresh(updateType); + }); } - else if (refreshEdge && type === COMBO) { + } else if (type === COMBO) { + item.update(cfg, updateType); + if (!isNaN(cfg.x) || !isNaN(cfg.y)) { + // if there is x y in model, place the combo according to it and move its succeed items. that means, the priority of the combo's position is higher than succeed items' + const dx = (cfg.x - oriX) || 0; + const dy = (cfg.y - oriY) || 0; + // In the same time, adjust the children's positions + this.updateComboSucceeds(model.id, dx, dy); + } + const edges: IEdge[] = (item as INode).getEdges(); + const refreshEdge = updateType?.includes('bbox') || updateType === 'move'; + if (refreshEdge && type === COMBO) { const shapeFactory = item.get('shapeFactory'); const shapeType = (model.type as string) || 'circle'; const comboAnimate = @@ -311,7 +329,6 @@ export default class ItemController { } graph.emit('afterupdateitem', { item, cfg }); } - /** * 更新边限流,同时可以防止相同的边频繁重复更新 * */ @@ -324,6 +341,9 @@ export default class ItemController { Object.keys(edgeToBeUpdateMap).forEach(eid => { const edge = edgeToBeUpdateMap[eid].edge; if (!edge || edge.destroyed) return; + const source = edge.getSource(); + const target = edge.getTarget(); + if (!source || source.destroyed || !target || target.destroyed) return; edge.refresh(edgeToBeUpdateMap[eid].updateType); }); this.edgeToBeUpdateMap = {}; @@ -342,7 +362,7 @@ export default class ItemController { * @returns * @memberof ItemController */ - public updateCombo(combo: ICombo | string, children: ComboTree[]) { + public updateCombo(combo: ICombo | string, children: ComboTree[], followCombo?: boolean) { const { graph } = this; if (isString(combo)) { @@ -358,10 +378,17 @@ export default class ItemController { const { x: comboX, y: comboY } = comboBBox; combo.set('bbox', comboBBox); - combo.update({ - x: comboX || model.x, - y: comboY || model.y, - }); + let x = comboX, y = comboY; + if (followCombo) { + // position of combo model first + x = isNaN(model.x) ? comboX : model.x; + y = isNaN(model.y) ? comboY : model.y; + } else { + // position of succeed items first + x = isNaN(comboX) ? model.x : comboX; + y = isNaN(comboY) ? model.y : comboY; + } + combo.update({ x, y }); const shapeFactory = combo.get('shapeFactory'); const shapeType = (model.type as string) || 'circle'; @@ -415,6 +442,37 @@ export default class ItemController { }); } + /** + * 根据位置差量 dx dy,更新 comboId 后继元素的位置 + * */ + public updateComboSucceeds(comboId, dx, dy, children = []) { + const { graph } = this; + if (!dx && !dy) return; + let kids = children; + if (!kids?.length) { + const comboTrees = graph.get('comboTrees'); + comboTrees?.forEach(child => { + traverseTree(child, subTree => { + if (subTree.id === comboId) { + kids = subTree.children; + return false; + } + return true; + }); + }); + } + kids?.forEach(child => { + const childItem = graph.findById(child.id); + if (childItem) { + const childModel = childItem.getModel(); + this.updateItem(child.id, { + x: (childModel.x || 0) + dx, + y: (childModel.y || 0) + dy + }); + } + }); + } + /** * 展开 combo,相关元素出现 * 若子 combo 原先是收起状态,则保持它的收起状态 diff --git a/packages/core/src/graph/controller/layout.ts b/packages/core/src/graph/controller/layout.ts index e05152e4556..42792b52b98 100644 --- a/packages/core/src/graph/controller/layout.ts +++ b/packages/core/src/graph/controller/layout.ts @@ -66,12 +66,12 @@ export default abstract class LayoutController { // 绘制 public refreshLayout() { - const { graph } = this; + const { graph, layoutType, data } = this; if (!graph) return; if (graph.get('animate')) { - graph.positionsAnimate(); + graph.positionsAnimate(layoutType === 'comboCombined'); } else { - graph.refreshPositions(); + graph.refreshPositions(layoutType === 'comboCombined'); } } @@ -154,7 +154,7 @@ export default abstract class LayoutController { if (comboItem.destroyed) continue; const model = comboItem.getModel(); if (!comboItem.isVisible()) { - hiddenEdges.push(model); + hiddenCombos.push(model); continue; } combos.push(model); @@ -214,13 +214,15 @@ export default abstract class LayoutController { start = start.then(() => this.reLayoutMethod(layoutMethod, currentCfg)); }); - start - .then(() => { - if (layoutCfg.onAllLayoutEnd) layoutCfg.onAllLayoutEnd(); - }) - .catch((error) => { - console.warn('relayout failed', error); - }); + if (layoutMethods?.length) { + start + .then(() => { + if (layoutCfg.onAllLayoutEnd) layoutCfg.onAllLayoutEnd(); + }) + .catch((error) => { + console.warn('relayout failed', error); + }); + } } // 筛选参与布局的nodes和edges @@ -279,7 +281,7 @@ export default abstract class LayoutController { // 控制布局动画 // eslint-disable-next-line class-methods-use-this - public layoutAnimate() { } + public layoutAnimate() {} // 将当前节点的平均中心移动到原点 public moveToZero() { diff --git a/packages/core/src/graph/graph.ts b/packages/core/src/graph/graph.ts index 45930b15175..b1da52b0466 100644 --- a/packages/core/src/graph/graph.ts +++ b/packages/core/src/graph/graph.ts @@ -613,7 +613,6 @@ export default abstract class AbstractGraph extends EventEmitter implements IAbs }); } else { matrix = transform(matrix, [['t', dx, dy]]); - group.setMatrix(matrix); this.emit('viewportchange', { action: 'translate', matrix }); @@ -1005,7 +1004,7 @@ export default abstract class AbstractGraph extends EventEmitter implements IAbs if (isString(item)) nodeItem = this.findById(item as string); if (!nodeItem && isString(item)) { - console.warn('The item to be removed does not exist!'); + console.warn(`The item ${item} to be removed does not exist!`); } else if (nodeItem) { let type = ''; if ((nodeItem as Item).getType) type = (nodeItem as Item).getType(); @@ -1095,8 +1094,7 @@ export default abstract class AbstractGraph extends EventEmitter implements IAbs } let item; - let comboTrees = this.get('comboTrees'); - if (!comboTrees) comboTrees = []; + const comboTrees = this.get('comboTrees') || []; if (type === 'combo') { const itemMap = this.get('itemMap'); let foundParent = false; @@ -1148,7 +1146,7 @@ export default abstract class AbstractGraph extends EventEmitter implements IAbs const itemMap = this.get('itemMap'); let foundParent = false, foundNode = false; - (comboTrees || []).forEach((ctree: ComboTree) => { + comboTrees.forEach((ctree: ComboTree) => { if (foundNode || foundParent) return; // terminate the forEach traverseTreeUp(ctree, child => { if (child.id === model.id) { @@ -1381,8 +1379,8 @@ export default abstract class AbstractGraph extends EventEmitter implements IAbs }); // process the data to tree structure - if (combos && combos.length !== 0) { - const comboTrees = plainCombosToTrees(combos, nodes); + if (combos?.length !== 0) { + const comboTrees = plainCombosToTrees((combos as ComboConfig[]), (nodes as NodeConfig[])); this.set('comboTrees', comboTrees); // add combos self.addCombos(combos); @@ -1725,9 +1723,9 @@ export default abstract class AbstractGraph extends EventEmitter implements IAbs if (subtree.id === comboId) { treeToBeUncombo = subtree; // delete the related edges - const edges = comboItem.getEdges(); - edges.forEach(edge => { - this.removeItem(edge, false); + const edgeIds = comboItem.getEdges().map(edge => edge.getID()); + edgeIds.forEach(edgeId => { + this.removeItem(edgeId, false); }); const index = comboItems.indexOf(comboItem); comboItems.splice(index, 1); @@ -1783,9 +1781,10 @@ export default abstract class AbstractGraph extends EventEmitter implements IAbs } /** - * 根据节点的 bbox 更新所有 combos 的绘制,包括 combos 的位置和范围 + * 根据 combo 位置更新内部节点位置 followCombo = true + * 或根据内部元素的 bbox 更新所有 combos 的绘制,包括 combos 的位置和范围,followCombo = false */ - public updateCombos() { + public updateCombos(followCombo: boolean = false) { const self = this; const comboTrees = this.get('comboTrees'); const itemController: ItemController = self.get('itemController'); @@ -1803,7 +1802,7 @@ export default abstract class AbstractGraph extends EventEmitter implements IAbs each(states, state => this.setItemState(childItem, state, false)); // 更新具体的 Combo - itemController.updateCombo(childItem, child.children); + itemController.updateCombo(childItem, child.children, followCombo); // 更新 Combo 后,还原已有的状态 each(states, state => this.setItemState(childItem, state, true)); @@ -2113,7 +2112,7 @@ export default abstract class AbstractGraph extends EventEmitter implements IAbs /** * 根据 graph 上的 animateCfg 进行视图中节点位置动画接口 */ - public positionsAnimate(): void { + public positionsAnimate(referComboModel?: boolean): void { const self = this; self.emit('beforeanimate'); @@ -2121,7 +2120,7 @@ export default abstract class AbstractGraph extends EventEmitter implements IAbs const { onFrame } = animateCfg; - const nodes = self.getNodes(); + const nodes = referComboModel ? self.getNodes().concat(self.getCombos()) : self.getNodes(); const toNodes = nodes.map(node => { const model = node.getModel(); @@ -2151,26 +2150,31 @@ export default abstract class AbstractGraph extends EventEmitter implements IAbs const model: NodeConfig = node.get('model'); - if (!originAttrs) { - let containerMatrix = node.getContainer().getMatrix(); - if (!containerMatrix) containerMatrix = [1, 0, 0, 0, 1, 0, 0, 0, 1]; - originAttrs = { + let containerMatrix = node.getContainer().getMatrix(); + + if (originAttrs === undefined) { + // 变换前存在位置,设置到 originAttrs 上。否则标记 0 表示变换前不存在位置,不需要计算动画 + node.set('originAttrs', containerMatrix ? { x: containerMatrix[6], y: containerMatrix[7], - }; - node.set('originAttrs', originAttrs); + } : 0); } if (onFrame) { - const attrs = onFrame(node, ratio, data, originAttrs); + const attrs = onFrame(node, ratio, data, originAttrs || { x: 0, y: 0 }); node.set('model', Object.assign(model, attrs)); - } else { + } else if (originAttrs) { + // 变换前存在位置,进行动画 model.x = originAttrs.x + (data.x - originAttrs.x) * ratio; model.y = originAttrs.y + (data.y - originAttrs.y) * ratio; + } else { + // 若在变换前不存在位置信息,则直接放到最终位置上 + model.x = data.x; + model.y = data.y; } }); - self.refreshPositions(); + self.refreshPositions(referComboModel); }, { duration: animateCfg.duration, @@ -2193,7 +2197,7 @@ export default abstract class AbstractGraph extends EventEmitter implements IAbs /** * 当节点位置在外部发生改变时,刷新所有节点位置,重计算边 */ - public refreshPositions() { + public refreshPositions(referComboModel?: boolean) { const self = this; self.emit('beforegraphrefreshposition'); @@ -2206,19 +2210,26 @@ export default abstract class AbstractGraph extends EventEmitter implements IAbs const updatedNodes: { [key: string]: boolean } = {}; - each(nodes, (node: INode) => { - model = node.getModel() as NodeConfig; - const originAttrs = node.get('originAttrs'); - if (originAttrs && model.x === originAttrs.x && model.y === originAttrs.y) { - return; - } - const changed = node.updatePosition({ x: model.x!, y: model.y! }); - updatedNodes[model.id] = changed; - if (model.comboId) updatedNodes[model.comboId] = updatedNodes[model.comboId] || changed; - }); + const updateItems = (items) => { + each(items, (item: INode) => { + model = item.getModel() as NodeConfig; + const originAttrs = item.get('originAttrs'); + if (originAttrs && model.x === originAttrs.x && model.y === originAttrs.y) { + return; + } + const changed = item.updatePosition({ x: model.x!, y: model.y! }); + updatedNodes[model.id] = changed; + if (model.comboId) updatedNodes[model.comboId] = updatedNodes[model.comboId] || changed; + }); + } + + updateItems(nodes); if (combos && combos.length !== 0) { self.updateCombos(); + if (referComboModel) { + updateItems(combos); + } } each(edges, (edge: IEdge) => { @@ -2398,6 +2409,7 @@ export default abstract class AbstractGraph extends EventEmitter implements IAbs * @param {string | ICombo} combo combo ID 或 combo item */ public collapseCombo(combo: string | ICombo): void { + if (this.destroyed) return; if (isString(combo)) { combo = this.findById(combo) as ICombo; } diff --git a/packages/core/src/interface/graph.ts b/packages/core/src/interface/graph.ts index 138a3de8c22..5ed4e27537a 100644 --- a/packages/core/src/interface/graph.ts +++ b/packages/core/src/interface/graph.ts @@ -280,12 +280,12 @@ export interface IAbstractGraph extends EventEmitter { /** * 根据 graph 上的 animateCfg 进行视图中节点位置动画接口 */ - positionsAnimate: () => void; + positionsAnimate: (updateCombo?: boolean) => void; /** * 当节点位置在外部发生改变时,刷新所有节点位置,重计算边 */ - refreshPositions: () => void; + refreshPositions: (referComboModel?: boolean) => void; /** * 根据data接口的数据渲染视图 diff --git a/packages/core/src/item/edge.ts b/packages/core/src/item/edge.ts index 2579e4ffd47..4bcc8d3e449 100644 --- a/packages/core/src/item/edge.ts +++ b/packages/core/src/item/edge.ts @@ -138,8 +138,8 @@ export default class Edge extends Item implements IEdge { public getShapeCfg(model: EdgeConfig, updateType?: UpdateType): EdgeConfig { const self = this; const linkCenter: boolean = self.get('linkCenter'); // 如果连接到中心,忽视锚点、忽视控制点 - - const cfg = updateType?.includes('move') ? model : super.getShapeCfg(model) as EdgeConfig; + + const cfg = updateType?.includes('move') ? model : super.getShapeCfg(model) as EdgeConfig; if (linkCenter) { cfg.startPoint = self.getEndCenter('source'); @@ -151,7 +151,7 @@ export default class Edge extends Item implements IEdge { } cfg.sourceNode = self.get('sourceNode'); cfg.targetNode = self.get('targetNode'); - + return cfg; } diff --git a/packages/core/src/item/item.ts b/packages/core/src/item/item.ts index 461f41e346d..1a2e212b207 100644 --- a/packages/core/src/item/item.ts +++ b/packages/core/src/item/item.ts @@ -662,7 +662,7 @@ export default class ItemBase implements IItemBase { const model = this.get('model'); const shape = model.type; // 判定是否允许更新 - // 1. 注册的节点允许更新 + // 1. 注册的节点允许更新(即有继承的/复写的 update 方法,即 update 方法没有被复写为 undefined) // 2. 更新后的 shape 等于原先的 shape if (shapeFactory.shouldUpdate(shape) && shape === this.get('currentShape')) { const updateCfg = this.getShapeCfg(model, updateType); @@ -726,7 +726,7 @@ export default class ItemBase implements IItemBase { * @return {Object} 包含 x,y,width,height, centerX, centerY */ public getCanvasBBox(): IBBox { - // 计算 bbox 开销有些大,缓存 + // 计算 bbox 开销大,缓存 let bbox: IBBox = this.get(CACHE_CANVAS_BBOX); if (!bbox) { bbox = this.calculateCanvasBBox(); diff --git a/packages/core/src/util/graphic.ts b/packages/core/src/util/graphic.ts index a0c196b201e..b46275abb32 100644 --- a/packages/core/src/util/graphic.ts +++ b/packages/core/src/util/graphic.ts @@ -517,10 +517,10 @@ export const reconstructTree = ( } traverseTree(tree, (child: any) => { comboChildsMap[child.id] = { - children: child.children, + children: child?.children || [], }; // store the old parent id to delete the subtree from the old parent's children in next recursion - brothers = comboChildsMap[child.parentId || child.comboId || 'root'].children; + brothers = comboChildsMap[child.parentId || child.comboId || 'root']?.children; if (child && (child.removed || subtreeId === child.id) && brothers) { oldParentId = child.parentId || child.comboId || 'root'; subtree = child; @@ -537,7 +537,7 @@ export const reconstructTree = ( }); }); - brothers = comboChildsMap[oldParentId].children; + brothers = comboChildsMap[oldParentId]?.children; const index = brothers ? brothers.indexOf(subtree) : -1; if (index > -1) brothers.splice(index, 1); diff --git a/packages/core/tests/unit/shape/combo-collapsed-pos-spec.ts b/packages/core/tests/unit/shape/combo-collapsed-pos-spec.ts new file mode 100644 index 00000000000..81d7205d30c --- /dev/null +++ b/packages/core/tests/unit/shape/combo-collapsed-pos-spec.ts @@ -0,0 +1,1346 @@ +import { clone, groupBy } from '@antv/util'; +import Graph from '../implement-graph'; + +const div = document.createElement('div'); +div.id = 'combo-shape'; +document.body.appendChild(div); +const graphCfg = { container: div, width: 500, height: 600, groupByTypes: false, }; + +describe('simple data', () => { + const simpleData = { + nodes: [ + { id: '0', x: 50, y: 20, comboId: 'a' }, + { id: '1', x: 100, y: 20, comboId: 'a' }, + { id: '2', x: 150, y: 30, comboId: 'b' }, + { id: '3', x: 200, y: 30, comboId: 'b' } + ], combos: [ + { id: 'a', label: 'a', x: 100, y: 400 }, + // initially collapsed + { id: 'b', label: 'b-initially-collapsed', collapsed: true, x: 400, y: 400 }, + // empty + { id: 'c', label: 'c-empty', x: 300, y: 300 } + ] + } + simpleData.nodes.forEach(node => { + node.label = node.id + }); + let graph = new Graph(graphCfg); + graph.read(clone(simpleData)); + + // 加几个测试标记圆点 + const group = graph.getGroup(); + const poses = [{ x: 100, y: 400 }, { x: 400, y: 400 }, { x: 300, y: 300 }]; + poses.forEach(pos => { + group.addShape('circle', { + attrs: { r: 3, x: pos.x, y: pos.y, fill: '#f00' } + }); + group.addShape('text', { + attrs: { text: `(${pos.x}, ${pos.y})`, fill: '#666', fontSize: 10, x: pos.x, y: pos.y + 14, textAlign: 'center' } + }); + }) + it('render: combos and nodes with pos', () => { + const comboModels = graph.getCombos().map(combo => combo.getModel()); + expect(comboModels[0].x).toBe(100); + expect(comboModels[0].y).toBe(400); + expect(comboModels[1].x).toBe(400); + expect(comboModels[1].y).toBe(400); + expect(comboModels[2].x).toBe(300); + expect(comboModels[2].y).toBe(300); + const nodeModels = graph.getNodes().map(node => node.getModel()); + expect(nodeModels[0].x).toBe(100 - 75 + 50); + expect(nodeModels[0].y).toBe(400 - 20 + 20); + expect(nodeModels[1].x).toBe(100 - 75 + 100); + expect(nodeModels[1].y).toBe(400 - 20 + 20); + expect(nodeModels[2].x).toBe(400 - 175 + 150); + expect(nodeModels[2].y).toBe(400 - 30 + 30); + expect(nodeModels[3].x).toBe(400 - 175 + 200); + expect(nodeModels[3].y).toBe(400 - 30 + 30); + }); + it('render: combos without pos, nodes with pos', () => { + graph.destroy(); + const testData = clone(simpleData); + testData.combos.forEach(combo => { + delete combo.x; + delete combo.y; + }); + graph = new Graph(graphCfg); + graph.read(testData); + + const comboModels = graph.getCombos().map(combo => combo.getModel()); + expect(comboModels[0].x).toBe(75); + expect(comboModels[0].y).toBe(20); + expect(comboModels[1].x).toBe(175); + expect(comboModels[1].y).toBe(30); + expect(comboModels[2].x).not.toBe(undefined); + expect(comboModels[2].y).not.toBe(undefined); + const nodeModels = graph.getNodes().map(node => node.getModel()); + expect(nodeModels[0].x).toBe(50); + expect(nodeModels[0].y).toBe(20); + expect(nodeModels[1].x).toBe(100); + expect(nodeModels[1].y).toBe(20); + expect(nodeModels[2].x).toBe(150); + expect(nodeModels[2].y).toBe(30); + expect(nodeModels[3].x).toBe(200); + expect(nodeModels[3].y).toBe(30); + }); + it('updateItem: combos from no pos update to pos', () => { + const testData = clone(simpleData); + testData.combos.forEach(combo => { + delete combo.x; + delete combo.y; + }); + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + graph.updateItem('a', { ...poses[0] }) + graph.updateItem('b', { ...poses[1] }) + graph.updateItem('c', { ...poses[2] }) + + const comboModels = graph.getCombos().map(combo => combo.getModel()); + expect(comboModels[0].x).toBe(100); + expect(comboModels[0].y).toBe(400); + expect(comboModels[1].x).toBe(400); + expect(comboModels[1].y).toBe(400); + expect(comboModels[2].x).toBe(300); + expect(comboModels[2].y).toBe(300); + const nodeModels = graph.getNodes().map(node => node.getModel()); + expect(nodeModels[0].x).toBe(100 - 75 + 50); + expect(nodeModels[0].y).toBe(400 - 20 + 20); + expect(nodeModels[1].x).toBe(100 - 75 + 100); + expect(nodeModels[1].y).toBe(400 - 20 + 20); + expect(nodeModels[2].x).toBe(400 - 175 + 150); + expect(nodeModels[2].y).toBe(400 - 30 + 30); + expect(nodeModels[3].x).toBe(400 - 175 + 200); + expect(nodeModels[3].y).toBe(400 - 30 + 30); + }); + it('updateItem: update the nodes inside expanded/collapsed combo', () => { + const nodes = graph.getNodes(); + const newNodePoses = [{ x: 50, y: 50 }, { x: 150, y: 150 }, { x: 450, y: 450 }, { x: 450, y: 400 }]; + graph.updateItem(nodes[0], newNodePoses[0]); + graph.updateItem(nodes[1], newNodePoses[1]); + graph.updateItem(nodes[2], newNodePoses[2]); + graph.updateItem(nodes[3], newNodePoses[3]); + // 上面手动更新单个节点是不会触发相关 combo 更新的,需要调用下面方法。但是下面方法是根据数据绘制的位置,如何控制根据内部元素位置还是 combo 数据位置 + // 是否加参数,内部更新 combo 或渲染 combo 时才用 combo 数据位置 + graph.updateCombos(); + + const expectComboPoses = [ + { + x: (newNodePoses[0].x + newNodePoses[1].x) / 2, + y: (newNodePoses[0].y + newNodePoses[1].y) / 2, + }, + { + x: (newNodePoses[2].x + newNodePoses[3].x) / 2, + y: (newNodePoses[2].y + newNodePoses[3].y) / 2, + } + ] + + const comboModels = graph.getCombos().map(combo => combo.getModel()); + expect(comboModels[0].x).toBe(expectComboPoses[0].x); + expect(comboModels[0].y).toBe(expectComboPoses[0].y); + expect(comboModels[1].x).toBe(expectComboPoses[1].x); + expect(comboModels[1].y).toBe(expectComboPoses[1].y); + }); + it('1 initial collapsed with pos, 2 expand, 3 move combo, 4 move node, 5 collapse, 6 move combo, 7 move node', (done) => { + graph.destroy(); + const testData = clone(simpleData); + graph = new Graph(graphCfg); + graph.read(testData); + + // initial collapsed with pos + const combo = graph.findById('b'); + const model = combo.getModel(); + expect(model.x).toBe(400); + expect(model.y).toBe(400); + + setTimeout(() => { + // 2 epxand + graph.collapseExpandCombo('b'); + expect(model.x).toBe(400); + expect(model.y).toBe(400); + // 3 move combo + graph.updateItem(combo, { x: 200, y: 100 }); + expect(model.x).toBe(200); + expect(model.y).toBe(100); + // 4 move node + const node = graph.findById('2'); + graph.updateItem(node, { x: 300, y: 200 }); + graph.updateCombos(); + expect(node.getModel().x).toBe(300); + expect(node.getModel().y).toBe(200); + expect(model.x).toBe(262.5); + expect(model.y).toBe(150); + setTimeout(() => { + expect(Math.abs(combo.getKeyShape().attr('r') - 102) < 1).toBe(true); + // 5 collapse + graph.collapseExpandCombo('b'); + expect(model.x).toBe(262.5); + expect(model.y).toBe(150); + setTimeout(() => { + expect(Math.abs(combo.getKeyShape().attr('r') - 35) < 1).toBe(true); + // 6 move combo + graph.updateItem(combo, { x: 400, y: 50 }); + expect(model.x).toBe(400); + expect(model.y).toBe(50); + expect(Math.abs(combo.getKeyShape().attr('r') - 35) < 1).toBe(true); + // 7 move node + const node = graph.findById('3'); + graph.updateItem(node, { x: 150, y: 400 }); + graph.updateCombos(); + expect(node.getModel().x).toBe(150); + expect(node.getModel().y).toBe(400); + // 收起状态下移动内部节点,combo 不会更新位置,因为计算 bbox 的时候忽略了隐藏元素 + expect(model.x).toBe(400); + expect(model.y).toBe(50); + graph.collapseExpandCombo('b'); + setTimeout(() => { + expect(model.x).toBe(293.75); + expect(model.y).toBe(250); + done(); + }, 500) + }, 500) + }, 500) + }, 500) + }); + it('1 initial collapsed without pos, 2 expand, 3 move combo, 4 move node, 5 collapse, 6 move combo, 7 move node', (done) => { + const testData = clone(simpleData); + testData.combos.forEach(combo => { + delete combo.x; + delete combo.y; + }); + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + // initial collapsed with pos + const combo = graph.findById('b'); + const model = combo.getModel(); + expect(model.x).toBe(175); + expect(model.y).toBe(30); + + setTimeout(() => { + // 2 epxand + graph.collapseExpandCombo('b'); + setTimeout(() => { + expect(model.x).toBe(175); + expect(model.y).toBe(30); + // 3 move combo + graph.updateItem(combo, { x: 200, y: 100 }); + expect(model.x).toBe(200); + expect(model.y).toBe(100); + // 4 move node + const node = graph.findById('2'); + graph.updateItem(node, { x: 300, y: 200 }); + graph.updateCombos(); + expect(node.getModel().x).toBe(300); + expect(node.getModel().y).toBe(200); + expect(model.x).toBe(262.5); + expect(model.y).toBe(150); + setTimeout(() => { + expect(Math.abs(combo.getKeyShape().attr('r') - 102) < 1).toBe(true); + // 5 collapse + graph.collapseExpandCombo('b'); + setTimeout(() => { + expect(model.x).toBe(262.5); + expect(model.y).toBe(150); + expect(Math.abs(combo.getKeyShape().attr('r') - 35) < 1).toBe(true); + // 6 7 与上一个 it 内容相同,不再重复 + done(); + }, 500); + }, 500); + }, 500); + }, 500); + }); + it('1 initial expand with pos, 2 move node, 3 move combo, 4 collapse, 5 move node, 6 expand', (done) => { + graph.destroy(); + const testData = clone(simpleData); + graph = new Graph(graphCfg); + graph.read(testData); + + // 1 initial expand with pos + const combo = graph.findById('a'); + const model = combo.getModel(); + expect(model.x).toBe(100); + expect(model.y).toBe(400); + + // 2 move node + const node = graph.findById('1'); + const node1Model = node.getModel(); + const node0Model = graph.findById('0').getModel(); + graph.updateItem(node, { x: 100, y: 100 }); + graph.updateCombos(); + expect(node.getModel().x).toBe(100); + expect(node.getModel().y).toBe(100); + expect(model.x).toBe(87.5); + expect(model.y).toBe(250); + setTimeout(() => { + expect(Math.abs(combo.getKeyShape().attr('r') - 187) < 1).toBe(true); + // 3 move combo + graph.updateItem(combo, { x: 300, y: 400 }); + expect(model.x).toBe(300); + expect(model.y).toBe(400); + expect(node1Model.x).toBe(300 - 87.5 + 100); + expect(node1Model.y).toBe(400 - 250 + 100); + expect(node0Model.x).toBe(287.5); + expect(node0Model.y).toBe(550); + // 4 collapse + graph.collapseExpandCombo('a'); + setTimeout(() => { + expect(Math.abs(combo.getKeyShape().attr('r') - 35) < 1).toBe(true); + expect(model.x).toBe(300); + expect(model.y).toBe(400); + // 5 move node, collapse 状态下移动节点,combo 位置不变 + graph.updateItem('0', { x: 50, y: 50 }); + expect(node0Model.x).toBe(50); + expect(node0Model.y).toBe(50); + expect(model.x).toBe(300); + expect(model.y).toBe(400); + // 6 expand + graph.collapseExpandCombo('a'); + setTimeout(() => { + expect(Math.abs(combo.getKeyShape().attr('r') - 204) < 1).toBe(true); + expect(model.x).toBe(181.25); + expect(model.y).toBe(150); + done(); + }, 500) + }, 500) + }, 500); + }); + it('1 initial expand without pos, 2 move node, 3 move combo, 4 collapse, 5 move node, 6 expand', (done) => { + const testData = clone(simpleData); + testData.combos.forEach(combo => { + delete combo.x; + delete combo.y; + }); + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + // 1 initial collapsed without pos + const combo = graph.findById('a'); + const model = combo.getModel(); + expect(model.x).toBe(75); + expect(model.y).toBe(20); + + setTimeout(() => { + // 2 move node + const node = graph.findById('1'); + const node1Model = node.getModel(); + const node0Model = graph.findById('0').getModel(); + graph.updateItem(node, { x: 300, y: 200 }); + graph.updateCombos(); + expect(node1Model.x).toBe(300); + expect(node1Model.y).toBe(200); + expect(model.x).toBe(175); + expect(model.y).toBe(110); + setTimeout(() => { + expect(Math.abs(combo.getKeyShape().attr('r') - 193) < 1).toBe(true); + // 3 move combo + graph.updateItem(combo, { x: 50, y: 350 }); + expect(model.x).toBe(50); + expect(model.y).toBe(350); + expect(node1Model.x).toBe(50 - 175 + 300); + expect(node1Model.y).toBe(350 - 110 + 200); + expect(node0Model.x).toBe(-75); + expect(node0Model.y).toBe(260); + // 4 collapse + graph.collapseExpandCombo('a'); + setTimeout(() => { + expect(Math.abs(combo.getKeyShape().attr('r') - 35) < 1).toBe(true); + expect(model.x).toBe(50); + expect(model.y).toBe(350); + // 5 expand + graph.collapseExpandCombo('a'); + setTimeout(() => { + expect(Math.abs(combo.getKeyShape().attr('r') - 193) < 1).toBe(true); + expect(model.x).toBe(50); + expect(model.y).toBe(350); + graph.destroy(); + done(); + }, 500); + }, 500); + }, 500); + }, 500); + }); +}); + +describe('hierarchy data 1: combo A has one child: an empty combo B', () => { + const data = { + combos: [ + { id: 'A', x: 100, y: 200, label: 'A' }, + { id: 'B', x: 300, y: 400, label: 'B', parentId: 'A' }, + ] + } + let graph = new Graph(graphCfg); + graph.read(clone(data)); + let comboA = graph.findById('A'); + let comboAModel = comboA.getModel(); + let comboB = graph.findById('B'); + let comboBModel = comboB.getModel(); + it('nested combo has different initial pos', () => { + expect(comboAModel.x).toBe(100); + expect(comboAModel.y).toBe(200); + // the child combo follow the parent + expect(comboBModel.x).toBe(100); + expect(comboBModel.y).toBe(200); + }); + it('move child empty combo B', (done) => { + graph.updateItem('B', { x: 330, y: 120 }); + expect(comboBModel.x).toBe(330); + expect(comboBModel.y).toBe(120); + // the parent combo follow the child + graph.updateCombos(); + expect(comboAModel.x).toBe(330); + expect(comboAModel.y).toBe(120); + // collpase combo B + graph.collapseExpandCombo('B'); + setTimeout(() => { + // move combo B + graph.updateItem('B', { x: 430, y: 200 }); + expect(comboBModel.x).toBe(430); + expect(comboBModel.y).toBe(200); + // the parrent follow the child + graph.updateCombos(); + expect(comboAModel.x).toBe(430); + expect(comboAModel.y).toBe(200); + done() + }, 500); + }); + it('move parent combo A', (done) => { + graph.updateItem('A', { x: 50, y: 50 }); + expect(comboAModel.x).toBe(50); + expect(comboAModel.y).toBe(50); + // the child follow the parent + expect(comboBModel.x).toBe(50); + expect(comboBModel.y).toBe(50); + // expand B + graph.collapseExpandCombo('B'); + setTimeout(() => { + // no changes + expect(comboAModel.x).toBe(50); + expect(comboAModel.y).toBe(50); + expect(comboBModel.x).toBe(50); + expect(comboBModel.y).toBe(50); + done(); + }, 500); + }); + it('parent combo without pos, child combo with pos', () => { + const testData = clone(data); + delete testData.combos[0].x; + delete testData.combos[0].y; + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + comboA = graph.findById('A'); + comboAModel = comboA.getModel(); + comboB = graph.findById('B'); + comboBModel = comboB.getModel(); + + expect(comboBModel.x).toBe(300); + expect(comboBModel.y).toBe(400); + // A has no position, follows the child + expect(comboAModel.x).toBe(300); + expect(comboAModel.y).toBe(400); + + // move B + graph.updateItem('B', { x: 300, y: 100 }); + expect(comboBModel.x).toBe(300); + expect(comboBModel.y).toBe(100); + // A follows the child + graph.updateCombos(); + expect(comboAModel.x).toBe(300); + expect(comboAModel.y).toBe(100); + + // move A + graph.updateItem('A', { x: 400, y: 120 }); + expect(comboBModel.x).toBe(400); + expect(comboBModel.y).toBe(120); + // B follows the parent + expect(comboAModel.x).toBe(400); + expect(comboAModel.y).toBe(120); + }); + it('parent combo with pos, child combo without', () => { + const testData = clone(data); + delete testData.combos[1].x; + delete testData.combos[1].y; + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + comboA = graph.findById('A'); + comboAModel = comboA.getModel(); + comboB = graph.findById('B'); + comboBModel = comboB.getModel(); + + expect(comboBModel.x).toBe(100); + expect(comboBModel.y).toBe(200); + // A has no position, follows the child + expect(comboAModel.x).toBe(100); + expect(comboAModel.y).toBe(200); + }); + it('parent combo collapsed with pos, child combo with pos', (done) => { + const testData = clone(data); + testData.combos[0].collapsed = true; + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + comboA = graph.findById('A'); + comboAModel = comboA.getModel(); + comboB = graph.findById('B'); + comboBModel = comboB.getModel(); + + expect(comboBModel.x).toBe(100); + expect(comboBModel.y).toBe(200); + // A has no position, follows the child + expect(comboAModel.x).toBe(100); + expect(comboAModel.y).toBe(200); + + // move A + graph.updateItem('A', { x: 300, y: 350 }); + expect(comboAModel.x).toBe(300); + expect(comboAModel.y).toBe(350); + // child follows A + expect(comboBModel.x).toBe(300); + expect(comboBModel.y).toBe(350); + + // expand A + graph.collapseExpandCombo('A'); + setTimeout(() => { + expect(comboAModel.x).toBe(300); + expect(comboAModel.y).toBe(350); + // child follows A + expect(comboBModel.x).toBe(300); + expect(comboBModel.y).toBe(350); + graph.destroy(); + done() + }, 500); + }); +}); + +describe('hierarchy data 2: combo A has 2 children: an empty combo B, a node', () => { + const data = { + nodes: [{ id: '0', x: 10, y: 20, comboId: 'A' }], + combos: [ + { id: 'A', x: 100, y: 200, label: 'A' }, + { id: 'B', x: 300, y: 400, label: 'B', parentId: 'A' }, + ] + } + let graph = new Graph(graphCfg); + graph.read(clone(data)); + let comboA = graph.findById('A'); + let comboAModel = comboA.getModel(); + let comboB = graph.findById('B'); + let comboBModel = comboB.getModel(); + let node = graph.findById('0'); + let nodeModel = node.getModel(); + + it('nested combo has different initial pos', () => { + expect(comboAModel.x).toBe(100); + expect(comboAModel.y).toBe(200); + // the child combo follow the parent + expect(comboBModel.x).toBe(232.75); + expect(comboBModel.y).toBe(377.75); + expect(nodeModel.x).toBe(-57.25); + expect(nodeModel.y).toBe(-2.25); + }); + it('move child empty combo B', () => { + graph.updateItem('B', { x: 330, y: 120 }); + expect(comboBModel.x).toBe(330); + expect(comboBModel.y).toBe(120); + // the sibling node is not changed + expect(nodeModel.x).toBe(-57.25); + expect(nodeModel.y).toBe(-2.25); + // the parent combo follows the children + graph.updateCombos(); + expect(comboAModel.x).toBe(148.625); + expect(comboAModel.y).toBe(71.125); + }); + it('move parent combo A', () => { // done + graph.updateItem('A', { x: 450, y: 350 }); + expect(comboAModel.x).toBe(450); + expect(comboAModel.y).toBe(350); + // the child follow the parent + expect(comboBModel.x).toBe(450 - 148.625 + 330); + expect(comboBModel.y).toBe(350 - 71.125 + 120); + expect(nodeModel.x).toBe(450 - 148.625 - 57.25); + expect(nodeModel.y).toBe(350 - 71.125 - 2.25); + }); + it('parent combo without pos, children with pos', () => { + const testData = clone(data); + delete testData.combos[0].x; + delete testData.combos[0].y; + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + comboA = graph.findById('A'); + comboAModel = comboA.getModel(); + comboB = graph.findById('B'); + comboBModel = comboB.getModel(); + node = graph.findById('0'); + nodeModel = node.getModel(); + + expect(comboBModel.x).toBe(300); + expect(comboBModel.y).toBe(400); + expect(nodeModel.x).toBe(10); + expect(nodeModel.y).toBe(20); + // A has no position, follows the child + expect(comboAModel.x).toBe(167.25); + expect(comboAModel.y).toBe(222.25); + }); + it('parent combo with pos, child combo without pos', () => { + const testData = clone(data); + delete testData.combos[1].x; + delete testData.combos[1].y; + delete testData.nodes[0].x; + delete testData.nodes[0].y; + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + comboA = graph.findById('A'); + comboAModel = comboA.getModel(); + comboB = graph.findById('B'); + comboBModel = comboB.getModel(); + node = graph.findById('0'); + nodeModel = node.getModel(); + + expect(comboAModel.x).toBe(100); + expect(comboAModel.y).toBe(200); + // B and 0 have no position, follow the parent + // the position is randomed, but inside the parent parent + expect(comboBModel.x).not.toBe(NaN); + expect(comboBModel.y).not.toBe(NaN); + expect(nodeModel.x).not.toBe(NaN); + expect(nodeModel.y).not.toBe(NaN); + }); + it('parent combo collapsed with pos, child combo with pos', () => { + const testData = clone(data); + testData.combos[0].collapsed = true; + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + comboA = graph.findById('A'); + comboAModel = comboA.getModel(); + comboB = graph.findById('B'); + comboBModel = comboB.getModel(); + + expect(comboAModel.x).toBe(100); + expect(comboAModel.y).toBe(200); + expect(comboBModel.x).not.toBe(NaN); + expect(comboBModel.y).not.toBe(NaN); + expect(nodeModel.x).not.toBe(NaN); + expect(nodeModel.y).not.toBe(NaN); + graph.destroy(); + }); +}); + + +describe('hierarchy data3: combo A has 2 children: combo B with 2 nodes, 2 nodes', () => { + const data = { + nodes: [ + { id: '0', x: 140, y: 120, comboId: 'A' }, + { id: '1', x: 150, y: 130, comboId: 'A' }, + { id: '2', x: 180, y: 220, comboId: 'B' }, + { id: '3', x: 190, y: 230, comboId: 'B' } + ], + combos: [ + { id: 'A', x: 100, y: 200, label: 'A' }, + { id: 'B', x: 300, y: 400, label: 'B', parentId: 'A' }, + ] + } + data.nodes.forEach(node => node.label = node.id); + let graph = new Graph(graphCfg); + graph.read(clone(data)); + let comboA = graph.findById('A'); + let comboAModel = comboA.getModel(); + let comboB = graph.findById('B'); + let comboBModel = comboB.getModel(); + let nodes = graph.getNodes(); + let nodeModels = nodes.map(node => node.getModel()); + + it('nested combo has different initial pos', () => { + expect(comboAModel.x).toBe(100); + expect(comboAModel.y).toBe(200); + // the child combo follow the parent + expect(comboBModel.x).toBe(161.7898448916085); + expect(comboBModel.y).toBe(321.7898448916085); + expect(nodeModels[0].x).toBe(1.7898448916085101); + expect(nodeModels[0].y).toBe(41.78984489160848); + expect(nodeModels[1].x).toBe(11.78984489160851); + expect(nodeModels[1].y).toBe(51.78984489160848); + expect(nodeModels[2].x).toBe(156.7898448916085); + expect(nodeModels[2].y).toBe(316.7898448916085); + expect(nodeModels[3].x).toBe(166.7898448916085); + expect(nodeModels[3].y).toBe(326.7898448916085); + }); + it('move child combo B', () => { + graph.updateItem('B', { x: 330, y: 120 }); + graph.updateCombos(); + expect(comboBModel.x).toBe(330); + expect(comboBModel.y).toBe(120); + // the sibling node is not changed + expect(nodeModels[0].x).toBe(1.7898448916085101); + expect(nodeModels[0].y).toBe(41.78984489160848); + expect(nodeModels[1].x).toBe(11.78984489160851); + expect(nodeModels[1].y).toBe(51.78984489160848); + // the children nodes of combo B follow B + expect(nodeModels[2].x).toBe(325); + expect(nodeModels[2].y).toBe(115); + expect(nodeModels[3].x).toBe(335); + expect(nodeModels[3].y).toBe(125); + + // the parent combo follows the children + expect(comboAModel.x).toBe(184.10507755419576); + expect(comboAModel.y).toBe(99.10507755419573); + }); + it('move parent combo A', (done) => { + graph.updateItem('A', { x: 150, y: 150 }); + expect(comboAModel.x).toBe(150); + expect(comboAModel.y).toBe(150); + // the child follow the parent + const dx = 150 - 184.10507755419576, dy = 150 - 99.10507755419573; + expect(comboBModel.x).toBe(dx + 330); + expect(comboBModel.y).toBe(dy + 120); + expect(nodeModels[0].x).toBe(dx + 1.7898448916085101); + expect(nodeModels[0].y).toBe(dy + 41.78984489160848); + expect(nodeModels[1].x).toBe(dx + 11.78984489160851); + expect(nodeModels[1].y).toBe(dy + 51.78984489160848); + expect(nodeModels[2].x).toBe(dx + 325); + expect(nodeModels[2].y).toBe(dy + 115); + expect(nodeModels[3].x).toBe(dx + 335); + expect(nodeModels[3].y).toBe(dy + 125); + // collapse B + graph.collapseExpandCombo('B'); + setTimeout(() => { + // move B + graph.updateItem('B', { x: 50, y: 50 }); + expect(comboBModel.x).toBe(50); + expect(comboBModel.y).toBe(50); + graph.updateCombos(); + setTimeout(() => { + expect(Math.abs(comboA.getKeyShape().attr('r') - 105) < 1).toBe(true); + expect(comboAModel.x).toBe(21.092383668706375); + expect(comboAModel.y).toBe(64.09238366870638); + done(); + }, 500); + }, 500); + }); + it('parent combo without pos, children with pos', () => { + const testData = clone(data); + delete testData.combos[0].x; + delete testData.combos[0].y; + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + comboA = graph.findById('A'); + comboAModel = comboA.getModel(); + comboB = graph.findById('B'); + comboBModel = comboB.getModel(); + nodes = graph.getNodes(); + nodeModels = nodes.map(node => node.getModel()); + + expect(comboBModel.x).toBe(300); + expect(comboBModel.y).toBe(400); + expect(nodeModels[0].x).toBe(data.nodes[0].x); + expect(nodeModels[0].y).toBe(data.nodes[0].y); + expect(nodeModels[1].x).toBe(data.nodes[1].x); + expect(nodeModels[1].y).toBe(data.nodes[1].y); + // 2 3 follows B + expect(nodeModels[2].x).toBe(295); + expect(nodeModels[2].y).toBe(395); + expect(nodeModels[3].x).toBe(305); + expect(nodeModels[3].y).toBe(405); + // A has no position, follows the child + expect(comboAModel.x).toBe(238.2101551083915); + expect(comboAModel.y).toBe(278.2101551083915); + }); + it('parent combo with pos, child combo without pos', () => { + const testData = clone(data); + delete testData.combos[1].x; + delete testData.combos[1].y; + testData.nodes.forEach(node => { + delete node.x; + delete node.y; + }); + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + comboA = graph.findById('A'); + comboAModel = comboA.getModel(); + comboB = graph.findById('B'); + comboBModel = comboB.getModel(); + nodes = graph.getNodes(); + nodeModels = nodes.map(node => node.getModel()); + + expect(comboAModel.x).toBe(100); + expect(comboAModel.y).toBe(200); + // B and nodes have no position, follow the parent + expect(comboBModel.x).toBe(100); + expect(comboBModel.y).toBe(200); + nodeModels.forEach(nodeModel => { + expect(nodeModel.x).toBe(100); + expect(nodeModel.y).toBe(200); + }); + // move node 3 + graph.updateItem('3', { x: 300, y: 400 }); + expect(nodeModels[3].x).toBe(300); + expect(nodeModels[3].y).toBe(400); + graph.updateCombos(); + // B and A follow the change + expect(comboBModel.x).toBe((100 + 300) / 2); + expect(comboBModel.y).toBe((200 + 400) / 2); + expect(comboAModel.x).toBe((100 + 300) / 2); + expect(comboAModel.y).toBe((200 + 400) / 2); + }); + it('parent combo collapsed with pos, child combo with pos', (done) => { + const testData = clone(data); + testData.combos[0].collapsed = true; + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + comboA = graph.findById('A'); + comboAModel = comboA.getModel(); + comboB = graph.findById('B'); + comboBModel = comboB.getModel(); + nodes = graph.getNodes(); + nodeModels = nodes.map(node => node.getModel()); + + expect(comboAModel.x).toBe(100); + expect(comboAModel.y).toBe(200); + setTimeout(() => { + // expand A + graph.collapseExpandCombo('A'); + setTimeout(() => { + expect(comboB.isVisible()).toBe(true); + // the child combo follow the parent + expect(comboBModel.x).toBe(161.7898448916085); + expect(comboBModel.y).toBe(321.7898448916085); + expect(nodeModels[0].x).toBe(1.7898448916085101); + expect(nodeModels[0].y).toBe(41.78984489160848); + expect(nodeModels[1].x).toBe(11.78984489160851); + expect(nodeModels[1].y).toBe(51.78984489160848); + expect(nodeModels[2].x).toBe(156.7898448916085); + expect(nodeModels[2].y).toBe(316.7898448916085); + expect(nodeModels[3].x).toBe(166.7898448916085); + expect(nodeModels[3].y).toBe(326.7898448916085); + graph.destroy(); + done() + }, 500) + }, 500); + }); +}); + +describe('hierarchy data4: combo A has 2 children: combo B with 2 nodes, 2 nodes', () => { + const data = { + combos: [ + { id: 'A', x: 100, y: 200, label: 'A' }, + { id: 'B', x: 300, y: 400, label: 'B', parentId: 'A' }, + { id: 'C', x: 150, y: 100, label: 'C', parentId: 'A' }, + ] + } + let graph = new Graph(graphCfg); + graph.read(clone(data)); + let combos = [graph.findById('A'), graph.findById('B'), graph.findById('C')]; // graph.getCombos 返回的不是数据顺序 + let comboModels = combos.map(combo => combo.getModel()); + + it('nested combo has different initial pos', () => { + expect(comboModels[0].x).toBe(100); + expect(comboModels[0].y).toBe(200); + // the child combo follow the parent + expect(comboModels[1].x).toBe(175); + expect(comboModels[1].y).toBe(350); + expect(comboModels[2].x).toBe(25); + expect(comboModels[2].y).toBe(50); + }); + it('move child combo B', () => { + graph.updateItem('B', { x: 130, y: 120 }); + graph.updateCombos(); + expect(comboModels[1].x).toBe(130); + expect(comboModels[1].y).toBe(120); + // the sibling node is not changed + expect(comboModels[2].x).toBe(25); + expect(comboModels[2].y).toBe(50); + // the parent combo follows the children + expect(comboModels[0].x).toBe(77.5); + expect(comboModels[0].y).toBe(85); + expect(Math.abs(combos[0].getKeyShape().attr('r') - 240) < 1).toBe(true); + }); + it('move parent combo A', () => { + graph.updateItem('A', { x: 150, y: 150 }); + expect(comboModels[0].x).toBe(150); + expect(comboModels[0].y).toBe(150); + // the child follow the parent + const dx = 150 - 77.5, dy = 150 - 85; + expect(comboModels[1].x).toBe(dx + 130); + expect(comboModels[1].y).toBe(dy + 120); + expect(comboModels[2].x).toBe(dx + 25); + expect(comboModels[2].y).toBe(dy + 50); + }); + it('parent combo without pos, children with pos', () => { + const testData = clone(data); + delete testData.combos[0].x; + delete testData.combos[0].y; + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + combos = [graph.findById('A'), graph.findById('B'), graph.findById('C')]; + comboModels = combos.map(combo => combo.getModel()); + + expect(comboModels[1].x).toBe(300); + expect(comboModels[1].y).toBe(400); + expect(comboModels[2].x).toBe(150); + expect(comboModels[2].y).toBe(100); + // A has no position, follows the child + expect(comboModels[0].x).toBe((300 + 150) / 2); + expect(comboModels[0].y).toBe((400 + 100) / 2); + }); + it('parent combo with pos, child combo without pos', () => { + const testData = clone(data); + delete testData.combos[1].x; + delete testData.combos[1].y; + delete testData.combos[2].x; + delete testData.combos[2].y; + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + combos = [graph.findById('A'), graph.findById('B'), graph.findById('C')]; + comboModels = combos.map(combo => combo.getModel()); + + expect(comboModels[0].x).toBe(100); + expect(comboModels[0].y).toBe(200); + expect(comboModels[1].x).not.toBe(NaN); + expect(comboModels[1].y).not.toBe(NaN); + expect(comboModels[2].x).not.toBe(NaN); + expect(comboModels[2].y).not.toBe(NaN); + // move node B and C + graph.updateItem('B', { x: 300, y: 400 }); + graph.updateItem('C', { x: 200, y: 400 }); + expect(comboModels[1].x).toBe(300); + expect(comboModels[1].y).toBe(400); + expect(comboModels[2].x).toBe(200); + expect(comboModels[2].y).toBe(400); + graph.updateCombos(); + expect(comboModels[0].x).toBe((300 + 200) / 2); + expect(comboModels[0].y).toBe((400 + 400) / 2); + }); + it('parent combo collapsed with pos, child combo with pos', (done) => { + const testData = clone(data); + testData.combos[0].collapsed = true; + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + combos = [graph.findById('A'), graph.findById('B'), graph.findById('C')]; + comboModels = combos.map(combo => combo.getModel()); + + expect(comboModels[0].x).toBe(100); + expect(comboModels[0].y).toBe(200); + setTimeout(() => { + // expand A + graph.collapseExpandCombo('A'); + setTimeout(() => { + expect(combos[1].isVisible()).toBe(true); + expect(combos[2].isVisible()).toBe(true); + // the child combo follow the parent + expect(comboModels[1].x).toBe(175); + expect(comboModels[1].y).toBe(350); + expect(comboModels[2].x).toBe(25); + expect(comboModels[2].y).toBe(50); + graph.destroy(); + done(); + }, 500) + }, 500); + }); +}); + + +describe('hierarchy data5: combo A has 2 children: combo B with 2 nodes, 2 nodes', () => { + const data = { + nodes: [ + { id: '0', x: 50, y: 50, label: '0', comboId: 'B' }, + { id: '1', x: 150, y: 50, label: '1', comboId: 'B' }, + ], + combos: [ + { id: 'A', x: 100, y: 200, label: 'A' }, + { id: 'B', x: 300, y: 400, label: 'B', parentId: 'A' }, + { id: 'C', x: 250, y: 300, label: 'C', parentId: 'A' }, + ] + } + let graph = new Graph(graphCfg); + graph.read(clone(data)); + let combos = [graph.findById('A'), graph.findById('B'), graph.findById('C')]; // graph.getCombos 返回的不是数据顺序 + let comboModels = combos.map(combo => combo.getModel()); + let node0Model = graph.findById('0').getModel(); + let node1Model = graph.findById('1').getModel(); + + it('nested combo has different initial pos', () => { + expect(comboModels[0].x).toBe(100); + expect(comboModels[0].y).toBe(200); + // the child combo follow the parent + expect(comboModels[1].x).toBe(100); + expect(comboModels[1].y).toBe(224.29780138165995); + expect(comboModels[2].x).toBe(50); + expect(comboModels[2].y).toBe(124.29780138165995); + expect(node0Model.x).toBe(50); + expect(node0Model.y).toBe(224.29780138165995); + expect(node1Model.x).toBe(150); + expect(node1Model.y).toBe(224.29780138165995); + }); + it('move child combo B', (done) => { + graph.updateItem('B', { x: 130, y: 120 }); + graph.updateCombos(); + expect(comboModels[1].x).toBe(130); + expect(comboModels[1].y).toBe(120); + // the sibling node is not changed + expect(comboModels[2].x).toBe(50); + expect(comboModels[2].y).toBe(124.29780138165995); + // the parent combo follows the children + setTimeout(() => { + expect(comboModels[0].x).toBe(115.70219861834002); + expect(comboModels[0].y).toBe(120); + expect(Math.abs(combos[0].getKeyShape().attr('r') - 157) < 1).toBe(true); + done(); + }, 500) + }); + it('move parent combo A', () => { + graph.updateItem('A', { x: 150, y: 150 }); + expect(comboModels[0].x).toBe(150); + expect(comboModels[0].y).toBe(150); + // the child follow the parent + const dx = 150 - 115.70219861834002, dy = 150 - 120; + expect(comboModels[1].x).toBe(dx + 130); + expect(comboModels[1].y).toBe(dy + 120); + expect(comboModels[2].x).toBe(dx + 50); + expect(comboModels[2].y).toBe(dy + 124.29780138165995); + }); + it('parent combo without pos, children with pos', () => { + const testData = clone(data); + delete testData.combos[0].x; + delete testData.combos[0].y; + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + combos = [graph.findById('A'), graph.findById('B'), graph.findById('C')]; + comboModels = combos.map(combo => combo.getModel()); + + expect(comboModels[1].x).toBe(300); + expect(comboModels[1].y).toBe(400); + expect(comboModels[2].x).toBe(250); + expect(comboModels[2].y).toBe(300); + // A has no position, follows the child + expect(comboModels[0].x).toBe(300); + expect(comboModels[0].y).toBe(375.70219861834005); + }); + it('parent combo with pos, child combo without pos', () => { + const testData = clone(data); + delete testData.combos[1].x; + delete testData.combos[1].y; + delete testData.combos[2].x; + delete testData.combos[2].y; + testData.nodes.forEach(node => { + delete node.x; + delete node.y; + }) + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + combos = [graph.findById('A'), graph.findById('B'), graph.findById('C')]; + comboModels = combos.map(combo => combo.getModel()); + + expect(comboModels[0].x).toBe(100); + expect(comboModels[0].y).toBe(200); + expect(comboModels[1].x).not.toBe(NaN); + expect(comboModels[1].y).not.toBe(NaN); + expect(comboModels[2].x).not.toBe(NaN); + expect(comboModels[2].y).not.toBe(NaN); + // move node B and C + graph.updateItem('B', { x: 300, y: 400 }); + graph.updateItem('C', { x: 200, y: 400 }); + expect(comboModels[1].x).toBe(300); + expect(comboModels[1].y).toBe(400); + expect(comboModels[2].x).toBe(200); + expect(comboModels[2].y).toBe(400); + graph.updateCombos(); + expect(comboModels[0].x).toBe(252.42462120245875); + expect(comboModels[0].y).toBe(400); + }); + it('parent combo collapsed with pos, child combo with pos', (done) => { + const testData = clone(data); + testData.combos[0].collapsed = true; + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + combos = [graph.findById('A'), graph.findById('B'), graph.findById('C')]; + comboModels = combos.map(combo => combo.getModel()); + + expect(comboModels[0].x).toBe(100); + expect(comboModels[0].y).toBe(200); + setTimeout(() => { + // expand A + graph.collapseExpandCombo('A'); + setTimeout(() => { + expect(combos[1].isVisible()).toBe(true); + expect(combos[2].isVisible()).toBe(true); + // the child combo follow the parent + expect(comboModels[1].x).toBe(100); + expect(comboModels[1].y).toBe(224.29780138165995); + expect(comboModels[2].x).toBe(50); + expect(comboModels[2].y).toBe(124.29780138165995); + graph.destroy(); + done() + }, 500); + }, 500); + }); +}); + + +describe('hierarchy data6: combo A has 2 children: combo B with 2 nodes, 2 nodes', () => { + const data = { + combos: [ + { id: 'A', x: 100, y: 200, label: 'A' }, + { id: 'B', x: 300, y: 400, label: 'B', parentId: 'A' }, + { id: 'C', x: 400, y: 500, label: 'C', parentId: 'B' }, + ] + } + let graph = new Graph(graphCfg); + graph.read(clone(data)); + let combos = [graph.findById('A'), graph.findById('B'), graph.findById('C')]; // graph.getCombos 返回的不是数据顺序 + let comboModels = combos.map(combo => combo.getModel()); + + it('nested combo has different initial pos', () => { + expect(comboModels[0].x).toBe(100); + expect(comboModels[0].y).toBe(200); + // the child combo follow the parent + expect(comboModels[1].x).toBe(100); + expect(comboModels[1].y).toBe(200); + expect(comboModels[2].x).toBe(100); + expect(comboModels[2].y).toBe(200); + }); + it('move child combo B', (done) => { + graph.updateItem('B', { x: 130, y: 120 }); + graph.updateCombos(); + expect(comboModels[1].x).toBe(130); + expect(comboModels[1].y).toBe(120); + // child follows + expect(comboModels[2].x).toBe(130); + expect(comboModels[2].y).toBe(120); + // the parent combo follows the children + setTimeout(() => { + expect(comboModels[0].x).toBe(130); + expect(comboModels[0].y).toBe(120); + expect(Math.abs(combos[0].getKeyShape().attr('r') - 130) < 1).toBe(true); + done(); + }, 500) + }); + it('move parent combo A', () => { + graph.updateItem('A', { x: 150, y: 150 }); + expect(comboModels[0].x).toBe(150); + expect(comboModels[0].y).toBe(150); + // the child follow the parent + expect(comboModels[1].x).toBe(150); + expect(comboModels[1].y).toBe(150); + expect(comboModels[2].x).toBe(150); + expect(comboModels[2].y).toBe(150); + }); + it('parent combo without pos, B and C has position', () => { + const testData = clone(data); + delete testData.combos[0].x; + delete testData.combos[0].y; + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + combos = [graph.findById('A'), graph.findById('B'), graph.findById('C')]; + comboModels = combos.map(combo => combo.getModel()); + + // follow B + expect(comboModels[1].x).toBe(300); + expect(comboModels[1].y).toBe(400); + expect(comboModels[2].x).toBe(300); + expect(comboModels[2].y).toBe(400); + // A has no position, follows the child + expect(comboModels[0].x).toBe(300); + expect(comboModels[0].y).toBe(400); + }); + it('A and B combo without pos, C has position', () => { + const testData = clone(data); + delete testData.combos[0].x; + delete testData.combos[0].y; + delete testData.combos[1].x; + delete testData.combos[1].y; + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + combos = [graph.findById('A'), graph.findById('B'), graph.findById('C')]; + comboModels = combos.map(combo => combo.getModel()); + + // follow C + expect(comboModels[1].x).toBe(400); + expect(comboModels[1].y).toBe(500); + expect(comboModels[2].x).toBe(400); + expect(comboModels[2].y).toBe(500); + // A has no position, follows the child + expect(comboModels[0].x).toBe(400); + expect(comboModels[0].y).toBe(500); + }); + it('parent combo collapsed with pos, child combo with pos', (done) => { + const testData = clone(data); + testData.combos[0].collapsed = true; + graph.destroy(); + graph = new Graph(graphCfg); + graph.read(testData); + + combos = [graph.findById('A'), graph.findById('B'), graph.findById('C')]; + comboModels = combos.map(combo => combo.getModel()); + + expect(comboModels[0].x).toBe(100); + expect(comboModels[0].y).toBe(200); + setTimeout(() => { + // expand A + graph.collapseExpandCombo('A'); + setTimeout(() => { + expect(combos[1].isVisible()).toBe(true); + expect(combos[2].isVisible()).toBe(true); + // the child combo follow the parent + expect(comboModels[1].x).toBe(100); + expect(comboModels[1].y).toBe(200); + expect(comboModels[2].x).toBe(100); + expect(comboModels[2].y).toBe(200); + graph.destroy(); + done(); + }, 500); + }, 500); + }); +}); + +describe('placea grid combo and nodes', () => { + const data = { + nodes: [ + { + id: '0', + comboId: 'a', + }, + { + id: '1', + comboId: 'a', + }, + { + id: '2', + comboId: 'b', + }, + { + id: '3', + comboId: 'b', + }, + { + id: '4', + comboId: 'b', + }, + { + id: '5', + comboId: 'c', + }, + { + id: '6', + comboId: 'c', + }, + ], + edges: [ + { + source: '0', + target: '1', + }, + { + source: '1', + target: '2', + }, + { + source: '0', + target: '3', + }, + { + source: '0', + target: '4', + }, + { + source: '6', + target: '5', + } + ], + combos: [ + { + id: 'a', + label: 'Combo A', + }, + { + id: 'b', + label: 'Combo B', + parentId: 'a' + }, + { + id: 'c', + label: 'Combo C', + style: { + fill: '#f00', + opacity: 0.4 + } + }, + { + id: 'd', + label: 'empty D' + } + ] + } + let graph = new Graph({ + ...graphCfg, + defaultCombo: { + type: 'rect' + } + }); + it('grid', () => { + const testData = clone(data); + const groupNodes = groupBy(testData.nodes, node => node.comboId); + Object.keys(groupNodes).forEach(key => { + groupNodes[key].forEach((node, i) => { + node.x = i * 50; + node.y = 0; + }); + }); + testData.combos[0].x = 250; + testData.combos[0].y = 250; + + testData.combos[1].x = 0; + testData.combos[1].y = 100; + + testData.combos[2].x = 250; + testData.combos[2].y = 450; + + testData.combos[3].x = 250; + testData.combos[3].y = 550; + + graph.read(testData); + + const combos = testData.combos.map(cdata => graph.findById(cdata.id)); + const comboModels = combos.map(combo => combo.getModel()); + + expect(comboModels[2].x).toBe(comboModels[0].x); + expect(comboModels[2].x).toBe(comboModels[1].x); + expect(comboModels[2].x).toBe(comboModels[3].x); + + graph.destroy(); + }) +}); \ No newline at end of file diff --git a/packages/element/package.json b/packages/element/package.json index c9415af8f4c..6c1e661e639 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g6-element", - "version": "0.5.5", + "version": "0.6.0", "description": "A Graph Visualization Framework in JavaScript", "keywords": [ "antv", @@ -61,7 +61,7 @@ }, "dependencies": { "@antv/g-base": "^0.5.1", - "@antv/g6-core": "*", + "@antv/g6-core": "^0.6.0", "@antv/util": "~2.0.5" }, "devDependencies": { @@ -89,4 +89,4 @@ "typescript": "^3.9.5", "@antv/g6": "4.5.1" } -} \ No newline at end of file +} diff --git a/packages/g6/package.json b/packages/g6/package.json index c2958f37469..3e3ab5e6c40 100644 --- a/packages/g6/package.json +++ b/packages/g6/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g6", - "version": "4.5.5", + "version": "4.6.0", "description": "A Graph Visualization Framework in JavaScript", "keywords": [ "antv", @@ -66,7 +66,7 @@ ] }, "dependencies": { - "@antv/g6-pc": "*" + "@antv/g6-pc": "^0.6.0" }, "devDependencies": { "@babel/core": "^7.7.7", @@ -90,4 +90,4 @@ "webpack": "^4.41.4", "webpack-cli": "^3.3.10" } -} \ No newline at end of file +} diff --git a/packages/g6/src/index.ts b/packages/g6/src/index.ts index 5edab8d4b31..417638389d0 100644 --- a/packages/g6/src/index.ts +++ b/packages/g6/src/index.ts @@ -1,7 +1,7 @@ import G6 from '@antv/g6-pc'; -G6.version = '4.5.5'; +G6.version = '4.6.0'; export * from '@antv/g6-pc'; export default G6; -export const version = '4.5.5'; +export const version = '4.6.0'; diff --git a/packages/pc/package.json b/packages/pc/package.json index 4867948b8a8..617b154b600 100644 --- a/packages/pc/package.json +++ b/packages/pc/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g6-pc", - "version": "0.5.5", + "version": "0.6.0", "description": "A Graph Visualization Framework in JavaScript", "keywords": [ "antv", @@ -68,18 +68,18 @@ }, "dependencies": { "@ant-design/colors": "^4.0.5", + "@antv/algorithm": "^0.1.8", "@antv/dom-util": "^2.0.1", "@antv/event-emitter": "~0.1.0", "@antv/g-base": "^0.5.1", "@antv/g-canvas": "^0.5.2", "@antv/g-math": "^0.1.1", "@antv/g-svg": "^0.5.1", - "@antv/g6-core": "*", - "@antv/g6-plugin": "*", - "@antv/g6-element": "*", - "@antv/algorithm": "^0.1.8", + "@antv/g6-core": "^0.6.0", + "@antv/g6-element": "^0.6.0", + "@antv/g6-plugin": "^0.6.0", "@antv/hierarchy": "^0.6.7", - "@antv/layout": "^0.1.22", + "@antv/layout": "^0.2.0", "@antv/matrix-util": "^3.1.0-beta.3", "@antv/path-util": "^2.0.3", "@antv/util": "~2.0.5", @@ -124,4 +124,4 @@ "worker-loader": "^3.0.0", "stats-js": "1.0.1" } -} \ No newline at end of file +} diff --git a/packages/pc/src/global.ts b/packages/pc/src/global.ts index bc0c0dd05ed..aafc35bc36f 100644 --- a/packages/pc/src/global.ts +++ b/packages/pc/src/global.ts @@ -7,7 +7,7 @@ const textColor = 'rgb(0, 0, 0)'; const colorSet = getColorsWithSubjectColor(subjectColor, backColor); export default { - version: '0.5.5', + version: '0.6.0', rootContainerClassName: 'root-container', nodeContainerClassName: 'node-container', edgeContainerClassName: 'edge-container', diff --git a/packages/pc/src/graph/controller/layout.ts b/packages/pc/src/graph/controller/layout.ts index c983ab16283..e78c5f3b789 100644 --- a/packages/pc/src/graph/controller/layout.ts +++ b/packages/pc/src/graph/controller/layout.ts @@ -139,7 +139,7 @@ export default class LayoutController extends AbstractLayout { graph.refreshPositions(); }; layoutCfg.tick = tick; - } else if (layoutCfg.type === 'comboForce') { + } else if (layoutType === 'comboForce' || layoutType === 'comboCombined') { layoutCfg.comboTrees = graph.get('comboTrees'); } @@ -282,23 +282,28 @@ export default class LayoutController extends AbstractLayout { } let start = Promise.resolve(); + let hasLayout = false; if (layoutCfg.type) { + hasLayout = true; start = start.then(async () => await this.execLayoutMethod(layoutCfg, 0)); } else if (layoutCfg.pipes) { + hasLayout = true; layoutCfg.pipes.forEach((cfg, index) => { start = start.then(async () => await this.execLayoutMethod(cfg, index)); }); } - // 最后统一在外部调用onAllLayoutEnd - start.then(() => { - if (layoutCfg.onAllLayoutEnd) layoutCfg.onAllLayoutEnd(); - // 在执行 execute 后立即执行 success,且在 timeBar 中有 throttle,可以防止 timeBar 监听 afterrender 进行 changeData 后 layout,从而死循环 - // 对于 force 一类布局完成后的 fitView 需要用户自己在 onLayoutEnd 中配置 - if (success) success(); - }).catch((error) => { - console.warn('graph layout failed,', error); - }); + if (hasLayout) { + // 最后统一在外部调用onAllLayoutEnd + start.then(() => { + if (layoutCfg.onAllLayoutEnd) layoutCfg.onAllLayoutEnd(); + // 在执行 execute 后立即执行 success,且在 timeBar 中有 throttle,可以防止 timeBar 监听 afterrender 进行 changeData 后 layout,从而死循环 + // 对于 force 一类布局完成后的 fitView 需要用户自己在 onLayoutEnd 中配置 + if (success) success(); + }).catch((error) => { + console.warn('graph layout failed,', error); + }); + } return false; } @@ -326,20 +331,25 @@ export default class LayoutController extends AbstractLayout { graph.emit('beforelayout'); let start = Promise.resolve(); + let hasLayout = false; if (layoutCfg.type) { + hasLayout = true; start = start.then(() => this.runWebworker(worker, data, layoutCfg)); } else if (layoutCfg.pipes) { + hasLayout = true; for (const cfg of layoutCfg.pipes) { start = start.then(() => this.runWebworker(worker, data, cfg)); } } - // 最后统一在外部调用onAllLayoutEnd - start.then(() => { - if (layoutCfg.onAllLayoutEnd) layoutCfg.onAllLayoutEnd(); - }).catch((error) => { - console.error('layout failed', error); - }); + if (hasLayout) { + // 最后统一在外部调用onAllLayoutEnd + start.then(() => { + if (layoutCfg.onAllLayoutEnd) layoutCfg.onAllLayoutEnd(); + }).catch((error) => { + console.error('layout failed', error); + }); + } return true; } @@ -479,20 +489,25 @@ export default class LayoutController extends AbstractLayout { graph.emit('beforelayout'); let start = Promise.resolve(); + let hasLayout = false; if (layoutMethods.length === 1) { + hasLayout = true; start = start.then(async () => await this.updateLayoutMethod(layoutMethods[0], layoutCfg)); } else { + hasLayout = true; layoutMethods?.forEach((layoutMethod, index) => { const currentCfg = layoutCfg.pipes[index]; - start = start.then(async() => await this.updateLayoutMethod(layoutMethod, currentCfg)); + start = start.then(async () => await this.updateLayoutMethod(layoutMethod, currentCfg)); }); } - start.then(() => { - if (layoutCfg.onAllLayoutEnd) layoutCfg.onAllLayoutEnd(); - }).catch((error) => { - console.warn('layout failed', error); - }); + if (hasLayout) { + start.then(() => { + if (layoutCfg.onAllLayoutEnd) layoutCfg.onAllLayoutEnd(); + }).catch((error) => { + console.warn('layout failed', error); + }); + } } protected adjustPipesBox(data, adjust: string): Promise { @@ -503,7 +518,7 @@ export default class LayoutController extends AbstractLayout { } if (!LAYOUT_PIPES_ADJUST_NAMES.includes(adjust)) { - console.warn(`The adjust type ${adjust} is not supported yet, please assign it with 'force', 'grid', or 'circular'.` ); + console.warn(`The adjust type ${adjust} is not supported yet, please assign it with 'force', 'grid', or 'circular'.`); resolve(); } diff --git a/packages/pc/src/layout/index.ts b/packages/pc/src/layout/index.ts index 9f5eb85ccb4..3ed27122604 100644 --- a/packages/pc/src/layout/index.ts +++ b/packages/pc/src/layout/index.ts @@ -6,6 +6,7 @@ import { ForceLayout, CircularLayout, DagreLayout, + DagreCompoundLayout, RadialLayout, ConcentricLayout, MDSLayout, @@ -14,8 +15,9 @@ import { GForceLayout, GForceGPULayout, ComboForceLayout, - ForceAtlas2Layout -} from '@antv/layout'; + ComboCombinedLayout, + ForceAtlas2Layout, +} from '@antv/layout/lib'; import TreeLayout from './tree-layout'; @@ -24,6 +26,7 @@ oRegisterLayout('random', RandomLayout); oRegisterLayout('force', ForceLayout); oRegisterLayout('circular', CircularLayout); oRegisterLayout('dagre', DagreLayout); +oRegisterLayout('dagreCompound', DagreCompoundLayout); oRegisterLayout('radial', RadialLayout); oRegisterLayout('concentric', ConcentricLayout); oRegisterLayout('mds', MDSLayout); @@ -32,6 +35,7 @@ oRegisterLayout('fruchterman-gpu', FruchtermanGPULayout); oRegisterLayout('gForce', GForceLayout); oRegisterLayout('gForce-gpu', GForceGPULayout); oRegisterLayout('comboForce', ComboForceLayout); +oRegisterLayout('comboCombined', ComboCombinedLayout); oRegisterLayout('forceAtlas2', ForceAtlas2Layout); const registerLayout = (name: string, layoutOverride: any) => { diff --git a/packages/pc/src/layout/worker/layout.worker.ts b/packages/pc/src/layout/worker/layout.worker.ts index 54222aa8641..70f0367e8ee 100644 --- a/packages/pc/src/layout/worker/layout.worker.ts +++ b/packages/pc/src/layout/worker/layout.worker.ts @@ -9,7 +9,6 @@ export const LayoutWorker = ( workerScriptURL: string = 'https://unpkg.com/@antv/layout@latest/dist/layout.min.js', ) => { function workerCode() { - const LAYOUT_MESSAGE = { // run layout RUN: 'LAYOUT_RUN', @@ -34,6 +33,8 @@ export const LayoutWorker = ( // @ts-ignore layout.registerLayout('dagre', layout.DagreLayout); // @ts-ignore + layout.registerLayout('dagreCompound', layout.DagreCompoundLayout); + // @ts-ignore layout.registerLayout('radial', layout.RadialLayout); // @ts-ignore layout.registerLayout('concentric', layout.ConcentricLayout); @@ -50,6 +51,8 @@ export const LayoutWorker = ( // @ts-ignore layout.registerLayout('comboForce', layout.ComboForceLayout); // @ts-ignore + layout.registerLayout('comboCombined', layout.ComboCombinedLayout); + // @ts-ignore layout.registerLayout('forceAtlas2', layout.ForceAtlas2Layout); function isLayoutMessage(event: Event) { @@ -73,6 +76,7 @@ export const LayoutWorker = ( break; } + // eslint-disable-next-line prefer-const let layoutMethod; layoutCfg.onLayoutEnd = () => { this.postMessage({ type: LAYOUT_MESSAGE.END, nodes }); @@ -115,7 +119,7 @@ export const LayoutWorker = ( break; } } - onmessage = event => { + onmessage = (event) => { if (isLayoutMessage(event)) { handleLayoutMessage(event); } diff --git a/packages/pc/tests/unit/element/nodes/icon-iconfont-spec.ts b/packages/pc/tests/unit/element/nodes/icon-iconfont-spec.ts index 9a573fe0dec..5a5a6d3bd22 100644 --- a/packages/pc/tests/unit/element/nodes/icon-iconfont-spec.ts +++ b/packages/pc/tests/unit/element/nodes/icon-iconfont-spec.ts @@ -14,7 +14,7 @@ describe('icon with iconfont', () => { }, }; const graph = new Graph(cfg); - it('default circle config', () => { + it.only('default circle config', () => { const data = { nodes: [ { @@ -37,7 +37,6 @@ describe('icon with iconfont', () => { }) }) - graph.emit('canvas:click', {}); expect(graph.getNodes()[0].get('group').find(e => e.get('name') === 'circle-icon').attr('text')).toBe('xxx'); }); it('update iconfont node', () => { diff --git a/packages/pc/tests/unit/layout/combo-combined-spec.ts b/packages/pc/tests/unit/layout/combo-combined-spec.ts new file mode 100644 index 00000000000..1bcebe75738 --- /dev/null +++ b/packages/pc/tests/unit/layout/combo-combined-spec.ts @@ -0,0 +1,298 @@ +import G6 from '../../../src'; +import { clone } from '@antv/util'; +import dataset from './data'; +import { ConcentricLayout, GForceLayout, ForceAtlas2Layout } from '@antv/layout'; +import * as d3Force from 'd3-force'; +import { isFunction } from 'util'; + +const data = dataset.comboData; + +const div = document.createElement('div'); +div.id = 'force-layout'; +document.body.appendChild(div); + +describe('no node and one node', () => { + it('layout without node', (done) => { + const testData = {}; + const graph = new G6.Graph({ + container: div, + layout: { + type: 'comboCombined', + }, + width: 500, + height: 500, + }); + graph.data(testData); + graph.render(); + graph.destroy(); + done(); + }); + it('layout with one node', (done) => { + const testData = { + nodes: [ + { + id: 'node', + x: 0, + y: 0, + }, + ], + }; + const graph = new G6.Graph({ + container: div, + layout: { + type: 'comboCombined', + }, + width: 500, + height: 500, + }); + graph.on('afterlayout', () => { + expect(testData.nodes[0].x).toBe(250); + expect(testData.nodes[0].y).toBe(250); + done(); + }); + graph.data(testData); + graph.render(); + graph.destroy(); + }); +}); + +describe('scenario', () => { + const scenarioData = { + nodes: [...data.nodes], + edges: [...data.edges], + combos: [...data.combos] + }; + // 模拟 combo 节点和其他部分的边 + const comboNodeEdges = [ + { source: 'a', target: '14' }, + { source: 'a', target: '16' }, + { source: '13', target: 'a' }, + { source: 'b', target: '18' }, + { source: 'b', target: '19' }, + { source: 'b', target: '4' }, + { source: 'c', target: '30' }, + { source: 'c', target: '13' }, + { source: '15', target: 'c' }, + { source: 'c', target: 'c' }, + ] + const nodeMap = {}; + const oriItemComboMap = {}; + const oriComboItemMap = {}; + scenarioData.nodes.forEach(node => { + nodeMap[node.id] = node; + oriItemComboMap[node.id] = node.comboId; + if (!oriComboItemMap[node.comboId]) oriComboItemMap[node.comboId] = [] + oriComboItemMap[node.comboId].push(node.id); + // 移除 combo d + // if (node.id === '32' || node.id === '31' || node.id === '33') delete node.comboId + }); + scenarioData.combos.forEach((combo, i) => { + oriItemComboMap[combo.id] = combo.parentId; + if (!oriComboItemMap[combo.parentId]) oriComboItemMap[combo.parentId] = [] + oriComboItemMap[combo.parentId].push(combo.id); + // 移除 combo d + // if (combo.id === 'd') scenarioData.combos.splice(i, 1) + }) + // 删除跨越 combo 的边,替代为 comboEdges(无论边数量、方向,均用一条虚线边代替) + const comboEdgesMap = {}; + const removedEdges = []; + for (let i = scenarioData.edges.length - 1; i >= 0; i--) { + const edge = scenarioData.edges[i]; + delete edge.style; + const sourceParentId = nodeMap[edge.source]?.comboId || nodeMap[edge.source]?.parentId; + const targetParentId = nodeMap[edge.target]?.comboId || nodeMap[edge.target]?.parentId; + if (sourceParentId !== targetParentId) { + removedEdges.push(edge); + scenarioData.edges.splice(i, 1); + const key = edge.source < edge.target ? `${edge.source}-${edge.target}` : `${edge.target}-${edge.source}`; + comboEdgesMap[key] = { + source: sourceParentId, + target: targetParentId, + style: { + lineDash: [5, 5] + } + } + } + } + const comboEdges = Object.values(comboEdgesMap); + scenarioData.edges = scenarioData.edges.concat(comboEdges); + const graph = new G6.Graph({ + container: div, + layout: { + type: 'comboCombined', + innerLayout: new ConcentricLayout({ sortBy: 'degree' }) + }, + width: 500, + height: 500, + defaultCombo: { + padding: 1, + style: { + fillOpacity: 0.7 + }, + labelCfg: { + style: { + fill: '#fff' + } + } + }, + modes: { + default: ['drag-canvas', 'zoom-canvas', { + type: 'drag-combo', + onlyChangeComboSize: true + }, { + type: 'drag-node', + onlyChangeComboSize: true + }] // 'collapse-expand-combo', + }, + animate: true, + animateCfg: { duration: 500, easing: 'easeCubic' }, + groupByTypes: false + }); + graph.data(scenarioData); + graph.render(); + // 点击 combo 时,uncombo,并加入一个代表被解散 combo 的节点,恢复相关的边(包括内部元素到其他 combo 的边,以及新节点相关的边(random)) + graph.on('combo:click', e => { + const combo = e.item; + const comboChildren = combo.getChildren(); + const children = comboChildren.nodes.concat(comboChildren.combos); + const newEdges = []; + const comboId = combo.getID(); + + // 代替 combo 的节点 id + const comboNodeId = 'combo-node-' + comboId + const comboBBox = { ...combo.getBBox() }; + // 增加代替 combo 的节点 + graph.addItem('node', { + id: comboNodeId, + label: combo.getModel().label, + x: comboBBox.centerX, + y: comboBBox.minY, + comboId: combo.getModel().parentId + }); + // 恢复该节点和其他元素的连接 + comboNodeEdges.forEach(cEdge => { + const sourceModel = graph.findById(cEdge.source)?.getModel(); + const targetModel = graph.findById(cEdge.target)?.getModel(); + if (!sourceModel || !targetModel) return; + newEdges.push(graph.addItem('edge', { + source: cEdge.source === comboId ? comboNodeId : sourceModel.comboId || sourceModel.parentId || cEdge.source, + target: cEdge.target === comboId ? comboNodeId : targetModel.comboId || targetModel.parentId || cEdge.target, + })) + }); + + children.forEach((child, i) => { + const childId = child.getID(); + // 恢复所有子节点与其他元素的连接 + removedEdges.forEach(rEdge => { + const isSource = rEdge.source === childId; + const otherEndItem = isSource ? graph.findById(rEdge.target) : graph.findById(rEdge.source); + if (!otherEndItem) return; + const otherEndModel = otherEndItem.getModel(); + const otherEndCurrentParent = otherEndModel.comboId || otherEndModel.parentId; + newEdges.push(graph.addItem('edge', { + source: isSource ? childId : otherEndCurrentParent || rEdge.source, + target: isSource ? otherEndCurrentParent || rEdge.target : childId, + style: otherEndCurrentParent ? { + lineDash: [5, 5] + } : {} + })); + }); + }); + + console.log('====', graph.findById('test0'), graph.findById('test1')) + + // uncombo + // 增加动画 + combo.getKeyShape().animate({ y: -comboBBox.height / 2, r: 10 }, { duration: 300, easing: 'easeCubic' }); + setTimeout(() => { + graph.uncombo(combo.getID()); + graph.layout(); + }, 300); + + // 标记新增的边,用于观察 + console.log('newEdges.leng', newEdges.length); + newEdges.forEach(nEdge => { + if (nEdge.destroyed) return; + graph.updateItem(nEdge, { + style: { + stroke: '#f00' + } + }) + }) + }); + // graph.on('combo:dragend', e => { + // const model = e.item.getModel() + // model.fx = model.x; + // model.fy = model.y; + // graph.layout(); + // }) + + graph.on('node:click', e => { + const nodeId = e.item.getID(); + const oriComboId = oriItemComboMap[nodeId] + const meanCenter = { x: 0, y: 0, count: 0 }; + Object.keys(oriItemComboMap).forEach(nodeId => { + if (oriItemComboMap[nodeId] === oriComboId) { + const nodeModel = graph.findById(nodeId)?.getModel() + if (nodeModel) { + meanCenter.x += (nodeModel.x || 0); + meanCenter.y += (nodeModel.y || 0); + meanCenter.count++; + } + } + }); + meanCenter.x /= meanCenter.count; + meanCenter.y /= meanCenter.count; + + if (oriComboId && !e.item.getModel().comboId) { + console.log('add combo', oriComboId); + graph.addItem('combo', { + id: oriComboId, + label: oriComboId, + x: meanCenter.x, + y: meanCenter.y + }); + const childIds = oriComboItemMap[oriComboId]; + childIds.forEach(childId => { + console.log('update children', childId) + const childItem = graph.findById(childId); + if (!childItem) return; + graph.updateComboTree(childId, oriComboId); + }) + graph.removeItem('combo-node-' + oriComboId); + const edgeItems = graph.getEdges(); + const virtualEdgeToBeAdded = {} + for (let i = edgeItems.length - 1; i >= 0; i--) { + graph.getEdges().forEach(edge => { + const model = edge.getModel(); + const isRelated = childIds.includes(model.source) || childIds.includes(model.target); + const sourceRelated = isRelated && childIds.includes(model.source); + if (isRelated) { + const key = model.source < model.target ? `${model.source}-${model.target}` : `${model.target}-${model.source}`; + const sourceModel = graph.findById(model.source)?.getModel(); + const targetModel = graph.findById(model.target)?.getModel(); + if (!sourceModel || !targetModel) return; + const sourceParentId = sourceModel.comboId || sourceModel.parentId; + const targetParentId = targetModel.comboId || targetModel.parentId; + if (sourceParentId === targetParentId) return; + graph.removeItem(edge); + virtualEdgeToBeAdded[key] = { + source: sourceRelated ? oriComboId : sourceParentId || sourceModel.id, + target: sourceRelated ? targetParentId || targetModel.id : oriComboId, + style: { + lineDash: [5, 5] + } + } + } + }) + } + Object.values(virtualEdgeToBeAdded).map(edgeInfo => graph.addItem('edge', edgeInfo)); + setTimeout(() => { + graph.layout(); + }, 300); + } + }); + + it('senario', () => { + }); +}); \ No newline at end of file diff --git a/packages/pc/tests/unit/layout/dagre-spec.ts b/packages/pc/tests/unit/layout/dagre-spec.ts index bad3110979a..27b9826af29 100644 --- a/packages/pc/tests/unit/layout/dagre-spec.ts +++ b/packages/pc/tests/unit/layout/dagre-spec.ts @@ -297,7 +297,7 @@ describe('dagre layout with combo', () => { data2.nodes.forEach((node) => { node.label = node.id; }); - it('layout with one level combo', () => { + it('layout with one level combo', (done) => { const graph = new G6.Graph({ container: div, width: 500, @@ -327,14 +327,15 @@ describe('dagre layout with combo', () => { console.log(graph.findById('1-2').getModel()); console.log(graph.findById('1-1-1').getModel()); - expect(graph.findById('1').getModel().x).toBe(195); + expect(graph.findById('1').getModel().x).toBe(145); expect(graph.findById('1').getModel().y).toBe(21.5); expect(graph.findById('1-2').getModel().x).toBe(45); - expect(graph.findById('1-2').getModel().y).toBe(64.5); - expect(graph.findById('1-1-1').getModel().x).toBe(370); - expect(graph.findById('1-1-1').getModel().y).toBe(108); + expect(graph.findById('1-2').getModel().y).toBe(107.5); + expect(graph.findById('1-1-1').getModel().x).toBe(470); + expect(graph.findById('1-1-1').getModel().y).toBe(107.5); graph.destroy(); + done() }) }); diff --git a/packages/pc/tests/unit/layout/data/combo-test-data.ts b/packages/pc/tests/unit/layout/data/combo-test-data.ts index c575cc746d9..4e42e1b76d4 100644 --- a/packages/pc/tests/unit/layout/data/combo-test-data.ts +++ b/packages/pc/tests/unit/layout/data/combo-test-data.ts @@ -172,6 +172,19 @@ export default { }, ], edges: [ + { + source: 'a', + target: 'a', + size: 3, + style: { + stroke: 'red', + }, + type: 'loop', + loopCfg: { + position: 'top', + dist: 20, + }, + }, { source: 'a', target: 'b', diff --git a/packages/plugin/package.json b/packages/plugin/package.json index fadd0c44ae1..164b9657098 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g6-plugin", - "version": "0.5.5", + "version": "0.6.0", "description": "G6 Plugin", "main": "lib/index.js", "module": "es/index.js", @@ -22,7 +22,7 @@ "@antv/g-base": "^0.5.1", "@antv/g-canvas": "^0.5.2", "@antv/g-svg": "^0.5.2", - "@antv/g6-core": "*", + "@antv/g6-core": "^0.6.0", "@antv/matrix-util": "^3.1.0-beta.3", "@antv/scale": "^0.3.4", "@antv/util": "^2.0.9", @@ -59,4 +59,4 @@ "ts-jest": "^26.4.4", "@antv/g6": "4.5.1" } -} \ No newline at end of file +} diff --git a/packages/plugin/src/minimap/index.ts b/packages/plugin/src/minimap/index.ts index 944804ecf91..24ba3b1a940 100644 --- a/packages/plugin/src/minimap/index.ts +++ b/packages/plugin/src/minimap/index.ts @@ -314,8 +314,8 @@ export default class MiniMap extends Base { if (combos && combos.length) { const comboGroup = group.find(e => e.get('name') === 'comboGroup') || group.addGroup({ - name: 'comboGroup' - }); + name: 'comboGroup' + }); setTimeout(() => { if (this.destroyed) return; each(combos, (combo) => { @@ -424,8 +424,8 @@ export default class MiniMap extends Base { if (combos && combos.length) { const comboGroup = group.find(e => e.get('name') === 'comboGroup') || group.addGroup({ - name: 'comboGroup' - }); + name: 'comboGroup' + }); setTimeout(() => { if (this.destroyed) return; each(combos, (combo) => { @@ -717,9 +717,9 @@ export default class MiniMap extends Base { } public destroy() { - this.get('canvas').destroy(); + this.get('canvas')?.destroy(); const container = this.get('container'); - container.parentNode.removeChild(container); + if (container?.parentNode) container.parentNode.removeChild(container); } } diff --git a/packages/site/examples/case/treeDemos/demo/indentedTree.js b/packages/site/examples/case/treeDemos/demo/indentedTree.js index aba8d3aa570..89e61f42195 100644 --- a/packages/site/examples/case/treeDemos/demo/indentedTree.js +++ b/packages/site/examples/case/treeDemos/demo/indentedTree.js @@ -1477,8 +1477,6 @@ const tree = new G6.TreeGraph({ container: 'container', width: 800, height: 800, - fitView: true, - fitViewPadding: [10, 20], layout: { type: 'indented', direction: 'LR', @@ -1528,7 +1526,6 @@ const tree = new G6.TreeGraph({ }); tree.on('afterrender', e => { - console.log('after') tree.getEdges().forEach(edge => { const targetNode = edge.getTarget().getModel(); const color = targetNode.branchColor; @@ -1536,6 +1533,7 @@ tree.on('afterrender', e => { }); setTimeout(() => { tree.moveTo(32, 32); + tree.zoomTo(0.7) }, 16); });