mirror of
https://gitee.com/antv/g6.git
synced 2024-11-29 18:28:19 +08:00
feat: dynamically toggle label visibility based on density to ensure adaptive display (#6398)
* feat: toggle label visibility * fix: fix ci issues * test: add test * refactor: rename toggle-label-visibility to auto-adapt-label * fix: add update method to unbind and rebind events when options are updated * refactor: rename page name * fix: update remarks * test: add unit tests * fix: update default value
This commit is contained in:
parent
7c87eb3576
commit
2bacd5d716
73
packages/g6/__tests__/demos/behavior-auto-adapt-label.ts
Normal file
73
packages/g6/__tests__/demos/behavior-auto-adapt-label.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import data from '@@/dataset/language-tree.json';
|
||||
import { Graph, IPointerEvent, type Element } from '@antv/g6';
|
||||
|
||||
export const behaviorAutoAdaptLabel: TestCase = async (context) => {
|
||||
const graph = new Graph({
|
||||
...context,
|
||||
padding: 20,
|
||||
theme: 'light',
|
||||
data,
|
||||
node: {
|
||||
style: {
|
||||
labelText: (d) => d.id,
|
||||
labelBackground: true,
|
||||
labelFontFamily: 'Gill Sans',
|
||||
labelFill: '#333',
|
||||
},
|
||||
state: {
|
||||
active: {
|
||||
label: true,
|
||||
},
|
||||
},
|
||||
palette: {
|
||||
type: 'group',
|
||||
color: 'tableau',
|
||||
field: 'group',
|
||||
},
|
||||
},
|
||||
edge: {
|
||||
style: {
|
||||
stroke: '#E2E3E1',
|
||||
endArrow: true,
|
||||
},
|
||||
},
|
||||
behaviors: [
|
||||
'drag-canvas',
|
||||
'zoom-canvas',
|
||||
function () {
|
||||
return {
|
||||
type: 'hover-activate',
|
||||
degree: 0,
|
||||
onHover: (e: IPointerEvent<Element>) => {
|
||||
this.frontElement(e.target.id);
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
key: 'auto-adapt-label',
|
||||
type: 'auto-adapt-label',
|
||||
},
|
||||
],
|
||||
layout: {
|
||||
type: 'd3-force',
|
||||
manyBody: { strength: -200 },
|
||||
x: {},
|
||||
y: {},
|
||||
},
|
||||
transforms: [
|
||||
{
|
||||
key: 'map-node-size',
|
||||
type: 'map-node-size',
|
||||
maxSize: 60,
|
||||
minSize: 12,
|
||||
scale: 'linear',
|
||||
},
|
||||
],
|
||||
plugins: [{ type: 'background', background: '#fff' }],
|
||||
animation: false,
|
||||
});
|
||||
|
||||
await graph.render();
|
||||
|
||||
return graph;
|
||||
};
|
@ -5,6 +5,7 @@ export { animationElementPosition } from './animation-element-position';
|
||||
export { animationElementState } from './animation-element-state';
|
||||
export { animationElementStateSwitch } from './animation-element-state-switch';
|
||||
export { animationElementStylePosition } from './animation-element-style-position';
|
||||
export { behaviorAutoAdaptLabel } from './behavior-auto-adapt-label';
|
||||
export { behaviorBrushSelect } from './behavior-brush-select';
|
||||
export { behaviorClickSelect } from './behavior-click-select';
|
||||
export { behaviorCreateEdge } from './behavior-create-edge';
|
||||
|
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 215 KiB |
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 215 KiB |
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 215 KiB |
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 215 KiB |
@ -0,0 +1,37 @@
|
||||
import { positionOf, type Graph } from '@/src';
|
||||
import { behaviorAutoAdaptLabel } from '@@/demos';
|
||||
import { createDemoGraph } from '@@/utils';
|
||||
|
||||
describe('behavior auto adapt label', () => {
|
||||
let graph: Graph;
|
||||
|
||||
beforeAll(async () => {
|
||||
graph = await createDemoGraph(behaviorAutoAdaptLabel, { animation: false });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
graph.destroy();
|
||||
});
|
||||
|
||||
it('default', async () => {
|
||||
await expect(graph).toMatchSnapshot(__filename);
|
||||
});
|
||||
|
||||
it('disable', async () => {
|
||||
graph.updateBehavior({ key: 'auto-adapt-label', enable: false });
|
||||
await expect(graph).toMatchSnapshot(__filename, 'disable');
|
||||
});
|
||||
|
||||
it('update options', async () => {
|
||||
graph.updateBehavior({ key: 'auto-adapt-label', enable: true, padding: 60 });
|
||||
await expect(graph).toMatchSnapshot(__filename, 'padding-60');
|
||||
});
|
||||
|
||||
it('update sorter', async () => {
|
||||
graph.updateBehavior({ key: 'auto-adapt-label', padding: 0 });
|
||||
const origin = positionOf(graph.getNodeData('Albanian'));
|
||||
graph.zoomTo(3, false, origin);
|
||||
await expect(graph).toMatchSnapshot(__filename, 'zoom-3');
|
||||
graph.zoomTo(1, false, origin);
|
||||
});
|
||||
});
|
@ -10,6 +10,7 @@ import {
|
||||
getNodeBBox,
|
||||
getPointBBox,
|
||||
getTriangleCenter,
|
||||
isBBoxInside,
|
||||
isPointInBBox,
|
||||
isPointOnBBoxBoundary,
|
||||
isPointOutsideBBox,
|
||||
@ -62,6 +63,17 @@ describe('bbox', () => {
|
||||
expect(getCombinedBBox([])).toEqual(new AABB());
|
||||
});
|
||||
|
||||
it('isBBoxInside', () => {
|
||||
const bbox1 = new AABB();
|
||||
bbox1.setMinMax([0, 0, 0], [1, 1, 1]);
|
||||
const bbox2 = new AABB();
|
||||
bbox2.setMinMax([0.5, 0.5, 0], [1.5, 1.5, 1]);
|
||||
const bbox3 = new AABB();
|
||||
bbox3.setMinMax([0, 0, 0], [2, 2, 2]);
|
||||
expect(isBBoxInside(bbox1, bbox2)).toBe(false);
|
||||
expect(isBBoxInside(bbox1, bbox3)).toBe(true);
|
||||
});
|
||||
|
||||
it('isPointInBBox', () => {
|
||||
expect(isPointInBBox([0.5, 0.5, 0], bbox)).toBe(true);
|
||||
expect(isPointInBBox([0, 0, 0], bbox)).toBe(true);
|
||||
|
252
packages/g6/src/behaviors/auto-adapt-label.ts
Normal file
252
packages/g6/src/behaviors/auto-adapt-label.ts
Normal file
@ -0,0 +1,252 @@
|
||||
import { AABB } from '@antv/g';
|
||||
import { groupBy, isFunction, throttle } from '@antv/util';
|
||||
import { GraphEvent } from '../constants';
|
||||
import type { RuntimeContext } from '../runtime/types';
|
||||
import type { Combo, Edge, Element, ID, IEvent, Node, NodeCentralityOptions, Padding } from '../types';
|
||||
import { getExpandedBBox, isBBoxInside } from '../utils/bbox';
|
||||
import { getNodeCentralities } from '../utils/centrality';
|
||||
import { arrayDiff } from '../utils/diff';
|
||||
import { setVisibility } from '../utils/visibility';
|
||||
import type { BaseBehaviorOptions } from './base-behavior';
|
||||
import { BaseBehavior } from './base-behavior';
|
||||
|
||||
/**
|
||||
* <zh/> 标签自适应显示配置项
|
||||
*
|
||||
* <en/> Auto Adapt Label Options
|
||||
*/
|
||||
export interface AutoAdaptLabelOptions extends BaseBehaviorOptions {
|
||||
/**
|
||||
* <zh/> 是否启用
|
||||
*
|
||||
* <en/> Whether to enable
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
enable?: boolean | ((event: IEvent) => boolean);
|
||||
/**
|
||||
* <zh/> 根据元素的重要性从高到低排序,重要性越高的元素其标签显示优先级越高。一般情况下 combo > node > edge
|
||||
*
|
||||
* <en/> Sort elements by their importance in descending order; elements with higher importance have higher label display priority; usually combo > node > edge
|
||||
*/
|
||||
sorter?: (labelElementsInViewport: Element[]) => Element[];
|
||||
/**
|
||||
* <zh/> 根据节点的重要性从高到低排序,重要性越高的节点其标签显示优先级越高。内置几种中心性算法,也可以自定义排序函数。需要注意,如果设置了 `sorter`,则 `nodeSorter` 不会生效
|
||||
*
|
||||
* <en/> Sort nodes by importance in descending order; nodes with higher importance have higher label display priority. Several centrality algorithms are built in, and custom sorting functions can also be defined. It should be noted that if `sorter` is set, `nodeSorter` will not take effect
|
||||
* @defaultValue { type: 'degree' }
|
||||
*/
|
||||
nodeSorter?: NodeCentralityOptions | ((labeledNodesInViewport: Node[]) => Node[]);
|
||||
/**
|
||||
* <zh/> 根据边的重要性从高到低排序,重要性越高的边其标签显示优先级越高。默认按照数据先后进行排序。需要注意,如果设置了 `sorter`,则 `edgeSorter` 不会生效
|
||||
*
|
||||
* <en/> Sort edges by importance in descending order; edges with higher importance have higher label display priority. By default, they are sorted according to the data. It should be noted that if `sorter` is set, `edgeSorter` will not take effect
|
||||
*/
|
||||
edgeSorter?: (labeledEdgesInViewport: Edge[]) => Edge[];
|
||||
/**
|
||||
* <zh/> 根据群组的重要性从高到低排序,重要性越高的群组其标签显示优先级越高。默认按照数据先后进行排序。需要注意,如果设置了 `sorter`,则 `comboSorter` 不会生效
|
||||
*
|
||||
* <en/> Sort combos by importance in descending order; combos with higher importance have higher label display priority. By default, they are sorted according to the data. It should be noted that if `sorter` is set, `comboSorter` will not take effect
|
||||
*/
|
||||
comboSorter?: (labeledCombosInViewport: Combo[]) => Combo[];
|
||||
/**
|
||||
* <zh/> 设置标签的内边距,用于判断标签是否重叠,以避免标签显示过于密集
|
||||
*
|
||||
* <en/> Set the padding of the label to determine whether the label overlaps to avoid the label being displayed too densely
|
||||
* @defaultValue 0
|
||||
*/
|
||||
padding?: Padding;
|
||||
/**
|
||||
* <zh/> 节流时间
|
||||
*
|
||||
* <en/> Throttle time
|
||||
* @defaultValue 32
|
||||
*/
|
||||
throttle?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* <zh/> 标签自适应显示
|
||||
*
|
||||
* <en/> Auto Adapt Label
|
||||
* @remarks
|
||||
* <zh/> 标签自适应显示是一种动态标签管理策略,旨在根据当前可视范围的空间分配、节点重要性等因素,智能调整哪些标签应显示或隐藏。通过对可视区域的实时分析,确保用户在不同的交互场景下获得最相关最清晰的信息展示,同时避免视觉过载和信息冗余。
|
||||
*
|
||||
* <en/ >Label Adaptive Display is a dynamic label management strategy designed to intelligently adjust which labels should be shown or hidden based on factors such as the spatial allocation of the current viewport and node importance. By analyzing the visible area in real-time, it ensures that users receive the most relevant and clear information display in various interactive scenarios, while avoiding visual overload and information redundancy.
|
||||
*/
|
||||
export class AutoAdaptLabel extends BaseBehavior<AutoAdaptLabelOptions> {
|
||||
static defaultOptions: Partial<AutoAdaptLabelOptions> = {
|
||||
enable: true,
|
||||
throttle: 100,
|
||||
padding: 0,
|
||||
nodeSorter: { type: 'degree' },
|
||||
};
|
||||
|
||||
constructor(context: RuntimeContext, options: AutoAdaptLabelOptions) {
|
||||
super(context, Object.assign({}, AutoAdaptLabel.defaultOptions, options));
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
public update(options: Partial<AutoAdaptLabelOptions>): void {
|
||||
this.unbindEvents();
|
||||
super.update(options);
|
||||
this.bindEvents();
|
||||
this.onToggleVisibility({} as IEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* <zh/> 检查当前包围盒是否有足够的空间进行展示;如果与已经展示的包围盒有重叠,或者超出视窗范围,则不会展示
|
||||
*
|
||||
* <en/> Check whether the current bounding box has enough space to display; if it overlaps with the displayed bounding box or exceeds the viewport range, it will not be displayed
|
||||
* @param bbox - bbox
|
||||
* @param bboxes - occupied bboxes which are already shown
|
||||
* @returns whether the bbox is overlapping with the bboxes or outside the viewpointBounds
|
||||
*/
|
||||
private isOverlapping = (bbox: AABB, bboxes: AABB[]) => {
|
||||
return !isBBoxInside(bbox, this.viewpointBounds) || bboxes.some((b) => bbox.intersects(b));
|
||||
};
|
||||
|
||||
private get viewpointBounds(): AABB {
|
||||
const { canvas } = this.context;
|
||||
|
||||
const [minX, minY] = canvas.getCanvasByViewport([0, 0]);
|
||||
const [maxX, maxY] = canvas.getCanvasByViewport(canvas.getSize());
|
||||
const viewpointBounds = new AABB();
|
||||
viewpointBounds.setMinMax([minX, minY, 0], [maxX, maxY, 0]);
|
||||
|
||||
return getExpandedBBox(viewpointBounds, 2);
|
||||
}
|
||||
|
||||
private occupiedBounds: AABB[] = [];
|
||||
|
||||
private detectLabelCollision = (elements: Element[]): { show: Element[]; hide: Element[] } => {
|
||||
const res: { show: Element[]; hide: Element[] } = { show: [], hide: [] };
|
||||
this.occupiedBounds = [];
|
||||
|
||||
elements.forEach((element) => {
|
||||
const labelBounds = element.getShape('label').getRenderBounds();
|
||||
if (!this.isOverlapping(labelBounds, this.occupiedBounds)) {
|
||||
res.show.push(element);
|
||||
this.occupiedBounds.push(getExpandedBBox(labelBounds, this.options.padding));
|
||||
} else {
|
||||
res.hide.push(element);
|
||||
}
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
private get labelElements(): Record<ID, Element> {
|
||||
// @ts-expect-error access private property
|
||||
const elements = Object.values(this.context.element.elementMap);
|
||||
const labelElements = elements.filter((el: Element) => el.isVisible() && el.getShape('label'));
|
||||
return Object.fromEntries(labelElements.map((el) => [el.id, el]));
|
||||
}
|
||||
|
||||
private getLabelElementsInView(): Element[] {
|
||||
const viewport = this.context.viewport!;
|
||||
return Object.values(this.labelElements).filter((node) =>
|
||||
viewport.isInViewport(node.getShape('key').getRenderBounds()),
|
||||
);
|
||||
}
|
||||
|
||||
private hideLabelIfExceedViewport = (prevElementsInView: Element[], currentElementsInView: Element[]) => {
|
||||
const { exit } = arrayDiff<Element>(prevElementsInView, currentElementsInView, (d) => d.id);
|
||||
exit?.forEach(this.hideLabel);
|
||||
};
|
||||
|
||||
private nodeCentralities: Map<ID, number> = new Map();
|
||||
|
||||
private sortNodesByCentrality = (nodes: Node[], centrality: NodeCentralityOptions) => {
|
||||
const { model } = this.context;
|
||||
const graphData = model.getData();
|
||||
const getRelatedEdgesData = model.getRelatedEdgesData.bind(model);
|
||||
|
||||
const nodesWithCentrality = nodes.map((node) => {
|
||||
if (!this.nodeCentralities.has(node.id)) {
|
||||
this.nodeCentralities = getNodeCentralities(graphData, getRelatedEdgesData, centrality);
|
||||
}
|
||||
return { node, centrality: this.nodeCentralities.get(node.id)! };
|
||||
});
|
||||
return nodesWithCentrality.sort((a, b) => b.centrality - a.centrality).map((item) => item.node);
|
||||
};
|
||||
|
||||
protected sortLabelElementsInView = (labelElements: Element[]): Element[] => {
|
||||
const { sorter, nodeSorter, comboSorter, edgeSorter } = this.options;
|
||||
|
||||
if (isFunction(this.options.sorter)) return sorter(labelElements);
|
||||
|
||||
const { node: nodes = [], edge: edges = [], combo: combos = [] } = groupBy(labelElements, (el) => (el as any).type);
|
||||
|
||||
const sortedCombos = isFunction(comboSorter) ? comboSorter(combos as Combo[]) : combos;
|
||||
|
||||
const sortedNodes = isFunction(nodeSorter)
|
||||
? nodeSorter(nodes as Node[])
|
||||
: this.sortNodesByCentrality(nodes as Node[], nodeSorter!);
|
||||
|
||||
const sortedEdges = isFunction(edgeSorter) ? edgeSorter(edges as Edge[]) : edges;
|
||||
|
||||
return [...sortedCombos, ...sortedNodes, ...sortedEdges];
|
||||
};
|
||||
|
||||
private labelElementsInView: Element[] = [];
|
||||
|
||||
protected onToggleVisibility = (event: IEvent) => {
|
||||
if (!this.validate(event)) {
|
||||
if (this.hiddenElements.size > 0) {
|
||||
this.hiddenElements.forEach(this.showLabel);
|
||||
this.hiddenElements.clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const labelElementsInView = this.getLabelElementsInView();
|
||||
this.hideLabelIfExceedViewport(this.labelElementsInView, labelElementsInView);
|
||||
this.labelElementsInView = labelElementsInView;
|
||||
|
||||
// 根据元素的重要性从高到低排序,重要性越高的元素其标签显示优先级越高;通常 combo > node > edge
|
||||
// Sort elements by their importance in descending order; elements with higher importance have higher label display priority; usually combo > node > edge
|
||||
const sortedElements = this.sortLabelElementsInView(this.labelElementsInView);
|
||||
const { show, hide } = this.detectLabelCollision(sortedElements);
|
||||
|
||||
show.forEach(this.showLabel);
|
||||
hide.forEach(this.hideLabel);
|
||||
};
|
||||
|
||||
private hiddenElements: Map<ID, Element> = new Map();
|
||||
|
||||
private hideLabel = (element: Element) => {
|
||||
const label = element.getShape('label');
|
||||
if (label) setVisibility(label, 'hidden');
|
||||
this.hiddenElements.set(element.id, element);
|
||||
};
|
||||
|
||||
private showLabel = (element: Element) => {
|
||||
const label = element.getShape('label');
|
||||
if (label) setVisibility(label, 'visible');
|
||||
this.hiddenElements.delete(element.id);
|
||||
};
|
||||
|
||||
protected onTransform = throttle(this.onToggleVisibility, this.options.throttle, { leading: true }) as () => void;
|
||||
|
||||
private bindEvents() {
|
||||
const { graph } = this.context;
|
||||
graph.once(GraphEvent.AFTER_RENDER, this.onToggleVisibility);
|
||||
graph.on(GraphEvent.AFTER_TRANSFORM, this.onTransform);
|
||||
}
|
||||
|
||||
private unbindEvents() {
|
||||
const { graph } = this.context;
|
||||
graph.off(GraphEvent.AFTER_TRANSFORM, this.onTransform);
|
||||
}
|
||||
|
||||
private validate(event: IEvent): boolean {
|
||||
if (this.destroyed) return false;
|
||||
const { enable } = this.options;
|
||||
if (isFunction(enable)) return enable(event);
|
||||
return !!enable;
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.unbindEvents();
|
||||
super.destroy();
|
||||
}
|
||||
}
|
@ -41,6 +41,7 @@ export interface FixElementSizeOptions extends BaseBehaviorOptions {
|
||||
* <zh/> 指定要固定大小的元素状态
|
||||
*
|
||||
* <en/> Specify the state of elements to be fixed in size
|
||||
* @defaultValue `'selected'`
|
||||
*/
|
||||
state?: State;
|
||||
/**
|
||||
@ -48,9 +49,9 @@ export interface FixElementSizeOptions extends BaseBehaviorOptions {
|
||||
*
|
||||
* <en/> Node configuration for defining which node attributes should remain fixed in size visually. If not specified (i.e., undefined), the entire node will be fixed in size.
|
||||
* @example
|
||||
* <zh/> 例如,在缩放过程中固定节点的 lineWidth 属性,可以配置如下:
|
||||
* <zh/> 如果在缩放过程中希望固定节点主图形的 lineWidth 属性,可以这样配置:
|
||||
*
|
||||
* <en/> For example, to fix `lineWidth` attribute of a node during zooming, you can configure it as follows:
|
||||
* <en/> If you want to fix the lineWidth attribute of the key shape of the node during zooming, you can configure it like this:
|
||||
* ```ts
|
||||
* {
|
||||
* node: [
|
||||
@ -60,6 +61,15 @@ export interface FixElementSizeOptions extends BaseBehaviorOptions {
|
||||
* },
|
||||
* ],
|
||||
* }
|
||||
*
|
||||
* <zh/> 如果希望固定节点标签的文字大小和行高,可以这样配置:
|
||||
*
|
||||
* <en/> If you want to fix the font size and line height of the node label, you can configure it like this:
|
||||
* ```ts
|
||||
* {
|
||||
* shape: (shapes: DisplayObject[]) => shapes.find((shape) => shape.parentElement?.className === 'label' && shape.className === 'text')!,
|
||||
* fields: ['fontSize', 'lineHeight'],
|
||||
* },
|
||||
* ```
|
||||
*/
|
||||
node?: FixShapeConfig | FixShapeConfig[];
|
||||
@ -146,7 +156,7 @@ export class FixElementSize extends BaseBehavior {
|
||||
configs.forEach((config: FixShapeConfig) => {
|
||||
const { shape: shapeFilter, fields } = config;
|
||||
const shape = shapeFilter(descendantShapes);
|
||||
|
||||
if (!shape) return;
|
||||
fields.forEach((field) => {
|
||||
if (!hasCachedStyle(shape, field)) cacheStyle(shape, field);
|
||||
const oriFieldValue = getCachedStyle(shape, field);
|
||||
|
@ -1,3 +1,4 @@
|
||||
export { AutoAdaptLabel } from './auto-adapt-label';
|
||||
export { BaseBehavior } from './base-behavior';
|
||||
export { BrushSelect } from './brush-select';
|
||||
export { ClickSelect } from './click-select';
|
||||
@ -14,6 +15,7 @@ export { OptimizeViewportTransform } from './optimize-viewport-transform';
|
||||
export { ScrollCanvas } from './scroll-canvas';
|
||||
export { ZoomCanvas } from './zoom-canvas';
|
||||
|
||||
export type { AutoAdaptLabelOptions } from './auto-adapt-label';
|
||||
export type { BaseBehaviorOptions } from './base-behavior';
|
||||
export type { BrushSelectOptions } from './brush-select';
|
||||
export type { ClickSelectOptions } from './click-select';
|
||||
|
@ -262,6 +262,7 @@ export type {
|
||||
LoopStyleProps,
|
||||
Node,
|
||||
NodeBadgeStyleProps,
|
||||
NodeCentralityOptions,
|
||||
NodeLabelStyleProps,
|
||||
NodeLikeData,
|
||||
NodePortStyleProps,
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
} from '@antv/g';
|
||||
import { ComboCollapse, ComboExpand, Fade, NodeCollapse, NodeExpand, PathIn, PathOut, Translate } from '../animations';
|
||||
import {
|
||||
AutoAdaptLabel,
|
||||
BrushSelect,
|
||||
ClickSelect,
|
||||
CollapseExpand,
|
||||
@ -128,6 +129,7 @@ const BUILT_IN_EXTENSIONS: ExtensionRegistry = {
|
||||
'focus-element': FocusElement,
|
||||
'hover-activate': HoverActivate,
|
||||
'lasso-select': LassoSelect,
|
||||
'auto-adapt-label': AutoAdaptLabel,
|
||||
'optimize-viewport-transform': OptimizeViewportTransform,
|
||||
'scroll-canvas': ScrollCanvas,
|
||||
'zoom-canvas': ZoomCanvas,
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { findShortestPath, pageRank } from '@antv/algorithm';
|
||||
import { deepMix } from '@antv/util';
|
||||
import { deepMix, isEqual } from '@antv/util';
|
||||
import type { RuntimeContext } from '../runtime/types';
|
||||
import type { GraphData } from '../spec';
|
||||
import type { EdgeDirection, ID, Node, Size, STDSize } from '../types';
|
||||
import type { ID, Node, NodeCentralityOptions, Size, STDSize } from '../types';
|
||||
import type { CentralityResult } from '../utils/centrality';
|
||||
import { getNodeCentralities } from '../utils/centrality';
|
||||
import { idOf } from '../utils/id';
|
||||
import { linear, log, pow, sqrt } from '../utils/scale';
|
||||
import { parseSize } from '../utils/size';
|
||||
@ -30,13 +31,7 @@ export interface MapNodeSizeOptions extends BaseTransformOptions {
|
||||
* - Custom centrality calculation method: `(graphData: GraphData) => Map<ID, number>`, where `graphData` is the graph data, and `Map<ID, number>` 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<ID, number>);
|
||||
centrality?: NodeCentralityOptions | ((graphData: GraphData) => Map<ID, number>);
|
||||
/**
|
||||
* <zh/> 节点最大尺寸
|
||||
*
|
||||
@ -75,8 +70,6 @@ export interface MapNodeSizeOptions extends BaseTransformOptions {
|
||||
| ((value: number, domain: [number, number], range: [number, number]) => number);
|
||||
}
|
||||
|
||||
type CentralityResult = Map<ID, number>;
|
||||
|
||||
/**
|
||||
* <zh/> 根据节点中心性调整节点的大小
|
||||
*
|
||||
@ -119,7 +112,10 @@ export class MapNodeSize extends BaseTransform<MapNodeSizeOptions> {
|
||||
this.options.scale,
|
||||
);
|
||||
const element = this.context.element?.getElement<Node>(idOf(datum));
|
||||
reassignTo(input, element ? 'update' : 'add', 'node', deepMix(datum, { style: { size } }));
|
||||
|
||||
if (!element || !isEqual(size, element.attributes.size)) {
|
||||
reassignTo(input, element ? 'update' : 'add', 'node', deepMix(datum, { style: { size } }));
|
||||
}
|
||||
});
|
||||
return input;
|
||||
}
|
||||
@ -130,26 +126,8 @@ export class MapNodeSize extends BaseTransform<MapNodeSizeOptions> {
|
||||
|
||||
if (typeof centrality === 'function') return centrality(graphData);
|
||||
|
||||
switch (centrality.type) {
|
||||
case 'degree': {
|
||||
const centralityResult = new Map<ID, number>();
|
||||
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);
|
||||
}
|
||||
const getRelatedEdgesData = model.getRelatedEdgesData.bind(model);
|
||||
return getNodeCentralities(graphData, getRelatedEdgesData, centrality);
|
||||
}
|
||||
|
||||
private assignSizeByCentrality = (
|
||||
@ -186,175 +164,3 @@ export class MapNodeSize extends BaseTransform<MapNodeSizeOptions> {
|
||||
return [interpolate(centrality, rangeX), interpolate(centrality, rangeY), interpolate(centrality, rangeZ)];
|
||||
};
|
||||
}
|
||||
|
||||
const initCentralityResult = (graphData: GraphData): CentralityResult => {
|
||||
const centralityResult = new Map<ID, number>();
|
||||
graphData.nodes?.forEach((node) => {
|
||||
centralityResult.set(idOf(node), 0);
|
||||
});
|
||||
return centralityResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* <zh/> 计算图中每个节点的中介中心性
|
||||
*
|
||||
* <en/> Calculate the betweenness centrality for each node in the graph
|
||||
* @param graphData - <zh/> 图数据 | <en/>Graph data
|
||||
* @param directed - <zh/> 是否为有向图 | <en/>Whether the graph is directed
|
||||
* @param weightPropertyName - <zh/> 边的权重属性名 | <en/>The weight property name of the edge
|
||||
* @returns <zh/> 每个节点的中介中心性值 | <en/>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;
|
||||
};
|
||||
|
||||
/**
|
||||
* <zh/> 计算图中每个节点的接近中心性
|
||||
*
|
||||
* <en/> Calculate the closeness centrality for each node in the graph
|
||||
* @param graphData - <zh/> 图数据 | <en/>Graph data
|
||||
* @param directed - <zh/> 是否为有向图 | <en/>Whether the graph is directed
|
||||
* @param weightPropertyName - <zh/> 边的权重属性名 | <en/>The weight property name of the edge
|
||||
* @returns <zh/> 每个节点的接近中心性值 | <en/>The closeness centrality for each node
|
||||
*/
|
||||
const calculateClosenessCentrality = (
|
||||
graphData: GraphData,
|
||||
directed?: boolean,
|
||||
weightPropertyName?: string,
|
||||
): CentralityResult => {
|
||||
const centralityResult = new Map<ID, number>();
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* <zh/> 计算图中每个节点的 PageRank 中心性
|
||||
*
|
||||
* <en/> Calculate the PageRank centrality for each node in the graph
|
||||
* @param graphData - <zh/> 图数据 | <en/>Graph data
|
||||
* @param epsilon - <zh/> PageRank 算法的收敛容差 | <en/>The convergence tolerance of the PageRank algorithm
|
||||
* @param linkProb - <zh/> PageRank 算法的阻尼系数,指任意时刻,用户访问到某节点后继续访问该节点链接的下一个节点的概率,经验值 0.85 | <en/>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 <zh/> 每个节点的 PageRank 中心性值 | <en/>The PageRank centrality for each node
|
||||
*/
|
||||
const calculatePageRankCentrality = (graphData: GraphData, epsilon?: number, linkProb?: number): CentralityResult => {
|
||||
const centralityResult = new Map<ID, number>();
|
||||
const data = pageRank(graphData, epsilon, linkProb);
|
||||
graphData.nodes?.forEach((node) => {
|
||||
centralityResult.set(idOf(node), data[idOf(node)]);
|
||||
});
|
||||
return centralityResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* <zh/> 计算图中每个节点的特征向量中心性
|
||||
*
|
||||
* <en/> Calculate the eigenvector centrality for each node in the graph.
|
||||
* @param graphData - <zh/> 图数据 | <en/>Graph data
|
||||
* @param directed - <zh/> 是否为有向图 | <en/>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<ID, number>();
|
||||
nodes.forEach((node, index) => {
|
||||
centralityResult.set(idOf(node), eigenvector[index]);
|
||||
});
|
||||
|
||||
return centralityResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* <zh/> 创建图的邻接矩阵
|
||||
*
|
||||
* <en/> Create the adjacency matrix for the graph.
|
||||
* @param graphData - <zh/> 图数据 | <en/>Graph data
|
||||
* @param directed - <zh/> 是否为有向图 | <en/>Whether the graph is directed
|
||||
* @returns <zh/> 邻接矩阵 | <en/>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;
|
||||
};
|
||||
|
||||
/**
|
||||
* <zh/> 使用幂迭代法计算主特征向量
|
||||
*
|
||||
* <en/> Calculate the principal eigenvector using the power iteration method
|
||||
* @see https://en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors
|
||||
* @param matrix - <zh/> 邻接矩阵 | <en/> The adjacency matrix
|
||||
* @param numNodes - <zh/> 节点数量 | <en/> The number of nodes
|
||||
* @param maxIterations - <zh/> 最大迭代次数 | <en/> The maximum number of iterations
|
||||
* @param tolerance - <zh/> 收敛容差 | <en/> The convergence tolerance
|
||||
* @returns <zh/> 主特征向量 | <en/> 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;
|
||||
};
|
||||
|
8
packages/g6/src/types/centrality.ts
Normal file
8
packages/g6/src/types/centrality.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import type { EdgeDirection } from './edge';
|
||||
|
||||
export type NodeCentralityOptions =
|
||||
| { 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 };
|
@ -1,6 +1,7 @@
|
||||
export type * from './anchor';
|
||||
export type * from './animation';
|
||||
export type * from './canvas';
|
||||
export type * from './centrality';
|
||||
export type * from './change';
|
||||
export type * from './combo';
|
||||
export type * from './data';
|
||||
|
@ -105,6 +105,23 @@ export function getCombinedBBox(bboxes: AABB[]): AABB {
|
||||
return bbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* <zh/> 判断 bbox1 是否完全包含在 bbox2 内
|
||||
*
|
||||
* <en/> Determine whether bbox1 is completely contained in bbox2
|
||||
* @param bbox1 - <zh/> 目标包围盒 | <en/> Target bounding box
|
||||
* @param bbox2 - <zh/> 参考包围盒 | <en/> Reference bounding box
|
||||
* @returns <zh/> 如果 bbox1 完全包含在 bbox2 内返回 true,否则返回 false | <en/> Returns true if bbox1 is completely contained in bbox2, false otherwise
|
||||
*/
|
||||
export function isBBoxInside(bbox1: AABB, bbox2: AABB): boolean {
|
||||
const [minX1, minY1] = bbox1.min;
|
||||
const [maxX1, maxY1] = bbox1.max;
|
||||
const [minX2, minY2] = bbox2.min;
|
||||
const [maxX2, maxY2] = bbox2.max;
|
||||
|
||||
return minX1 >= minX2 && maxX1 <= maxX2 && minY1 >= minY2 && maxY1 <= maxY2;
|
||||
}
|
||||
|
||||
/**
|
||||
* <zh/> 判断点是否在给定的包围盒内
|
||||
*
|
||||
|
209
packages/g6/src/utils/centrality.ts
Normal file
209
packages/g6/src/utils/centrality.ts
Normal file
@ -0,0 +1,209 @@
|
||||
import { findShortestPath, pageRank } from '@antv/algorithm';
|
||||
import type { EdgeData, GraphData } from '../spec';
|
||||
import type { EdgeDirection, ID, NodeCentralityOptions } from '../types';
|
||||
import { idOf } from './id';
|
||||
|
||||
export type CentralityResult = Map<ID, number>;
|
||||
|
||||
export const getNodeCentralities = (
|
||||
graphData: GraphData,
|
||||
getRelatedEdgesData: (id: ID, direction?: EdgeDirection) => EdgeData[],
|
||||
centrality: NodeCentralityOptions,
|
||||
) => {
|
||||
switch (centrality.type) {
|
||||
case 'degree': {
|
||||
const centralityResult = new Map<ID, number>();
|
||||
graphData.nodes?.forEach((node) => {
|
||||
const degree = getRelatedEdgesData(idOf(node), centrality.direction).length;
|
||||
centralityResult.set(idOf(node), degree);
|
||||
});
|
||||
return centralityResult;
|
||||
}
|
||||
case 'betweenness':
|
||||
return computeNodeBetweennessCentrality(graphData, centrality.directed, centrality.weightPropertyName);
|
||||
case 'closeness':
|
||||
return computeNodeClosenessCentrality(graphData, centrality.directed, centrality.weightPropertyName);
|
||||
case 'eigenvector':
|
||||
return computeNodeEigenvectorCentrality(graphData, centrality.directed);
|
||||
case 'pagerank':
|
||||
return computeNodePageRankCentrality(graphData, centrality.epsilon, centrality.linkProb);
|
||||
default:
|
||||
return initCentralityResult(graphData);
|
||||
}
|
||||
};
|
||||
|
||||
export const initCentralityResult = (graphData: GraphData): CentralityResult => {
|
||||
const centralityResult = new Map<ID, number>();
|
||||
graphData.nodes?.forEach((node) => {
|
||||
centralityResult.set(idOf(node), 0);
|
||||
});
|
||||
return centralityResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* <zh/> 计算图中每个节点的中介中心性
|
||||
*
|
||||
* <en/> Calculate the betweenness centrality for each node in the graph
|
||||
* @param graphData - <zh/> 图数据 | <en/>Graph data
|
||||
* @param directed - <zh/> 是否为有向图 | <en/>Whether the graph is directed
|
||||
* @param weightPropertyName - <zh/> 边的权重属性名 | <en/>The weight property name of the edge
|
||||
* @returns <zh/> 每个节点的中介中心性值 | <en/>The betweenness centrality for each node
|
||||
*/
|
||||
export const computeNodeBetweennessCentrality = (
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* <zh/> 计算图中每个节点的接近中心性
|
||||
*
|
||||
* <en/> Calculate the closeness centrality for each node in the graph
|
||||
* @param graphData - <zh/> 图数据 | <en/>Graph data
|
||||
* @param directed - <zh/> 是否为有向图 | <en/>Whether the graph is directed
|
||||
* @param weightPropertyName - <zh/> 边的权重属性名 | <en/>The weight property name of the edge
|
||||
* @returns <zh/> 每个节点的接近中心性值 | <en/>The closeness centrality for each node
|
||||
*/
|
||||
export const computeNodeClosenessCentrality = (
|
||||
graphData: GraphData,
|
||||
directed?: boolean,
|
||||
weightPropertyName?: string,
|
||||
): CentralityResult => {
|
||||
const centralityResult = new Map<ID, number>();
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* <zh/> 计算图中每个节点的 PageRank 中心性
|
||||
*
|
||||
* <en/> Calculate the PageRank centrality for each node in the graph
|
||||
* @param graphData - <zh/> 图数据 | <en/>Graph data
|
||||
* @param epsilon - <zh/> PageRank 算法的收敛容差 | <en/>The convergence tolerance of the PageRank algorithm
|
||||
* @param linkProb - <zh/> PageRank 算法的阻尼系数,指任意时刻,用户访问到某节点后继续访问该节点链接的下一个节点的概率,经验值 0.85 | <en/>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 <zh/> 每个节点的 PageRank 中心性值 | <en/>The PageRank centrality for each node
|
||||
*/
|
||||
export const computeNodePageRankCentrality = (
|
||||
graphData: GraphData,
|
||||
epsilon?: number,
|
||||
linkProb?: number,
|
||||
): CentralityResult => {
|
||||
const centralityResult = new Map<ID, number>();
|
||||
const data = pageRank(graphData, epsilon, linkProb);
|
||||
graphData.nodes?.forEach((node) => {
|
||||
centralityResult.set(idOf(node), data[idOf(node)]);
|
||||
});
|
||||
return centralityResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* <zh/> 计算图中每个节点的特征向量中心性
|
||||
*
|
||||
* <en/> Calculate the eigenvector centrality for each node in the graph.
|
||||
* @param graphData - <zh/> 图数据 | <en/>Graph data
|
||||
* @param directed - <zh/> 是否为有向图 | <en/>Whether the graph is directed
|
||||
* @returns 每个节点的特征向量中心性值 The eigenvector centrality for each node.
|
||||
*/
|
||||
export const computeNodeEigenvectorCentrality = (graphData: GraphData, directed?: boolean): CentralityResult => {
|
||||
const { nodes = [] } = graphData;
|
||||
const adjacencyMatrix = createAdjacencyMatrix(graphData, directed);
|
||||
const eigenvector = powerIteration(adjacencyMatrix, nodes.length);
|
||||
|
||||
const centralityResult = new Map<ID, number>();
|
||||
nodes.forEach((node, index) => {
|
||||
centralityResult.set(idOf(node), eigenvector[index]);
|
||||
});
|
||||
|
||||
return centralityResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* <zh/> 创建图的邻接矩阵
|
||||
*
|
||||
* <en/> Create the adjacency matrix for the graph.
|
||||
* @param graphData - <zh/> 图数据 | <en/>Graph data
|
||||
* @param directed - <zh/> 是否为有向图 | <en/>Whether the graph is directed
|
||||
* @returns <zh/> 邻接矩阵 | <en/>The adjacency matrix
|
||||
*/
|
||||
export 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* <zh/> 使用幂迭代法计算主特征向量
|
||||
*
|
||||
* <en/> Calculate the principal eigenvector using the power iteration method
|
||||
* @see https://en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors
|
||||
* @param matrix - <zh/> 邻接矩阵 | <en/> The adjacency matrix
|
||||
* @param numNodes - <zh/> 节点数量 | <en/> The number of nodes
|
||||
* @param maxIterations - <zh/> 最大迭代次数 | <en/> The maximum number of iterations
|
||||
* @param tolerance - <zh/> 收敛容差 | <en/> The convergence tolerance
|
||||
* @returns <zh/> 主特征向量 | <en/> 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;
|
||||
};
|
@ -44,6 +44,7 @@
|
||||
"RadialLayout": ["Radial", "径向布局"],
|
||||
"RandomLayout": ["Random", "随机布局"],
|
||||
"BaseBehavior": ["BaseBehavior", "基础交互"],
|
||||
"AutoAdaptLabel": ["AutoAdaptLabel", "标签自适应显示"],
|
||||
"BrushSelect": ["BrushSelect", "框选"],
|
||||
"ClickSelect": ["ClickSelect", "点击选中"],
|
||||
"CollapseExpand": ["CollapseExpand", "展开/收起元素"],
|
||||
|
Loading…
Reference in New Issue
Block a user