Skip to content

Commit

Permalink
feat: 交叉表支持冻结首行能力 (#2419)
Browse files Browse the repository at this point in the history
* fix: 修复交叉模式下 行序号位置不正确&总计行未添加行序号 (#2412)

fix: fix row series number position and height bug

Co-authored-by: wuding.why <[email protected]>

* refactor: extract freeze public logic

* feat: the pivot table supports freezing the first row as a whole

* fix: lint error fixed

* fix: test case optimize

---------

Co-authored-by: wuhaiyang <[email protected]>
Co-authored-by: wuding.why <[email protected]>
  • Loading branch information
3 people authored Nov 17, 2023
1 parent fdf75ba commit be5e32e
Show file tree
Hide file tree
Showing 34 changed files with 1,892 additions and 795 deletions.
104 changes: 104 additions & 0 deletions packages/s2-core/__tests__/unit/facet/header/frozen-row-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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,
showSeriesNumber: true,
}),
...DEFAULT_STYLE,
height: 480,
width: 400,
cellCfg: {
width: 200,
height: 50,
},
});
describe('Frozen Row Header Test', () => {
let facet;

test.each(['grid', 'tree'])(
'frozen row header group api',
(hierarchyType: 'grid' | 'tree') => {
s2.setOptions({ hierarchyType });
s2.render();
facet = s2.facet as PivotFacet;

expect(facet.rowHeader instanceof FrozenRowHeader).toBeTrue();
expect(facet.rowHeader.frozenHeadGroup).toBeTruthy();
expect(facet.rowHeader.scrollGroup).toBeTruthy();

expect(facet.rowHeader.frozenHeadGroup.getChildren()).toHaveLength(1);
const frozenRowCell = facet.rowHeader.frozenHeadGroup.getChildren()[0];

expect(frozenRowCell instanceof RowCell).toBeTrue();
expect(frozenRowCell.meta.height).toEqual(30);

expect(facet.rowHeader.scrollGroup.getChildren()).toHaveLength(3);
const scrollCell = facet.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(rowHeader.getFrozenRowCount()).toBe(1);
},
);
});

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 seriesNumberCell =
facet.rowIndexHeader.frozenHeadGroup.getChildren();
expect(seriesNumberCell).toHaveLength(1);

expect(
facet.rowIndexHeader.scrollGroup.getChildren()[0] instanceof
SeriesNumberCell,
).toBe(true);

expect(seriesNumberCell[0] instanceof SeriesNumberCell).toBe(true);

expect(seriesNumberCell[0].meta.height).toBe(30);
},
);
});
120 changes: 117 additions & 3 deletions packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@ 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,
FrozenRowHeader,
RowHeader,
} from '@/facet/header';
import type { Fields, ViewMeta } from '@/common/interface/basic';
import { RootInteraction } from '@/interaction/root';
import { areAllFieldsEmpty } from '@/facet/utils';
import { areAllFieldsEmpty, getFrozenOptionsPivot } from '@/facet/utils';
import { FrozenGroup } from '@/common/constant';

jest.mock('@/interaction/root');

Expand Down Expand Up @@ -249,7 +256,9 @@ describe('Pivot Mode Facet Test', () => {
test('get header after render', () => {
const { rowHeader, cornerHeader, columnHeader, centerFrame } = facet;

expect(rowHeader instanceof RowHeader).toBeTrue();
expect(
rowHeader instanceof FrozenRowHeader || rowHeader instanceof RowHeader,
).toBeTrue();
expect(rowHeader.cfg.children).toHaveLength(10);
expect(rowHeader.cfg.visible).toBeTrue();

Expand Down Expand Up @@ -475,3 +484,108 @@ 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;
});
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
101 changes: 101 additions & 0 deletions packages/s2-core/src/cell/frozen-row-cell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {
KEY_GROUP_ROW_RESIZE_AREA,
ResizeAreaEffect,
ResizeDirectionType,
} from '../common/constant';
import {
getOrCreateResizeAreaGroupById,
getResizeAreaAttrs,
shouldAddResizeArea,
} from '../utils/interaction/resize';
import type { BaseHeaderConfig } from '../facet/header/base';
import { RowCell } from './row-cell';

export class FrozenRowCell extends RowCell {
private frozenRowHeight: number;

protected handleRestOptions(
...[headerConfig, ...options]: [BaseHeaderConfig, number]
) {
super.handleRestOptions(headerConfig, options);
this.frozenRowHeight = options[0];
}

protected drawResizeAreaInLeaf(): void {
if (
!this.meta.isLeaf ||
!this.shouldDrawResizeAreaByType('rowCellVertical', this)
) {
return;
}

const { x, y, width, height } = this.getCellArea();
const resizeStyle = this.getResizeAreaStyle();
const resizeArea = getOrCreateResizeAreaGroupById(
this.spreadsheet,
KEY_GROUP_ROW_RESIZE_AREA,
);

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,
};

if (
!shouldAddResizeArea(resizeAreaBBox, resizeClipAreaBBox, {
scrollX,
scrollY,
})
) {
return;
}

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,
},
});
}
}
2 changes: 1 addition & 1 deletion packages/s2-core/src/cell/header-cell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export abstract class HeaderCell extends BaseCell<Node> {

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;
Expand Down
4 changes: 4 additions & 0 deletions packages/s2-core/src/cell/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -22,4 +24,6 @@ export {
CornerCell,
BaseCell,
HeaderCell,
FrozenRowCell,
SeriesNumberCell,
};
Loading

0 comments on commit be5e32e

Please sign in to comment.