From 2674a34b12c42511886f5fd9d52197f3623baf35 Mon Sep 17 00:00:00 2001 From: yvonneyx Date: Mon, 16 Sep 2024 16:14:22 +0800 Subject: [PATCH 1/9] fix: adjust graph data to layout model --- packages/g6/src/utils/layout.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/g6/src/utils/layout.ts b/packages/g6/src/utils/layout.ts index 44088426466..1bedba861be 100644 --- a/packages/g6/src/utils/layout.ts +++ b/packages/g6/src/utils/layout.ts @@ -147,16 +147,17 @@ export function layoutAdapter( const { nodes = [], edges = [], combos = [] } = data; const nodesToLayout: LayoutNodeData[] = nodes.map((datum) => { const id = idOf(datum); - const { data, style, combo } = datum; + const { data, style, combo, ...rest } = datum; const result = { id, data: { - ...data, + data: { ...data }, // antv-dagre 会读取 data.parentId // antv-dagre will read data.parentId ...(combo ? { parentId: combo } : {}), + style: { ...style }, + ...rest, }, - style: { ...style }, }; // 一些布局会从 data 中读取位置信息 if (style?.x) Object.assign(result.data, { x: style.x }); From 335bb5d45ea58968ca316f06b0ff3df908a37484 Mon Sep 17 00:00:00 2001 From: yvonneyx Date: Mon, 16 Sep 2024 16:14:52 +0800 Subject: [PATCH 2/9] feat: add map-node-size transform --- packages/g6/src/exports.ts | 9 +- packages/g6/src/registry/build-in.ts | 6 +- packages/g6/src/transforms/index.ts | 2 + packages/g6/src/transforms/map-node-size.ts | 360 ++++++++++++++++++++ packages/g6/src/utils/scale.ts | 75 ++++ 5 files changed, 448 insertions(+), 4 deletions(-) create mode 100644 packages/g6/src/transforms/map-node-size.ts create mode 100644 packages/g6/src/utils/scale.ts diff --git a/packages/g6/src/exports.ts b/packages/g6/src/exports.ts index 9c6531777d1..3aa6596bbb8 100644 --- a/packages/g6/src/exports.ts +++ b/packages/g6/src/exports.ts @@ -95,7 +95,7 @@ export { export { getExtension, getExtensions } from './registry/get'; export { register } from './registry/register'; export { Graph } from './runtime/graph'; -export { BaseTransform, PlaceRadialLabels, ProcessParallelEdges } from './transforms'; +export { BaseTransform, MapNodeSize, PlaceRadialLabels, ProcessParallelEdges } from './transforms'; export { isCollapsed } from './utils/collapsibility'; export { idOf } from './utils/id'; export { invokeLayoutMethod } from './utils/layout'; @@ -219,7 +219,12 @@ export type { CustomBehaviorOption } from './spec/behavior'; export type { AnimationStage } from './spec/element/animation'; export type { LayoutOptions, STDLayoutOptions, SingleLayoutOptions } from './spec/layout'; export type { CustomPluginOption } from './spec/plugin'; -export type { BaseTransformOptions, PlaceRadialLabelsOptions, ProcessParallelEdgesOptions } from './transforms'; +export type { + BaseTransformOptions, + MapNodeSizeOptions, + PlaceRadialLabelsOptions, + ProcessParallelEdgesOptions, +} from './transforms'; export type { DrawData } from './transforms/types'; export type { BaseElementStyleProps, diff --git a/packages/g6/src/registry/build-in.ts b/packages/g6/src/registry/build-in.ts index 072f88c5a07..91c5b99d55d 100644 --- a/packages/g6/src/registry/build-in.ts +++ b/packages/g6/src/registry/build-in.ts @@ -93,6 +93,7 @@ import { CollapseExpandCombo, CollapseExpandNode, GetEdgeActualEnds, + MapNodeSize, PlaceRadialLabels, ProcessParallelEdges, UpdateRelatedEdge, @@ -204,13 +205,14 @@ const BUILT_IN_EXTENSIONS: ExtensionRegistry = { watermark: Watermark, }, transform: { - 'update-related-edges': UpdateRelatedEdge, 'arrange-draw-order': ArrangeDrawOrder, 'collapse-expand-combo': CollapseExpandCombo, 'collapse-expand-node': CollapseExpandNode, - 'process-parallel-edges': ProcessParallelEdges, 'get-edge-actual-ends': GetEdgeActualEnds, + 'map-node-size': MapNodeSize, 'place-radial-labels': PlaceRadialLabels, + 'process-parallel-edges': ProcessParallelEdges, + 'update-related-edges': UpdateRelatedEdge, }, shape: { circle: GCircle, diff --git a/packages/g6/src/transforms/index.ts b/packages/g6/src/transforms/index.ts index 9e861f8268e..283a8bd9a4a 100644 --- a/packages/g6/src/transforms/index.ts +++ b/packages/g6/src/transforms/index.ts @@ -3,10 +3,12 @@ export { BaseTransform } from './base-transform'; export { CollapseExpandCombo } from './collapse-expand-combo'; export { CollapseExpandNode } from './collapse-expand-node'; export { GetEdgeActualEnds } from './get-edge-actual-ends'; +export { MapNodeSize } from './map-node-size'; export { PlaceRadialLabels } from './place-radial-labels'; export { ProcessParallelEdges } from './process-parallel-edges'; export { UpdateRelatedEdge } from './update-related-edge'; export type { BaseTransformOptions } from './base-transform'; +export type { MapNodeSizeOptions } from './map-node-size'; export type { PlaceRadialLabelsOptions } from './place-radial-labels'; export type { ProcessParallelEdgesOptions } from './process-parallel-edges'; diff --git a/packages/g6/src/transforms/map-node-size.ts b/packages/g6/src/transforms/map-node-size.ts new file mode 100644 index 00000000000..dba19e7fbc7 --- /dev/null +++ b/packages/g6/src/transforms/map-node-size.ts @@ -0,0 +1,360 @@ +import { findShortestPath, pageRank } from '@antv/algorithm'; +import { deepMix } from '@antv/util'; +import type { RuntimeContext } from '../runtime/types'; +import type { GraphData } from '../spec'; +import type { EdgeDirection, ID, Size, STDSize } from '../types'; +import { idOf } from '../utils/id'; +import { linear, log, powerLaw, 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'; + +export interface MapNodeSizeOptions extends BaseTransformOptions { + /** + * 节点中心性的度量方法 + * - `'degree'`:度中心性,通过节点的度数(连接的边的数量)来衡量其重要性。度中心性高的节点通常具有较多的直接连接,在网络中可能扮演着重要的角色 + * - `'betweenness'`:介数中心性,通过节点在所有最短路径中出现的次数来衡量其重要性。介数中心性高的节点通常在网络中起到桥梁作用,控制着信息的流动 + * - `'closeness'`:接近中心性,通过节点到其他所有节点的最短路径长度总和的倒数来衡量其重要性。接近中心性高的节点通常能够更快地到达网络中的其他节点 + * - `'eigenvector'`:特征向量中心性,通过节点与其他中心节点的连接程度来衡量其重要性。特征向量中心性高的节点通常连接着其他重要节点 + * - `'pagerank'`:PageRank 中心性,通过节点被其他节点引用的次数来衡量其重要性,常用于有向图。PageRank 中心性高的节点通常在网络中具有较高的影响力,类似于网页排名算法 + * - 自定义中心性计算方法:`(graphData: GraphData) => CentralityResult`,其中 `graphData` 为图数据,`CentralityResult` 为节点 ID 到中心性值的映射 + * + * The method of measuring the node centrality + * - `'degree'`: Degree centrality, measures centrality by the degree (number of connected edges) of a node. Nodes with high degree centrality usually have more direct connections and may play important roles in the network + * - `'betweenness'`: Betweenness centrality, measures centrality by the number of times a node appears in all shortest paths. Nodes with high betweenness centrality usually act as bridges in the network, controlling the flow of information + * - `'closeness'`: Closeness centrality, measures centrality by the reciprocal of the average shortest path length from a node to all other nodes. Nodes with high closeness centrality usually can reach other nodes in the network more quickly + * - `'eigenvector'`: Eigenvector centrality, measures centrality by the degree of connection between a node and other central nodes. Nodes with high eigenvector centrality usually connect to other important nodes + * - `'pagerank'`: PageRank centrality, measures centrality by the number of times a node is referenced by other nodes, commonly used in directed graphs. Nodes with high PageRank centrality usually have high influence in the network, similar to the page ranking algorithm + * - Custom centrality calculation method: `(graphData: GraphData) => Map`, where `graphData` is the graph data, and `Map` is the mapping from node ID to centrality value + * @defaultValue `{ type: 'eigenvector' }` + */ + centrality?: + | { type: 'degree'; direction?: EdgeDirection } + | { type: 'betweenness'; directed?: boolean; weightPropertyName?: string } + | { type: 'closeness'; directed?: boolean; weightPropertyName?: string } + | { type: 'eigenvector'; directed?: boolean } + | { type: 'pagerank'; epsilon?: number; linkProb?: number } + | ((graphData: GraphData) => Map); + /** + * 节点最大尺寸 + * + * The maximum size of the node + * @defaultValue `80` + */ + maxSize?: Size; + /** + * 节点最小尺寸 + * + * The minimum size of the node + * @defaultValue `20` + */ + minSize?: Size; + /** + * 插值函数,用于将节点中心性映射到节点大小 + * - `'linear'`:线性插值函数,将一个值从一个范围线性映射到另一个范围,常用于处理中心性值的差异较小的情况 + * - `'log'`:对数插值函数,将一个值从一个范围对数映射到另一个范围,常用于处理中心性值的差异较大的情况 + * - `'pow'`:幂律插值函数,将一个值从一个范围幂律映射到另一个范围,常用于处理中心性值的差异较大的情况 + * - `'sqrt'`:平方根插值函数,将一个值从一个范围平方根映射到另一个范围,常用于处理中心性值的差异较大的情况 + * - 自定义插值函数:`(value: number, domain: [number, number], range: [number, number]) => number`,其中 `value` 为需要映射的值,`domain` 为输入值的范围,`range` 为输出值的范围 + * + * Scale type + * - `'linear'`: Linear scale, maps a value from one range to another range linearly, commonly used for cases where the difference in centrality values is small + * - `'log'`: Logarithmic scale, maps a value from one range to another range logarithmically, commonly used for cases where the difference in centrality values is large + * - `'pow'`: Power-law scale, maps a value from one range to another range using power law, commonly used for cases where the difference in centrality values is large + * - `'sqrt'`: Square root scale, maps a value from one range to another range using square root, commonly used for cases where the difference in centrality values is large + * - Custom scale: `(value: number, domain: [number, number], range: [number, number]) => number`,where `value` is the value to be mapped, `domain` is the input range, and `range` is the output range + * @defaultValue `'log'` + */ + scale?: + | 'linear' + | 'log' + | 'pow' + | 'sqrt' + | ((value: number, domain: [number, number], range: [number, number]) => number); +} + +type CentralityResult = Map; + +/** + * 根据节点中心性调整节点的大小 + * + * Map node size based on node importance + * @remarks + * 在图可视化中,节点的大小通常用于传达节点的重要性或影响力。通过根据节点中心性调整节点的大小,我们可以更直观地展示网络中各个节点的重要性,从而帮助用户更好地理解和分析复杂的网络结构。 + * + * In graph visualization, the size of a node is usually used to convey the importance or influence of the node. By adjusting the size of the node based on the centrality of the node, we can more intuitively show the importance of each node in the network, helping users better understand and analyze complex network structures. + */ +export class MapNodeSize extends BaseTransform { + static defaultOptions: Partial = { + centrality: { type: 'eigenvector' }, + maxSize: 80, + minSize: 20, + scale: 'log', + }; + + constructor(context: RuntimeContext, options: MapNodeSizeOptions) { + super(context, deepMix({}, MapNodeSize.defaultOptions, options)); + } + + public beforeDraw(input: DrawData): DrawData { + const { model } = this.context; + const nodes = model.getNodeData(); + + const maxSize = parseSize(this.options.maxSize); + const minSize = parseSize(this.options.minSize); + + const centralities = this.getCentralities(this.options.centrality); + + const maxCentrality = centralities.size > 0 ? Math.max(...centralities.values()) : 0; + const minCentrality = centralities.size > 0 ? Math.min(...centralities.values()) : 0; + nodes.forEach((datum) => { + const size = this.assignSizeByCentrality( + centralities.get(idOf(datum)) || 0, + minCentrality, + maxCentrality, + minSize, + maxSize, + this.options.scale, + ); + const element = this.context.element?.getElement(idOf(datum)); + const mergedNodeDatum = Object.assign(datum, { style: { size } }); + reassignTo(input, element ? 'update' : 'add', 'node', mergedNodeDatum, true); + }); + return input; + } + + private getCentralities(centrality: Required['centrality']): CentralityResult { + const { model } = this.context; + const graphData = model.getData(); + + if (typeof centrality === 'function') return centrality(graphData); + + switch (centrality.type) { + case 'degree': { + const centralityResult = new Map(); + graphData.nodes?.forEach((node) => { + const degree = model.getRelatedEdgesData(idOf(node), centrality.direction).length; + centralityResult.set(idOf(node), degree); + }); + return centralityResult; + } + case 'betweenness': + return calculateBetweennessCentrality(graphData, centrality.directed, centrality.weightPropertyName); + case 'closeness': + return calculateClosenessCentrality(graphData, centrality.directed, centrality.weightPropertyName); + case 'eigenvector': + return calculateEigenvectorCentrality(graphData, centrality.directed); + case 'pagerank': + return calculatePageRankCentrality(graphData, centrality.epsilon, centrality.linkProb); + default: + return initCentralityResult(graphData); + } + } + + private assignSizeByCentrality = ( + centrality: number, + minCentrality: number, + maxCentrality: number, + minSize: STDSize, + maxSize: STDSize, + scale: MapNodeSizeOptions['scale'], + ): STDSize => { + const domain: [number, number] = [minCentrality, maxCentrality]; + const rangeX: [number, number] = [minSize[0], maxSize[0]]; + const rangeY: [number, number] = [minSize[1], maxSize[1]]; + const rangeZ: [number, number] = [minSize[2], maxSize[2]]; + + const interpolate = (centrality: number, range: [number, number]): number => { + if (typeof scale === 'function') { + return scale(centrality, domain, range); + } + switch (scale) { + case 'linear': + return linear(centrality, domain, range); + case 'log': + return log(centrality, domain, range); + case 'pow': + return powerLaw(centrality, domain, range, 2); + case 'sqrt': + return sqrt(centrality, domain, range); + default: + return range[0]; + } + }; + + return [interpolate(centrality, rangeX), interpolate(centrality, rangeY), interpolate(centrality, rangeZ)]; + }; +} + +const initCentralityResult = (graphData: GraphData): CentralityResult => { + const centralityResult = new Map(); + graphData.nodes?.forEach((node) => { + centralityResult.set(idOf(node), 0); + }); + return centralityResult; +}; + +/** + * 计算图中每个节点的中介中心性 + * + * Calculate the betweenness centrality for each node in the graph + * @param graphData - 图数据 | Graph data + * @param directed - 是否为有向图 | Whether the graph is directed + * @param weightPropertyName - 边的权重属性名 | The weight property name of the edge + * @returns 每个节点的中介中心性值 | The betweenness centrality for each node + */ +const calculateBetweennessCentrality = ( + graphData: GraphData, + directed?: boolean, + weightPropertyName?: string, +): CentralityResult => { + const centralityResult = initCentralityResult(graphData); + const { nodes = [] } = graphData; + nodes.forEach((source) => { + nodes.forEach((target) => { + if (source !== target) { + const { allPath } = findShortestPath(graphData, idOf(source), idOf(target), directed, weightPropertyName); + const pathCount = allPath.length; + (allPath as ID[][]).flat().forEach((nodeId) => { + if (nodeId !== idOf(source) && nodeId !== idOf(target)) { + centralityResult.set(nodeId, centralityResult.get(nodeId)! + 1 / pathCount); + } + }); + } + }); + }); + return centralityResult; +}; + +/** + * 计算图中每个节点的接近中心性 + * + * Calculate the closeness centrality for each node in the graph + * @param graphData - 图数据 | Graph data + * @param directed - 是否为有向图 | Whether the graph is directed + * @param weightPropertyName - 边的权重属性名 | The weight property name of the edge + * @returns 每个节点的接近中心性值 | The closeness centrality for each node + */ +const calculateClosenessCentrality = ( + graphData: GraphData, + directed?: boolean, + weightPropertyName?: string, +): CentralityResult => { + const centralityResult = new Map(); + const { nodes = [] } = graphData; + nodes.forEach((source) => { + const totalLength = nodes.reduce((acc, target) => { + if (source !== target) { + const { length } = findShortestPath(graphData, idOf(source), idOf(target), directed, weightPropertyName); + acc += length; + } + return acc; + }, 0); + centralityResult.set(idOf(source), 1 / totalLength); + }); + return centralityResult; +}; + +/** + * 计算图中每个节点的 PageRank 中心性 + * + * Calculate the PageRank centrality for each node in the graph + * @param graphData - 图数据 | Graph data + * @param epsilon - PageRank 算法的收敛容差 | The convergence tolerance of the PageRank algorithm + * @param linkProb - PageRank 算法的阻尼系数,指任意时刻,用户访问到某节点后继续访问该节点链接的下一个节点的概率,经验值 0.85 | The damping factor of the PageRank algorithm, which refers to the probability that a user will continue to visit the next node linked to a node at any time, with an empirical value of 0.85 + * @returns 每个节点的 PageRank 中心性值 | The PageRank centrality for each node + */ +const calculatePageRankCentrality = (graphData: GraphData, epsilon?: number, linkProb?: number): CentralityResult => { + const centralityResult = new Map(); + const data = pageRank(graphData, epsilon, linkProb); + graphData.nodes?.forEach((node) => { + centralityResult.set(idOf(node), data[idOf(node)]); + }); + return centralityResult; +}; + +/** + * 计算图中每个节点的特征向量中心性 + * + * Calculate the eigenvector centrality for each node in the graph. + * @param graphData - 图数据 | Graph data + * @param directed - 是否为有向图 | Whether the graph is directed + * @returns 每个节点的特征向量中心性值 The eigenvector centrality for each node. + */ +const calculateEigenvectorCentrality = (graphData: GraphData, directed?: boolean): CentralityResult => { + const { nodes = [] } = graphData; + const adjacencyMatrix = createAdjacencyMatrix(graphData, directed); + const eigenvector = powerIteration(adjacencyMatrix, nodes.length); + + const centralityResult = new Map(); + nodes.forEach((node, index) => { + centralityResult.set(idOf(node), eigenvector[index]); + }); + + return centralityResult; +}; + +/** + * 创建图的邻接矩阵 + * + * Create the adjacency matrix for the graph. + * @param graphData - 图数据 | Graph data + * @param directed - 是否为有向图 | Whether the graph is directed + * @returns 邻接矩阵 | The adjacency matrix + */ +const createAdjacencyMatrix = (graphData: GraphData, directed?: boolean): number[][] => { + const { nodes = [], edges = [] } = graphData; + const matrix: number[][] = Array(nodes.length) + .fill(null) + .map(() => Array(nodes.length).fill(0)); + + edges.forEach(({ source, target }) => { + const uIndex = nodes.findIndex((node) => idOf(node) === source); + const vIndex = nodes.findIndex((node) => idOf(node) === target); + if (directed) { + matrix[uIndex][vIndex] = 1; + } else { + matrix[uIndex][vIndex] = 1; + matrix[vIndex][uIndex] = 1; + } + }); + + return matrix; +}; + +/** + * 使用幂迭代法计算主特征向量 + * + * Calculate the principal eigenvector using the power iteration method + * @param matrix - 邻接矩阵 | The adjacency matrix + * @param numNodes - 节点数量 | The number of nodes + * @param maxIterations - 最大迭代次数 | The maximum number of iterations + * @param tolerance - 收敛容差 | The convergence tolerance + * @returns 主特征向量 | The principal eigenvector + */ +const powerIteration = (matrix: number[][], numNodes: number, maxIterations = 100, tolerance = 1e-6): number[] => { + let eigenvector = Array(numNodes).fill(1); + let diff = Infinity; + + for (let iter = 0; iter < maxIterations && diff > tolerance; iter++) { + const newEigenvector = Array(numNodes).fill(0); + + for (let i = 0; i < numNodes; i++) { + for (let j = 0; j < numNodes; j++) { + newEigenvector[i] += matrix[i][j] * eigenvector[j]; + } + } + + const norm = Math.sqrt(newEigenvector.reduce((sum, val) => sum + val * val, 0)); + for (let i = 0; i < numNodes; i++) { + newEigenvector[i] /= norm; + } + + diff = Math.sqrt(newEigenvector.reduce((sum, val, index) => sum + (val - eigenvector[index]) * val, 0)); + eigenvector = newEigenvector; + } + + return eigenvector; +}; diff --git a/packages/g6/src/utils/scale.ts b/packages/g6/src/utils/scale.ts new file mode 100644 index 00000000000..ea818782133 --- /dev/null +++ b/packages/g6/src/utils/scale.ts @@ -0,0 +1,75 @@ +/** + * 将一个值从一个范围线性映射到另一个范围 + * + * Linearly maps a value from one range to another range + * @param value - 需要映射的值 | The value to be mapped + * @param domain - 输入值的范围 [最小值, 最大值] | The input range [min, max] + * @param range - 输出值的范围 [最小值, 最大值] | The output range [min, max] + * @returns 映射后的值 | The mapped value + */ +export const linear = (value: number, domain: [number, number], range: [number, number]) => { + const [d0, d1] = domain; + const [r0, r1] = range; + + if (d1 === d0) return r0; + + const ratio = (value - d0) / (d1 - d0); + return r0 + ratio * (r1 - r0); +}; + +/** + * 将一个值从一个范围对数映射到另一个范围 + * + * Logarithmically maps a value from one range to another range + * @param value - 需要映射的值 | The value to be mapped + * @param domain - 输入值的范围 [最小值, 最大值] | The input range [min, max] + * @param range - 输出值的范围 [最小值, 最大值] | The output range [min, max] + * @returns 映射后的值 | The mapped value + */ +export const log = (value: number, domain: [number, number], range: [number, number]) => { + const [d0, d1] = domain; + const [r0, r1] = range; + + const ratio = Math.log(value - d0 + 1) / Math.log(d1 - d0 + 1); + return r0 + ratio * (r1 - r0); +}; + +/** + * 将一个值从一个范围幂映射到另一个范围 + * + * Maps a value from one range to another range + * @param value - 需要映射的值 | The value to be mapped + * @param domain - 输入值的范围 [最小值, 最大值] | The input range [min, max] + * @param range - 输出值的范围 [最小值, 最大值] | The output range [min, max] + * @param exponent - 幂指数 | The exponent + * @returns 映射后的值 | The mapped value + */ +export const powerLaw = ( + value: number, + domain: [number, number], + range: [number, number], + exponent: number, +): number => { + const [d0, d1] = domain; + const [r0, r1] = range; + + const ratio = Math.pow((value - d0) / (d1 - d0), exponent); + return r0 + ratio * (r1 - r0); +}; + +/** + * 将一个值从一个范围平方根映射到另一个范围 + * + * Maps a value from one range to another range using square root + * @param value - 需要映射的值 | The value to be mapped + * @param domain - 输入值的范围 [最小值, 最大值] | The input range [min, max] + * @param range - 输出值的范围 [最小值, 最大值] | The output range [min, max] + * @returns 映射后的值 | The mapped value + */ +export const sqrt = (value: number, domain: [number, number], range: [number, number]) => { + const [d0, d1] = domain; + const [r0, r1] = range; + + const ratio = Math.sqrt((value - d0) / (d1 - d0)); + return r0 + ratio * (r1 - r0); +}; From 77fe7428f91d8ce5a6d94d4dcd391e6a8a6c7253 Mon Sep 17 00:00:00 2001 From: yvonneyx Date: Mon, 16 Sep 2024 16:15:21 +0800 Subject: [PATCH 3/9] test: add map-node-size unit demo --- .vscode/settings.json | 2 + .../g6/__tests__/dataset/language-tree.json | 956 ++++++++++++++++++ .../g6/__tests__/demos/case-language-tree.ts | 55 + packages/g6/__tests__/demos/index.ts | 2 + .../demos/transform-map-node-size.ts | 47 + .../transform-map-node-size.spec.ts | 104 ++ .../g6/__tests__/unit/utils/scale.spec.ts | 28 + packages/g6/package.json | 3 +- 8 files changed, 1196 insertions(+), 1 deletion(-) create mode 100644 packages/g6/__tests__/dataset/language-tree.json create mode 100644 packages/g6/__tests__/demos/case-language-tree.ts create mode 100644 packages/g6/__tests__/demos/transform-map-node-size.ts create mode 100644 packages/g6/__tests__/unit/transforms/transform-map-node-size.spec.ts create mode 100644 packages/g6/__tests__/unit/utils/scale.spec.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index d6dc2978962..57661de7e33 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -38,6 +38,7 @@ "beforestagelayout", "beforetransform", "beforeviewportanimate", + "betweenness", "Bezier", "bubblesets", "cancelviewportanimate", @@ -55,6 +56,7 @@ "GSHAPE", "mindmap", "onframe", + "pagerank", "Phong", "pointset", "Polyline", diff --git a/packages/g6/__tests__/dataset/language-tree.json b/packages/g6/__tests__/dataset/language-tree.json new file mode 100644 index 00000000000..e90ded5e339 --- /dev/null +++ b/packages/g6/__tests__/dataset/language-tree.json @@ -0,0 +1,956 @@ +{ + "nodes": [ + { + "id": "Proto Indo-European", + "children": [ + "Balto-Slavic", + "Germanic", + "Celtic", + "Italic", + "Hellenic", + "Anatolian", + "Indo-Iranian", + "Tocharian", + "Phrygian", + "Armenian", + "Albanian", + "Thracian" + ] + }, + { + "id": "Balto-Slavic", + "children": ["Baltic", "Slavic"] + }, + { + "id": "Baltic", + "children": ["Old Prussian", "Lithuanian", "Latvian"] + }, + { + "id": "Old Prussian" + }, + { + "id": "Lithuanian" + }, + { + "id": "Latvian" + }, + { + "id": "Slavic", + "children": ["East Slavic", "West Slavic", "South Slavic"] + }, + { + "id": "East Slavic", + "children": ["Bulgarian", "Old Church Slavonic", "Macedonian", "Serbo-Croatian", "Slovene"] + }, + { + "id": "Bulgarian" + }, + { + "id": "Old Church Slavonic" + }, + { + "id": "Macedonian" + }, + { + "id": "Serbo-Croatian" + }, + { + "id": "Slovene" + }, + { + "id": "West Slavic", + "children": ["Polish", "Slovak", "Czech", "Wendish"] + }, + { + "id": "Polish" + }, + { + "id": "Slovak" + }, + { + "id": "Czech" + }, + { + "id": "Wendish" + }, + { + "id": "South Slavic", + "children": ["Russian", "Ukrainian", "Belarusian", "Rusyn"] + }, + { + "id": "Russian" + }, + { + "id": "Ukrainian" + }, + { + "id": "Belarusian" + }, + { + "id": "Rusyn" + }, + { + "id": "Germanic", + "children": ["North Germanic", "West Germanic", "East Germanic"] + }, + { + "id": "North Germanic", + "children": ["Old Norse", "Old Swedish", "Old Danish"] + }, + { + "id": "Old Norse", + "children": ["Old Icelandic", "Old Norwegian", "Faroese"] + }, + { + "id": "Old Icelandic", + "children": ["Icelandic"] + }, + { + "id": "Icelandic" + }, + { + "id": "Old Norwegian", + "children": ["Middle Norwegian"] + }, + { + "id": "Middle Norwegian", + "children": ["Norwegian"] + }, + { + "id": "Norwegian" + }, + { + "id": "Faroese" + }, + { + "id": "Old Swedish", + "children": ["Middle Swedish"] + }, + { + "id": "Middle Swedish", + "children": ["Swedish"] + }, + { + "id": "Swedish" + }, + { + "id": "Old Danish", + "children": ["Middle Danish"] + }, + { + "id": "Middle Danish", + "children": ["Danish"] + }, + { + "id": "Danish" + }, + { + "id": "West Germanic", + "children": ["Old English", "Old Frisian", "Old Dutch", "Old Low German", "Old High German"] + }, + { + "id": "Old English", + "children": ["Middle English"] + }, + { + "id": "Middle English", + "children": ["English"] + }, + { + "id": "English" + }, + { + "id": "Old Frisian", + "children": ["Frisian"] + }, + { + "id": "Frisian" + }, + { + "id": "Old Dutch", + "children": ["Middle Dutch"] + }, + { + "id": "Middle Dutch", + "children": ["Hollandic", "Flemish", "Dutch", "Limburgish", "Brabantian", "Rhinelandic"] + }, + { + "id": "Hollandic" + }, + { + "id": "Flemish" + }, + { + "id": "Dutch" + }, + { + "id": "Limburgish" + }, + { + "id": "Brabantian" + }, + { + "id": "Rhinelandic" + }, + { + "id": "Old Low German", + "children": ["Middle Low German"] + }, + { + "id": "Middle Low German", + "children": ["Low German"] + }, + { + "id": "Low German" + }, + { + "id": "Old High German", + "children": ["Middle High German"] + }, + { + "id": "Middle High German", + "children": ["(High) German", "Yiddish"] + }, + { + "id": "(High) German" + }, + { + "id": "Yiddish" + }, + { + "id": "East Germanic", + "children": ["Gothic"] + }, + { + "id": "Gothic" + }, + { + "id": "Celtic", + "children": ["Brythonic", "Goidelic"] + }, + { + "id": "Brythonic", + "children": ["Welsh", "Breton", "Cornish", "Cuymbric"] + }, + { + "id": "Welsh" + }, + { + "id": "Breton" + }, + { + "id": "Cornish" + }, + { + "id": "Cuymbric" + }, + { + "id": "Goidelic", + "children": ["Modern Irish", "Scottish Gaelic", "Manx"] + }, + { + "id": "Modern Irish" + }, + { + "id": "Scottish Gaelic" + }, + { + "id": "Manx" + }, + { + "id": "Italic", + "children": ["Osco-Umbrian", "Latino-Faliscan"] + }, + { + "id": "Osco-Umbrian", + "children": ["Umbrian", "Oscan"] + }, + { + "id": "Umbrian" + }, + { + "id": "Oscan" + }, + { + "id": "Latino-Faliscan", + "children": ["Latin", "Faliscan"] + }, + { + "id": "Latin", + "children": [ + "Portugese", + "Spanish", + "French", + "Romanian", + "Italian", + "Catalan", + "Franco-Provençal", + "Rhaeto-Romance" + ] + }, + { + "id": "Portugese" + }, + { + "id": "Spanish" + }, + { + "id": "French" + }, + { + "id": "Romanian" + }, + { + "id": "Italian" + }, + { + "id": "Catalan" + }, + { + "id": "Franco-Provençal" + }, + { + "id": "Rhaeto-Romance" + }, + { + "id": "Faliscan" + }, + { + "id": "Hellenic", + "children": ["Greek"] + }, + { + "id": "Greek" + }, + { + "id": "Anatolian", + "children": ["Hittite", "Palaic", "Luwic", "Lydian"] + }, + { + "id": "Hittite" + }, + { + "id": "Palaic" + }, + { + "id": "Luwic" + }, + { + "id": "Lydian" + }, + { + "id": "Indo-Iranian", + "children": ["Dardic", "Indic", "Iranian"] + }, + { + "id": "Dardic", + "children": ["Dard"] + }, + { + "id": "Dard" + }, + { + "id": "Indic", + "children": ["Sanskrit"] + }, + { + "id": "Sanskrit", + "children": [ + "Sindhi", + "Romani", + "Urdu", + "Hindi", + "Bihari", + "Assamese", + "Bengali", + "Marathi", + "Gujarati", + "Punjabi", + "Sinhalese" + ] + }, + { + "id": "Sindhi" + }, + { + "id": "Romani" + }, + { + "id": "Urdu" + }, + { + "id": "Hindi" + }, + { + "id": "Bihari" + }, + { + "id": "Assamese" + }, + { + "id": "Bengali" + }, + { + "id": "Marathi" + }, + { + "id": "Gujarati" + }, + { + "id": "Punjabi" + }, + { + "id": "Sinhalese" + }, + { + "id": "Iranian", + "children": ["Old Persian", "Balochi", "Kurdish", "Pashto", "Sogdian"] + }, + { + "id": "Old Persian", + "children": ["Middle Persian", "Pahlavi"] + }, + { + "id": "Middle Persian", + "children": ["Persian"] + }, + { + "id": "Persian" + }, + { + "id": "Pahlavi" + }, + { + "id": "Balochi" + }, + { + "id": "Kurdish" + }, + { + "id": "Pashto" + }, + { + "id": "Sogdian" + }, + { + "id": "Tocharian", + "children": ["Tocharian A", "Tocharian B"] + }, + { + "id": "Tocharian A" + }, + { + "id": "Tocharian B" + }, + { + "id": "Phrygian" + }, + { + "id": "Armenian" + }, + { + "id": "Albanian" + }, + { + "id": "Thracian" + } + ], + "edges": [ + { + "source": "Proto Indo-European", + "target": "Balto-Slavic" + }, + { + "source": "Proto Indo-European", + "target": "Germanic" + }, + { + "source": "Proto Indo-European", + "target": "Celtic" + }, + { + "source": "Proto Indo-European", + "target": "Italic" + }, + { + "source": "Proto Indo-European", + "target": "Hellenic" + }, + { + "source": "Proto Indo-European", + "target": "Anatolian" + }, + { + "source": "Proto Indo-European", + "target": "Indo-Iranian" + }, + { + "source": "Proto Indo-European", + "target": "Tocharian" + }, + { + "source": "Proto Indo-European", + "target": "Phrygian" + }, + { + "source": "Proto Indo-European", + "target": "Armenian" + }, + { + "source": "Proto Indo-European", + "target": "Albanian" + }, + { + "source": "Proto Indo-European", + "target": "Thracian" + }, + { + "source": "Balto-Slavic", + "target": "Baltic" + }, + { + "source": "Balto-Slavic", + "target": "Slavic" + }, + { + "source": "Baltic", + "target": "Old Prussian" + }, + { + "source": "Baltic", + "target": "Lithuanian" + }, + { + "source": "Baltic", + "target": "Latvian" + }, + { + "source": "Slavic", + "target": "East Slavic" + }, + { + "source": "Slavic", + "target": "West Slavic" + }, + { + "source": "Slavic", + "target": "South Slavic" + }, + { + "source": "East Slavic", + "target": "Bulgarian" + }, + { + "source": "East Slavic", + "target": "Old Church Slavonic" + }, + { + "source": "East Slavic", + "target": "Macedonian" + }, + { + "source": "East Slavic", + "target": "Serbo-Croatian" + }, + { + "source": "East Slavic", + "target": "Slovene" + }, + { + "source": "West Slavic", + "target": "Polish" + }, + { + "source": "West Slavic", + "target": "Slovak" + }, + { + "source": "West Slavic", + "target": "Czech" + }, + { + "source": "West Slavic", + "target": "Wendish" + }, + { + "source": "South Slavic", + "target": "Russian" + }, + { + "source": "South Slavic", + "target": "Ukrainian" + }, + { + "source": "South Slavic", + "target": "Belarusian" + }, + { + "source": "South Slavic", + "target": "Rusyn" + }, + { + "source": "Germanic", + "target": "North Germanic" + }, + { + "source": "Germanic", + "target": "West Germanic" + }, + { + "source": "Germanic", + "target": "East Germanic" + }, + { + "source": "North Germanic", + "target": "Old Norse" + }, + { + "source": "North Germanic", + "target": "Old Swedish" + }, + { + "source": "North Germanic", + "target": "Old Danish" + }, + { + "source": "Old Norse", + "target": "Old Icelandic" + }, + { + "source": "Old Norse", + "target": "Old Norwegian" + }, + { + "source": "Old Norse", + "target": "Faroese" + }, + { + "source": "Old Icelandic", + "target": "Icelandic" + }, + { + "source": "Old Norwegian", + "target": "Middle Norwegian" + }, + { + "source": "Middle Norwegian", + "target": "Norwegian" + }, + { + "source": "Old Swedish", + "target": "Middle Swedish" + }, + { + "source": "Middle Swedish", + "target": "Swedish" + }, + { + "source": "Old Danish", + "target": "Middle Danish" + }, + { + "source": "Middle Danish", + "target": "Danish" + }, + { + "source": "West Germanic", + "target": "Old English" + }, + { + "source": "West Germanic", + "target": "Old Frisian" + }, + { + "source": "West Germanic", + "target": "Old Dutch" + }, + { + "source": "West Germanic", + "target": "Old Low German" + }, + { + "source": "West Germanic", + "target": "Old High German" + }, + { + "source": "Old English", + "target": "Middle English" + }, + { + "source": "Middle English", + "target": "English" + }, + { + "source": "Old Frisian", + "target": "Frisian" + }, + { + "source": "Old Dutch", + "target": "Middle Dutch" + }, + { + "source": "Middle Dutch", + "target": "Hollandic" + }, + { + "source": "Middle Dutch", + "target": "Flemish" + }, + { + "source": "Middle Dutch", + "target": "Dutch" + }, + { + "source": "Middle Dutch", + "target": "Limburgish" + }, + { + "source": "Middle Dutch", + "target": "Brabantian" + }, + { + "source": "Middle Dutch", + "target": "Rhinelandic" + }, + { + "source": "Old Low German", + "target": "Middle Low German" + }, + { + "source": "Middle Low German", + "target": "Low German" + }, + { + "source": "Old High German", + "target": "Middle High German" + }, + { + "source": "Middle High German", + "target": "(High) German" + }, + { + "source": "Middle High German", + "target": "Yiddish" + }, + { + "source": "East Germanic", + "target": "Gothic" + }, + { + "source": "Celtic", + "target": "Brythonic" + }, + { + "source": "Celtic", + "target": "Goidelic" + }, + { + "source": "Brythonic", + "target": "Welsh" + }, + { + "source": "Brythonic", + "target": "Breton" + }, + { + "source": "Brythonic", + "target": "Cornish" + }, + { + "source": "Brythonic", + "target": "Cuymbric" + }, + { + "source": "Goidelic", + "target": "Modern Irish" + }, + { + "source": "Goidelic", + "target": "Scottish Gaelic" + }, + { + "source": "Goidelic", + "target": "Manx" + }, + { + "source": "Italic", + "target": "Osco-Umbrian" + }, + { + "source": "Italic", + "target": "Latino-Faliscan" + }, + { + "source": "Osco-Umbrian", + "target": "Umbrian" + }, + { + "source": "Osco-Umbrian", + "target": "Oscan" + }, + { + "source": "Latino-Faliscan", + "target": "Latin" + }, + { + "source": "Latino-Faliscan", + "target": "Faliscan" + }, + { + "source": "Latin", + "target": "Portugese" + }, + { + "source": "Latin", + "target": "Spanish" + }, + { + "source": "Latin", + "target": "French" + }, + { + "source": "Latin", + "target": "Romanian" + }, + { + "source": "Latin", + "target": "Italian" + }, + { + "source": "Latin", + "target": "Catalan" + }, + { + "source": "Latin", + "target": "Franco-Provençal" + }, + { + "source": "Latin", + "target": "Rhaeto-Romance" + }, + { + "source": "Hellenic", + "target": "Greek" + }, + { + "source": "Anatolian", + "target": "Hittite" + }, + { + "source": "Anatolian", + "target": "Palaic" + }, + { + "source": "Anatolian", + "target": "Luwic" + }, + { + "source": "Anatolian", + "target": "Lydian" + }, + { + "source": "Indo-Iranian", + "target": "Dardic" + }, + { + "source": "Indo-Iranian", + "target": "Indic" + }, + { + "source": "Indo-Iranian", + "target": "Iranian" + }, + { + "source": "Dardic", + "target": "Dard" + }, + { + "source": "Indic", + "target": "Sanskrit" + }, + { + "source": "Sanskrit", + "target": "Sindhi" + }, + { + "source": "Sanskrit", + "target": "Romani" + }, + { + "source": "Sanskrit", + "target": "Urdu" + }, + { + "source": "Sanskrit", + "target": "Hindi" + }, + { + "source": "Sanskrit", + "target": "Bihari" + }, + { + "source": "Sanskrit", + "target": "Assamese" + }, + { + "source": "Sanskrit", + "target": "Bengali" + }, + { + "source": "Sanskrit", + "target": "Marathi" + }, + { + "source": "Sanskrit", + "target": "Gujarati" + }, + { + "source": "Sanskrit", + "target": "Punjabi" + }, + { + "source": "Sanskrit", + "target": "Sinhalese" + }, + { + "source": "Iranian", + "target": "Old Persian" + }, + { + "source": "Iranian", + "target": "Balochi" + }, + { + "source": "Iranian", + "target": "Kurdish" + }, + { + "source": "Iranian", + "target": "Pashto" + }, + { + "source": "Iranian", + "target": "Sogdian" + }, + { + "source": "Old Persian", + "target": "Middle Persian" + }, + { + "source": "Old Persian", + "target": "Pahlavi" + }, + { + "source": "Middle Persian", + "target": "Persian" + }, + { + "source": "Tocharian", + "target": "Tocharian A" + }, + { + "source": "Tocharian", + "target": "Tocharian B" + } + ] +} diff --git a/packages/g6/__tests__/demos/case-language-tree.ts b/packages/g6/__tests__/demos/case-language-tree.ts new file mode 100644 index 00000000000..ee224ad6b70 --- /dev/null +++ b/packages/g6/__tests__/demos/case-language-tree.ts @@ -0,0 +1,55 @@ +import { labelPropagation } from '@antv/algorithm'; +import { Graph, NodeData } from '@antv/g6'; +import data from '../dataset/language-tree.json'; + +export const caseLanguageTree: TestCase = async (context) => { + const size = (node: NodeData) => Math.max(...(node.style?.size as [number, number, number])); + + const graph = new Graph({ + ...context, + autoFit: 'view', + data: { + ...data, + nodes: labelPropagation(data).clusters.flatMap((cluster) => cluster.nodes), + }, + node: { + style: { + labelText: (d) => d.id, + labelBackground: true, + iconSrc: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg', + }, + palette: { + field: (d) => d.clusterId, + }, + }, + layout: { + type: 'd3-force', + link: { + distance: (edge) => size(edge.source) + size(edge.target), + }, + collide: { + radius: (node: NodeData) => size(node) + 1, + }, + manyBody: { + strength: (node: NodeData) => -4 * size(node), + }, + animation: false, + }, + transforms: ['map-node-size'], + behaviors: [ + 'drag-canvas', + 'zoom-canvas', + { + key: 'hover-activate', + type: 'hover-activate', + degree: 1, + inactiveState: 'inactive', + }, + ], + animation: false, + }); + + await graph.render(); + + return graph; +}; diff --git a/packages/g6/__tests__/demos/index.ts b/packages/g6/__tests__/demos/index.ts index 6175dbebb71..2ec05e8963e 100644 --- a/packages/g6/__tests__/demos/index.ts +++ b/packages/g6/__tests__/demos/index.ts @@ -23,6 +23,7 @@ export { bugTooltipResize } from './bug-tooltip-resize'; export { canvasCursor } from './canvas-cursor'; export { caseFundFlow } from './case-fund-flow'; export { caseIndentedTree } from './case-indented-tree'; +export { caseLanguageTree } from './case-language-tree'; export { caseMindmap } from './case-mindmap'; export { caseOrgChart } from './case-org-chart'; export { caseRadialDendrogram } from './case-radial-dendrogram'; @@ -136,6 +137,7 @@ export { pluginTooltip } from './plugin-tooltip'; export { pluginWatermark } from './plugin-watermark'; export { pluginWatermarkImage } from './plugin-watermark-image'; export { theme } from './theme'; +export { transformMapNodeSize } from './transform-map-node-size'; export { transformPlaceRadialLabels } from './transform-place-radial-labels'; export { transformProcessParallelEdges } from './transform-process-parallel-edges'; export { viewportFit } from './viewport-fit'; diff --git a/packages/g6/__tests__/demos/transform-map-node-size.ts b/packages/g6/__tests__/demos/transform-map-node-size.ts new file mode 100644 index 00000000000..d7108d1e1f1 --- /dev/null +++ b/packages/g6/__tests__/demos/transform-map-node-size.ts @@ -0,0 +1,47 @@ +import { Graph } from '@antv/g6'; + +export const transformMapNodeSize: TestCase = async (context) => { + const graph = new Graph({ + ...context, + data: { + nodes: [{ id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }], + edges: [ + { source: 'node-1', target: 'node-2' }, + { source: 'node-1', target: 'node-3' }, + { source: 'node-1', target: 'node-4' }, + { source: 'node-4', target: 'node-5' }, + ], + }, + node: { + style: { + labelText: (d) => d.id, + }, + }, + layout: { + type: 'force', + }, + transforms: [ + { + key: 'map-node-size', + type: 'map-node-size', + }, + ], + animation: false, + }); + + await graph.render(); + + const config = { 'centrality.type': 'eigenvector' }; + + transformMapNodeSize.form = (panel) => [ + panel + .add(config, 'centrality.type', ['degree', 'betweenness', 'closeness', 'eigenvector', 'pagerank']) + .name('Centrality Type') + .onChange((type: string) => { + graph.updateTransform({ key: 'map-node-size', centrality: { type } }); + graph.draw(); + }), + ]; + + return graph; +}; diff --git a/packages/g6/__tests__/unit/transforms/transform-map-node-size.spec.ts b/packages/g6/__tests__/unit/transforms/transform-map-node-size.spec.ts new file mode 100644 index 00000000000..c9f323eab7a --- /dev/null +++ b/packages/g6/__tests__/unit/transforms/transform-map-node-size.spec.ts @@ -0,0 +1,104 @@ +import type { Graph } from '@/src'; +import { transformMapNodeSize } from '@@/demos'; +import { createDemoGraph } from '@@/utils'; + +const nodeSizeMap = (graph: Graph) => + Object.fromEntries(graph.getNodeData().map((node) => [node.id, node.style?.size])); + +describe('transform map node size', () => { + let graph: Graph; + + beforeAll(async () => { + graph = await createDemoGraph(transformMapNodeSize, { animation: false }); + }); + + afterAll(() => { + graph.destroy(); + }); + + it('centrality', async () => { + graph.updateTransform({ + key: 'map-node-size', + centrality: { type: 'degree' }, + minSize: 10, + maxSize: 40, + scale: 'linear', + }); + await graph.render(); + + expect(nodeSizeMap(graph)).toEqual({ + 'node-1': [40, 40, 40], + 'node-2': [10, 10, 10], + 'node-3': [10, 10, 10], + 'node-4': [25, 25, 25], + 'node-5': [10, 10, 10], + }); + + graph.updateTransform({ + key: 'map-node-size', + centrality: { type: 'betweenness' }, + }); + await graph.render(); + + expect(nodeSizeMap(graph)).toEqual({ + 'node-1': [40, 40, 40], + 'node-2': [10, 10, 10], + 'node-3': [10, 10, 10], + 'node-4': [28, 28, 28], + 'node-5': [10, 10, 10], + }); + + graph.updateTransform({ + key: 'map-node-size', + centrality: { type: 'pagerank' }, + }); + await graph.render(); + + expect(nodeSizeMap(graph)['node-1']).toEqual([10, 10, 10]); + expect(nodeSizeMap(graph)['node-5']).toEqual([40, 40, 40]); + + graph.updateTransform({ + key: 'map-node-size', + centrality: { type: 'eigenvector' }, + }); + await graph.render(); + + expect(nodeSizeMap(graph)).toEqual({ + 'node-1': [40, 40, 40], + 'node-2': [10, 10, 10], + 'node-3': [10, 10, 10], + 'node-4': [25, 25, 25], + 'node-5': [10, 10, 10], + }); + + graph.updateTransform({ + key: 'map-node-size', + centrality: { type: 'eigenvector', directed: true }, + }); + await graph.render(); + + expect(nodeSizeMap(graph)).toEqual({ + 'node-1': [40, 40, 40], + 'node-2': [10, 10, 10], + 'node-3': [10, 10, 10], + 'node-4': [20, 20, 20], + 'node-5': [10, 10, 10], + }); + + graph.updateTransform({ + key: 'map-node-size', + centrality: { type: 'closeness' }, + minSize: 10, + maxSize: 50, + }); + await graph.render(); + + expect(nodeSizeMap(graph)).toEqual({ + 'node-1': [50, 50, 50], + 'node-2': [16.25, 16.25, 16.25], + 'node-3': [16.25, 16.25, 16.25], + 'node-4': [35, 35, 35], + 'node-5': [10, 10, 10], + }); + }); +}); diff --git a/packages/g6/__tests__/unit/utils/scale.spec.ts b/packages/g6/__tests__/unit/utils/scale.spec.ts new file mode 100644 index 00000000000..2e0cf79a5b8 --- /dev/null +++ b/packages/g6/__tests__/unit/utils/scale.spec.ts @@ -0,0 +1,28 @@ +import { linear, log, powerLaw, sqrt } from '@/src/utils/scale'; + +describe('scale', () => { + it('linear', () => { + expect(linear(0.2, [0, 1], [0, 0])).toEqual(0); + expect(linear(0, [0, 1], [0, 100])).toEqual(0); + expect(linear(0.5, [0, 1], [0, 100])).toEqual(50); + expect(linear(1, [0, 1], [0, 100])).toEqual(100); + }); + + it('log', () => { + expect(log(0, [0, 1], [0, 100])).toEqual(0); + expect(log(0.5, [0, 1], [0, 100])).toEqual((Math.log(1.5) / Math.log(2)) * 100); + expect(log(1, [0, 1], [0, 100])).toEqual(100); + }); + + it('powerLaw', () => { + expect(powerLaw(0, [0, 1], [0, 100], 2)).toEqual(0); + expect(powerLaw(0.5, [0, 1], [0, 100], 2)).toEqual(25); + expect(powerLaw(1, [0, 1], [0, 100], 2)).toEqual(100); + }); + + it('sqrt', () => { + expect(sqrt(0, [0, 1], [0, 100])).toEqual(0); + expect(sqrt(0.25, [0, 1], [0, 100])).toEqual(50); + expect(sqrt(1, [0, 1], [0, 100])).toEqual(100); + }); +}); diff --git a/packages/g6/package.json b/packages/g6/package.json index 10afe1f714c..07e13f4908e 100644 --- a/packages/g6/package.json +++ b/packages/g6/package.json @@ -57,6 +57,7 @@ "version": "node ./scripts/version.mjs" }, "dependencies": { + "@antv/algorithm": "^0.1.26", "@antv/component": "^2.0.4", "@antv/event-emitter": "^0.1.3", "@antv/g": "^6.0.13", @@ -64,7 +65,7 @@ "@antv/g-plugin-dragndrop": "^2.0.9", "@antv/graphlib": "^2.0.3", "@antv/hierarchy": "^0.6.13", - "@antv/layout": "^1.2.14-beta.7", + "@antv/layout": "^1.2.14-beta.8", "@antv/util": "^3.3.8", "bubblesets-js": "^2.3.3", "hull.js": "^1.0.6" From 0533577e9d5b6f84300459797eb75774a01577c9 Mon Sep 17 00:00:00 2001 From: yvonneyx Date: Mon, 16 Sep 2024 16:15:53 +0800 Subject: [PATCH 4/9] docs: update page name --- packages/site/package.json | 2 +- packages/site/src/constants/locales/page-name.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/site/package.json b/packages/site/package.json index 4eb9bd14533..5de4a03698c 100644 --- a/packages/site/package.json +++ b/packages/site/package.json @@ -48,7 +48,7 @@ "@antv/g6": "workspace:*", "@antv/g6-extension-3d": "workspace:*", "@antv/g6-extension-react": "workspace:*", - "@antv/layout": "^1.2.14-beta.7", + "@antv/layout": "^1.2.14-beta.8", "@antv/layout-gpu": "^1.1.7", "@antv/layout-wasm": "^1.4.2", "@antv/util": "^3.3.8", diff --git a/packages/site/src/constants/locales/page-name.json b/packages/site/src/constants/locales/page-name.json index 5dbbeb94802..85d430719cc 100644 --- a/packages/site/src/constants/locales/page-name.json +++ b/packages/site/src/constants/locales/page-name.json @@ -76,5 +76,6 @@ "Tooltip": ["Tooltip", "提示框"], "Watermark": ["Watermark", "水印"], "ProcessParallelEdges": ["ProcessParallelEdges", "平行边"], - "PlaceRadialLabels": ["PlaceRadialLabels", "径向标签"] + "PlaceRadialLabels": ["PlaceRadialLabels", "径向标签"], + "MapNodeSize": ["MapNodeSize", "动态调整节点大小"] } From 49faa015aebce9f8522af83fdd471f271706f4a9 Mon Sep 17 00:00:00 2001 From: yvonneyx Date: Wed, 18 Sep 2024 10:41:59 +0800 Subject: [PATCH 5/9] refactor: optimize performance --- .../g6/__tests__/unit/utils/memoize.spec.ts | 12 ++ packages/g6/src/transforms/map-node-size.ts | 170 +++++++++--------- packages/g6/src/utils/layout.ts | 3 + packages/g6/src/utils/memoize.ts | 25 +++ packages/g6/src/utils/transform.ts | 2 +- 5 files changed, 126 insertions(+), 86 deletions(-) create mode 100644 packages/g6/__tests__/unit/utils/memoize.spec.ts create mode 100644 packages/g6/src/utils/memoize.ts diff --git a/packages/g6/__tests__/unit/utils/memoize.spec.ts b/packages/g6/__tests__/unit/utils/memoize.spec.ts new file mode 100644 index 00000000000..bd45c2eef6c --- /dev/null +++ b/packages/g6/__tests__/unit/utils/memoize.spec.ts @@ -0,0 +1,12 @@ +import { deepMemoize } from '@/src/utils/memoize'; + +describe('memoize utils', () => { + it('deepMemoize', () => { + const func = jest.fn((a: number, b: number) => a + b); + const memoizedFunc = deepMemoize(func); + + expect(memoizedFunc(1, 2)).toBe(3); + expect(memoizedFunc(1, 2)).toBe(3); + expect(memoizedFunc(2, 1)).toBe(3); + }); +}); diff --git a/packages/g6/src/transforms/map-node-size.ts b/packages/g6/src/transforms/map-node-size.ts index dba19e7fbc7..2c51847b5de 100644 --- a/packages/g6/src/transforms/map-node-size.ts +++ b/packages/g6/src/transforms/map-node-size.ts @@ -2,8 +2,9 @@ import { findShortestPath, pageRank } from '@antv/algorithm'; import { deepMix } from '@antv/util'; import type { RuntimeContext } from '../runtime/types'; import type { GraphData } from '../spec'; -import type { EdgeDirection, ID, Size, STDSize } from '../types'; +import type { EdgeDirection, ID, Node, Size, STDSize } from '../types'; import { idOf } from '../utils/id'; +import { deepMemoize } from '../utils/memoize'; import { linear, log, powerLaw, sqrt } from '../utils/scale'; import { parseSize } from '../utils/size'; import { reassignTo } from '../utils/transform'; @@ -118,9 +119,8 @@ export class MapNodeSize extends BaseTransform { maxSize, this.options.scale, ); - const element = this.context.element?.getElement(idOf(datum)); - const mergedNodeDatum = Object.assign(datum, { style: { size } }); - reassignTo(input, element ? 'update' : 'add', 'node', mergedNodeDatum, true); + const element = this.context.element?.getElement(idOf(datum)); + reassignTo(input, element ? 'update' : 'add', 'node', deepMix(datum, { style: { size } })); }); return input; } @@ -153,39 +153,41 @@ export class MapNodeSize extends BaseTransform { } } - private assignSizeByCentrality = ( - centrality: number, - minCentrality: number, - maxCentrality: number, - minSize: STDSize, - maxSize: STDSize, - scale: MapNodeSizeOptions['scale'], - ): STDSize => { - const domain: [number, number] = [minCentrality, maxCentrality]; - const rangeX: [number, number] = [minSize[0], maxSize[0]]; - const rangeY: [number, number] = [minSize[1], maxSize[1]]; - const rangeZ: [number, number] = [minSize[2], maxSize[2]]; + private assignSizeByCentrality = deepMemoize( + ( + centrality: number, + minCentrality: number, + maxCentrality: number, + minSize: STDSize, + maxSize: STDSize, + scale: MapNodeSizeOptions['scale'], + ): STDSize => { + const domain: [number, number] = [minCentrality, maxCentrality]; + const rangeX: [number, number] = [minSize[0], maxSize[0]]; + const rangeY: [number, number] = [minSize[1], maxSize[1]]; + const rangeZ: [number, number] = [minSize[2], maxSize[2]]; - const interpolate = (centrality: number, range: [number, number]): number => { - if (typeof scale === 'function') { - return scale(centrality, domain, range); - } - switch (scale) { - case 'linear': - return linear(centrality, domain, range); - case 'log': - return log(centrality, domain, range); - case 'pow': - return powerLaw(centrality, domain, range, 2); - case 'sqrt': - return sqrt(centrality, domain, range); - default: - return range[0]; - } - }; + const interpolate = (centrality: number, range: [number, number]): number => { + if (typeof scale === 'function') { + return scale(centrality, domain, range); + } + switch (scale) { + case 'linear': + return linear(centrality, domain, range); + case 'log': + return log(centrality, domain, range); + case 'pow': + return powerLaw(centrality, domain, range, 2); + case 'sqrt': + return sqrt(centrality, domain, range); + default: + return range[0]; + } + }; - return [interpolate(centrality, rangeX), interpolate(centrality, rangeY), interpolate(centrality, rangeZ)]; - }; + return [interpolate(centrality, rangeX), interpolate(centrality, rangeY), interpolate(centrality, rangeZ)]; + }, + ); } const initCentralityResult = (graphData: GraphData): CentralityResult => { @@ -205,28 +207,26 @@ const initCentralityResult = (graphData: GraphData): CentralityResult => { * @param weightPropertyName - 边的权重属性名 | The weight property name of the edge * @returns 每个节点的中介中心性值 | The betweenness centrality for each node */ -const calculateBetweennessCentrality = ( - graphData: GraphData, - directed?: boolean, - weightPropertyName?: string, -): CentralityResult => { - const centralityResult = initCentralityResult(graphData); - const { nodes = [] } = graphData; - nodes.forEach((source) => { - nodes.forEach((target) => { - if (source !== target) { - const { allPath } = findShortestPath(graphData, idOf(source), idOf(target), directed, weightPropertyName); - const pathCount = allPath.length; - (allPath as ID[][]).flat().forEach((nodeId) => { - if (nodeId !== idOf(source) && nodeId !== idOf(target)) { - centralityResult.set(nodeId, centralityResult.get(nodeId)! + 1 / pathCount); - } - }); - } +const calculateBetweennessCentrality = deepMemoize( + (graphData: GraphData, directed?: boolean, weightPropertyName?: string): CentralityResult => { + const centralityResult = initCentralityResult(graphData); + const { nodes = [] } = graphData; + nodes.forEach((source) => { + nodes.forEach((target) => { + if (source !== target) { + const { allPath } = findShortestPath(graphData, idOf(source), idOf(target), directed, weightPropertyName); + const pathCount = allPath.length; + (allPath as ID[][]).flat().forEach((nodeId) => { + if (nodeId !== idOf(source) && nodeId !== idOf(target)) { + centralityResult.set(nodeId, centralityResult.get(nodeId)! + 1 / pathCount); + } + }); + } + }); }); - }); - return centralityResult; -}; + return centralityResult; + }, +); /** * 计算图中每个节点的接近中心性 @@ -237,25 +237,23 @@ const calculateBetweennessCentrality = ( * @param weightPropertyName - 边的权重属性名 | The weight property name of the edge * @returns 每个节点的接近中心性值 | The closeness centrality for each node */ -const calculateClosenessCentrality = ( - graphData: GraphData, - directed?: boolean, - weightPropertyName?: string, -): CentralityResult => { - const centralityResult = new Map(); - const { nodes = [] } = graphData; - nodes.forEach((source) => { - const totalLength = nodes.reduce((acc, target) => { - if (source !== target) { - const { length } = findShortestPath(graphData, idOf(source), idOf(target), directed, weightPropertyName); - acc += length; - } - return acc; - }, 0); - centralityResult.set(idOf(source), 1 / totalLength); - }); - return centralityResult; -}; +const calculateClosenessCentrality = deepMemoize( + (graphData: GraphData, directed?: boolean, weightPropertyName?: string): CentralityResult => { + const centralityResult = new Map(); + const { nodes = [] } = graphData; + nodes.forEach((source) => { + const totalLength = nodes.reduce((acc, target) => { + if (source !== target) { + const { length } = findShortestPath(graphData, idOf(source), idOf(target), directed, weightPropertyName); + acc += length; + } + return acc; + }, 0); + centralityResult.set(idOf(source), 1 / totalLength); + }); + return centralityResult; + }, +); /** * 计算图中每个节点的 PageRank 中心性 @@ -266,14 +264,16 @@ const calculateClosenessCentrality = ( * @param linkProb - PageRank 算法的阻尼系数,指任意时刻,用户访问到某节点后继续访问该节点链接的下一个节点的概率,经验值 0.85 | The damping factor of the PageRank algorithm, which refers to the probability that a user will continue to visit the next node linked to a node at any time, with an empirical value of 0.85 * @returns 每个节点的 PageRank 中心性值 | The PageRank centrality for each node */ -const calculatePageRankCentrality = (graphData: GraphData, epsilon?: number, linkProb?: number): CentralityResult => { - const centralityResult = new Map(); - const data = pageRank(graphData, epsilon, linkProb); - graphData.nodes?.forEach((node) => { - centralityResult.set(idOf(node), data[idOf(node)]); - }); - return centralityResult; -}; +const calculatePageRankCentrality = deepMemoize( + (graphData: GraphData, epsilon?: number, linkProb?: number): CentralityResult => { + const centralityResult = new Map(); + const data = pageRank(graphData, epsilon, linkProb); + graphData.nodes?.forEach((node) => { + centralityResult.set(idOf(node), data[idOf(node)]); + }); + return centralityResult; + }, +); /** * 计算图中每个节点的特征向量中心性 @@ -283,7 +283,7 @@ const calculatePageRankCentrality = (graphData: GraphData, epsilon?: number, lin * @param directed - 是否为有向图 | Whether the graph is directed * @returns 每个节点的特征向量中心性值 The eigenvector centrality for each node. */ -const calculateEigenvectorCentrality = (graphData: GraphData, directed?: boolean): CentralityResult => { +const calculateEigenvectorCentrality = deepMemoize((graphData: GraphData, directed?: boolean): CentralityResult => { const { nodes = [] } = graphData; const adjacencyMatrix = createAdjacencyMatrix(graphData, directed); const eigenvector = powerIteration(adjacencyMatrix, nodes.length); @@ -294,7 +294,7 @@ const calculateEigenvectorCentrality = (graphData: GraphData, directed?: boolean }); return centralityResult; -}; +}); /** * 创建图的邻接矩阵 diff --git a/packages/g6/src/utils/layout.ts b/packages/g6/src/utils/layout.ts index 1bedba861be..c565efedcad 100644 --- a/packages/g6/src/utils/layout.ts +++ b/packages/g6/src/utils/layout.ts @@ -151,6 +151,9 @@ export function layoutAdapter( const result = { id, data: { + // grid 布局会直接读取 data[sortBy],兼容处理,需要避免用户 data 下使用 data, style 等字段 + // The grid layout will directly read data[sortBy], compatible processing, need to avoid users using data, style and other fields under data + ...data, data: { ...data }, // antv-dagre 会读取 data.parentId // antv-dagre will read data.parentId diff --git a/packages/g6/src/utils/memoize.ts b/packages/g6/src/utils/memoize.ts new file mode 100644 index 00000000000..12810108a47 --- /dev/null +++ b/packages/g6/src/utils/memoize.ts @@ -0,0 +1,25 @@ +import { isEqual } from '@antv/util'; + +/** + * 通过缓存函数调用的参数和结果,避免对相同参数的重复计算。与 lodash 的 memoize 不同,deepMemoize 使用深度比较来检查参数是否相等 + * + * Deep memoization function + * @param func - 函数 | Function + * @returns 计算结果 | Result + */ +export function deepMemoize any>(func: T): T { + const cache = new Map>(); + + return function (...args: Parameters): ReturnType { + for (const [key, value] of cache.entries()) { + if (isEqual(key, args)) { + return value; + } + } + + // @ts-expect-error this + const result = func.apply(this, args); + cache.set(args, result); + return result; + } as T; +} diff --git a/packages/g6/src/utils/transform.ts b/packages/g6/src/utils/transform.ts index ac14204b934..04b2df24e60 100644 --- a/packages/g6/src/utils/transform.ts +++ b/packages/g6/src/utils/transform.ts @@ -44,7 +44,7 @@ export const reassignTo = ( 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) : exitsDatum); + if (type === _type) value[typeName].set(id, merge ? deepMix(exitsDatum, datum) : datum); else value[typeName].delete(id); }); }; From c852a9117429bfc3cda73b062f1d3059ad420c83 Mon Sep 17 00:00:00 2001 From: yvonneyx Date: Wed, 18 Sep 2024 10:55:23 +0800 Subject: [PATCH 6/9] chore: increase limit-size --- packages/g6/package.json | 4 ++-- packages/g6/src/utils/memoize.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/g6/package.json b/packages/g6/package.json index 07e13f4908e..30b6285669f 100644 --- a/packages/g6/package.json +++ b/packages/g6/package.json @@ -88,11 +88,11 @@ "limit-size": [ { "gzip": true, - "limit": "300 Kb", + "limit": "310 Kb", "path": "dist/g6.min.js" }, { - "limit": "1 Mb", + "limit": "1.1 Mb", "path": "dist/g6.min.js" } ] diff --git a/packages/g6/src/utils/memoize.ts b/packages/g6/src/utils/memoize.ts index 12810108a47..0ab01bcabe4 100644 --- a/packages/g6/src/utils/memoize.ts +++ b/packages/g6/src/utils/memoize.ts @@ -17,7 +17,7 @@ export function deepMemoize any>(func: T): T { } } - // @ts-expect-error this + // @ts-expect-error ignore const result = func.apply(this, args); cache.set(args, result); return result; From 9aba04ae29f762f5ffe1ccbbb2d299eb5dbd1074 Mon Sep 17 00:00:00 2001 From: yvonneyx Date: Wed, 18 Sep 2024 15:38:14 +0800 Subject: [PATCH 7/9] fix: fix cr issues --- .../g6/__tests__/unit/utils/memoize.spec.ts | 12 -- .../g6/__tests__/unit/utils/scale.spec.ts | 11 +- packages/g6/src/transforms/map-node-size.ts | 167 +++++++++--------- packages/g6/src/utils/memoize.ts | 25 --- packages/g6/src/utils/scale.ts | 7 +- 5 files changed, 90 insertions(+), 132 deletions(-) delete mode 100644 packages/g6/__tests__/unit/utils/memoize.spec.ts delete mode 100644 packages/g6/src/utils/memoize.ts diff --git a/packages/g6/__tests__/unit/utils/memoize.spec.ts b/packages/g6/__tests__/unit/utils/memoize.spec.ts deleted file mode 100644 index bd45c2eef6c..00000000000 --- a/packages/g6/__tests__/unit/utils/memoize.spec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { deepMemoize } from '@/src/utils/memoize'; - -describe('memoize utils', () => { - it('deepMemoize', () => { - const func = jest.fn((a: number, b: number) => a + b); - const memoizedFunc = deepMemoize(func); - - expect(memoizedFunc(1, 2)).toBe(3); - expect(memoizedFunc(1, 2)).toBe(3); - expect(memoizedFunc(2, 1)).toBe(3); - }); -}); diff --git a/packages/g6/__tests__/unit/utils/scale.spec.ts b/packages/g6/__tests__/unit/utils/scale.spec.ts index 2e0cf79a5b8..e9e9269c4cc 100644 --- a/packages/g6/__tests__/unit/utils/scale.spec.ts +++ b/packages/g6/__tests__/unit/utils/scale.spec.ts @@ -1,4 +1,4 @@ -import { linear, log, powerLaw, sqrt } from '@/src/utils/scale'; +import { linear, log, pow, sqrt } from '@/src/utils/scale'; describe('scale', () => { it('linear', () => { @@ -14,10 +14,11 @@ describe('scale', () => { expect(log(1, [0, 1], [0, 100])).toEqual(100); }); - it('powerLaw', () => { - expect(powerLaw(0, [0, 1], [0, 100], 2)).toEqual(0); - expect(powerLaw(0.5, [0, 1], [0, 100], 2)).toEqual(25); - expect(powerLaw(1, [0, 1], [0, 100], 2)).toEqual(100); + it('pow', () => { + expect(pow(0, [0, 1], [0, 100], 2)).toEqual(0); + expect(pow(0.5, [0, 1], [0, 100])).toEqual(25); + expect(pow(0.5, [0, 1], [0, 100], 2)).toEqual(25); + expect(pow(1, [0, 1], [0, 100], 2)).toEqual(100); }); it('sqrt', () => { diff --git a/packages/g6/src/transforms/map-node-size.ts b/packages/g6/src/transforms/map-node-size.ts index 2c51847b5de..6c1c5289d5d 100644 --- a/packages/g6/src/transforms/map-node-size.ts +++ b/packages/g6/src/transforms/map-node-size.ts @@ -4,8 +4,7 @@ import type { RuntimeContext } from '../runtime/types'; import type { GraphData } from '../spec'; import type { EdgeDirection, ID, Node, Size, STDSize } from '../types'; import { idOf } from '../utils/id'; -import { deepMemoize } from '../utils/memoize'; -import { linear, log, powerLaw, sqrt } from '../utils/scale'; +import { linear, log, pow, sqrt } from '../utils/scale'; import { parseSize } from '../utils/size'; import { reassignTo } from '../utils/transform'; import type { BaseTransformOptions } from './base-transform'; @@ -89,7 +88,7 @@ type CentralityResult = Map; */ export class MapNodeSize extends BaseTransform { static defaultOptions: Partial = { - centrality: { type: 'eigenvector' }, + centrality: { type: 'degree' }, maxSize: 80, minSize: 20, scale: 'log', @@ -153,41 +152,39 @@ export class MapNodeSize extends BaseTransform { } } - private assignSizeByCentrality = deepMemoize( - ( - centrality: number, - minCentrality: number, - maxCentrality: number, - minSize: STDSize, - maxSize: STDSize, - scale: MapNodeSizeOptions['scale'], - ): STDSize => { - const domain: [number, number] = [minCentrality, maxCentrality]; - const rangeX: [number, number] = [minSize[0], maxSize[0]]; - const rangeY: [number, number] = [minSize[1], maxSize[1]]; - const rangeZ: [number, number] = [minSize[2], maxSize[2]]; + private assignSizeByCentrality = ( + centrality: number, + minCentrality: number, + maxCentrality: number, + minSize: STDSize, + maxSize: STDSize, + scale: MapNodeSizeOptions['scale'], + ): STDSize => { + const domain: [number, number] = [minCentrality, maxCentrality]; + const rangeX: [number, number] = [minSize[0], maxSize[0]]; + const rangeY: [number, number] = [minSize[1], maxSize[1]]; + const rangeZ: [number, number] = [minSize[2], maxSize[2]]; - const interpolate = (centrality: number, range: [number, number]): number => { - if (typeof scale === 'function') { - return scale(centrality, domain, range); - } - switch (scale) { - case 'linear': - return linear(centrality, domain, range); - case 'log': - return log(centrality, domain, range); - case 'pow': - return powerLaw(centrality, domain, range, 2); - case 'sqrt': - return sqrt(centrality, domain, range); - default: - return range[0]; - } - }; + const interpolate = (centrality: number, range: [number, number]): number => { + if (typeof scale === 'function') { + return scale(centrality, domain, range); + } + switch (scale) { + case 'linear': + return linear(centrality, domain, range); + case 'log': + return log(centrality, domain, range); + case 'pow': + return pow(centrality, domain, range, 2); + case 'sqrt': + return sqrt(centrality, domain, range); + default: + return range[0]; + } + }; - return [interpolate(centrality, rangeX), interpolate(centrality, rangeY), interpolate(centrality, rangeZ)]; - }, - ); + return [interpolate(centrality, rangeX), interpolate(centrality, rangeY), interpolate(centrality, rangeZ)]; + }; } const initCentralityResult = (graphData: GraphData): CentralityResult => { @@ -207,26 +204,28 @@ const initCentralityResult = (graphData: GraphData): CentralityResult => { * @param weightPropertyName - 边的权重属性名 | The weight property name of the edge * @returns 每个节点的中介中心性值 | The betweenness centrality for each node */ -const calculateBetweennessCentrality = deepMemoize( - (graphData: GraphData, directed?: boolean, weightPropertyName?: string): CentralityResult => { - const centralityResult = initCentralityResult(graphData); - const { nodes = [] } = graphData; - nodes.forEach((source) => { - nodes.forEach((target) => { - if (source !== target) { - const { allPath } = findShortestPath(graphData, idOf(source), idOf(target), directed, weightPropertyName); - const pathCount = allPath.length; - (allPath as ID[][]).flat().forEach((nodeId) => { - if (nodeId !== idOf(source) && nodeId !== idOf(target)) { - centralityResult.set(nodeId, centralityResult.get(nodeId)! + 1 / pathCount); - } - }); - } - }); +const calculateBetweennessCentrality = ( + graphData: GraphData, + directed?: boolean, + weightPropertyName?: string, +): CentralityResult => { + const centralityResult = initCentralityResult(graphData); + const { nodes = [] } = graphData; + nodes.forEach((source) => { + nodes.forEach((target) => { + if (source !== target) { + const { allPath } = findShortestPath(graphData, idOf(source), idOf(target), directed, weightPropertyName); + const pathCount = allPath.length; + (allPath as ID[][]).flat().forEach((nodeId) => { + if (nodeId !== idOf(source) && nodeId !== idOf(target)) { + centralityResult.set(nodeId, centralityResult.get(nodeId)! + 1 / pathCount); + } + }); + } }); - return centralityResult; - }, -); + }); + return centralityResult; +}; /** * 计算图中每个节点的接近中心性 @@ -237,23 +236,25 @@ const calculateBetweennessCentrality = deepMemoize( * @param weightPropertyName - 边的权重属性名 | The weight property name of the edge * @returns 每个节点的接近中心性值 | The closeness centrality for each node */ -const calculateClosenessCentrality = deepMemoize( - (graphData: GraphData, directed?: boolean, weightPropertyName?: string): CentralityResult => { - const centralityResult = new Map(); - const { nodes = [] } = graphData; - nodes.forEach((source) => { - const totalLength = nodes.reduce((acc, target) => { - if (source !== target) { - const { length } = findShortestPath(graphData, idOf(source), idOf(target), directed, weightPropertyName); - acc += length; - } - return acc; - }, 0); - centralityResult.set(idOf(source), 1 / totalLength); - }); - return centralityResult; - }, -); +const calculateClosenessCentrality = ( + graphData: GraphData, + directed?: boolean, + weightPropertyName?: string, +): CentralityResult => { + const centralityResult = new Map(); + const { nodes = [] } = graphData; + nodes.forEach((source) => { + const totalLength = nodes.reduce((acc, target) => { + if (source !== target) { + const { length } = findShortestPath(graphData, idOf(source), idOf(target), directed, weightPropertyName); + acc += length; + } + return acc; + }, 0); + centralityResult.set(idOf(source), 1 / totalLength); + }); + return centralityResult; +}; /** * 计算图中每个节点的 PageRank 中心性 @@ -264,16 +265,14 @@ const calculateClosenessCentrality = deepMemoize( * @param linkProb - PageRank 算法的阻尼系数,指任意时刻,用户访问到某节点后继续访问该节点链接的下一个节点的概率,经验值 0.85 | The damping factor of the PageRank algorithm, which refers to the probability that a user will continue to visit the next node linked to a node at any time, with an empirical value of 0.85 * @returns 每个节点的 PageRank 中心性值 | The PageRank centrality for each node */ -const calculatePageRankCentrality = deepMemoize( - (graphData: GraphData, epsilon?: number, linkProb?: number): CentralityResult => { - const centralityResult = new Map(); - const data = pageRank(graphData, epsilon, linkProb); - graphData.nodes?.forEach((node) => { - centralityResult.set(idOf(node), data[idOf(node)]); - }); - return centralityResult; - }, -); +const calculatePageRankCentrality = (graphData: GraphData, epsilon?: number, linkProb?: number): CentralityResult => { + const centralityResult = new Map(); + const data = pageRank(graphData, epsilon, linkProb); + graphData.nodes?.forEach((node) => { + centralityResult.set(idOf(node), data[idOf(node)]); + }); + return centralityResult; +}; /** * 计算图中每个节点的特征向量中心性 @@ -283,7 +282,7 @@ const calculatePageRankCentrality = deepMemoize( * @param directed - 是否为有向图 | Whether the graph is directed * @returns 每个节点的特征向量中心性值 The eigenvector centrality for each node. */ -const calculateEigenvectorCentrality = deepMemoize((graphData: GraphData, directed?: boolean): CentralityResult => { +const calculateEigenvectorCentrality = (graphData: GraphData, directed?: boolean): CentralityResult => { const { nodes = [] } = graphData; const adjacencyMatrix = createAdjacencyMatrix(graphData, directed); const eigenvector = powerIteration(adjacencyMatrix, nodes.length); @@ -294,7 +293,7 @@ const calculateEigenvectorCentrality = deepMemoize((graphData: GraphData, direct }); return centralityResult; -}); +}; /** * 创建图的邻接矩阵 diff --git a/packages/g6/src/utils/memoize.ts b/packages/g6/src/utils/memoize.ts deleted file mode 100644 index 0ab01bcabe4..00000000000 --- a/packages/g6/src/utils/memoize.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { isEqual } from '@antv/util'; - -/** - * 通过缓存函数调用的参数和结果,避免对相同参数的重复计算。与 lodash 的 memoize 不同,deepMemoize 使用深度比较来检查参数是否相等 - * - * Deep memoization function - * @param func - 函数 | Function - * @returns 计算结果 | Result - */ -export function deepMemoize any>(func: T): T { - const cache = new Map>(); - - return function (...args: Parameters): ReturnType { - for (const [key, value] of cache.entries()) { - if (isEqual(key, args)) { - return value; - } - } - - // @ts-expect-error ignore - const result = func.apply(this, args); - cache.set(args, result); - return result; - } as T; -} diff --git a/packages/g6/src/utils/scale.ts b/packages/g6/src/utils/scale.ts index ea818782133..88cca7a263d 100644 --- a/packages/g6/src/utils/scale.ts +++ b/packages/g6/src/utils/scale.ts @@ -44,12 +44,7 @@ export const log = (value: number, domain: [number, number], range: [number, num * @param exponent - 幂指数 | The exponent * @returns 映射后的值 | The mapped value */ -export const powerLaw = ( - value: number, - domain: [number, number], - range: [number, number], - exponent: number, -): number => { +export const pow = (value: number, domain: [number, number], range: [number, number], exponent: number = 2): number => { const [d0, d1] = domain; const [r0, r1] = range; From e5ca273fcd7db8c21cbd821d0d2058d159b70104 Mon Sep 17 00:00:00 2001 From: yvonneyx Date: Wed, 18 Sep 2024 17:02:58 +0800 Subject: [PATCH 8/9] test: add unit test --- .../demos/transform-map-node-size.ts | 2 +- .../transform-map-node-size/default.svg | 97 +++++++++++++++++++ .../transform-map-node-size.spec.ts | 2 + .../demo/performance-diagnosis-flowchart.js | 4 +- 4 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 packages/g6/__tests__/snapshots/transforms/transform-map-node-size/default.svg diff --git a/packages/g6/__tests__/demos/transform-map-node-size.ts b/packages/g6/__tests__/demos/transform-map-node-size.ts index d7108d1e1f1..64dd4d28795 100644 --- a/packages/g6/__tests__/demos/transform-map-node-size.ts +++ b/packages/g6/__tests__/demos/transform-map-node-size.ts @@ -18,7 +18,7 @@ export const transformMapNodeSize: TestCase = async (context) => { }, }, layout: { - type: 'force', + type: 'grid', }, transforms: [ { diff --git a/packages/g6/__tests__/snapshots/transforms/transform-map-node-size/default.svg b/packages/g6/__tests__/snapshots/transforms/transform-map-node-size/default.svg new file mode 100644 index 00000000000..5a9a1397c7e --- /dev/null +++ b/packages/g6/__tests__/snapshots/transforms/transform-map-node-size/default.svg @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + node-1 + + + + + + + + + + + + node-2 + + + + + + + + + + + + node-3 + + + + + + + + + + + + node-4 + + + + + + + + + + + + node-5 + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/unit/transforms/transform-map-node-size.spec.ts b/packages/g6/__tests__/unit/transforms/transform-map-node-size.spec.ts index c9f323eab7a..ca9abf50505 100644 --- a/packages/g6/__tests__/unit/transforms/transform-map-node-size.spec.ts +++ b/packages/g6/__tests__/unit/transforms/transform-map-node-size.spec.ts @@ -17,6 +17,8 @@ describe('transform map node size', () => { }); it('centrality', async () => { + await expect(graph).toMatchSnapshot(__filename); + graph.updateTransform({ key: 'map-node-size', centrality: { type: 'degree' }, diff --git a/packages/site/examples/scene-case/default/demo/performance-diagnosis-flowchart.js b/packages/site/examples/scene-case/default/demo/performance-diagnosis-flowchart.js index bf585908b70..4cf828bddf3 100644 --- a/packages/site/examples/scene-case/default/demo/performance-diagnosis-flowchart.js +++ b/packages/site/examples/scene-case/default/demo/performance-diagnosis-flowchart.js @@ -18,8 +18,8 @@ const COLOR_MAP = { class HoverElement extends HoverActivate { getActiveIds(event) { const { model, graph } = this.context; - const { targetType, target } = event; - const targetId = target.id; + const targetId = event.target.id; + const targetType = graph.getElementType(targetId); const ids = [targetId]; if (targetType === 'edge') { From 680ef045e963ef0a11d99f7018658da323b91068 Mon Sep 17 00:00:00 2001 From: yvonneyx Date: Wed, 18 Sep 2024 17:16:00 +0800 Subject: [PATCH 9/9] fix: modify code annotation --- .eslintrc.js | 1 + packages/g6/src/transforms/map-node-size.ts | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index b1c06d01e61..074753e74ad 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -56,6 +56,7 @@ module.exports = { // TODO: rules below will be set to 2 in the future 'jsdoc/require-jsdoc': 1, 'jsdoc/check-access': 1, + 'jsdoc/valid-types': 0, /** * js plugin rules */ diff --git a/packages/g6/src/transforms/map-node-size.ts b/packages/g6/src/transforms/map-node-size.ts index 6c1c5289d5d..16cf01b7556 100644 --- a/packages/g6/src/transforms/map-node-size.ts +++ b/packages/g6/src/transforms/map-node-size.ts @@ -19,7 +19,7 @@ export interface MapNodeSizeOptions extends BaseTransformOptions { * - `'closeness'`:接近中心性,通过节点到其他所有节点的最短路径长度总和的倒数来衡量其重要性。接近中心性高的节点通常能够更快地到达网络中的其他节点 * - `'eigenvector'`:特征向量中心性,通过节点与其他中心节点的连接程度来衡量其重要性。特征向量中心性高的节点通常连接着其他重要节点 * - `'pagerank'`:PageRank 中心性,通过节点被其他节点引用的次数来衡量其重要性,常用于有向图。PageRank 中心性高的节点通常在网络中具有较高的影响力,类似于网页排名算法 - * - 自定义中心性计算方法:`(graphData: GraphData) => CentralityResult`,其中 `graphData` 为图数据,`CentralityResult` 为节点 ID 到中心性值的映射 + * - 自定义中心性计算方法:`(graphData: GraphData) => Map`,其中 `graphData` 为图数据,`Map` 为节点 ID 到中心性值的映射 * * The method of measuring the node centrality * - `'degree'`: Degree centrality, measures centrality by the degree (number of connected edges) of a node. Nodes with high degree centrality usually have more direct connections and may play important roles in the network @@ -28,7 +28,7 @@ export interface MapNodeSizeOptions extends BaseTransformOptions { * - `'eigenvector'`: Eigenvector centrality, measures centrality by the degree of connection between a node and other central nodes. Nodes with high eigenvector centrality usually connect to other important nodes * - `'pagerank'`: PageRank centrality, measures centrality by the number of times a node is referenced by other nodes, commonly used in directed graphs. Nodes with high PageRank centrality usually have high influence in the network, similar to the page ranking algorithm * - Custom centrality calculation method: `(graphData: GraphData) => Map`, where `graphData` is the graph data, and `Map` is the mapping from node ID to centrality value - * @defaultValue `{ type: 'eigenvector' }` + * @defaultValue { type: 'eigenvector' } */ centrality?: | { type: 'degree'; direction?: EdgeDirection } @@ -41,14 +41,14 @@ export interface MapNodeSizeOptions extends BaseTransformOptions { * 节点最大尺寸 * * The maximum size of the node - * @defaultValue `80` + * @defaultValue 80 */ maxSize?: Size; /** * 节点最小尺寸 * * The minimum size of the node - * @defaultValue `20` + * @defaultValue 20 */ minSize?: Size; /** @@ -65,7 +65,7 @@ export interface MapNodeSizeOptions extends BaseTransformOptions { * - `'pow'`: Power-law scale, maps a value from one range to another range using power law, commonly used for cases where the difference in centrality values is large * - `'sqrt'`: Square root scale, maps a value from one range to another range using square root, commonly used for cases where the difference in centrality values is large * - Custom scale: `(value: number, domain: [number, number], range: [number, number]) => number`,where `value` is the value to be mapped, `domain` is the input range, and `range` is the output range - * @defaultValue `'log'` + * @defaultValue 'log' */ scale?: | 'linear' @@ -327,6 +327,7 @@ const createAdjacencyMatrix = (graphData: GraphData, directed?: boolean): number * 使用幂迭代法计算主特征向量 * * Calculate the principal eigenvector using the power iteration method + * @see https://en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors * @param matrix - 邻接矩阵 | The adjacency matrix * @param numNodes - 节点数量 | The number of nodes * @param maxIterations - 最大迭代次数 | The maximum number of iterations