From b81b7957b9e8b8e1fbac9ebc6cacdf45a14e5412 Mon Sep 17 00:00:00 2001 From: wuhaiyang Date: Mon, 4 Dec 2023 18:56:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=A4=E5=8F=89=E8=A1=A8=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=86=BB=E7=BB=93=E9=A6=96=E8=A1=8C=E8=83=BD=E5=8A=9B?= =?UTF-8?q?=20(#2416)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 交叉表支持冻结首行 * feat: 调整冻结实现逻辑 & CR遗留问题 & 行头部分重构实现 * fix: 修复 CR 系列反馈的问题 * fix: 修复二轮 CR 反馈的问题 --------- Co-authored-by: wuding.why --- .../s2-core/__tests__/bugs/issue-1191-spec.ts | 9 +- .../s2-core/__tests__/bugs/issue-1201-spec.ts | 5 +- .../__snapshots__/theme-spec.ts.snap | 2 +- .../__tests__/spreadsheet/theme-spec.ts | 21 +- .../__tests__/unit/cell/row-cell-spec.ts | 21 +- .../unit/facet/header/frozen-row-spec.ts | 69 +++ .../unit/facet/pivot-facet-frozen-spec.ts | 406 ++++++++++++++++++ .../__tests__/unit/facet/pivot-facet-spec.ts | 16 +- .../unit/sheet-type/pivot-sheet-spec.ts | 3 +- packages/s2-core/src/cell/frozen-row-cell.ts | 80 ++++ packages/s2-core/src/cell/header-cell.ts | 2 +- packages/s2-core/src/cell/index.ts | 4 + packages/s2-core/src/cell/row-cell.ts | 52 ++- .../s2-core/src/cell/series-number-cell.ts | 39 ++ .../s2-core/src/common/constant/frozen.ts | 8 +- .../s2-core/src/common/interface/s2Options.ts | 6 +- packages/s2-core/src/facet/base-facet.ts | 54 ++- packages/s2-core/src/facet/frozen-facet.ts | 8 +- .../src/facet/header/base-frozen-row.ts | 127 ++++++ packages/s2-core/src/facet/header/index.ts | 1 + .../s2-core/src/facet/header/pivot-row.ts | 19 + packages/s2-core/src/facet/header/row.ts | 48 ++- .../s2-core/src/facet/header/series-number.ts | 205 ++------- packages/s2-core/src/facet/pivot-facet.ts | 140 +++++- packages/s2-core/src/facet/utils.ts | 42 ++ packages/s2-core/src/interaction/root.ts | 7 +- .../s2-core/src/sheet-type/pivot-sheet.ts | 22 +- .../s2-core/src/sheet-type/spread-sheet.ts | 6 +- packages/s2-core/src/theme/index.ts | 2 +- packages/s2-core/src/utils/indexes.ts | 2 +- .../__tests__/spreadsheet/drill-down-spec.tsx | 2 +- s2-site/docs/api/general/S2Options.zh.md | 5 +- .../manual/basic/sheet-type/pivot-mode.en.md | 27 ++ .../manual/basic/sheet-type/pivot-mode.zh.md | 34 ++ .../advanced/demo/frozen-pivot-grid.ts | 24 ++ .../advanced/demo/frozen-pivot-tree.ts | 19 + .../interaction/advanced/demo/meta.json | 16 + 37 files changed, 1279 insertions(+), 274 deletions(-) create mode 100644 packages/s2-core/__tests__/unit/facet/header/frozen-row-spec.ts create mode 100644 packages/s2-core/__tests__/unit/facet/pivot-facet-frozen-spec.ts create mode 100644 packages/s2-core/src/cell/frozen-row-cell.ts create mode 100644 packages/s2-core/src/cell/series-number-cell.ts create mode 100644 packages/s2-core/src/facet/header/base-frozen-row.ts create mode 100644 packages/s2-core/src/facet/header/pivot-row.ts create mode 100644 s2-site/examples/interaction/advanced/demo/frozen-pivot-grid.ts create mode 100644 s2-site/examples/interaction/advanced/demo/frozen-pivot-tree.ts diff --git a/packages/s2-core/__tests__/bugs/issue-1191-spec.ts b/packages/s2-core/__tests__/bugs/issue-1191-spec.ts index d6e4fb6b3f..022d8ccd3b 100644 --- a/packages/s2-core/__tests__/bugs/issue-1191-spec.ts +++ b/packages/s2-core/__tests__/bugs/issue-1191-spec.ts @@ -7,6 +7,7 @@ */ import * as mockDataConfig from 'tests/data/simple-data.json'; import { getContainer } from 'tests/util/helpers'; +import type { Group } from '@antv/g-canvas'; import type { S2DataConfig, S2Options } from '@/common/interface'; import { PivotSheet, SpreadSheet } from '@/sheet-type'; @@ -76,7 +77,9 @@ describe('Link Field Tests', () => { test('province row cell should use link field style', () => { // 浙江省对应 cell - const province = s2.facet.rowHeader.getChildByIndex(0); + const province = ( + s2.facet.rowHeader.getChildByIndex(0) as Group + ).getChildByIndex(0); // @ts-ignore expect(province.textShape.attr('fill')).toEqual('red'); // @ts-ignore @@ -84,7 +87,9 @@ describe('Link Field Tests', () => { }); test('city row cell should not use link field style', () => { // 义乌对应 cell - const city = s2.facet.rowHeader.getChildByIndex(1); + const city = ( + s2.facet.rowHeader.getChildByIndex(0) as Group + ).getChildByIndex(1); // @ts-ignore expect(city.textShape.attr('fill')).not.toEqual('red'); // @ts-ignore diff --git a/packages/s2-core/__tests__/bugs/issue-1201-spec.ts b/packages/s2-core/__tests__/bugs/issue-1201-spec.ts index 04dffac64f..99e3f885ab 100644 --- a/packages/s2-core/__tests__/bugs/issue-1201-spec.ts +++ b/packages/s2-core/__tests__/bugs/issue-1201-spec.ts @@ -4,7 +4,7 @@ * https://github.com/antvis/S2/issues/1201 * fillOpacity */ -import type { IGroup } from '@antv/g-canvas'; +import type { Group, IGroup } from '@antv/g-canvas'; import { getContainer } from '../util/helpers'; import * as mockDataConfig from '../data/data-issue-292.json'; import { PivotSheet } from '@/sheet-type'; @@ -45,7 +45,8 @@ describe('background color opacity test', () => { expect(cornerCell.backgroundShape.attr('fillOpacity')).toEqual(0.1); // row cell - const rowCell = s2.facet.rowHeader.getChildByIndex(0); + const rowHeaderScrollGroup = s2.facet.rowHeader.getChildByIndex(0) as Group; + const rowCell = rowHeaderScrollGroup.getFirst(); // @ts-ignore expect(rowCell.backgroundShape.attr('fillOpacity')).toEqual(0.2); diff --git a/packages/s2-core/__tests__/spreadsheet/__snapshots__/theme-spec.ts.snap b/packages/s2-core/__tests__/spreadsheet/__snapshots__/theme-spec.ts.snap index 50d2c40be0..7e8b2d3100 100644 --- a/packages/s2-core/__tests__/spreadsheet/__snapshots__/theme-spec.ts.snap +++ b/packages/s2-core/__tests__/spreadsheet/__snapshots__/theme-spec.ts.snap @@ -493,7 +493,7 @@ Object { "linkTextFill": "#326EF4", "opacity": 1, "textAlign": "center", - "textBaseline": "middle", + "textBaseline": "top", }, "text": Object { "fill": "#000000", diff --git a/packages/s2-core/__tests__/spreadsheet/theme-spec.ts b/packages/s2-core/__tests__/spreadsheet/theme-spec.ts index 391a54ffe0..0856ed10c5 100644 --- a/packages/s2-core/__tests__/spreadsheet/theme-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/theme-spec.ts @@ -1,6 +1,6 @@ /* eslint-disable jest/expect-expect */ import { createPivotSheet } from 'tests/util/helpers'; -import type { IGroup, ShapeAttrs } from '@antv/g-canvas'; +import type { Group, IGroup, ShapeAttrs } from '@antv/g-canvas'; import { get } from 'lodash'; import type { TextBaseline, @@ -157,7 +157,9 @@ describe('SpreadSheet Theme Tests', () => { }, }); s2.render(); - const rowCell = s2.facet.rowHeader.getFirst() as RowCell; + const rowCell = ( + s2.facet.rowHeader.getChildByIndex(0) as Group + ).getFirst() as RowCell; const actionIconCfg: ShapeAttrs = get(rowCell, 'actionIcons.[0].cfg'); expect(actionIconCfg.fill).toEqual(iconInfo.fill); @@ -191,7 +193,9 @@ describe('SpreadSheet Theme Tests', () => { s2.setThemeCfg(getRowCellThemeCfg(align)); s2.render(); - const rowCell = s2.facet.rowHeader.getFirst() as RowCell; + const rowCell = ( + s2.facet.rowHeader.getChildByIndex(0) as Group + ).getFirst() as RowCell; const rowCellWidth = get(rowCell, 'meta.width'); const actionIconCfg: ShapeAttrs = get(rowCell, 'actionIcons.[0].cfg'); @@ -458,13 +462,18 @@ describe('SpreadSheet Theme Tests', () => { s2.render(); - const rowCell = s2.facet.rowHeader.getChildByIndex(0) as IGroup; // 浙江省 + const rowCell = ( + s2.facet.rowHeader.getChildByIndex(0) as Group + ).getFirst() as IGroup; // 浙江省 const textOfRowCell = getTextShape(rowCell); - const seriesCell = s2.facet.rowIndexHeader.getChildByIndex(3) as IGroup; // 序号1 + const seriesCell = ( + s2.facet.rowIndexHeader.getChildByIndex(0) as Group + ).getFirst() as IGroup; // 序号1 const textOfSeriesCell = getTextShape(seriesCell); + expect(textOfRowCell.attr('textBaseline')).toEqual(textBaseline); - expect(textOfSeriesCell.attr('textBaseline')).toEqual('top'); + expect(textOfSeriesCell.attr('textBaseline')).toEqual(textBaseline); expect(textOfRowCell.attr('y')).toEqual(textOfSeriesCell.attr('y')); }, ); diff --git a/packages/s2-core/__tests__/unit/cell/row-cell-spec.ts b/packages/s2-core/__tests__/unit/cell/row-cell-spec.ts index 18ca4ad3d9..1dca138ff3 100644 --- a/packages/s2-core/__tests__/unit/cell/row-cell-spec.ts +++ b/packages/s2-core/__tests__/unit/cell/row-cell-spec.ts @@ -1,6 +1,7 @@ import { get } from 'lodash'; import { createPivotSheet } from 'tests/util/helpers'; import type { RowCell } from '@antv/s2'; +import type { Group } from '@antv/g-canvas'; import type { SpreadSheet } from '@/sheet-type'; import type { TextAlign } from '@/common'; @@ -34,7 +35,9 @@ describe('Row Cell Tests', () => { }); s2.render(); - const provinceCell = s2.facet.rowHeader.getChildByIndex(0) as RowCell; + const provinceCell = ( + s2.facet.rowHeader.getChildByIndex(0) as Group + ).getChildByIndex(0) as RowCell; const { minX, maxX } = (provinceCell as any).linkFieldShape.getBBox(); // 宽度相当 @@ -67,7 +70,8 @@ describe('Row Cell Tests', () => { }); test('should draw right condition text shape', () => { s2.render(); - const rowCell = s2.facet.rowHeader.getChildByIndex(1); + const scrollGroup = s2.facet.rowHeader.getChildByIndex(0) as Group; + const rowCell = scrollGroup.getChildByIndex(1); expect(get(rowCell, 'textShape.attrs.fill')).toEqual('#5083F5'); }); @@ -88,7 +92,8 @@ describe('Row Cell Tests', () => { }, }); s2.render(); - const rowCell = s2.facet.rowHeader.getChildByIndex(1); + const scrollRowGroup = s2.facet.rowHeader.getChildByIndex(0) as Group; + const rowCell = scrollRowGroup.getChildByIndex(1); expect(get(rowCell, 'conditionIconShape.cfg.name')).toEqual('CellUp'); expect(get(rowCell, 'conditionIconShape.cfg.fill')).toEqual('red'); }); @@ -109,7 +114,8 @@ describe('Row Cell Tests', () => { }, }); s2.render(); - const rowCell = s2.facet.rowHeader.getChildByIndex(0); + const scrollGroup = s2.facet.rowHeader.getChildByIndex(0) as Group; + const rowCell = scrollGroup.getChildByIndex(0); expect(get(rowCell, 'backgroundShape.attrs.fill')).toEqual('#F7B46F'); }); }); @@ -135,9 +141,10 @@ describe('Row Cell Tests', () => { }); s2.render(); test('should draw right condition background shape', () => { - const rowCell0 = s2.facet.rowHeader.getChildByIndex(0); - const rowCell1 = s2.facet.rowHeader.getChildByIndex(1); - const rowCell2 = s2.facet.rowHeader.getChildByIndex(2); + const scrollGroup = s2.facet.rowHeader.getChildByIndex(0) as Group; + const rowCell0 = scrollGroup.getChildByIndex(0); + const rowCell1 = scrollGroup.getChildByIndex(1); + const rowCell2 = scrollGroup.getChildByIndex(2); expect(get(rowCell0, 'actualText')).toEqual('浙江'); expect(get(rowCell0, 'backgroundShape.attrs.fill')).toEqual(defaultColor); expect(get(rowCell1, 'actualText')).toEqual('义乌'); diff --git a/packages/s2-core/__tests__/unit/facet/header/frozen-row-spec.ts b/packages/s2-core/__tests__/unit/facet/header/frozen-row-spec.ts new file mode 100644 index 0000000000..d34aa54dec --- /dev/null +++ b/packages/s2-core/__tests__/unit/facet/header/frozen-row-spec.ts @@ -0,0 +1,69 @@ +import { createPivotSheet } from 'tests/util/helpers'; +import { get } from 'lodash'; +import { DEFAULT_OPTIONS } from '@/common'; + +import { FrozenRowCell, SeriesNumberCell } from '@/cell'; +import { PivotRowHeader } from '@/facet/header'; +import { SeriesNumberHeader } from '@/facet/header/series-number'; + +const s2 = createPivotSheet( + { + ...DEFAULT_OPTIONS, + frozenFirstRowPivot: true, + totals: { row: { showGrandTotals: true, reverseLayout: true } }, + showSeriesNumber: true, + }, + { useSimpleData: false }, +); +describe('Frozen Row Header Test', () => { + test.each(['grid', 'tree'])( + 'frozen row header group api', + (hierarchyType: 'grid' | 'tree') => { + s2.setOptions({ hierarchyType }); + s2.render(); + const rowHeader: PivotRowHeader = s2.facet.rowHeader as PivotRowHeader; + + expect(rowHeader instanceof PivotRowHeader).toBeTrue(); + expect(rowHeader.frozenHeadGroup).toBeTruthy(); + expect(rowHeader.scrollGroup).toBeTruthy(); + + expect(rowHeader.frozenHeadGroup.getChildren()).toHaveLength(1); + const frozenRowCell = rowHeader.frozenHeadGroup.getChildren()[0]; + + expect(frozenRowCell instanceof FrozenRowCell).toBeTrue(); + expect(get(frozenRowCell, 'meta.height')).toEqual(30); + + expect(rowHeader.scrollGroup.getChildren()).toHaveLength(10); + const scrollCell = rowHeader.scrollGroup.getChildren()[0]; + + expect(scrollCell instanceof FrozenRowCell).toBeTrue(); + expect(get(frozenRowCell, 'meta.height')).toEqual(30); + + expect(rowHeader.getFrozenFirstRowHeight()).toBe(30); + }, + ); +}); + +describe('Frozen Series Number Test', () => { + test.each(['grid', 'tree'])( + 'series number test', + (hierarchyType: 'grid' | 'tree') => { + s2.setOptions({ hierarchyType }); + s2.render(); + const rowIndexHeader: SeriesNumberHeader = s2.facet + .rowIndexHeader as SeriesNumberHeader; + expect(rowIndexHeader instanceof SeriesNumberHeader).toBe(true); + + const seriesNumberCell = rowIndexHeader.frozenHeadGroup.getChildren(); + expect(seriesNumberCell).toHaveLength(1); + + expect( + rowIndexHeader.scrollGroup.getChildren()[0] instanceof SeriesNumberCell, + ).toBe(true); + + expect(seriesNumberCell[0] instanceof SeriesNumberCell).toBe(true); + + expect(get(seriesNumberCell[0], 'meta.height')).toBe(30); + }, + ); +}); diff --git a/packages/s2-core/__tests__/unit/facet/pivot-facet-frozen-spec.ts b/packages/s2-core/__tests__/unit/facet/pivot-facet-frozen-spec.ts new file mode 100644 index 0000000000..23e8fdf5e9 --- /dev/null +++ b/packages/s2-core/__tests__/unit/facet/pivot-facet-frozen-spec.ts @@ -0,0 +1,406 @@ +/** + * pivot mode pivot test. + */ +import { createPivotSheet } from 'tests/util/helpers'; +import type { IGroup } from '@antv/g-canvas'; +import { get } from 'lodash'; + +import type { PivotSheet, SpreadSheet } from '@antv/s2'; +import { FrozenRowCell, SeriesNumberCell } from '@/cell'; +import { getFrozenRowCfgPivot } from '@/facet/utils'; +import { + FrozenGroup, + KEY_GROUP_ROW_HEADER_FROZEN, + KEY_GROUP_ROW_SCROLL, +} from '@/common'; +import type { FrozenFacet } from '@/facet/frozen-facet'; + +const defaultOptions = { + frozenFirstRowPivot: true, + totals: { + row: { + showGrandTotals: true, + reverseLayout: true, + }, + }, +}; +const enableFrozenFistRowOption = { + frozenRowCount: 1, + frozenColCount: 0, + frozenTrailingColCount: 0, + frozenTrailingRowCount: 0, + enableFrozenFirstRow: true, + frozenRowHeight: 30, +}; +const disableFrozenFistRowOption = { + ...enableFrozenFistRowOption, + frozenRowCount: 0, + enableFrozenFirstRow: false, + frozenRowHeight: 0, +}; + +let s2: PivotSheet; + +describe('test getFrozenRowCfgPivot', () => { + beforeEach(() => { + s2 = createPivotSheet(defaultOptions, { useSimpleData: false }); + }); + + afterEach(() => { + s2.destroy(); + }); + + test.each(['grid', 'tree'])( + 'test getFrozenRowCfgPivot %s mode', + (hierarchyType: 'grid' | 'tree') => { + s2.setOptions({ + hierarchyType, + }); + s2.render(); + + expect( + getFrozenRowCfgPivot(s2.options, s2.facet.layoutResult.rowNodes), + ).toStrictEqual(enableFrozenFistRowOption); + + s2.setOptions({ pagination: { pageSize: 5, current: 1 } }); + s2.render(); + + expect( + getFrozenRowCfgPivot(s2.options, s2.facet.layoutResult.rowNodes), + ).toStrictEqual(disableFrozenFistRowOption); + }, + ); +}); + +describe('test getFrozenRowCfgPivot in tree', () => { + beforeEach(() => { + s2 = createPivotSheet( + { + ...defaultOptions, + hierarchyType: 'tree', + pagination: { + pageSize: 0, + current: 0, + }, + }, + { useSimpleData: false }, + ); + }); + + afterEach(() => { + s2.destroy(); + }); + + test('showSeriesNumber no totals', () => { + s2.setOptions({ + showSeriesNumber: true, + totals: { row: { showGrandTotals: false } }, + }); + s2.render(); + + expect( + getFrozenRowCfgPivot(s2.options, s2.facet.layoutResult.rowNodes), + ).toStrictEqual(disableFrozenFistRowOption); + }); + + test('showSeriesNumber has totals', () => { + s2.setOptions({ + showSeriesNumber: true, + ...defaultOptions, + }); + s2.render(); + + expect( + getFrozenRowCfgPivot(s2.options, s2.facet.layoutResult.rowNodes), + ).toStrictEqual(enableFrozenFistRowOption); + }); +}); + +describe('test cell XYIndexes frozen first row', () => { + beforeEach(() => { + s2 = createPivotSheet(defaultOptions, { useSimpleData: false }); + s2.render(); + }); + + afterEach(() => { + s2.destroy(); + }); + + test('should get correct frozenGroupInfo', () => { + expect((s2.facet as FrozenFacet).frozenGroupInfo).toStrictEqual({ + [FrozenGroup.FROZEN_COL]: { + width: 0, + }, + [FrozenGroup.FROZEN_ROW]: { + height: 30, + range: [0, 0], + }, + [FrozenGroup.FROZEN_TRAILING_COL]: { + width: 0, + }, + [FrozenGroup.FROZEN_TRAILING_ROW]: { + height: 0, + }, + }); + }); + + test('should get correct xy indexes with frozen in grid', () => { + s2.setOptions({ hierarchyType: 'grid' }); + s2.render(false); + + expect(s2.facet.calculateXYIndexes(0, 0)).toMatchInlineSnapshot(` + Object { + "center": Array [ + 0, + 3, + 1, + 8, + ], + "frozenCol": Array [ + 0, + -1, + 1, + 8, + ], + "frozenRow": Array [ + 0, + 3, + 0, + 0, + ], + "frozenTrailingCol": Array [ + 4, + 3, + 1, + 8, + ], + "frozenTrailingRow": Array [ + 0, + 3, + 9, + 8, + ], + } + `); + expect(s2.facet.calculateXYIndexes(110, 30)).toMatchInlineSnapshot(` + Object { + "center": Array [ + 1, + 3, + 2, + 8, + ], + "frozenCol": Array [ + 0, + -1, + 2, + 8, + ], + "frozenRow": Array [ + 1, + 3, + 0, + 0, + ], + "frozenTrailingCol": Array [ + 4, + 3, + 2, + 8, + ], + "frozenTrailingRow": Array [ + 1, + 3, + 9, + 8, + ], + } + `); + }); + + test('should get correct xy indexes with frozen in tree', () => { + s2.setOptions({ hierarchyType: 'tree' }); + s2.render(false); + + expect(s2.facet.calculateXYIndexes(0, 0)).toMatchInlineSnapshot(` + Object { + "center": Array [ + 0, + 3, + 1, + 10, + ], + "frozenCol": Array [ + 0, + -1, + 1, + 10, + ], + "frozenRow": Array [ + 0, + 3, + 0, + 0, + ], + "frozenTrailingCol": Array [ + 4, + 3, + 1, + 10, + ], + "frozenTrailingRow": Array [ + 0, + 3, + 11, + 10, + ], + } + `); + expect(s2.facet.calculateXYIndexes(110, 30)).toMatchInlineSnapshot(` + Object { + "center": Array [ + 0, + 3, + 2, + 10, + ], + "frozenCol": Array [ + 0, + -1, + 2, + 10, + ], + "frozenRow": Array [ + 0, + 3, + 0, + 0, + ], + "frozenTrailingCol": Array [ + 4, + 3, + 2, + 10, + ], + "frozenTrailingRow": Array [ + 0, + 3, + 11, + 10, + ], + } + `); + }); + + test('should get correct indexes with row height gt canvas height', () => { + const originHeight = s2.facet.panelBBox.viewportHeight; + s2.facet.panelBBox.viewportHeight = 10; + expect(s2.facet.calculateXYIndexes(0, 0)).toMatchInlineSnapshot(` + Object { + "center": Array [ + 0, + 3, + 1, + 0, + ], + "frozenCol": Array [ + 0, + -1, + 1, + 0, + ], + "frozenRow": Array [ + 0, + 3, + 0, + 0, + ], + "frozenTrailingCol": Array [ + 4, + 3, + 1, + 0, + ], + "frozenTrailingRow": Array [ + 0, + 3, + 9, + 8, + ], + } + `); + // reset + s2.facet.panelBBox.viewportHeight = originHeight; + }); +}); + +describe('test frozen group', () => { + beforeEach(() => { + s2 = createPivotSheet( + { + ...defaultOptions, + showSeriesNumber: true, + }, + { useSimpleData: false }, + ); + }); + + afterEach(() => { + s2.destroy(); + }); + + test.each(['grid', 'tree'])( + 'row header group', + (hierarchyType: 'grid' | 'tree') => { + s2.setOptions({ hierarchyType }); + s2.render(); + // row header + const rowHeader = s2.facet.rowHeader; + const scrollHeaderGroup = rowHeader.getChildren()[0]; + expect(rowHeader.getChildren()).toHaveLength(2); + expect(scrollHeaderGroup.cfg.name).toBe(KEY_GROUP_ROW_SCROLL); + expect(rowHeader.getChildren()[1].cfg.name).toBe( + KEY_GROUP_ROW_HEADER_FROZEN, + ); + const frozenRowGroupChildren = ( + rowHeader.getChildren()[1] as IGroup + ).getChildren(); + const scrollRowHeaderGroupChildren = ( + scrollHeaderGroup as IGroup + ).getChildren(); + expect(frozenRowGroupChildren).toHaveLength(1); + expect(frozenRowGroupChildren[0] instanceof FrozenRowCell).toBe(true); + expect(get(frozenRowGroupChildren[0], 'meta.value')).toBe('总计'); + expect(scrollRowHeaderGroupChildren).toHaveLength(10); + expect(scrollRowHeaderGroupChildren[0] instanceof FrozenRowCell).toBe( + true, + ); + expect(get(scrollRowHeaderGroupChildren[0], 'meta.value')).toBe('浙江省'); + + // serial number header + const rowIndexHeader = s2.facet.rowIndexHeader; + expect(rowIndexHeader.getChildren()).toHaveLength(2); + expect(rowIndexHeader.getChildren()[0].cfg.name).toBe( + KEY_GROUP_ROW_SCROLL, + ); + expect(rowIndexHeader.getChildren()[1].cfg.name).toBe( + KEY_GROUP_ROW_HEADER_FROZEN, + ); + const frozenSeriesRowGroupChildren = ( + rowIndexHeader.getChildren()[1] as IGroup + ).getChildren(); + const scrollSeriesRowScrollGroupChildren = ( + rowIndexHeader.getChildren()[0] as IGroup + ).getChildren(); + expect(frozenSeriesRowGroupChildren).toHaveLength(1); + expect(frozenSeriesRowGroupChildren[0] instanceof SeriesNumberCell).toBe( + true, + ); + expect(get(frozenSeriesRowGroupChildren[0], 'meta.value')).toBe('1'); + expect(scrollSeriesRowScrollGroupChildren).toHaveLength(2); + expect(get(scrollSeriesRowScrollGroupChildren[0], 'meta.value')).toBe( + '2', + ); + }, + ); +}); diff --git a/packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts b/packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts index a17d7819ad..88f7fc1227 100644 --- a/packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts @@ -15,7 +15,13 @@ import { CornerCell, DataCell } from '@/cell'; import { Store } from '@/common/store'; import { getTheme } from '@/theme'; import { DEFAULT_OPTIONS, DEFAULT_STYLE } from '@/common/constant/options'; -import { ColHeader, CornerHeader, Frame, RowHeader } from '@/facet/header'; +import { + ColHeader, + CornerHeader, + Frame, + PivotRowHeader, + RowHeader, +} from '@/facet/header'; import type { Fields, ViewMeta } from '@/common/interface/basic'; import { RootInteraction } from '@/interaction/root'; import { areAllFieldsEmpty } from '@/facet/utils'; @@ -249,8 +255,11 @@ describe('Pivot Mode Facet Test', () => { test('get header after render', () => { const { rowHeader, cornerHeader, columnHeader, centerFrame } = facet; - expect(rowHeader instanceof RowHeader).toBeTrue(); - expect(rowHeader.cfg.children).toHaveLength(10); + expect( + rowHeader instanceof PivotRowHeader || rowHeader instanceof RowHeader, + ).toBeTrue(); + + expect(rowHeader.cfg.children[0].getChildren()).toHaveLength(10); expect(rowHeader.cfg.visible).toBeTrue(); expect(cornerHeader instanceof CornerHeader).toBeTrue(); @@ -265,7 +274,6 @@ describe('Pivot Mode Facet Test', () => { const { backgroundGroup } = facet; const rect = get(backgroundGroup, 'cfg.children[0]'); - expect(backgroundGroup.cfg.children).toHaveLength(3); expect(rect.cfg.type).toBe('rect'); expect(rect.cfg.visible).toBeTrue(); diff --git a/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts b/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts index 7f6fd15f46..c0de4f2fb4 100644 --- a/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts +++ b/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts @@ -604,7 +604,8 @@ describe('PivotSheet Tests', () => { expect(s2.foregroundGroup.getChildren()).toHaveLength(9); // panel scroll group - expect(s2.panelGroup.getChildren()).toHaveLength(1); + // contain panelScrollGroup and frozenRowGroup + expect(s2.panelGroup.getChildren()).toHaveLength(2); expect(s2.panelGroup.findAllByName(KEY_GROUP_PANEL_SCROLL)).toHaveLength(1); }); diff --git a/packages/s2-core/src/cell/frozen-row-cell.ts b/packages/s2-core/src/cell/frozen-row-cell.ts new file mode 100644 index 0000000000..3728072c39 --- /dev/null +++ b/packages/s2-core/src/cell/frozen-row-cell.ts @@ -0,0 +1,80 @@ +import type { SimpleBBox } from '@antv/g-canvas'; +import { getAdjustPosition } from '../utils/text-absorption'; +import { getFrozenRowCfgPivot } from '../facet/utils'; +import type { BaseHeaderConfig } from '../facet/header/base'; +import { RowCell } from './row-cell'; + +/** + * Adapting the frozen first row for cells pivot table + */ +export class FrozenRowCell extends RowCell { + /** + * To indicate whether the current node is a frozen node + * + * PS: It is a specific config for the cell node, so it should not be extended in the headerConfig. + */ + protected frozenRowCell: boolean; + + protected handleRestOptions( + ...[headerConfig, ...options]: [BaseHeaderConfig, boolean] + ) { + super.handleRestOptions(headerConfig, options); + this.frozenRowCell = options[0]; + } + + protected getAdjustTextAreaHeight( + textArea: SimpleBBox, + scrollY: number, + viewportHeight: number, + ): number { + const correctY = textArea.y - this.getFrozenFirstRowHeight(); + let adjustTextAreaHeight = textArea.height; + if ( + !this.spreadsheet.facet.vScrollBar && + correctY + textArea.height > scrollY + viewportHeight + ) { + adjustTextAreaHeight = scrollY + viewportHeight - correctY; + } + return adjustTextAreaHeight; + } + + protected calculateTextY({ + textArea, + adjustTextAreaHeight, + }: { + textArea: SimpleBBox; + adjustTextAreaHeight: number; + }): number { + const { scrollY, viewportHeight } = this.headerConfig; + const { fontSize } = this.getTextStyle(); + return getAdjustPosition( + textArea.y, + adjustTextAreaHeight, + // viewportLeft: start at the frozen row position + scrollY + this.getFrozenFirstRowHeight(), + viewportHeight, + fontSize, + ); + } + + protected getResizeClipAreaBBox(): SimpleBBox { + return { + ...super.getResizeClipAreaBBox(), + y: this.getFrozenFirstRowHeight(), + }; + } + + private getFrozenFirstRowHeight(): number { + if (this.frozenRowCell) { + // frozen row cell + return 0; + } + const { spreadsheet } = this.headerConfig; + const { facet } = spreadsheet; + const { frozenRowHeight } = getFrozenRowCfgPivot( + spreadsheet.options, + facet?.layoutResult?.rowNodes, + ); + return frozenRowHeight; + } +} diff --git a/packages/s2-core/src/cell/header-cell.ts b/packages/s2-core/src/cell/header-cell.ts index 538b9c654b..8af3528c43 100644 --- a/packages/s2-core/src/cell/header-cell.ts +++ b/packages/s2-core/src/cell/header-cell.ts @@ -48,7 +48,7 @@ export abstract class HeaderCell extends BaseCell { protected abstract isBolderText(): boolean; - protected handleRestOptions(...[headerConfig]: [BaseHeaderConfig]) { + protected handleRestOptions(...[headerConfig]: [BaseHeaderConfig, unknown]) { this.headerConfig = { ...headerConfig }; const { value, query } = this.meta; const sortParams = this.spreadsheet.dataCfg.sortParams; diff --git a/packages/s2-core/src/cell/index.ts b/packages/s2-core/src/cell/index.ts index fc45940492..54eb1eafba 100644 --- a/packages/s2-core/src/cell/index.ts +++ b/packages/s2-core/src/cell/index.ts @@ -9,6 +9,8 @@ import { TableColCell } from './table-col-cell'; import { TableCornerCell } from './table-corner-cell'; import { TableDataCell } from './table-data-cell'; import { TableSeriesCell } from './table-series-cell'; +import { FrozenRowCell } from './frozen-row-cell'; +import { SeriesNumberCell } from './series-number-cell'; export { TableCornerCell, @@ -22,4 +24,6 @@ export { CornerCell, BaseCell, HeaderCell, + FrozenRowCell, + SeriesNumberCell, }; diff --git a/packages/s2-core/src/cell/row-cell.ts b/packages/s2-core/src/cell/row-cell.ts index 3b0107e0fa..f26f76ccb7 100644 --- a/packages/s2-core/src/cell/row-cell.ts +++ b/packages/s2-core/src/cell/row-cell.ts @@ -168,8 +168,7 @@ export class RowCell extends HeaderCell { if (!isCollapsed) { const oldScrollY = this.spreadsheet.store.get('scrollY'); // 可视窗口高度 - const viewportHeight = - this.spreadsheet.facet.panelBBox.viewportHeight || 0; + const viewportHeight = this.headerConfig.viewportHeight || 0; // 被折叠项的高度 const deleteHeight = getAllChildrenNodeHeight(this.meta); // 折叠后真实高度 @@ -257,6 +256,16 @@ export class RowCell extends HeaderCell { }); } + protected getResizeClipAreaBBox(): SimpleBBox { + const { width, viewportHeight } = this.headerConfig; + return { + x: 0, + y: 0, + width, + height: viewportHeight, + }; + } + protected drawResizeAreaInLeaf() { if ( !this.meta.isLeaf || @@ -276,24 +285,19 @@ export class RowCell extends HeaderCell { position, seriesNumberWidth, width: headerWidth, - viewportHeight: headerHeight, scrollX, scrollY, } = this.headerConfig; const resizeAreaBBox = { - x, + // fix: When scrolling without the entire frozen header horizontally, the resize area would be removed permanently. + x: x + seriesNumberWidth, y: y + height - resizeStyle.size / 2, width, height: resizeStyle.size, }; - const resizeClipAreaBBox = { - x: 0, - y: 0, - width: headerWidth, - height: headerHeight, - }; + const resizeClipAreaBBox = this.getResizeClipAreaBBox(); if ( !shouldAddResizeArea(resizeAreaBBox, resizeClipAreaBBox, { @@ -450,6 +454,24 @@ export class RowCell extends HeaderCell { return adjustTextAreaHeight; } + protected calculateTextY({ + textArea, + adjustTextAreaHeight, + }: { + textArea: SimpleBBox; + adjustTextAreaHeight: number; + }): number { + const { scrollY, viewportHeight } = this.headerConfig; + const { fontSize } = this.getTextStyle(); + return getAdjustPosition( + textArea.y, + adjustTextAreaHeight, + scrollY, + viewportHeight, + fontSize, + ); + } + protected getTextPosition(): Point { const textArea = this.getTextArea(); const { scrollY, viewportHeight } = this.headerConfig; @@ -459,15 +481,7 @@ export class RowCell extends HeaderCell { scrollY, viewportHeight, ); - - const { fontSize } = this.getTextStyle(); - const textY = getAdjustPosition( - textArea.y, - adjustTextAreaHeight, - scrollY, - viewportHeight, - fontSize, - ); + const textY = this.calculateTextY({ textArea, adjustTextAreaHeight }); const textX = getTextAndFollowingIconPosition( textArea, this.getTextStyle(), diff --git a/packages/s2-core/src/cell/series-number-cell.ts b/packages/s2-core/src/cell/series-number-cell.ts new file mode 100644 index 0000000000..99193ef260 --- /dev/null +++ b/packages/s2-core/src/cell/series-number-cell.ts @@ -0,0 +1,39 @@ +import type { Point } from '@antv/g-canvas'; +import type { Condition, IconTheme, MappingResult, TextTheme } from '../common'; +import { FrozenRowCell } from './frozen-row-cell'; + +export class SeriesNumberCell extends FrozenRowCell { + protected initCell(): void { + this.drawBackgroundShape(); + this.drawRectBorder(); + this.drawTextShape(); + } + + protected getTextStyle(): TextTheme { + return this.getStyle()?.seriesText; + } + + protected getTextPosition(): Point { + return super.getTextPosition(); + } + + protected getActionIconsCount(): number { + return 0; + } + + public getIconStyle(): IconTheme { + return undefined; + } + + protected drawResizeAreaInLeaf(): void {} + + public update() {} + + public findFieldCondition(): Condition | undefined { + return undefined; + } + + public mappingValue(): MappingResult | undefined { + return undefined; + } +} diff --git a/packages/s2-core/src/common/constant/frozen.ts b/packages/s2-core/src/common/constant/frozen.ts index 4de2801ec5..058586bd3d 100644 --- a/packages/s2-core/src/common/constant/frozen.ts +++ b/packages/s2-core/src/common/constant/frozen.ts @@ -26,10 +26,10 @@ export const FrozenCellGroupMap = { }; export interface FrozenOpts { - frozenRowCount: number; - frozenColCount: number; - frozenTrailingRowCount: number; - frozenTrailingColCount: number; + frozenRowCount?: number; + frozenColCount?: number; + frozenTrailingRowCount?: number; + frozenTrailingColCount?: number; } export interface FrozenCellIndex { diff --git a/packages/s2-core/src/common/interface/s2Options.ts b/packages/s2-core/src/common/interface/s2Options.ts index 7ee7c9d042..93ba041e08 100644 --- a/packages/s2-core/src/common/interface/s2Options.ts +++ b/packages/s2-core/src/common/interface/s2Options.ts @@ -124,8 +124,10 @@ export interface S2TableSheetOptions { } // Pivot sheet options -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface S2PivotSheetOptions {} +export interface S2PivotSheetOptions { + // pivot sheet type: frozen head row, default false + frozenFirstRowPivot?: boolean; +} export interface S2Options< T = TooltipContentType, diff --git a/packages/s2-core/src/facet/base-facet.ts b/packages/s2-core/src/facet/base-facet.ts index ff6cc9ac36..2a01d44132 100644 --- a/packages/s2-core/src/facet/base-facet.ts +++ b/packages/s2-core/src/facet/base-facet.ts @@ -64,6 +64,7 @@ import { Frame, RowHeader, SeriesNumberHeader, + type RowHeaderConfig, } from './header'; import type { ViewCellHeights } from './layout/interface'; import type { Node } from './layout/node'; @@ -74,6 +75,7 @@ import { optimizeScrollXY, translateGroup, } from './utils'; +import type { BaseHeader, BaseHeaderConfig } from './header/base'; export abstract class BaseFacet { // spreadsheet instance @@ -1018,7 +1020,7 @@ export abstract class BaseFacet { }); }; - protected clip(scrollX: number, scrollY: number) { + protected panelScrollGroupClip(scrollX: number, scrollY: number) { const isFrozenRowHeader = this.spreadsheet.isFrozenRowHeader(); this.spreadsheet.panelScrollGroup?.setClip({ type: 'rect', @@ -1031,6 +1033,10 @@ export abstract class BaseFacet { }); } + protected clip(scrollX: number, scrollY: number) { + this.panelScrollGroupClip(scrollX, scrollY); + } + /** * Translate panelGroup, rowHeader, cornerHeader, columnHeader ect * according to new scroll offset @@ -1210,22 +1216,26 @@ export abstract class BaseFacet { this.foregroundGroup.add(this.centerFrame); } + protected getRowHeaderCfg(): RowHeaderConfig { + const { y, viewportHeight, viewportWidth, height } = this.panelBBox; + const seriesNumberWidth = this.getSeriesNumberWidth(); + return { + width: this.cornerBBox.width, + height, + viewportWidth, + viewportHeight, + position: { x: 0, y }, + data: this.layoutResult.rowNodes, + hierarchyType: this.cfg.hierarchyType, + linkFields: this.cfg.spreadsheet.options?.interaction?.linkFields ?? [], + seriesNumberWidth, + spreadsheet: this.spreadsheet, + }; + } + protected getRowHeader(): RowHeader { if (!this.rowHeader) { - const { y, viewportHeight, viewportWidth, height } = this.panelBBox; - const seriesNumberWidth = this.getSeriesNumberWidth(); - return new RowHeader({ - width: this.cornerBBox.width, - height, - viewportWidth, - viewportHeight, - position: { x: 0, y }, - data: this.layoutResult.rowNodes, - hierarchyType: this.cfg.hierarchyType, - linkFields: this.cfg.spreadsheet.options?.interaction?.linkFields ?? [], - seriesNumberWidth, - spreadsheet: this.spreadsheet, - }); + return new RowHeader(this.getRowHeaderCfg()); } return this.rowHeader; } @@ -1265,13 +1275,13 @@ export abstract class BaseFacet { } protected getSeriesNumberHeader(): SeriesNumberHeader { - return SeriesNumberHeader.getSeriesNumberHeader( - this.panelBBox, - this.getSeriesNumberWidth(), - this.layoutResult.rowsHierarchy.getNodes(0), - this.spreadsheet, - this.cornerBBox.width, - ); + return SeriesNumberHeader.getSeriesNumberHeader({ + viewportBBox: this.panelBBox, + seriesNumberWidth: this.getSeriesNumberWidth(), + leafNodes: this.layoutResult.rowsHierarchy.getNodes(0), + spreadsheet: this.spreadsheet, + cornerWidth: this.cornerBBox.width, + }); } protected getCenterFrame(): Frame { diff --git a/packages/s2-core/src/facet/frozen-facet.ts b/packages/s2-core/src/facet/frozen-facet.ts index 4129b3473c..05223b69dc 100644 --- a/packages/s2-core/src/facet/frozen-facet.ts +++ b/packages/s2-core/src/facet/frozen-facet.ts @@ -60,7 +60,7 @@ export abstract class FrozenFacet extends BaseFacet { }, }; - public panelScrollGroupIndexes = []; + public panelScrollGroupIndexes: Indexes = []; public constructor(cfg: SpreadSheetFacetCfg) { super(cfg); @@ -480,12 +480,6 @@ export abstract class FrozenFacet extends BaseFacet { } if (frozenTrailingColCount > 0) { - // const width = colLeafNodes.reduceRight((prev, item, idx) => { - // if (idx >= colLeafNodes.length - frozenTrailingColCount) { - // return prev + item.width; - // } - // return prev; - // }, 0); const { x } = colLeafNodes[colLeafNodes.length - frozenTrailingColCount]; const height = frozenTrailingRowCount ? panelHeight : viewportHeight; renderLine( diff --git a/packages/s2-core/src/facet/header/base-frozen-row.ts b/packages/s2-core/src/facet/header/base-frozen-row.ts new file mode 100644 index 0000000000..d7f34ef3a2 --- /dev/null +++ b/packages/s2-core/src/facet/header/base-frozen-row.ts @@ -0,0 +1,127 @@ +import type { IGroup } from '@antv/g-canvas'; +import type { Node } from '../layout/node'; +import { + getFrozenRowCfgPivot, + translateGroup, + translateGroupX, +} from '../utils'; +import { + FRONT_GROUND_GROUP_FROZEN_Z_INDEX, + FRONT_GROUND_GROUP_SCROLL_Z_INDEX, + KEY_GROUP_ROW_HEADER_FROZEN, + KEY_GROUP_ROW_SCROLL, +} from '../../common/constant'; +import { RowHeader, type RowHeaderConfig } from './row'; + +export class BaseFrozenRowHeader extends RowHeader { + public scrollGroup: IGroup; + + public frozenHeadGroup: IGroup; + + constructor(cfg: RowHeaderConfig) { + super(cfg); + this.scrollGroup = this.addGroup({ + name: KEY_GROUP_ROW_SCROLL, + zIndex: FRONT_GROUND_GROUP_SCROLL_Z_INDEX, + }); + + this.frozenHeadGroup = this.addGroup({ + name: KEY_GROUP_ROW_HEADER_FROZEN, + zIndex: FRONT_GROUND_GROUP_FROZEN_Z_INDEX, + }); + + const { position, seriesNumberWidth } = this.headerConfig; + translateGroup( + this.frozenHeadGroup, + position.x + seriesNumberWidth, + position.y, + ); + } + + protected rowCellInRectXDir(item: Node): boolean { + const { width, scrollX, seriesNumberWidth } = this.headerConfig; + const itemX = item.x + seriesNumberWidth; + return width + scrollX > itemX && scrollX < itemX + item.width; + } + + protected rowCellInRectYDir(item: Node): boolean { + const { viewportHeight, scrollY } = this.headerConfig; + const itemY = item.y - this.getFrozenFirstRowHeight(); + return viewportHeight + scrollY > itemY && scrollY < itemY + item.height; + } + + protected rowCellInRect(item: Node): boolean { + const visibleInXDir = this.rowCellInRectXDir(item); + if (this.isFrozenRow(item)) { + return visibleInXDir; + } + return visibleInXDir && this.rowCellInRectYDir(item); + } + + protected getCellGroup(item: Node): IGroup { + if (this.isFrozenRow(item)) { + return this.frozenHeadGroup; + } + return this.scrollGroup; + } + + protected offset() { + const { scrollX, scrollY, position, seriesNumberWidth } = this.headerConfig; + // 向右多移动的seriesNumberWidth是序号的宽度 + const translateX = position.x - scrollX + seriesNumberWidth; + translateGroup(this.scrollGroup, translateX, position.y - scrollY); + translateGroupX(this.frozenHeadGroup, translateX); + } + + public clip(): void { + const { width, viewportHeight, scrollX, scrollY, seriesNumberWidth } = + this.headerConfig; + // 由于多移动了seriesNumberWidth跨度,所有需要向左切。 - 是反向剪裁(右 -> 左) + const clipX = scrollX - seriesNumberWidth; + this.scrollGroup.setClip({ + type: 'rect', + attrs: { + x: clipX, + y: scrollY + this.getFrozenFirstRowHeight(), + width, + height: viewportHeight, + }, + }); + this.frozenHeadGroup.setClip({ + type: 'rect', + attrs: { + x: clipX, + y: 0, + width: this.headerConfig.width, + height: this.getFrozenFirstRowHeight(), + }, + }); + } + + public isFrozenRow(item: Node): boolean { + const { spreadsheet } = this.headerConfig; + const { facet } = spreadsheet; + const { frozenRowCount } = getFrozenRowCfgPivot( + spreadsheet.options, + facet.layoutResult?.rowNodes, + ); + return ( + frozenRowCount > 0 && item.rowIndex >= 0 && item.rowIndex < frozenRowCount + ); + } + + public getFrozenFirstRowHeight(): number { + const { spreadsheet } = this.headerConfig; + const { facet } = spreadsheet; + const { frozenRowHeight } = getFrozenRowCfgPivot( + spreadsheet.options, + facet.layoutResult?.rowNodes, + ); + return frozenRowHeight; + } + + public clear(): void { + this.frozenHeadGroup.clear(); + this.scrollGroup.clear(); + } +} diff --git a/packages/s2-core/src/facet/header/index.ts b/packages/s2-core/src/facet/header/index.ts index b3b69e5d36..67966fc0d5 100644 --- a/packages/s2-core/src/facet/header/index.ts +++ b/packages/s2-core/src/facet/header/index.ts @@ -3,3 +3,4 @@ export { CornerHeader, type CornerHeaderConfig } from './corner'; export { Frame } from './frame'; export { RowHeader, type RowHeaderConfig } from './row'; export { SeriesNumberHeader } from './series-number'; +export { PivotRowHeader } from './pivot-row'; diff --git a/packages/s2-core/src/facet/header/pivot-row.ts b/packages/s2-core/src/facet/header/pivot-row.ts new file mode 100644 index 0000000000..493bd13772 --- /dev/null +++ b/packages/s2-core/src/facet/header/pivot-row.ts @@ -0,0 +1,19 @@ +import type { Node } from '../layout/node'; +import { FrozenRowCell, RowCell } from '../../cell'; +import { BaseFrozenRowHeader } from './base-frozen-row'; + +export class PivotRowHeader extends BaseFrozenRowHeader { + protected createCellInstance(item: Node): RowCell { + const { spreadsheet, scrollY } = this.headerConfig; + const frozenRow = this.isFrozenRow(item); + return new FrozenRowCell( + item, + spreadsheet, + { + ...this.headerConfig, + scrollY: frozenRow ? 0 : scrollY, + }, + frozenRow, + ); + } +} diff --git a/packages/s2-core/src/facet/header/row.ts b/packages/s2-core/src/facet/header/row.ts index 911460845f..4e6d2b2523 100644 --- a/packages/s2-core/src/facet/header/row.ts +++ b/packages/s2-core/src/facet/header/row.ts @@ -1,5 +1,6 @@ import type { GM } from '@antv/g-gesture'; import { each, isEmpty } from 'lodash'; +import type { IGroup } from '@antv/g-canvas'; import { RowCell } from '../../cell'; import type { S2CellType, S2Options, ViewMeta } from '../../common/interface'; import type { Node } from '../layout/node'; @@ -33,29 +34,31 @@ export class RowHeader extends BaseHeader { } } - protected layout() { - const { - data, - spreadsheet, - width, - viewportHeight, - seriesNumberWidth, - scrollY, - scrollX, - } = this.headerConfig; + // row'cell only show when visible + protected rowCellInRect(item: Node): boolean { + const { width, viewportHeight, seriesNumberWidth, scrollY, scrollX } = + this.headerConfig; + return ( + viewportHeight + scrollY > item.y && // bottom + scrollY < item.y + item.height && // top + width - seriesNumberWidth + scrollX > item.x && // left + scrollX - seriesNumberWidth < item.x + item.width + ); // right + } + + protected createCellInstance(item: Node) { + return new RowCell(item, this.headerConfig.spreadsheet, this.headerConfig); + } + + protected getCellGroup(item: Node): IGroup { + return this; + } + protected layout() { + const { data, spreadsheet } = this.headerConfig; const rowCell = spreadsheet?.facet?.cfg?.rowCell; - // row'cell only show when visible - const rowCellInRect = (item: Node): boolean => { - return ( - viewportHeight + scrollY > item.y && // bottom - scrollY < item.y + item.height && // top - width - seriesNumberWidth + scrollX > item.x && // left - scrollX - seriesNumberWidth < item.x + item.width - ); // right - }; each(data, (item: Node) => { - if (rowCellInRect(item) && item.height !== 0) { + if (this.rowCellInRect(item) && item.height !== 0) { let cell: S2CellType; // 首先由外部控制UI展示 if (rowCell) { @@ -64,11 +67,12 @@ export class RowHeader extends BaseHeader { // 如果外部没处理,就用默认的 if (isEmpty(cell)) { if (spreadsheet.isPivotMode()) { - cell = new RowCell(item, spreadsheet, this.headerConfig); + cell = this.createCellInstance(item); } } item.belongsCell = cell; - this.add(cell); + const group = this.getCellGroup(item); + group.add(cell); } }); } diff --git a/packages/s2-core/src/facet/header/series-number.ts b/packages/s2-core/src/facet/header/series-number.ts index 86d097b9b6..b46410ca96 100644 --- a/packages/s2-core/src/facet/header/series-number.ts +++ b/packages/s2-core/src/facet/header/series-number.ts @@ -1,20 +1,11 @@ -import type { Group, IGroup, IShape } from '@antv/g-canvas'; -import { each } from 'lodash'; -import { CellBorderPosition, type Padding } from '../../common/interface'; +import { RowCell, SeriesNumberCell } from '../../cell'; import type { SpreadSheet } from '../../sheet-type/index'; -import { getBorderPositionAndStyle } from '../../utils/cell/cell'; -import { renderLine, renderRect } from '../../utils/g-renders'; -import { getAdjustPosition } from '../../utils/text-absorption'; import type { PanelBBox } from '../bbox/panelBBox'; import { Node } from '../layout/node'; -import { translateGroup } from '../utils'; -import { BaseHeader, type BaseHeaderConfig } from './base'; - -export class SeriesNumberHeader extends BaseHeader { - private backgroundShape: IShape; - - private leftBorderShape: IShape; +import { getFrozenRowCfgPivot } from '../utils'; +import { BaseFrozenRowHeader } from './base-frozen-row'; +export class SeriesNumberHeader extends BaseFrozenRowHeader { /** * Get seriesNumber header by config * @param viewportBBox @@ -23,17 +14,23 @@ export class SeriesNumberHeader extends BaseHeader { * @param spreadsheet * @param cornerWidth */ - - public static getSeriesNumberHeader( - viewportBBox: PanelBBox, - seriesNumberWidth: number, - leafNodes: Node[], - spreadsheet: SpreadSheet, - cornerWidth: number, - ): SeriesNumberHeader { + public static getSeriesNumberHeader({ + viewportBBox, + seriesNumberWidth, + leafNodes, + spreadsheet, + cornerWidth, + }: { + viewportBBox: PanelBBox; + seriesNumberWidth: number; + leafNodes: Node[]; + spreadsheet: SpreadSheet; + cornerWidth: number; + }): SeriesNumberHeader { const { height, viewportHeight } = viewportBBox; const seriesNodes: Node[] = []; const isHierarchyTreeType = spreadsheet.isHierarchyTreeType(); + leafNodes.forEach((node: Node): void => { // 1、is spreadsheet and node is not total(grand or sub) // 2、is listSheet @@ -48,160 +45,44 @@ export class SeriesNumberHeader extends BaseHeader { ? node.getTotalHeightForTreeHierarchy() : node.height; sNode.width = seriesNumberWidth; + sNode.rowIndex = node.rowIndex; + sNode.isLeaf = true; seriesNodes.push(sNode); }); + const { facet } = spreadsheet; + const { frozenRowCount, frozenRowHeight } = getFrozenRowCfgPivot( + spreadsheet.options, + facet.layoutResult?.rowNodes, + ); + const enableFrozenFirstRow = !!frozenRowCount; return new SeriesNumberHeader({ width: cornerWidth, height, viewportWidth: cornerWidth, - viewportHeight, + viewportHeight: enableFrozenFirstRow + ? viewportHeight - frozenRowHeight + : viewportHeight, position: { x: 0, y: viewportBBox.y }, data: seriesNodes, spreadsheet, + // There are no other lines before the serial number row + seriesNumberWidth: 0, + hierarchyType: spreadsheet.options.hierarchyType, + linkFields: [], }); } - constructor(cfg: BaseHeaderConfig) { - super(cfg); - } - - public clip(): void { - const { width, viewportHeight, scrollY } = this.headerConfig; - this.setClip({ - type: 'rect', - attrs: { - x: 0, - y: scrollY, - width, - height: viewportHeight, - }, - }); - } - - public layout() { - const { data, scrollY, viewportHeight, spreadsheet } = this.headerConfig; - if (spreadsheet.isPivotMode) { - // 添加矩形背景 - this.addBackGround(); - } - - const borderGroup = this.addGroup(); - each(data, (cellData) => { - const { y, height: cellHeight, isLeaf } = cellData; - const isHeaderCellInViewport = this.isHeaderCellInViewport( - y, - cellHeight, - scrollY, - viewportHeight, - ); - if (isHeaderCellInViewport) { - // 按需渲染:视窗内的才渲染 - const group = this.addGroup(); - - // 添加文本 - this.addText(group, cellData); - - this.add(group); - - // 添加边框 - if (!isLeaf) { - this.addBorder(borderGroup, cellData); - } - } - }); - } - - protected offset() { - const { scrollY, scrollX, position } = this.headerConfig; - translateGroup(this, position.x - scrollX, position.y - scrollY); - if (this.backgroundShape) { - this.backgroundShape.translate(position.x, position.y + scrollY); - } - if (this.leftBorderShape) { - this.leftBorderShape.translate(position.x, position.y + scrollY); - } - } - - private addBackGround() { - const rowCellTheme = this.getStyle().cell; - const { position, width, viewportHeight } = this.headerConfig; - - this.backgroundShape = renderRect(this, { - x: position.x, - y: -position.y, - width, - height: viewportHeight, - fill: rowCellTheme.backgroundColor, - stroke: 'transparent', - opacity: rowCellTheme.backgroundColorOpacity, - }); - - const { position: borderPosition, style: borderStyle } = - getBorderPositionAndStyle( - CellBorderPosition.LEFT, - { - x: position.x, - y: -position.y, - width, - height: viewportHeight, - }, - rowCellTheme, - ); - - this.leftBorderShape = renderLine(this, borderPosition, borderStyle); - } - - private addBorder(group: IGroup, cellData) { - const cellTheme = this.getStyle().cell; - - const { position: horizontalPosition, style: horizontalStyle } = - getBorderPositionAndStyle(CellBorderPosition.BOTTOM, cellData, cellTheme); - - renderLine(group as Group, horizontalPosition, horizontalStyle); - } - - private getStyle() { - return this.headerConfig.spreadsheet.theme.rowCell; - } - - private addText(group: IGroup, cellData: Node) { - const { scrollY, viewportHeight: height } = this.headerConfig; - const textStyle = { - ...this.getStyle().seriesText, - textBaseline: 'top' as const, - }; - const { label, x, y, width: cellWidth, height: cellHeight } = cellData; - const padding = this.getTextPadding(label, cellWidth); - const textY = getAdjustPosition( - y + padding.top, - cellHeight - padding.top - padding.bottom, - scrollY, - height, - textStyle.fontSize, - ); - - group.addShape('text', { - attrs: { - x: x + padding.left, - y: textY, - text: label, - ...textStyle, - cursor: 'pointer', + protected createCellInstance(item: Node): RowCell { + const frozenRow = this.isFrozenRow(item); + const cell = new SeriesNumberCell( + item, + this.headerConfig.spreadsheet, + { + ...this.headerConfig, + scrollY: frozenRow ? 0 : this.headerConfig.scrollY, }, - }); - } - - private getTextPadding(text: string, cellWidth: number): Padding { - const rowCellTheme = this.getStyle(); - const textWidth = this.headerConfig.spreadsheet.measureTextWidth( - text, - rowCellTheme.seriesText, + frozenRow, ); - const padding = Math.max(Math.abs((cellWidth - textWidth) / 2), 4); - return { - ...rowCellTheme.cell.padding, - left: padding, - right: padding, - }; + return cell; } } diff --git a/packages/s2-core/src/facet/pivot-facet.ts b/packages/s2-core/src/facet/pivot-facet.ts index b12e81320b..067b8c6bb7 100644 --- a/packages/s2-core/src/facet/pivot-facet.ts +++ b/packages/s2-core/src/facet/pivot-facet.ts @@ -14,28 +14,144 @@ import { size, sumBy, } from 'lodash'; +import type { Group } from '@antv/g-canvas'; import { DEFAULT_TREE_ROW_WIDTH, LAYOUT_SAMPLE_COUNT, type IconTheme, type MultiData, + FrozenGroup, + KEY_GROUP_FROZEN_SPLIT_LINE, + FRONT_GROUND_GROUP_FROZEN_Z_INDEX, } from '../common'; import { EXTRA_FIELD, LayoutWidthTypes, VALUE_FIELD } from '../common/constant'; import { CellTypes } from '../common/constant/interaction'; import { DebuggerUtil } from '../common/debug'; -import type { LayoutResult, ViewMeta } from '../common/interface'; +import type { + LayoutResult, + S2TableSheetOptions, + SplitLine, + ViewMeta, +} from '../common/interface'; import { getDataCellId, handleDataItem } from '../utils/cell/data-cell'; import { getActionIconConfig } from '../utils/cell/header-cell'; 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 { getRowsForGrid } from '../utils/grid'; +import { renderLine } from '..'; +import { FrozenFacet } from './frozen-facet'; import { buildHeaderHierarchy } from './layout/build-header-hierarchy'; import type { Hierarchy } from './layout/hierarchy'; import { layoutCoordinate, layoutDataPosition } from './layout/layout-hooks'; import { Node } from './layout/node'; +import { getFrozenRowCfgPivot } from './utils'; +import { PivotRowHeader, RowHeader } from './header'; + +export class PivotFacet extends FrozenFacet { + protected updateFrozenGroupGrid(): void { + [FrozenGroup.FROZEN_ROW].forEach((key) => { + if (!this.frozenGroupInfo[key].range) { + return; + } + let cols = []; + let rows = []; + if (key.toLowerCase().includes('row')) { + const [rowMin, rowMax] = this.frozenGroupInfo[key].range; + cols = this.gridInfo.cols; + rows = getRowsForGrid(rowMin, rowMax, this.viewCellHeights); + } + this.spreadsheet[`${key}Group`].updateGrid( + { + cols, + rows, + }, + `${key}Group`, + ); + }); + } + + protected getBizRevisedFrozenOptions(): S2TableSheetOptions { + return getFrozenRowCfgPivot(this.cfg, this.layoutResult.rowNodes); + } + + protected renderFrozenGroupSplitLine = (scrollX: number, scrollY: number) => { + // remove previous splitline group + this.foregroundGroup.findById(KEY_GROUP_FROZEN_SPLIT_LINE)?.remove(); + if (this.enableFrozenFirstRow()) { + // 在分页条件下需要额外处理 Y 轴滚动值 + const relativeScrollY = Math.floor(scrollY - this.getPaginationScrollY()); + const splitLineGroup = this.foregroundGroup.addGroup({ + id: KEY_GROUP_FROZEN_SPLIT_LINE, + zIndex: FRONT_GROUND_GROUP_FROZEN_Z_INDEX, + }); + const style: SplitLine = get(this.cfg, 'spreadsheet.theme.splitLine'); + const horizontalBorderStyle = { + lineWidth: style?.horizontalBorderWidth, + stroke: style?.horizontalBorderColor, + opacity: style?.horizontalBorderColorOpacity, + }; + const { height: cornerHeight } = this.cornerBBox; + + const cellRange = this.getCellRange(); + const y = + cornerHeight + + this.getTotalHeightForRange(cellRange.start, cellRange.start); + const width = + this.panelBBox.viewportWidth + + this.layoutResult.rowsHierarchy.width + + this.getSeriesNumberWidth(); + renderLine( + splitLineGroup as Group, + { + x1: 0, + x2: width, + y1: y, + y2: y, + }, + { + ...horizontalBorderStyle, + }, + ); + + if (style.showShadow && relativeScrollY > 0) { + splitLineGroup.addShape('rect', { + attrs: { + x: 0, + y, + width, + height: style.shadowWidth, + fill: this.getShadowFill(90), + }, + }); + } + } + }; + + protected clip(scrollX: number, scrollY: number): void { + const { isFrozenRowHeader, frozenRowGroup } = this.spreadsheet; + if (!isFrozenRowHeader.call(this.spreadsheet)) { + // adapt: close the entire frozen header. + // 1. panelScrollGroup clip (default) + // 2. frozenRowGroup clip + this.panelScrollGroupClip(scrollX, scrollY); + if (this.enableFrozenFirstRow()) { + const paginationScrollY = this.getPaginationScrollY(); + frozenRowGroup.setClip({ + type: 'rect', + attrs: { + x: 0, + y: paginationScrollY, + width: this.panelBBox.width + scrollX, + height: frozenRowGroup.getBBox().height, + }, + }); + } + return; + } + super.clip(scrollX, scrollY); + } -export class PivotFacet extends BaseFacet { get rowCellTheme() { return this.spreadsheet.theme.rowCell.cell; } @@ -881,4 +997,22 @@ export class PivotFacet extends BaseFacet { }, }; } + + protected getRowHeader(): RowHeader { + if (!this.rowHeader) { + const { viewportHeight, ...otherProps } = this.getRowHeaderCfg(); + const { frozenRowHeight } = getFrozenRowCfgPivot( + this.cfg, + this.layoutResult.rowNodes, + ); + return new PivotRowHeader({ + ...otherProps, + viewportHeight: viewportHeight - frozenRowHeight, + }); + } + } + + public enableFrozenFirstRow(): boolean { + return !!this.getBizRevisedFrozenOptions().frozenRowCount; + } } diff --git a/packages/s2-core/src/facet/utils.ts b/packages/s2-core/src/facet/utils.ts index 5e8713520a..a02053cf1f 100644 --- a/packages/s2-core/src/facet/utils.ts +++ b/packages/s2-core/src/facet/utils.ts @@ -7,7 +7,11 @@ import type { ColumnNode, Columns, Pagination, + S2Options, + S2PivotSheetOptions, + S2TableSheetOptions, ScrollSpeedRatio, + SpreadSheetFacetCfg, } from '../common/interface'; import type { Fields } from '../common/interface'; import type { Indexes } from '../utils/indexes'; @@ -532,3 +536,41 @@ export const areAllFieldsEmpty = (fields: Fields) => { isEmpty(fields.customTreeItems) ); }; + +/** + * get frozen options pivot-sheet (business limit) + * @param options + * @returns + */ +export const getFrozenRowCfgPivot = ( + options: Pick< + S2Options, + 'frozenFirstRowPivot' | 'pagination' | 'hierarchyType' | 'showSeriesNumber' + >, + rowNodes: Node[], +): S2TableSheetOptions & { + frozenRowHeight: number; + enableFrozenFirstRow: boolean; +} => { + const { pagination, frozenFirstRowPivot, hierarchyType, showSeriesNumber } = + options; + const enablePagination = pagination && pagination.pageSize; + let enableFrozenFirstRow = false; + const headNode = rowNodes?.[0]; + if (!enablePagination && frozenFirstRowPivot) { + // first node no children: entire row + enableFrozenFirstRow = headNode?.children?.length === 0; + const treeMode = hierarchyType === 'tree' || hierarchyType === 'customTree'; + if (treeMode && !enableFrozenFirstRow) { + enableFrozenFirstRow = !showSeriesNumber; + } + } + return { + frozenRowCount: enableFrozenFirstRow ? 1 : 0, + frozenColCount: 0, + frozenTrailingColCount: 0, + frozenTrailingRowCount: 0, + enableFrozenFirstRow, + frozenRowHeight: enableFrozenFirstRow ? headNode.height : 0, + }; +}; diff --git a/packages/s2-core/src/interaction/root.ts b/packages/s2-core/src/interaction/root.ts index 03c4c66c56..b7293ca5c7 100644 --- a/packages/s2-core/src/interaction/root.ts +++ b/packages/s2-core/src/interaction/root.ts @@ -21,7 +21,7 @@ import type { S2CellType, SelectHeaderCellInfo, } from '../common/interface'; -import { ColHeader, RowHeader } from '../facet/header'; +import { ColHeader, RowHeader, SeriesNumberHeader } from '../facet/header'; import { Node } from '../facet/layout/node'; import type { SpreadSheet } from '../sheet-type'; import { getAllChildCells } from '../utils/get-all-child-cells'; @@ -219,7 +219,10 @@ export class RootInteraction { public getAllRowHeaderCells(): RowCell[] { const children = this.spreadsheet.foregroundGroup?.getChildren() || []; - const rowHeader = children.find((group) => group instanceof RowHeader); + const rowHeader = children.find( + (group) => + group instanceof RowHeader && !(group instanceof SeriesNumberHeader), // series cell not belong to row header cell + ); const headerChildren = rowHeader?.cfg?.children || []; return getAllChildCells(headerChildren, RowCell).filter( diff --git a/packages/s2-core/src/sheet-type/pivot-sheet.ts b/packages/s2-core/src/sheet-type/pivot-sheet.ts index 8ce4509308..ed49d65064 100644 --- a/packages/s2-core/src/sheet-type/pivot-sheet.ts +++ b/packages/s2-core/src/sheet-type/pivot-sheet.ts @@ -1,9 +1,11 @@ import type { Event as CanvasEvent } from '@antv/g-canvas'; import { clone, last } from 'lodash'; -import { DataCell } from '../cell'; +import { BaseCell, DataCell, SeriesNumberCell } from '../cell'; import { EXTRA_FIELD, InterceptType, + KEY_GROUP_PANEL_FROZEN_ROW, + PANEL_GROUP_FROZEN_GROUP_Z_INDEX, S2Event, getTooltipOperatorSortMenus, } from '../common/constant'; @@ -20,6 +22,7 @@ import { PivotDataSet } from '../data-set'; import { CustomTreePivotDataSet } from '../data-set/custom-tree-pivot-data-set'; import { PivotFacet } from '../facet'; import type { Node } from '../facet/layout/node'; +import { FrozenGroup } from '../group/frozen-group'; import { SpreadSheet } from './spread-sheet'; export class PivotSheet extends SpreadSheet { @@ -227,4 +230,21 @@ export class PivotSheet extends SpreadSheet { forceRender: true, }); } + + protected initPanelGroupChildren(): void { + super.initPanelGroupChildren(); + const commonParams = { + zIndex: PANEL_GROUP_FROZEN_GROUP_Z_INDEX, + s2: this, + }; + this.frozenRowGroup = new FrozenGroup({ + KEY_GROUP_PANEL_FROZEN_ROW, + ...commonParams, + }); + this.panelGroup.add(this.frozenRowGroup); + } + + protected isCellType(cell?: CanvasEvent['target']): boolean { + return cell instanceof BaseCell && !(cell instanceof SeriesNumberCell); + } } diff --git a/packages/s2-core/src/sheet-type/spread-sheet.ts b/packages/s2-core/src/sheet-type/spread-sheet.ts index f3d48faad9..6eb4ef9699 100644 --- a/packages/s2-core/src/sheet-type/spread-sheet.ts +++ b/packages/s2-core/src/sheet-type/spread-sheet.ts @@ -575,6 +575,10 @@ export abstract class SpreadSheet extends EE { return this.options?.mappingDisplayDataItem; } + protected isCellType(cell?: CanvasEvent['target']) { + return cell instanceof BaseCell; + } + // 获取当前cell实例 public getCell( target: CanvasEvent['target'], @@ -582,7 +586,7 @@ export abstract class SpreadSheet extends EE { let parent = target; // 一直索引到g顶层的canvas来检查是否在指定的cell中 while (parent && !(parent instanceof Canvas)) { - if (parent instanceof BaseCell) { + if (this.isCellType(parent)) { // 在单元格中,返回true return parent as T; } diff --git a/packages/s2-core/src/theme/index.ts b/packages/s2-core/src/theme/index.ts index 21ac42bc90..9270cc979b 100644 --- a/packages/s2-core/src/theme/index.ts +++ b/packages/s2-core/src/theme/index.ts @@ -227,7 +227,7 @@ export const getTheme = ( fill: basicColors[14], linkTextFill: basicColors[6], opacity: 1, - textBaseline: 'middle', + textBaseline: 'top', textAlign: 'center', }, measureText: { diff --git a/packages/s2-core/src/utils/indexes.ts b/packages/s2-core/src/utils/indexes.ts index 5dfdbfa7e7..122f83ce57 100644 --- a/packages/s2-core/src/utils/indexes.ts +++ b/packages/s2-core/src/utils/indexes.ts @@ -1,6 +1,6 @@ import { isEmpty } from 'lodash'; -export type Indexes = [number, number, number, number]; +export type Indexes = [number?, number?, number?, number?]; export type PanelIndexes = { center: Indexes; diff --git a/packages/s2-react/__tests__/spreadsheet/drill-down-spec.tsx b/packages/s2-react/__tests__/spreadsheet/drill-down-spec.tsx index fe1473813c..44e6e6faaf 100644 --- a/packages/s2-react/__tests__/spreadsheet/drill-down-spec.tsx +++ b/packages/s2-react/__tests__/spreadsheet/drill-down-spec.tsx @@ -41,7 +41,7 @@ const partDrillDownParams: SheetComponentsProps['partDrillDown'] = { const findDrillDownIcon = (instance: SpreadSheet) => { const rowHeaderActionIcons = get( - (instance.facet.rowHeader.getChildren() as RowCell[]).find( + (instance.facet.rowHeader.getChildren()[0].getChildren() as RowCell[]).find( (item) => item.getActualText() === '杭州', ), 'actionIcons', diff --git a/s2-site/docs/api/general/S2Options.zh.md b/s2-site/docs/api/general/S2Options.zh.md index 412d0e0c8a..551847d2bf 100644 --- a/s2-site/docs/api/general/S2Options.zh.md +++ b/s2-site/docs/api/general/S2Options.zh.md @@ -13,8 +13,8 @@ const s2Options = { } ``` -| 参数 | 类型 | 必选 | 默认值 | 功能描述 | -| -- | --- | -- | -- | --- | +| 参数 | 类型 | 必选 | 默认值 | 功能描述 | 版本 | +| -- | --- | -- | -- | --- | --- | | width | `number` | | 600 | 表格宽度 | | height | `number` | | 480 | 表格高度 | | debug | `boolean` | |`false` | 是否开启调试模式 | @@ -34,6 +34,7 @@ const s2Options = { | frozenColCount | `number` | | | 冻结列的数量,从左侧开始计数 (明细表有效) | | frozenTrailingRowCount | `number` | | | 冻结行数量,从底部开始计数 (明细表有效) | | frozenTrailingColCount | `number` | | | 冻结列的数量,从右侧开始计数 (明细表有效) | +| frozenFirstRowPivot | `boolean` | | `false` | 首行不存在子节点时, 冻结首行, 适用于聚合模式总计置于顶部冻结总计行, 树状模式冻结首行等场景 (透视表有效) | `@antv/s2@1.53.0` | | hdAdapter | `boolean` | | `true` | 是否开启高清屏适配,解决多屏切换,高清视网膜屏字体渲染模糊的问题。[查看更多](/manual/advanced/hd-adapter) | | mergedCellsInfo | [MergedCellInfo[][]](#mergedcellinfo) | | | 合并单元格信息 | | placeholder | `string \| (meta: Record) => string` | | | 空单元格的填充内容 | diff --git a/s2-site/docs/manual/basic/sheet-type/pivot-mode.en.md b/s2-site/docs/manual/basic/sheet-type/pivot-mode.en.md index 012453eb49..79ebcab4c8 100644 --- a/s2-site/docs/manual/basic/sheet-type/pivot-mode.en.md +++ b/s2-site/docs/manual/basic/sheet-type/pivot-mode.en.md @@ -96,3 +96,30 @@ pivotSheet.render(); ``` ​📊 View [the pivot table of the demo class](/examples/basic/pivot#grid) . + +### FrozenFirstRow @antv/s2@^1.53.0 new feature + +Translation: Currently, only the ability to freeze the first row is provided, which is different from freezing rows and columns in a detail table. Due to the complex layout caused by the grouping feature in a pivot table, and to ensure reasonable interaction, the following limitations are in place: + +The first row does not have any child nodes (suitable for scenarios where the total is placed at the top or for tree-like structures). +Pagination scenarios are not currently supported. To enable freezing of the first row, set frozenFirstRowPivot in s2Options configuration. + +```ts +const s2Options = { + frozenFirstRowPivot: boolean; + totals: { + row: { + showGrandTotals: true, + reverseLayout: true, + }, + }, +} +``` + +picture & demo: + +preview +preview + + + diff --git a/s2-site/docs/manual/basic/sheet-type/pivot-mode.zh.md b/s2-site/docs/manual/basic/sheet-type/pivot-mode.zh.md index cc6e38d1dc..0cebeac18d 100644 --- a/s2-site/docs/manual/basic/sheet-type/pivot-mode.zh.md +++ b/s2-site/docs/manual/basic/sheet-type/pivot-mode.zh.md @@ -96,3 +96,37 @@ s2.render(); ``` ​📊 查看 [类方式透视表示例](/examples/basic/pivot#grid) 和 [API 文档](/api/general/s2options)。 + +### 冻结首行 @antv/s2@^1.53.0 新增 + +:::info{title=""} + +目前仅提供**冻结首行**能力,和[明细表行列冻结](https://s2.antv.antgroup.com/manual/basic/sheet-type/table-mode#%E8%A1%8C%E5%88%97%E5%86%BB%E7%BB%93)不同, 透视表由于带有分组的特性, 布局比较复杂, 考虑到交互合理性, 目前有如下限制 + +- 首行不存在子节点 (适用于总计置于顶部, 树状模式等场景)。 +- 分页场景暂不支持。 +`s2Options` 中配置 `frozenFirstRowPivot` 开启首行冻结能力 + +::: + +```ts +const s2Options = { + // 是否开启冻结首行 + frozenFirstRowPivot: boolean; + // 平铺模式,需要开启行总计 & 位置置顶 + totals: { + row: { + showGrandTotals: true, + reverseLayout: true, + }, + }, +} +``` + +#### 平铺模式 + + + +#### 树状模式 + + diff --git a/s2-site/examples/interaction/advanced/demo/frozen-pivot-grid.ts b/s2-site/examples/interaction/advanced/demo/frozen-pivot-grid.ts new file mode 100644 index 0000000000..b5f352bf28 --- /dev/null +++ b/s2-site/examples/interaction/advanced/demo/frozen-pivot-grid.ts @@ -0,0 +1,24 @@ +import { PivotSheet } from '@antv/s2'; + +fetch( + 'https://gw.alipayobjects.com/os/bmw-prod/2a5dbbc8-d0a7-4d02-b7c9-34f6ca63cff6.json', +) + .then((res) => res.json()) + .then((dataCfg) => { + const container = document.getElementById('container'); + + const s2Options = { + width: 600, + height: 300, + frozenFirstRowPivot: true, + totals: { + row: { + showGrandTotals: true, + reverseLayout: true, + }, + }, + }; + const s2 = new PivotSheet(container, dataCfg, s2Options); + + s2.render(); + }); diff --git a/s2-site/examples/interaction/advanced/demo/frozen-pivot-tree.ts b/s2-site/examples/interaction/advanced/demo/frozen-pivot-tree.ts new file mode 100644 index 0000000000..b5bd94ae5f --- /dev/null +++ b/s2-site/examples/interaction/advanced/demo/frozen-pivot-tree.ts @@ -0,0 +1,19 @@ +import { PivotSheet } from '@antv/s2'; + +fetch( + 'https://gw.alipayobjects.com/os/bmw-prod/2a5dbbc8-d0a7-4d02-b7c9-34f6ca63cff6.json', +) + .then((res) => res.json()) + .then((dataCfg) => { + const container = document.getElementById('container'); + + const s2Options = { + width: 600, + height: 300, + hierarchyType: 'tree', + frozenFirstRowPivot: true, + }; + const s2 = new PivotSheet(container, dataCfg, s2Options); + + s2.render(); + }); diff --git a/s2-site/examples/interaction/advanced/demo/meta.json b/s2-site/examples/interaction/advanced/demo/meta.json index d68eaec408..bdc80aaa85 100644 --- a/s2-site/examples/interaction/advanced/demo/meta.json +++ b/s2-site/examples/interaction/advanced/demo/meta.json @@ -107,6 +107,22 @@ "en": "Reaching the boundary of a scrolling area" }, "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/JRAt1kb93/Kapture%2525202022-06-06%252520at%25252011.28.43.gif" + }, + { + "filename": "frozen-pivot-grid.ts", + "title": { + "zh": "透视表 - 平铺模式冻结首行", + "en": "pivot mode freezes head rows" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ge0_S5iMB-wAAAAAAAAAAAAADmJ7AQ/original" + }, + { + "filename": "frozen-pivot-tree.ts", + "title": { + "zh": "透视表 - 树状模式冻结首行", + "en": "tree mode freezes head rows" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ncdCT7NB2I0AAAAAAAAAAAAADmJ7AQ/original" } ] }