import type { ComponentType, JSXElement } from '../../jsx';
import { Defs, Ellipse, getElementBounds, Group, Rect } from '../../jsx';
import type { CompareData } from '../../types';
import { ItemsGroup, ShapesGroup } from '../components';
import { LinearGradient } from '../defs';
import { FlexLayout } from '../layouts';
import { getPaletteColor } from '../utils';
import { registerStructure } from './registry';
import type { BaseStructureProps } from './types';
export interface CompareHierarchyLeftRightProps extends BaseStructureProps {
/** 同侧数据项上下间隔 */
gap?: number;
/** 左右两侧间隔 */
groupGap?: number;
/** 子节点是否环绕根节点 */
surround?: boolean;
/** 数据项指示器样式 */
decoration?: 'none' | 'dot-line' | 'arc-dot' | 'split-line';
/** 是否翻转根节点 */
flipRoot?: boolean;
/** 是否翻转叶子节点 */
flipLeaf?: boolean;
}
const decorationWidthMap = {
none: 5,
'dot-line': 100,
'arc-dot': 20,
'split-line': 5,
};
export const CompareHierarchyLeftRight: ComponentType<
CompareHierarchyLeftRightProps
> = (props) => {
const {
Title,
Items,
data,
gap = 20,
groupGap = 0,
decoration = 'none',
surround = true,
flipRoot = false,
flipLeaf = false,
options,
} = props;
const [RootItem, Item] = Items;
const { title, desc, items = [] } = data as CompareData;
const titleContent = Title ?
: null;
const rootItemContent = (
);
const itemContent = (
);
const rootItemBounds = getElementBounds(rootItemContent);
const itemBounds = getElementBounds(itemContent);
const itemElements: JSXElement[] = [];
const decoElements: JSXElement[] = [];
const [leftRoot, rightRoot] = items;
const leftItems = leftRoot?.children || [];
const rightItems = rightRoot?.children || [];
const totalHeight = Math.max(
rootItemBounds.height,
leftItems.length * (itemBounds.height + gap) - gap,
rightItems.length * (itemBounds.height + gap) - gap,
);
const decorationWidth = decorationWidthMap[decoration] || 0;
// create root items
const leftRootX = itemBounds.width + decorationWidth;
const rightRootX = leftRootX + rootItemBounds.width + groupGap;
const rootY = (totalHeight - rootItemBounds.height) / 2;
if (leftRoot) {
itemElements.push(
,
);
}
if (rightRoot) {
itemElements.push(
,
);
}
const addDecoElement = (
side: 'left' | 'right',
pos: [number, number],
indexes: number[],
) => {
if (decoration === 'none') return;
const [x, y] = pos;
const currentColor = getPaletteColor(options, indexes);
const props: DecorationProps = {
x,
y,
width: itemBounds.width,
height: itemBounds.height,
side,
color: currentColor || '#ccc',
colorBg: options.themeConfig.colorBg || '#fff',
};
if (decoration === 'split-line') {
decoElements.push();
} else if (decoration === 'dot-line') {
decoElements.push();
}
};
if (surround) {
const diameter = 2 * rootItemBounds.width + groupGap + itemBounds.width;
const radius = diameter / 2 + decorationWidth;
const circleCenterX = leftRootX + rootItemBounds.width + groupGap / 2;
const circleCenterY = rootY + rootItemBounds.height / 2;
leftItems.forEach((item, index) => {
const leftItemsHeight =
leftItems.length * (itemBounds.height + gap) - gap;
const leftStartY = (totalHeight - leftItemsHeight) / 2;
const itemY = leftStartY + index * (itemBounds.height + gap);
const itemCenterY = itemY + itemBounds.height / 2;
const dy = itemCenterY - circleCenterY;
const dxSq = Math.max(0, radius * radius - dy * dy);
const xCenter = circleCenterX - Math.sqrt(dxSq);
const leftX = xCenter - itemBounds.width / 2;
itemElements.push(
,
);
addDecoElement('left', [leftX, itemY], [0, index]);
});
rightItems.forEach((item, index) => {
const rightItemsHeight =
rightItems.length * (itemBounds.height + gap) - gap;
const rightStartY = (totalHeight - rightItemsHeight) / 2;
const itemY = rightStartY + index * (itemBounds.height + gap);
const itemCenterY = itemY + itemBounds.height / 2;
const dy = itemCenterY - circleCenterY;
const dxSq = Math.max(0, radius * radius - dy * dy);
const xCenter = circleCenterX + Math.sqrt(dxSq);
const rightX = xCenter - itemBounds.width / 2;
itemElements.push(
,
);
addDecoElement('right', [rightX, itemY], [1, index]);
});
} else {
// create left items
leftItems.forEach((item, index) => {
const leftItemsHeight =
leftItems.length * (itemBounds.height + gap) - gap;
const leftStartY = (totalHeight - leftItemsHeight) / 2;
const itemY = leftStartY + index * (itemBounds.height + gap);
const indexes = [0, index];
const leftX = 0;
itemElements.push(
,
);
addDecoElement('left', [leftX, itemY], indexes);
});
// create right items
rightItems.forEach((item, index) => {
const rightItemsHeight =
rightItems.length * (itemBounds.height + gap) - gap;
const rightStartY = (totalHeight - rightItemsHeight) / 2;
const itemY = rightStartY + index * (itemBounds.height + gap);
const indexes = [1, index];
const rightX = rightRootX + rootItemBounds.width + decorationWidth;
itemElements.push(
,
);
addDecoElement('right', [rightX, itemY], indexes);
});
}
return (
{titleContent}
{itemElements}
{decoElements}
);
};
interface DecorationProps {
x: number;
y: number;
side: 'left' | 'right';
width: number;
height: number;
color: string;
colorBg: string;
}
const SplitLine = (props: DecorationProps) => {
const { x, y, width, height, color, colorBg, side } = props;
const lineY = y + height;
const linearGradientId = `split-line-linear-gradient-${side}`;
return (
<>
>
);
};
const DotLine = (props: DecorationProps) => {
const { x, y, side, width, height, color, colorBg } = props;
const radius = 6;
const innerRadius = radius / 3;
const d = radius * 2;
const innerD = innerRadius * 2;
const gap = 5;
const cx = side === 'left' ? x + width + radius + gap : x - radius - gap;
const cy = y + height / 2;
const innerX = cx - innerRadius;
const innerY = cy - innerRadius;
const lineLength = 80;
const dx = side === 'left' ? lineLength : -lineLength;
const linearGradientId = `dot-line-linear-gradient-${side}`;
return (
);
};
registerStructure('compare-hierarchy-left-right', {
component: CompareHierarchyLeftRight,
composites: ['title', 'item'],
});