Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ indent_size = 1

[*.md]
trim_trailing_whitespace = false
indent_size = 4
indent_size = 2
8 changes: 7 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"graphlib",
"nodesep",
"rankdir",
"ranksep"
"ranksep",
"relayout"
],
"rust-analyzer.linkedProjects": ["./packages/layout-rust/Cargo.toml"]
}
232 changes: 232 additions & 0 deletions docs/api/circular.md
Original file line number Diff line number Diff line change
@@ -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).

<p align="center">
<img width="300" src="https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*D85cS7-yqNEAAAAAAAAAAABkARQnAQ" />
</p>

## 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` |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The description for the divisions option states that it "Takes effect when endRadius - startRadius !== 0". This seems to be incorrect. Based on the implementation, divisions are used to partition the circle into sectors and this logic is applied regardless of whether it's a spiral layout or a simple circle layout. This condition should be removed to avoid confusion.

| 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. | <code>null &#124; 'topology' &#124; 'topology-directed' &#124; 'degree'</code> | `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. | <code>number &#124; [number, number] &#124; ((node: Node) => number &#124; [number, number])</code> | `undefined` |
| nodeSpacing | The spacing between nodes. Used with `nodeSize` to calculate radius automatically. | <code>number &#124; ((node: Node) => number)</code> | `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

<a name="circular_execute" href="#circular_execute">#</a> **execute**<i>(graph: Graph, options?: CircularLayoutOptions): Promise&lt;LayoutMapping&gt;</i>

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

<a name="circular_assign" href="#circular_assign">#</a> **assign**<i>(graph: Graph, options?: CircularLayoutOptions): Promise&lt;void&gt;</i>

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
31 changes: 21 additions & 10 deletions packages/layout/__tests__/demos/circular.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading