diff --git a/.editorconfig b/.editorconfig
index b7ee6ef3..25fec9f7 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -18,4 +18,4 @@ indent_size = 1
[*.md]
trim_trailing_whitespace = false
-indent_size = 4
+indent_size = 2
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 720aded6..73259f95 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -24,7 +24,13 @@ jobs:
- name: build
run: |
pnpm run ci
-
+
- name: test
run: |
pnpm test
+
+ - name: Upload coverage reports to Codecov
+ uses: codecov/codecov-action@v5
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ slug: antvis/layout
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 4d827228..88c9a298 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -6,7 +6,8 @@
"graphlib",
"nodesep",
"rankdir",
- "ranksep"
+ "ranksep",
+ "relayout"
],
"rust-analyzer.linkedProjects": ["./packages/layout-rust/Cargo.toml"]
}
diff --git a/docs/api/circular.md b/docs/api/circular.md
new file mode 100644
index 00000000..ba3bdfad
--- /dev/null
+++ b/docs/api/circular.md
@@ -0,0 +1,232 @@
+# Circular
+
+Circular layout arranges nodes on a circle. By tuning the configurations, user can adjust the node ordering method, division number, radial layout, and so on. We implement it according to the paper: [A framework and algorithms for circular drawings of graphs](https://www.sciencedirect.com/science/article/pii/S1570866705000031).
+
+
+
+
+
+## Usage
+
+First we need to create a graph model with `@antv/graphlib`.
+
+```ts
+import { Graph } from '@antv/graphlib';
+
+const graph = new Graph({
+ nodes: [
+ { id: 'node1', data: {} },
+ { id: 'node2', data: {} },
+ { id: 'node3', data: {} },
+ // ...
+ ],
+ edges: [
+ { id: 'edge1', source: 'node1', target: 'node2', data: {} },
+ // ...
+ ],
+});
+```
+
+Then select a circular layout from `@antv/layout`.
+
+```ts
+import { CircularLayout } from '@antv/layout';
+
+const circular = new CircularLayout({
+ center: [250, 250],
+ radius: 200,
+});
+```
+
+Returns the positions of nodes after calculating the layout.
+
+```ts
+const positions = await circular.execute(graph);
+// positions = { nodes: [...], edges: [...] }
+```
+
+Or we can directly assign the positions to the nodes by calling `assign`:
+
+```ts
+await circular.assign(graph);
+```
+
+## Options
+
+| Key | Description | Type | Default |
+| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ------------------------- |
+| width | The width of the layout. | `number` | `undefined` |
+| height | The height of the layout. | `number` | `undefined` |
+| center | The center of the layout. | `[number, number]` | `[width / 2, height / 2]` |
+| radius | The radius of the circle. If specified, `startRadius` and `endRadius` will be ignored. | `number` | `null` |
+| startRadius | The start radius for spiral layout. | `number` | `null` |
+| endRadius | The end radius for spiral layout. | `number` | `null` |
+| startAngle | The start angle in radians for placing nodes. | `number` | `0` |
+| endAngle | The end angle in radians for placing nodes. | `number` | `2 * Math.PI` |
+| clockwise | Whether to place nodes in clockwise order. | `boolean` | `true` |
+| divisions | The division number of nodes on the circle. Takes effect when `endRadius - startRadius !== 0`. | `number` | `1` |
+| ordering | The ordering method for nodes. `null` means nodes are arranged in data order; `'topology'` means in topology order; `'topology-directed'` means in directed topology order; `'degree'` means in degree order. | null | 'topology' | 'topology-directed' | 'degree' | `null` |
+| angleRatio | The ratio of the angle between nodes. For example, `angleRatio: 1` means the angle between nodes is `(endAngle - startAngle) / nodeCount`. | `number` | `1` |
+| nodeSize | The size of nodes. Used with `nodeSpacing` to calculate radius automatically. | number | [number, number] | ((node: Node) => number | [number, number]) | `undefined` |
+| nodeSpacing | The spacing between nodes. Used with `nodeSize` to calculate radius automatically. | number | ((node: Node) => number) | `undefined` |
+
+## Layout Types
+
+### Circle Layout
+
+When `radius` is specified, nodes are arranged on a single circle.
+
+```ts
+const circular = new CircularLayout({
+ center: [250, 250],
+ radius: 200,
+});
+```
+
+```plain
+ node2
+ ●
+ ● ●
+ node1 node3
+
+ ● ● ●
+ node8 C node4
+
+ ● ●
+ node7 node5
+ ●
+ node6
+
+C = center (250, 250)
+radius = 200
+```
+
+### Spiral Layout
+
+When `startRadius` and `endRadius` are specified (without `radius`), nodes are arranged in a spiral pattern.
+
+```ts
+const circular = new CircularLayout({
+ center: [250, 250],
+ startRadius: 50,
+ endRadius: 200,
+});
+```
+
+```plain
+ node_n
+ ●
+ ● ●
+ ●
+ ● C ●
+ node1
+ ● ●
+ ●
+
+Nodes spiral outward from startRadius to endRadius
+```
+
+### Multi-Division Layout
+
+When `divisions > 1`, nodes are divided into multiple arcs.
+
+```ts
+const circular = new CircularLayout({
+ center: [250, 250],
+ radius: 200,
+ divisions: 4,
+});
+```
+
+```plain
+Division 1 Division 2
+ ● ● ● ●
+
+ ● ●
+ C
+ ● ●
+
+ ● ● ● ●
+Division 4 Division 3
+
+Each division occupies 2π/4 radians
+```
+
+## Node Ordering
+
+### Default Ordering (null)
+
+Nodes are arranged in their original data order.
+
+### Topology Ordering
+
+Nodes are arranged based on graph topology, placing neighbors close to each other.
+
+```ts
+const circular = new CircularLayout({
+ ordering: 'topology',
+});
+```
+
+### Topology-Directed Ordering
+
+Similar to topology ordering but considers edge direction.
+
+```ts
+const circular = new CircularLayout({
+ ordering: 'topology-directed',
+});
+```
+
+### Degree Ordering
+
+Nodes are sorted by their degree (number of connections) in ascending order.
+
+```ts
+const circular = new CircularLayout({
+ ordering: 'degree',
+});
+```
+
+## Methods
+
+### execute
+
+# **execute**(graph: Graph, options?: CircularLayoutOptions): Promise<LayoutMapping>
+
+Returns the positions of nodes and edges after calculating the layout.
+
+**Parameters:**
+
+- `graph`: The graph instance.
+- `options`: Optional layout options to override the constructor options.
+
+**Returns:**
+
+```ts
+{
+ nodes: Node[], // nodes with x, y positions in data
+ edges: Edge[] // edges
+}
+```
+
+### assign
+
+# **assign**(graph: Graph, options?: CircularLayoutOptions): Promise<void>
+
+Directly assigns the calculated positions to the nodes in the graph.
+
+**Parameters:**
+
+- `graph`: The graph instance.
+- `options`: Optional layout options to override the constructor options.
+
+## Use Cases
+
+Circular layout is particularly useful for:
+
+- **Social networks**: Visualizing connections in a circular pattern
+- **Cycle detection**: Highlighting circular dependencies
+- **Timeline visualization**: Arranging events in a circular timeline
+- **Periodic data**: Displaying seasonal or cyclical patterns
+- **Network topology**: Showing network structure in a symmetric way
diff --git a/packages/layout/__tests__/demos/circular.ts b/packages/layout/__tests__/demos/circular.ts
index 3daf02df..34d7236c 100644
--- a/packages/layout/__tests__/demos/circular.ts
+++ b/packages/layout/__tests__/demos/circular.ts
@@ -34,53 +34,64 @@ export function render(canvas: Canvas, gui?: GUI) {
endAngle: 2 * Math.PI,
clockwise: true,
divisions: 1,
+ ordering: 'original',
};
folder.add(config, 'centerX', 0, 500).onChange((centerX: number) => {
- relayout({ center: [centerX, config.centerY] });
+ relayout({ ...config, center: [centerX, config.centerY] });
});
folder.add(config, 'centerY', 0, 500).onChange((centerY: number) => {
- relayout({ center: [config.centerX, centerY] });
+ relayout({ ...config, center: [config.centerX, centerY] });
});
folder.add(config, 'radius', 0, 500).onChange((radius: number) => {
- relayout({ radius });
+ relayout({ ...config, radius });
});
folder
.add(config, 'startRadius', 0, 500)
.onChange((startRadius: number) => {
- relayout({ radius: 0, startRadius, endRadius: config.endRadius });
+ relayout({ ...config, radius: 0, startRadius });
});
folder.add(config, 'endRadius', 0, 500).onChange((endRadius: number) => {
- relayout({ radius: 0, endRadius, startRadius: config.startRadius });
+ relayout({ ...config, radius: 0, endRadius });
});
folder
.add(config, 'startAngle', 0, 2 * Math.PI)
.onChange((startAngle: number) => {
- relayout({ startAngle });
+ relayout({ ...config, startAngle });
});
folder
.add(config, 'endAngle', 0, 2 * Math.PI)
.onChange((endAngle: number) => {
- relayout({ endAngle });
+ relayout({ ...config, endAngle });
});
folder.add(config, 'clockwise').onChange((clockwise: boolean) => {
relayout({
+ ...config,
clockwise,
radius: 0,
- endRadius: config.endRadius,
- startRadius: config.startRadius,
});
});
folder.add(config, 'divisions', 0, 10).onChange((divisions: number) => {
- relayout({ divisions });
+ relayout({ ...config, divisions });
});
+
+ folder
+ .add(config, 'ordering', [
+ 'original',
+ 'degree',
+ 'topology',
+ 'topology-directed',
+ ])
+ .onChange((ordering) => {
+ relayout({ ...config, ordering });
+ });
}
return canvas;
diff --git a/packages/layout/__tests__/snapshots/circular/counterclockwise-radius-range.svg b/packages/layout/__tests__/snapshots/circular/counterclockwise-radius-range.svg
new file mode 100644
index 00000000..d2d036d6
--- /dev/null
+++ b/packages/layout/__tests__/snapshots/circular/counterclockwise-radius-range.svg
@@ -0,0 +1,198 @@
+
diff --git a/packages/layout/__tests__/snapshots/circular/custom-center.svg b/packages/layout/__tests__/snapshots/circular/custom-center.svg
new file mode 100644
index 00000000..213d277e
--- /dev/null
+++ b/packages/layout/__tests__/snapshots/circular/custom-center.svg
@@ -0,0 +1,198 @@
+
diff --git a/packages/layout/__tests__/snapshots/circular/default.svg b/packages/layout/__tests__/snapshots/circular/default.svg
index e7100e2f..1bb3e90f 100644
--- a/packages/layout/__tests__/snapshots/circular/default.svg
+++ b/packages/layout/__tests__/snapshots/circular/default.svg
@@ -1,197 +1,197 @@