@antv/layout 2.0.0-beta.0 → 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/dist/index.js +296 -209
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.min.js +2 -2
  4. package/dist/index.min.js.map +1 -1
  5. package/dist/worker.js +1 -1
  6. package/dist/worker.js.map +1 -1
  7. package/lib/algorithm/antv-dagre/index.js +8 -7
  8. package/lib/algorithm/antv-dagre/index.js.map +1 -1
  9. package/lib/algorithm/antv-dagre/types.d.ts +3 -14
  10. package/lib/algorithm/circular/index.js +8 -7
  11. package/lib/algorithm/circular/index.js.map +1 -1
  12. package/lib/algorithm/circular/types.d.ts +0 -16
  13. package/lib/algorithm/combo-combined/index.js +9 -7
  14. package/lib/algorithm/combo-combined/index.js.map +1 -1
  15. package/lib/algorithm/combo-combined/types.d.ts +6 -18
  16. package/lib/algorithm/concentric/index.js +10 -16
  17. package/lib/algorithm/concentric/index.js.map +1 -1
  18. package/lib/algorithm/concentric/types.d.ts +3 -17
  19. package/lib/algorithm/d3-force/index.js +16 -12
  20. package/lib/algorithm/d3-force/index.js.map +1 -1
  21. package/lib/algorithm/d3-force/types.d.ts +10 -24
  22. package/lib/algorithm/d3-force-3d/index.js +1 -1
  23. package/lib/algorithm/d3-force-3d/index.js.map +1 -1
  24. package/lib/algorithm/d3-force-3d/types.d.ts +5 -4
  25. package/lib/algorithm/dagre/index.js +10 -7
  26. package/lib/algorithm/dagre/index.js.map +1 -1
  27. package/lib/algorithm/dagre/types.d.ts +8 -14
  28. package/lib/algorithm/force/index.d.ts +1 -1
  29. package/lib/algorithm/force/index.js +56 -44
  30. package/lib/algorithm/force/index.js.map +1 -1
  31. package/lib/algorithm/force/types.d.ts +39 -36
  32. package/lib/algorithm/force-atlas2/index.d.ts +1 -1
  33. package/lib/algorithm/force-atlas2/index.js +8 -7
  34. package/lib/algorithm/force-atlas2/index.js.map +1 -1
  35. package/lib/algorithm/force-atlas2/types.d.ts +0 -14
  36. package/lib/algorithm/fruchterman/index.d.ts +1 -1
  37. package/lib/algorithm/fruchterman/index.js +8 -10
  38. package/lib/algorithm/fruchterman/index.js.map +1 -1
  39. package/lib/algorithm/fruchterman/simulation.js +2 -1
  40. package/lib/algorithm/fruchterman/simulation.js.map +1 -1
  41. package/lib/algorithm/fruchterman/types.d.ts +2 -1
  42. package/lib/algorithm/grid/index.d.ts +1 -1
  43. package/lib/algorithm/grid/index.js +29 -34
  44. package/lib/algorithm/grid/index.js.map +1 -1
  45. package/lib/algorithm/grid/types.d.ts +5 -24
  46. package/lib/algorithm/mds/index.js +1 -0
  47. package/lib/algorithm/mds/index.js.map +1 -1
  48. package/lib/algorithm/radial/index.js +8 -5
  49. package/lib/algorithm/radial/index.js.map +1 -1
  50. package/lib/algorithm/radial/radial-nonoverlap-force.js +2 -4
  51. package/lib/algorithm/radial/radial-nonoverlap-force.js.map +1 -1
  52. package/lib/algorithm/radial/types.d.ts +3 -16
  53. package/lib/algorithm/random/index.js +1 -0
  54. package/lib/algorithm/random/index.js.map +1 -1
  55. package/lib/algorithm/types.d.ts +16 -0
  56. package/lib/index.d.ts +5 -3
  57. package/lib/index.js +3 -1
  58. package/lib/index.js.map +1 -1
  59. package/lib/node_modules/@antv/expr/dist/index.esm.js +4 -0
  60. package/lib/node_modules/@antv/expr/dist/index.esm.js.map +1 -0
  61. package/lib/types/common.d.ts +17 -1
  62. package/lib/types/data.d.ts +1 -1
  63. package/lib/util/expr.d.ts +12 -0
  64. package/lib/util/expr.js +26 -0
  65. package/lib/util/expr.js.map +1 -0
  66. package/lib/util/format.d.ts +37 -0
  67. package/lib/util/format.js +61 -17
  68. package/lib/util/format.js.map +1 -1
  69. package/lib/util/order.d.ts +3 -4
  70. package/lib/util/order.js.map +1 -1
  71. package/lib/util/size.d.ts +2 -1
  72. package/lib/util/size.js +9 -1
  73. package/lib/util/size.js.map +1 -1
  74. package/lib/worker.js +283 -227
  75. package/lib/worker.js.map +1 -1
  76. package/package.json +2 -1
  77. package/src/algorithm/antv-dagre/index.ts +15 -12
  78. package/src/algorithm/antv-dagre/types.ts +3 -14
  79. package/src/algorithm/circular/index.ts +5 -4
  80. package/src/algorithm/circular/types.ts +0 -15
  81. package/src/algorithm/combo-combined/index.ts +21 -17
  82. package/src/algorithm/combo-combined/types.ts +6 -21
  83. package/src/algorithm/concentric/index.ts +18 -16
  84. package/src/algorithm/concentric/types.ts +2 -16
  85. package/src/algorithm/d3-force/index.ts +25 -18
  86. package/src/algorithm/d3-force/types.ts +9 -22
  87. package/src/algorithm/d3-force-3d/index.ts +1 -1
  88. package/src/algorithm/d3-force-3d/types.ts +5 -0
  89. package/src/algorithm/dagre/index.ts +9 -7
  90. package/src/algorithm/dagre/types.ts +7 -15
  91. package/src/algorithm/force/index.ts +64 -40
  92. package/src/algorithm/force/types.ts +76 -45
  93. package/src/algorithm/force-atlas2/index.ts +13 -15
  94. package/src/algorithm/force-atlas2/types.ts +0 -12
  95. package/src/algorithm/fruchterman/index.ts +7 -15
  96. package/src/algorithm/fruchterman/simulation.ts +2 -2
  97. package/src/algorithm/fruchterman/types.ts +5 -2
  98. package/src/algorithm/grid/index.ts +45 -46
  99. package/src/algorithm/grid/types.ts +6 -35
  100. package/src/algorithm/radial/index.ts +14 -6
  101. package/src/algorithm/radial/radial-nonoverlap-force.ts +11 -6
  102. package/src/algorithm/radial/types.ts +2 -15
  103. package/src/algorithm/types.ts +18 -0
  104. package/src/types/common.ts +22 -0
  105. package/src/types/data.ts +1 -1
  106. package/src/util/expr.ts +26 -0
  107. package/src/util/format.ts +71 -27
  108. package/src/util/index.ts +2 -0
  109. package/src/util/order.ts +3 -9
  110. package/src/util/size.ts +8 -0
@@ -1,13 +1,13 @@
1
- import { BaseLayout } from '../base-layout';
2
- import { LayoutNode, Point } from '../../types';
3
- import { applySingleNodeLayout, normalizeViewport, parseSize } from '../../util';
4
- import { formatNumberFn, formatSizeFn } from '../../util/format';
5
- import { orderByDegree, orderById, orderBySorter } from '../../util/order';
6
1
  import type { GraphLib } from '../../model/data';
2
+ import { LayoutNode, NodeData, Point, Sorter } from '../../types';
3
+ import { applySingleNodeLayout, normalizeViewport } from '../../util';
4
+ import { formatFn, formatNodeSizeFn } from '../../util/format';
5
+ import { orderByDegree, orderById, orderBySorter } from '../../util/order';
6
+ import { BaseLayout } from '../base-layout';
7
7
  import type {
8
8
  GridLayoutOptions,
9
9
  IdMapRowAndCol,
10
- NormalizedGridLayoutOptions,
10
+ ParsedGridLayoutOptions,
11
11
  RowAndCol,
12
12
  RowsAndCols,
13
13
  VisitMap,
@@ -15,6 +15,20 @@ import type {
15
15
 
16
16
  export type { GridLayoutOptions };
17
17
 
18
+ const DEFAULT_LAYOUT_OPTIONS: Partial<GridLayoutOptions> = {
19
+ begin: [0, 0],
20
+ preventOverlap: true,
21
+ condense: false,
22
+ rows: undefined,
23
+ cols: undefined,
24
+ position: undefined,
25
+ sortBy: 'degree',
26
+ nodeSize: 30,
27
+ nodeSpacing: 10,
28
+ width: 300,
29
+ height: 300,
30
+ };
31
+
18
32
  /**
19
33
  * <zh/> 网格布局
20
34
  *
@@ -24,26 +38,19 @@ export class GridLayout extends BaseLayout<GridLayoutOptions> {
24
38
  id = 'grid';
25
39
 
26
40
  protected getDefaultOptions(): Partial<GridLayoutOptions> {
27
- return {
28
- begin: [0, 0],
29
- preventOverlap: true,
30
- preventOverlapPadding: 10,
31
- condense: false,
32
- rows: undefined,
33
- cols: undefined,
34
- position: undefined,
35
- sortBy: 'degree',
36
- nodeSize: 30,
37
- width: 300,
38
- height: 300,
39
- };
41
+ return DEFAULT_LAYOUT_OPTIONS;
40
42
  }
41
43
 
42
- private normalizeOptions(
44
+ private parseOptions(
43
45
  options: Partial<GridLayoutOptions> = {},
44
46
  model: GraphLib,
45
- ): NormalizedGridLayoutOptions {
46
- const { rows: propRows, cols: propCols } = options;
47
+ ): ParsedGridLayoutOptions {
48
+ const {
49
+ rows: propRows,
50
+ cols: propCols,
51
+ position: propPosition,
52
+ sortBy: propSortBy,
53
+ } = options;
47
54
 
48
55
  const { width, height, center } = normalizeViewport(options);
49
56
  let rows = options.rows;
@@ -98,23 +105,20 @@ export class GridLayout extends BaseLayout<GridLayoutOptions> {
98
105
  }
99
106
  }
100
107
 
101
- const preventOverlap =
102
- options.preventOverlap || options.nodeSpacing !== undefined;
103
- const nodeSpacing = formatNumberFn(options.nodeSpacing, 10);
104
- const nodeSize = formatSizeFn(options.nodeSize, 30);
108
+ const sortBy = !propSortBy
109
+ ? (DEFAULT_LAYOUT_OPTIONS.sortBy as 'degree')
110
+ : propSortBy === 'degree' || propSortBy === 'id'
111
+ ? propSortBy
112
+ : (formatFn(propSortBy, ['nodeA', 'nodeB']) as Sorter<NodeData>);
105
113
 
106
114
  return {
107
- ...options,
108
- begin: options.begin || [0, 0],
109
- sortBy: options.sortBy || 'degree',
110
- preventOverlapPadding: options.preventOverlapPadding ?? 0,
111
- preventOverlap,
112
- nodeSpacing,
113
- nodeSize,
115
+ ...(options as Required<GridLayoutOptions>),
116
+ sortBy,
114
117
  rcs,
115
118
  center,
116
119
  width,
117
120
  height,
121
+ position: formatFn(propPosition, ['node']),
118
122
  };
119
123
  }
120
124
 
@@ -126,12 +130,11 @@ export class GridLayout extends BaseLayout<GridLayoutOptions> {
126
130
  width,
127
131
  height,
128
132
  condense,
129
- preventOverlapPadding,
130
133
  preventOverlap,
131
134
  nodeSpacing,
132
135
  nodeSize,
133
136
  position,
134
- } = this.normalizeOptions(this.options, this.model);
137
+ } = this.parseOptions(this.options, this.model);
135
138
 
136
139
  const n = this.model.nodeCount();
137
140
 
@@ -152,18 +155,14 @@ export class GridLayout extends BaseLayout<GridLayoutOptions> {
152
155
  let cellHeight = condense ? 0 : height / rcs.rows;
153
156
 
154
157
  if (preventOverlap) {
158
+ const sizeFn = formatNodeSizeFn(
159
+ nodeSize,
160
+ nodeSpacing,
161
+ DEFAULT_LAYOUT_OPTIONS.nodeSize as number,
162
+ DEFAULT_LAYOUT_OPTIONS.nodeSpacing as number,
163
+ );
155
164
  this.model.forEachNode((node) => {
156
- const nodeData = node._original;
157
- const [nodeW, nodeH] = parseSize(nodeSize(nodeData) || 30);
158
-
159
- const p =
160
- nodeSpacing !== undefined
161
- ? nodeSpacing(nodeData)
162
- : preventOverlapPadding;
163
-
164
- const w = nodeW + p;
165
- const h = nodeH + p;
166
-
165
+ const [w, h] = sizeFn(node._original);
167
166
  cellWidth = Math.max(cellWidth, w);
168
167
  cellHeight = Math.max(cellHeight, h);
169
168
  });
@@ -1,5 +1,5 @@
1
+ import type { Expr, NodeData, Point, Sorter } from '../../types';
1
2
  import type { BaseLayoutOptions } from '../types';
2
- import type { NodeData, Point, Size } from '../../types';
3
3
 
4
4
  export interface GridLayoutOptions extends BaseLayoutOptions {
5
5
  /**
@@ -21,25 +21,6 @@ export interface GridLayoutOptions extends BaseLayoutOptions {
21
21
  * @defaultValue false
22
22
  */
23
23
  preventOverlap?: boolean;
24
- /**
25
- * <zh/> 节点大小(直径)。用于防止节点重叠时的碰撞检测
26
- *
27
- * <en/> Node size (diameter). Used for collision detection when nodes overlap
28
- */
29
- nodeSize?: Size | ((d?: NodeData) => Size);
30
- /**
31
- * <zh/> 环与环之间最小间距,用于调整半径
32
- *
33
- * <en/> Minimum spacing between rings, used to adjust the radius
34
- */
35
- nodeSpacing?: number | ((d?: NodeData) => number);
36
- /**
37
- * <zh/> 避免重叠时节点的间距 padding。preventOverlap 为 true 时生效
38
- *
39
- * <en/> Padding between nodes to prevent overlap. It takes effect when preventOverlap is true
40
- * @defaultValue 10
41
- */
42
- preventOverlapPadding?: number;
43
24
  /**
44
25
  * <zh/> 为 false 时表示利用所有可用画布空间,为 true 时表示利用最小的画布空间
45
26
  *
@@ -67,38 +48,28 @@ export interface GridLayoutOptions extends BaseLayoutOptions {
67
48
  * <en/> Specify the basis for sorting (node attribute name). The higher the value, the more the node will be placed in the center. If it is undefined, the degree of the node will be calculated, and the higher the degree, the more the node will be placed in the center
68
49
  * @defaultValue undefined
69
50
  */
70
- sortBy?: 'id' | 'degree' | ((nodeA: NodeData, nodeB: NodeData) => -1 | 0 | 1);
51
+ sortBy?: 'id' | 'degree' | Expr | Sorter<NodeData>;
71
52
  /**
72
53
  * <zh/> 指定每个节点所在的行和列
73
54
  *
74
55
  * <en/> Specify the row and column where each node is located
75
56
  * @defaultValue undefined
76
57
  */
77
- position?: (node: NodeData) => { row?: number; col?: number };
58
+ position?: Expr | ((node: NodeData) => { row?: number; col?: number });
78
59
  }
79
60
 
80
- export interface NormalizedGridLayoutOptions
61
+ export interface ParsedGridLayoutOptions
81
62
  extends Omit<
82
63
  GridLayoutOptions,
83
- | 'begin'
84
- | 'nodeSize'
85
- | 'nodeSpacing'
86
- | 'preventOverlap'
87
- | 'preventOverlapPadding'
88
- | 'sortBy'
89
- | 'rows'
90
- | 'cols'
64
+ 'begin' | 'preventOverlap' | 'sortBy' | 'rows' | 'cols'
91
65
  > {
92
66
  width: number;
93
67
  height: number;
94
68
  center: Point;
95
69
  begin: Point;
96
70
  rcs: { rows: number; cols: number };
97
- nodeSize: (node?: NodeData) => Size;
98
- nodeSpacing: (node?: NodeData) => number;
99
71
  preventOverlap: boolean;
100
- preventOverlapPadding: number;
101
- sortBy: 'id' | 'degree' | ((nodeA: NodeData, nodeB: NodeData) => -1 | 0 | 1);
72
+ sortBy: 'id' | 'degree' | Sorter<NodeData>;
102
73
  }
103
74
 
104
75
  export type RowsAndCols = {
@@ -1,10 +1,10 @@
1
- import { BaseLayout } from '../base-layout';
2
- import { runMDS } from '../mds';
3
- import type { ID, Matrix } from '../../types';
4
1
  import type { GraphLib } from '../../model/data';
2
+ import type { ID, Matrix } from '../../types';
5
3
  import { getAdjList, johnson, normalizeViewport } from '../../util';
6
4
  import { applySingleNodeLayout } from '../../util/common';
7
- import { formatNodeSizeFn } from '../../util/format';
5
+ import { formatFn, formatNodeSizeFn } from '../../util/format';
6
+ import { BaseLayout } from '../base-layout';
7
+ import { runMDS } from '../mds';
8
8
  import {
9
9
  radialNonoverlapForce,
10
10
  RadialNonoverlapForceOptions,
@@ -22,6 +22,8 @@ const DEFAULTS_LAYOUT_OPTIONS: Partial<RadialLayoutOptions> = {
22
22
  sortStrength: 10,
23
23
  strictRadial: true,
24
24
  unitRadius: null,
25
+ nodeSize: 10,
26
+ nodeSpacing: 0,
25
27
  };
26
28
 
27
29
  /**
@@ -126,7 +128,12 @@ export class RadialLayout extends BaseLayout<RadialLayoutOptions> {
126
128
 
127
129
  // stagger the overlapped nodes
128
130
  if (preventOverlap) {
129
- const nodeSizeFunc = formatNodeSizeFn(nodeSize, nodeSpacing);
131
+ const nodeSizeFunc = formatNodeSizeFn(
132
+ nodeSize,
133
+ nodeSpacing,
134
+ DEFAULTS_LAYOUT_OPTIONS.nodeSize as number,
135
+ DEFAULTS_LAYOUT_OPTIONS.nodeSpacing as number,
136
+ );
130
137
  const nonoverlapForceParams: RadialNonoverlapForceOptions = {
131
138
  nodeSizeFunc,
132
139
  radiiMap,
@@ -230,7 +237,8 @@ const eIdealDisMatrix = (
230
237
 
231
238
  const baseLink = (linkDistance + unitRadius) / 2;
232
239
  const sortCache = new Map<ID, number>();
233
- const sortFn = typeof sortBy === 'function' ? sortBy : null;
240
+ const sortFn =
241
+ !sortBy || sortBy === 'data' ? null : formatFn(sortBy, ['node']);
234
242
  const isDataSort = sortBy === 'data';
235
243
 
236
244
  for (let i = 0; i < n; i++) {
@@ -1,6 +1,11 @@
1
- import type { DisplacementMap, ID, LayoutNode, NodeData, Size } from '../../types';
2
1
  import type { GraphLib } from '../../model/data';
3
- import { parseSize } from '../../util';
2
+ import type {
3
+ DisplacementMap,
4
+ ID,
5
+ LayoutNode,
6
+ NodeData,
7
+ STDSize,
8
+ } from '../../types';
4
9
 
5
10
  const SPEED_DIVISOR = 800;
6
11
 
@@ -22,7 +27,7 @@ export type RadialNonoverlapForceOptions = {
22
27
  /** Gravity factor pulling nodes towards their target radius */
23
28
  gravity?: number;
24
29
  /** Function to get the size of a node (includes node self and spacing) */
25
- nodeSizeFunc: (node?: NodeData) => Size;
30
+ nodeSizeFunc: (node: NodeData) => STDSize;
26
31
  };
27
32
 
28
33
  const DEFAULTS_LAYOUT_OPTIONS: Partial<RadialNonoverlapForceOptions> = {
@@ -83,7 +88,7 @@ const getRepulsion = (
83
88
  displacements: DisplacementMap,
84
89
  k: number,
85
90
  radiiMap: Map<ID, number>,
86
- nodeSizeFunc: (d?: NodeData) => Size,
91
+ nodeSizeFunc: (node: NodeData) => STDSize,
87
92
  ) => {
88
93
  let i = 0;
89
94
 
@@ -112,8 +117,8 @@ const getRepulsion = (
112
117
  vecy = 0.01 * sign;
113
118
  }
114
119
 
115
- const nodeSizeU = Math.max(...parseSize(nodeSizeFunc(nodeU._original)));
116
- const nodeSizeV = Math.max(...parseSize(nodeSizeFunc(nodeV._original)));
120
+ const nodeSizeU = Math.max(...nodeSizeFunc(nodeU._original));
121
+ const nodeSizeV = Math.max(...nodeSizeFunc(nodeV._original));
117
122
 
118
123
  // these two nodes overlap
119
124
  if (vecLength < nodeSizeV / 2 + nodeSizeU / 2) {
@@ -1,5 +1,5 @@
1
+ import type { Expr, NodeData } from '../../types';
1
2
  import type { BaseLayoutOptions } from '../base-layout';
2
- import type { NodeData, Size } from '../../types';
3
3
 
4
4
  /**
5
5
  * <zh/> Radial 辐射布局的配置项
@@ -41,19 +41,6 @@ export interface RadialLayoutOptions extends BaseLayoutOptions {
41
41
  * @defaultValue false
42
42
  */
43
43
  preventOverlap?: boolean;
44
- /**
45
- * <zh/> 节点大小(直径)。用于防止节点重叠时的碰撞检测
46
- *
47
- * <en/> Node size (diameter). Used for collision detection when preventing node overlap
48
- */
49
- nodeSize?: Size | ((d?: NodeData) => Size);
50
- /**
51
- * <zh/> preventOverlap 为 true 时生效, 防止重叠时节点边缘间距的最小值。可以是回调函数, 为不同节点设置不同的最小间距
52
- *
53
- * <en/> 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
54
- * @defaultValue 10
55
- */
56
- nodeSpacing?: number | ((d?: NodeData) => number);
57
44
  /**
58
45
  * <zh/> 防止重叠步骤的最大迭代次数
59
46
  *
@@ -82,7 +69,7 @@ export interface RadialLayoutOptions extends BaseLayoutOptions {
82
69
  * <en/> The default is undefined, which means arranging based on the topological structure of the data (the shortest path between nodes). Nodes that are closer in proximity or have a smaller shortest path between them will be arranged as close together as possible. 'data' indicates arranging based on the order of nodes in the data, so nodes that are closer in the data order will be arranged as close together as possible. You can also specify a field name in the node data, such as 'cluster' or 'name' (it must exist in the data of the graph)
83
70
  * @defaultValue undefined
84
71
  */
85
- sortBy?: 'data' | ((d?: NodeData) => number | string);
72
+ sortBy?: 'data' | ((node: NodeData) => number | string) | Expr;
86
73
  /**
87
74
  * <zh/> 同层节点根据 sortBy 排列的强度,数值越大,sortBy 指定的方式计算出距离越小的越靠近。sortBy 不为 undefined 时生效
88
75
  *
@@ -1,11 +1,13 @@
1
1
  import type {
2
2
  EdgeData,
3
+ Expr,
3
4
  GraphData,
4
5
  GraphEdge,
5
6
  GraphNode,
6
7
  ID,
7
8
  NodeData,
8
9
  Point,
10
+ Size,
9
11
  } from '../types';
10
12
 
11
13
  export interface DataOptions<
@@ -63,6 +65,22 @@ export interface BaseLayoutOptions<
63
65
  */
64
66
  height?: number;
65
67
 
68
+ /**
69
+ * <zh/> 节点大小(直径)。用于防止节点重叠时的碰撞检测
70
+ *
71
+ * <en/> Node size (diameter). Used for collision detection when nodes overlap
72
+ * @defaultValue 10
73
+ */
74
+ nodeSize?: Size | Expr | ((node: NodeData) => Size);
75
+
76
+ /**
77
+ * <zh/> 节点之间的最小间距
78
+ *
79
+ * <en/> Minimum spacing between nodes
80
+ * @defaultValue 0
81
+ */
82
+ nodeSpacing?: Size | Expr | ((node: NodeData) => Size);
83
+
66
84
  /**
67
85
  * <zh/> 是否启用 WebWorker
68
86
  *
@@ -1,3 +1,25 @@
1
1
  export type PlainObject = Record<string, any>;
2
2
 
3
3
  export type Matrix = number[][];
4
+
5
+ /**
6
+ * String expression evaluated by `@antv/expr`.
7
+ *
8
+ * Notes:
9
+ * - `Expr` is structured-cloneable and can be passed into WebWorkers.
10
+ * - Function callbacks cannot be structured-cloned; prefer `Expr` when `enableWorker: true`.
11
+ */
12
+ export type Expr = string;
13
+
14
+ /**
15
+ * CallableExpr<(node: NodeData) => number>
16
+ *
17
+ * => 'node.degree' | (node: NodeData) => number
18
+ */
19
+ export type CallableExpr<TData = any, TResult = any> =
20
+ | Expr
21
+ | ((data: TData) => TResult);
22
+
23
+ export type ExprContext = Record<string, any>;
24
+
25
+ export type Sorter<T = any> = (a: T, b: T) => -1 | 0 | 1;
package/src/types/data.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { EdgeLabelPos } from './edge-label';
2
1
  import type { PlainObject } from './common';
2
+ import type { EdgeLabelPos } from './edge-label';
3
3
  import type { ID } from './id';
4
4
  import type { Point } from './point';
5
5
  import type { Size } from './size';
@@ -0,0 +1,26 @@
1
+ import { compile, evaluate } from '@antv/expr';
2
+ import type { ExprContext } from '../types';
3
+
4
+ /**
5
+ * Evaluate an expression if (and only if) it's a valid string expression.
6
+ * - Returns `undefined` when `expression` is not a string, empty, or invalid.
7
+ *
8
+ * @example
9
+ * evaluateExpression('x + y', { x: 10, y: 20 }) // 30
10
+ */
11
+ export function evaluateExpression(
12
+ expression: unknown,
13
+ context: ExprContext,
14
+ ): unknown | undefined {
15
+ if (typeof expression !== 'string') return undefined;
16
+
17
+ const source = expression.trim();
18
+ if (!source) return undefined;
19
+
20
+ try {
21
+ compile(source);
22
+ return evaluate(source, context);
23
+ } catch {
24
+ return undefined;
25
+ }
26
+ }
@@ -1,6 +1,30 @@
1
- import { isFunction, isNumber, isObject } from '@antv/util';
2
- import type { NodeData, Size } from '../types';
3
- import { parseSize } from './size';
1
+ import { isFunction, isNil, isNumber, isString } from '@antv/util';
2
+ import type { Expr, NodeData, Size, STDSize } from '../types';
3
+ import { evaluateExpression } from './expr';
4
+ import { isSize, parseSize } from './size';
5
+
6
+ /**
7
+ * Format a value into a callable function when it is a string expression.
8
+ * - `string` => `(context) => evaluateExpression(string, context)`
9
+ * - `function` => returned as-is
10
+ * - other => returned as-is
11
+ */
12
+ export function formatFn<
13
+ TContext extends Record<string, any> = Record<string, any>,
14
+ >(value: unknown, argNames: (keyof TContext & string)[]) {
15
+ if (typeof value === 'function') return value;
16
+ if (typeof value === 'string') {
17
+ const expr = value;
18
+ return (...argv: any[]) => {
19
+ const ctx = {} as TContext;
20
+ for (let i = 0; i < argNames.length; i++) {
21
+ (ctx as any)[argNames[i]] = argv[i];
22
+ }
23
+ return evaluateExpression(expr, ctx);
24
+ };
25
+ }
26
+ return () => value;
27
+ }
4
28
 
5
29
  /**
6
30
  * Format value with multiple types into a function that returns a number
@@ -8,10 +32,26 @@ import { parseSize } from './size';
8
32
  * @param defaultValue The default value when value is invalid
9
33
  * @returns A function that returns a number
10
34
  */
11
- export function formatNumberFn<T = NodeData>(
12
- value: number | ((d?: T) => number) | undefined,
35
+ export function formatNumberFn<T extends NodeData = NodeData>(
36
+ value: number | Expr | ((d: T) => number) | undefined,
13
37
  defaultValue: number,
14
- ): (d?: T) => number {
38
+ type: 'node' | 'edge' | 'combo' = 'node',
39
+ ): (d: T) => number {
40
+ // If value is undefined, return default value function
41
+ if (isNil(value)) {
42
+ return () => defaultValue;
43
+ }
44
+
45
+ // If value is an expression, return a function that evaluates the expression
46
+ if (isString(value)) {
47
+ const numberFn = formatFn(value, [type]);
48
+ return (d: T) => {
49
+ const evaluated = numberFn(d);
50
+ if (isNumber(evaluated)) return evaluated;
51
+ return defaultValue;
52
+ };
53
+ }
54
+
15
55
  // If value is a function, return it directly
16
56
  if (isFunction(value)) {
17
57
  return value;
@@ -33,15 +73,26 @@ export function formatNumberFn<T = NodeData>(
33
73
  * @param resultIsNumber Whether to return a number (max of width/height) or size array
34
74
  * @returns A function that returns a size
35
75
  */
36
- export function formatSizeFn<T extends NodeData>(
37
- value?: Size | { width: number; height: number } | ((d?: T) => Size),
76
+ export function formatSizeFn<T extends NodeData = NodeData>(
77
+ value?: Size | Expr | ((d: T) => Size),
38
78
  defaultValue: number = 10,
39
- ): (d?: T) => Size {
79
+ type: 'node' | 'edge' | 'combo' = 'node',
80
+ ): (d: T) => Size {
40
81
  // If value is undefined, return default value function
41
- if (!value) {
82
+ if (isNil(value)) {
42
83
  return () => defaultValue;
43
84
  }
44
85
 
86
+ // If value is an expression, return a function that evaluates the expression
87
+ if (isString(value)) {
88
+ const sizeFn = formatFn(value, [type]);
89
+ return (d: T) => {
90
+ const evaluated = sizeFn(d);
91
+ if (isSize(evaluated)) return evaluated;
92
+ return defaultValue;
93
+ };
94
+ }
95
+
45
96
  // If value is a function, return it directly
46
97
  if (isFunction(value)) {
47
98
  return value;
@@ -57,11 +108,6 @@ export function formatSizeFn<T extends NodeData>(
57
108
  return () => value;
58
109
  }
59
110
 
60
- // If value is an object with width and height
61
- if (isObject(value) && value.width && value.height) {
62
- return () => [value.width, value.height];
63
- }
64
-
65
111
  return () => defaultValue;
66
112
  }
67
113
 
@@ -70,23 +116,21 @@ export function formatSizeFn<T extends NodeData>(
70
116
  * @param nodeSize The size of the node
71
117
  * @param nodeSpacing The spacing around the node
72
118
  * @param defaultNodeSize The default node size when value is invalid
119
+ * @param defaultNodeSpacing The default node spacing when value is invalid
73
120
  * @returns A function that returns the total size (node size + spacing)
74
121
  */
75
122
  export const formatNodeSizeFn = (
76
- nodeSize:
77
- | Size
78
- | { width: number; height: number }
79
- | ((node?: NodeData) => Size)
80
- | undefined,
81
- nodeSpacing: number | ((node?: NodeData) => number) | undefined,
123
+ nodeSize?: Size | Expr | ((node: NodeData) => Size),
124
+ nodeSpacing?: Size | Expr | ((node: NodeData) => Size),
82
125
  defaultNodeSize: number = 10,
83
- ): ((node?: NodeData) => number) => {
84
- const nodeSpacingFunc = formatNumberFn(nodeSpacing, 0);
126
+ defaultNodeSpacing: number = 0,
127
+ ): ((d: NodeData) => STDSize) => {
128
+ const nodeSpacingFunc = formatSizeFn(nodeSpacing, defaultNodeSpacing);
85
129
  const nodeSizeFunc = formatSizeFn(nodeSize, defaultNodeSize);
86
130
 
87
- return (node?: NodeData) => {
88
- const size = nodeSizeFunc(node);
89
- const spacing = nodeSpacingFunc(node);
90
- return Math.max(...parseSize(size)) + spacing;
131
+ return (d: NodeData) => {
132
+ const [sizeW, sizeH, sizeD] = parseSize(nodeSizeFunc(d));
133
+ const [spacingW, spacingH, spacingD] = parseSize(nodeSpacingFunc(d));
134
+ return [sizeW + spacingW, sizeH + spacingH, sizeD + spacingD];
91
135
  };
92
136
  };
package/src/util/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from './array';
2
2
  export * from './common';
3
+ export * from './expr';
4
+ export * from './format';
3
5
  export * from './math';
4
6
  export * from './object';
5
7
  export * from './order';
package/src/util/order.ts CHANGED
@@ -1,11 +1,5 @@
1
- import type { LayoutNode, NodeData } from '../types';
2
1
  import type { GraphLib } from '../model/data';
3
-
4
- export type SortComparator<N extends NodeData = NodeData> = (
5
- nodeA: LayoutNode<N>,
6
- nodeB: LayoutNode<N>,
7
- nodes: LayoutNode<N>[],
8
- ) => -1 | 0 | 1;
2
+ import type { LayoutNode, NodeData, Sorter } from '../types';
9
3
 
10
4
  /**
11
5
  * 通用排序核心函数
@@ -29,7 +23,7 @@ export function orderByDegree<N extends NodeData = NodeData>(
29
23
  return sort(model, (nodeA, nodeB) => {
30
24
  const degreeA = model.degree(nodeA.id);
31
25
  const degreeB = model.degree(nodeB.id);
32
- if(order === 'asc') {
26
+ if (order === 'asc') {
33
27
  return degreeA - degreeB; // ascending order
34
28
  }
35
29
  return degreeB - degreeA; // descending order
@@ -59,7 +53,7 @@ export function orderById<N extends NodeData = NodeData>(
59
53
  */
60
54
  export function orderBySorter<N extends NodeData = NodeData>(
61
55
  model: GraphLib<N>,
62
- sorter: (a: NodeData, b: NodeData) => -1 | 0 | 1,
56
+ sorter: Sorter<N>,
63
57
  ): GraphLib<N> {
64
58
  return sort(model, (nodeA, nodeB) => {
65
59
  const a = model.originalNode(nodeA.id);
package/src/util/size.ts CHANGED
@@ -8,3 +8,11 @@ export function parseSize(size?: Size): STDSize {
8
8
  const [x, y = x, z = x] = size;
9
9
  return [x, y, z];
10
10
  }
11
+
12
+ export function isSize(value: unknown): value is Size {
13
+ if (isNumber(value)) return true;
14
+ if (Array.isArray(value)) {
15
+ return value.every((item) => isNumber(item));
16
+ }
17
+ return false;
18
+ }