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 index 1acb772798..5297b77ad9 100644 --- a/packages/s2-core/__tests__/unit/facet/header/frozen-row-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/header/frozen-row-spec.ts @@ -1,104 +1,73 @@ import { createPivotSheet } from 'tests/util/helpers'; -import { assembleDataCfg, assembleOptions } from 'tests/util'; -import { DEFAULT_STYLE } from '@/common'; - -import { FrozenRowCell, RowCell, SeriesNumberCell } from '@/cell'; -import type { PivotFacet } from '@/facet'; -import { FrozenRowHeader } from '@/facet/header'; -import { FrozenSeriesNumber } from '@/facet/header/frozen-series-number'; - -const s2 = createPivotSheet({ - ...assembleDataCfg().fields, - valueInCols: true, - ...assembleOptions({ - frozenEntireHeadRowPivot: true, - totals: { - row: { - showGrandTotals: true, - reverseLayout: true, - }, - }, - frozenColCount: 2, - frozenRowCount: 2, - frozenTrailingColCount: 2, - frozenTrailingRowCount: 2, +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, - }), - ...DEFAULT_STYLE, - height: 480, - width: 400, - cellCfg: { - width: 200, - height: 50, }, -}); -describe('Frozen Row Header Test', () => { - let facet; + { useSimpleData: false }, +); +describe('Frozen Row Header Test', () => { test.each(['grid', 'tree'])( 'frozen row header group api', (hierarchyType: 'grid' | 'tree') => { s2.setOptions({ hierarchyType }); s2.render(); - facet = s2.facet as PivotFacet; + const rowHeader: PivotRowHeader = s2.facet.rowHeader as PivotRowHeader; - expect(facet.rowHeader instanceof FrozenRowHeader).toBeTrue(); - expect(facet.rowHeader.frozenHeadGroup).toBeTruthy(); - expect(facet.rowHeader.scrollGroup).toBeTruthy(); + expect(rowHeader instanceof PivotRowHeader).toBeTrue(); + expect(rowHeader.frozenHeadGroup).toBeTruthy(); + expect(rowHeader.scrollGroup).toBeTruthy(); - expect(facet.rowHeader.frozenHeadGroup.getChildren()).toHaveLength(1); - const frozenRowCell = facet.rowHeader.frozenHeadGroup.getChildren()[0]; + expect(rowHeader.frozenHeadGroup.getChildren()).toHaveLength(1); + const frozenRowCell = rowHeader.frozenHeadGroup.getChildren()[0]; - expect(frozenRowCell instanceof RowCell).toBeTrue(); - expect(frozenRowCell.meta.height).toEqual(30); + expect(frozenRowCell instanceof FrozenRowCell).toBeTrue(); + expect(get(frozenRowCell, 'meta.height')).toEqual(30); - expect(facet.rowHeader.scrollGroup.getChildren()).toHaveLength(3); - const scrollCell = facet.rowHeader.scrollGroup.getChildren()[0]; + expect(rowHeader.scrollGroup.getChildren()).toHaveLength(10); + const scrollCell = rowHeader.scrollGroup.getChildren()[0]; expect(scrollCell instanceof FrozenRowCell).toBeTrue(); - expect(frozenRowCell.meta.height).toEqual(30); - - const rowHeader = facet.rowHeader; - expect(rowHeader.getFrozenRowHeight()).toBe(30); - - expect( - rowHeader.isFrozenRow({ - rowIndex: 0, - }), - ).toBe(true); - expect( - rowHeader.isFrozenRow({ - rowIndex: -1, - }), - ).toBe(false); + expect(get(frozenRowCell, 'meta.height')).toEqual(30); - expect(rowHeader.getFrozenRowCount()).toBe(1); + expect(rowHeader.getFrozenFirstRowHeight()).toBe(30); }, ); }); describe('Frozen Series Number Test', () => { - let facet; test.each(['grid', 'tree'])( 'series number test', (hierarchyType: 'grid' | 'tree') => { s2.setOptions({ hierarchyType }); s2.render(); - facet = s2.facet as PivotFacet; - expect(facet.rowIndexHeader instanceof FrozenSeriesNumber).toBe(true); + const rowIndexHeader: SeriesNumberHeader = s2.facet + .rowIndexHeader as SeriesNumberHeader; + expect(rowIndexHeader instanceof SeriesNumberHeader).toBe(true); - const seriesNumberCell = - facet.rowIndexHeader.frozenHeadGroup.getChildren(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const seriesNumberCell = rowIndexHeader.frozenHeadGroup.getChildren(); expect(seriesNumberCell).toHaveLength(1); expect( - facet.rowIndexHeader.scrollGroup.getChildren()[0] instanceof - SeriesNumberCell, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + rowIndexHeader.scrollGroup.getChildren()[0] instanceof SeriesNumberCell, ).toBe(true); expect(seriesNumberCell[0] instanceof SeriesNumberCell).toBe(true); - expect(seriesNumberCell[0].meta.height).toBe(30); + 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 db005b81c3..2f252f516b 100644 --- a/packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts @@ -2,30 +2,27 @@ * pivot mode pivot test. */ import { Canvas, Group } from '@antv/g-canvas'; +import { find, get, size } from 'lodash'; import { assembleDataCfg, assembleOptions } from 'tests/util'; -import { size, get, find } from 'lodash'; +import { PivotRowHeader } from '../../../src/facet/header/pivot-row'; import { getMockPivotMeta } from './util'; -import { Node } from '@/facet/layout/node'; -import { DEFAULT_TREE_ROW_WIDTH } from '@/common/constant/options'; -import type { PanelScrollGroup } from '@/group/panel-scroll-group'; -import { SpreadSheet } from '@/sheet-type'; -import { PivotDataSet } from '@/data-set/pivot-data-set'; -import { PivotFacet } from '@/facet/pivot-facet'; 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, - FrozenRowHeader, - RowHeader, -} from '@/facet/header'; + DEFAULT_OPTIONS, + DEFAULT_STYLE, + DEFAULT_TREE_ROW_WIDTH, +} from '@/common/constant/options'; import type { Fields, ViewMeta } from '@/common/interface/basic'; +import { Store } from '@/common/store'; +import { PivotDataSet } from '@/data-set/pivot-data-set'; +import { ColHeader, CornerHeader, Frame, RowHeader } from '@/facet/header'; +import { Node } from '@/facet/layout/node'; +import { PivotFacet } from '@/facet/pivot-facet'; +import { areAllFieldsEmpty } from '@/facet/utils'; +import type { PanelScrollGroup } from '@/group/panel-scroll-group'; import { RootInteraction } from '@/interaction/root'; -import { areAllFieldsEmpty, getFrozenOptionsPivot } from '@/facet/utils'; -import { FrozenGroup } from '@/common/constant'; +import { SpreadSheet } from '@/sheet-type'; +import { getTheme } from '@/theme'; jest.mock('@/interaction/root'); @@ -259,9 +256,10 @@ describe('Pivot Mode Facet Test', () => { const { rowHeader, cornerHeader, columnHeader, centerFrame } = facet; expect( - rowHeader instanceof FrozenRowHeader || rowHeader instanceof RowHeader, + rowHeader instanceof PivotRowHeader || rowHeader instanceof RowHeader, ).toBeTrue(); - expect(rowHeader.cfg.children).toHaveLength(10); + + expect(rowHeader.cfg.children[0].getChildren()).toHaveLength(10); expect(rowHeader.cfg.visible).toBeTrue(); expect(cornerHeader instanceof CornerHeader).toBeTrue(); @@ -276,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(); @@ -486,108 +483,3 @@ describe('Pivot Mode Facet Test', () => { expect(facet.getHiddenColumnsInfo(node)).toEqual(hiddenColumnsInfo); }); }); - -describe.each(['grid', 'tree'])( - 'Pivot Mode Facet frozen entire row', - (hierarchyType: 'grid' | 'tree') => { - const s2: SpreadSheet = new MockSpreadSheet(); - const dataSet: PivotDataSet = new MockPivotDataSet(s2); - s2.dataSet = dataSet; - s2.interaction = new RootInteraction(s2); - const facet: PivotFacet = new PivotFacet({ - spreadsheet: s2, - dataSet, - dataCell: (fct) => new DataCell(fct, s2), - ...assembleDataCfg().fields, - valueInCols: true, - ...assembleOptions({ - frozenEntireHeadRowPivot: true, - totals: { - row: { - showGrandTotals: true, - reverseLayout: true, - }, - }, - frozenColCount: 2, - frozenRowCount: 2, - frozenTrailingColCount: 2, - frozenTrailingRowCount: 2, - }), - ...DEFAULT_STYLE, - hierarchyType, - }); - - test('should get correct frozenOptions', () => { - expect(getFrozenOptionsPivot(facet.cfg)).toStrictEqual({ - frozenRowCount: 1, - frozenColCount: 0, - frozenTrailingColCount: 0, - frozenTrailingRowCount: 0, - }); - - expect( - getFrozenOptionsPivot({ - ...{ - ...facet.cfg, - showSeriesNumber: true, - }, - }), - ).toStrictEqual({ - frozenRowCount: 1, - frozenColCount: 0, - frozenTrailingColCount: 0, - frozenTrailingRowCount: 0, - }); - }); - - test('should get correct frozenInfo', () => { - facet.calculateFrozenGroupInfo(); - expect(facet.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', () => { - expect(facet.calculateXYIndexes(0, 0)).toStrictEqual({ - center: [0, 3, 1, 7], - frozenCol: [0, -1, 1, 7], - frozenRow: [0, 3, 0, 0], - frozenTrailingCol: [4, 3, 1, 7], - frozenTrailingRow: [0, 3, 8, 7], - }); - expect(facet.calculateXYIndexes(110, 30)).toStrictEqual({ - center: [1, 3, 2, 7], - frozenCol: [0, -1, 2, 7], - frozenRow: [1, 3, 0, 0], - frozenTrailingCol: [4, 3, 2, 7], - frozenTrailingRow: [1, 3, 8, 7], - }); - }); - - test('should get correct indexes with row height gt canvas height', () => { - const originHeight = facet.panelBBox.viewportHeight; - facet.panelBBox.viewportHeight = 10; - expect(facet.calculateXYIndexes(0, 0)).toStrictEqual({ - center: [0, 3, 1, 0], - frozenCol: [0, -1, 1, 0], - frozenRow: [0, 3, 0, 0], - frozenTrailingCol: [4, 3, 1, 0], - frozenTrailingRow: [0, 3, 8, 7], - }); - // reset - facet.panelBBox.viewportHeight = originHeight; - }); - }, -); diff --git a/packages/s2-core/src/cell/frozen-row-cell.ts b/packages/s2-core/src/cell/frozen-row-cell.ts index a0f78e5c79..3728072c39 100644 --- a/packages/s2-core/src/cell/frozen-row-cell.ts +++ b/packages/s2-core/src/cell/frozen-row-cell.ts @@ -1,101 +1,80 @@ -import { - KEY_GROUP_ROW_RESIZE_AREA, - ResizeAreaEffect, - ResizeDirectionType, -} from '../common/constant'; -import { - getOrCreateResizeAreaGroupById, - getResizeAreaAttrs, - shouldAddResizeArea, -} from '../utils/interaction/resize'; +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 { - private frozenRowHeight: number; + /** + * 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, number] + ...[headerConfig, ...options]: [BaseHeaderConfig, boolean] ) { super.handleRestOptions(headerConfig, options); - this.frozenRowHeight = options[0]; + this.frozenRowCell = options[0]; } - protected drawResizeAreaInLeaf(): void { + protected getAdjustTextAreaHeight( + textArea: SimpleBBox, + scrollY: number, + viewportHeight: number, + ): number { + const correctY = textArea.y - this.getFrozenFirstRowHeight(); + let adjustTextAreaHeight = textArea.height; if ( - !this.meta.isLeaf || - !this.shouldDrawResizeAreaByType('rowCellVertical', this) + !this.spreadsheet.facet.vScrollBar && + correctY + textArea.height > scrollY + viewportHeight ) { - return; + adjustTextAreaHeight = scrollY + viewportHeight - correctY; } + return adjustTextAreaHeight; + } - const { x, y, width, height } = this.getCellArea(); - const resizeStyle = this.getResizeAreaStyle(); - const resizeArea = getOrCreateResizeAreaGroupById( - this.spreadsheet, - KEY_GROUP_ROW_RESIZE_AREA, + 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, ); + } - const { - position, - seriesNumberWidth, - width: headerWidth, - viewportHeight: headerHeight, - scrollX, - scrollY, - } = this.headerConfig; - - // const frozenRowHeight = this.getFrozenRowHeight(); - const resizeAreaBBox = { - // fix: When horizontally scrolling and closing the entire frozen header, the resize area is being removed prematurely. - x: x + seriesNumberWidth, - // packages/s2-core/src/facet/header/frozen-row.ts The y-coordinate has been decreased by the height of the frozen rows. need plus frozenRowHeight - y: y + this.frozenRowHeight + height - resizeStyle.size / 2, - width, - height: resizeStyle.size, - }; - - const resizeClipAreaBBox = { - x: 0, - // There are frozen rows, so the clip should start from the position of the frozen rows. - y: this.frozenRowHeight, - width: headerWidth, - height: headerHeight, + protected getResizeClipAreaBBox(): SimpleBBox { + return { + ...super.getResizeClipAreaBBox(), + y: this.getFrozenFirstRowHeight(), }; + } - if ( - !shouldAddResizeArea(resizeAreaBBox, resizeClipAreaBBox, { - scrollX, - scrollY, - }) - ) { - return; + private getFrozenFirstRowHeight(): number { + if (this.frozenRowCell) { + // frozen row cell + return 0; } - - const offsetX = position?.x + x - scrollX + seriesNumberWidth; - const offsetY = position?.y + y + this.frozenRowHeight - scrollY; - - const resizeAreaWidth = this.spreadsheet.isFrozenRowHeader() - ? headerWidth - seriesNumberWidth - (x - scrollX) - : width; - - resizeArea.addShape('rect', { - attrs: { - ...getResizeAreaAttrs({ - id: this.meta.id, - theme: resizeStyle, - type: ResizeDirectionType.Vertical, - effect: ResizeAreaEffect.Cell, - offsetX, - offsetY, - width, - height, - meta: this.meta, - }), - x: offsetX, - y: offsetY + height - resizeStyle.size / 2, - width: resizeAreaWidth, - }, - }); + 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/row-cell.ts b/packages/s2-core/src/cell/row-cell.ts index b588a21f42..f26f76ccb7 100644 --- a/packages/s2-core/src/cell/row-cell.ts +++ b/packages/s2-core/src/cell/row-cell.ts @@ -256,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 || @@ -275,25 +285,19 @@ export class RowCell extends HeaderCell { position, seriesNumberWidth, width: headerWidth, - viewportHeight: headerHeight, scrollX, scrollY, } = this.headerConfig; const resizeAreaBBox = { - // fix: When horizontally scrolling and closing the entire frozen header, the resize area is being removed prematurely. + // 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 index 0b6e9fb8c1..99193ef260 100644 --- a/packages/s2-core/src/cell/series-number-cell.ts +++ b/packages/s2-core/src/cell/series-number-cell.ts @@ -1,7 +1,8 @@ -import type { Condition, MappingResult, TextTheme } from '../common'; -import { RowCell } from './row-cell'; +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 RowCell { +export class SeriesNumberCell extends FrozenRowCell { protected initCell(): void { this.drawBackgroundShape(); this.drawRectBorder(); @@ -9,8 +10,19 @@ export class SeriesNumberCell extends RowCell { } protected getTextStyle(): TextTheme { - const style = super.getTextStyle(); - return { ...style, textAlign: 'center' }; + return this.getStyle()?.seriesText; + } + + protected getTextPosition(): Point { + return super.getTextPosition(); + } + + protected getActionIconsCount(): number { + return 0; + } + + public getIconStyle(): IconTheme { + return undefined; } protected drawResizeAreaInLeaf(): void {} diff --git a/packages/s2-core/src/common/interface/s2Options.ts b/packages/s2-core/src/common/interface/s2Options.ts index 6b156caaa4..2b0e1f134a 100644 --- a/packages/s2-core/src/common/interface/s2Options.ts +++ b/packages/s2-core/src/common/interface/s2Options.ts @@ -126,8 +126,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 1ed736367f..5e4a596674 100644 --- a/packages/s2-core/src/facet/base-facet.ts +++ b/packages/s2-core/src/facet/base-facet.ts @@ -1274,14 +1274,14 @@ export abstract class BaseFacet { return this.cornerHeader; } - protected getSeriesNumberHeader(): BaseHeader { - return SeriesNumberHeader.getSeriesNumberHeader( - this.panelBBox, - this.getSeriesNumberWidth(), - this.layoutResult.rowsHierarchy.getNodes(0), - this.spreadsheet, - this.cornerBBox.width, - ); + protected getSeriesNumberHeader(): SeriesNumberHeader { + 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 54eb82dc47..52c278bc83 100644 --- a/packages/s2-core/src/facet/frozen-facet.ts +++ b/packages/s2-core/src/facet/frozen-facet.ts @@ -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 index 154f7e021e..d7f34ef3a2 100644 --- a/packages/s2-core/src/facet/header/base-frozen-row.ts +++ b/packages/s2-core/src/facet/header/base-frozen-row.ts @@ -1,8 +1,7 @@ import type { IGroup } from '@antv/g-canvas'; -import { get } from 'lodash'; import type { Node } from '../layout/node'; import { - getFrozenOptionsPivot, + getFrozenRowCfgPivot, translateGroup, translateGroupX, } from '../utils'; @@ -15,9 +14,9 @@ import { import { RowHeader, type RowHeaderConfig } from './row'; export class BaseFrozenRowHeader extends RowHeader { - protected scrollGroup: IGroup; + public scrollGroup: IGroup; - protected frozenHeadGroup: IGroup; + public frozenHeadGroup: IGroup; constructor(cfg: RowHeaderConfig) { super(cfg); @@ -47,7 +46,8 @@ export class BaseFrozenRowHeader extends RowHeader { protected rowCellInRectYDir(item: Node): boolean { const { viewportHeight, scrollY } = this.headerConfig; - return viewportHeight + scrollY > item.y && scrollY < item.y + item.height; + const itemY = item.y - this.getFrozenFirstRowHeight(); + return viewportHeight + scrollY > itemY && scrollY < itemY + item.height; } protected rowCellInRect(item: Node): boolean { @@ -68,26 +68,21 @@ export class BaseFrozenRowHeader extends RowHeader { protected offset() { const { scrollX, scrollY, position, seriesNumberWidth } = this.headerConfig; // 向右多移动的seriesNumberWidth是序号的宽度 - translateGroup( - this.scrollGroup, - position.x - scrollX + seriesNumberWidth, - position.y + this.getFrozenRowHeight() - scrollY, - ); - translateGroupX( - this.frozenHeadGroup, - position.x - scrollX + 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: { - // 由于多移动了seriesNumberWidth跨度,所有需要向左切。 - 是反向剪裁(右 -> 左) - x: scrollX - seriesNumberWidth, - y: scrollY, + x: clipX, + y: scrollY + this.getFrozenFirstRowHeight(), width, height: viewportHeight, }, @@ -95,44 +90,38 @@ export class BaseFrozenRowHeader extends RowHeader { this.frozenHeadGroup.setClip({ type: 'rect', attrs: { - x: scrollX - seriesNumberWidth, + x: clipX, y: 0, width: this.headerConfig.width, - height: this.getFrozenRowHeight(), + height: this.getFrozenFirstRowHeight(), }, }); } - protected getFrozenRowHeight = () => { - // get row head height TODO: There may be a better solution - const frozenCount = this.getFrozenRowCount(); - let sum = 0; - for (let i = 0, len = frozenCount; i < len; i++) { - sum += get( - this.headerConfig.spreadsheet, - `facet.layoutResult.rowNodes[${i}].height`, - 0, - ); - } - return sum; - }; - - protected isFrozenRow(item: Node): boolean { - const frozenRowCount = this.getFrozenRowCount(); + 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 ); } - protected getFrozenRowCount(): number { + public getFrozenFirstRowHeight(): number { const { spreadsheet } = this.headerConfig; - const { frozenRowCount } = getFrozenOptionsPivot(spreadsheet.facet?.cfg); - return frozenRowCount; + const { facet } = spreadsheet; + const { frozenRowHeight } = getFrozenRowCfgPivot( + spreadsheet.options, + facet.layoutResult?.rowNodes, + ); + return frozenRowHeight; } public clear(): void { this.frozenHeadGroup.clear(); this.scrollGroup.clear(); - // super.clear(); } } diff --git a/packages/s2-core/src/facet/header/frozen-row.ts b/packages/s2-core/src/facet/header/frozen-row.ts deleted file mode 100644 index de65f96891..0000000000 --- a/packages/s2-core/src/facet/header/frozen-row.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { Node } from '../layout/node'; -import { FrozenRowCell, RowCell } from '../../cell'; -import { BaseFrozenRowHeader } from './base-frozen-row'; - -export class FrozenRowHeader extends BaseFrozenRowHeader { - // To avoid the "performance impact" of traverse the data to adjust the y-coordinate, we can correct the y-coordinate to determine if a cell is within the visible viewport. - protected rowCellInRectYDir(item: Node): boolean { - const { viewportHeight, scrollY } = this.headerConfig; - const itemY = item.y - this.getFrozenRowHeight(); - return viewportHeight + scrollY > itemY && scrollY < itemY + item.height; - } - - protected onItemReady(item: Node): Node { - if (!this.isFrozenRow(item)) { - // relative to scroll group position, scroll group position translateY frozenHeight - return Object.assign({}, item, { y: item.y - this.getFrozenRowHeight() }); - } - return item; - } - - protected createCellInstance(item: Node): RowCell { - const { spreadsheet } = this.headerConfig; - if (this.isFrozenRow(item)) { - return new RowCell(item, spreadsheet, { - ...this.headerConfig, - // prevent the text in the frozen header row from having a sticky effect. - scrollY: 0, - }); - } - return new FrozenRowCell( - item, - spreadsheet, - this.headerConfig, - this.getFrozenRowHeight(), - ); - } -} diff --git a/packages/s2-core/src/facet/header/frozen-series-number.ts b/packages/s2-core/src/facet/header/frozen-series-number.ts deleted file mode 100644 index 13cd25c390..0000000000 --- a/packages/s2-core/src/facet/header/frozen-series-number.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { RowCell, SeriesNumberCell } from '../../cell'; -import type { SpreadSheet } from '../../sheet-type/index'; -import type { PanelBBox } from '../bbox/panelBBox'; -import { Node } from '../layout/node'; -import { BaseFrozenRowHeader } from './base-frozen-row'; - -export class FrozenSeriesNumber extends BaseFrozenRowHeader { - /** - * Get seriesNumber header by config - * @param viewportBBox - * @param seriesNumberWidth - * @param leafNodes - * @param spreadsheet - * @param cornerWidth - */ - - public static getFrozenSeriesNumberHeader({ - viewportBBox, - seriesNumberWidth, - leafNodes, - spreadsheet, - cornerWidth, - frozenRowCount, - frozenRowHeight, - }: { - viewportBBox: PanelBBox; - seriesNumberWidth: number; - leafNodes: Node[]; - spreadsheet: SpreadSheet; - cornerWidth: number; - frozenRowCount: number; - frozenRowHeight: number; - }): FrozenSeriesNumber { - 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 - const frozenRow = frozenRowCount > 0 && node.rowIndex < frozenRowCount; - const sNode = new Node({ - id: '', - key: '', - value: `${seriesNodes.length + 1}`, - }); - sNode.x = node.x; - sNode.y = frozenRow ? node.y : node.y - frozenRowHeight; - sNode.height = isHierarchyTreeType - ? node.getTotalHeightForTreeHierarchy() - : node.height; - sNode.width = seriesNumberWidth; - sNode.rowIndex = node.rowIndex; - seriesNodes.push(sNode); - }); - return new FrozenSeriesNumber({ - width: cornerWidth, - height, - viewportWidth: cornerWidth, - viewportHeight: - frozenRowCount > 0 ? 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.facet.cfg.hierarchyType, - linkFields: [], - }); - } - - 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, - }); - return cell; - } -} diff --git a/packages/s2-core/src/facet/header/index.ts b/packages/s2-core/src/facet/header/index.ts index 331b71c8c2..67966fc0d5 100644 --- a/packages/s2-core/src/facet/header/index.ts +++ b/packages/s2-core/src/facet/header/index.ts @@ -3,4 +3,4 @@ export { CornerHeader, type CornerHeaderConfig } from './corner'; export { Frame } from './frame'; export { RowHeader, type RowHeaderConfig } from './row'; export { SeriesNumberHeader } from './series-number'; -export { FrozenRowHeader } from './frozen-row'; +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 2f26cec30f..753f5ff5cc 100644 --- a/packages/s2-core/src/facet/header/row.ts +++ b/packages/s2-core/src/facet/header/row.ts @@ -72,11 +72,11 @@ export class RowHeader extends BaseHeader { // 如果外部没处理,就用默认的 if (isEmpty(cell)) { if (spreadsheet.isPivotMode()) { - cell = this.createCellInstance(newItem); + cell = this.createCellInstance(item); } } - newItem.belongsCell = cell; - const group = this.getCellGroup(newItem); + item.belongsCell = 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 71ef105671..2957aa99f3 100644 --- a/packages/s2-core/src/facet/pivot-facet.ts +++ b/packages/s2-core/src/facet/pivot-facet.ts @@ -46,10 +46,8 @@ 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 { getFrozenOptionsPivot } from './utils'; -import { FrozenRowHeader, RowHeader } from './header'; -import { FrozenSeriesNumber } from './header/frozen-series-number'; -import type { BaseHeader, BaseHeaderConfig } from './header/base'; +import { getFrozenRowCfgPivot } from './utils'; +import { PivotRowHeader, RowHeader } from './header'; export class PivotFacet extends FrozenFacet { protected updateFrozenGroupGrid(): void { @@ -75,13 +73,13 @@ export class PivotFacet extends FrozenFacet { } protected getBizRevisedFrozenOptions(): S2TableSheetOptions { - return getFrozenOptionsPivot(this.cfg); + 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.enableFrozenTotalRow()) { + if (this.enableFrozenFirstRow()) { // 在分页条件下需要额外处理 Y 轴滚动值 const relativeScrollY = Math.floor(scrollY - this.getPaginationScrollY()); const splitLineGroup = this.foregroundGroup.addGroup({ @@ -138,7 +136,7 @@ export class PivotFacet extends FrozenFacet { // 1. panelScrollGroup clip (default) // 2. frozenRowGroup clip this.panelScrollGroupClip(scrollX, scrollY); - if (this.enableFrozenTotalRow()) { + if (this.enableFrozenFirstRow()) { const paginationScrollY = this.getPaginationScrollY(); frozenRowGroup.setClip({ type: 'rect', @@ -155,15 +153,6 @@ export class PivotFacet extends FrozenFacet { super.clip(scrollX, scrollY); } - public getFrozenRowHeight = (): number => { - const { frozenRowCount } = this.getBizRevisedFrozenOptions(); - let sum = 0; - for (let i = 0, len = frozenRowCount; i < len; i++) { - sum += get(this.layoutResult, `rowNodes[${i}].height`, 0); - } - return sum; - }; - get rowCellTheme() { return this.spreadsheet.theme.rowCell.cell; } @@ -1003,33 +992,19 @@ export class PivotFacet extends FrozenFacet { protected getRowHeader(): RowHeader { if (!this.rowHeader) { - if (this.enableFrozenTotalRow()) { - const { viewportHeight, ...otherProps } = this.getRowHeaderCfg(); - return new FrozenRowHeader({ - ...otherProps, - viewportHeight: viewportHeight - this.getFrozenRowHeight(), - }); - } - } - return super.getRowHeader(); - } - - protected getSeriesNumberHeader(): BaseHeader { - if (this.enableFrozenTotalRow()) { - return FrozenSeriesNumber.getFrozenSeriesNumberHeader({ - viewportBBox: this.panelBBox, - seriesNumberWidth: this.getSeriesNumberWidth(), - leafNodes: this.layoutResult.rowsHierarchy.getNodes(0), - spreadsheet: this.spreadsheet, - cornerWidth: this.cornerBBox.width, - frozenRowCount: this.getBizRevisedFrozenOptions().frozenRowCount, - frozenRowHeight: this.getFrozenRowHeight(), + const { viewportHeight, ...otherProps } = this.getRowHeaderCfg(); + const { frozenRowHeight } = getFrozenRowCfgPivot( + this.cfg, + this.layoutResult.rowNodes, + ); + return new PivotRowHeader({ + ...otherProps, + viewportHeight: viewportHeight - frozenRowHeight, }); } - return super.getSeriesNumberHeader(); } - public enableFrozenTotalRow(): boolean { + 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 520957141e..a9946b47ff 100644 --- a/packages/s2-core/src/facet/utils.ts +++ b/packages/s2-core/src/facet/utils.ts @@ -1,21 +1,20 @@ import type { IGroup, SimpleBBox } from '@antv/g-canvas'; import { findIndex, isEmpty, isNil } from 'lodash'; - import type { FrozenCellIndex, FrozenOpts } from '../common/constant/frozen'; import { FrozenCellType } from '../common/constant/frozen'; +import { DEFAULT_PAGE_INDEX } from '../common/constant/pagination'; import type { ColumnNode, Columns, + Fields, Pagination, + S2Options, S2TableSheetOptions, ScrollSpeedRatio, - SpreadSheetFacetCfg, } from '../common/interface'; -import type { Fields } from '../common/interface'; import type { Indexes } from '../utils/indexes'; -import { DEFAULT_PAGE_INDEX } from '../common/constant/pagination'; -import type { Node } from './layout/node'; import type { ViewCellHeights } from './layout/interface'; +import type { Node } from './layout/node'; export const isFrozenCol = (colIndex: number, frozenCount: number) => { return frozenCount > 0 && colIndex < frozenCount; @@ -540,46 +539,35 @@ export const areAllFieldsEmpty = (fields: Fields) => { * @param options * @returns */ -export const getFrozenOptionsPivot = ( +export const getFrozenRowCfgPivot = ( options: Pick< - SpreadSheetFacetCfg, - | 'hierarchyType' - | 'totals' - | 'pagination' - | 'frozenEntireHeadRowPivot' - | 'showSeriesNumber' - | 'valueInCols' + S2Options, + 'frozenFirstRowPivot' | 'pagination' | 'hierarchyType' | 'showSeriesNumber' >, -): S2TableSheetOptions => { - const { - totals, - valueInCols, - pagination, - frozenEntireHeadRowPivot, - hierarchyType, - showSeriesNumber, - } = options; - let frozenRowCount = 0; - const { showGrandTotals, reverseLayout } = totals?.row || {}; - const grandTotalInHeadRow = showGrandTotals && reverseLayout; + rowNodes: Node[], +): S2TableSheetOptions & { + frozenRowHeight: number; + enableFrozenFirstRow: boolean; +} => { + const { pagination, frozenFirstRowPivot, hierarchyType, showSeriesNumber } = + options; const enablePagination = pagination && pagination.pageSize; - if (enablePagination || !frozenEntireHeadRowPivot) { - frozenRowCount = 0; - } else if (hierarchyType === 'grid') { - if (grandTotalInHeadRow && valueInCols) { - frozenRowCount = 1; - } - } else if (hierarchyType === 'tree') { - frozenRowCount = 1; - if (showSeriesNumber && !grandTotalInHeadRow) { - frozenRowCount = 0; + 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, + 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 56d7e5db44..6bb14a2730 100644 --- a/packages/s2-core/src/sheet-type/pivot-sheet.ts +++ b/packages/s2-core/src/sheet-type/pivot-sheet.ts @@ -244,7 +244,7 @@ export class PivotSheet extends SpreadSheet { this.panelGroup.add(this.frozenRowGroup); } - protected isCellType(cell?: any): boolean { + 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 d8a870f05c..6eb4ef9699 100644 --- a/packages/s2-core/src/sheet-type/spread-sheet.ts +++ b/packages/s2-core/src/sheet-type/spread-sheet.ts @@ -575,7 +575,7 @@ export abstract class SpreadSheet extends EE { return this.options?.mappingDisplayDataItem; } - protected isCellType(cell?: any) { + protected isCellType(cell?: CanvasEvent['target']) { return cell instanceof BaseCell; } 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-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 9762c153a1..79a72b2f94 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 index 46d31e372b..b5f352bf28 100644 --- a/s2-site/examples/interaction/advanced/demo/frozen-pivot-grid.ts +++ b/s2-site/examples/interaction/advanced/demo/frozen-pivot-grid.ts @@ -10,7 +10,7 @@ fetch( const s2Options = { width: 600, height: 300, - frozenEntireHeadRowPivot: true, + frozenFirstRowPivot: true, totals: { row: { showGrandTotals: true, diff --git a/s2-site/examples/interaction/advanced/demo/frozen-pivot-tree.ts b/s2-site/examples/interaction/advanced/demo/frozen-pivot-tree.ts index ecfb40a893..b5bd94ae5f 100644 --- a/s2-site/examples/interaction/advanced/demo/frozen-pivot-tree.ts +++ b/s2-site/examples/interaction/advanced/demo/frozen-pivot-tree.ts @@ -11,7 +11,7 @@ fetch( width: 600, height: 300, hierarchyType: 'tree', - frozenEntireHeadRowPivot: true, + frozenFirstRowPivot: true, }; const s2 = new PivotSheet(container, dataCfg, s2Options); diff --git a/s2-site/examples/interaction/advanced/demo/meta.json b/s2-site/examples/interaction/advanced/demo/meta.json index 39576b6ffb..bdc80aaa85 100644 --- a/s2-site/examples/interaction/advanced/demo/meta.json +++ b/s2-site/examples/interaction/advanced/demo/meta.json @@ -111,18 +111,18 @@ { "filename": "frozen-pivot-grid.ts", "title": { - "zh": "交叉平铺模式冻结总计行", - "en": "pivot mode freezes total rows" + "zh": "透视表 - 平铺模式冻结首行", + "en": "pivot mode freezes head rows" }, - "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/JRAt1kb93/Kapture%2525202022-06-06%252520at%25252011.28.43.gif" + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ge0_S5iMB-wAAAAAAAAAAAAADmJ7AQ/original" }, { "filename": "frozen-pivot-tree.ts", "title": { - "zh": "交叉树状模式冻结首行", + "zh": "透视表 - 树状模式冻结首行", "en": "tree mode freezes head rows" }, - "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/JRAt1kb93/Kapture%2525202022-06-06%252520at%25252011.28.43.gif" + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ncdCT7NB2I0AAAAAAAAAAAAADmJ7AQ/original" } ] }