diff --git a/packages/s2-core/__tests__/data/simple-table-data-rowspan.json b/packages/s2-core/__tests__/data/simple-table-data-rowspan.json new file mode 100644 index 0000000000..94d5e30229 --- /dev/null +++ b/packages/s2-core/__tests__/data/simple-table-data-rowspan.json @@ -0,0 +1,50 @@ +{ + "fields": { + "columns": [ + { + "key": "price", + "rowSpan": 2, + "children": [ + { + "key": "cost", + "rowSpan": 1 + } + ] + }, + "province", + { + "key": "city", + "rowSpan": 1, + "children": [ + { + "key": "type", + "rowSpan": 2 + } + ] + } + ] + }, + "data": [ + { + "province": "浙江", + "city": "义乌", + "type": "笔", + "price": 1, + "cost": 2 + }, + { + "province": "浙江", + "city": "义乌", + "type": "笔", + "price": 1, + "cost": 2 + }, + { + "province": "浙江", + "city": "杭州", + "type": "笔", + "price": 1, + "cost": 2 + } + ] +} diff --git a/packages/s2-core/__tests__/spreadsheet/table-sheet-col-rowspan-spec.ts b/packages/s2-core/__tests__/spreadsheet/table-sheet-col-rowspan-spec.ts new file mode 100644 index 0000000000..b2c28f11f3 --- /dev/null +++ b/packages/s2-core/__tests__/spreadsheet/table-sheet-col-rowspan-spec.ts @@ -0,0 +1,24 @@ +/* + * @description spec for issue #2150 + * https://github.com/antvis/S2/issues/2150 + * 明细表-列分组增加 rowSpan 配置项, 用来支持自定义合并单元格 + */ +import { getContainer } from 'tests/util/helpers'; +import dataCfg from 'tests/data/simple-table-data-rowspan.json'; +import { TableSheet } from '@/sheet-type'; +import type { S2Options } from '@/common'; + +const s2Options: S2Options = { + width: 600, + height: 480, +}; + +describe('TableSheet Sort Tests', () => { + test('should be rendered based on the rowSpan', () => { + const s2 = new TableSheet(getContainer(), dataCfg, s2Options); + s2.render(); + const { height: cellHeightOfLevel0 } = s2.getColumnNodes(0).slice(-1)[0]; + const { height: cellHeightOfLevel1 } = s2.getColumnNodes(1).slice(-1)[0]; + expect(cellHeightOfLevel0 * 2).toBe(cellHeightOfLevel1); + }); +}); diff --git a/packages/s2-core/src/common/interface/basic.ts b/packages/s2-core/src/common/interface/basic.ts index 62d5c0a57f..71bf3d0032 100644 --- a/packages/s2-core/src/common/interface/basic.ts +++ b/packages/s2-core/src/common/interface/basic.ts @@ -106,6 +106,7 @@ export interface Fields { export interface ColumnNode { key: string; children?: Columns; + rowSpan?: number; } export interface TotalsStatus { diff --git a/packages/s2-core/src/facet/table-facet.ts b/packages/s2-core/src/facet/table-facet.ts index 19e57c7b76..1985b9ccbc 100644 --- a/packages/s2-core/src/facet/table-facet.ts +++ b/packages/s2-core/src/facet/table-facet.ts @@ -4,8 +4,10 @@ import { isBoolean, isNil, isNumber, + isString, last, maxBy, + omit, set, values, } from 'lodash'; @@ -23,6 +25,8 @@ import { import { FrozenCellGroupMap } from '../common/constant/frozen'; import { DebuggerUtil } from '../common/debug'; import type { + ColumnNode, + Columns, FilterParam, LayoutResult, ResizeInteractionOptions, @@ -289,11 +293,68 @@ export class TableFacet extends BaseFacet { return cellCfg?.width; } + /** 扁平化树形 columns */ + private flattenTree(tree: Columns): Columns { + return tree.reduce((prev, curr) => { + if (isString(curr)) { + prev = prev.concat(curr); + } else { + prev = prev.concat(omit(curr, 'children')); + if (curr.children?.length) { + prev = prev.concat(this.flattenTree(curr.children)); + } + } + return prev; + }, []); + } + + /** 当前节点所在列是否配置了 rowSpan */ + private hasRowSpanInBranch(tree: Columns, field: string): boolean { + const columns = tree.map((item) => this.flattenTree([item])); + const branch = columns.find((items) => { + return items.some((item) => { + if (isString(item)) { + return item === field; + } + if (item.key === field) { + return true; + } + return false; + }); + }); + return branch?.some((item) => !isString(item) && Boolean(item?.rowSpan)); + } + + /** 当前节点的 rowSpan */ + private findRowSpanInCurrentNode( + columns: Columns, + field: string, + ): number | void { + const flattedColumns = this.flattenTree(columns); + const column = flattedColumns.find((item) => { + return !isString(item) && item.key === field; + }); + return !isString(column) && column?.rowSpan; + } + + /** TIP: 获取列头 Node 高度 */ private getColNodeHeight(col: Node, totalHeight?: number) { - const { colCfg } = this.cfg; + const { colCfg, columns } = this.cfg; + // 明细表所有列节点高度保持一致 const userDragHeight = values(colCfg?.heightByField)[0]; + + const hasRowSpanInBranch = this.hasRowSpanInBranch(columns, col.field); + const currentRowSpan = this.findRowSpanInCurrentNode(columns, col.field); + + /** 用户拖拽高度 > 配置高度 */ const height = userDragHeight || colCfg?.height; + + // 如果当前列任意 leaf 设置了 rowSpan, 按照 rowSpan 划分列头单元格高度 + if (hasRowSpanInBranch) { + return height * (currentRowSpan || 1); + } + if (!totalHeight) { return height; } @@ -308,15 +369,35 @@ export class TableFacet extends BaseFacet { return totalHeight; } + private getKeysOfFirstColumn(columns: Columns) { + let res = []; + const key = isString(columns?.[0]) ? columns?.[0] : columns?.[0]?.key; + if (key) { + res = res.concat(key); + } + if (!isString(columns?.[0]) && columns?.[0]?.children?.length) { + res = res.concat(this.getKeysOfFirstColumn(columns?.[0]?.children)); + } + return res; + } + private calculateColNodesCoordinate( colLeafNodes: Node[], colsHierarchy: Hierarchy, ) { + const { columns } = this.cfg; let preLeafNode = Node.blankNode(); const allNodes = colsHierarchy.getNodes(); - for (const levelSample of colsHierarchy.sampleNodesForAllLevels) { - levelSample.height = this.getColNodeHeight(levelSample); - colsHierarchy.height += levelSample.height; + + const keys = this.getKeysOfFirstColumn(columns); + + const nodesOfFirstColumn = allNodes.filter((node) => { + return keys.includes(node.field); + }); + + for (const nodeSample of nodesOfFirstColumn) { + nodeSample.height = this.getColNodeHeight(nodeSample); + colsHierarchy.height += nodeSample.height; } const adaptiveColWidth = this.getAdaptiveColWidth(colLeafNodes); let currentCollIndex = 0; diff --git a/s2-site/docs/api/general/S2DataConfig.en.md b/s2-site/docs/api/general/S2DataConfig.en.md index f477162b0b..df8f4f8ff9 100644 --- a/s2-site/docs/api/general/S2DataConfig.en.md +++ b/s2-site/docs/api/general/S2DataConfig.en.md @@ -116,5 +116,6 @@ Function description: used to support custom data cell rendering of multiple ind | 属性名称 | 说明 | 类型 | 默认值 | 必选 | | ------- | ---------| -------| ------|------| -| name | 列字段 id 或分组 id | string | | ✓ | +| key | 列字段 id 或分组 id | string | | ✓ | +| rowSpan | 合并单元格行数,配置后则优先按照 rowSpan 规划列头单元格高度 | number | | | | children | 分组下面的子级 | Array\ | | | diff --git a/s2-site/docs/api/general/S2DataConfig.zh.md b/s2-site/docs/api/general/S2DataConfig.zh.md index f06b8effbe..8d6740f28e 100644 --- a/s2-site/docs/api/general/S2DataConfig.zh.md +++ b/s2-site/docs/api/general/S2DataConfig.zh.md @@ -115,4 +115,5 @@ object **必选**,_default:null_ | 属性名称 | 说明 | 类型 | 默认值 | 必选 | | ------- | ---------| -------| ------|------| | key | 列字段 id 或分组 id | string | | ✓ | +| rowSpan | 合并单元格行数,配置后则优先按照 rowSpan 规划列头单元格高度 | number | | | | children | 分组下面的子级 | `Array` | | | diff --git a/s2-site/examples/basic/table/demo/meta.json b/s2-site/examples/basic/table/demo/meta.json index 57336fa764..312d6e6891 100644 --- a/s2-site/examples/basic/table/demo/meta.json +++ b/s2-site/examples/basic/table/demo/meta.json @@ -19,6 +19,14 @@ "en": "Column Group" }, "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*DgnhTYveL1AAAAAAAAAAAAAADmJ7AQ/original" + }, + { + "filename": "table-column-group-customize-cell.ts", + "title": { + "zh": "列分组自定义合并单元格", + "en": "Column Group Customize The Cell" + }, + "screenshot": "/rowspan.png" } ] } diff --git a/s2-site/examples/basic/table/demo/table-column-group-customize-cell.ts b/s2-site/examples/basic/table/demo/table-column-group-customize-cell.ts new file mode 100644 index 0000000000..1a773c1a51 --- /dev/null +++ b/s2-site/examples/basic/table/demo/table-column-group-customize-cell.ts @@ -0,0 +1,78 @@ +import { TableSheet } from '@antv/s2'; + +fetch('https://assets.antv.antgroup.com/s2/basic-table-mode.json') + .then((res) => res.json()) + .then((data) => { + const container = document.getElementById('container'); + const s2DataConfig = { + fields: { + columns: [ + { + key: 'area', + rowSpan: 1, + children: [ + { + key: 'province', + rowSpan: 2, + }, + { + key: 'city', + rowSpan: 2, + }, + ], + }, + 'type', + { + key: 'money', + rowSpan: 2, + children: [ + { + key: 'price', + rowSpan: 1, + }, + ], + }, + ], + }, + meta: [ + { + field: 'province', + name: '省份', + }, + { + field: 'city', + name: '城市', + }, + { + field: 'type', + name: '商品类别', + }, + { + field: 'price', + name: '价格', + }, + { + field: 'cost', + name: '成本', + }, + { + field: 'area', + name: '位置', + }, + { + field: 'money', + name: '金额', + }, + ], + data, + }; + + const s2Options = { + width: 600, + height: 480, + showSeriesNumber: true, + }; + const s2 = new TableSheet(container, s2DataConfig, s2Options); + + s2.render(); + }); diff --git a/s2-site/public/rowspan.png b/s2-site/public/rowspan.png new file mode 100644 index 0000000000..4ba32d8bbd Binary files /dev/null and b/s2-site/public/rowspan.png differ