From d7951cc32d85e734a72da8d1a7bc5846ea5a2ae7 Mon Sep 17 00:00:00 2001 From: NoobNot <56724970+NoobNotN@users.noreply.github.com> Date: Fri, 22 Sep 2023 18:02:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B0=8F=E8=AE=A1/=E6=80=BB=E8=AE=A1?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81=E6=8C=89=E7=BB=B4?= =?UTF-8?q?=E5=BA=A6=E5=88=86=E7=BB=84=E6=B1=87=E6=80=BB=20(#2346)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(Api): 添加 totalDimensionGroup/subTotalDimensionGroup api,以及一些临时的开发函数 * feat(Hierarchy): 总计小计结点下添加Hierarchy * feat(Render): getMultipleMap 实现,计算总计小计下的布局信息 * feat(Render): 按维度分组的小计总计下表头位置的调整和渲染 * feat(DataSet): 存在维度分组时的汇总值获取 * feat(DataSet): 存在维度分组时的汇总值获取 * feat(DataSet): 存在维度分组时的汇总值获取 * feat: 补充注释 * feat: 单测快照更新,添加isTotalRoot属性 * fix: 有多个 Value 时不允许隐藏度量列 * fix: 有多个 Value 时不允许隐藏度量列 * fix: 删除了一个莫名其妙的函数 * test: 按维度分组汇总能力单测 * docs: 按维度分组汇总能力文档 * test: 更新,多度量指标不允许隐藏指标头 * docs: 图片示例 * test: 更新 snap 数据文件 * chore: 版本号更新 * chore: 版本号更新 * chore: 版本号更新 * chore: 版本号更新 * chore: 版本号更新 * chore: 版本号更新 * chore: 版本号更新 * Merge remote-tracking branch 'origin/Juze_TotalsDimGroup' into Juze_TotalsDimGroup * test: 更新快照 * chore: 删除开发测试文件 * fix: 汇总指标节点也是汇总节点 * chore: 删除无用文件 * fix: isTotalRoot 替换 isTotals * fix: isTotalRoot 替换 isTotals * fix: isTotalRoot 替换 isTotals * test: 更新 React 包快照 * fix: 修复树状模式下总计节点的指标节点没有被格式化数据 * refactor: 修改代码风格和编码规范 * test: 修改代码风格和编码规范 * test: 修改代码风格和编码规范 * test: 修改代码风格和编码规范 * chore: 更改 interface 命名规范 --------- Co-authored-by: JuZe Co-authored-by: Wenjun Xu <906626481@qq.com> Co-authored-by: Jinke Li --- .../s2-core/__tests__/bugs/issue-1715-spec.ts | 7 +- .../__snapshots__/corner-spec.ts.snap | 24 ++ .../__tests__/spreadsheet/corner-spec.ts | 8 + .../spreadsheet/sort-by-order-spec.ts | 11 + .../__tests__/unit/cell/header-cell-spec.ts | 5 +- .../pivot-data-set-total-spec.ts.snap | 121 +++++++ .../data-set/pivot-data-set-total-spec.ts | 240 ++++++++++++++ .../unit/utils/export/export-spec.ts | 71 ++++ .../__tests__/unit/utils/facet-spec.ts | 19 -- packages/s2-core/src/cell/header-cell.ts | 7 +- .../s2-core/src/common/interface/basic.ts | 4 + .../s2-core/src/data-set/base-data-set.ts | 22 ++ packages/s2-core/src/data-set/interface.ts | 18 + .../s2-core/src/data-set/pivot-data-set.ts | 308 ++++++++++++++---- .../s2-core/src/data-set/table-data-set.ts | 4 + .../README-adjustTotalNodesCoordinate.md | 57 ++++ .../src/facet/layout/build-gird-hierarchy.ts | 163 +++++---- .../facet/layout/build-row-tree-hierarchy.ts | 8 +- .../s2-core/src/facet/layout/interface.ts | 15 +- packages/s2-core/src/facet/layout/node.ts | 5 + .../s2-core/src/facet/layout/total-class.ts | 21 +- packages/s2-core/src/facet/pivot-facet.ts | 193 +++++------ .../s2-core/src/sheet-type/spread-sheet.ts | 10 +- .../src/utils/dataset/pivot-data-set.ts | 16 +- packages/s2-core/src/utils/export/copy.ts | 40 ++- packages/s2-core/src/utils/facet.ts | 12 - .../s2-core/src/utils/layout/add-totals.ts | 14 +- .../src/utils/layout/generate-header-nodes.ts | 41 ++- .../layout/get-dims-condition-by-node.ts | 2 +- .../src/utils/layout/whether-leaf-by-level.ts | 17 + .../build-table-hierarchy-spec.tsx.snap | 21 ++ s2-site/docs/api/basic-class/node.en.md | 3 +- s2-site/docs/api/basic-class/node.zh.md | 63 ++-- s2-site/docs/common/totals.en.md | 2 + s2-site/docs/common/totals.zh.md | 2 + s2-site/docs/manual/basic/totals.en.md | 30 +- s2-site/docs/manual/basic/totals.zh.md | 42 ++- .../totals/demo/dimension-group-col.ts | 68 ++++ .../totals/demo/dimension-group-row.ts | 67 ++++ .../examples/analysis/totals/demo/meta.json | 16 + 40 files changed, 1410 insertions(+), 387 deletions(-) create mode 100644 packages/s2-core/__tests__/unit/data-set/__snapshots__/pivot-data-set-total-spec.ts.snap create mode 100644 packages/s2-core/src/facet/README-adjustTotalNodesCoordinate.md create mode 100644 packages/s2-core/src/utils/layout/whether-leaf-by-level.ts create mode 100644 s2-site/examples/analysis/totals/demo/dimension-group-col.ts create mode 100644 s2-site/examples/analysis/totals/demo/dimension-group-row.ts diff --git a/packages/s2-core/__tests__/bugs/issue-1715-spec.ts b/packages/s2-core/__tests__/bugs/issue-1715-spec.ts index 28436e1d61..7018af631d 100644 --- a/packages/s2-core/__tests__/bugs/issue-1715-spec.ts +++ b/packages/s2-core/__tests__/bugs/issue-1715-spec.ts @@ -81,9 +81,10 @@ describe('Multi Values GrandTotal Height Test', () => { const grandTotalsNode = s2 .getColumnNodes() - .find((node) => node.isGrandTotals); + .find((node) => node.isGrandTotals && node.isTotalRoot); - expect(s2.facet.layoutResult.colsHierarchy.height).toBe(60); - expect(grandTotalsNode.height).toEqual(30); + // 有多个 Value 时不允许隐藏度量列 + expect(s2.facet.layoutResult.colsHierarchy.height).toBe(90); + expect(grandTotalsNode.height).toEqual(60); }); }); diff --git a/packages/s2-core/__tests__/spreadsheet/__snapshots__/corner-spec.ts.snap b/packages/s2-core/__tests__/spreadsheet/__snapshots__/corner-spec.ts.snap index aa859d5c67..b3dc716fbd 100644 --- a/packages/s2-core/__tests__/spreadsheet/__snapshots__/corner-spec.ts.snap +++ b/packages/s2-core/__tests__/spreadsheet/__snapshots__/corner-spec.ts.snap @@ -18,6 +18,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "province", "label": "province", @@ -47,6 +48,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "city", "label": "city", @@ -81,6 +83,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "series-number-node", "label": "序号", @@ -110,6 +113,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "province", "label": "province", @@ -139,6 +143,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "city", "label": "city", @@ -173,6 +178,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "", "label": "province/city/数值", @@ -207,6 +213,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "series-number-node", "label": "序号", @@ -236,6 +243,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "", "label": "province/city/数值", @@ -270,6 +278,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "province", "label": "province", @@ -299,6 +308,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "city", "label": "city", @@ -333,6 +343,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "series-number-node", "label": "序号", @@ -362,6 +373,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "province", "label": "province", @@ -391,6 +403,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "city", "label": "city", @@ -425,6 +438,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "", "label": "province/city", @@ -459,6 +473,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "series-number-node", "label": "序号", @@ -488,6 +503,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "", "label": "province/city", @@ -522,6 +538,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "province", "label": "province", @@ -551,6 +568,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "city", "label": "city", @@ -585,6 +603,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "series-number-node", "label": "序号", @@ -614,6 +633,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "province", "label": "province", @@ -643,6 +663,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "city", "label": "city", @@ -677,6 +698,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "", "label": "province/city", @@ -711,6 +733,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "series-number-node", "label": "序号", @@ -740,6 +763,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "", "label": "province/city", diff --git a/packages/s2-core/__tests__/spreadsheet/corner-spec.ts b/packages/s2-core/__tests__/spreadsheet/corner-spec.ts index bdc06461fd..b2d7396bb8 100644 --- a/packages/s2-core/__tests__/spreadsheet/corner-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/corner-spec.ts @@ -49,6 +49,7 @@ describe('PivotSheet Corner Tests', () => { fields: { ...simpleDataConfig.fields, columns: [], + values: ['price'], }, }); s2.setOptions({ @@ -88,6 +89,13 @@ describe('PivotSheet Corner Tests', () => { }, }, }); + s2.setDataCfg({ + ...simpleDataConfig, + fields: { + ...simpleDataConfig.fields, + values: ['price'], + }, + }); s2.render(); const cornerNodes = s2.facet.getCornerNodes(); diff --git a/packages/s2-core/__tests__/spreadsheet/sort-by-order-spec.ts b/packages/s2-core/__tests__/spreadsheet/sort-by-order-spec.ts index 468d201e4b..b45c75bc02 100644 --- a/packages/s2-core/__tests__/spreadsheet/sort-by-order-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/sort-by-order-spec.ts @@ -104,4 +104,15 @@ describe('Manual Sort Tests', () => { }), ).toEqual(['整体访问', '小程序访问', '支付宝访问']); }); + + test('getTotalDimensionValues should include correct values', () => { + const sortedType1 = s2.dataSet.getTotalDimensionValues('type1', {}); + expect(sortedType1).toEqual(['整体访问', '小程序访问', '支付宝访问']); + + expect(s2.dataSet.getTotalDimensionValues('type2', {})).toEqual([ + '整体访问', + '小程序访问', + '支付宝访问', + ]); + }); }); diff --git a/packages/s2-core/__tests__/unit/cell/header-cell-spec.ts b/packages/s2-core/__tests__/unit/cell/header-cell-spec.ts index 14b9f566ee..870c3947db 100644 --- a/packages/s2-core/__tests__/unit/cell/header-cell-spec.ts +++ b/packages/s2-core/__tests__/unit/cell/header-cell-spec.ts @@ -60,7 +60,7 @@ describe('header cell formatter test', () => { value: '总计', parent: root, label: '总计', - isTotals: true, + isTotalRoot: true, }); const rowNode = new Node({ id: `root[&]杭州[&]小计`, @@ -68,7 +68,7 @@ describe('header cell formatter test', () => { value: '小计', parent: root, label: '小计', - isTotals: true, + isTotalRoot: true, }); const formatter: Formatter = (value) => { @@ -92,6 +92,7 @@ describe('header cell formatter test', () => { label: '总计', isTotals: true, isGrandTotals: true, + isTotalRoot: true, }); const rowSubTotalNode = new Node({ diff --git a/packages/s2-core/__tests__/unit/data-set/__snapshots__/pivot-data-set-total-spec.ts.snap b/packages/s2-core/__tests__/unit/data-set/__snapshots__/pivot-data-set-total-spec.ts.snap new file mode 100644 index 0000000000..91b43b95ef --- /dev/null +++ b/packages/s2-core/__tests__/unit/data-set/__snapshots__/pivot-data-set-total-spec.ts.snap @@ -0,0 +1,121 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Pivot Dataset Total Test test for total with dimension group get correct MultiData when query need to be processed 1`] = ` +Array [ + Object { + "$$extra$$": "number", + "$$value$$": 7789, + "city": "杭州市", + "number": 7789, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "$$extra$$": "number", + "$$value$$": 2367, + "city": "绍兴市", + "number": 2367, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "$$extra$$": "number", + "$$value$$": 3877, + "city": "宁波市", + "number": 3877, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "$$extra$$": "number", + "$$value$$": 4342, + "city": "舟山市", + "number": 4342, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, +] +`; + +exports[`Pivot Dataset Total Test test for total with dimension group get correct MultiData when query need to be processed 2`] = `Array []`; + +exports[`Pivot Dataset Total Test test for total with dimension group get correct MultiData when query need to be processed 3`] = ` +Array [ + Object { + "$$extra$$": "number", + "$$value$$": 7789, + "city": "杭州市", + "number": 7789, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "$$extra$$": "number", + "$$value$$": 2367, + "city": "绍兴市", + "number": 2367, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "$$extra$$": "number", + "$$value$$": 3877, + "city": "宁波市", + "number": 3877, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "$$extra$$": "number", + "$$value$$": 4342, + "city": "舟山市", + "number": 4342, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "$$extra$$": "number", + "$$value$$": 1723, + "city": "成都市", + "number": 1723, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "$$extra$$": "number", + "$$value$$": 1822, + "city": "绵阳市", + "number": 1822, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "$$extra$$": "number", + "$$value$$": 1943, + "city": "南充市", + "number": 1943, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "$$extra$$": "number", + "$$value$$": 2330, + "city": "乐山市", + "number": 2330, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, +] +`; diff --git a/packages/s2-core/__tests__/unit/data-set/pivot-data-set-total-spec.ts b/packages/s2-core/__tests__/unit/data-set/pivot-data-set-total-spec.ts index cb791efaed..2a7ac2d86b 100644 --- a/packages/s2-core/__tests__/unit/data-set/pivot-data-set-total-spec.ts +++ b/packages/s2-core/__tests__/unit/data-set/pivot-data-set-total-spec.ts @@ -443,6 +443,62 @@ describe('Pivot Dataset Total Test', () => { dataSet = new PivotDataSet(mockSheet); dataSet.setDataCfg(dataCfg); }); + + test('should get correct total cell data when totals calculated by calcFunc and Existential dimension grouping', () => { + const totalStatus = { + isRowTotal: true, + isColTotal: true, + isRowSubTotal: true, + isColSubTotal: true, + }; + + expect( + dataSet.getCellData({ + query: { + province: '浙江省', + sub_type: '桌子', + [EXTRA_FIELD]: 'number', + }, + isTotals: true, + totalStatus, + }), + ).toContainEntries([[VALUE_FIELD, 18375]]); + + expect( + dataSet.getCellData({ + query: { + province: '浙江省', + [EXTRA_FIELD]: 'number', + }, + totalStatus, + isTotals: true, + }), + ).toContainEntries([[VALUE_FIELD, 43098]]); + + expect( + dataSet.getCellData({ + query: { + sub_type: '桌子', + [EXTRA_FIELD]: 'number', + }, + totalStatus, + isTotals: true, + }), + ).toContainEntries([[VALUE_FIELD, 26193]]); + + expect( + dataSet.getCellData({ + query: { + province: '浙江省', + type: '家具', + [EXTRA_FIELD]: 'number', + }, + isTotals: true, + totalStatus, + }), + ).toContainEntries([[VALUE_FIELD, 32418]]); + }); + test('should get correct total cell data when totals calculated by calcFunc', () => { expect( dataSet.getCellData({ @@ -675,4 +731,188 @@ describe('Pivot Dataset Total Test', () => { expect(isColSubTotal4).toBeTrue(); }); }); + + describe('test for total with dimension group', () => { + beforeEach(() => { + MockPivotSheet.mockClear(); + const mockSheet = new MockPivotSheet(); + mockSheet.store = new Store(); + mockSheet.isValueInCols = () => true; + dataSet = new PivotDataSet(mockSheet); + + dataCfg = assembleDataCfg({ + meta: [], + fields: { + rows: ['province', 'city', 'type', 'sub_type'], + columns: [], + }, + }); + dataSet.setDataCfg(dataCfg); + }); + test('should get correct total dimension values', () => { + expect( + dataSet.getTotalDimensionValues('sub_type', { + province: '浙江省', + city: undefined, + }), + ).toEqual(['桌子', '沙发', '笔', '纸张']); + + expect( + dataSet.getTotalDimensionValues('sub_type', { + province: '浙江省', + city: undefined, + }), + ).toEqual(['桌子', '沙发', '笔', '纸张']); + + expect( + dataSet.getTotalDimensionValues('sub_type', { + province: undefined, + city: undefined, + type: '办公用品', + }), + ).toEqual(['笔', '纸张']); + + expect(dataSet.getTotalDimensionValues('city', {})).toEqual([ + '杭州市', + '绍兴市', + '宁波市', + '舟山市', + '成都市', + '绵阳市', + '南充市', + '乐山市', + ]); + + expect( + dataSet.getTotalDimensionValues('sub_type', { + province: undefined, + city: undefined, + type: undefined, + }), + ).toEqual(['桌子', '沙发', '笔', '纸张']); + }); + + test('should get correct boolean of grouping scenarios where query need to be processed', () => { + expect( + dataSet.checkExistDimensionGroup({ + province: 'A', + type: 'A', + sub_type: 'A', + }), + ).toEqual(true); + expect( + dataSet.checkExistDimensionGroup({ + province: 'A', + sub_type: 'A', + }), + ).toEqual(true); + expect( + dataSet.checkExistDimensionGroup({ + province: 'A', + city: 'A', + sub_type: 'A', + }), + ).toEqual(true); + expect( + dataSet.checkExistDimensionGroup({ + city: 'A', + sub_type: 'A', + }), + ).toEqual(true); + expect( + dataSet.checkExistDimensionGroup({ + province: 'A', + city: 'A', + }), + ).toEqual(false); + expect( + dataSet.checkExistDimensionGroup({ + province: 'A', + city: 'A', + type: 'A', + }), + ).toEqual(false); + }); + test('should get correct boolean of dimensionValue is a query condition', () => { + expect( + dataSet.checkAccordQueryWithDimensionValue({ + dimensionValues: '浙江省[&]杭州市[&]家具[&]桌子', + query: { + province: '浙江省', + city: 'A', + type: 'Abc', + }, + dimensions: dataCfg.fields.rows, + field: 'province', + }), + ).toEqual(true); + expect( + dataSet.checkAccordQueryWithDimensionValue({ + dimensionValues: '浙江省[&]杭州市[&]家具[&]桌子', + query: { + province: '浙江省', + city: '杭州市', + type: '家具', + }, + dimensions: dataCfg.fields.rows, + field: 'sub_type', + }), + ).toEqual(true); + expect( + dataSet.checkAccordQueryWithDimensionValue({ + dimensionValues: '浙江省[&]杭州市[&]家具[&]桌子', + query: { + province: '浙江省', + city: '不是杭州市', + type: '家具', + }, + dimensions: dataCfg.fields.rows, + field: 'sub_type', + }), + ).toEqual(false); + expect( + dataSet.checkAccordQueryWithDimensionValue({ + dimensionValues: '浙江省[&]杭州市[&]家具[&]桌子', + query: { + province: '浙江省', + }, + dimensions: dataCfg.fields.rows, + field: 'sub_type', + }), + ).toEqual(true); + }); + test('get correct query list when query need to be processed', () => { + expect( + dataSet.getTotalGroupQueries(dataCfg.fields.rows, { + province: '浙江省', + sub_type: '桌子', + }), + ).toEqual([ + { province: '浙江省', sub_type: '桌子', type: '家具', city: '杭州市' }, + { province: '浙江省', sub_type: '桌子', type: '家具', city: '绍兴市' }, + { province: '浙江省', sub_type: '桌子', type: '家具', city: '宁波市' }, + { province: '浙江省', sub_type: '桌子', type: '家具', city: '舟山市' }, + ]); + }); + + test('get correct MultiData when query need to be processed', () => { + expect( + dataSet.getMultiData({ + province: '浙江省', + sub_type: '桌子', + }), + ).toMatchSnapshot(); + expect( + dataSet.getMultiData({ + province: '浙江省', + sub_type: '杭州市', + }), + ).toMatchSnapshot(); + expect( + dataSet.getMultiData({ + sub_type: '桌子', + }), + ).toMatchSnapshot(); + }); + }); }); diff --git a/packages/s2-core/__tests__/unit/utils/export/export-spec.ts b/packages/s2-core/__tests__/unit/utils/export/export-spec.ts index 8845f7511e..d160c70afe 100644 --- a/packages/s2-core/__tests__/unit/utils/export/export-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/export/export-spec.ts @@ -237,6 +237,77 @@ describe('PivotSheet Export Test', () => { }); }); + it('should export correct data in grid mode with grouped totals in col', () => { + const s2 = new PivotSheet( + getContainer(), + assembleDataCfg({ + fields: { + valueInCols: true, + columns: ['province', 'city', 'type', 'sub_type', 'number'], + }, + }), + assembleOptions({ + hierarchyType: 'grid', + totals: { + row: { + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['province'], + }, + col: { + totalsGroupDimensions: ['city', 'type'], + subTotalsGroupDimensions: ['sub_type'], + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['type'], + }, + }, + }), + ); + s2.render(); + const data = copyData(s2, '\t'); + const rows = data.split('\n'); + expect(rows).toHaveLength(17); + rows.forEach((e) => { + expect(e.split('\t')).toHaveLength(60); + }); + }); + + it('should export correct data in grid mode with grouped totals in row', () => { + const s2 = new PivotSheet( + getContainer(), + assembleDataCfg({ + fields: { + valueInCols: false, + columns: ['province', 'city', 'type', 'sub_type', 'number'], + }, + }), + assembleOptions({ + hierarchyType: 'grid', + totals: { + row: { + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['province'], + }, + col: { + totalsGroupDimensions: ['city', 'sub_type', 'province'], + subTotalsGroupDimensions: ['sub_type'], + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['type'], + }, + }, + }), + ); + s2.render(); + const data = copyData(s2, '\t'); + const rows = data.split('\n'); + expect(rows).toHaveLength(16); + rows.forEach((e) => { + expect(e.split('\t')).toHaveLength(63); + }); + }); it('should export correct data in grid mode with totals in row', () => { const s2 = new PivotSheet( getContainer(), diff --git a/packages/s2-core/__tests__/unit/utils/facet-spec.ts b/packages/s2-core/__tests__/unit/utils/facet-spec.ts index 6dc2ca2561..5c9d1aa44a 100644 --- a/packages/s2-core/__tests__/unit/utils/facet-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/facet-spec.ts @@ -1,29 +1,10 @@ import { - getSubTotalNodeWidthOrHeightByLevel, getIndexRangeWithOffsets, getAdjustedRowScrollX, getAdjustedScrollOffset, } from '@/utils/facet'; describe('Facet util test', () => { - test('should get correct width of subTotal node', () => { - const sampleNodesForAllLevels = [ - { - id: 'root[&]测试', - value: '测试', - isSubTotals: true, - width: 20, - level: 0, - }, - ]; - expect( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - getSubTotalNodeWidthOrHeightByLevel(sampleNodesForAllLevels, -1, 'width'), - ).toEqual(20); - expect(getSubTotalNodeWidthOrHeightByLevel([], -1, 'width')).toEqual(0); - }); - test('should get correct index range for given offsets', () => { const offsets = [0, 30, 60, 90, 120, 150, 160, 170, 190]; expect(getIndexRangeWithOffsets(offsets, 0, 31)).toStrictEqual({ diff --git a/packages/s2-core/src/cell/header-cell.ts b/packages/s2-core/src/cell/header-cell.ts index f60de70711..0bfebe0b5f 100644 --- a/packages/s2-core/src/cell/header-cell.ts +++ b/packages/s2-core/src/cell/header-cell.ts @@ -85,7 +85,7 @@ export abstract class HeaderCell extends BaseCell { } protected getFormattedFieldValue(): FormatResult { - const { label, isTotals, isGrandTotals } = this.meta; + const { label, isTotalRoot, isGrandTotals } = this.meta; const formatter = this.spreadsheet.dataSet.getFieldFormatter( this.meta.field, @@ -99,16 +99,15 @@ export abstract class HeaderCell extends BaseCell { if (this.spreadsheet.isTableMode()) { shouldFormat = false; } else if (this.spreadsheet.isHierarchyTreeType()) { - shouldFormat = !isGrandTotals; + shouldFormat = !(isGrandTotals && isTotalRoot); } else { - shouldFormat = !isTotals; + shouldFormat = !isTotalRoot; } const formattedValue = shouldFormat && formatter ? formatter(label, undefined, this.meta) : label; - return { formattedValue, value: label, diff --git a/packages/s2-core/src/common/interface/basic.ts b/packages/s2-core/src/common/interface/basic.ts index 62d5c0a57f..cc63caae50 100644 --- a/packages/s2-core/src/common/interface/basic.ts +++ b/packages/s2-core/src/common/interface/basic.ts @@ -151,6 +151,10 @@ export interface Total { label?: string; // sub label's display name, default = '小计' subLabel?: string; + /** 总计分组维度 */ + totalsGroupDimensions?: string[]; + /** 小计分组维度 */ + subTotalsGroupDimensions?: string[]; } /** diff --git a/packages/s2-core/src/data-set/base-data-set.ts b/packages/s2-core/src/data-set/base-data-set.ts index 49d0c36072..1c95f5e736 100644 --- a/packages/s2-core/src/data-set/base-data-set.ts +++ b/packages/s2-core/src/data-set/base-data-set.ts @@ -165,6 +165,28 @@ export abstract class BaseDataSet { */ public abstract getDimensionValues(field: string, query?: DataType): string[]; + /** + * province city type + * 辽宁省 + * 达州市 A + * B + * 芜湖市 C + * 浙江省 + * 杭州市 B + * D + * 宁波市 E + * query = {province: "浙江省"} + * field = 'type' + * * => [B,D,E] + * + * @param field current dimensions + * @param query dimension value query + */ + public abstract getTotalDimensionValues( + field: string, + query?: DataType, + ): string[]; + /** * In most cases, this function to get the specific * cross data cell data diff --git a/packages/s2-core/src/data-set/interface.ts b/packages/s2-core/src/data-set/interface.ts index c1570113d3..672766c8ef 100644 --- a/packages/s2-core/src/data-set/interface.ts +++ b/packages/s2-core/src/data-set/interface.ts @@ -47,6 +47,24 @@ export interface CellDataParams { rowNode?: Node; // mark row's cell isRow?: boolean; + // use with isTotals + totalStatus?: TotalStatus; +} + +export interface CheckAccordQueryParams { + // item of sortedDimensionValues,es: "浙江省[&]杭州市[&]家具[&]桌子" + dimensionValues: string; + query: DataType; + // rows or columns + dimensions: string[]; + field: string; +} + +export interface TotalStatus { + isRowTotal: boolean; + isRowSubTotal: boolean; + isColTotal: boolean; + isColSubTotal: boolean; } export interface SortActionParams { diff --git a/packages/s2-core/src/data-set/pivot-data-set.ts b/packages/s2-core/src/data-set/pivot-data-set.ts index af71751c04..545d9eebf8 100644 --- a/packages/s2-core/src/data-set/pivot-data-set.ts +++ b/packages/s2-core/src/data-set/pivot-data-set.ts @@ -1,5 +1,6 @@ import { compact, + concat, difference, each, every, @@ -14,6 +15,7 @@ import { isNumber, isUndefined, keys, + some, uniq, unset, values, @@ -61,9 +63,11 @@ import type { CellMeta } from '../common'; import { BaseDataSet } from './base-data-set'; import type { CellDataParams, + CheckAccordQueryParams, DataType, PivotMeta, SortedDimensionValues, + TotalStatus, } from './interface'; export class PivotDataSet extends BaseDataSet { @@ -200,6 +204,7 @@ export class PivotDataSet extends BaseDataSet { } // 2. 删除 rowPivotMeta 当前下钻层级对应 meta 信息 deleteMetaById(this.rowPivotMeta, rowNodeId); + // 3. 删除下钻缓存路径 idPathMap.delete(rowNodeId); @@ -317,6 +322,36 @@ export class PivotDataSet extends BaseDataSet { }; } + public getDimensionsByField(field: string): string[] { + const { rows = [], columns = [] } = this.fields || {}; + if (includes(rows, field)) { + return rows; + } + if (includes(columns, field)) { + return columns as string[]; + } + return []; + } + + // rows :['province','city','type'] + // query: ['浙江省',undefined] => return: ['文具','家具'] + public getTotalDimensionValues(field: string, query?: DataType): string[] { + const dimensions = this.getDimensionsByField(field); + const allCurrentFieldDimensionValues = ( + this.sortedDimensionValues[field] || [] + ).filter((dimValue) => + this.checkAccordQueryWithDimensionValue({ + dimensionValues: dimValue, + query, + dimensions, + field, + }), + ); + return filterUndefined( + uniq(getDimensionsWithoutPathPre([...allCurrentFieldDimensionValues])), + ); + } + public getDimensionValues(field: string, query?: DataType): string[] { const { rows = [], columns = [] } = this.fields || {}; let meta: PivotMeta = new Map(); @@ -328,7 +363,6 @@ export class PivotDataSet extends BaseDataSet { meta = this.colPivotMeta; dimensions = columns as string[]; } - if (!isEmpty(query)) { let sortedMeta = []; const dimensionValuePath = []; @@ -367,10 +401,12 @@ export class PivotDataSet extends BaseDataSet { return filterUndefined([...meta.keys()]); } - getTotalValue(query: DataType) { + getTotalValue(query: DataType, totalStatus?: TotalStatus) { + const effectiveStatus = some(totalStatus); + const status = effectiveStatus ? totalStatus : this.getTotalStatus(query); const { aggregation, calcFunc } = getAggregationAndCalcFuncByQuery( - this.getTotalStatus(query), + status, this.spreadsheet.options?.totals, ) || {}; const calcAction = calcActionByType[aggregation]; @@ -379,7 +415,6 @@ export class PivotDataSet extends BaseDataSet { if (calcAction || calcFunc) { const data = this.getMultiData(query); let totalValue: number; - if (calcFunc) { totalValue = calcFunc(query, data); } else if (calcAction) { @@ -395,7 +430,7 @@ export class PivotDataSet extends BaseDataSet { } public getCellData(params: CellDataParams): DataType { - const { query, rowNode, isTotals = false } = params || {}; + const { query, rowNode, isTotals = false, totalStatus } = params || {}; const { columns, rows: originRows } = this.fields; let rows = originRows; @@ -427,8 +462,7 @@ export class PivotDataSet extends BaseDataSet { // 如果已经有数据则取已有数据 return data; } - - return isTotals ? this.getTotalValue(query) : data; + return isTotals ? this.getTotalValue(query, totalStatus) : data; } getCustomData = (path: number[]) => { @@ -482,6 +516,147 @@ export class PivotDataSet extends BaseDataSet { }; }; + /** + * 检查是否属于需要填充中间汇总维度的场景 + * [undefined , '杭州市' , undefined , 'number'] => true + * ['浙江省' , '杭州市' , undefined , 'number'] => true + */ + checkExistDimensionGroup(query: DataType): boolean { + const { rows, columns } = this.fields; + const check = (dimensions: string[]) => { + let existDimensionValue = false; + for (let i = dimensions.length; i > 0; i--) { + const key = dimensions[i - 1]; + if (keys(query).includes(key)) { + if (key !== EXTRA_FIELD) { + existDimensionValue = true; + } + } else if (existDimensionValue) { + return true; + } + } + return false; + }; + return check(rows) || check(columns as string[]); + } + + /** + * 检查 DimensionValue 是否符合 query 条件 + * dimensions = ['province','city'] + * query = [province: '杭州市', type: '文具'] + * field = 'sub_type' + * DimensionValue: 浙江省[&]杭州市[&]家具[&]桌子 => true + * DimensionValue: 四川省[&]成都市[&]文具[&]笔 => false + */ + checkAccordQueryWithDimensionValue(params: CheckAccordQueryParams): boolean { + const { dimensionValues, query, dimensions, field } = params; + for (const [index, dimension] of dimensions.entries()) { + const queryValue = get(query, dimension); + if (queryValue) { + const arrTypeValue = dimensionValues.split(ID_SEPARATOR); + const dimensionValue = arrTypeValue[index]; + if (dimensionValue !== queryValue) { + return false; + } + } + if (field === dimension) { + break; + } + } + return true; + } + + /** + * 补足分组汇总场景的前置 undefined + * {undefined,'可乐','undefined','price'} + * => [ + * {'可口公司','可乐','undefined','price'}, + * {'百事公司','可乐','undefined','price'}, + * ] + */ + getTotalGroupQueries(dimensions: string[], originQuery: DataType) { + let queries = [originQuery]; + let existDimensionGroupKey = null; + for (let i = dimensions.length; i > 0; i--) { + const key = dimensions[i - 1]; + if (keys(originQuery).includes(key)) { + if (key !== EXTRA_FIELD) { + existDimensionGroupKey = key; + } + } else if (existDimensionGroupKey) { + const allCurrentFieldDimensionValues = + this.sortedDimensionValues[existDimensionGroupKey]; + let res = []; + const arrayLength = + allCurrentFieldDimensionValues[0].split(ID_SEPARATOR).length; + for (const query of queries) { + const resKeys = []; + for (const dimValue of allCurrentFieldDimensionValues) { + if ( + this.checkAccordQueryWithDimensionValue({ + dimensionValues: dimValue, + query, + dimensions, + field: existDimensionGroupKey, + }) + ) { + const arrTypeValue = dimValue.split(ID_SEPARATOR); + const currentKey = arrTypeValue[arrayLength - 2]; + if (currentKey !== 'undefined') { + resKeys.push(currentKey); + } + } + } + const queryList = uniq(resKeys).map((v) => { + return { ...query, [key]: v }; + }); + res = concat(res, queryList); + } + queries = res; + existDimensionGroupKey = key; + } + } + return queries; + } + + // 有中间维度汇总的分组场景,将有中间 undefined 值的 query 处理为一组合法 query 后查询数据再合并 + private getGroupTotalMultiData( + totalRows: string[], + originQuery: DataType, + ): DataType[] { + const { rows, columns } = this.fields; + let result = []; + const rowTotalGroupQueries = this.getTotalGroupQueries( + totalRows, + originQuery, + ); + let totalGroupQueries = []; + for (const query of rowTotalGroupQueries) { + totalGroupQueries = concat( + totalGroupQueries, + this.getTotalGroupQueries(columns as string[], query), + ); + } + + for (const query of totalGroupQueries) { + const rowDimensionValues = getQueryDimValues(totalRows, query); + const colDimensionValues = getQueryDimValues(columns as string[], query); + const path = getDataPath({ + rowDimensionValues, + colDimensionValues, + careUndefined: true, + isFirstCreate: true, + rowFields: rows, + colFields: columns as string[], + rowPivotMeta: this.rowPivotMeta, + colPivotMeta: this.colPivotMeta, + }); + const currentData = this.getCustomData(path); + result = concat(result, compact(customFlatten(currentData))); + } + return result; + } + public getMultiData( query: DataType, isTotals?: boolean, @@ -495,70 +670,77 @@ export class PivotDataSet extends BaseDataSet { const totalRows = !isEmpty(drillDownFields) ? rows.concat(drillDownFields) : rows; - const rowDimensionValues = getQueryDimValues(totalRows, query); - const colDimensionValues = getQueryDimValues(columns as string[], query); - const path = getDataPath({ - rowDimensionValues, - colDimensionValues, - careUndefined: true, - isFirstCreate: true, - rowFields: rows, - colFields: columns as string[], - rowPivotMeta: this.rowPivotMeta, - colPivotMeta: this.colPivotMeta, - }); - const currentData = this.getCustomData(path); - let result = compact(customFlatten(currentData)); - if (isTotals) { - // 总计/小计(行/列) - // need filter extra data - // grand total => {$$extra$$: 'price'} - // sub total => {$$extra$$: 'price', category: 'xxxx'} - // [undefined, undefined, "price"] => [category] - let fieldKeys = []; - const rowKeys = getFieldKeysByDimensionValues(rowDimensionValues, rows); - const colKeys = getFieldKeysByDimensionValues( + // existDimensionGroup:当 undefined 维度后面有非 undefined,为维度分组场景,将非 undefined 维度前的维度填充为所有可能的维度值。 + // 如 [undefined , '杭州市' , undefined , 'number'] + const existDimensionGroup = this.checkExistDimensionGroup(query); + let result = []; + if (existDimensionGroup) { + result = this.getGroupTotalMultiData(totalRows, query); + } else { + const rowDimensionValues = getQueryDimValues(totalRows, query); + const colDimensionValues = getQueryDimValues(columns as string[], query); + const path = getDataPath({ + rowDimensionValues, colDimensionValues, - columns as string[], - ); - if (isRow) { - // 行总计 - fieldKeys = rowKeys; - } else { - // 只有一个值,此时为列总计 - const isCol = keys(query)?.length === 1 && has(query, EXTRA_FIELD); - - if (isCol) { - fieldKeys = colKeys; + careUndefined: true, + isFirstCreate: true, + rowFields: rows, + colFields: columns as string[], + rowPivotMeta: this.rowPivotMeta, + colPivotMeta: this.colPivotMeta, + }); + const currentData = this.getCustomData(path); + result = compact(customFlatten(currentData)); + if (isTotals) { + // 总计/小计(行/列) + // need filter extra data + // grand total => {$$extra$$: 'price'} + // sub total => {$$extra$$: 'price', category: 'xxxx'} + // [undefined, undefined, "price"] => [category] + let fieldKeys = []; + const rowKeys = getFieldKeysByDimensionValues(rowDimensionValues, rows); + const colKeys = getFieldKeysByDimensionValues( + colDimensionValues, + columns as string[], + ); + if (isRow) { + // 行总计 + fieldKeys = rowKeys; } else { - const getTotalStatus = (dimensions: string[]) => { - return isEveryUndefined( - dimensions?.filter((item) => !valueList?.includes(item)), - ); - }; - const isRowTotal = getTotalStatus(colDimensionValues); - const isColTotal = getTotalStatus(rowDimensionValues); - - if (isRowTotal) { - // 行小计 - fieldKeys = rowKeys; - } else if (isColTotal) { - // 列小计 + // 只有一个值,此时为列总计 + const isCol = keys(query)?.length === 1 && has(query, EXTRA_FIELD); + + if (isCol) { fieldKeys = colKeys; } else { - // 行小计+列 or 列小计+行 - fieldKeys = [...rowKeys, ...colKeys]; + const getTotalStatus = (dimensions: string[]) => { + return isEveryUndefined( + dimensions?.filter((item) => !valueList?.includes(item)), + ); + }; + const isRowTotal = getTotalStatus(colDimensionValues); + const isColTotal = getTotalStatus(rowDimensionValues); + + if (isRowTotal) { + // 行小计 + fieldKeys = rowKeys; + } else if (isColTotal) { + // 列小计 + fieldKeys = colKeys; + } else { + // 行小计+列 or 列小计+行 + fieldKeys = [...rowKeys, ...colKeys]; + } } } + result = result.filter( + (r) => + !fieldKeys?.find( + (item) => item !== EXTRA_FIELD && keys(r)?.includes(item), + ), + ); } - result = result.filter( - (r) => - !fieldKeys?.find( - (item) => item !== EXTRA_FIELD && keys(r)?.includes(item), - ), - ); } - return result || []; } diff --git a/packages/s2-core/src/data-set/table-data-set.ts b/packages/s2-core/src/data-set/table-data-set.ts index 0d9c8e457f..619d04456a 100644 --- a/packages/s2-core/src/data-set/table-data-set.ts +++ b/packages/s2-core/src/data-set/table-data-set.ts @@ -150,6 +150,10 @@ export class TableDataSet extends BaseDataSet { }); }; + public getTotalDimensionValues(field: string, query?: DataType): string[] { + return []; + } + public getDimensionValues(field: string, query?: DataType): string[] { return []; } diff --git a/packages/s2-core/src/facet/README-adjustTotalNodesCoordinate.md b/packages/s2-core/src/facet/README-adjustTotalNodesCoordinate.md new file mode 100644 index 0000000000..3998b5fcf6 --- /dev/null +++ b/packages/s2-core/src/facet/README-adjustTotalNodesCoordinate.md @@ -0,0 +1,57 @@ +## 小计总计节点布局逻辑 + +1. 计算一个 multipleMap ,表示每个 level 的汇总节点实际占据一个单元格 +2. 遍历所有汇总单元格,根据 multipleMap 中的值,计算宽或高为合并多个单元格 + +🌰 例子: + +rows: [ 省份, 城市, 类别, 子类别 ] + +Total - row - totalDimensionGroup: [ 城市, 类别 ] + +Total - row - subTotalDimensionGroup: [ 子类别 ] + +row + +### multipleMap 如何计算 - getMultipleMap 函数 + +行总计 multipleMap: + +1. 初始化一个长度与 rows 相同的数组 [ 1, 1, 1 ,1 ] +2. 从后往前判断 totalDimensionGroup.includes(field) + - 第一个处理子类别,不做处理 + - 第二个处理 类别,判断为 false,将类别数值向前移动[ 1, 2, 0, 1] + - 第三个判断 城市,判断为 false,将城市数值向前移动[ 3, 0, 0, 1] + - 第四个 省份,首个维度不做遍历 +3. multipleMap 结果为 [ 3, 0, 0, 1] + +行小计 multipleMap: + +1. 初始化一个长度与 rows 相同的数组 [ 1, 1, 1 ,1 ] +2. 从后往前判断 subTotalDimensionGroup.includes(field) + - 第一个处理子类别,判断为 true,不做处理 + - 第二个处理 类别,判断为 false,将类别数值向前移动[ 1, 1, 2, 0] + - 第三个判断 城市,判断为 flase,不做处理 + - 第四个 省份,首个维度不做遍历 +3. multipleMap 结果为 [ 1, 1, 2, 0] + +### multipleMap 如何使用 - adjustTotalNodesCoordinate 函数 + +总计的 multipleMap = [ 1 , 1 ,2 ,0 ] + +multipleMap 表示: + +- row 的第一第二个维度占据一个单元格 +- 第三个维度合并两个单元格 +- 第四个维度没有节点 + +小计的 multipleMap = [ 3,0,0,1 ] + +multipleMap 表示: + +- 小计结点,row的第一个维度合并三个单元格 +- 小计结点的最后一个维度占据一个单元格 + +当小计节点出现在第二个维度,则合并量为‘最近的上一个不为0的值’ - level 差 + +即实际该小计节点实际的 multipleMap 为 [1,2,0,1] diff --git a/packages/s2-core/src/facet/layout/build-gird-hierarchy.ts b/packages/s2-core/src/facet/layout/build-gird-hierarchy.ts index bec4de2b0d..eb8de392ec 100644 --- a/packages/s2-core/src/facet/layout/build-gird-hierarchy.ts +++ b/packages/s2-core/src/facet/layout/build-gird-hierarchy.ts @@ -1,36 +1,17 @@ import { isEmpty, isUndefined } from 'lodash'; import { EXTRA_FIELD } from '../../common/constant'; -import type { SpreadSheetFacetCfg } from '../../common/interface'; import { addTotals } from '../../utils/layout/add-totals'; import { generateHeaderNodes } from '../../utils/layout/generate-header-nodes'; import { getDimsCondition } from '../../utils/layout/get-dims-condition-by-node'; import type { FieldValue, GridHeaderParams } from '../layout/interface'; import { layoutArrange } from '../layout/layout-hooks'; import { TotalMeasure } from '../layout/total-measure'; +import { whetherLeafByLevel } from '../../utils/layout/whether-leaf-by-level'; +import { TotalClass } from './total-class'; -const hideMeasureColumn = ( - fieldValues: FieldValue[], - field: string, - cfg: SpreadSheetFacetCfg, -) => { - const hideMeasure = cfg.colCfg?.hideMeasureColumn ?? false; - const valueInCol = cfg.dataSet.fields.valueInCols; - for (const value of fieldValues) { - if (hideMeasure && valueInCol && field === EXTRA_FIELD) { - fieldValues.splice(fieldValues.indexOf(value), 1); - } - } -}; - -/** - * Build grid hierarchy in rows or columns - * - * @param params - */ -export const buildGridHierarchy = (params: GridHeaderParams) => { +const buildTotalGridHierarchy = (params: GridHeaderParams) => { const { addTotalMeasureInTotal, - addMeasureInTotalQuery, parentNode, currentField, fields, @@ -42,64 +23,118 @@ export const buildGridHierarchy = (params: GridHeaderParams) => { const { dataSet, values, spreadsheet } = facetCfg; const fieldValues: FieldValue[] = []; + const fieldName = dataSet.getFieldName(currentField); let query = {}; - if (parentNode.isTotals) { - // add total measures - if (addTotalMeasureInTotal) { - query = getDimsCondition(parentNode.parent, true); - // add total measures - fieldValues.push(...values.map((v) => new TotalMeasure(v))); + const totalsConfig = spreadsheet.getTotalsConfig(currentField); + const dimensionGroup = parentNode.isGrandTotals + ? totalsConfig.totalsGroupDimensions + : totalsConfig.subTotalsGroupDimensions; + if (dimensionGroup?.includes(currentField)) { + query = getDimsCondition(parentNode); + const dimValues = dataSet.getTotalDimensionValues(currentField, query); + fieldValues.push( + ...(dimValues || []).map( + (value) => + new TotalClass({ + label: value, + isSubTotals: parentNode.isSubTotals, + isGrandTotals: parentNode.isGrandTotals, + isTotalRoot: false, + }), + ), + ); + if (isEmpty(fieldValues)) { + fieldValues.push(fieldName); } + } else if (addTotalMeasureInTotal && currentField === EXTRA_FIELD) { + // add total measures + query = getDimsCondition(parentNode); + fieldValues.push(...values.map((v) => new TotalMeasure(v))); + } else if (whetherLeafByLevel({ facetCfg, level: index, fields })) { + // 如果最后一级没有分组维度,则将上一个结点设为叶子结点 + parentNode.isLeaf = true; + hierarchy.pushIndexNode(parentNode); + parentNode.rowIndex = hierarchy.getIndexNodes().length - 1; + return; } else { - // field(dimension)'s all values - query = getDimsCondition(parentNode, true); + // 如果是空维度,则跳转到下一级 level + buildTotalGridHierarchy({ ...params, currentField: fields[index + 1] }); + return; + } - const dimValues = dataSet.getDimensionValues(currentField, query); + const displayFieldValues = fieldValues.filter((value) => !isUndefined(value)); + generateHeaderNodes({ + ...params, + fieldValues: displayFieldValues, + level: index, + parentNode, + query, + }); +}; - const arrangedValues = layoutArrange( - dimValues, - facetCfg, - parentNode, - currentField, - ); - fieldValues.push(...(arrangedValues || [])); +const buildNormalGridHierarchy = (params: GridHeaderParams) => { + const { parentNode, currentField, fields, facetCfg } = params; - // add skeleton for empty data + const index = fields.indexOf(currentField); - const fieldName = dataSet.getFieldName(currentField); + const { dataSet, spreadsheet } = facetCfg; + const fieldValues: FieldValue[] = []; + const fieldName = dataSet.getFieldName(currentField); - if (isEmpty(fieldValues)) { - if (currentField === EXTRA_FIELD) { - fieldValues.push(...dataSet.fields?.values); - } else { - fieldValues.push(fieldName); - } + let query = {}; + + // field(dimension)'s all values + query = getDimsCondition(parentNode, true); + + const dimValues = dataSet.getDimensionValues(currentField, query); + + const arrangedValues = layoutArrange( + dimValues, + facetCfg, + parentNode, + currentField, + ); + fieldValues.push(...(arrangedValues || [])); + + // add skeleton for empty data + + if (isEmpty(fieldValues)) { + if (currentField === EXTRA_FIELD) { + fieldValues.push(...dataSet.fields?.values); + } else { + fieldValues.push(fieldName); } - // hide measure in columns - hideMeasureColumn(fieldValues, currentField, facetCfg); - // add totals if needed - addTotals({ - currentField, - lastField: fields[index - 1], - isFirstField: index === 0, - fieldValues, - spreadsheet, - }); } - const displayFieldValues = fieldValues.filter((value) => !isUndefined(value)); + // add totals if needed + addTotals({ + currentField, + lastField: fields[index - 1], + isFirstField: index === 0, + fieldValues, + spreadsheet, + }); + const displayFieldValues = fieldValues.filter((value) => !isUndefined(value)); generateHeaderNodes({ - currentField, - fields, + ...params, fieldValues: displayFieldValues, - facetCfg, - hierarchy, - parentNode, level: index, + parentNode, query, - addMeasureInTotalQuery, - addTotalMeasureInTotal, }); }; + +/** + * Build grid hierarchy in rows or columns + * + * @param params + */ +export const buildGridHierarchy = (params: GridHeaderParams) => { + if (params.parentNode.isTotals) { + buildTotalGridHierarchy(params); + } else { + buildNormalGridHierarchy(params); + } +}; diff --git a/packages/s2-core/src/facet/layout/build-row-tree-hierarchy.ts b/packages/s2-core/src/facet/layout/build-row-tree-hierarchy.ts index 05afda5ffe..28697f9c11 100644 --- a/packages/s2-core/src/facet/layout/build-row-tree-hierarchy.ts +++ b/packages/s2-core/src/facet/layout/build-row-tree-hierarchy.ts @@ -20,7 +20,13 @@ const addTotals = ( // TODO valueInCol = false and one or more values if (totalsConfig.showGrandTotals) { const func = totalsConfig.reverseLayout ? 'unshift' : 'push'; - fieldValues[func](new TotalClass(totalsConfig.label, false, true)); + fieldValues[func]( + new TotalClass({ + label: totalsConfig.label, + isSubTotals: false, + isGrandTotals: true, + }), + ); } }; diff --git a/packages/s2-core/src/facet/layout/interface.ts b/packages/s2-core/src/facet/layout/interface.ts index ab051f243d..3e15a02f5d 100644 --- a/packages/s2-core/src/facet/layout/interface.ts +++ b/packages/s2-core/src/facet/layout/interface.ts @@ -42,15 +42,8 @@ export interface TotalParams { spreadsheet: SpreadSheet; } -export interface HeaderNodesParams { - currentField: string; - fields: string[]; +export interface HeaderNodesParams extends GridHeaderParams { fieldValues: FieldValue[]; - addTotalMeasureInTotal: boolean; - addMeasureInTotalQuery: boolean; - facetCfg: SpreadSheetFacetCfg; - hierarchy: Hierarchy; - parentNode: Node; level: number; query: Record; } @@ -93,3 +86,9 @@ export interface CustomTreeHeaderParams { hierarchy: Hierarchy; customTreeItems: CustomTreeItem[]; } + +export interface WhetherLeafParams { + facetCfg: SpreadSheetFacetCfg; + fields: string[]; + level: number; +} diff --git a/packages/s2-core/src/facet/layout/node.ts b/packages/s2-core/src/facet/layout/node.ts index 195bf0051a..cfc09d5466 100644 --- a/packages/s2-core/src/facet/layout/node.ts +++ b/packages/s2-core/src/facet/layout/node.ts @@ -21,6 +21,7 @@ export interface BaseNodeConfig { isSubTotals?: boolean; isCollapsed?: boolean; isGrandTotals?: boolean; + isTotalRoot?: boolean; hierarchy?: Hierarchy; isPivotMode?: boolean; seriesNumberWidth?: number; @@ -62,6 +63,7 @@ export class Node { isGrandTotals, isSubTotals, isCollapsed, + isTotalRoot, hierarchy, isPivotMode, seriesNumberWidth, @@ -95,6 +97,7 @@ export class Node { this.isLeaf = isLeaf; this.isGrandTotals = isGrandTotals; this.isSubTotals = isSubTotals; + this.isTotalRoot = isTotalRoot; this.extra = extra; } @@ -301,6 +304,8 @@ export class Node { public isSubTotals?: boolean; + public isTotalRoot?: boolean; + public hiddenChildNodeInfo?: HiddenColumnsInfo | null; public extra?: Record; diff --git a/packages/s2-core/src/facet/layout/total-class.ts b/packages/s2-core/src/facet/layout/total-class.ts index 51df2d35c1..36f47c995d 100644 --- a/packages/s2-core/src/facet/layout/total-class.ts +++ b/packages/s2-core/src/facet/layout/total-class.ts @@ -1,6 +1,16 @@ /** * Class to mark '小计' & '总计' */ + +export interface TotalClassConfig { + label: string; + // 是否属于小计汇总格 + isSubTotals: boolean; + // 是否属于总计汇总格 + isGrandTotals: boolean; + // 是否是”小计“、”总计“单元格本身 + isTotalRoot?: boolean; +} export class TotalClass { public label: string; @@ -8,13 +18,14 @@ export class TotalClass { public isGrandTotals: boolean; - public constructor( - label: string, - isSubTotals = false, - isGrandTotals = false, - ) { + // 是否为 小计/总计 根结点,即 value = “小计”,单元格,此类结点不参与 query + public isTotalRoot: boolean; + + public constructor(params: TotalClassConfig) { + const { label, isSubTotals, isGrandTotals, isTotalRoot } = params; this.label = label; this.isSubTotals = isSubTotals; this.isGrandTotals = isGrandTotals; + this.isTotalRoot = isTotalRoot; } } diff --git a/packages/s2-core/src/facet/pivot-facet.ts b/packages/s2-core/src/facet/pivot-facet.ts index 559a2a43a8..3b432f44b5 100644 --- a/packages/s2-core/src/facet/pivot-facet.ts +++ b/packages/s2-core/src/facet/pivot-facet.ts @@ -1,5 +1,5 @@ import { - find, + filter, forEach, get, isArray, @@ -26,11 +26,9 @@ import { DebuggerUtil } from '../common/debug'; import type { LayoutResult, ViewMeta } from '../common/interface'; import { getDataCellId, handleDataItem } from '../utils/cell/data-cell'; import { getActionIconConfig } from '../utils/cell/header-cell'; -import { - getIndexRangeWithOffsets, - getSubTotalNodeWidthOrHeightByLevel, -} from '../utils/facet'; +import { getIndexRangeWithOffsets } from '../utils/facet'; import { getCellWidth, safeJsonParse } from '../utils/text'; +import { getHeaderTotalStatus } from '../utils/dataset/pivot-data-set'; import { BaseFacet } from './base-facet'; import { buildHeaderHierarchy } from './layout/build-header-hierarchy'; import type { Hierarchy } from './layout/hierarchy'; @@ -97,10 +95,12 @@ export class PivotFacet extends BaseFacet { } : {}; const dataQuery = merge({}, rowQuery, colQuery, measureInfo); + const totalStatus = getHeaderTotalStatus(row, col); const data = dataSet.getCellData({ query: dataQuery, rowNode: row, isTotals, + totalStatus, }); let valueField: string; let fieldValue = null; @@ -182,9 +182,17 @@ export class PivotFacet extends BaseFacet { ) { let preLeafNode = Node.blankNode(); const allNodes = colsHierarchy.getNodes(); - for (const levelSample of colsHierarchy.sampleNodesForAllLevels) { + const sampleNodesForAllLevels = colsHierarchy.sampleNodesForAllLevels; + for (let level = 0; level < sampleNodesForAllLevels.length; level++) { + const levelSample = sampleNodesForAllLevels[level]; levelSample.height = this.getColNodeHeight(levelSample); colsHierarchy.height += levelSample.height; + if (levelSample.level === 0) { + levelSample.y = 0; + } else { + const preLevelSample = sampleNodesForAllLevels[level - 1]; + levelSample.y = preLevelSample?.y + preLevelSample?.height ?? 0; + } } let currentCollIndex = 0; for (let i = 0; i < allNodes.length; i++) { @@ -211,17 +219,21 @@ export class PivotFacet extends BaseFacet { ); currentNode.y = preLevelSample?.y + preLevelSample?.height ?? 0; } - // 数值置于行头时, 列头的总计即叶子节点, 此时应该用列高: https://github.com/antvis/S2/issues/1715 - currentNode.height = - currentNode.isGrandTotals && currentNode.isLeaf - ? colsHierarchy.height - : this.getColNodeHeight(currentNode); + currentNode.height = this.getColNodeHeight(currentNode); layoutCoordinate(this.cfg, null, currentNode); } this.autoCalculateColNodeWidthAndX(colLeafNodes); if (!isEmpty(this.spreadsheet.options.totals?.col)) { - this.adjustTotalNodesCoordinate(colsHierarchy); - this.adjustSubTotalNodesCoordinate(colsHierarchy); + this.adjustTotalNodesCoordinate({ + hierarchy: colsHierarchy, + isRowHeader: false, + isSubTotal: true, + }); + this.adjustTotalNodesCoordinate({ + hierarchy: colsHierarchy, + isRowHeader: false, + isSubTotal: false, + }); } } @@ -311,6 +323,7 @@ export class PivotFacet extends BaseFacet { col.isTotalMeasure || rowNode.isTotals || rowNode.isTotalMeasure, + totalStatus: getHeaderTotalStatus(rowNode, col), }); if (cellData) { @@ -487,8 +500,16 @@ export class PivotFacet extends BaseFacet { if (!isTree) { this.autoCalculateRowNodeHeightAndY(rowLeafNodes); if (!isEmpty(spreadsheet.options.totals?.row)) { - this.adjustTotalNodesCoordinate(rowsHierarchy, true); - this.adjustSubTotalNodesCoordinate(rowsHierarchy, true); + this.adjustTotalNodesCoordinate({ + hierarchy: rowsHierarchy, + isRowHeader: true, + isSubTotal: false, + }); + this.adjustTotalNodesCoordinate({ + hierarchy: rowsHierarchy, + isRowHeader: true, + isSubTotal: true, + }); } } } @@ -517,105 +538,64 @@ export class PivotFacet extends BaseFacet { } } - /** - * @description adjust the coordinate of total nodes and their children - * @param hierarchy Hierarchy - * @param isRowHeader boolean - */ - private adjustTotalNodesCoordinate( + // please read README-adjustTotalNodesCoordinate.md to understand this function + private getMultipleMap( hierarchy: Hierarchy, isRowHeader?: boolean, + isSubTotal?: boolean, ) { - const moreThanOneValue = this.cfg.dataSet.moreThanOneValue(); const { maxLevel } = hierarchy; - const grandTotalNode = find( - hierarchy.getNodes(0), - (node: Node) => node.isGrandTotals, - ); - if (!(grandTotalNode instanceof Node)) { - return; - } - const grandTotalChildren = grandTotalNode.children; - // 总计节点层级 (有且有两级) - if (isRowHeader) { - // 填充行总单元格宽度 - grandTotalNode.width = hierarchy.width; - // 调整其叶子节点位置和宽度 - forEach(grandTotalChildren, (node: Node) => { - const maxLevelNode = hierarchy.getNodes(maxLevel)[0]; - node.x = maxLevelNode.x; - node.width = maxLevelNode.width; - }); - } else if (maxLevel > 1 || (maxLevel <= 1 && !moreThanOneValue)) { - // 只有当列头总层级大于1级或列头为1级单指标时总计格高度才需要填充 - // 填充列总计单元格高度 - const grandTotalChildrenHeight = grandTotalChildren?.[0]?.height ?? 0; - grandTotalNode.height = hierarchy.height - grandTotalChildrenHeight; - // 调整其叶子结点位置, 以非小计行为准 - const positionY = find( - hierarchy.getNodes(maxLevel), - (node: Node) => !node.isTotalMeasure, - )?.y; - forEach(grandTotalChildren, (node: Node) => { - node.y = positionY; - }); + const { totals, dataSet } = this.cfg; + const moreThanOneValue = dataSet.moreThanOneValue(); + const { rows, columns } = dataSet.fields; + const fields = isRowHeader ? rows : columns; + const totalConfig = isRowHeader ? totals.row : totals.col; + const dimensionGroup = isSubTotal + ? totalConfig.subTotalsGroupDimensions || [] + : totalConfig.totalsGroupDimensions || []; + const multipleMap: number[] = Array.from({ length: maxLevel + 1 }, () => 1); + for (let level = maxLevel; level > 0; level--) { + const currentField = fields[level] as string; + // 若不符合【分组维度包含此维度】或【者指标维度下非单指标维度】,此表头单元格为空,将宽高合并到上级单元格 + const existValueField = currentField === EXTRA_FIELD && moreThanOneValue; + if (!(dimensionGroup.includes(currentField) || existValueField)) { + multipleMap[level - 1] += multipleMap[level]; + multipleMap[level] = 0; + } } + return multipleMap; } - /** - * @description adust the coordinate of subTotal nodes when there is just one value - * @param hierarchy Hierarchy - * @param isRowHeader boolean - */ - private adjustSubTotalNodesCoordinate( - hierarchy: Hierarchy, - isRowHeader?: boolean, - ) { - const subTotalNodes = hierarchy - .getNodes() - .filter((node: Node) => node.isSubTotals); - - if (isEmpty(subTotalNodes)) { - return; - } - const { maxLevel } = hierarchy; - forEach(subTotalNodes, (subTotalNode: Node) => { - const subTotalNodeChildren = subTotalNode.children; - if (isRowHeader) { - // 填充行总单元格宽度 - subTotalNode.width = getSubTotalNodeWidthOrHeightByLevel( - hierarchy.sampleNodesForAllLevels, - subTotalNode.level, - 'width', - ); - - // 调整其叶子结点位置 - forEach(subTotalNodeChildren, (node: Node) => { - node.x = hierarchy.getNodes(maxLevel)[0].x; - }); - } else { - // 填充列总单元格高度 - const totalHeight = getSubTotalNodeWidthOrHeightByLevel( - hierarchy.sampleNodesForAllLevels, - subTotalNode.level, - 'height', - ); - const subTotalNodeChildrenHeight = - subTotalNodeChildren?.[0]?.height ?? 0; - subTotalNode.height = totalHeight - subTotalNodeChildrenHeight; - // 调整其叶子结点位置,以非小计单元格为准 - forEach(subTotalNodeChildren, (node: Node) => { - node.y = hierarchy.getNodes(maxLevel)[0].y; - }); - // 调整其叶子结点位置, 以非小计行为准 - const positionY = find( - hierarchy.getNodes(maxLevel), - (node: Node) => !node.isTotalMeasure, - )?.y; - forEach(subTotalNodeChildren, (node: Node) => { - node.y = positionY; - }); + // please read README-adjustTotalNodesCoordinate.md to understand this function + private adjustTotalNodesCoordinate(params: { + hierarchy: Hierarchy; + isRowHeader?: boolean; + isSubTotal?: boolean; + }) { + const { hierarchy, isRowHeader, isSubTotal } = params; + const multipleMap = this.getMultipleMap(hierarchy, isRowHeader, isSubTotal); + const totalNodes = filter(hierarchy.getNodes(), (node: Node) => + isSubTotal ? node.isSubTotals : node.isGrandTotals, + ); + const key = isRowHeader ? 'width' : 'height'; + forEach(totalNodes, (node: Node) => { + let multiple = multipleMap[node.level]; + // 小计根节点若为 0,则改为最近上级倍数 - level 差 + if (!multiple && isSubTotal) { + let lowerLevelIndex = 1; + while (multiple < 1) { + multiple = + multipleMap[node.level - lowerLevelIndex] - lowerLevelIndex; + lowerLevelIndex++; + } + } + let res = 0; + for (let i = 0; i < multiple; i++) { + res += hierarchy.sampleNodesForAllLevels.find( + (sampleNode) => sampleNode.level === node.level + i, + )[key]; } + node[key] = res; }); } @@ -699,6 +679,7 @@ export class PivotFacet extends BaseFacet { col.isTotalMeasure || rowNode.isTotals || rowNode.isTotalMeasure, + totalStatus: getHeaderTotalStatus(rowNode, col), }); const cellDataKeys = keys(cellData); diff --git a/packages/s2-core/src/sheet-type/spread-sheet.ts b/packages/s2-core/src/sheet-type/spread-sheet.ts index 6fae391b5b..f3d48faad9 100644 --- a/packages/s2-core/src/sheet-type/spread-sheet.ts +++ b/packages/s2-core/src/sheet-type/spread-sheet.ts @@ -616,12 +616,12 @@ export abstract class SpreadSheet extends EE { ? totalConfig.showSubTotals : false; return { + label: i18n('总计'), + subLabel: i18n('小计'), + totalsGroupDimensions: [], + subTotalsGroupDimensions: [], + ...totalConfig, showSubTotals, - showGrandTotals: totalConfig.showGrandTotals, - reverseLayout: totalConfig.reverseLayout, - reverseSubLayout: totalConfig.reverseSubLayout, - label: totalConfig.label || i18n('总计'), - subLabel: totalConfig.subLabel || i18n('小计'), }; } diff --git a/packages/s2-core/src/utils/dataset/pivot-data-set.ts b/packages/s2-core/src/utils/dataset/pivot-data-set.ts index 85b3f7570f..02bcc254f3 100644 --- a/packages/s2-core/src/utils/dataset/pivot-data-set.ts +++ b/packages/s2-core/src/utils/dataset/pivot-data-set.ts @@ -8,15 +8,16 @@ import { reduce, set, } from 'lodash'; -import { i18n } from '../../common/i18n'; import { EXTRA_FIELD, ID_SEPARATOR, ROOT_ID } from '../../common/constant'; import type { DataPathParams, DataType, PivotMeta, SortedDimensionValues, + TotalStatus, } from '../../data-set/interface'; import type { Meta } from '../../common/interface/basic'; +import type { Node } from '../../facet/layout/node'; interface Param { rows: string[]; @@ -197,9 +198,7 @@ export function getDataPath(params: DataPathParams) { rowPivotMeta, colPivotMeta, ); - const result = rowPath.concat(...colPath); - - return result; + return rowPath.concat(...colPath); } /** @@ -344,3 +343,12 @@ export function generateExtraFieldMeta( return extraFieldMeta; } + +export function getHeaderTotalStatus(row: Node, col: Node): TotalStatus { + return { + isRowTotal: row.isGrandTotals, + isRowSubTotal: row.isSubTotals, + isColTotal: col.isGrandTotals, + isColSubTotal: col.isSubTotals, + }; +} diff --git a/packages/s2-core/src/utils/export/copy.ts b/packages/s2-core/src/utils/export/copy.ts index 0c691d353d..b2a11af36a 100644 --- a/packages/s2-core/src/utils/export/copy.ts +++ b/packages/s2-core/src/utils/export/copy.ts @@ -10,21 +10,20 @@ import { max, orderBy, reduce, - repeat, zip, } from 'lodash'; import type { ColCell, RowCell } from '../../cell'; import { + type CellMeta, CellTypes, CopyType, EMPTY_PLACEHOLDER, EXTRA_FIELD, ID_SEPARATOR, InteractionStateName, + type RowData, SERIES_NUMBER_FIELD, VALUE_FIELD, - type CellMeta, - type RowData, } from '../../common'; import type { DataType } from '../../data-set/interface'; import type { Node } from '../../facet/layout/node'; @@ -32,6 +31,7 @@ import type { SpreadSheet } from '../../sheet-type'; import { copyToClipboard } from '../../utils/export'; import { flattenDeep } from '../data-set-operate'; import { getEmptyPlaceholder } from '../text'; +import { getHeaderTotalStatus } from '../dataset/pivot-data-set'; export function keyEqualTo(key: string, compareKey: string) { if (!key || !compareKey) { @@ -115,6 +115,7 @@ const getValueFromMeta = ( rowNode.isTotalMeasure || colNode.isTotals || colNode.isTotalMeasure, + totalStatus: getHeaderTotalStatus(rowNode, colNode), }); return cell?.[VALUE_FIELD] ?? ''; } @@ -394,6 +395,7 @@ const getDataMatrix = ( rowNode.isTotalMeasure || colNode.isTotals || colNode.isTotalMeasure, + totalStatus: getHeaderTotalStatus(rowNode, colNode), }); return getFormat( colNode.colIndex, @@ -658,6 +660,27 @@ function getLastLevelCells( }); } +/** 处理有合并单元格的复制(小记总计格) + * 维度1 | 维度2 | 维度3 + * 总计 | 维度三 + * => 总计 总计 维度三 + */ +function getTotalCellMatrixId(meta: Node, maxLevel: number) { + let nextNode = meta; + let lastNode = { level: maxLevel }; + let cellId = nextNode.label; + while (nextNode.level >= 0) { + let repeatNumber = lastNode.level - nextNode.level; + while (repeatNumber > 0) { + cellId = `${nextNode.label}${ID_SEPARATOR}${cellId}`; + repeatNumber--; + } + lastNode = nextNode; + nextNode = nextNode.parent; + } + return cellId; +} + function getCellMatrix( lastLevelCells: Array, maxLevel: number, @@ -665,22 +688,19 @@ function getCellMatrix( ) { return map(lastLevelCells, (cell: RowCell | ColCell) => { const meta = cell.getMeta(); - const { id, label, isTotals, level } = meta; + const { id, label, isTotals } = meta; let cellId = id; - // 为总计小计补齐高度 - if (isTotals && level !== maxLevel) { - cellId = id + ID_SEPARATOR + repeat(label, maxLevel - level); + if (isTotals) { + cellId = getTotalCellMatrixId(meta, maxLevel); } // 将指标维度单元格的标签替换为实际文本 const actualText = cell.getActualText(); const headerList = getHeaderList(cellId, allLevel.size); - const formattedHeaderList = map(headerList, (header) => + return map(headerList, (header) => isEqual(header, label) ? actualText : header, ); - - return formattedHeaderList; }); } diff --git a/packages/s2-core/src/utils/facet.ts b/packages/s2-core/src/utils/facet.ts index 56377cdcf6..74db5444fd 100644 --- a/packages/s2-core/src/utils/facet.ts +++ b/packages/s2-core/src/utils/facet.ts @@ -1,16 +1,4 @@ import { findIndex } from 'lodash'; -import type { Node } from '../facet/layout/node'; - -export const getSubTotalNodeWidthOrHeightByLevel = ( - sampleNodesForAllLevels: Node[], - level: number, - key: 'width' | 'height', -) => { - return sampleNodesForAllLevels - .filter((node: Node) => node.level >= level) - .map((value) => value[key]) - .reduce((sum, current) => sum + current, 0); -}; /** * 根据视窗高度计算需要展示的数据数组下标 diff --git a/packages/s2-core/src/utils/layout/add-totals.ts b/packages/s2-core/src/utils/layout/add-totals.ts index c267da8c33..9b29207ca1 100644 --- a/packages/s2-core/src/utils/layout/add-totals.ts +++ b/packages/s2-core/src/utils/layout/add-totals.ts @@ -15,7 +15,12 @@ export const addTotals = (params: TotalParams) => { // check to see if grand total is added if (totalsConfig?.showGrandTotals) { action = totalsConfig.reverseLayout ? 'unshift' : 'push'; - totalValue = new TotalClass(totalsConfig.label, false, true); + totalValue = new TotalClass({ + label: totalsConfig.label, + isSubTotals: false, + isGrandTotals: true, + isTotalRoot: true, + }); } } else if ( /** @@ -29,7 +34,12 @@ export const addTotals = (params: TotalParams) => { currentField !== EXTRA_FIELD ) { action = totalsConfig.reverseSubLayout ? 'unshift' : 'push'; - totalValue = new TotalClass(totalsConfig.subLabel, true); + totalValue = new TotalClass({ + label: totalsConfig.subLabel, + isSubTotals: true, + isGrandTotals: false, + isTotalRoot: true, + }); } fieldValues[action]?.(totalValue); diff --git a/packages/s2-core/src/utils/layout/generate-header-nodes.ts b/packages/s2-core/src/utils/layout/generate-header-nodes.ts index 9a1db34d0f..402eb59120 100644 --- a/packages/s2-core/src/utils/layout/generate-header-nodes.ts +++ b/packages/s2-core/src/utils/layout/generate-header-nodes.ts @@ -1,4 +1,4 @@ -import { includes, isBoolean } from 'lodash'; +import { isBoolean } from 'lodash'; import { EXTRA_FIELD, SERIES_NUMBER_FIELD } from '../../common/constant'; import { i18n } from '../../common/i18n'; import { buildGridHierarchy } from '../../facet/layout/build-gird-hierarchy'; @@ -12,6 +12,7 @@ import { TotalClass } from '../../facet/layout/total-class'; import { TotalMeasure } from '../../facet/layout/total-measure'; import { generateId } from '../../utils/layout/generate-id'; import type { Columns } from '../../common'; +import { whetherLeafByLevel } from './whether-leaf-by-level'; export const generateHeaderNodes = (params: HeaderNodesParams) => { const { @@ -26,7 +27,7 @@ export const generateHeaderNodes = (params: HeaderNodesParams) => { addMeasureInTotalQuery, addTotalMeasureInTotal, } = params; - const { spreadsheet, collapsedCols, colCfg } = facetCfg; + const { spreadsheet, collapsedCols } = facetCfg; for (const [index, fieldValue] of fieldValues.entries()) { const isTotals = fieldValue instanceof TotalClass; @@ -36,32 +37,33 @@ export const generateHeaderNodes = (params: HeaderNodesParams) => { let isLeaf = false; let isGrandTotals = false; let isSubTotals = false; + let isTotalRoot = false; let adjustedField = currentField; if (isTotals) { const totalClass = fieldValue as TotalClass; isGrandTotals = totalClass.isGrandTotals; isSubTotals = totalClass.isSubTotals; + isTotalRoot = totalClass.isTotalRoot; value = i18n((fieldValue as TotalClass).label); - if (addMeasureInTotalQuery) { - // root[&]四川[&]总计 => {province: '四川', EXTRA_FIELD: 'price'} - nodeQuery = { - ...query, - [EXTRA_FIELD]: spreadsheet?.dataSet?.fields.values[0], - }; - isLeaf = true; + if (isTotalRoot) { + nodeQuery = query; } else { // root[&]四川[&]总计 => {province: '四川'} - nodeQuery = query; - if (!addTotalMeasureInTotal) { - isLeaf = true; - } + nodeQuery = { ...query, [currentField]: value }; } + if (addMeasureInTotalQuery) { + // root[&]四川[&]总计 => {province: '四川', EXTRA_FIELD: 'price'} + nodeQuery[EXTRA_FIELD] = spreadsheet?.dataSet?.fields.values[0]; + } + isLeaf = whetherLeafByLevel({ facetCfg, level, fields }); } else if (isTotalMeasure) { value = i18n((fieldValue as TotalMeasure).label); // root[&]四川[&]总计[&]price => {province: '四川',EXTRA_FIELD: 'price' } nodeQuery = { ...query, [EXTRA_FIELD]: value }; adjustedField = EXTRA_FIELD; - isLeaf = true; + isGrandTotals = parentNode.isGrandTotals; + isSubTotals = parentNode.isSubTotals; + isLeaf = whetherLeafByLevel({ facetCfg, level, fields }); } else if (spreadsheet.isTableMode()) { value = fieldValue; adjustedField = fields[index]; @@ -71,13 +73,7 @@ export const generateHeaderNodes = (params: HeaderNodesParams) => { value = fieldValue; // root[&]四川[&]成都 => {province: '四川', city: '成都' } nodeQuery = { ...query, [currentField]: value }; - const isValueInCols = spreadsheet.dataCfg.fields?.valueInCols ?? true; - const isHideMeasure = - colCfg?.hideMeasureColumn && - isValueInCols && - includes(fields, EXTRA_FIELD); - const extraSize = isHideMeasure ? 2 : 1; - isLeaf = level === fields.length - extraSize; + isLeaf = whetherLeafByLevel({ facetCfg, level, fields }); } const uniqueId = generateId(parentNode.id, value); if (!uniqueId) { @@ -95,11 +91,12 @@ export const generateHeaderNodes = (params: HeaderNodesParams) => { level, field: adjustedField, parent: parentNode, - isTotals, + isTotals: isTotals || isTotalMeasure, isGrandTotals, isSubTotals, isTotalMeasure, isCollapsed, + isTotalRoot, hierarchy, query: nodeQuery, spreadsheet, diff --git a/packages/s2-core/src/utils/layout/get-dims-condition-by-node.ts b/packages/s2-core/src/utils/layout/get-dims-condition-by-node.ts index 35befc837a..3bc44b3b31 100644 --- a/packages/s2-core/src/utils/layout/get-dims-condition-by-node.ts +++ b/packages/s2-core/src/utils/layout/get-dims-condition-by-node.ts @@ -8,7 +8,7 @@ export function getDimsCondition(parent: Node, force?: boolean) { * 当为表格布局时,小计行的内容是“小计”不需要作为筛选条件 * 当为树状布局时,force可以强行指定小计行,即父类目作为筛选条件 */ - if (!p.isTotals || force) { + if (!p.isTotalRoot || force) { cond[p.key] = p.value; } p = p.parent; diff --git a/packages/s2-core/src/utils/layout/whether-leaf-by-level.ts b/packages/s2-core/src/utils/layout/whether-leaf-by-level.ts new file mode 100644 index 0000000000..c03632b673 --- /dev/null +++ b/packages/s2-core/src/utils/layout/whether-leaf-by-level.ts @@ -0,0 +1,17 @@ +import { includes } from 'lodash'; +import type { WhetherLeafParams } from '../../facet/layout/interface'; +import { EXTRA_FIELD } from '../../common'; + +export const whetherLeafByLevel = (params: WhetherLeafParams) => { + const { facetCfg, level, fields } = params; + const { colCfg, spreadsheet, dataSet } = facetCfg; + const moreThanOneValue = dataSet.moreThanOneValue(); + const isValueInCols = spreadsheet.dataCfg.fields?.valueInCols ?? true; + const isHideMeasure = + colCfg?.hideMeasureColumn && + isValueInCols && + !moreThanOneValue && + includes(fields, EXTRA_FIELD); + const extraSize = isHideMeasure ? 2 : 1; + return level === fields.length - extraSize; +}; diff --git a/packages/s2-react/__tests__/unit/utils/__snapshots__/build-table-hierarchy-spec.tsx.snap b/packages/s2-react/__tests__/unit/utils/__snapshots__/build-table-hierarchy-spec.tsx.snap index fad5bec42a..36ce2b1f24 100644 --- a/packages/s2-react/__tests__/unit/utils/__snapshots__/build-table-hierarchy-spec.tsx.snap +++ b/packages/s2-react/__tests__/unit/utils/__snapshots__/build-table-hierarchy-spec.tsx.snap @@ -19,6 +19,7 @@ Object { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "$$series_number$$", "label": "序号", @@ -49,6 +50,7 @@ Object { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "province", "label": "省份", @@ -79,6 +81,7 @@ Object { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "city", "label": "城市", @@ -109,6 +112,7 @@ Object { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "type", "label": "类别", @@ -139,6 +143,7 @@ Object { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "sub_type", "label": "子类别", @@ -169,6 +174,7 @@ Object { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "number", "label": "数量", @@ -197,6 +203,7 @@ Object { "isPivotMode": undefined, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "key": "", "label": "", @@ -230,6 +237,7 @@ Hierarchy { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "$$series_number$$", "label": "序号", @@ -260,6 +268,7 @@ Hierarchy { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "province", "label": "省份", @@ -290,6 +299,7 @@ Hierarchy { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "city", "label": "城市", @@ -320,6 +330,7 @@ Hierarchy { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "type", "label": "类别", @@ -350,6 +361,7 @@ Hierarchy { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "sub_type", "label": "子类别", @@ -380,6 +392,7 @@ Hierarchy { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "number", "label": "数量", @@ -413,6 +426,7 @@ Hierarchy { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "$$series_number$$", "label": "序号", @@ -443,6 +457,7 @@ Hierarchy { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "province", "label": "省份", @@ -473,6 +488,7 @@ Hierarchy { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "city", "label": "城市", @@ -503,6 +519,7 @@ Hierarchy { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "type", "label": "类别", @@ -533,6 +550,7 @@ Hierarchy { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "sub_type", "label": "子类别", @@ -563,6 +581,7 @@ Hierarchy { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "number", "label": "数量", @@ -595,6 +614,7 @@ Hierarchy { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "$$series_number$$", "label": "序号", @@ -626,6 +646,7 @@ Hierarchy { "isPivotMode": undefined, "isSubTotals": false, "isTotalMeasure": false, + "isTotalRoot": false, "isTotals": false, "key": "$$series_number$$", "label": "序号", diff --git a/s2-site/docs/api/basic-class/node.en.md b/s2-site/docs/api/basic-class/node.en.md index 374df5e90a..701f3ed5ea 100644 --- a/s2-site/docs/api/basic-class/node.en.md +++ b/s2-site/docs/api/basic-class/node.en.md @@ -5,7 +5,7 @@ order: 5 Function description: layout node. [details](https://github.com/antvis/S2/blob/master/packages/s2-core/src/facet/layout/node.ts) | parameter | illustrate | type | -| ----------------- | -------------------------------------- | --------------------------------------------------- | +|-------------------|----------------------------------------| --------------------------------------------------- | | id | node id | `string` | | key | node key | `string` | | value | node value | `string` | @@ -17,6 +17,7 @@ Function description: layout node. [details](https://github.com/antvis/S2/blob/m | isTotals | Is it summary | `boolean` | | isSubTotals | Is it a subtotal | `boolean` | | isGrandTotals | Is it total | `boolean` | +| isTotalRoot | Is it total root | `boolean` | | isCollapsed | Whether to expand | `boolean` | | hierarchy | hierarchical structure | [Hierarchy](#) | | isPivotMode | Is it a pivot table | `boolean` | diff --git a/s2-site/docs/api/basic-class/node.zh.md b/s2-site/docs/api/basic-class/node.zh.md index 31b90c3520..e1bc3044c8 100644 --- a/s2-site/docs/api/basic-class/node.zh.md +++ b/s2-site/docs/api/basic-class/node.zh.md @@ -9,34 +9,35 @@ order: 5 node.isTotals // false ``` -| 参数 | 说明 | 类型 | -| --- | --- | --- | -| id | 节点 id | `string` | -| key | 节点 key | `string` | -| value | 节点值 | `string` | -| label | 节点标题 | `string` | -| level | 节点等级 | `number` | -| rowIndex | 行头索引 | `number` | -| colIndex | 列头索引 | `number` | -| parent | 父节点 | [Node](/docs/api/basic-class/node) | -| isTotals | 是否是汇总 | `boolean` | -| isSubTotals | 是否是小计 | `boolean` | -| isGrandTotals | 是否是总计 | `boolean` | -| isCollapsed | 是否展开 | `boolean` | -| hierarchy | 层级结构 | [Hierarchy](#) | -| isPivotMode | 是否是透视表 | `boolean` | -| seriesNumberWidth | 序号宽度 | `number` | -| field | dataCfg 对应的 field | `string` | -| spreadsheet | 表格实例 | [SpreadSheet](/docs/api/basic-class/spreadsheet) | -| query | 当前节点对应的数据 | `Record` | -| belongsCell | 对应的单元格 | [S2CellType](/docs/api/basic-class/base-cell) | -| isTotalMeasure | 是否是数值小计 | `boolean` | -| inCollapseNode | 是否展开的节点 | `boolean` | -| isLeaf | 是否是叶子节点 | `boolean` | -| x | x 轴坐标 | `number` | -| y | y 轴坐标 | `number` | -| width | 宽度 | `number` | -| height | 高度 | `number` | -| padding | 间距 | `number` | -| hiddenChildNodeInfo | 隐藏的子节点信息 | [hiddenColumnsInfo](/api/basic-class/store#hiddencolumnsinfo) | -| children | 子节点 | [Node[]](/docs/api/basic-class/node) | +| 参数 | 说明 | 类型 | +|---------------------|-------------------| --- | +| id | 节点 id | `string` | +| key | 节点 key | `string` | +| value | 节点值 | `string` | +| label | 节点标题 | `string` | +| level | 节点等级 | `number` | +| rowIndex | 行头索引 | `number` | +| colIndex | 列头索引 | `number` | +| parent | 父节点 | [Node](/docs/api/basic-class/node) | +| isTotals | 是否是汇总 | `boolean` | +| isSubTotals | 是否是小计 | `boolean` | +| isGrandTotals | 是否是总计 | `boolean` | +| isCollapsed | 是否展开 | `boolean` | +| isTotalRoot | 是否是汇总结点的根节点 | `boolean` | +| hierarchy | 层级结构 | [Hierarchy](#) | +| isPivotMode | 是否是透视表 | `boolean` | +| seriesNumberWidth | 序号宽度 | `number` | +| field | dataCfg 对应的 field | `string` | +| spreadsheet | 表格实例 | [SpreadSheet](/docs/api/basic-class/spreadsheet) | +| query | 当前节点对应的数据 | `Record` | +| belongsCell | 对应的单元格 | [S2CellType](/docs/api/basic-class/base-cell) | +| isTotalMeasure | 是否是数值小计 | `boolean` | +| inCollapseNode | 是否展开的节点 | `boolean` | +| isLeaf | 是否是叶子节点 | `boolean` | +| x | x 轴坐标 | `number` | +| y | y 轴坐标 | `number` | +| width | 宽度 | `number` | +| height | 高度 | `number` | +| padding | 间距 | `number` | +| hiddenChildNodeInfo | 隐藏的子节点信息 | [hiddenColumnsInfo](/api/basic-class/store#hiddencolumnsinfo) | +| children | 子节点 | [Node[]](/docs/api/basic-class/node) | diff --git a/s2-site/docs/common/totals.en.md b/s2-site/docs/common/totals.en.md index e04c4f7bc0..d9b8df6b75 100644 --- a/s2-site/docs/common/totals.en.md +++ b/s2-site/docs/common/totals.en.md @@ -27,6 +27,8 @@ object is **required** , *default: null* Function description: subtotal total co | subLabel | subtotal alias | `string` | | | | calcTotals | Custom Calculated Totals | [CalcTotals](#calctotals) | | | | calcSubTotals | Custom Calculated Subtotals | [CalcTotals](#calctotals) | | | +| totalsGroupDimensions | grouping dimension of the total |`string[]` | | | +| subTotalsGroupDimensions | grouping dimension of the subtotal | `string[]` | | | ## CalcTotals diff --git a/s2-site/docs/common/totals.zh.md b/s2-site/docs/common/totals.zh.md index 6c27917856..6edacd116a 100644 --- a/s2-site/docs/common/totals.zh.md +++ b/s2-site/docs/common/totals.zh.md @@ -27,6 +27,8 @@ object **必选**,_default:null_ 功能描述: 小计总计配置 | subLabel | 小计别名 | `string` | | | | calcTotals | 自定义计算总计 | [CalcTotals](#calctotals) | | | | calcSubTotals | 自定义计算小计 | [CalcTotals](#calctotals) | | | +| totalsGroupDimensions | 总计的分组维度 |`string[]` | | | +| subTotalsGroupDimensions | 小计的分组维度 | `string[]` | | | ## CalcTotals diff --git a/s2-site/docs/manual/basic/totals.en.md b/s2-site/docs/manual/basic/totals.en.md index 37848725ab..805319532b 100644 --- a/s2-site/docs/manual/basic/totals.en.md +++ b/s2-site/docs/manual/basic/totals.en.md @@ -12,19 +12,21 @@ order: 5 object is **required** , *default: null* Function description: Subtotal calculation configuration -| parameter | illustrate | type | Defaults | required | | -| ------------------- |---------------------------| ------------ | --------------------- | -------- | - | -| showGrandTotals | Whether to display the total | `boolean` | false | ✓ | | -| showSubTotals | Whether to display subtotals. When configured as an object, always controls whether to always display -subtotals when there are less than 2 subdimensions, and does not display by default. | `boolean \| { always: boolean }` -| false | ✓ | | -| subTotalsDimensions | Summary Dimensions for Subtotals | `string[]` | [] | ✓ | | -| reverseLayout | total layout position, default bottom or right | `boolean` | false | ✓ | | -| reverseSubLayout | Subtotal layout position, default bottom or right | `boolean` | false | ✓ | | -| label | total alias | `string` | | | | -| subLabel | subtotal alias | `string` | | | | -| calcTotals | calculate the total | `CalcTotals` | | | | -| calcSubTotals | calculate subtotal | `CalcTotals` | | | | +| parameter | illustrate | type | Defaults | required | | +|--------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|--------------|----------|----------|-----| +| showGrandTotals | Whether to display the total | `boolean` | false | ✓ | | +| showSubTotals | Whether to display subtotals. When configured as an object, always controls whether to always display | | | | | +| subtotals when there are less than 2 subdimensions, and does not display by default. | `boolean \| { always: boolean }` | | | | | +| false | ✓ | | | | | +| subTotalsDimensions | Summary Dimensions for Subtotals | `string[]` | [] | ✓ | | +| reverseLayout | total layout position, default bottom or right | `boolean` | false | ✓ | | +| reverseSubLayout | Subtotal layout position, default bottom or right | `boolean` | false | ✓ | | +| label | total alias | `string` | | | | +| subLabel | subtotal alias | `string` | | | | +| calcTotals | calculate the total | `CalcTotals` | | | | +| calcSubTotals | calculate subtotal | `CalcTotals` | | | | +| totalsGroupDimensions | grouping dimension of the total |`string[]` | | | +| subTotalsGroupDimensions | grouping dimension of the subtotal | `string[]` | | | ```typescript const s2Options = { @@ -35,6 +37,8 @@ subtotals when there are less than 2 subdimensions, and does not display by defa reverseLayout: true, reverseSubLayout: true, subTotalsDimensions: [ 'province' ], + totalsGroupDimensions: ['city'], + subTotalsGroupDimensions: ['type', 'sub_type'], }, col: { showGrandTotals: true, diff --git a/s2-site/docs/manual/basic/totals.zh.md b/s2-site/docs/manual/basic/totals.zh.md index 0821574acb..cffaf101cb 100644 --- a/s2-site/docs/manual/basic/totals.zh.md +++ b/s2-site/docs/manual/basic/totals.zh.md @@ -47,6 +47,22 @@ order: 5 row +### 分组汇总 + +按维度进行 小计/总计 的汇总计算,用于进行某一维度的数据对比分析等。 + + + +#### 行总计小计分组 + +行总计按 “类别” 分组,行小计按 “类别”,“子类别” 分组: + +row + +#### 列总计小计分组 + +col + ## 使用 ### 1. 显示配置 @@ -66,17 +82,19 @@ object **必选**,_default:null_ 功能描述: 小计总计配置 object **必选**,_default:null_ 功能描述: 小计总计算配置 -| 参数 | 说明 | 类型 | 默认值 | 必选 | -| ------------------- | ------------------------ | ------------ | ------ | ---- | -| showGrandTotals | 是否显示总计 | `boolean` | false | ✓ | -| showSubTotals | 是否显示小计。当配置为对象时,always 控制是否在子维度不足 2 个时始终展示小计,默认不展示。 | `boolean | { always: boolean }` | false | ✓ | -| subTotalsDimensions | 小计的汇总维度 | `string[]` | [] | ✓ | -| reverseLayout | 总计布局位置,默认下或右 | `boolean` | false | ✓ | -| reverseSubLayout | 小计布局位置,默认下或右 | `boolean` | false | ✓ | -| label | 总计别名 | `string` | | | -| subLabel | 小计别名 | `string` | | | -| calcTotals | 计算总计 | `CalcTotals` | | | -| calcSubTotals | 计算小计 | `CalcTotals` | | | +| 参数 | 说明 | 类型 | 默认值 | 必选 | +|----------------------------------------|----------------------------------------------------|--------------|--------------------| ---- | +| showGrandTotals | 是否显示总计 | `boolean` | false | ✓ | +| showSubTotals | 是否显示小计。当配置为对象时,always 控制是否在子维度不足 2 个时始终展示小计,默认不展示。 | `boolean | { always: boolean }` | false | ✓ | +| subTotalsDimensions | 小计的汇总维度 | `string[]` | [] | ✓ | +| reverseLayout | 总计布局位置,默认下或右 | `boolean` | false | ✓ | +| reverseSubLayout | 小计布局位置,默认下或右 | `boolean` | false | ✓ | +| label | 总计别名 | `string` | | | +| subLabel | 小计别名 | `string` | | | +| calcTotals | 计算总计 | `CalcTotals` | | | +| calcSubTotals | 计算小计 | `CalcTotals` | | | +| totalsGroupDimensions | 总计的分组维度 |`string[]` | | | +| subTotalsGroupDimensions | 小计的分组维度 | `string[]` | | | ```ts const s2Options = { @@ -87,6 +105,8 @@ const s2Options = { reverseLayout: true, reverseSubLayout: true, subTotalsDimensions: ['province'], + totalsGroupDimensions: ['city'], + subTotalsGroupDimensions: ['type', 'sub_type'], }, col: { showGrandTotals: true, diff --git a/s2-site/examples/analysis/totals/demo/dimension-group-col.ts b/s2-site/examples/analysis/totals/demo/dimension-group-col.ts new file mode 100644 index 0000000000..89bd78473a --- /dev/null +++ b/s2-site/examples/analysis/totals/demo/dimension-group-col.ts @@ -0,0 +1,68 @@ +import { PivotSheet } from '@antv/s2'; + +fetch('https://gw.alipayobjects.com/os/bmw-prod/6eede6eb-8021-4da8-bb12-67891a5705b7.json') + .then((res) => res.json()) + .then((data) => { + const container = document.getElementById('container'); + const s2DataConfig = { + fields: { + rows: [], + columns: ['province', 'city', 'type'], + values: ['price' ,'cost'], + valueInCols: false, + }, + meta: [ + { + field: 'province', + name: '省份', + }, + { + field: 'city', + name: '城市', + }, + { + field: 'type', + name: '商品类别', + }, + { + field: 'price', + name: '价格', + }, + { + field: 'cost', + name: '成本', + }, + ], + data, + }; + + const s2Options = { + width: 600, + height: 480, + // 配置行小计总计显示,且按维度分组(列小计总计同理) + totals: { + col: { + showGrandTotals: true, + showSubTotals: true, + reverseLayout: true, + reverseSubLayout: true, + subTotalsDimensions: ['province'], + calcTotals: { + // 设置总计汇总计算方式为求和 + aggregation: 'SUM', + }, + calcSubTotals: { + // 设置小计汇总计算方式为求和 + aggregation: 'SUM', + }, + // 总计分组下,city 城市维度会出现分组 + totalsGroupDimensions: ['city'], + // 小计维度下,type 类别维度下会出现分组 + subTotalsGroupDimensions: ['type'], + }, + }, + }; + const s2 = new PivotSheet(container, s2DataConfig, s2Options); + + s2.render(); + }); diff --git a/s2-site/examples/analysis/totals/demo/dimension-group-row.ts b/s2-site/examples/analysis/totals/demo/dimension-group-row.ts new file mode 100644 index 0000000000..e12518827e --- /dev/null +++ b/s2-site/examples/analysis/totals/demo/dimension-group-row.ts @@ -0,0 +1,67 @@ +import { PivotSheet } from '@antv/s2'; + +fetch('https://gw.alipayobjects.com/os/bmw-prod/6eede6eb-8021-4da8-bb12-67891a5705b7.json') + .then((res) => res.json()) + .then((data) => { + const container = document.getElementById('container'); + const s2DataConfig = { + fields: { + rows: ['province', 'city', 'type'], + columns: [], + values: ['price' ,'cost'], + }, + meta: [ + { + field: 'province', + name: '省份', + }, + { + field: 'city', + name: '城市', + }, + { + field: 'type', + name: '商品类别', + }, + { + field: 'price', + name: '价格', + }, + { + field: 'cost', + name: '成本', + }, + ], + data, + }; + + const s2Options = { + width: 600, + height: 480, + // 配置行小计总计显示,且按维度分组(列小计总计同理) + totals: { + row: { + showGrandTotals: true, + showSubTotals: true, + reverseLayout: true, + reverseSubLayout: true, + subTotalsDimensions: ['province'], + calcTotals: { + // 设置总计汇总计算方式为求和 + aggregation: 'SUM', + }, + calcSubTotals: { + // 设置小计汇总计算方式为求和 + aggregation: 'SUM', + }, + // 总计分组下,city 城市维度会出现分组 + totalsGroupDimensions: ['city'], + // 小计维度下,type 类别维度下会出现分组 + subTotalsGroupDimensions: ['type'], + }, + }, + }; + const s2 = new PivotSheet(container, s2DataConfig, s2Options); + + s2.render(); + }); diff --git a/s2-site/examples/analysis/totals/demo/meta.json b/s2-site/examples/analysis/totals/demo/meta.json index d751e58992..12ee0b59ab 100644 --- a/s2-site/examples/analysis/totals/demo/meta.json +++ b/s2-site/examples/analysis/totals/demo/meta.json @@ -20,6 +20,22 @@ }, "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/3SuoQrkTsR/5657d02f-8e7f-4fbc-8159-7a0e8f772462.png" }, + { + "filename": "dimension-group-row.ts", + "title": { + "zh": "行总计小计按维度分组", + "en": "Total Of Rows Grouped By Dimension" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*1SDsRpTA_kQAAAAAAAAAAAAADmJ7AQ/original" + }, + { + "filename": "dimension-group-col.ts", + "title": { + "zh": "列总计小计按维度分组", + "en": "Total Of Columns Grouped By Dimension" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*5PTqQpXXCcsAAAAAAAAAAAAADmJ7AQ/original" + }, { "filename": "tree.ts", "title": {