Skip to content

feat: 增加布局组件 #2055

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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: 2 additions & 0 deletions packages/f2/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ export {
withCandlestick,
CandlestickView,
} from './candlestick';

export { default as Layout, ChartLayoutProps } from './layout';
export { default as Pictorial, PictorialProps } from './pictorial';
134 changes: 134 additions & 0 deletions packages/f2/src/components/layout/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { Component, jsx } from '@antv/f-engine';
import { isString } from '@antv/util';

export interface ChartLayoutProps {
type?: 'horizontal' | 'vertical' | 'grid' | 'circular';
children?: any;
style?: any;
columns?: number; // 列数
gap?: number | [number, number]; // 间距
itemStyle?: any; // 子组件容器的样式
}

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const Shape = 2;

export default class Layout extends Component<ChartLayoutProps> {
getWorkTag(type) {
if (isString(type)) {
return Shape;
}
if (type.prototype && type.prototype.isF2Component) {
return ClassComponent;
}
return FunctionComponent;
}
getLayoutStyle() {
const { type, style } = this.props;
const baseStyle = {
display: 'flex',
...style,
};

switch (type) {
case 'horizontal':
return {
...baseStyle,
flexDirection: 'row',
alignItems: 'center',
};
case 'vertical':
return {
...baseStyle,
flexDirection: 'column',
alignItems: 'center',
};
case 'grid':
return {
...baseStyle,
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'flex-start',
alignItems: 'flex-start',
};
case 'circular':
return {
// ...baseStyle,
};
default:
return baseStyle;
}
}

calculateChildrenLayout() {
const { type, children, columns = 3, gap = 0, itemStyle } = this.props;
const { width: containerWidth, height: containerHeight } = this.layout;
const childArray = Array.isArray(children) ? children : [children];
const [verticalGap, horizontalGap] = Array.isArray(gap) ? gap : [gap, gap];

return childArray.map((child, index) => {
const childStyle = this.context.px2hd(child.props?.style);
let width = childStyle?.width || containerWidth;
let height = childStyle?.height || containerHeight;
let top = 0;
let left = 0;

if (type === 'horizontal') {
// 水平布局:子元素平均分配容器宽度
const totalGapWidth = (childArray.length - 1) * horizontalGap;
const itemWidth = (containerWidth - totalGapWidth) / childArray.length;
width = itemWidth;
left = index * (itemWidth + horizontalGap);
} else if (type === 'vertical') {
// 垂直布局:子元素平均分配容器高度
const totalGapHeight = (childArray.length - 1) * verticalGap;
const itemHeight = (containerHeight - totalGapHeight) / childArray.length;
height = itemHeight;
top = index * (itemHeight + verticalGap);
} else if (type === 'grid') {
// 双向布局:根据列数计算每个子元素的宽度和位置
const row = Math.floor(index / columns);
const col = index % columns;
const totalGapWidth = (columns - 1) * horizontalGap;
const itemWidth = (containerWidth - totalGapWidth) / columns;
width = itemWidth;
left = col * (itemWidth + horizontalGap);
top = row * (height + verticalGap);
} else if (type === 'circular') {
// 圆形布局:将子元素均匀分布在圆周上
const radius = Math.min(containerWidth, containerHeight) / 2;
const angle = (index * 2 * Math.PI) / childArray.length;
left = containerWidth / 2 + radius * Math.cos(angle);
top = containerHeight / 2 + radius * Math.sin(angle);
}
const tag = this.getWorkTag(child.type);

if (tag === Shape) {
if (type === 'circular') {
return <group style={{ x: left, y: top, ...itemStyle }}>{child}</group>;
}
return child;
}

return (
<group
style={{
width,
height,
...itemStyle,
}}
>
{child}
</group>
);
});
}

render() {
const layoutStyle = this.getLayoutStyle();
const childrenWithLayout = this.calculateChildrenLayout();

return <group style={layoutStyle}>{childrenWithLayout}</group>;
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
162 changes: 162 additions & 0 deletions packages/f2/test/components/layout/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { jsx, Component, Canvas, Chart, Line, Legend, Tooltip, TextGuide } from '../../../src';
import { Layout } from '../../../src/components';
import { createContext, delay } from '../../util';
const data1 = [
{ x: 0, y: 1, name: 'A' },
{ x: 1, y: 2, name: 'B' },
{ x: 2, y: 3, name: 'C' },
{ x: 3, y: 4, name: 'D' },
];

const data2 = [
{ x: 0, y: 4, name: 'E' },
{ x: 1, y: 3, name: 'F' },
{ x: 2, y: 2, name: 'G' },
{ x: 3, y: 1, name: 'H' },
];

const ChartA = (props) => {
const { data, color, style } = props;
return (
<Chart data={data} color={color} style={style}>
<Line x="x" y="y" color={color} />
<Legend />
<Tooltip />
<TextGuide content={`textGuide`} records={[data[1]]} />
</Chart>
);
};

describe('布局组件', () => {
describe('布局类型', () => {
it('横向布局', async () => {
const context = createContext('横向布局', {
width: '300px',
height: '100px',
});
const { props } = (
<Canvas context={context} pixelRatio={1}>
<Layout type="horizontal">
<rect style={{ width: '50px', height: '50px', fill: '#FF5733' }} />
<rect style={{ width: '50px', height: '50px', fill: '#33FF57' }} />
<rect style={{ width: '50px', height: '50px', fill: '#3357FF' }} />
</Layout>
</Canvas>
);
const canvas = new Canvas(props);
await canvas.render();

await delay(1000);
expect(context).toMatchImageSnapshot();
});

it('圆形布局', async () => {
const context = createContext('圆形布局', {
width: '300px',
height: '100px',
});
const { props } = (
<Canvas context={context} pixelRatio={1}>
<Layout type="circular">
<circle style={{ r: '10px', fill: '#FF6B6B' }} />
<circle style={{ r: '10px', fill: '#4ECDC4' }} />
<circle style={{ r: '10px', fill: '#45B7D1' }} />
<circle style={{ r: '10px', fill: '#96CEB4' }} />
<circle style={{ r: '10px', fill: '#FFEEAD' }} />
<circle style={{ r: '10px', fill: '#D4A5A5' }} />
</Layout>
</Canvas>
);
const canvas = new Canvas(props);
await canvas.render();

await delay(1000);
expect(context).toMatchImageSnapshot();
});

it('横向布局包含两个图表', async () => {
const context = createContext('横向布局', {
width: '300px',
height: '100px',
});
const { props } = (
<Canvas context={context}>
<Layout
type="horizontal"
itemStyle={{
stroke: '#333333',
lineWidth: 1,
}}
>
<ChartA data={data1} color="#4ECDC4" style={{ height: '120px' }} />
<ChartA data={data2} color="#FF6B6B" style={{ height: '80px' }} />
</Layout>
</Canvas>
);
const canvas = new Canvas(props);
await canvas.render();

await delay(1000);
expect(context).toMatchImageSnapshot();
});

it('纵向布局包含两个图表', async () => {
const context = createContext('纵向布局包含两个图表', {
width: '200px',
height: '200px',
});

const { props } = (
<Canvas context={context}>
<Layout
type="vertical"
itemStyle={{
stroke: '#333333',
lineWidth: 1,
}}
>
<ChartA data={data1} color="#4ECDC4" />
<ChartA data={data2} color="#FF6B6B" />
</Layout>
</Canvas>
);
const canvas = new Canvas(props);
await canvas.render();

await delay(1000);
expect(context).toMatchImageSnapshot();
});

it('Grid布局', async () => {
const context = createContext('Grid布局', {
width: '300px',
height: '300px',
});

const { props } = (
<Canvas context={context}>
<Layout
type="grid"
columns={2}
itemStyle={{
stroke: '#333333',
lineWidth: 1,
}}
>
{/* 第一行 */}
<ChartA data={data1} color="#4ECDC4" style={{ height: '120px' }} />
<ChartA data={data2} color="#FF6B6B" style={{ height: '120px' }} />
{/* 第二行 */}
<ChartA data={data1} color="#96CEB4" style={{ height: '120px' }} />
<ChartA data={data2} color="#FFEEAD" style={{ height: '120px' }} />
</Layout>
</Canvas>
);
const canvas = new Canvas(props);
await canvas.render();

await delay(1000);
expect(context).toMatchImageSnapshot();
});
});
});
Loading