Skip to content

Commit 6cec35f

Browse files
authored
feat: expr (#285)
* feat: expr * test: update tests * fix: d3force id * docs: update docs * fix: 修复边标签位置函数的类型判断 * docs: add webworker docs * fix: 更新边标签位置的 SVG 路径和坐标 * fix: 更新版本号至 2.0.0-beta.1
1 parent 59c85b8 commit 6cec35f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+812
-491
lines changed

__tests__/demos/combo-combined.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ export function render(gui?: GUI) {
4444
});
4545

4646
layout.forEachNode((node) => {
47-
console.log('node:', node);
4847
renderer.updateNodeAttributes(node.id, {
4948
cx: node.x,
5049
cy: node.y,

__tests__/demos/combo-combined2.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,6 @@ export function render(gui?: GUI) {
189189
});
190190

191191
layout.forEachNode((node: any) => {
192-
console.log('node:', node);
193192
renderer.updateNodeAttributes(node.id, {
194193
// cx: node.x,
195194
// cy: node.y,

__tests__/demos/fruchterman.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,12 @@ export async function render(gui?: GUI) {
5555
const clusterOptions = {
5656
...options,
5757
clustering: true,
58-
nodeClusterBy: (node: any) => node.cluster,
58+
// nodeClusterBy: (node: any) => node.cluster,
59+
nodeClusterBy: 'node.cluster',
5960
};
6061

6162
console.time('fruchterman layout');
62-
await layout.execute(processedData, options);
63+
await layout.execute(processedData, clusterOptions);
6364
console.timeEnd('fruchterman layout');
6465

6566
if (gui) {

__tests__/unit/circular.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import { CircularLayout } from '@/src';
22
import { createCanvas } from '@@/utils/create';
33
import type { Canvas } from '@antv/g';
44
import { countries as data } from '../dataset';
5-
import { GraphRenderer } from '../utils';
6-
import { calculatePositions } from '../utils';
5+
import { calculatePositions, GraphRenderer } from '../utils';
76

87
describe('layout circular', () => {
98
let canvas: Canvas;
@@ -43,6 +42,7 @@ describe('layout circular', () => {
4342
ordering: null,
4443
angleRatio: 1,
4544
nodeSize: 10,
45+
nodeSpacing: 0,
4646
});
4747
});
4848

__tests__/unit/force.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,49 @@ describe('layout force', () => {
244244
expect(tickCount).toBeGreaterThanOrEqual(1);
245245
});
246246

247+
it('should support Expr for accessor callbacks', async () => {
248+
const graph = {
249+
nodes: [
250+
{ id: 'a', data: { cluster: 'c1', mass: 2, ns: 123, cs: 50 } },
251+
{ id: 'b', data: { cluster: 'c2', mass: 3, ns: 456, cs: 60 } },
252+
],
253+
edges: [
254+
{
255+
id: 'e1',
256+
source: 'a',
257+
target: 'b',
258+
data: { len: 77, es: 0.5 },
259+
},
260+
],
261+
};
262+
263+
const layout = new ForceLayout({
264+
width,
265+
height,
266+
maxIteration: 1,
267+
minMovement: 0,
268+
clustering: true,
269+
nodeClusterBy: 'node.data.cluster',
270+
getMass: 'node.data.mass',
271+
nodeStrength: 'node.data.ns',
272+
edgeStrength: 'edge.data.es',
273+
linkDistance: 'edge.data.len',
274+
clusterNodeStrength: 'node.data.cs',
275+
getCenter: '[0, 0, 10]',
276+
});
277+
278+
await layout.execute(graph);
279+
280+
layout.forEachNode((node: any) => {
281+
expect(node.mass).toBe(node._original.data.mass);
282+
expect(node.nodeStrength).toBe(node._original.data.ns);
283+
});
284+
layout.forEachEdge((edge: any) => {
285+
expect(edge.edgeStrength).toBe(edge._original.data.es);
286+
expect(edge.linkDistance).toBe(edge._original.data.len);
287+
});
288+
});
289+
247290
it('should handle overlapped nodes', async () => {
248291
const overlapGraph = {
249292
nodes: [

__tests__/unit/fruchterman.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe('FruchtermanLayout', () => {
3939
clusterGravity: 10,
4040
width: 300,
4141
height: 300,
42-
nodeClusterBy: 'data.cluster',
42+
nodeClusterBy: 'node.cluster',
4343
dimensions: 2,
4444
});
4545
});

__tests__/unit/util/expr.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { evaluateExpression } from '@/src/util/expr';
2+
3+
describe('expr', () => {
4+
describe('evaluateExpression', () => {
5+
test('evaluateExpression returns result for valid expression', () => {
6+
expect(evaluateExpression('x + y', { x: 10, y: 20 })).toBe(30);
7+
});
8+
9+
test('evaluateExpression supports dot notation and array access', () => {
10+
const data = { values: [1, 2, 3], status: 'active' };
11+
expect(
12+
evaluateExpression('data.values[0] + data.values[1]', { data }),
13+
).toBe(3);
14+
});
15+
16+
test('evaluateExpression returns undefined for non-string/empty/invalid expression', () => {
17+
expect(evaluateExpression(123, { x: 1 })).toBeUndefined();
18+
expect(evaluateExpression(' ', { x: 1 })).toBeUndefined();
19+
expect(evaluateExpression('x +', { x: 1 })).toBeUndefined();
20+
});
21+
});
22+
});

__tests__/unit/util/format.test.ts

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,6 @@ describe('format', () => {
8585
expect(result()).toEqual([20, 30]);
8686
});
8787

88-
test('should handle object value with width and height', () => {
89-
const result = formatSizeFn<NodeData>({ width: 40, height: 50 }, 10);
90-
expect(result()).toEqual([40, 50]);
91-
});
92-
93-
test('should handle object value when resultIsNumber is false', () => {
94-
const result = formatSizeFn<NodeData>({ width: 40, height: 50 }, 10);
95-
expect(result()).toEqual([40, 50]);
96-
});
97-
9888
test('should return default value when value is undefined and no node provided', () => {
9989
const result = formatSizeFn<NodeData>(undefined, 10);
10090
expect(result()).toBe(10);
@@ -111,7 +101,7 @@ describe('format', () => {
111101

112102
test('should handle number zero as valid size', () => {
113103
const result = formatSizeFn<NodeData>(0, 10);
114-
expect(result()).toBe(10); // 0 is falsy, falls back to default
104+
expect(result()).toBe(0);
115105
});
116106

117107
test('should return default value when data has no size', () => {
@@ -157,41 +147,45 @@ describe('format', () => {
157147
describe('formatNodeSizeFn', () => {
158148
test('should return node size plus spacing', () => {
159149
const result = formatNodeSizeFn(20, 5, 10);
160-
expect(result()).toBe(25);
150+
expect(result()).toEqual([25, 25, 25]);
161151
});
162152

163153
test('should use default node size when nodeSize is undefined', () => {
164154
const result = formatNodeSizeFn(undefined, 5, 15);
165-
expect(result()).toBe(20); // 15 + 5
155+
expect(result()).toEqual([20, 20, 20]); // 15 + 5
166156
});
167157

168158
test('should handle zero spacing', () => {
169159
const result = formatNodeSizeFn(20, 0, 10);
170-
expect(result()).toBe(20);
160+
expect(result()).toEqual([20, 20, 20]);
171161
});
172162

173163
test('should handle undefined spacing', () => {
174164
const result = formatNodeSizeFn(20, undefined, 10);
175-
expect(result()).toBe(20);
165+
expect(result()).toEqual([20, 20, 20]);
176166
});
177167

178168
test('should handle nodeSize as array', () => {
179169
const result = formatNodeSizeFn([30, 40], 5, 10);
180-
expect(result()).toBe(45); // max(30, 40) + 5
170+
expect(result()).toEqual([35, 45, 35]);
181171
});
182172

183173
test('should handle nodeSize as function', () => {
184174
const sizeFn = (node?: NodeData) =>
185175
((node?.data as any)?.customSize as number) || 25;
186176
const result = formatNodeSizeFn(sizeFn, 10, 10);
187-
expect(result({ id: 'test', data: { customSize: 30 } })).toBe(40); // 30 + 10
177+
expect(result({ id: 'test', data: { customSize: 30 } })).toEqual([
178+
40, 40, 40,
179+
]);
188180
});
189181

190182
test('should handle nodeSpacing as function', () => {
191183
const spacingFn = (node?: NodeData) =>
192184
((node?.data as any)?.spacing as number) || 0;
193185
const result = formatNodeSizeFn(20, spacingFn, 10);
194-
expect(result({ id: 'test', data: { spacing: 5 } })).toBe(25); // 20 + 5
186+
expect(result({ id: 'test', data: { spacing: 5 } })).toEqual([
187+
25, 25, 25,
188+
]);
195189
});
196190

197191
test('should handle both nodeSize and nodeSpacing as functions', () => {
@@ -200,7 +194,9 @@ describe('format', () => {
200194
const spacingFn = (node?: NodeData) =>
201195
((node?.data as any)?.spacing as number) || 0;
202196
const result = formatNodeSizeFn(sizeFn, spacingFn, 10);
203-
expect(result({ id: 'test', data: { size: 30, spacing: 5 } })).toBe(35);
197+
expect(result({ id: 'test', data: { size: 30, spacing: 5 } })).toEqual([
198+
35, 35, 35,
199+
]);
204200
});
205201

206202
test('should return default when nodeSize undefined and node has no size in data', () => {
@@ -209,32 +205,34 @@ describe('format', () => {
209205
id: 'node1',
210206
data: {},
211207
};
212-
expect(result(nodeData)).toBe(15); // default 10 + 5 spacing
208+
expect(result(nodeData)).toEqual([15, 15, 15]); // default 10 + 5 spacing
213209
});
214210

215211
test('should handle negative spacing', () => {
216212
const result = formatNodeSizeFn(20, -5, 10);
217-
expect(result()).toBe(15); // 20 + (-5)
213+
expect(result()).toEqual([15, 15, 15]); // 20 + (-5)
218214
});
219215

220216
test('should handle fractional sizes and spacing', () => {
221217
const result = formatNodeSizeFn(20.5, 3.2, 10);
222-
expect(result()).toBeCloseTo(23.7);
218+
expect(result()[0]).toBeCloseTo(23.7);
219+
expect(result()[1]).toBeCloseTo(23.7);
220+
expect(result()[2]).toBeCloseTo(23.7);
223221
});
224222

225223
test('should handle number zero node size', () => {
226224
const result = formatNodeSizeFn(0, 5, 10);
227-
expect(result()).toBe(15); // 0 is falsy, falls back to default 10 + 5
225+
expect(result()).toEqual([5, 5, 5]);
228226
});
229227

230228
test('should handle large sizes', () => {
231229
const result = formatNodeSizeFn(1000, 100, 10);
232-
expect(result()).toBe(1100);
230+
expect(result()).toEqual([1100, 1100, 1100]);
233231
});
234232

235233
test('should use default when all values are undefined', () => {
236234
const result = formatNodeSizeFn(undefined, undefined, 12);
237-
expect(result()).toBe(12);
235+
expect(result()).toEqual([12, 12, 12]);
238236
});
239237

240238
test('should handle node data without spacing function', () => {
@@ -243,12 +241,12 @@ describe('format', () => {
243241
id: 'node1',
244242
data: {},
245243
};
246-
expect(result(nodeData)).toBe(25);
244+
expect(result(nodeData)).toEqual([25, 25, 25]);
247245
});
248246

249247
test('should handle single element array size', () => {
250248
const result = formatNodeSizeFn([35], 5, 10);
251-
expect(result()).toBe(40); // max(35) + 5
249+
expect(result()).toEqual([40, 40, 40]); // max(35) + 5
252250
});
253251
});
254252
});

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@antv/layout",
3-
"version": "2.0.0-beta.0",
3+
"version": "2.0.0-beta.1",
44
"description": "graph layout algorithm",
55
"main": "dist/index.min.js",
66
"module": "lib/index.js",
@@ -35,6 +35,7 @@
3535
],
3636
"dependencies": {
3737
"@antv/event-emitter": "^0.1.3",
38+
"@antv/expr": "^1.0.2",
3839
"@antv/graphlib": "^2.0.0",
3940
"@antv/util": "^3.3.2",
4041
"comlink": "^4.4.1",
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
["getting-started", "installation", "data-mapping", "layout-configuration"]
1+
["getting-started", "installation", "webworker", "data-mapping", "layout-configuration"]

0 commit comments

Comments
 (0)