import { normalizeViewport, orderByDegree, orderByTopology } from '../../util';
import { applySingleNodeLayout } from '../../util/common';
import { formatNodeSizeFn } from '../../util/format';
import { BaseLayout } from '../base-layout';
import type { CircularLayoutOptions } from './types';
export type { CircularLayoutOptions };
const DEFAULT_LAYOUT_OPTIONS: CircularLayoutOptions = {
radius: null,
startRadius: null,
endRadius: null,
startAngle: 0,
endAngle: 2 * Math.PI,
clockwise: true,
divisions: 1,
ordering: null,
angleRatio: 1,
nodeSize: 10,
nodeSpacing: 0,
};
/**
* 环形布局
*
* Circular layout
*/
export class CircularLayout extends BaseLayout {
id = 'circular';
protected getDefaultOptions(): Partial {
return DEFAULT_LAYOUT_OPTIONS;
}
protected async layout(): Promise {
const { width, height, center } = normalizeViewport(this.options);
const n = this.model.nodeCount();
if (!n || n === 1) {
applySingleNodeLayout(this.model, center);
return;
}
const {
ordering,
nodeSpacing,
nodeSize,
endAngle = 2 * Math.PI,
startAngle = 0,
divisions,
angleRatio,
clockwise,
} = this.options;
// Order nodes based on strategy
if (ordering === 'topology') {
// layout according to the graph topology ignoring edge directions
orderByTopology(this.model, false);
} else if (ordering === 'topology-directed') {
// layout according to the graph topology considering edge directions
orderByTopology(this.model, true);
} else if (ordering === 'degree') {
// layout according to the descent order of degrees
orderByDegree(this.model, 'asc');
}
let { radius, startRadius, endRadius } = this.options;
const nodes = this.model.nodes();
const sizeFn = formatNodeSizeFn(
nodeSize,
nodeSpacing,
DEFAULT_LAYOUT_OPTIONS.nodeSize as number,
DEFAULT_LAYOUT_OPTIONS.nodeSpacing as number,
);
if (nodeSpacing) {
let perimeter = 0;
for (const node of nodes) {
perimeter += Math.max(...sizeFn(node._original));
}
radius = perimeter / (2 * Math.PI);
} else if (!radius && !startRadius && !endRadius) {
radius = Math.min(height, width) / 2;
} else if (!startRadius && endRadius) {
startRadius = endRadius;
} else if (startRadius && !endRadius) {
endRadius = startRadius;
}
// Calculate node positions
const angleStep = (endAngle - startAngle) / n;
const adjustedStep = angleStep * angleRatio!;
const nodesPerDivision = Math.ceil(n / divisions!);
const divAngle = (2 * Math.PI) / divisions!;
for (let i = 0; i < n; ) {
const node = nodes[i];
// Calculate radius for this node
let r = radius;
if (!r && startRadius !== null && endRadius !== null) {
r = startRadius! + (i * (endRadius! - startRadius!)) / (n - 1);
}
if (!r) {
r = 10 + (i * 100) / (n - 1);
}
// Calculate angle for this node
const divisionIndex = Math.floor(i / nodesPerDivision);
const indexInDivision = i % nodesPerDivision;
const theta = indexInDivision * adjustedStep + divAngle * divisionIndex;
let angle = startAngle + theta;
if (!clockwise) {
angle = endAngle - theta;
}
// Set position
node.x = center[0] + Math.cos(angle) * r;
node.y = center[1] + Math.sin(angle) * r;
i++;
}
}
}