-
Notifications
You must be signed in to change notification settings - Fork 60
feat: expr #285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat: expr #285
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
7bb73c9
feat: expr
yvonneyx a15926e
test: update tests
yvonneyx a5f5486
fix: d3force id
yvonneyx e7ccfd8
docs: update docs
yvonneyx 2a730f3
fix: 修复边标签位置函数的类型判断
yvonneyx cf9dda2
docs: add webworker docs
yvonneyx 985cae2
fix: 更新边标签位置的 SVG 路径和坐标
yvonneyx 8aa1ad2
fix: 更新版本号至 2.0.0-beta.1
yvonneyx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { evaluateExpression } from '@/src/util/expr'; | ||
|
|
||
| describe('expr', () => { | ||
| describe('evaluateExpression', () => { | ||
| test('evaluateExpression returns result for valid expression', () => { | ||
| expect(evaluateExpression('x + y', { x: 10, y: 20 })).toBe(30); | ||
| }); | ||
|
|
||
| test('evaluateExpression supports dot notation and array access', () => { | ||
| const data = { values: [1, 2, 3], status: 'active' }; | ||
| expect( | ||
| evaluateExpression('data.values[0] + data.values[1]', { data }), | ||
| ).toBe(3); | ||
| }); | ||
|
|
||
| test('evaluateExpression returns undefined for non-string/empty/invalid expression', () => { | ||
| expect(evaluateExpression(123, { x: 1 })).toBeUndefined(); | ||
| expect(evaluateExpression(' ', { x: 1 })).toBeUndefined(); | ||
| expect(evaluateExpression('x +', { x: 1 })).toBeUndefined(); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -85,16 +85,6 @@ describe('format', () => { | |
| expect(result()).toEqual([20, 30]); | ||
| }); | ||
|
|
||
| test('should handle object value with width and height', () => { | ||
| const result = formatSizeFn<NodeData>({ width: 40, height: 50 }, 10); | ||
| expect(result()).toEqual([40, 50]); | ||
| }); | ||
|
|
||
| test('should handle object value when resultIsNumber is false', () => { | ||
| const result = formatSizeFn<NodeData>({ width: 40, height: 50 }, 10); | ||
| expect(result()).toEqual([40, 50]); | ||
| }); | ||
|
|
||
| test('should return default value when value is undefined and no node provided', () => { | ||
| const result = formatSizeFn<NodeData>(undefined, 10); | ||
| expect(result()).toBe(10); | ||
|
|
@@ -111,7 +101,7 @@ describe('format', () => { | |
|
|
||
| test('should handle number zero as valid size', () => { | ||
| const result = formatSizeFn<NodeData>(0, 10); | ||
| expect(result()).toBe(10); // 0 is falsy, falls back to default | ||
| expect(result()).toBe(0); | ||
| }); | ||
|
|
||
| test('should return default value when data has no size', () => { | ||
|
|
@@ -157,41 +147,45 @@ describe('format', () => { | |
| describe('formatNodeSizeFn', () => { | ||
| test('should return node size plus spacing', () => { | ||
| const result = formatNodeSizeFn(20, 5, 10); | ||
| expect(result()).toBe(25); | ||
| expect(result()).toEqual([25, 25, 25]); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这三个值分别是啥意思
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 【长、宽、高】 |
||
| }); | ||
|
|
||
| test('should use default node size when nodeSize is undefined', () => { | ||
| const result = formatNodeSizeFn(undefined, 5, 15); | ||
| expect(result()).toBe(20); // 15 + 5 | ||
| expect(result()).toEqual([20, 20, 20]); // 15 + 5 | ||
| }); | ||
|
|
||
| test('should handle zero spacing', () => { | ||
| const result = formatNodeSizeFn(20, 0, 10); | ||
| expect(result()).toBe(20); | ||
| expect(result()).toEqual([20, 20, 20]); | ||
| }); | ||
|
|
||
| test('should handle undefined spacing', () => { | ||
| const result = formatNodeSizeFn(20, undefined, 10); | ||
| expect(result()).toBe(20); | ||
| expect(result()).toEqual([20, 20, 20]); | ||
| }); | ||
|
|
||
| test('should handle nodeSize as array', () => { | ||
| const result = formatNodeSizeFn([30, 40], 5, 10); | ||
| expect(result()).toBe(45); // max(30, 40) + 5 | ||
| expect(result()).toEqual([35, 45, 35]); | ||
| }); | ||
|
|
||
| test('should handle nodeSize as function', () => { | ||
| const sizeFn = (node?: NodeData) => | ||
| ((node?.data as any)?.customSize as number) || 25; | ||
| const result = formatNodeSizeFn(sizeFn, 10, 10); | ||
| expect(result({ id: 'test', data: { customSize: 30 } })).toBe(40); // 30 + 10 | ||
| expect(result({ id: 'test', data: { customSize: 30 } })).toEqual([ | ||
| 40, 40, 40, | ||
| ]); | ||
| }); | ||
|
|
||
| test('should handle nodeSpacing as function', () => { | ||
| const spacingFn = (node?: NodeData) => | ||
| ((node?.data as any)?.spacing as number) || 0; | ||
| const result = formatNodeSizeFn(20, spacingFn, 10); | ||
| expect(result({ id: 'test', data: { spacing: 5 } })).toBe(25); // 20 + 5 | ||
| expect(result({ id: 'test', data: { spacing: 5 } })).toEqual([ | ||
| 25, 25, 25, | ||
| ]); | ||
| }); | ||
|
|
||
| test('should handle both nodeSize and nodeSpacing as functions', () => { | ||
|
|
@@ -200,7 +194,9 @@ describe('format', () => { | |
| const spacingFn = (node?: NodeData) => | ||
| ((node?.data as any)?.spacing as number) || 0; | ||
| const result = formatNodeSizeFn(sizeFn, spacingFn, 10); | ||
| expect(result({ id: 'test', data: { size: 30, spacing: 5 } })).toBe(35); | ||
| expect(result({ id: 'test', data: { size: 30, spacing: 5 } })).toEqual([ | ||
| 35, 35, 35, | ||
| ]); | ||
| }); | ||
|
|
||
| test('should return default when nodeSize undefined and node has no size in data', () => { | ||
|
|
@@ -209,32 +205,34 @@ describe('format', () => { | |
| id: 'node1', | ||
| data: {}, | ||
| }; | ||
| expect(result(nodeData)).toBe(15); // default 10 + 5 spacing | ||
| expect(result(nodeData)).toEqual([15, 15, 15]); // default 10 + 5 spacing | ||
| }); | ||
|
|
||
| test('should handle negative spacing', () => { | ||
| const result = formatNodeSizeFn(20, -5, 10); | ||
| expect(result()).toBe(15); // 20 + (-5) | ||
| expect(result()).toEqual([15, 15, 15]); // 20 + (-5) | ||
| }); | ||
|
|
||
| test('should handle fractional sizes and spacing', () => { | ||
| const result = formatNodeSizeFn(20.5, 3.2, 10); | ||
| expect(result()).toBeCloseTo(23.7); | ||
| expect(result()[0]).toBeCloseTo(23.7); | ||
| expect(result()[1]).toBeCloseTo(23.7); | ||
| expect(result()[2]).toBeCloseTo(23.7); | ||
| }); | ||
|
|
||
| test('should handle number zero node size', () => { | ||
| const result = formatNodeSizeFn(0, 5, 10); | ||
| expect(result()).toBe(15); // 0 is falsy, falls back to default 10 + 5 | ||
| expect(result()).toEqual([5, 5, 5]); | ||
| }); | ||
|
|
||
| test('should handle large sizes', () => { | ||
| const result = formatNodeSizeFn(1000, 100, 10); | ||
| expect(result()).toBe(1100); | ||
| expect(result()).toEqual([1100, 1100, 1100]); | ||
| }); | ||
|
|
||
| test('should use default when all values are undefined', () => { | ||
| const result = formatNodeSizeFn(undefined, undefined, 12); | ||
| expect(result()).toBe(12); | ||
| expect(result()).toEqual([12, 12, 12]); | ||
| }); | ||
|
|
||
| test('should handle node data without spacing function', () => { | ||
|
|
@@ -243,12 +241,12 @@ describe('format', () => { | |
| id: 'node1', | ||
| data: {}, | ||
| }; | ||
| expect(result(nodeData)).toBe(25); | ||
| expect(result(nodeData)).toEqual([25, 25, 25]); | ||
| }); | ||
|
|
||
| test('should handle single element array size', () => { | ||
| const result = formatNodeSizeFn([35], 5, 10); | ||
| expect(result()).toBe(40); // max(35) + 5 | ||
| expect(result()).toEqual([40, 40, 40]); // max(35) + 5 | ||
| }); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| ["getting-started", "installation", "data-mapping", "layout-configuration"] | ||
| ["getting-started", "installation", "webworker", "data-mapping", "layout-configuration"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| import { Badge } from '@theme'; | ||
|
|
||
| # Using WebWorker <Badge text="Performance" type="info" /> | ||
|
|
||
| WebWorker is a browser capability that runs JavaScript in a **separate thread**, helping you avoid main-thread jank caused by heavy computations. | ||
|
|
||
| In `@antv/layout`, when `enableWorker: true` is set and `Worker` is available, layout computation runs in a Worker to reduce main-thread blocking. If the Worker is unavailable or fails to load, it automatically falls back to the main thread. | ||
|
|
||
| ## How to enable | ||
|
|
||
| Just set `enableWorker: true` in layout options: | ||
|
|
||
| ```ts | ||
| import { ForceAtlas2Layout } from '@antv/layout'; | ||
|
|
||
| const layout = new ForceAtlas2Layout({ enableWorker: true }); | ||
| await layout.execute(data); | ||
| ``` | ||
|
|
||
| If you use the UMD build via CDN, make sure `dist/worker.js` is accessible and lives alongside `dist/index.min.js` (the worker URL is derived from `index.js/index.min.js` and points to `worker.js` in the same directory). See “Worker notes” in the Installation page for details. | ||
|
|
||
| ## Limitations in Worker mode | ||
|
|
||
| In Worker mode, the library transfers `data/options` between the main thread and the Worker via structured clone. **Functions are not structured-cloneable**, so function callbacks inside options can break the transfer, typically with: | ||
|
|
||
| ```txt | ||
| DataCloneError: Failed to execute 'postMessage' on 'Worker': function could not be cloned | ||
| ``` | ||
|
|
||
| Below are common ways to address it. | ||
|
|
||
| ### Option 1: Use `Expr` (string expressions) instead of callbacks | ||
|
|
||
| `@antv/layout` supports `Expr` (powered by `@antv/expr`) for many numeric/size fields. If a field’s type includes `Expr`, you can pass a string expression instead of a function. | ||
|
|
||
| Example (ForceLayout): | ||
|
|
||
| ```ts | ||
| import { ForceLayout } from '@antv/layout'; | ||
|
|
||
| const layout = new ForceLayout({ | ||
| enableWorker: true, | ||
| // Expr supports expressions only; it does NOT support ?? / ?. / undefined / == / != | ||
| nodeStrength: | ||
| 'node.data.strength === 0 || node.data.strength ? node.data.strength : -30', | ||
| edgeStrength: | ||
| 'edge.data.weight === 0 || edge.data.weight ? edge.data.weight : 0.1', | ||
| nodeSize: 'node.data.size === 0 || node.data.size ? node.data.size : 10', | ||
| }); | ||
|
|
||
| await layout.execute(data); | ||
| ``` | ||
|
|
||
| Expression variable names are **taken from the callback parameter names in the type definitions** (i.e. if the field originally supports a function like `(node) => ...`, then your `Expr` should use `node`): | ||
|
|
||
| - `(node) => ...`: use `node` (e.g. `nodeStrength` / `nodeSize`) | ||
| - `(edge) => ...`: use `edge` (e.g. `edgeStrength`) | ||
| - `(edge, source, target) => ...`: use `edge` / `source` / `target` (e.g. `linkDistance`) | ||
| - If the combo callback parameter is named `combo`, use `combo` | ||
|
|
||
| #### `Expr` syntax tips | ||
|
|
||
| `Expr` follows `@antv/expr` syntax (see: https://github.com/antvis/expr). Common patterns: | ||
|
|
||
| - Property access: `node.data.size`, `edge.source`, `combo.id`, `items[0]` | ||
| - Arithmetic / logic: `a + b * c`, `a > 10 && b < 5`, `!flag` | ||
| - Ternary: `node.data.type === "big" ? 20 : 10` | ||
| - Built-in functions (prefixed with `@`): `@max(a, b)`, `@min(a, b)`, `@sqrt(x)` (built-ins: `abs/ceil/floor/round/sqrt/pow/max/min`) | ||
|
|
||
| Notes: `@antv/expr` does not support `??`, `?.`, `undefined` literals, or `==/!=`. For fallbacks, prefer `?:` / `&&` / `||`, for example: | ||
|
|
||
| ```ts | ||
| // Treat 0 as a valid value | ||
| nodeSize: 'node.data.size === 0 || node.data.size ? node.data.size : 10' | ||
| ``` | ||
|
|
||
| ### Option 2: Pre-compute values into your data | ||
|
|
||
| If you used to do: | ||
|
|
||
| ```ts | ||
| nodeSize: (node) => node.data.size | ||
| ``` | ||
|
|
||
| You can pre-fill `data.nodes[i].data.size` and then use `nodeSize: 'node.data.size'` (or rely on defaults). | ||
|
|
||
| ### Option 3: If you need callbacks, don’t enable Worker | ||
|
|
||
| Some capabilities inherently require functions and cannot be passed into Workers: | ||
|
|
||
| - `onTick`-style callbacks | ||
| - `node` / `edge` mapping functions | ||
| - Any custom logic without an `Expr` alternative | ||
|
|
||
| In these cases, run the layout on the main thread by keeping `enableWorker` off. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| ["getting-started", "installation", "data-mapping", "layout-configuration"] | ||
| ["getting-started", "installation", "webworker", "data-mapping", "layout-configuration"] |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个是
@antv/expr支持的还是 layout 自己实现的There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
antv/expr 支持