diff --git a/packages/layout/src/antv-dagre.ts b/packages/layout/src/antv-dagre.ts
index e3cc53f..5b805ee 100644
--- a/packages/layout/src/antv-dagre.ts
+++ b/packages/layout/src/antv-dagre.ts
@@ -14,7 +14,9 @@ import type {
Point,
PointTuple,
} from './types';
-import { cloneFormatData, formatNodeSize, formatNumberFn } from './util';
+import { cloneFormatData, formatNumberFn, formatSizeFn } from './util';
+import type { Size } from './util/size';
+import { parseSize } from './util/size';
/**
* 层次/流程图布局的配置项
@@ -75,7 +77,7 @@ export interface AntVDagreLayoutOptions {
* Used for collision detection when nodes overlap
* @defaultValue undefined
*/
- nodeSize?: number | number[] | ((nodeData: Node) => number);
+ nodeSize?: Size | ((nodeData: Node) => Size);
/**
* 节点间距(px)
*
@@ -275,7 +277,6 @@ export class AntVDagreLayout implements Layout {
// focusNode,
preset,
} = mergedOptions;
-
const g = new Graph({
tree: [],
});
@@ -288,21 +289,19 @@ export class AntVDagreLayout implements Layout {
horisep = ranksepfunc;
vertisep = nodesepfunc;
}
- const nodeSizeFunc = formatNodeSize(nodeSize, undefined);
+
+ const nodeSizeFunc = formatSizeFn(10, nodeSize, false);
// copy graph to g
const nodes: Node[] = graph.getAllNodes();
const edges: Edge[] = graph.getAllEdges();
nodes.forEach((node) => {
- const size = nodeSizeFunc(node);
+ const size = parseSize(nodeSizeFunc(node));
const verti = vertisep(node);
const hori = horisep(node);
- // FIXME: support 2 dimensions?
- // const width = size[0] + 2 * hori;
- // const height = size[1] + 2 * verti;
- const width = size + 2 * hori;
- const height = size + 2 * verti;
+ const width = size[0] + 2 * hori;
+ const height = size[1] + 2 * verti;
const layer = node.data.layer;
if (isNumber(layer)) {
// 如果有layer属性,加入到node的label中
diff --git a/packages/layout/src/concentric.ts b/packages/layout/src/concentric.ts
index eaad139..f3061a0 100644
--- a/packages/layout/src/concentric.ts
+++ b/packages/layout/src/concentric.ts
@@ -11,6 +11,7 @@ import type {
} from './types';
import { cloneFormatData, isArray } from './util';
import { handleSingleNodeGraph } from './util/common';
+import { parseSize } from './util/size';
const DEFAULTS_LAYOUT_OPTIONS: Partial = {
nodeSize: 30,
@@ -26,7 +27,7 @@ const DEFAULTS_LAYOUT_OPTIONS: Partial = {
/**
* 同心圆布局
- *
+ *
* Concentric layout
*/
export class ConcentricLayout implements Layout {
@@ -51,7 +52,7 @@ export class ConcentricLayout implements Layout {
* To directly assign the positions to the nodes.
*/
async assign(graph: Graph, options?: ConcentricLayoutOptions) {
- await this.genericConcentricLayout(true, graph, options);
+ await this.genericConcentricLayout(true, graph, options);
}
private async genericConcentricLayout(
@@ -112,7 +113,7 @@ export class ConcentricLayout implements Layout {
} else if (isFunction(nodeSize)) {
maxNodeSize = -Infinity;
nodes.forEach((node) => {
- const currentSize = nodeSize(node);
+ const currentSize = Math.max(...parseSize(nodeSize(node)));
if (currentSize > maxNodeSize) maxNodeSize = currentSize;
});
} else {
diff --git a/packages/layout/src/force-atlas2/index.ts b/packages/layout/src/force-atlas2/index.ts
index 249820f..56cf585 100644
--- a/packages/layout/src/force-atlas2/index.ts
+++ b/packages/layout/src/force-atlas2/index.ts
@@ -1,5 +1,5 @@
import { Graph as GGraph } from '@antv/graphlib';
-import { isFunction, isNumber, isObject } from '@antv/util';
+import { isNumber } from '@antv/util';
import type {
Edge,
EdgeData,
@@ -14,8 +14,9 @@ import type {
OutNodeData,
PointTuple,
} from '../types';
-import { cloneFormatData, isArray } from '../util';
+import { cloneFormatData, formatNodeSizeToNumber } from '../util';
import { handleSingleNodeGraph } from '../util/common';
+import type { Size } from '../util/size';
import Body from './body';
import Quad from './quad';
import QuadTree from './quad-tree';
@@ -57,7 +58,7 @@ type CalcGraph = GGraph;
/**
* Atlas2 力导向布局
- *
+ *
* Force Atlas 2 layout
*/
export class ForceAtlas2Layout implements Layout {
@@ -82,7 +83,7 @@ export class ForceAtlas2Layout implements Layout {
* To directly assign the positions to the nodes.
*/
async assign(graph: Graph, options?: ForceAtlas2LayoutOptions) {
- await this.genericForceAtlas2Layout(true, graph, options);
+ await this.genericForceAtlas2Layout(true, graph, options);
}
private async genericForceAtlas2Layout(
@@ -122,7 +123,7 @@ export class ForceAtlas2Layout implements Layout {
nodes: calcNodes,
edges: calcEdges,
});
- const sizes: SizeMap = this.getSizes(calcGraph, graph, nodeSize);
+ const sizes: SizeMap = this.getSizes(calcGraph, nodeSize);
this.run(calcGraph, graph, maxIteration, sizes, assign, mergedOptions);
@@ -164,41 +165,18 @@ export class ForceAtlas2Layout implements Layout {
* Init the node positions if there is no initial positions.
* And pre-calculate the size (max of width and height) for each node.
* @param calcGraph graph for calculation
- * @param graph origin graph
* @param nodeSize node size config from layout options
* @returns {SizeMap} node'id mapped to max of its width and height
*/
private getSizes(
calcGraph: CalcGraph,
- graph: Graph,
- nodeSize?: number | number[] | ((d?: Node) => number),
+ nodeSize?: Size | ((d?: Node) => Size),
): SizeMap {
const nodes = calcGraph.getAllNodes();
const sizes: SizeMap = {};
for (let i = 0; i < nodes.length; i += 1) {
- const { id, data } = nodes[i];
- sizes[id] = 10;
- if (isNumber(data.size)) {
- sizes[id] = data.size;
- } else if (isArray(data.size)) {
- if (!isNaN(data.size[0])) sizes[id] = Math.max(data.size[0]);
- if (!isNaN(data.size[1])) sizes[id] = Math.max(data.size[1]);
- } else if (isObject(data.size)) {
- // @ts-ignore
- sizes[id] = Math.max(data.size.width, data.size.height);
- } else if (isFunction(nodeSize)) {
- const originNode = graph.getNode(id);
- const size = nodeSize(originNode);
- if (isArray(size)) {
- sizes[id] = Math.max(...size);
- } else {
- sizes[id] = size;
- }
- } else if (isArray(nodeSize)) {
- sizes[id] = Math.max(...nodeSize);
- } else if (isNumber(nodeSize)) {
- sizes[id] = nodeSize;
- }
+ const node = nodes[i];
+ sizes[node.id] = formatNodeSizeToNumber(nodeSize, undefined)(node);
}
return sizes;
}
diff --git a/packages/layout/src/force/index.ts b/packages/layout/src/force/index.ts
index 839ae4e..6b51b3b 100644
--- a/packages/layout/src/force/index.ts
+++ b/packages/layout/src/force/index.ts
@@ -1,5 +1,5 @@
import { Graph as IGraph } from '@antv/graphlib';
-import { isFunction, isNumber, isObject } from '@antv/util';
+import { isNumber } from '@antv/util';
import type {
Edge,
ForceLayoutOptions,
@@ -11,7 +11,7 @@ import type {
OutNode,
Point,
} from '../types';
-import { formatNumberFn, isArray } from '../util';
+import { formatNodeSizeToNumber, formatNumberFn } from '../util';
import { forceNBody } from './force-n-body';
import {
CalcEdge,
@@ -42,7 +42,7 @@ const DEFAULTS_LAYOUT_OPTIONS: Partial = {
/**
* 力导向布局
- *
+ *
* Force-directed layout
*/
export class ForceLayout implements LayoutWithIterations {
@@ -85,7 +85,7 @@ export class ForceLayout implements LayoutWithIterations {
* To directly assign the positions to the nodes.
*/
async assign(graph: Graph, options?: ForceLayoutOptions) {
- await this.genericForceLayout(true, graph, options);
+ await this.genericForceLayout(true, graph, options);
}
/**
@@ -292,12 +292,7 @@ export class ForceLayout implements LayoutWithIterations {
graph: Graph,
): FormatedOptions {
const formattedOptions = { ...options } as FormatedOptions;
- const {
- width: propsWidth,
- height: propsHeight,
- getMass,
- nodeSize,
- } = options;
+ const { width: propsWidth, height: propsHeight, getMass } = options;
// === formating width, height, and center =====
formattedOptions.width =
@@ -326,34 +321,10 @@ export class ForceLayout implements LayoutWithIterations {
}
// === formating node size =====
-
- const nodeSpacingFunc = formatNumberFn(0, options.nodeSpacing);
- let nodeSizeFn;
- if (!nodeSize) {
- nodeSizeFn = (d?: Node) => {
- const { size } = d?.data || {};
- if (size) {
- if (isArray(size)) {
- return Math.max(size[0], size[1]) + nodeSpacingFunc(d);
- }
- if (isObject<{ width: number; height: number }>(size)) {
- return Math.max(size.width, size.height) + nodeSpacingFunc(d);
- }
- return (size as number) + nodeSpacingFunc(d);
- }
- return 10 + nodeSpacingFunc(d);
- };
- } else if (isFunction(nodeSize)) {
- nodeSizeFn = (d?: Node) => (nodeSize as Function)(d) + nodeSpacingFunc(d);
- } else if (isArray(nodeSize)) {
- nodeSizeFn = (d?: Node) => {
- const nodeSizeArr = nodeSize as [number, number];
- return Math.max(nodeSizeArr[0], nodeSizeArr[1]) + nodeSpacingFunc(d);
- };
- } else {
- nodeSizeFn = (d?: Node) => (nodeSize as number) + nodeSpacingFunc(d);
- }
- formattedOptions.nodeSize = nodeSizeFn;
+ formattedOptions.nodeSize = formatNodeSizeToNumber(
+ options.nodeSize,
+ options.nodeSpacing,
+ );
// === formating node / edge strengths =====
const linkDistanceFn = options.linkDistance
diff --git a/packages/layout/src/grid.ts b/packages/layout/src/grid.ts
index 5da9034..4853a72 100644
--- a/packages/layout/src/grid.ts
+++ b/packages/layout/src/grid.ts
@@ -9,8 +9,9 @@ import type {
OutNode,
PointTuple,
} from './types';
-import { cloneFormatData, formatNumberFn, formatSizeFn, isArray } from './util';
+import { cloneFormatData, formatNumberFn, formatSizeFn } from './util';
import { handleSingleNodeGraph } from './util/common';
+import { parseSize } from './util/size';
type RowsAndCols = {
rows: number;
@@ -46,7 +47,7 @@ const DEFAULTS_LAYOUT_OPTIONS: Partial = {
/**
* 网格布局
- *
+ *
* Grid layout
*/
export class GridLayout implements Layout {
@@ -69,7 +70,7 @@ export class GridLayout implements Layout {
* To directly assign the positions to the nodes.
*/
async assign(graph: Graph, options?: GridLayoutOptions) {
- await this.genericGridLayout(true, graph, options);
+ await this.genericGridLayout(true, graph, options);
}
private async genericGridLayout(
@@ -219,18 +220,7 @@ export class GridLayout implements Layout {
}
const oNode = graph.getNode(node.id);
- const res = nodeSize(oNode) || 30;
-
- let nodeW;
- let nodeH;
-
- if (isArray(res)) {
- nodeW = res[0];
- nodeH = res[1];
- } else {
- nodeW = res;
- nodeH = res;
- }
+ const [nodeW, nodeH] = parseSize(nodeSize(oNode) || 30);
const p =
nodeSpacing !== undefined ? nodeSpacing(node) : preventOverlapPadding;
diff --git a/packages/layout/src/radial/index.ts b/packages/layout/src/radial/index.ts
index 237c4f5..e9561a2 100644
--- a/packages/layout/src/radial/index.ts
+++ b/packages/layout/src/radial/index.ts
@@ -13,7 +13,7 @@ import type {
import {
cloneFormatData,
floydWarshall,
- formatNodeSize,
+ formatNodeSizeToNumber,
getAdjMatrix,
getEuclideanDistance,
} from '../util';
@@ -37,7 +37,7 @@ const DEFAULTS_LAYOUT_OPTIONS: Partial = {
/**
* 径向布局
- *
+ *
* Radial layout
*/
export class RadialLayout implements Layout {
@@ -193,7 +193,7 @@ export class RadialLayout implements Layout {
let nodeSizeFunc;
// stagger the overlapped nodes
if (preventOverlap) {
- nodeSizeFunc = formatNodeSize(nodeSize, nodeSpacing);
+ nodeSizeFunc = formatNodeSizeToNumber(nodeSize, nodeSpacing);
const nonoverlapForceParams: RadialNonoverlapForceOptions = {
nodes,
nodeSizeFunc,
diff --git a/packages/layout/src/types.ts b/packages/layout/src/types.ts
index c6ea6af..5743ef3 100644
--- a/packages/layout/src/types.ts
+++ b/packages/layout/src/types.ts
@@ -1,4 +1,5 @@
import { Edge as IEdge, Graph as IGraph, Node as INode } from '@antv/graphlib';
+import type { Size } from './util/size';
/**
* 节点数据
@@ -240,7 +241,7 @@ export interface CircularLayoutOptions {
*
* Node size (diameter). Used for collision detection when nodes overlap
*/
- nodeSize?: number | number[] | ((nodeData: Node) => number);
+ nodeSize?: Size | ((nodeData: Node) => Size);
}
export interface GridLayoutOptions {
@@ -290,7 +291,7 @@ export interface GridLayoutOptions {
*
* Node size (diameter). Used for collision detection when nodes overlap
*/
- nodeSize?: number | number[] | ((nodeData: Node) => number);
+ nodeSize?: Size | ((nodeData: Node) => Size);
/**
* 避免重叠时节点的间距 padding。preventOverlap 为 true 时生效
*
@@ -416,7 +417,7 @@ export interface ConcentricLayoutOptions {
*
* Node size (diameter). Used for collision detection when preventing node overlap
*/
- nodeSize?: number | PointTuple | ((nodeData: Node) => number);
+ nodeSize?: Size | ((nodeData: Node) => Size);
/**
* 第一个节点与最后一个节点之间的弧度差
*
@@ -564,14 +565,14 @@ export interface RadialLayoutOptions {
*
* Node size (diameter). Used for collision detection when preventing node overlap
*/
- nodeSize?: number | number[] | ((nodeData: Node) => number);
+ nodeSize?: Size | ((nodeData: Node) => Size);
/**
* preventOverlap 为 true 时生效, 防止重叠时节点边缘间距的最小值。可以是回调函数, 为不同节点设置不同的最小间距
*
* Effective when preventOverlap is true. The minimum edge spacing when preventing node overlap. It can be a callback function, and set different minimum spacing for different nodes
* @defaultValue 10
*/
- nodeSpacing?: number | Function;
+ nodeSpacing?: number | ((nodeData: Node) => number);
/**
* 防止重叠步骤的最大迭代次数
*
@@ -674,13 +675,13 @@ export interface D3ForceLayoutOptions {
*
* Node size (diameter). Used for collision detection when preventing node overlapping
*/
- nodeSize?: number | number[] | ((node?: Node) => number);
+ nodeSize?: Size | ((node?: Node) => Size);
/**
* preventOverlap 为 true 时生效, 防止重叠时节点边缘间距的最小值。可以是回调函数, 为不同节点设置不同的最小间距
*
* It takes effect when preventOverlap is true. The minimum spacing of the node edge when preventing node overlapping. It can be a callback function, and set different minimum spacing for different nodes
*/
- nodeSpacing?: number | number[] | ((node?: Node) => number);
+ nodeSpacing?: number | ((node?: Node) => number);
/**
* 当前的迭代收敛阈值
*
@@ -850,7 +851,7 @@ export interface ComboCombinedLayoutOptions {
* @example
* ```ts
* import { ForceLayout } from '@antv/layout';
- *
+ *
* outerLayout: new ForceLayout({
* gravity: 1,
* factor: 2,
@@ -870,7 +871,7 @@ export interface ComboCombinedLayoutOptions {
* @example
* ```ts
* import { ConcentricLayout } from '@antv/layout';
- *
+ *
* innerLayout: new ConcentricLayout({
* sortBy: 'id'
* });
@@ -995,7 +996,7 @@ export interface ForceLayoutOptions extends CommonForceLayoutOptions {
*
* The size of the node (diameter). Used for collision detection when preventing node overlap
*/
- nodeSize?: number | number[] | ((d?: Node) => number);
+ nodeSize?: Size | ((d?: Node) => Size);
/**
* preventOverlap 为 true 时生效, 防止重叠时节点边缘间距的最小值。可以是回调函数, 为不同节点设置不同的最小间距
*
@@ -1236,7 +1237,7 @@ export interface ForceAtlas2LayoutOptions extends CommonForceLayoutOptions {
*
* Node size (diameter). Used for collision detection when preventing node overlap
*/
- nodeSize?: number | number[] | ((node?: Node) => number);
+ nodeSize?: Size | ((node?: Node) => Size);
/**
* 每一次迭代的回调函数
*
diff --git a/packages/layout/src/util/function.ts b/packages/layout/src/util/function.ts
index 78747f7..2e12fab 100644
--- a/packages/layout/src/util/function.ts
+++ b/packages/layout/src/util/function.ts
@@ -1,5 +1,6 @@
import { isFunction, isNumber, isObject } from '@antv/util';
import { Node } from '../types';
+import { parseSize, type Size } from './size';
/**
* Format value with multiple types into a function returns number.
@@ -34,49 +35,44 @@ export function formatNumberFn(
export function formatSizeFn(
defaultValue: number,
value?:
- | number
- | number[]
+ | Size
| { width: number; height: number }
- | ((d?: T) => number)
+ | ((d?: T) => Size)
| undefined,
resultIsNumber: boolean = true,
-): (d: T) => number | number[] {
+): (d: T) => Size {
if (!value && value !== 0) {
return (d) => {
const { size } = d.data || {};
if (size) {
- if (Array.isArray(size)) {
- return size[0] > size[1] ? size[0] : size[1];
- }
- if (isObject<{ width: number; height: number }>(size)) {
- return size.width > size.height ? size.width : size.height;
+ if (Array.isArray(size))
+ return resultIsNumber ? Math.max(...size) || defaultValue : size;
+ if (
+ isObject<{ width: number; height: number }>(size) &&
+ size.width &&
+ size.height
+ ) {
+ return resultIsNumber
+ ? Math.max(size.width, size.height) || defaultValue
+ : [size.width, size.height];
}
return size;
}
return defaultValue;
};
}
- if (isFunction(value)) {
- return value;
- }
- if (isNumber(value)) {
- return () => value;
- }
+ if (isFunction(value)) return value;
+ if (isNumber(value)) return () => value;
if (Array.isArray(value)) {
return () => {
- if (resultIsNumber) {
- const max = Math.max(...value);
- return isNaN(max) ? defaultValue : max;
- }
+ if (resultIsNumber) return Math.max(...value) || defaultValue;
return value;
};
}
- if (isObject(value)) {
+ if (isObject(value) && value.width && value.height) {
return () => {
- if (resultIsNumber) {
- const max = Math.max(value.width, value.height);
- return isNaN(max) ? defaultValue : max;
- }
+ if (resultIsNumber)
+ return Math.max(value.width, value.height) || defaultValue;
return [value.width, value.height];
};
}
@@ -89,50 +85,40 @@ export function formatSizeFn(
* @param nodeSpacing
* @returns
*/
-export const formatNodeSize = (
- nodeSize: number | number[] | ((nodeData: Node) => number) | undefined,
- nodeSpacing: number | Function | undefined,
-): ((nodeData: Node) => number) => {
- let nodeSizeFunc;
- let nodeSpacingFunc: Function;
- if (isNumber(nodeSpacing)) {
- nodeSpacingFunc = () => nodeSpacing;
- } else if (isFunction(nodeSpacing)) {
- nodeSpacingFunc = nodeSpacing;
- } else {
- nodeSpacingFunc = () => 0;
- }
+export const formatNodeSizeToNumber = (
+ nodeSize: Size | ((node: Node) => Size) | undefined,
+ nodeSpacing: number | ((node: Node) => number) | undefined,
+ defaultNodeSize: number = 10,
+): ((node: Node) => number) => {
+ let nodeSizeFunc: (node: Node) => Size;
+ const nodeSpacingFunc =
+ typeof nodeSpacing === 'function' ? nodeSpacing : () => nodeSpacing || 0;
if (!nodeSize) {
nodeSizeFunc = (d: Node) => {
- if (d.data?.bboxSize) {
- return (
- Math.max(d.data.bboxSize[0], d.data.bboxSize[1]) + nodeSpacingFunc(d)
- );
- }
+ if (d.data?.bboxSize) return d.data?.bboxSize;
if (d.data?.size) {
- if (Array.isArray(d.data.size)) {
- return Math.max(d.data.size[0], d.data.size[1]) + nodeSpacingFunc(d);
- }
const dataSize = d.data.size;
- if (isObject<{ width: number; height: number }>(dataSize)) {
- const res =
- dataSize.width > dataSize.height ? dataSize.width : dataSize.height;
- return res + nodeSpacingFunc(d);
- }
- return dataSize + nodeSpacingFunc(d);
+ if (Array.isArray(dataSize)) return dataSize;
+ if (isObject<{ width: number; height: number }>(dataSize))
+ return [dataSize.width, dataSize.height];
+ return dataSize;
}
- return 10 + nodeSpacingFunc(d);
+ return defaultNodeSize;
};
} else if (Array.isArray(nodeSize)) {
- nodeSizeFunc = (d: Node) => {
- const res = nodeSize[0] > nodeSize[1] ? nodeSize[0] : nodeSize[1];
- return res + nodeSpacingFunc(d);
- };
+ nodeSizeFunc = (d: Node) => nodeSize;
} else if (isFunction(nodeSize)) {
- nodeSizeFunc = nodeSize as (nodeData: Node) => number;
+ nodeSizeFunc = nodeSize;
} else {
- nodeSizeFunc = (d: Node) => nodeSize + nodeSpacingFunc(d);
+ nodeSizeFunc = (d: Node) => nodeSize;
}
- return nodeSizeFunc;
+
+ const func = (d: Node) => {
+ const nodeSize = nodeSizeFunc(d) as Size;
+ const nodeSpacing = nodeSpacingFunc(d);
+ return Math.max(...parseSize(nodeSize)) + nodeSpacing;
+ };
+
+ return func;
};
diff --git a/vite.config.js b/vite.config.js
index 75b2abb..0b0ad65 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,32 +1,32 @@
-import { resolve } from "path";
-import { defineConfig } from "vite";
+import { resolve } from 'path';
+import { defineConfig } from 'vite';
export default defineConfig({
- root: "./site/",
+ root: './site/',
server: {
port: 8080,
- open: "/",
+ open: '/',
},
// publicDir: "../packages/layout-wasm/dist",
- base: "/layout/",
+ base: '/layout/',
build: {
rollupOptions: {
input: {
- main: resolve(__dirname, "site/index.html"),
- benchmark: resolve(__dirname, "site/benchmark/index.html"),
- "3d": resolve(__dirname, "site/3d/index.html"),
+ main: resolve(__dirname, 'site/index.html'),
+ benchmark: resolve(__dirname, 'site/benchmark/index.html'),
+ '3d': resolve(__dirname, 'site/3d/index.html'),
},
},
},
plugins: [
{
- name: "isolation",
+ name: 'isolation',
configureServer(server) {
// The multithreads version of @antv/layout-wasm needs to use SharedArrayBuffer, which should be used in a secure context.
// @see https://gist.github.com/mizchi/afcc5cf233c9e6943720fde4b4579a2b
server.middlewares.use((_req, res, next) => {
- res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
- res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
+ res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
next();
});
},