-
Notifications
You must be signed in to change notification settings - Fork 647
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
feat: 增加布局组件 #2055
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
|
Comment on lines
+102
to
+103
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
| const tag = this.getWorkTag(child.type); | ||
|
|
||
| if (tag === Shape) { | ||
| if (type === 'circular') { | ||
| return <group style={{ x: left, y: top, ...itemStyle }}>{child}</group>; | ||
| } | ||
| return child; | ||
| } | ||
|
Comment on lines
+107
to
+112
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| return ( | ||
| <group | ||
| style={{ | ||
| width, | ||
| height, | ||
| ...itemStyle, | ||
| }} | ||
| > | ||
| {child} | ||
| </group> | ||
|
Comment on lines
+115
to
+123
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
另外,在 |
||
| ); | ||
| }); | ||
| } | ||
|
|
||
| render() { | ||
| const layoutStyle = this.getLayoutStyle(); | ||
| const childrenWithLayout = this.calculateChildrenLayout(); | ||
|
|
||
| return <group style={layoutStyle}>{childrenWithLayout}</group>; | ||
| } | ||
| } | ||
| 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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| 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(); | ||
| }); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
grid布局的行定位计算方式存在问题。这里的top值是基于height计算的,而height来自子元素自身的样式,如果子元素没有设置高度,则会取容器的高度。这在栅格布局中会导致问题:height会是containerHeight,这通常是不对的。建议考虑为
grid布局增加一个itemHeight属性,或者动态计算每行的最大高度来确定下一行的偏移量。