Skip to content

Commit

Permalink
fix: avoid unnecessary redraws for parallel edges (#6406)
Browse files Browse the repository at this point in the history
* refactor: avoid triggering redraw for parallel edges when unnecessary

* fix: update isStyleEqual

* docs: add parallel edges remarks

* fix: update snapshots
  • Loading branch information
yvonneyx authored Oct 15, 2024
1 parent 6e213ba commit b30018c
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 82 deletions.
2 changes: 1 addition & 1 deletion packages/g6/__tests__/demos/transform-map-node-size.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export const transformProcessParallelEdges: TestCase = async (context) => {
{
type: 'hover-activate',
key: 'hover-activate',
enable: (event: any) => event.targetType === 'edge',
},
],
transforms: [
Expand Down
1 change: 0 additions & 1 deletion packages/g6/src/transforms/collapse-expand-combo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
10 changes: 4 additions & 6 deletions packages/g6/src/transforms/map-node-size.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 {
/**
Expand Down Expand Up @@ -127,10 +127,8 @@ export class MapNodeSize extends BaseTransform<MapNodeSizeOptions> {
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;
Expand Down
91 changes: 50 additions & 41 deletions packages/g6/src/transforms/process-parallel-edges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -60,11 +61,14 @@ export interface ProcessParallelEdgesOptions extends BaseTransformOptions {
* <zh/> 处理平行边,即多条边共享同一源节点和目标节点
*
* <en/> Process parallel edges which share the same source and target nodes
* @remarks
* <zh/> 平行边(Parallel Edges)是指在图结构中,两个节点之间存在多条边。这些边共享相同的源节点和目标节点,但可能代表不同的关系或属性。为了避免边的重叠和混淆,提供了两种处理平行边的方式:(1) 捆绑模式(bundle):将平行边捆绑在一起,通过改变曲率与其他边分开;(2) 合并模式(merge):将平行边合并为一条聚合。
*
* <en/> 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<ProcessParallelEdgesOptions> {
static defaultOptions: Partial<ProcessParallelEdgesOptions> = {
mode: 'bundle',
edges: undefined,
distance: 15, // only valid for bundling mode
};

Expand Down Expand Up @@ -130,7 +134,8 @@ export class ProcessParallelEdges extends BaseTransform<ProcessParallelEdgesOpti
edges.forEach((_: EdgeData, id: ID) => !this.options.edges.includes(id) && edges.delete(id));
}

// <zh/> 按照用户指定的顺序排序,防止捆绑时的抖动 | <en/> 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])));
};
Expand All @@ -140,30 +145,28 @@ export class ProcessParallelEdges extends BaseTransform<ProcessParallelEdgesOpti

edgeMap.forEach((arcEdges) => {
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);
}
});
});
};
Expand All @@ -190,7 +193,10 @@ export class ProcessParallelEdges extends BaseTransform<ProcessParallelEdgesOpti
if (edges.length === 1) {
const edge = edges[0];
const element = this.context.element?.getElement(idOf(edge));
reassignTo(input, element ? 'update' : 'add', 'edge', this.resetEdgeStyle(edge), true);
const edgeStyle = this.resetEdgeStyle(edge);
if (!element || !isStyleEqual(edgeStyle, element.attributes)) {
reassignTo(input, element ? 'update' : 'add', 'edge', edgeStyle);
}
return;
}

Expand All @@ -208,22 +214,25 @@ export class ProcessParallelEdges extends BaseTransform<ProcessParallelEdgesOpti
.reduce((acc, style) => ({ ...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);
}
});
});
Expand Down
12 changes: 12 additions & 0 deletions packages/g6/src/transforms/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,15 @@ export function reassignTo(
else value[typeName].delete(id);
});
}

/**
* <zh/> 判断样式是否与原始样式一致
*
* <en/> Determine whether the style is consistent with the original style
* @param style - <zh/> 样式 | <en/> style
* @param originalStyle - <zh/> 原始样式 | <en/> original style
* @returns <zh/> 是否一致 | <en/> Whether it is consistent
*/
export function isStyleEqual(style: Record<string, unknown>, originalStyle: Record<string, unknown>) {
return Object.keys(style).every((key) => style[key] === originalStyle[key]);
}
32 changes: 0 additions & 32 deletions packages/g6/src/utils/transform.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
* <zh/> 从 transform 字符串中替换 translate 部分
*
Expand All @@ -21,30 +16,3 @@ export function replaceTranslateInTransform(x: number, y: number, z: number, tra
return `translate3d(${x}, ${y}, ${z})${removedTranslate}`;
}
}

/**
* <zh/> 重新分配绘制任务
*
* <en/> Reassign drawing tasks
* @param input - <zh/>绘制数据 | <en/>DrawData
* @param type - <zh/>类型 | <en/>type
* @param elementType - <zh/>元素类型 | <en/>element type
* @param datum - <zh/>数据 | <en/>data
* @param merge - <zh/>是否合并 | <en/>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);
});
};

0 comments on commit b30018c

Please sign in to comment.