diff --git a/packages/f2/src/components/index.ts b/packages/f2/src/components/index.ts index 92fb5361a..943375408 100644 --- a/packages/f2/src/components/index.ts +++ b/packages/f2/src/components/index.ts @@ -32,4 +32,6 @@ export { withCandlestick, CandlestickView, } from './candlestick'; + +export { default as Layout, ChartLayoutProps } from './layout'; export { default as Pictorial, PictorialProps } from './pictorial'; diff --git a/packages/f2/src/components/layout/index.tsx b/packages/f2/src/components/layout/index.tsx new file mode 100644 index 000000000..12831583a --- /dev/null +++ b/packages/f2/src/components/layout/index.tsx @@ -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 { + 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 {child}; + } + return child; + } + + return ( + + {child} + + ); + }); + } + + render() { + const layoutStyle = this.getLayoutStyle(); + const childrenWithLayout = this.calculateChildrenLayout(); + + return {childrenWithLayout}; + } +} diff --git "a/packages/f2/test/components/layout/__image_snapshots__/index-test-tsx-\345\270\203\345\261\200\347\273\204\344\273\266-\345\270\203\345\261\200\347\261\273\345\236\213-grid\345\270\203\345\261\200-1-snap.png" "b/packages/f2/test/components/layout/__image_snapshots__/index-test-tsx-\345\270\203\345\261\200\347\273\204\344\273\266-\345\270\203\345\261\200\347\261\273\345\236\213-grid\345\270\203\345\261\200-1-snap.png" new file mode 100644 index 000000000..63b578c20 Binary files /dev/null and "b/packages/f2/test/components/layout/__image_snapshots__/index-test-tsx-\345\270\203\345\261\200\347\273\204\344\273\266-\345\270\203\345\261\200\347\261\273\345\236\213-grid\345\270\203\345\261\200-1-snap.png" differ diff --git "a/packages/f2/test/components/layout/__image_snapshots__/index-test-tsx-\345\270\203\345\261\200\347\273\204\344\273\266-\345\270\203\345\261\200\347\261\273\345\236\213-\345\234\206\345\275\242\345\270\203\345\261\200-1-snap.png" "b/packages/f2/test/components/layout/__image_snapshots__/index-test-tsx-\345\270\203\345\261\200\347\273\204\344\273\266-\345\270\203\345\261\200\347\261\273\345\236\213-\345\234\206\345\275\242\345\270\203\345\261\200-1-snap.png" new file mode 100644 index 000000000..aebb84ceb Binary files /dev/null and "b/packages/f2/test/components/layout/__image_snapshots__/index-test-tsx-\345\270\203\345\261\200\347\273\204\344\273\266-\345\270\203\345\261\200\347\261\273\345\236\213-\345\234\206\345\275\242\345\270\203\345\261\200-1-snap.png" differ diff --git "a/packages/f2/test/components/layout/__image_snapshots__/index-test-tsx-\345\270\203\345\261\200\347\273\204\344\273\266-\345\270\203\345\261\200\347\261\273\345\236\213-\346\250\252\345\220\221\345\270\203\345\261\200-1-snap.png" "b/packages/f2/test/components/layout/__image_snapshots__/index-test-tsx-\345\270\203\345\261\200\347\273\204\344\273\266-\345\270\203\345\261\200\347\261\273\345\236\213-\346\250\252\345\220\221\345\270\203\345\261\200-1-snap.png" new file mode 100644 index 000000000..a90c9292d Binary files /dev/null and "b/packages/f2/test/components/layout/__image_snapshots__/index-test-tsx-\345\270\203\345\261\200\347\273\204\344\273\266-\345\270\203\345\261\200\347\261\273\345\236\213-\346\250\252\345\220\221\345\270\203\345\261\200-1-snap.png" differ diff --git "a/packages/f2/test/components/layout/__image_snapshots__/index-test-tsx-\345\270\203\345\261\200\347\273\204\344\273\266-\345\270\203\345\261\200\347\261\273\345\236\213-\346\250\252\345\220\221\345\270\203\345\261\200\345\214\205\345\220\253\344\270\244\344\270\252\345\233\276\350\241\250-1-snap.png" "b/packages/f2/test/components/layout/__image_snapshots__/index-test-tsx-\345\270\203\345\261\200\347\273\204\344\273\266-\345\270\203\345\261\200\347\261\273\345\236\213-\346\250\252\345\220\221\345\270\203\345\261\200\345\214\205\345\220\253\344\270\244\344\270\252\345\233\276\350\241\250-1-snap.png" new file mode 100644 index 000000000..b310378ef Binary files /dev/null and "b/packages/f2/test/components/layout/__image_snapshots__/index-test-tsx-\345\270\203\345\261\200\347\273\204\344\273\266-\345\270\203\345\261\200\347\261\273\345\236\213-\346\250\252\345\220\221\345\270\203\345\261\200\345\214\205\345\220\253\344\270\244\344\270\252\345\233\276\350\241\250-1-snap.png" differ diff --git "a/packages/f2/test/components/layout/__image_snapshots__/index-test-tsx-\345\270\203\345\261\200\347\273\204\344\273\266-\345\270\203\345\261\200\347\261\273\345\236\213-\347\272\265\345\220\221\345\270\203\345\261\200\345\214\205\345\220\253\344\270\244\344\270\252\345\233\276\350\241\250-1-snap.png" "b/packages/f2/test/components/layout/__image_snapshots__/index-test-tsx-\345\270\203\345\261\200\347\273\204\344\273\266-\345\270\203\345\261\200\347\261\273\345\236\213-\347\272\265\345\220\221\345\270\203\345\261\200\345\214\205\345\220\253\344\270\244\344\270\252\345\233\276\350\241\250-1-snap.png" new file mode 100644 index 000000000..bc5a6e5e0 Binary files /dev/null and "b/packages/f2/test/components/layout/__image_snapshots__/index-test-tsx-\345\270\203\345\261\200\347\273\204\344\273\266-\345\270\203\345\261\200\347\261\273\345\236\213-\347\272\265\345\220\221\345\270\203\345\261\200\345\214\205\345\220\253\344\270\244\344\270\252\345\233\276\350\241\250-1-snap.png" differ diff --git a/packages/f2/test/components/layout/index.test.tsx b/packages/f2/test/components/layout/index.test.tsx new file mode 100644 index 000000000..b01621134 --- /dev/null +++ b/packages/f2/test/components/layout/index.test.tsx @@ -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 ( + + + + + + + ); +}; + +describe('布局组件', () => { + describe('布局类型', () => { + it('横向布局', async () => { + const context = createContext('横向布局', { + width: '300px', + height: '100px', + }); + const { props } = ( + + + + + + + + ); + 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 } = ( + + + + + + + + + + + ); + 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 } = ( + + + + + + + ); + 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 } = ( + + + + + + + ); + 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 } = ( + + + {/* 第一行 */} + + + {/* 第二行 */} + + + + + ); + const canvas = new Canvas(props); + await canvas.render(); + + await delay(1000); + expect(context).toMatchImageSnapshot(); + }); + }); +});