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(); }); },