From b30018c23dbc79d38cc5321e64cf509eecd5c3ea Mon Sep 17 00:00:00 2001 From: Yuxin <55794321+yvonneyx@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:38:02 +0800 Subject: [PATCH] fix: avoid unnecessary redraws for parallel edges (#6406) * refactor: avoid triggering redraw for parallel edges when unnecessary * fix: update isStyleEqual * docs: add parallel edges remarks * fix: update snapshots --- .../demos/transform-map-node-size.ts | 2 +- .../demos/transform-process-parallel-edges.ts | 1 - .../src/transforms/collapse-expand-combo.ts | 1 - packages/g6/src/transforms/map-node-size.ts | 10 +- .../src/transforms/process-parallel-edges.ts | 91 ++++++++++--------- packages/g6/src/transforms/utils.ts | 12 +++ packages/g6/src/utils/transform.ts | 32 ------- 7 files changed, 67 insertions(+), 82 deletions(-) diff --git a/packages/g6/__tests__/demos/transform-map-node-size.ts b/packages/g6/__tests__/demos/transform-map-node-size.ts index 87fe267c8ec..abbd734504a 100644 --- a/packages/g6/__tests__/demos/transform-map-node-size.ts +++ b/packages/g6/__tests__/demos/transform-map-node-size.ts @@ -32,7 +32,7 @@ export const transformMapNodeSize: TestCase = async (context) => { await graph.render(); - const config = { 'centrality.type': 'eigenvector', mapLabelSize: false }; + const config = { 'centrality.type': 'degree', mapLabelSize: false }; transformMapNodeSize.form = (panel) => [ panel diff --git a/packages/g6/__tests__/demos/transform-process-parallel-edges.ts b/packages/g6/__tests__/demos/transform-process-parallel-edges.ts index ef1b4475b0e..1abe42e4aa0 100644 --- a/packages/g6/__tests__/demos/transform-process-parallel-edges.ts +++ b/packages/g6/__tests__/demos/transform-process-parallel-edges.ts @@ -11,7 +11,6 @@ export const transformProcessParallelEdges: TestCase = async (context) => { { type: 'hover-activate', key: 'hover-activate', - enable: (event: any) => event.targetType === 'edge', }, ], transforms: [ diff --git a/packages/g6/src/transforms/collapse-expand-combo.ts b/packages/g6/src/transforms/collapse-expand-combo.ts index e229fe17f37..4e4c4f34e72 100644 --- a/packages/g6/src/transforms/collapse-expand-combo.ts +++ b/packages/g6/src/transforms/collapse-expand-combo.ts @@ -4,7 +4,6 @@ import type { ComboData } from '../spec'; import { isCollapsed } from '../utils/collapsibility'; import { getSubgraphRelatedEdges } from '../utils/edge'; import { idOf } from '../utils/id'; -// import { reassignTo } from '../utils/transform'; import { BaseTransform } from './base-transform'; import type { DrawData } from './types'; import { reassignTo } from './utils'; diff --git a/packages/g6/src/transforms/map-node-size.ts b/packages/g6/src/transforms/map-node-size.ts index 5a7ddb1f5c8..19615741e5c 100644 --- a/packages/g6/src/transforms/map-node-size.ts +++ b/packages/g6/src/transforms/map-node-size.ts @@ -1,4 +1,4 @@ -import { deepMix, isEqual, pick } from '@antv/util'; +import { deepMix, pick } from '@antv/util'; import type { RuntimeContext } from '../runtime/types'; import type { GraphData, NodeData } from '../spec'; import type { NodeStyle } from '../spec/element/node'; @@ -9,10 +9,10 @@ import { idOf } from '../utils/id'; import { getVerticalPadding } from '../utils/padding'; import { linear, log, pow, sqrt } from '../utils/scale'; import { parseSize } from '../utils/size'; -import { reassignTo } from '../utils/transform'; import type { BaseTransformOptions } from './base-transform'; import { BaseTransform } from './base-transform'; import type { DrawData } from './types'; +import { isStyleEqual, reassignTo } from './utils'; export interface MapNodeSizeOptions extends BaseTransformOptions { /** @@ -127,10 +127,8 @@ export class MapNodeSize extends BaseTransform { const style: NodeStyle = { size }; this.assignLabelStyle(style, size, datum, element); - const isStyleEqual = element && Object.keys(style).every((key) => isEqual(style[key], element.attributes[key])); - - if (!element || !isStyleEqual) { - reassignTo(input, element ? 'update' : 'add', 'node', deepMix(datum, { style })); + if (!element || !isStyleEqual(style, element.attributes)) { + reassignTo(input, element ? 'update' : 'add', 'node', deepMix(datum, { style }), true); } }); return input; diff --git a/packages/g6/src/transforms/process-parallel-edges.ts b/packages/g6/src/transforms/process-parallel-edges.ts index 76d9d424b9a..884d02e79b7 100644 --- a/packages/g6/src/transforms/process-parallel-edges.ts +++ b/packages/g6/src/transforms/process-parallel-edges.ts @@ -2,14 +2,15 @@ import type { PathStyleProps } from '@antv/g'; import { isBoolean, isEmpty, isEqual, isFunction } from '@antv/util'; import type { RuntimeContext } from '../runtime/types'; import type { EdgeData } from '../spec'; +import type { EdgeStyle } from '../spec/element/edge'; import type { ID, LoopPlacement, NodeLikeData } from '../types'; import { groupByChangeType, reduceDataChanges } from '../utils/change'; import { idOf } from '../utils/id'; -import { reassignTo } from '../utils/transform'; import type { BaseTransformOptions } from './base-transform'; import { BaseTransform } from './base-transform'; import { getEdgeEndsContext } from './get-edge-actual-ends'; import type { DrawData } from './types'; +import { isStyleEqual, reassignTo } from './utils'; const CUBIC_EDGE_TYPE = 'quadratic'; @@ -60,11 +61,14 @@ export interface ProcessParallelEdgesOptions extends BaseTransformOptions { * 处理平行边,即多条边共享同一源节点和目标节点 * * Process parallel edges which share the same source and target nodes + * @remarks + * 平行边(Parallel Edges)是指在图结构中,两个节点之间存在多条边。这些边共享相同的源节点和目标节点,但可能代表不同的关系或属性。为了避免边的重叠和混淆,提供了两种处理平行边的方式:(1) 捆绑模式(bundle):将平行边捆绑在一起,通过改变曲率与其他边分开;(2) 合并模式(merge):将平行边合并为一条聚合。 + * + * Parallel Edges refer to multiple edges existing between two nodes in a graph structure. These edges share the same source and target nodes but may represent different relationships or attributes. To avoid edge overlap and confusion, two methods are provided for handling parallel edges: (1) Bundle Mode: Bundles parallel edges together and separates them from other edges by altering their curvature; (2) Merge Mode: Merges parallel edges into a single aggregated edge. */ export class ProcessParallelEdges extends BaseTransform { static defaultOptions: Partial = { mode: 'bundle', - edges: undefined, distance: 15, // only valid for bundling mode }; @@ -130,7 +134,8 @@ export class ProcessParallelEdges extends BaseTransform !this.options.edges.includes(id) && edges.delete(id)); } - // 按照用户指定的顺序排序,防止捆绑时的抖动 | Sort by user-set order to prevent jitter during bundling + // 按照用户指定的顺序排序,防止捆绑时的抖动 + // Sort by user-set order to prevent jitter during bundling const edgeIds = model.getEdgeData().map(idOf); return new Map([...edges].sort((a, b) => edgeIds.indexOf(a[0]) - edgeIds.indexOf(b[0]))); }; @@ -140,30 +145,28 @@ export class ProcessParallelEdges extends BaseTransform { arcEdges.forEach((edge, i, edgeArr) => { - const computeStyle = () => { - const length = edgeArr.length; - const style: EdgeData['style'] = {}; - if (edge.source === edge.target) { - const len = CUBIC_LOOP_PLACEMENTS.length; - style.loopPlacement = CUBIC_LOOP_PLACEMENTS[i % len]; - style.loopDist = Math.floor(i / len) * distance + 50; - } else if (length === 1) { - style.curveOffset = 0; - } else { - const sign = (i % 2 === 0 ? 1 : -1) * (reverses[`${edge.source}|${edge.target}|${i}`] ? -1 : 1); - style.curveOffset = - length % 2 === 1 - ? sign * Math.ceil(i / 2) * distance * 2 - : sign * (Math.floor(i / 2) * distance * 2 + distance); - } - return Object.assign({}, edge.style, style); - }; - - const mergedEdgeData = Object.assign(edge, { type: CUBIC_EDGE_TYPE, style: computeStyle() }); + const length = edgeArr.length; + const style: EdgeStyle = edge.style || {}; + if (edge.source === edge.target) { + const len = CUBIC_LOOP_PLACEMENTS.length; + style.loopPlacement = CUBIC_LOOP_PLACEMENTS[i % len]; + style.loopDist = Math.floor(i / len) * distance + 50; + } else if (length === 1) { + style.curveOffset = 0; + } else { + const sign = (i % 2 === 0 ? 1 : -1) * (reverses[`${edge.source}|${edge.target}|${i}`] ? -1 : 1); + style.curveOffset = + length % 2 === 1 + ? sign * Math.ceil(i / 2) * distance * 2 + : sign * (Math.floor(i / 2) * distance * 2 + distance); + } + const mergedEdgeData = Object.assign(edge, { type: CUBIC_EDGE_TYPE, style }); const element = this.context.element?.getElement(idOf(edge)); - if (element) reassignTo(input, 'update', 'edge', mergedEdgeData, true); - else reassignTo(input, 'add', 'edge', mergedEdgeData, true); + + if (!element || !isStyleEqual(mergedEdgeData.style, element.attributes)) { + reassignTo(input, element ? 'update' : 'add', 'edge', mergedEdgeData, true); + } }); }); }; @@ -190,7 +193,10 @@ export class ProcessParallelEdges extends BaseTransform ({ ...acc, ...style }), {}); edges.forEach((edge, i, edges) => { - if (i === 0) { - const parsedStyle = Object.assign( - {}, - isFunction(this.options.style) ? this.options.style(edges) : this.options.style, - { childrenData: edges }, - ); - this.cacheMergeStyle.set(idOf(edge), parsedStyle); - const mergedEdgeData = { - ...edge, - type: 'line', - style: { ...mergedStyle, ...parsedStyle }, - }; - const element = this.context.element?.getElement(idOf(edge)); - reassignTo(input, element ? 'update' : 'add', 'edge', mergedEdgeData, true); - } else { + if (i !== 0) { reassignTo(input, 'remove', 'edge', edge); + return; + } + const parsedStyle = Object.assign( + {}, + isFunction(this.options.style) ? this.options.style(edges) : this.options.style, + { childrenData: edges }, + ); + this.cacheMergeStyle.set(idOf(edge), parsedStyle); + const mergedEdgeData = { + ...edge, + type: 'line', + style: { ...edge.style, ...mergedStyle, ...parsedStyle }, + }; + + const element = this.context.element?.getElement(idOf(edge)); + if (!element || !isStyleEqual(mergedEdgeData.style, element.attributes)) { + reassignTo(input, element ? 'update' : 'add', 'edge', mergedEdgeData, true); } }); }); diff --git a/packages/g6/src/transforms/utils.ts b/packages/g6/src/transforms/utils.ts index 4bfb8f3fe60..acc529933b5 100644 --- a/packages/g6/src/transforms/utils.ts +++ b/packages/g6/src/transforms/utils.ts @@ -29,3 +29,15 @@ export function reassignTo( else value[typeName].delete(id); }); } + +/** + * 判断样式是否与原始样式一致 + * + * Determine whether the style is consistent with the original style + * @param style - 样式 | style + * @param originalStyle - 原始样式 | original style + * @returns 是否一致 | Whether it is consistent + */ +export function isStyleEqual(style: Record, originalStyle: Record) { + return Object.keys(style).every((key) => style[key] === originalStyle[key]); +} diff --git a/packages/g6/src/utils/transform.ts b/packages/g6/src/utils/transform.ts index 04b2df24e60..1a5bd814dc2 100644 --- a/packages/g6/src/utils/transform.ts +++ b/packages/g6/src/utils/transform.ts @@ -1,8 +1,3 @@ -import { deepMix } from '@antv/util'; -import type { DrawData, ProcedureData } from '../transforms/types'; -import type { ElementDatum, ElementType } from '../types'; -import { idOf } from './id'; - /** * 从 transform 字符串中替换 translate 部分 * @@ -21,30 +16,3 @@ export function replaceTranslateInTransform(x: number, y: number, z: number, tra return `translate3d(${x}, ${y}, ${z})${removedTranslate}`; } } - -/** - * 重新分配绘制任务 - * - * Reassign drawing tasks - * @param input - 绘制数据 | DrawData - * @param type - 类型 | type - * @param elementType - 元素类型 | element type - * @param datum - 数据 | data - * @param merge - 是否合并 | whether to merge - */ -export const reassignTo = ( - input: DrawData, - type: 'add' | 'update' | 'remove', - elementType: ElementType, - datum: ElementDatum, - merge = false, -) => { - const id = idOf(datum); - const typeName = `${elementType}s` as keyof ProcedureData; - const exitsDatum: any = - input.add[typeName].get(id) || input.update[typeName].get(id) || input.remove[typeName].get(id) || datum; - Object.entries(input).forEach(([_type, value]) => { - if (type === _type) value[typeName].set(id, merge ? deepMix(exitsDatum, datum) : datum); - else value[typeName].delete(id); - }); -};