Skip to content

Commit 6d6b502

Browse files
author
xuying.xu
committed
feat: 增加布局组件
1 parent 382eaef commit 6d6b502

8 files changed

+298
-0
lines changed

packages/f2/src/components/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,6 @@ export {
3232
withCandlestick,
3333
CandlestickView,
3434
} from './candlestick';
35+
36+
export { default as Layout, ChartLayoutProps } from './layout';
3537
export { default as Pictorial, PictorialProps } from './pictorial';
+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { Component, jsx } from '@antv/f-engine';
2+
import { isString } from '@antv/util';
3+
4+
export interface ChartLayoutProps {
5+
type?: 'horizontal' | 'vertical' | 'grid' | 'circular';
6+
children?: any;
7+
style?: any;
8+
columns?: number; // 列数
9+
gap?: number | [number, number]; // 间距
10+
itemStyle?: any; // 子组件容器的样式
11+
}
12+
13+
export const FunctionComponent = 0;
14+
export const ClassComponent = 1;
15+
export const Shape = 2;
16+
17+
export default class Layout extends Component<ChartLayoutProps> {
18+
getWorkTag(type) {
19+
if (isString(type)) {
20+
return Shape;
21+
}
22+
if (type.prototype && type.prototype.isF2Component) {
23+
return ClassComponent;
24+
}
25+
return FunctionComponent;
26+
}
27+
getLayoutStyle() {
28+
const { type, style } = this.props;
29+
const baseStyle = {
30+
display: 'flex',
31+
...style,
32+
};
33+
34+
switch (type) {
35+
case 'horizontal':
36+
return {
37+
...baseStyle,
38+
flexDirection: 'row',
39+
alignItems: 'center',
40+
};
41+
case 'vertical':
42+
return {
43+
...baseStyle,
44+
flexDirection: 'column',
45+
alignItems: 'center',
46+
};
47+
case 'grid':
48+
return {
49+
...baseStyle,
50+
flexDirection: 'row',
51+
flexWrap: 'wrap',
52+
justifyContent: 'flex-start',
53+
alignItems: 'flex-start',
54+
};
55+
case 'circular':
56+
return {
57+
// ...baseStyle,
58+
};
59+
default:
60+
return baseStyle;
61+
}
62+
}
63+
64+
calculateChildrenLayout() {
65+
const { type, children, columns = 3, gap = 0, itemStyle } = this.props;
66+
const { width: containerWidth, height: containerHeight } = this.layout;
67+
const childArray = Array.isArray(children) ? children : [children];
68+
const [verticalGap, horizontalGap] = Array.isArray(gap) ? gap : [gap, gap];
69+
70+
return childArray.map((child, index) => {
71+
const childStyle = this.context.px2hd(child.props?.style);
72+
let width = childStyle?.width || containerWidth;
73+
let height = childStyle?.height || containerHeight;
74+
let top = 0;
75+
let left = 0;
76+
77+
if (type === 'horizontal') {
78+
// 水平布局:子元素平均分配容器宽度
79+
const totalGapWidth = (childArray.length - 1) * horizontalGap;
80+
const itemWidth = (containerWidth - totalGapWidth) / childArray.length;
81+
width = itemWidth;
82+
left = index * (itemWidth + horizontalGap);
83+
} else if (type === 'vertical') {
84+
// 垂直布局:子元素平均分配容器高度
85+
const totalGapHeight = (childArray.length - 1) * verticalGap;
86+
const itemHeight = (containerHeight - totalGapHeight) / childArray.length;
87+
height = itemHeight;
88+
top = index * (itemHeight + verticalGap);
89+
} else if (type === 'grid') {
90+
// 双向布局:根据列数计算每个子元素的宽度和位置
91+
const row = Math.floor(index / columns);
92+
const col = index % columns;
93+
const totalGapWidth = (columns - 1) * horizontalGap;
94+
const itemWidth = (containerWidth - totalGapWidth) / columns;
95+
width = itemWidth;
96+
left = col * (itemWidth + horizontalGap);
97+
top = row * (height + verticalGap);
98+
} else if (type === 'circular') {
99+
// 圆形布局:将子元素均匀分布在圆周上
100+
const radius = Math.min(containerWidth, containerHeight) / 2;
101+
const angle = (index * 2 * Math.PI) / childArray.length;
102+
left = containerWidth / 2 + radius * Math.cos(angle);
103+
top = containerHeight / 2 + radius * Math.sin(angle);
104+
}
105+
const tag = this.getWorkTag(child.type);
106+
107+
if (tag === Shape) {
108+
if (type === 'circular') {
109+
return <group style={{ x: left, y: top, ...itemStyle }}>{child}</group>;
110+
}
111+
return child;
112+
}
113+
114+
return (
115+
<group
116+
style={{
117+
width,
118+
height,
119+
...itemStyle,
120+
}}
121+
>
122+
{child}
123+
</group>
124+
);
125+
});
126+
}
127+
128+
render() {
129+
const layoutStyle = this.getLayoutStyle();
130+
const childrenWithLayout = this.calculateChildrenLayout();
131+
132+
return <group style={layoutStyle}>{childrenWithLayout}</group>;
133+
}
134+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { jsx, Component, Canvas, Chart, Line, Legend, Tooltip, TextGuide } from '../../../src';
2+
import { Layout } from '../../../src/components';
3+
import { createContext, delay } from '../../util';
4+
const data1 = [
5+
{ x: 0, y: 1, name: 'A' },
6+
{ x: 1, y: 2, name: 'B' },
7+
{ x: 2, y: 3, name: 'C' },
8+
{ x: 3, y: 4, name: 'D' },
9+
];
10+
11+
const data2 = [
12+
{ x: 0, y: 4, name: 'E' },
13+
{ x: 1, y: 3, name: 'F' },
14+
{ x: 2, y: 2, name: 'G' },
15+
{ x: 3, y: 1, name: 'H' },
16+
];
17+
18+
const ChartA = (props) => {
19+
const { data, color, style } = props;
20+
return (
21+
<Chart data={data} color={color} style={style}>
22+
<Line x="x" y="y" color={color} />
23+
<Legend />
24+
<Tooltip />
25+
<TextGuide content={`textGuide`} records={[data[1]]} />
26+
</Chart>
27+
);
28+
};
29+
30+
describe('布局组件', () => {
31+
describe('布局类型', () => {
32+
it('横向布局', async () => {
33+
const context = createContext('横向布局', {
34+
width: '300px',
35+
height: '100px',
36+
});
37+
const { props } = (
38+
<Canvas context={context} pixelRatio={1}>
39+
<Layout type="horizontal">
40+
<rect style={{ width: '50px', height: '50px', fill: '#FF5733' }} />
41+
<rect style={{ width: '50px', height: '50px', fill: '#33FF57' }} />
42+
<rect style={{ width: '50px', height: '50px', fill: '#3357FF' }} />
43+
</Layout>
44+
</Canvas>
45+
);
46+
const canvas = new Canvas(props);
47+
await canvas.render();
48+
49+
await delay(1000);
50+
expect(context).toMatchImageSnapshot();
51+
});
52+
53+
it('圆形布局', async () => {
54+
const context = createContext('圆形布局', {
55+
width: '300px',
56+
height: '100px',
57+
});
58+
const { props } = (
59+
<Canvas context={context} pixelRatio={1}>
60+
<Layout type="circular">
61+
<circle style={{ r: '10px', fill: '#FF6B6B' }} />
62+
<circle style={{ r: '10px', fill: '#4ECDC4' }} />
63+
<circle style={{ r: '10px', fill: '#45B7D1' }} />
64+
<circle style={{ r: '10px', fill: '#96CEB4' }} />
65+
<circle style={{ r: '10px', fill: '#FFEEAD' }} />
66+
<circle style={{ r: '10px', fill: '#D4A5A5' }} />
67+
</Layout>
68+
</Canvas>
69+
);
70+
const canvas = new Canvas(props);
71+
await canvas.render();
72+
73+
await delay(1000);
74+
expect(context).toMatchImageSnapshot();
75+
});
76+
77+
it('横向布局包含两个图表', async () => {
78+
const context = createContext('横向布局', {
79+
width: '300px',
80+
height: '100px',
81+
});
82+
const { props } = (
83+
<Canvas context={context}>
84+
<Layout
85+
type="horizontal"
86+
itemStyle={{
87+
stroke: '#333333',
88+
lineWidth: 1,
89+
}}
90+
>
91+
<ChartA data={data1} color="#4ECDC4" style={{ height: '120px' }} />
92+
<ChartA data={data2} color="#FF6B6B" style={{ height: '80px' }} />
93+
</Layout>
94+
</Canvas>
95+
);
96+
const canvas = new Canvas(props);
97+
await canvas.render();
98+
99+
await delay(1000);
100+
expect(context).toMatchImageSnapshot();
101+
});
102+
103+
it('纵向布局包含两个图表', async () => {
104+
const context = createContext('纵向布局包含两个图表', {
105+
width: '200px',
106+
height: '200px',
107+
});
108+
109+
const { props } = (
110+
<Canvas context={context}>
111+
<Layout
112+
type="vertical"
113+
itemStyle={{
114+
stroke: '#333333',
115+
lineWidth: 1,
116+
}}
117+
>
118+
<ChartA data={data1} color="#4ECDC4" />
119+
<ChartA data={data2} color="#FF6B6B" />
120+
</Layout>
121+
</Canvas>
122+
);
123+
const canvas = new Canvas(props);
124+
await canvas.render();
125+
126+
await delay(1000);
127+
expect(context).toMatchImageSnapshot();
128+
});
129+
130+
it('Grid布局', async () => {
131+
const context = createContext('Grid布局', {
132+
width: '300px',
133+
height: '300px',
134+
});
135+
136+
const { props } = (
137+
<Canvas context={context}>
138+
<Layout
139+
type="grid"
140+
columns={2}
141+
itemStyle={{
142+
stroke: '#333333',
143+
lineWidth: 1,
144+
}}
145+
>
146+
{/* 第一行 */}
147+
<ChartA data={data1} color="#4ECDC4" style={{ height: '120px' }} />
148+
<ChartA data={data2} color="#FF6B6B" style={{ height: '120px' }} />
149+
{/* 第二行 */}
150+
<ChartA data={data1} color="#96CEB4" style={{ height: '120px' }} />
151+
<ChartA data={data2} color="#FFEEAD" style={{ height: '120px' }} />
152+
</Layout>
153+
</Canvas>
154+
);
155+
const canvas = new Canvas(props);
156+
await canvas.render();
157+
158+
await delay(1000);
159+
expect(context).toMatchImageSnapshot();
160+
});
161+
});
162+
});

0 commit comments

Comments
 (0)