diff --git a/packages/s2-core/__tests__/README.md b/packages/s2-core/__tests__/README.md
new file mode 100644
index 0000000000..71ef229c35
--- /dev/null
+++ b/packages/s2-core/__tests__/README.md
@@ -0,0 +1,15 @@
+### 调试单测
+
+如果你想查看单测的运行结果,除了常规的 `pnpm core:test` 和 `pnpm react:test` 来运行测试之外,还可以 `可视化的调试单测(基于 jest-electron)`, 可以更快的发现单测的问题。
+
+1. 选择单测
+
+命令行运行 `pnpm core:start` 或者 `pnpm react:start`
+
+
+
+2. 查看结果
+
+因为本质上就是一个浏览器,如果单测结果不符合预期,可以正常打断点进行调试,快速分析原因。
+
+
diff --git a/packages/s2-core/__tests__/spreadsheet/__snapshots__/spread-sheet-facet-layout-api-spec.ts.snap b/packages/s2-core/__tests__/spreadsheet/__snapshots__/spread-sheet-facet-layout-api-spec.ts.snap
index 31ea0d6095..cf6dc7c50d 100644
--- a/packages/s2-core/__tests__/spreadsheet/__snapshots__/spread-sheet-facet-layout-api-spec.ts.snap
+++ b/packages/s2-core/__tests__/spreadsheet/__snapshots__/spread-sheet-facet-layout-api-spec.ts.snap
@@ -256,7 +256,7 @@ Array [
},
Object {
"field": "",
- "id": "",
+ "id": "1",
"value": "1",
},
Object {
@@ -430,7 +430,7 @@ Array [
]
`;
-exports[`Facet Layout API Tests PivotSheet CornerCell #getCornerCell()s 1`] = `
+exports[`Facet Layout API Tests PivotSheet CornerCell #getCornerCells() 1`] = `
Array [
Object {
"field": "province",
@@ -694,7 +694,7 @@ exports[`Facet Layout API Tests PivotSheet SeriesNumberCell #getSeriesNumberCell
Array [
Object {
"field": "",
- "id": "",
+ "id": "1",
"value": "1",
},
]
@@ -704,7 +704,7 @@ exports[`Facet Layout API Tests PivotSheet SeriesNumberCell #getSeriesNumberNode
Array [
Object {
"field": "",
- "id": "",
+ "id": "1",
"value": "1",
},
]
diff --git a/packages/s2-core/__tests__/spreadsheet/corner-spec.ts b/packages/s2-core/__tests__/spreadsheet/corner-spec.ts
index 45dac87339..0b83b0e131 100644
--- a/packages/s2-core/__tests__/spreadsheet/corner-spec.ts
+++ b/packages/s2-core/__tests__/spreadsheet/corner-spec.ts
@@ -251,7 +251,10 @@ describe('PivotSheet Corner Tests', () => {
const getCellSpy = jest.spyOn(s2, 'getCell').mockImplementation(() => {
return {
- getMeta: () => node,
+ getMeta: () => ({
+ ...node,
+ cornerType: CornerNodeType.Row,
+ }),
} as unknown as S2CellType;
});
const selected = jest.fn();
@@ -286,7 +289,10 @@ describe('PivotSheet Corner Tests', () => {
jest.spyOn(s2, 'getCell').mockImplementationOnce(() => {
return {
- getMeta: () => node,
+ getMeta: () => ({
+ ...node,
+ cornerType: CornerNodeType.Row,
+ }),
} as unknown as S2CellType;
});
const selected = jest.fn();
@@ -308,7 +314,10 @@ describe('PivotSheet Corner Tests', () => {
jest.spyOn(s2, 'showTooltipWithInfo').mockImplementationOnce(() => {});
jest.spyOn(s2, 'getCell').mockImplementationOnce(() => {
return {
- getMeta: () => node,
+ getMeta: () => ({
+ ...node,
+ cornerType: CornerNodeType.Row,
+ }),
} as unknown as S2CellType;
});
diff --git a/packages/s2-core/__tests__/spreadsheet/interaction-brush-selection-scroll-spec.ts b/packages/s2-core/__tests__/spreadsheet/interaction-brush-selection-scroll-spec.ts
index 86f24da64e..e3fae89f97 100644
--- a/packages/s2-core/__tests__/spreadsheet/interaction-brush-selection-scroll-spec.ts
+++ b/packages/s2-core/__tests__/spreadsheet/interaction-brush-selection-scroll-spec.ts
@@ -233,7 +233,7 @@ describe('PivotSheet Brush Selection Scroll Tests', () => {
);
await s2.render();
- await sleep(20); // wait for anthor loop;
+ await sleep(500); // wait for anthor loop;
const rowCell = s2.facet.getRowCells()[0];
@@ -245,7 +245,7 @@ describe('PivotSheet Brush Selection Scroll Tests', () => {
} as any);
await emitBrushEvent(s2, 200, 200);
- await sleep(500);
+ await sleep(1000);
expect(s2.facet.getScrollOffset().scrollY).toBeGreaterThan(0);
expect(s2.interaction.getCells()).not.toBeEmpty();
diff --git a/packages/s2-core/__tests__/spreadsheet/interaction-corner-click-spec.ts b/packages/s2-core/__tests__/spreadsheet/interaction-corner-click-spec.ts
index 6eb3aa1eda..51af0504ab 100644
--- a/packages/s2-core/__tests__/spreadsheet/interaction-corner-click-spec.ts
+++ b/packages/s2-core/__tests__/spreadsheet/interaction-corner-click-spec.ts
@@ -3,9 +3,12 @@ import {
createPivotSheet,
getContainer,
} from 'tests/util/helpers';
-import { customRowGridSimpleFields } from '../data/custom-grid-simple-fields';
+import {
+ customColGridSimpleFields,
+ customRowGridSimpleFields,
+} from '../data/custom-grid-simple-fields';
import { CustomGridData } from '../data/data-custom-grid';
-import { S2Event } from '@/common/constant';
+import { S2Event, SERIES_NUMBER_FIELD } from '@/common/constant';
import { CornerNodeType, type S2Options } from '@/common/interface';
import type { GEvent, HierarchyType, S2DataConfig } from '@/index';
import { PivotSheet, SpreadSheet } from '@/sheet-type';
@@ -23,12 +26,12 @@ describe('Interaction Corner Cell Click Tests', () => {
});
test.each(['grid', 'tree'] as HierarchyType[])(
- 'should not selected row cell when col corner cell clicked for %s mode',
+ 'should selected row cell when row corner cell clicked for %s mode',
async (hierarchyType) => {
jest.spyOn(SpreadSheet.prototype, 'getCell').mockImplementationOnce(
() =>
- createMockCellInfo('testId', {
- cornerType: CornerNodeType.Col,
+ createMockCellInfo('city', {
+ cornerType: CornerNodeType.Row,
}).mockCell,
);
@@ -44,22 +47,30 @@ describe('Interaction Corner Cell Click Tests', () => {
s2.emit(S2Event.CORNER_CELL_CLICK, {} as unknown as GEvent);
- expect(reset).toHaveBeenCalledTimes(1);
- expect(s2.interaction.isSelectedState()).toBeFalsy();
+ expect(reset).not.toHaveBeenCalled();
+ expect(s2.interaction.getActiveCells()).toHaveLength(
+ s2.isHierarchyTreeType() ? 3 : 2,
+ );
+ expect(s2.interaction.isSelectedState()).toBeTruthy();
},
);
test.each(['grid', 'tree'] as HierarchyType[])(
- 'should selected row cell when row corner cell clicked for %s mode',
+ 'should selected custom row cell when row corner cell clicked for %s mode',
async (hierarchyType) => {
jest.spyOn(SpreadSheet.prototype, 'getCell').mockImplementationOnce(
() =>
- createMockCellInfo('city', {
+ createMockCellInfo('a-1-1', {
cornerType: CornerNodeType.Row,
}).mockCell,
);
- s2 = createPivotSheet({
+ const customRowDataCfg: S2DataConfig = {
+ data: CustomGridData,
+ fields: customRowGridSimpleFields,
+ };
+
+ s2 = new PivotSheet(getContainer(), customRowDataCfg, {
...s2Options,
hierarchyType,
});
@@ -71,27 +82,65 @@ describe('Interaction Corner Cell Click Tests', () => {
s2.emit(S2Event.CORNER_CELL_CLICK, {} as unknown as GEvent);
+ const gridFields = ['a-1-1', 'a-1-2'];
+ const treeFields = [
+ 'a-1',
+ 'a-1-1',
+ 'measure-1',
+ 'measure-2',
+ 'a-1-2',
+ 'a-2',
+ ];
+
expect(reset).not.toHaveBeenCalled();
- expect(s2.interaction.getActiveCells()).toHaveLength(
- s2.isHierarchyTreeType() ? 3 : 2,
+ expect(
+ s2.interaction.getActiveCells().map((cell) => cell.getMeta().field),
+ ).toEqual(s2.isHierarchyTreeType() ? treeFields : gridFields);
+ expect(s2.interaction.isSelectedState()).toBeTruthy();
+ },
+ );
+
+ test.each(['grid', 'tree'] as HierarchyType[])(
+ 'should selected col cell when col corner cell clicked for %s mode',
+ async (hierarchyType) => {
+ jest.spyOn(SpreadSheet.prototype, 'getCell').mockImplementationOnce(
+ () =>
+ createMockCellInfo('type', {
+ cornerType: CornerNodeType.Col,
+ }).mockCell,
);
+
+ s2 = createPivotSheet({
+ ...s2Options,
+ hierarchyType,
+ });
+ await s2.render();
+
+ const reset = jest
+ .spyOn(s2.interaction, 'reset')
+ .mockImplementationOnce(() => {});
+
+ s2.emit(S2Event.CORNER_CELL_CLICK, {} as unknown as GEvent);
+
+ expect(reset).not.toHaveBeenCalled();
+ expect(s2.interaction.getActiveCells()).toHaveLength(1);
expect(s2.interaction.isSelectedState()).toBeTruthy();
},
);
test.each(['grid', 'tree'] as HierarchyType[])(
- 'should selected custom row cell when row corner cell clicked for %s mode',
+ 'should selected custom col cell when row corner cell clicked for %s mode',
async (hierarchyType) => {
jest.spyOn(SpreadSheet.prototype, 'getCell').mockImplementationOnce(
() =>
- createMockCellInfo('a-1-1', {
- cornerType: CornerNodeType.Row,
+ createMockCellInfo('a-1', {
+ cornerType: CornerNodeType.Col,
}).mockCell,
);
const customRowDataCfg: S2DataConfig = {
data: CustomGridData,
- fields: customRowGridSimpleFields,
+ fields: customColGridSimpleFields,
};
s2 = new PivotSheet(getContainer(), customRowDataCfg, {
@@ -106,20 +155,39 @@ describe('Interaction Corner Cell Click Tests', () => {
s2.emit(S2Event.CORNER_CELL_CLICK, {} as unknown as GEvent);
- const gridFields = ['a-1-1', 'a-1-2'];
- const treeFields = [
- 'a-1',
- 'a-1-1',
- 'measure-1',
- 'measure-2',
- 'a-1-2',
- 'a-2',
- ];
-
expect(reset).not.toHaveBeenCalled();
expect(
s2.interaction.getActiveCells().map((cell) => cell.getMeta().field),
- ).toEqual(s2.isHierarchyTreeType() ? treeFields : gridFields);
+ ).toEqual(['a-1', 'a-2']);
+ expect(s2.interaction.isSelectedState()).toBeTruthy();
+ },
+ );
+
+ test.each(['grid', 'tree'] as HierarchyType[])(
+ 'should selected series cell when series corner cell clicked for %s mode',
+ async (hierarchyType) => {
+ jest.spyOn(SpreadSheet.prototype, 'getCell').mockImplementationOnce(
+ () =>
+ createMockCellInfo(SERIES_NUMBER_FIELD, {
+ cornerType: CornerNodeType.Series,
+ }).mockCell,
+ );
+
+ s2 = createPivotSheet({
+ ...s2Options,
+ seriesNumber: { enable: true },
+ hierarchyType,
+ });
+ await s2.render();
+
+ const reset = jest
+ .spyOn(s2.interaction, 'reset')
+ .mockImplementationOnce(() => {});
+
+ s2.emit(S2Event.CORNER_CELL_CLICK, {} as unknown as GEvent);
+
+ expect(reset).not.toHaveBeenCalled();
+ expect(s2.interaction.getActiveCells()).toHaveLength(1);
expect(s2.interaction.isSelectedState()).toBeTruthy();
},
);
diff --git a/packages/s2-core/__tests__/spreadsheet/interaction-multi-selection-spec.ts b/packages/s2-core/__tests__/spreadsheet/interaction-multi-selection-spec.ts
index c7763a0714..6cfd7af2e3 100644
--- a/packages/s2-core/__tests__/spreadsheet/interaction-multi-selection-spec.ts
+++ b/packages/s2-core/__tests__/spreadsheet/interaction-multi-selection-spec.ts
@@ -11,8 +11,9 @@ import {
getSelectedSum,
getTestTooltipData,
} from '../util/interaction';
+import { CellType, InteractionStateName } from '../../src';
import { PivotSheet, SpreadSheet } from '@/sheet-type';
-import type { S2Options } from '@/common/interface';
+import type { HierarchyType, S2Options } from '@/common/interface';
const s2Options: S2Options = {
width: 600,
@@ -22,6 +23,16 @@ const s2Options: S2Options = {
},
};
+const highlightCellConfig: Array<{
+ hierarchyType: HierarchyType;
+ stateName: InteractionStateName;
+}> = [
+ { hierarchyType: 'tree', stateName: InteractionStateName.HOVER },
+ { hierarchyType: 'tree', stateName: InteractionStateName.SELECTED },
+ { hierarchyType: 'grid', stateName: InteractionStateName.HOVER },
+ { hierarchyType: 'grid', stateName: InteractionStateName.SELECTED },
+];
+
describe('Interaction Multi Selection Tests', () => {
let s2: SpreadSheet;
@@ -197,4 +208,86 @@ describe('Interaction Multi Selection Tests', () => {
).toBeTruthy();
});
});
+
+ test.each(highlightCellConfig)(
+ 'should highlight relevancy header cell after selected data cell by %s mode',
+ async ({ hierarchyType, stateName }) => {
+ s2.setOptions({
+ hierarchyType,
+ interaction: {
+ selectedCellHighlight: true,
+ hoverHighlight: true,
+ },
+ seriesNumber: { enable: true },
+ });
+ await s2.render(false);
+
+ const dataCell = s2.facet.getDataCells()[0];
+
+ s2.interaction.updateDataCellRelevantHeaderCells(
+ stateName,
+ dataCell.getMeta(),
+ );
+
+ expect(s2.interaction.getInteractedCells()).toHaveLength(
+ s2.isHierarchyTreeType() ? 4 : 5,
+ );
+ },
+ );
+
+ test.each(highlightCellConfig)(
+ 'should highlight relevancy row cell after selected data cell by %s mode',
+ async ({ hierarchyType, stateName }) => {
+ s2.setOptions({
+ hierarchyType,
+ interaction: {
+ selectedCellHighlight: true,
+ hoverHighlight: true,
+ },
+ seriesNumber: { enable: true },
+ });
+ await s2.render(false);
+
+ const dataCell = s2.facet.getDataCells()[0];
+
+ s2.interaction.updateDataCellRelevantRowCells(
+ stateName,
+ dataCell.getMeta(),
+ );
+
+ const interactedCells = s2.interaction
+ .getInteractedCells()
+ .filter((cell) => cell.cellType === CellType.ROW_CELL);
+
+ expect(interactedCells).toHaveLength(s2.isHierarchyTreeType() ? 2 : 3);
+ },
+ );
+
+ test.each(highlightCellConfig)(
+ 'should highlight relevancy row cell after selected data cell by %s mode',
+ async ({ hierarchyType, stateName }) => {
+ s2.setOptions({
+ hierarchyType,
+ interaction: {
+ selectedCellHighlight: true,
+ hoverHighlight: true,
+ },
+ seriesNumber: { enable: true },
+ });
+ await s2.render(false);
+
+ const dataCell = s2.facet.getDataCells()[0];
+
+ s2.interaction.updateDataCellRelevantColCells(
+ stateName,
+ dataCell.getMeta(),
+ );
+
+ const interactedCells = s2.interaction
+ .getInteractedCells()
+ .filter((cell) => cell.cellType === CellType.COL_CELL);
+
+ expect(interactedCells).toHaveLength(2);
+ },
+ );
});
diff --git a/packages/s2-core/__tests__/spreadsheet/spread-sheet-facet-layout-api-spec.ts b/packages/s2-core/__tests__/spreadsheet/spread-sheet-facet-layout-api-spec.ts
index 8cf314bad5..1eea671955 100644
--- a/packages/s2-core/__tests__/spreadsheet/spread-sheet-facet-layout-api-spec.ts
+++ b/packages/s2-core/__tests__/spreadsheet/spread-sheet-facet-layout-api-spec.ts
@@ -99,7 +99,7 @@ describe('Facet Layout API Tests', () => {
expect(mapNodes(s2.facet.getCornerNodes())).toMatchSnapshot();
});
- test('#getCornerCell()s', () => {
+ test('#getCornerCells()', () => {
expect(mapCells(s2.facet.getCornerCells())).toMatchSnapshot();
});
diff --git a/packages/s2-core/__tests__/spreadsheet/spread-sheet-tree-mode-spec.ts b/packages/s2-core/__tests__/spreadsheet/spread-sheet-tree-mode-spec.ts
index 2861bae844..0134c6e6c9 100644
--- a/packages/s2-core/__tests__/spreadsheet/spread-sheet-tree-mode-spec.ts
+++ b/packages/s2-core/__tests__/spreadsheet/spread-sheet-tree-mode-spec.ts
@@ -1,6 +1,6 @@
import * as mockDataConfig from 'tests/data/simple-data.json';
import { createPivotSheet, getContainer } from 'tests/util/helpers';
-import { PivotSheet } from '../../src';
+import { CornerNodeType, PivotSheet } from '../../src';
import type { S2DataConfig, S2Options } from '@/common';
const s2Options: S2Options = {
@@ -17,7 +17,7 @@ describe('SpreadSheet Tree Mode Tests', () => {
});
afterEach(() => {
- container?.remove();
+ // container?.remove();
});
describe('Facet Tests', () => {
@@ -90,5 +90,25 @@ describe('SpreadSheet Tree Mode Tests', () => {
expect(cornerCell.getTextShapes()).toHaveLength(1);
expect(cornerCell.isMultiLineText()).toBeFalsy();
});
+
+ // https://github.com/antvis/S2/issues/2563
+ test('should render correctly tree icon position in row cell', async () => {
+ const s2 = createPivotSheet({
+ ...s2Options,
+ width: 300,
+ seriesNumber: {
+ enable: true,
+ },
+ });
+
+ await s2.render();
+
+ const [seriesNumberCell, rowCell] = s2.facet
+ .getCornerCells()
+ .filter((cell) => cell.getMeta().cornerType !== CornerNodeType.Col);
+
+ expect(seriesNumberCell.getTreeIcon()).toBeFalsy();
+ expect(rowCell.getTreeIcon()).toBeTruthy();
+ });
});
});
diff --git a/packages/s2-core/__tests__/unit/interaction/base-interaction/click/corner-cell-click-spec.ts b/packages/s2-core/__tests__/unit/interaction/base-interaction/click/corner-cell-click-spec.ts
index 08168ba37f..cb3f451460 100644
--- a/packages/s2-core/__tests__/unit/interaction/base-interaction/click/corner-cell-click-spec.ts
+++ b/packages/s2-core/__tests__/unit/interaction/base-interaction/click/corner-cell-click-spec.ts
@@ -3,7 +3,11 @@ import {
createMockCellInfo,
sleep,
} from 'tests/util/helpers';
-import { CellType, InteractionStateName, type Node } from '../../../../../src';
+import {
+ CornerNodeType,
+ InteractionStateName,
+ type Node,
+} from '../../../../../src';
import { InterceptType, S2Event } from '@/common/constant';
import type { HierarchyType, S2Options } from '@/common/interface';
import type { GEvent } from '@/index';
@@ -14,7 +18,9 @@ jest.mock('@/interaction/event-controller');
describe('Interaction Corner Cell Click Tests', () => {
let s2: SpreadSheet;
- const mockCellInfo = createMockCellInfo('testId');
+ const mockCellInfo = createMockCellInfo('testId', {
+ cornerType: CornerNodeType.Row,
+ });
let cornerCellClick: CornerCellClick;
beforeEach(() => {
@@ -68,7 +74,6 @@ describe('Interaction Corner Cell Click Tests', () => {
{
colIndex: -1,
rowIndex: -1,
- type: CellType.ROW_CELL,
id: mockCellInfo.mockCellMeta['id'],
},
],
diff --git a/packages/s2-core/__tests__/unit/interaction/base-interaction/hover-spec.ts b/packages/s2-core/__tests__/unit/interaction/base-interaction/hover-spec.ts
index 53a5c36dc8..29b29fc180 100644
--- a/packages/s2-core/__tests__/unit/interaction/base-interaction/hover-spec.ts
+++ b/packages/s2-core/__tests__/unit/interaction/base-interaction/hover-spec.ts
@@ -113,8 +113,13 @@ describe('Interaction Hover Tests', () => {
});
test('should trigger data cell hover depend on separate config', async () => {
- s2.facet.getColCells = jest.fn();
- s2.facet.getRowCells = jest.fn();
+ s2.facet.getRowCells().forEach((cell) => {
+ jest.spyOn(cell, 'update').mockImplementationOnce(() => {});
+ });
+
+ s2.facet.getColCells().forEach((cell) => {
+ jest.spyOn(cell, 'update').mockImplementationOnce(() => {});
+ });
s2.setOptions({
interaction: {
@@ -126,6 +131,7 @@ describe('Interaction Hover Tests', () => {
});
s2.emit(S2Event.DATA_CELL_HOVER, { target: {} } as GEvent);
+
expect(s2.interaction.getState()).toEqual({
cells: [mockCellMeta],
stateName: InteractionStateName.HOVER,
@@ -138,8 +144,13 @@ describe('Interaction Hover Tests', () => {
stateName: InteractionStateName.HOVER_FOCUS,
});
- expect(s2.facet.getColCells).toHaveBeenCalled();
- expect(s2.facet.getRowCells).not.toHaveBeenCalled();
+ s2.facet.getColCells().forEach((cell) => {
+ expect(cell.update).toHaveBeenCalled();
+ });
+
+ s2.facet.getRowCells().forEach((cell) => {
+ expect(cell.update).not.toHaveBeenCalled();
+ });
});
test('should not trigger data cell hover when hover cell not change', () => {
diff --git a/packages/s2-core/__tests__/unit/utils/interaction/hover-event-spec.ts b/packages/s2-core/__tests__/unit/utils/interaction/hover-event-spec.ts
index 589b1c90d4..d6fc55570a 100644
--- a/packages/s2-core/__tests__/unit/utils/interaction/hover-event-spec.ts
+++ b/packages/s2-core/__tests__/unit/utils/interaction/hover-event-spec.ts
@@ -1,6 +1,6 @@
import type { Node } from '@/facet/layout/node';
import {
- getActiveHoverRowColCells,
+ getActiveHoverHeaderCells,
updateAllColHeaderCellState,
} from '@/utils/interaction/hover-event';
import type { SpreadSheet } from '@/sheet-type/spread-sheet';
@@ -32,13 +32,13 @@ describe('Hover Event Utils Tests', () => {
const cells = [
new ColCell({} as unknown as Node, {} as unknown as SpreadSheet),
];
- let result = getActiveHoverRowColCells('root[&]city', cells, false);
+ let result = getActiveHoverHeaderCells('root[&]city', cells, false);
expect(result.map((cell) => cell.getMeta()?.id)).toStrictEqual([
'root[&]city',
]);
- result = getActiveHoverRowColCells('root[&]city', cells, true);
+ result = getActiveHoverHeaderCells('root[&]city', cells, true);
expect(result.map((cell) => cell.getMeta()?.id)).toStrictEqual([
'root[&]city',
]);
diff --git a/packages/s2-core/src/cell/corner-cell.ts b/packages/s2-core/src/cell/corner-cell.ts
index 4a41ced6d3..3b45b1eacd 100644
--- a/packages/s2-core/src/cell/corner-cell.ts
+++ b/packages/s2-core/src/cell/corner-cell.ts
@@ -54,10 +54,10 @@ export class CornerCell extends HeaderCell {
}
/**
- * 绘制折叠展开的icon
+ * 绘制折叠展开的 icon
*/
protected drawTreeIcon() {
- if (!this.showTreeIcon() || this.meta.cornerType === CornerNodeType.Col) {
+ if (!this.showTreeIcon()) {
return;
}
@@ -191,8 +191,11 @@ export class CornerCell extends HeaderCell {
}
protected showTreeIcon() {
- // 批量折叠或者展开的icon,只存在树状结构的第一个cell前
- return this.spreadsheet.isHierarchyTreeType() && this.meta?.x === 0;
+ // 批量折叠或者展开的 icon,渲染在行头对应的角头中
+ return (
+ this.spreadsheet.isHierarchyTreeType() &&
+ this.meta.cornerType === CornerNodeType.Row
+ );
}
protected getTreeIconWidth() {
diff --git a/packages/s2-core/src/cell/data-cell.ts b/packages/s2-core/src/cell/data-cell.ts
index d226657365..a2b3726865 100644
--- a/packages/s2-core/src/cell/data-cell.ts
+++ b/packages/s2-core/src/cell/data-cell.ts
@@ -178,10 +178,14 @@ export class DataCell extends BaseCell {
}
}
+ protected isDisableHover(cellMeta: CellMeta) {
+ return cellMeta.type !== CellType.DATA_CELL;
+ }
+
protected handleHover(cells: CellMeta[]) {
- const currentHoverCell = first(cells) as CellMeta;
+ const hoverCellMeta = first(cells) as CellMeta;
- if (currentHoverCell.type !== CellType.DATA_CELL) {
+ if (this.isDisableHover(hoverCellMeta)) {
this.hideInteractionShape();
return;
@@ -197,8 +201,8 @@ export class DataCell extends BaseCell {
// 当视图内的 cell 行列 index 与 hover 的 cell 一致,绘制hover的十字样式
if (
- (currentCol && currentColIndex === currentHoverCell?.colIndex) ||
- (currentRow && currentRowIndex === currentHoverCell?.rowIndex)
+ (currentCol && currentColIndex === hoverCellMeta?.colIndex) ||
+ (currentRow && currentRowIndex === hoverCellMeta?.rowIndex)
) {
this.updateByState(InteractionStateName.HOVER);
} else {
@@ -211,9 +215,9 @@ export class DataCell extends BaseCell {
// fix issue: https://github.com/antvis/S2/issues/1781
if (
- isEqual(currentHoverCell.id, id) &&
- isEqual(currentHoverCell.rowIndex, rowIndex) &&
- isEqual(currentHoverCell.colIndex, colIndex)
+ isEqual(hoverCellMeta.id, id) &&
+ isEqual(hoverCellMeta.rowIndex, rowIndex) &&
+ isEqual(hoverCellMeta.colIndex, colIndex)
) {
this.updateByState(InteractionStateName.HOVER_FOCUS);
}
diff --git a/packages/s2-core/src/cell/header-cell.ts b/packages/s2-core/src/cell/header-cell.ts
index 63fb383aa6..ae1bbe9224 100644
--- a/packages/s2-core/src/cell/header-cell.ts
+++ b/packages/s2-core/src/cell/header-cell.ts
@@ -442,6 +442,7 @@ export abstract class HeaderCell<
CellType.CORNER_CELL,
CellType.COL_CELL,
CellType.ROW_CELL,
+ CellType.SERIES_NUMBER_CELL,
]);
if (!first(cells)) {
@@ -479,7 +480,7 @@ export abstract class HeaderCell<
return [EXTRA_FIELD, EXTRA_COLUMN_FIELD].includes(this.meta.field);
}
- mappingValue(
+ public mappingValue(
condition: Condition,
): ConditionMappingResult {
const value = this.getMeta().value;
diff --git a/packages/s2-core/src/cell/series-number-cell.ts b/packages/s2-core/src/cell/series-number-cell.ts
index 79b20b51a8..73636e01b3 100644
--- a/packages/s2-core/src/cell/series-number-cell.ts
+++ b/packages/s2-core/src/cell/series-number-cell.ts
@@ -2,24 +2,12 @@ import type { PointLike } from '@antv/g';
import { CellType } from '../common/constant/interaction';
import type { AreaRange, FormatResult } from '../common/interface';
import { CellBorderPosition, CellClipBox } from '../common/interface/basic';
-import type { BaseHeaderConfig } from '../facet/header/interface';
-import type { Node } from '../facet/layout/node';
import { getHorizontalTextIconPosition } from '../utils/cell/cell';
import { adjustTextIconPositionWhileScrolling } from '../utils/cell/text-scrolling';
import { normalizeTextAlign } from '../utils/normalize';
-import { BaseCell } from './base-cell';
-
-export class SeriesNumberCell extends BaseCell {
- protected declare headerConfig: BaseHeaderConfig;
-
- protected handleRestOptions(...[headerConfig]: [BaseHeaderConfig]) {
- this.headerConfig = { ...headerConfig };
- }
-
- public getHeaderConfig() {
- return this.headerConfig || {};
- }
+import { HeaderCell } from './header-cell';
+export class SeriesNumberCell extends HeaderCell {
public get cellType() {
return CellType.ROW_CELL;
}
@@ -30,13 +18,16 @@ export class SeriesNumberCell extends BaseCell {
protected initCell(): void {
this.drawBackgroundShape();
+ this.drawInteractiveBgShape();
+ this.drawInteractiveBorderShape();
this.drawBorders();
this.drawTextShape();
+ this.update();
}
- protected getBackgroundColor() {
+ public getBackgroundColor() {
const { backgroundColor, backgroundColorOpacity } =
- this.getStyle()?.cell || {};
+ this.getCrossBackgroundColor(this.meta.rowIndex);
return {
backgroundColor,
@@ -45,10 +36,6 @@ export class SeriesNumberCell extends BaseCell {
};
}
- public update(): void {
- /** 序号单元格暂时没有交互的联动 */
- }
-
protected getTextStyle() {
const textOverflowStyle = this.getCellTextWordWrapStyle(
CellType.SERIES_NUMBER_CELL,
@@ -116,15 +103,19 @@ export class SeriesNumberCell extends BaseCell {
return { x: textX, y: textStart };
}
- protected findFieldCondition() {
+ protected isBolderText() {
+ return false;
+ }
+
+ public findFieldCondition() {
return undefined;
}
- protected mappingValue() {
+ public mappingValue() {
return undefined;
}
- protected getIconPosition(): PointLike {
+ public getIconPosition(): PointLike {
return { x: 0, y: 0 };
}
}
diff --git a/packages/s2-core/src/cell/table-data-cell.ts b/packages/s2-core/src/cell/table-data-cell.ts
index 13fa924d50..7f92f68f88 100644
--- a/packages/s2-core/src/cell/table-data-cell.ts
+++ b/packages/s2-core/src/cell/table-data-cell.ts
@@ -1,6 +1,7 @@
import { Frame } from '../facet/header/frame';
import { DataCell } from '../cell/data-cell';
import {
+ CellType,
FrozenGroupType,
KEY_GROUP_FROZEN_ROW_RESIZE_AREA,
KEY_GROUP_ROW_RESIZE_AREA,
@@ -18,6 +19,7 @@ import {
} from '../utils/interaction/resize';
import { CustomRect, type SimpleBBox } from '../engine';
import type { FrozenFacet } from '../facet/frozen-facet';
+import type { CellMeta } from '../common';
import { BaseCell } from './base-cell';
export class TableDataCell extends DataCell {
@@ -160,4 +162,8 @@ export class TableDataCell extends DataCell {
),
);
}
+
+ protected isDisableHover(cellMeta: CellMeta) {
+ return cellMeta?.type === CellType.COL_CELL;
+ }
}
diff --git a/packages/s2-core/src/facet/header/util.ts b/packages/s2-core/src/facet/header/util.ts
index 8da2016ef2..dbcc00a796 100644
--- a/packages/s2-core/src/facet/header/util.ts
+++ b/packages/s2-core/src/facet/header/util.ts
@@ -1,23 +1,7 @@
-import { isNil } from 'lodash';
import type { SpreadSheet } from '../../sheet-type';
import type { Hierarchy } from '../layout/hierarchy';
import { Node } from '../layout/node';
-export const getCellPadding = () => {
- const padding = [12, 4, 12, 4];
- const left = isNil(padding[3]) ? 0 : padding[3];
- const right = isNil(padding[1]) ? 0 : padding[1];
- const top = isNil(padding[0]) ? 0 : padding[0];
- const bottom = isNil(padding[2]) ? 0 : padding[2];
-
- return {
- left,
- right,
- top,
- bottom,
- };
-};
-
export const getSeriesNumberNodes = (
rowsHierarchy: Hierarchy,
seriesNumberWidth: number,
@@ -27,11 +11,12 @@ export const getSeriesNumberNodes = (
const rootNodes = rowsHierarchy.getNodes(0);
return rootNodes.map((node: Node, idx: number) => {
+ const value = `${idx + 1}`;
const sNode = new Node({
- id: '',
+ id: value,
field: '',
rowIndex: idx,
- value: `${idx + 1}`,
+ value,
});
sNode.x = node.x;
diff --git a/packages/s2-core/src/interaction/base-interaction/click/corner-cell-click.ts b/packages/s2-core/src/interaction/base-interaction/click/corner-cell-click.ts
index 316e5c1dd8..c22d43ed3b 100644
--- a/packages/s2-core/src/interaction/base-interaction/click/corner-cell-click.ts
+++ b/packages/s2-core/src/interaction/base-interaction/click/corner-cell-click.ts
@@ -24,58 +24,49 @@ export class CornerCellClick extends BaseEvent implements BaseEventImplement {
private bindCornerCellClick() {
this.spreadsheet.on(S2Event.CORNER_CELL_CLICK, (event) => {
- const { interaction } = this.spreadsheet;
const cornerCell = this.spreadsheet.getCell(event.target);
if (!cornerCell) {
return;
}
- // 获取当前角头所对应那一列的行头单元格节点
const cornerCellMeta = cornerCell.getMeta() as Node;
- // TODO: 序号/列角头的交互待拓展
- if (
- [CornerNodeType.Series, CornerNodeType.Col].includes(
- cornerCellMeta?.cornerType!,
- )
- ) {
- interaction.reset();
+ switch (cornerCellMeta?.cornerType) {
+ case CornerNodeType.Row:
+ this.onRowCornerClick(cornerCellMeta?.field, event);
- return;
+ break;
+ case CornerNodeType.Col:
+ this.onColCornerClick(cornerCellMeta?.field, event);
+
+ break;
+ case CornerNodeType.Series:
+ this.onSeriesCornerClick(cornerCellMeta?.field, event);
+
+ break;
+ default:
+ break;
}
+ });
+ }
- const rowNodes = this.getSelectedRowNodes(cornerCellMeta?.field!);
- const sample = rowNodes[0]?.belongsCell;
- const cells = this.getRowCellMetas(rowNodes);
+ private onRowCornerClick(field: string, event: CanvasEvent) {
+ const rowNodes = this.getSelectedRowNodes(field);
- if (sample && interaction.isSelectedCell(sample)) {
- interaction.reset();
- this.spreadsheet.emit(
- S2Event.GLOBAL_SELECTED,
- interaction.getActiveCells(),
- );
+ this.selectCells(rowNodes, event);
+ }
- return;
- }
+ private onColCornerClick(field: string, event: CanvasEvent) {
+ const colNodes = this.getSelectedColNodes(field);
- if (isEmpty(rowNodes) || isEmpty(cells)) {
- return;
- }
+ this.selectCells(colNodes, event);
+ }
- interaction.addIntercepts([InterceptType.HOVER]);
- interaction.changeState({
- cells,
- stateName: InteractionStateName.SELECTED,
- });
- interaction.highlightNodes(rowNodes);
+ private onSeriesCornerClick(field: string, event: CanvasEvent) {
+ const seriesNodes = this.spreadsheet.facet.getSeriesNumberNodes();
- this.showTooltip(event);
- this.spreadsheet.emit(
- S2Event.GLOBAL_SELECTED,
- interaction.getActiveCells(),
- );
- });
+ this.selectCells(seriesNodes, event);
}
private getSelectedRowNodes(field: string) {
@@ -96,18 +87,64 @@ export class CornerCellClick extends BaseEvent implements BaseEventImplement {
return facet.getRowNodes(sampleNode?.level);
}
- private getRowCellMetas(nodes: Node[]): CellMeta[] {
+ private getSelectedColNodes(field: string) {
+ const { facet } = this.spreadsheet;
+
+ if (!this.spreadsheet.isCustomColumnFields()) {
+ return facet.getColNodesByField(field);
+ }
+
+ // 自定义列头 field 都是独立的, 需要根据 level 区查找.
+ const sampleNode = facet.getColNodesByField(field)[0];
+
+ return facet.getColNodes(sampleNode?.level);
+ }
+
+ private getCellMetas(nodes: Node[], cellType: CellType): CellMeta[] {
return nodes.map((node) => {
return {
id: node.id,
// 选中角头而高亮的行头, 不需要联动数值单元格, 所以索引设置为 -1
colIndex: -1,
rowIndex: -1,
- type: CellType.ROW_CELL,
+ type: cellType,
};
});
}
+ private selectCells(nodes: Node[], event: CanvasEvent) {
+ const { interaction } = this.spreadsheet;
+ const sample = nodes[0]?.belongsCell;
+ const cells = this.getCellMetas(nodes, sample?.cellType!);
+
+ if (sample && interaction.isSelectedCell(sample)) {
+ interaction.reset();
+ this.spreadsheet.emit(
+ S2Event.GLOBAL_SELECTED,
+ interaction.getActiveCells(),
+ );
+
+ return;
+ }
+
+ if (isEmpty(nodes) || isEmpty(cells)) {
+ return;
+ }
+
+ interaction.addIntercepts([InterceptType.HOVER]);
+ interaction.changeState({
+ cells,
+ stateName: InteractionStateName.SELECTED,
+ });
+ interaction.highlightNodes(nodes);
+
+ this.showTooltip(event);
+ this.spreadsheet.emit(
+ S2Event.GLOBAL_SELECTED,
+ interaction.getActiveCells(),
+ );
+ }
+
private showTooltip(event: CanvasEvent) {
// 角头的选中是维值, 不需要计算数值总和, 显示 [`xx 项已选中`] 即可
const selectedData = this.spreadsheet.interaction.getActiveCells();
diff --git a/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts b/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts
index ca459b8069..afc117b292 100644
--- a/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts
+++ b/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts
@@ -1,5 +1,4 @@
import type { FederatedPointerEvent as CanvasEvent } from '@antv/g';
-import { forEach } from 'lodash';
import type { DataCell } from '../../../cell/data-cell';
import {
InteractionStateName,
@@ -13,16 +12,14 @@ import type {
ViewMetaData,
} from '../../../common/interface';
import {
- getCellMeta,
afterSelectDataCells,
- getRowCellForSelectedCell,
+ getCellMeta,
} from '../../../utils/interaction/select-event';
import {
getTooltipOptions,
getTooltipVisibleOperator,
} from '../../../utils/tooltip';
import { BaseEvent, type BaseEventImplement } from '../../base-event';
-import { updateAllColHeaderCellState } from '../../../utils/interaction';
export class DataCellClick extends BaseEvent implements BaseEventImplement {
public bindEvents() {
@@ -33,7 +30,7 @@ export class DataCellClick extends BaseEvent implements BaseEventImplement {
this.spreadsheet.on(S2Event.DATA_CELL_CLICK, (event: CanvasEvent) => {
event.stopPropagation();
- const { interaction, facet } = this.spreadsheet;
+ const { interaction } = this.spreadsheet;
interaction.clearHoverTimer();
@@ -47,7 +44,7 @@ export class DataCellClick extends BaseEvent implements BaseEventImplement {
return;
}
- const cell = this.spreadsheet.getCell(event.target) as DataCell;
+ const cell = this.spreadsheet.getCell(event.target)!;
const meta = cell.getMeta();
if (!meta) {
@@ -80,29 +77,10 @@ export class DataCellClick extends BaseEvent implements BaseEventImplement {
this.showTooltip(event, meta);
// 点击单元格,高亮对应的行头、列头
- const { rowId, colId, spreadsheet } = meta;
- const { colHeader, rowHeader } = interaction.getSelectedCellHighlight();
-
- if (colHeader) {
- updateAllColHeaderCellState(
- colId,
- facet.getColCells(),
- InteractionStateName.SELECTED,
- );
- }
-
- if (rowHeader) {
- if (rowId) {
- const allRowHeaderCells = getRowCellForSelectedCell(
- meta,
- spreadsheet,
- );
-
- forEach(allRowHeaderCells, (rowCell) => {
- rowCell.updateByState(InteractionStateName.SELECTED);
- });
- }
- }
+ interaction.updateDataCellRelevantHeaderCells(
+ InteractionStateName.SELECTED,
+ meta,
+ );
});
}
diff --git a/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts b/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts
index 8874c90730..9045c75758 100644
--- a/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts
+++ b/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts
@@ -29,6 +29,7 @@ import {
getTooltipVisibleOperator,
mergeCellInfo,
} from '../../../utils/tooltip';
+import { SeriesNumberCell } from '../../../cell';
import type { ViewMeta } from './../../../common/interface/basic';
export class RowColumnClick extends BaseEvent implements BaseEventImplement {
@@ -98,10 +99,13 @@ export class RowColumnClick extends BaseEvent implements BaseEventImplement {
const { interaction, options } = this.spreadsheet;
const cell = this.spreadsheet.getCell(event.target)!;
+ if (cell instanceof SeriesNumberCell) {
+ return;
+ }
+
const { multiSelection: enableMultiSelection } = options.interaction!;
// 关闭了多选就算按下了 Ctrl/Commend, 行/列也按单选处理
const isMultiSelection = !!(enableMultiSelection && this.isMultiSelection);
-
const success = interaction.selectHeaderCell({
cell,
isMultiSelection,
diff --git a/packages/s2-core/src/interaction/base-interaction/hover.ts b/packages/s2-core/src/interaction/base-interaction/hover.ts
index 92ae0bb175..7e8bb41256 100644
--- a/packages/s2-core/src/interaction/base-interaction/hover.ts
+++ b/packages/s2-core/src/interaction/base-interaction/hover.ts
@@ -1,5 +1,5 @@
import type { FederatedPointerEvent as CanvasEvent } from '@antv/g';
-import { forEach, isBoolean, isEmpty } from 'lodash';
+import { isBoolean, isEmpty } from 'lodash';
import { S2Event } from '../../common/constant';
import {
HOVER_FOCUS_DURATION,
@@ -12,56 +12,17 @@ import type {
TooltipOptions,
ViewMeta,
} from '../../common/interface';
-import {
- getActiveHoverRowColCells,
- updateAllColHeaderCellState,
-} from '../../utils/interaction/hover-event';
+import type { Node } from '../../facet/layout/node';
import { getCellMeta } from '../../utils/interaction/select-event';
import { BaseEvent, type BaseEventImplement } from '../base-event';
-import type { Node } from '../../facet/layout/node';
/**
* @description Hover event for data cells, row cells and col cells
*/
export class HoverEvent extends BaseEvent implements BaseEventImplement {
public bindEvents() {
- this.bindCornerCellHover();
this.bindDataCellHover();
- this.bindRowCellHover();
- this.bindColCellHover();
- }
-
- public updateRowColCells(meta: ViewMeta | Node) {
- const { rowId, colId } = meta;
- const { facet, interaction } = this.spreadsheet;
-
- updateAllColHeaderCellState(
- colId,
- facet.getColCells(),
- InteractionStateName.HOVER,
- );
- const { rowHeader, colHeader } = interaction.getHoverHighlight();
-
- if (colHeader) {
- updateAllColHeaderCellState(
- colId,
- facet.getColCells(),
- InteractionStateName.HOVER,
- );
- }
-
- if (rowHeader && rowId) {
- // update rowHeader cells
- const allRowHeaderCells = getActiveHoverRowColCells(
- rowId,
- facet.getRowCells(),
- this.spreadsheet.isHierarchyTreeType(),
- );
-
- forEach(allRowHeaderCells, (cell) => {
- cell.updateByState(InteractionStateName.HOVER);
- });
- }
+ this.bindHeaderCellHover();
}
/**
@@ -75,7 +36,7 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement {
return;
}
- const meta = cell.getMeta();
+ const meta = cell.getMeta() as ViewMeta;
const { interaction } = this.spreadsheet;
const { interaction: interactionOptions } = this.spreadsheet.options;
const { hoverFocus } = interactionOptions!;
@@ -99,23 +60,20 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement {
};
if (interactionOptions?.hoverHighlight) {
- const { rowHeader, colHeader } = interaction.getHoverHighlight();
-
- if (rowHeader || colHeader) {
- // highlight all the row and column cells which the cell belongs to
- this.updateRowColCells(meta);
- }
+ interaction.updateDataCellRelevantHeaderCells(
+ InteractionStateName.HOVER,
+ meta,
+ );
}
const data = this.getCellData(meta, onlyShowCellText);
this.spreadsheet.showTooltipWithInfo(event, data, options);
};
- let hoverFocusDuration = HOVER_FOCUS_DURATION;
- if (!isBoolean(hoverFocus)) {
- hoverFocusDuration = hoverFocus?.duration ?? HOVER_FOCUS_DURATION;
- }
+ const hoverFocusDuration = !isBoolean(hoverFocus)
+ ? hoverFocus?.duration ?? HOVER_FOCUS_DURATION
+ : HOVER_FOCUS_DURATION;
if (hoverFocusDuration === 0) {
handleHoverFocus();
@@ -230,12 +188,10 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement {
});
if (interactionOptions?.hoverHighlight) {
- const { rowHeader, colHeader } = interaction.getHoverHighlight();
-
- if (rowHeader || colHeader) {
- // highlight all the row and column cells which the cell belongs to
- this.updateRowColCells(meta);
- }
+ interaction.updateDataCellRelevantHeaderCells(
+ InteractionStateName.HOVER,
+ meta,
+ );
}
if (interactionOptions?.hoverFocus) {
@@ -244,21 +200,15 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement {
});
}
- public bindRowCellHover() {
- this.spreadsheet.on(S2Event.ROW_CELL_HOVER, (event: CanvasEvent) => {
- this.handleHeaderHover(event);
- });
- }
-
- public bindColCellHover() {
- this.spreadsheet.on(S2Event.COL_CELL_HOVER, (event: CanvasEvent) => {
- this.handleHeaderHover(event);
- });
- }
-
- public bindCornerCellHover() {
- this.spreadsheet.on(S2Event.CORNER_CELL_HOVER, (event: CanvasEvent) => {
- this.handleHeaderHover(event);
+ public bindHeaderCellHover() {
+ [
+ S2Event.ROW_CELL_HOVER,
+ S2Event.COL_CELL_HOVER,
+ S2Event.CORNER_CELL_HOVER,
+ ].forEach((eventName) => {
+ this.spreadsheet.on(eventName, (event: CanvasEvent) => {
+ this.handleHeaderHover(event);
+ });
});
}
}
diff --git a/packages/s2-core/src/interaction/root.ts b/packages/s2-core/src/interaction/root.ts
index 5dd1be12cc..aacf04f59d 100644
--- a/packages/s2-core/src/interaction/root.ts
+++ b/packages/s2-core/src/interaction/root.ts
@@ -1,5 +1,5 @@
import { concat, find, forEach, isBoolean, isEmpty, isNil, map } from 'lodash';
-import type { MergedCell } from '../cell';
+import { type MergedCell } from '../cell';
import {
CellType,
INTERACTION_STATE_INFO_KEY,
@@ -9,8 +9,8 @@ import {
S2Event,
} from '../common/constant';
import type {
- BrushSelectionOptions,
BrushSelectionInfo,
+ BrushSelectionOptions,
CellMeta,
CustomInteraction,
InteractionCellHighlightOptions,
@@ -19,12 +19,20 @@ import type {
MergedCellInfo,
S2CellType,
SelectHeaderCellInfo,
+ ViewMeta,
} from '../common/interface';
import type { Node } from '../facet/layout/node';
import type { SpreadSheet } from '../sheet-type';
import { hideColumnsByThunkGroup } from '../utils/hide-columns';
+import {
+ getActiveHoverHeaderCells,
+ updateAllColHeaderCellState,
+} from '../utils/interaction/hover-event';
import { mergeCell, unmergeCell } from '../utils/interaction/merge-cell';
-import { getCellMeta } from '../utils/interaction/select-event';
+import {
+ getCellMeta,
+ getRowCellForSelectedCell,
+} from '../utils/interaction/select-event';
import { clearState, setState } from '../utils/interaction/state-controller';
import { isMobile } from '../utils/is-mobile';
import type { BaseEvent } from './base-event';
@@ -629,4 +637,64 @@ export class RootInteraction {
colCell,
};
}
+
+ public updateDataCellRelevantHeaderCells(
+ stateName: InteractionStateName,
+ meta: ViewMeta,
+ ) {
+ this.updateDataCellRelevantColCells(stateName, meta);
+ this.updateDataCellRelevantRowCells(stateName, meta);
+ }
+
+ public updateDataCellRelevantRowCells(
+ stateName: InteractionStateName,
+ meta: ViewMeta,
+ ) {
+ const { rowId } = meta;
+ const { facet, interaction } = this.spreadsheet;
+ const isHoverState = stateName === InteractionStateName.HOVER;
+ const { rowHeader } = isHoverState
+ ? interaction.getHoverHighlight()
+ : interaction.getSelectedCellHighlight();
+
+ if (rowHeader && rowId) {
+ const activeRowCells = isHoverState
+ ? getActiveHoverHeaderCells(
+ rowId,
+ facet.getRowCells(),
+ this.spreadsheet.isHierarchyTreeType(),
+ )
+ : getRowCellForSelectedCell(meta, this.spreadsheet);
+
+ const activeSeriesNumberCells = facet
+ .getSeriesNumberCells()
+ .filter((seriesNumberCell) => {
+ return activeRowCells.find(
+ (rowCell) => rowCell.getMeta().y === seriesNumberCell.getMeta().y,
+ );
+ });
+
+ const activeHeaderCells = [...activeSeriesNumberCells, ...activeRowCells];
+
+ forEach(activeHeaderCells, (cell) => {
+ cell.updateByState(stateName);
+ });
+ }
+ }
+
+ public updateDataCellRelevantColCells(
+ stateName: InteractionStateName,
+ meta: ViewMeta,
+ ) {
+ const { colId } = meta;
+ const { facet, interaction } = this.spreadsheet;
+ const { colHeader } =
+ stateName === InteractionStateName.HOVER
+ ? interaction.getHoverHighlight()
+ : interaction.getSelectedCellHighlight();
+
+ if (colHeader && colId) {
+ updateAllColHeaderCellState(colId, facet.getColCells(), stateName);
+ }
+ }
}
diff --git a/packages/s2-core/src/utils/interaction/hover-event.ts b/packages/s2-core/src/utils/interaction/hover-event.ts
index a2bdd7549b..139983aa08 100644
--- a/packages/s2-core/src/utils/interaction/hover-event.ts
+++ b/packages/s2-core/src/utils/interaction/hover-event.ts
@@ -1,23 +1,23 @@
import { filter, forEach } from 'lodash';
-import type { ColCell, RowCell } from '../../cell';
-import { NODE_ID_SEPARATOR, InteractionStateName } from '../../common/constant';
+import type { ColCell, HeaderCell } from '../../cell';
+import { InteractionStateName, NODE_ID_SEPARATOR } from '../../common/constant';
import { generateId } from '../layout/generate-id';
/**
* @description Return all the row cells or column cells which are needed to be highlighted.
* @param id rowId or colId
* @param headerCells all the rowHeader cells or all the colHeader cells
- * @param isRowInHierarchyTreeType The tree mode will only highlight the leaf nodes at the head of the row
+ * @param isHierarchyTree The tree mode will only highlight the leaf nodes at the head of the row
*/
-export const getActiveHoverRowColCells = (
+export const getActiveHoverHeaderCells = (
id: string,
- headerCells: (ColCell | RowCell)[],
- isRowInHierarchyTreeType?: boolean,
+ headerCells: HeaderCell[],
+ isHierarchyTree?: boolean,
) => {
let allHeaderIds: string[];
const ids = id.split(NODE_ID_SEPARATOR);
- if (isRowInHierarchyTreeType) {
+ if (isHierarchyTree) {
allHeaderIds = [id];
} else {
allHeaderIds = [generateId(ids[0], ids[1])];
@@ -26,7 +26,7 @@ export const getActiveHoverRowColCells = (
}
}
- const allHeaderCells = filter(headerCells, (cell: ColCell | RowCell) =>
+ const allHeaderCells = filter(headerCells, (cell) =>
allHeaderIds.includes(cell.getMeta()?.id),
);
@@ -39,7 +39,7 @@ export const updateAllColHeaderCellState = (
stateName: InteractionStateName,
) => {
if (colId) {
- const allColHeaderCells = getActiveHoverRowColCells(colId, colHeaderCells);
+ const allColHeaderCells = getActiveHoverHeaderCells(colId, colHeaderCells);
forEach(allColHeaderCells, (cell) => {
cell.updateByState(stateName);
diff --git a/packages/s2-core/src/utils/interaction/select-event.ts b/packages/s2-core/src/utils/interaction/select-event.ts
index b94893437d..b88ab0f515 100644
--- a/packages/s2-core/src/utils/interaction/select-event.ts
+++ b/packages/s2-core/src/utils/interaction/select-event.ts
@@ -1,5 +1,5 @@
-import { forEach, reduce, uniqBy } from 'lodash';
-import { ColCell, RowCell, TableSeriesNumberCell } from '../../cell';
+import { reduce, uniqBy } from 'lodash';
+import { HeaderCell, TableSeriesNumberCell } from '../../cell';
import {
CellType,
InteractionKeyboardKey,
@@ -15,10 +15,7 @@ import type {
import type { Node } from '../../facet/layout/node';
import type { SpreadSheet } from '../../sheet-type';
import { getDataCellId } from '../cell/data-cell';
-import {
- getActiveHoverRowColCells,
- updateAllColHeaderCellState,
-} from './hover-event';
+import { getActiveHoverHeaderCells } from './hover-event';
type HeaderGetter = {
getter: typeof getRowHeaderByCellId;
@@ -82,7 +79,7 @@ export function getRangeIndex(
export function getRowCellForSelectedCell(
meta: ViewMeta,
spreadsheet: SpreadSheet,
-): (ColCell | RowCell | TableSeriesNumberCell)[] {
+): HeaderCell[] {
const { facet, options } = spreadsheet;
if (spreadsheet.isTableMode()) {
@@ -99,35 +96,16 @@ export function getRowCellForSelectedCell(
result.push(rowCell);
}
- return result;
+ return result as unknown as HeaderCell[];
}
- return getActiveHoverRowColCells(
+ return getActiveHoverHeaderCells(
meta.rowId!,
facet.getRowCells(),
spreadsheet.isHierarchyTreeType(),
);
}
-export function updateRowColCells(meta: ViewMeta) {
- const { rowId, colId, spreadsheet } = meta;
- const { facet } = spreadsheet;
-
- updateAllColHeaderCellState(
- colId!,
- facet.getColCells(),
- InteractionStateName.SELECTED,
- );
-
- if (rowId) {
- const allRowHeaderCells = getRowCellForSelectedCell(meta, spreadsheet);
-
- forEach(allRowHeaderCells, (cell) => {
- cell.updateByState(InteractionStateName.SELECTED);
- });
- }
-}
-
export const getRowHeaderByCellId = (cellId: string, s2: SpreadSheet): Node[] =>
s2.facet.getRowNodes().filter((node: Node) => cellId.includes(node.id));
diff --git a/packages/s2-react/__tests__/README.md b/packages/s2-react/__tests__/README.md
new file mode 100644
index 0000000000..71ef229c35
--- /dev/null
+++ b/packages/s2-react/__tests__/README.md
@@ -0,0 +1,15 @@
+### 调试单测
+
+如果你想查看单测的运行结果,除了常规的 `pnpm core:test` 和 `pnpm react:test` 来运行测试之外,还可以 `可视化的调试单测(基于 jest-electron)`, 可以更快的发现单测的问题。
+
+1. 选择单测
+
+命令行运行 `pnpm core:start` 或者 `pnpm react:start`
+
+
+
+2. 查看结果
+
+因为本质上就是一个浏览器,如果单测结果不符合预期,可以正常打断点进行调试,快速分析原因。
+
+
diff --git a/packages/s2-react/__tests__/unit/components/pagination/index-spec.tsx b/packages/s2-react/__tests__/unit/components/pagination/index-spec.tsx
index 2a79130223..6974a4c28a 100644
--- a/packages/s2-react/__tests__/unit/components/pagination/index-spec.tsx
+++ b/packages/s2-react/__tests__/unit/components/pagination/index-spec.tsx
@@ -16,6 +16,5 @@ describe('Pagination Component Tests', () => {
expect(result.asFragment()).toMatchSnapshot();
expect(screen.getByText('共计20条')).toBeDefined();
- expect(screen.getByText('1 / page')).toBeDefined();
});
});
diff --git a/packages/s2-react/playground/README.md b/packages/s2-react/playground/README.md
new file mode 100644
index 0000000000..e776e0dce2
--- /dev/null
+++ b/packages/s2-react/playground/README.md
@@ -0,0 +1,15 @@
+### 开发与调试
+
+根目录运行 `pnpm react:playground` 来运行 `S2`, 可用于调试 `@antv/s2` 和 `@antv/s2-react`, 提供了一些常用的图表场景和配置。
+
+![playground](https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*6t8RRbg5x_kAAAAAAAAAAAAADmJ7AQ/original)
+
+S2 基于 `AntV/G` 渲染引擎绘制,如果想像 DOM 一样调试的话,可以安装 [G 开发者工具](https://g.antv.antgroup.com/api/devtools/g-devtools)
+
+1. 访问 `chrome://extensions/` 安装后
+
+![extensions](https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*RTYXTpb3WuIAAAAAAAAAAAAADmJ7AQ/original)
+
+2. 开始调试
+
+![dev-tool](https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*sP9eQaWxDpcAAAAAAAAAAAAADmJ7AQ/original)
diff --git a/packages/s2-react/playground/config.tsx b/packages/s2-react/playground/config.tsx
index f1a82d3f02..def059f443 100644
--- a/packages/s2-react/playground/config.tsx
+++ b/packages/s2-react/playground/config.tsx
@@ -316,6 +316,9 @@ export const s2Options: SheetComponentOptions = {
width: 800,
height: 600,
hierarchyType: 'grid',
+ seriesNumber: {
+ enable: true,
+ },
transformCanvasConfig() {
return {
supportsCSSTransform: true,
@@ -338,6 +341,8 @@ export const s2Options: SheetComponentOptions = {
withHeader: true,
},
hoverAfterScroll: true,
+ hoverHighlight: true,
+ selectedCellHighlight: true,
selectedCellMove: true,
rangeSelection: true,
// 防止 mac 触控板横向滚动触发浏览器返回, 和移动端下拉刷新
diff --git a/packages/s2-react/playground/index.tsx b/packages/s2-react/playground/index.tsx
index a221cff2bf..c401a5565d 100644
--- a/packages/s2-react/playground/index.tsx
+++ b/packages/s2-react/playground/index.tsx
@@ -82,7 +82,7 @@ const onSheetMounted = (s2: SpreadSheet) => {
// @ts-ignore
window.s2 = s2;
// @ts-ignore
- window.g_instances = [s2.container];
+ window.__g_instances__ = [s2.container];
};
const CustomTooltip = () => (
@@ -1289,7 +1289,7 @@ function MainLayout() {
}}
/>
-
+
+
+ {
+ updateOptions({
+ interaction: {
+ selectedCellHighlight: checked,
+ },
+ });
+ }}
+ />
+
void` |
| isSelectedCell | 是否是选中的单元格 | (cell: [S2CellType](#s2celltype)) => void |
| isActiveCell | 是否是激活的单元格 | (cell: [S2CellType](#s2celltype)) => void |
-| getCells | 获取当前 interaction 记录的 Cells 元信息列表,包括不在可视范围内的单元格 | `() => Partial[]` |
+| getCells | 获取当前 interaction 记录的 Cells 元信息列表,包括不在可视范围内的单元格 | () => Partial<[ViewMeta](#viewmeta)>[] |
| getActiveCells | 获取当前在可视区域的单元格实例 | `() => S2CellType[]` |
| clearStyleIndependent | 清除单元格样式 | `() => void` |
| getUnSelectedDataCells | 获取可视区域内选中的数值单元格 | `() => DataCell[]` |
@@ -48,6 +48,9 @@ s2.interaction.reset()
| hasIntercepts | 是否有指定拦截的交互 | (interceptTypes: [InterceptType](#intercepttype)[]) => boolean |
| removeIntercepts | 移除指定交互拦截 | (interceptTypes: [InterceptType](#intercepttype)[]) => void |
| highlightNodes | 高亮节点对应的单元格 | (nodes: [Node](/docs/api/basic-class/node)[]) => void |
+| updateDataCellRelevantHeaderCells | 高亮数值单元格和所对应行列单元格 | (stateName: [InteractionStateName](#interactionstatename), meta: [ViewMeta](#viewmeta)) => void |
+| updateDataCellRelevantRowCells | 高亮数值单元格和所对应行头单元格 | (stateName: [InteractionStateName](#interactionstatename), meta: [ViewMeta](#viewmeta)) => void |
+| updateDataCellRelevantColCells | 高亮数值单元格和所对应列头单元格 | (stateName: [InteractionStateName](#interactionstatename), meta: [ViewMeta](#viewmeta)) => void |
diff --git a/s2-site/docs/manual/advanced/custom/custom-icon.zh.md b/s2-site/docs/manual/advanced/custom/custom-icon.zh.md
index e1195580b1..a34f8ba18f 100644
--- a/s2-site/docs/manual/advanced/custom/custom-icon.zh.md
+++ b/s2-site/docs/manual/advanced/custom/custom-icon.zh.md
@@ -70,7 +70,7 @@ const s2Options = {
}
```
-也可以覆盖内置 `icon`, 例如自定义树状表格收起展开 `icon`
+也可以覆盖内置 `icon`, 例如自定义树状表格收起展开 `icon`. [查看示例](/examples/custom/custom-icon/#custom-tree-icon)
``` ts
const s2Options = {
diff --git a/s2-site/docs/manual/contribution.zh.md b/s2-site/docs/manual/contribution.zh.md
index 3f4cb66920..7b08452d41 100644
--- a/s2-site/docs/manual/contribution.zh.md
+++ b/s2-site/docs/manual/contribution.zh.md
@@ -53,18 +53,39 @@ tag: Updated
## ⌨️ 本地开发
-:::info{title="提示"}
+
-我们使用 `pnpm` 作为包管理
+### 调试功能
-```bash
-npm i -g pnpm
-```
+根目录运行 `pnpm react:playground` 来运行 `S2`, 可用于调试 `@antv/s2` 和 `@antv/s2-react`, 提供了一些常用的图表场景和配置。
-推荐本地运行 `pnpm react:playground` 来调试 `@antv/s2` 和 `@antv/s2-react`
-:::
+
-
+S2 基于 `AntV/G` 渲染引擎绘制,如果想像 DOM 一样调试的话,可以安装 [G 开发者工具](https://g.antv.antgroup.com/api/devtools/g-devtools)
+
+1. 访问 `chrome://extensions/` 安装后
+
+
+
+1. 开始调试
+
+
+
+### 调试单测
+
+如果你想查看单测的运行结果,除了常规的 `pnpm core:test` 和 `pnpm react:test` 来运行测试之外,还可以 `可视化的调试单测(基于 jest-electron)`, 可以更快的发现单测的问题。
+
+1. 选择单测
+
+命令行运行 `pnpm core:start` 或者 `pnpm react:start`
+
+
+
+2. 查看结果
+
+因为本质上就是一个浏览器,如果单测结果不符合预期,可以正常打断点进行调试,快速分析原因。
+
+
## 📦 版本
diff --git a/s2-site/examples/custom/custom-icon/demo/custom-tree-icon.ts b/s2-site/examples/custom/custom-icon/demo/custom-tree-icon.ts
new file mode 100644
index 0000000000..62d8d50678
--- /dev/null
+++ b/s2-site/examples/custom/custom-icon/demo/custom-tree-icon.ts
@@ -0,0 +1,39 @@
+import { PivotSheet, S2Options, S2DataConfig } from '@antv/s2';
+
+fetch(
+ 'https://gw.alipayobjects.com/os/bmw-prod/cd9814d0-6dfa-42a6-8455-5a6bd0ff93ca.json',
+)
+ .then((res) => res.json())
+ .then(async (res) => {
+ const container = document.getElementById('container');
+ const s2DataConfig: S2DataConfig = {
+ fields: {
+ rows: ['province', 'city'],
+ columns: ['type', 'sub_type'],
+ values: ['number'],
+ },
+ meta: res.meta,
+ data: res.data,
+ };
+
+ const s2Options: S2Options = {
+ width: 600,
+ height: 480,
+ hierarchyType: 'tree',
+ // 覆盖掉默认的展开 (Plus) 收起 (Minus) 图标
+ customSVGIcons: [
+ {
+ name: 'Plus',
+ svg: 'https://gw.alipayobjects.com/zos/antfincdn/kXgP1pnClS/plus.svg',
+ },
+ {
+ name: 'Minus',
+ svg: 'https://gw.alipayobjects.com/zos/antfincdn/2aWYZ7%26rQF/minus-circle.svg',
+ },
+ ],
+ };
+
+ const s2 = new PivotSheet(container, s2DataConfig, s2Options);
+
+ await s2.render();
+ });
diff --git a/s2-site/examples/custom/custom-icon/demo/meta.json b/s2-site/examples/custom/custom-icon/demo/meta.json
index cedecee50f..fd13d08749 100644
--- a/s2-site/examples/custom/custom-icon/demo/meta.json
+++ b/s2-site/examples/custom/custom-icon/demo/meta.json
@@ -35,6 +35,15 @@
"en": "Custom display condition"
},
"screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Eb5CSq0nszQAAAAAAAAAAAAADmJ7AQ/original"
+ },
+ {
+ "filename": "custom-tree-icon.ts",
+ "title": {
+ "zh": "自定义树状模式展开收起图标",
+ "en": "Custom tree icon"
+ },
+ "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*EjLvQ6LrFOEAAAAAAAAAAAAADmJ7AQ/original",
+ "new": true
}
]
}
diff --git a/s2-site/examples/interaction/basic/demo/corner-cell-click-selection.ts b/s2-site/examples/interaction/basic/demo/corner-cell-click-selection.ts
new file mode 100644
index 0000000000..c270f79ac4
--- /dev/null
+++ b/s2-site/examples/interaction/basic/demo/corner-cell-click-selection.ts
@@ -0,0 +1,38 @@
+import { PivotSheet, S2Event, S2Options } from '@antv/s2';
+
+fetch(
+ 'https://gw.alipayobjects.com/os/bmw-prod/2a5dbbc8-d0a7-4d02-b7c9-34f6ca63cff6.json',
+)
+ .then((res) => res.json())
+ .then(async (dataCfg) => {
+ const container = document.getElementById('container');
+
+ /**
+ * 试一试点击角头:
+ * 点击列角头 (类别/子类别): 会选中对应的列头
+ * 点击行角头 (省份/城市): 会选中对应的行头
+ * 点击序号角头: 会选中对应的序号列
+ */
+ const s2Options: S2Options = {
+ width: 600,
+ height: 480,
+ seriesNumber: {
+ enable: true,
+ },
+ interaction: {
+ hoverHighlight: true,
+ },
+ };
+
+ const s2 = new PivotSheet(container, dataCfg, s2Options);
+
+ s2.on(S2Event.CORNER_CELL_CLICK, (event) => {
+ console.log('corner cell click:', event);
+ });
+
+ s2.on(S2Event.GLOBAL_SELECTED, (cells) => {
+ console.log('selected', cells);
+ });
+
+ await s2.render();
+ });
diff --git a/s2-site/examples/interaction/basic/demo/event.ts b/s2-site/examples/interaction/basic/demo/event.ts
index a3e82408c3..bf5aecf51c 100644
--- a/s2-site/examples/interaction/basic/demo/event.ts
+++ b/s2-site/examples/interaction/basic/demo/event.ts
@@ -8,26 +8,26 @@ import {
} from '@antv/s2';
function addButtons(s2: SpreadSheet) {
- const selectAllBtn = document.createElement('button');
- const selectHeaderCellBtn = document.createElement('button');
- const selectDataCellBtn = document.createElement('button');
- const hideColumnsBtn = document.createElement('button');
- const resetBtn = document.createElement('button');
-
- [
+ const [
selectAllBtn,
selectHeaderCellBtn,
selectDataCellBtn,
hideColumnsBtn,
+ highlightHeaderBtn,
resetBtn,
- ].forEach((btn) => {
+ ] = Array.from({ length: 6 }).map(() => {
+ const btn = document.createElement('button');
+
btn.className = 'ant-btn ant-btn-default';
+
+ return btn;
});
selectAllBtn.innerHTML = '选中全部';
selectHeaderCellBtn.innerHTML = '选中指定行列头单元格';
selectDataCellBtn.innerHTML = '选中指定数值单元格';
hideColumnsBtn.innerHTML = '隐藏指定列头';
+ highlightHeaderBtn.innerHTML = '高亮数值和对应的行列头单元格';
resetBtn.innerHTML = '重置';
// 查看更多 API: https://s2.antv.antgroup.com/api/basic-class/interaction
@@ -81,6 +81,25 @@ function addButtons(s2: SpreadSheet) {
]);
});
+ highlightHeaderBtn.addEventListener('click', () => {
+ const dataCellViewMeta = s2.facet.getCellMeta(1, 1);
+
+ s2.interaction.updateDataCellRelevantHeaderCells(
+ dataCellViewMeta,
+ InteractionStateName.HOVER,
+ );
+
+ // s2.interaction.updateDataCellRelevantRowCells(
+ // dataCellViewMeta,
+ // InteractionStateName.HOVER,
+ // );
+
+ // s2.interaction.updateDataCellRelevantColCells(
+ // dataCellViewMeta,
+ // InteractionStateName.HOVER,
+ // );
+ });
+
resetBtn.addEventListener('click', () => {
console.log('当前状态:', s2.interaction.getState());
console.log('当前发生过交互的单元格:', s2.interaction.getInteractedCells());
@@ -96,10 +115,12 @@ function addButtons(s2: SpreadSheet) {
if (canvas) {
canvas.style.marginTop = '10px';
+
canvas.before(selectAllBtn);
canvas.before(selectHeaderCellBtn);
canvas.before(selectDataCellBtn);
canvas.before(hideColumnsBtn);
+ canvas.before(highlightHeaderBtn);
canvas.before(resetBtn);
}
}
@@ -150,6 +171,9 @@ fetch(
[
S2Event.GLOBAL_SCROLL,
S2Event.ROW_CELL_CLICK,
+ S2Event.COL_CELL_CLICK,
+ S2Event.CORNER_CELL_CLICK,
+ S2Event.DATA_CELL_CLICK,
S2Event.GLOBAL_SELECTED,
S2Event.DATA_CELL_BRUSH_SELECTION,
].forEach((eventName) => {
diff --git a/s2-site/examples/interaction/basic/demo/meta.json b/s2-site/examples/interaction/basic/demo/meta.json
index 42631712bb..38fe4d4b95 100644
--- a/s2-site/examples/interaction/basic/demo/meta.json
+++ b/s2-site/examples/interaction/basic/demo/meta.json
@@ -28,6 +28,15 @@
},
"screenshot": "https://gw.alipayobjects.com/zos/antfincdn/XYZaL1w%24M/Kapture%2525202022-04-15%252520at%25252011.45.55.gif"
},
+ {
+ "filename": "corner-cell-click-selection.ts",
+ "title": {
+ "zh": "角头多选",
+ "en": "Corner Cell Click Selection"
+ },
+ "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*0csYQK0JHaQAAAAAAAAAAAAADmJ7AQ/original",
+ "new": true
+ },
{
"filename": "data-cell-range-selection.ts",
"title": {