diff --git a/.github/workflows/compressed-size.yml b/.github/workflows/compressed-size.yml index ee7b677838..9092245ce9 100644 --- a/.github/workflows/compressed-size.yml +++ b/.github/workflows/compressed-size.yml @@ -26,6 +26,6 @@ jobs: - uses: preactjs/compressed-size-action@v2 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" - pattern: "./packages/**/dist/**/*.{js,css}" + pattern: "./packages/{s2-core,s2-react,s2-vue}/dist/**/*.{js,css}" build-script: "build:umd" clean-script: "clean" diff --git a/.husky/pre-push b/.husky/pre-push index 68c17e3e60..88ee19f009 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -2,4 +2,4 @@ . "$(dirname -- "$0")/_/husky.sh" . "$(dirname -- "$0")/common.sh" -yarn lint:type +pnpm lint:type diff --git a/packages/s2-core/CHANGELOG.md b/packages/s2-core/CHANGELOG.md index 054cbdc79a..868fa7195d 100644 --- a/packages/s2-core/CHANGELOG.md +++ b/packages/s2-core/CHANGELOG.md @@ -1,6 +1,65 @@ # [@antv/s2-v2.0.0-next.3](https://github.com/antvis/S2/compare/@antv/s2-v2.0.0-next.2...@antv/s2-v2.0.0-next.3) (2022-12-16) ### Bug Fixes +# [@antv/s2-v1.40.0](https://github.com/antvis/S2/compare/@antv/s2-v1.39.1...@antv/s2-v1.40.0) (2023-01-03) + + +### Bug Fixes + +* bullet占位符绘制错误 ([#2022](https://github.com/antvis/S2/issues/2022)) ([c95c446](https://github.com/antvis/S2/commit/c95c446219216b190bf6af632104edd125a82ff3)) +* 修复单元格宽高为0时的无意义渲染 ([#2024](https://github.com/antvis/S2/issues/2024)) ([9f952fd](https://github.com/antvis/S2/commit/9f952fd4bfd280b657b589e5912399f89bb1f0ea)) +* 列头label存在数组,复制导出列头层级补齐错误 ([#1990](https://github.com/antvis/S2/issues/1990)) ([ec62409](https://github.com/antvis/S2/commit/ec62409b688c5dd5e39a93f5b292d909496ed830)) + + +### Features + +* selected cell highlight ([#1878](https://github.com/antvis/S2/issues/1878)) ([3e11a37](https://github.com/antvis/S2/commit/3e11a37bf94f758379ba2819ec5d8b3251708814)) +* 子弹图为空时使用placeholder占位 ([#2010](https://github.com/antvis/S2/issues/2010)) ([8d28254](https://github.com/antvis/S2/commit/8d28254aa9aa29d9b2a9e24efb21f185cb5ffe4c)) + +# [@antv/s2-v1.39.1](https://github.com/antvis/S2/compare/@antv/s2-v1.39.0...@antv/s2-v1.39.1) (2022-12-20) + + +### Bug Fixes + +* 明细表linkField失效 ([#2007](https://github.com/antvis/S2/issues/2007)) ([122552b](https://github.com/antvis/S2/commit/122552bdd25aa538cfd38a6210e9979698c13188)) +* 明细表紧凑模式宽度计算错误 ([#2006](https://github.com/antvis/S2/issues/2006)) ([89f5c9e](https://github.com/antvis/S2/commit/89f5c9eb7719834ce9a55d340bf04415639cc277)) + +# [@antv/s2-v1.39.0](https://github.com/antvis/S2/compare/@antv/s2-v1.38.0...@antv/s2-v1.39.0) (2022-12-19) + + +### Bug Fixes + +* gui icon 加载完成后,增加检测步骤,避免无意义的渲染 warning ([#1997](https://github.com/antvis/S2/issues/1997)) ([6f13aa4](https://github.com/antvis/S2/commit/6f13aa43d8910f9ed83d59a9c958b26a0eb163d6)) + + +### Features + +* 添加字段标记中,定制柱状图的长度的功能 (filedValue) ([#2002](https://github.com/antvis/S2/issues/2002)) ([457e5e7](https://github.com/antvis/S2/commit/457e5e7989ce460e445f46925eaee79b49f56615)) + +# [@antv/s2-v1.38.0](https://github.com/antvis/S2/compare/@antv/s2-v1.37.0...@antv/s2-v1.38.0) (2022-12-16) + + +### Features + +* 增加 linkFields 参数传入类型 ([#1992](https://github.com/antvis/S2/issues/1992)) ([66bce2a](https://github.com/antvis/S2/commit/66bce2ae77635b530058f56b0545bd5558c119e1)) +* 新增链接跳转配置 ([5e08055](https://github.com/antvis/S2/commit/5e08055b52a125e75cbc41bb748d247ef25ba016)) + +# [@antv/s2-v1.37.0](https://github.com/antvis/S2/compare/@antv/s2-v1.36.0...@antv/s2-v1.37.0) (2022-12-09) + + +### Bug Fixes + +* **tooltip:** 修复自定义操作菜单传入自定义 ReactNode 不显示 ([#1969](https://github.com/antvis/S2/issues/1969)) ([3eff993](https://github.com/antvis/S2/commit/3eff9932438cc95093686c03510b57648ff44391)) +* 修复 enableCopy 和 hideMeasureColumn 都开启为 true 时,复制报错问题 ([#1984](https://github.com/antvis/S2/issues/1984)) ([528d2b6](https://github.com/antvis/S2/commit/528d2b6b6b912f790449aaef015fc27d2e0e33c9)) + + +### Features + +* 实现树状模式下可复制 ([#1986](https://github.com/antvis/S2/issues/1986)) ([96ccb1e](https://github.com/antvis/S2/commit/96ccb1ee14908fc1daf82d1eccb3bd852e642f7d)) +* 暴露afterRealCellRender,这样能够更灵活的使用datacell ([#1970](https://github.com/antvis/S2/issues/1970)) ([66c5ab9](https://github.com/antvis/S2/commit/66c5ab9992c51b475be8acaf9a198d49f3114a49)) +* 适配链接跳转的判断方式 ([#1983](https://github.com/antvis/S2/issues/1983)) ([2a26259](https://github.com/antvis/S2/commit/2a2625971bcefd119d2e2a280608d1acf56b5d32)) + +# [@antv/s2-v1.36.0](https://github.com/antvis/S2/compare/@antv/s2-v1.35.1...@antv/s2-v1.36.0) (2022-12-02) * 暂时修复g版本冲突问题 ([#2003](https://github.com/antvis/S2/issues/2003)) ([1de7ec2](https://github.com/antvis/S2/commit/1de7ec215bc96c28e7493c8a32fe1764fd08cb2d)) diff --git a/packages/s2-core/__tests__/spreadsheet/corner-spec.ts b/packages/s2-core/__tests__/spreadsheet/corner-spec.ts new file mode 100644 index 0000000000..914f5debdf --- /dev/null +++ b/packages/s2-core/__tests__/spreadsheet/corner-spec.ts @@ -0,0 +1,111 @@ +import * as simpleDataConfig from 'tests/data/simple-data.json'; +import { DEFAULT_STYLE, type SpreadSheet } from '../../src'; +import { createPivotSheet } from '../util/helpers'; + +describe('PivotSheet Corner Tests', () => { + let s2: SpreadSheet; + + beforeEach(() => { + s2 = createPivotSheet({ + width: 300, + height: 300, + }); + s2.render(); + }); + + afterEach(() => { + s2.destroy(); + }); + + // https://github.com/antvis/S2/issues/1929 + test('should render row corner when columns is empty and measure hidden', () => { + s2.setDataCfg({ + ...simpleDataConfig, + fields: { + ...simpleDataConfig.fields, + columns: [], + }, + }); + s2.setOptions({ + style: { + colCell: { + hideValue: true, + }, + }, + }); + s2.render(); + + const cornerNodes = s2.facet.getCornerNodes(); + const { colsHierarchy } = s2.facet.layoutResult; + + expect(colsHierarchy.width).toEqual(0); + expect(colsHierarchy.height).toEqual(DEFAULT_STYLE.colCell!.height); + expect(colsHierarchy.sampleNodeForLastLevel!.y).toEqual(0); + expect(colsHierarchy.sampleNodeForLastLevel!.height).toEqual( + DEFAULT_STYLE.colCell!.height, + ); + expect(cornerNodes).toHaveLength(2); + }); + + test('should render row corner when measure hidden', () => { + s2.setOptions({ + style: { + colCell: { + hideValue: true, + }, + }, + }); + s2.render(); + + const cornerNodes = s2.facet.getCornerNodes(); + const { colsHierarchy } = s2.facet.layoutResult; + + expect(colsHierarchy.width).toEqual(100); + expect(colsHierarchy.height).toEqual(DEFAULT_STYLE.colCell!.height); + expect(colsHierarchy.sampleNodeForLastLevel!.y).toEqual(0); + expect(colsHierarchy.sampleNodeForLastLevel!.height).toEqual( + DEFAULT_STYLE.colCell!.height, + ); + expect(cornerNodes).toHaveLength(2); + }); + + test('should render row corner when columns and values is empty', () => { + s2.setDataCfg({ + ...simpleDataConfig, + fields: { + ...simpleDataConfig.fields, + columns: [], + values: [], + }, + }); + s2.render(); + + const cornerNodes = s2.facet.getCornerNodes(); + const { colsHierarchy } = s2.facet.layoutResult; + + expect(colsHierarchy.width).toEqual(0); + expect(colsHierarchy.height).toEqual(0); + expect(colsHierarchy.sampleNodeForLastLevel).toBeNull(); + expect(cornerNodes).toHaveLength(2); + }); + + test('should not render row corner when fields is empty', () => { + s2.setDataCfg({ + ...simpleDataConfig, + fields: { + rows: [], + columns: [], + values: [], + }, + }); + s2.render(); + + const cornerNodes = s2.facet.getCornerNodes(); + const { colsHierarchy } = s2.facet.layoutResult; + + expect(colsHierarchy.width).toEqual(0); + expect(colsHierarchy.height).toEqual(0); + expect(colsHierarchy.sampleNodeForLastLevel).toBeNull(); + expect(cornerNodes).toHaveLength(0); + }); +}); diff --git a/packages/s2-core/__tests__/spreadsheet/row-link-spec.ts b/packages/s2-core/__tests__/spreadsheet/row-link-spec.ts index 01b705785c..70f09221f4 100644 --- a/packages/s2-core/__tests__/spreadsheet/row-link-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/row-link-spec.ts @@ -99,6 +99,7 @@ describe('Row Text Link Tests', () => { expect(linkFieldJump).toBeCalledWith({ field: 'province', + cellData: rowNode, record: { province: '浙江', type: '笔', @@ -125,6 +126,7 @@ describe('Row Text Link Tests', () => { expect(linkFieldJump).toBeCalledWith({ field: 'city', + cellData: rowNode, record: { province: '浙江', city: '义乌1', @@ -152,6 +154,7 @@ describe('Row Text Link Tests', () => { expect(linkFieldJump).toBeCalledWith({ field: 'province', + cellData: rowNode, record: { province: '四川', type: '笔', diff --git a/packages/s2-core/__tests__/spreadsheet/table-sheet-spec.ts b/packages/s2-core/__tests__/spreadsheet/table-sheet-spec.ts index 9abf5d5537..487821b031 100644 --- a/packages/s2-core/__tests__/spreadsheet/table-sheet-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/table-sheet-spec.ts @@ -1,13 +1,16 @@ import { getContainer, getMockData, sleep } from 'tests/util/helpers'; +import { get, last } from 'lodash'; import { - ColCell, DeviceType, - ResizeType, TableSheet, type RawData, type S2DataConfig, type S2Options, + ResizeType, + ColCell, + TableDataCell, } from '@/index'; +import type { TableFacet } from '@/facet'; const data = getMockData( '../../../s2-react/__tests__/data/tableau-supermarket.csv', @@ -184,6 +187,10 @@ describe('TableSheet normal spec', () => { const s2 = new TableSheet(getContainer(), dataCfg, options); s2.render(); + const getLastColCell = () => + last(s2.getColumnNodes())!.belongsCell as ColCell; + const preColWidth = getLastColCell().getMeta().width; + await sleep(30); const { x, width, top } = s2.getCanvasElement().getBoundingClientRect(); @@ -198,9 +205,11 @@ describe('TableSheet normal spec', () => { await sleep(300); // 等待绘制响应 + const resizeLength = 100; + document.dispatchEvent( new MouseEvent('mousemove', { - clientX: x + width + 100, + clientX: x + width + resizeLength, clientY: top + 25, bubbles: true, }), @@ -209,7 +218,7 @@ describe('TableSheet normal spec', () => { document.dispatchEvent( new PointerEvent('pointerup', { - clientX: x + width + 100, + clientX: x + width + resizeLength, clientY: top + 25, bubbles: true, }), @@ -217,10 +226,26 @@ describe('TableSheet normal spec', () => { await sleep(300); - const columnNodes = s2.getColumnNodes(); - const lastColumnCell = columnNodes[columnNodes.length - 1] - .belongsCell as ColCell; + const currentColWidth = getLastColCell().getMeta().width; + expect(currentColWidth).toBeGreaterThanOrEqual(resizeLength + preColWidth); + }); + + test('should render link shape', () => { + const s2 = new TableSheet(getContainer(), dataCfg, { + ...options, + frozenRowCount: 0, + frozenColCount: 0, + frozenTrailingColCount: 0, + frozenTrailingRowCount: 0, + } as S2Options); + s2.render(); - expect(lastColumnCell.getMeta().width).toBe(198); + const orderIdDataCell = ( + (s2.facet as TableFacet).frozenColGroup.children as TableDataCell[] + ).find((item) => item.getMeta().valueField === 'order_id'); + + expect(get(orderIdDataCell, 'linkFieldShape')).toBeDefined(); + + s2.destroy(); }); }); diff --git a/packages/s2-core/__tests__/unit/cell/col-cell-spec.ts b/packages/s2-core/__tests__/unit/cell/col-cell-spec.ts index 6469fa2c91..71733f4430 100644 --- a/packages/s2-core/__tests__/unit/cell/col-cell-spec.ts +++ b/packages/s2-core/__tests__/unit/cell/col-cell-spec.ts @@ -112,6 +112,8 @@ describe('Col Cell Tests', () => { id: 1, fieldValue: 'fieldValue', value: 'value', + width: 100, + height: 10, } as unknown as Node; const headerConfig: Partial = { diff --git a/packages/s2-core/__tests__/unit/cell/data-cell-spec.ts b/packages/s2-core/__tests__/unit/cell/data-cell-spec.ts index 9d7395f93c..13ec8867f7 100644 --- a/packages/s2-core/__tests__/unit/cell/data-cell-spec.ts +++ b/packages/s2-core/__tests__/unit/cell/data-cell-spec.ts @@ -3,8 +3,15 @@ import type { Rect } from '@antv/g'; import { find, get } from 'lodash'; import { createPivotSheet, createTableSheet } from 'tests/util/helpers'; import { DataCell } from '@/cell'; -import type { Formatter, ViewMeta } from '@/common'; -import { GuiIcon } from '@/common'; +import { + GuiIcon, + type Formatter, + type ViewMeta, + S2Event, + type OriginalEvent, + type S2CellType, + CellTypes, +} from '@/common'; import { EXTRA_FIELD, VALUE_FIELD } from '@/common/constant/basic'; import { DEFAULT_FONT_COLOR, @@ -35,6 +42,8 @@ describe('Data Cell Tests', () => { [VALUE_FIELD]: 'value', [EXTRA_FIELD]: 12, }, + width: 100, + height: 100, } as unknown as ViewMeta; let s2: SpreadSheet; @@ -103,6 +112,15 @@ describe('Data Cell Tests', () => { } as unknown as PivotFacet; }); + test("shouldn't init when width or height is not positive", () => { + const dataCell = new DataCell({ ...meta, width: 0, height: 0 }, s2); + expect(dataCell.getTextShape()).toBeUndefined(); + // @ts-ignore + expect(dataCell.backgroundShape).toBeUndefined(); + // @ts-ignore + expect([...dataCell.stateShapes.keys()]).toBeEmpty(); + }); + test('should get text shape', () => { const dataCell = new DataCell(meta, s2); expect(dataCell.getTextShapes()).toEqual([dataCell.getTextShape()]); @@ -211,6 +229,7 @@ describe('Data Cell Tests', () => { const fieldValue = 27.334666666666667; const anotherMeta = { width: cellWidth, + height: 100, valueField: 'value', fieldValue, data: { @@ -368,4 +387,79 @@ describe('Data Cell Tests', () => { table.render(); }); }); + + describe('Data Cell Interaction', () => { + let s2: SpreadSheet; + + beforeEach(() => { + s2 = createPivotSheet({ + showSeriesNumber: true, + interaction: { + enableCopy: true, + }, + }); + s2.render(); + }); + const emitEvent = (type: S2Event, event: Partial) => { + s2.emit(type, { + originalEvent: event, + preventDefault() {}, + stopPropagation() {}, + ...event, + } as any); + }; + + test('should be highlight entire row data cells when the row header is clicked', () => { + const allRowCells = s2.interaction.getAllRowHeaderCells(); + const mockCell = allRowCells[0]; + s2.getCell = jest.fn().mockReturnValue(mockCell); + + emitEvent(S2Event.ROW_CELL_CLICK, { + x: 2, + y: 2, + }); + + const interactedCells = s2.interaction.getInteractedCells(); + const firstRowCell = find(interactedCells, (cell: S2CellType) => { + return cell.cellType === CellTypes.ROW_CELL; + }); + expect(interactedCells.length).toBe(7); + expect(firstRowCell!.getMeta().id).toBe(mockCell.getMeta().id); + }); + + test('should be highlight entire column data cells when the column header is clicked', () => { + const allColumnCells = s2.interaction.getAllColHeaderCells(); + const mockCell = allColumnCells[0]; + s2.getCell = jest.fn().mockReturnValue(mockCell); + + emitEvent(S2Event.COL_CELL_CLICK, { + x: mockCell.getMeta().x + 5, + y: mockCell.getMeta().y + 5, + }); + + const interactedCells = s2.interaction.getInteractedCells(); + const firstColCell = find(interactedCells, (cell: S2CellType) => { + return cell.cellType === CellTypes.COL_CELL; + }); + expect(interactedCells.length).toBe(8); + expect(firstColCell!.getMeta().id).toBe(mockCell.getMeta().id); + }); + + test('should be highlight data cell when the data cell is clicked', () => { + const allDataCells = s2.interaction.getAllCells(); + const mockCell = allDataCells[0]; + s2.getCell = jest.fn().mockReturnValue(mockCell); + + emitEvent(S2Event.DATA_CELL_CLICK, { + x: mockCell.getMeta().x + 5, + y: mockCell.getMeta().y + 5, + }); + + const activeCells = s2.interaction.getInteractedCells(); + + expect(activeCells.length).toBe(1); + expect(activeCells[0].getActualText()).toBe('1'); + expect(activeCells[0].getMeta().id).toBe(mockCell.getMeta().id); + }); + }); }); diff --git a/packages/s2-core/__tests__/unit/facet/bbox/cornerBBox-spec.ts b/packages/s2-core/__tests__/unit/facet/bbox/cornerBBox-spec.ts index d4ec6e909e..912241e46a 100644 --- a/packages/s2-core/__tests__/unit/facet/bbox/cornerBBox-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/bbox/cornerBBox-spec.ts @@ -3,6 +3,7 @@ import { CornerBBox } from '@/facet/bbox/cornerBBox'; describe('cornerBBox test', () => { let mockFacet: BaseFacet; + beforeEach(() => { mockFacet = { layoutResult: { @@ -13,6 +14,7 @@ describe('cornerBBox test', () => { colsHierarchy: { height: 100, width: 100, + sampleNodeForLastLevel: true, }, }, getSeriesNumberWidth() { @@ -24,9 +26,14 @@ describe('cornerBBox test', () => { }, options: { width: 400, + style: { + colCell: { + hideMeasureColumn: false, + }, + }, }, }, - } as BaseFacet; + } as unknown as BaseFacet; }); test('should return original width when scroll contains row header', () => { @@ -68,4 +75,13 @@ describe('cornerBBox test', () => { expect(bbox.width).toEqual(200); expect(bbox.originalWidth).toEqual(280); }); + + test('should use default column height when columns is empty', () => { + mockFacet.spreadsheet.options.style!.colCell!.height = 20; + mockFacet.layoutResult.colsHierarchy.sampleNodeForLastLevel = null; + + const bbox = new CornerBBox(mockFacet, true); + + expect(bbox.height).toEqual(20); + }); }); diff --git a/packages/s2-core/__tests__/unit/facet/layout/col-node-width-spec.ts b/packages/s2-core/__tests__/unit/facet/layout/col-node-width-spec.ts index 43c10413ce..7f75250124 100644 --- a/packages/s2-core/__tests__/unit/facet/layout/col-node-width-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/layout/col-node-width-spec.ts @@ -1,104 +1,143 @@ import * as mockDataConfig from 'tests/data/simple-data.json'; +import * as mockTableDataConfig from 'tests/data/simple-table-data.json'; import { getContainer } from 'tests/util/helpers'; -import { PivotSheet } from '@/sheet-type'; import type { S2DataConfig, S2Options } from '@/common'; +import { PivotSheet, TableSheet } from '@/sheet-type'; const s2options: S2Options = { width: 800, height: 600, }; -describe('Col width Test in grid mode', () => { - let s2: PivotSheet; - beforeEach(() => { - s2 = new PivotSheet( - getContainer(), - { - ...mockDataConfig, - data: [ - { - province: '浙江', - city: '义乌', - type: '笔', - // long text - price: 123456789, - cost: 2, - }, - ], - }, - s2options, - ); - s2.render(); - }); +describe('Col width Test', () => { + describe('Grid Mode', () => { + let s2: PivotSheet; + beforeEach(() => { + s2 = new PivotSheet( + getContainer(), + { + ...mockDataConfig, + data: [ + { + province: '浙江', + city: '义乌', + type: '笔', + // long text + price: 123456789, + cost: 2, + }, + ], + }, + s2options, + ); + s2.render(); + }); - test('get correct width in layoutWidthType adaptive mode', () => { - const { colLeafNodes } = s2.facet.layoutResult; - expect(colLeafNodes[0].width).toBe(200); - }); + test('get correct width in layoutWidthType adaptive mode', () => { + const { colLeafNodes } = s2.facet.layoutResult; + expect(colLeafNodes[0].width).toBe(200); + }); - test('get correct width in layoutWidthType adaptive mode when enable series number', () => { - s2.setOptions({ - showSeriesNumber: true, + test('get correct width in layoutWidthType adaptive mode when enable series number', () => { + s2.setOptions({ + showSeriesNumber: true, + }); + s2.render(); + const { colLeafNodes } = s2.facet.layoutResult; + expect(colLeafNodes[0].width).toBe(180); }); - s2.render(); - const { colLeafNodes } = s2.facet.layoutResult; - expect(colLeafNodes[0].width).toBe(180); - }); - test('get correct width in layoutWidthType adaptive tree mode', () => { - s2.setOptions({ - hierarchyType: 'tree', + test('get correct width in layoutWidthType adaptive tree mode', () => { + s2.setOptions({ + hierarchyType: 'tree', + }); + s2.render(); + const { colLeafNodes } = s2.facet.layoutResult; + expect(Math.round(colLeafNodes[0].width)).toBe(339); }); - s2.render(); - const { colLeafNodes } = s2.facet.layoutResult; - expect(Math.round(colLeafNodes[0].width)).toBe(339); - }); - test('get correct width in layoutWidthType adaptive tree mode when enable series number', () => { - s2.setOptions({ - hierarchyType: 'tree', - showSeriesNumber: true, + test('get correct width in layoutWidthType adaptive tree mode when enable series number', () => { + s2.setOptions({ + hierarchyType: 'tree', + showSeriesNumber: true, + }); + s2.render(); + const { colLeafNodes } = s2.facet.layoutResult; + expect(Math.round(colLeafNodes[0].width)).toBe(299); }); - s2.render(); - const { colLeafNodes } = s2.facet.layoutResult; - expect(Math.round(colLeafNodes[0].width)).toBe(299); - }); - test('get correct width in layoutWidthType compact mode', () => { - s2.setOptions({ - style: { - layoutWidthType: 'compact', - }, + test('get correct width in layoutWidthType compact mode', () => { + s2.setOptions({ + style: { + layoutWidthType: 'compact', + }, + }); + s2.render(); + + // 无 formatter + const { colLeafNodes } = s2.facet.layoutResult; + expect(Math.round(colLeafNodes[0].width)).toBe(78); }); - s2.render(); - // 无 formatter - const { colLeafNodes } = s2.facet.layoutResult; - expect(Math.round(colLeafNodes[0].width)).toBe(78); + test('get correct width in layoutWidthType compact mode when apply formatter', () => { + s2.setDataCfg({ + fields: undefined as unknown as S2DataConfig['fields'], + data: undefined as unknown as S2DataConfig['data'], + meta: [ + { + field: 'price', + formatter: (v) => { + return `${((v as number) / 1000000).toFixed(0)}百万`; + }, + }, + ], + }); + s2.setOptions({ + style: { + layoutWidthType: 'compact', + }, + }); + s2.render(); + + // 有formatter + const { colLeafNodes } = s2.facet.layoutResult; + expect(Math.round(colLeafNodes[0].width)).toBe(62); + }); }); - test('get correct width in layoutWidthType compact mode when apply formatter', () => { - s2.setDataCfg({ - fields: undefined as unknown as S2DataConfig['fields'], - data: undefined as unknown as S2DataConfig['data'], - meta: [ + describe('Table Mode', () => { + let s2: TableSheet; + beforeEach(() => { + s2 = new TableSheet( + getContainer(), { - field: 'price', - formatter: (v) => { - return `${((v as number) / 1000000).toFixed(0)}百万`; - }, + ...mockTableDataConfig, + meta: [ + { + field: 'cost', + formatter: (s) => `我是一个很长的格式化标签${s}`, + }, + ], }, - ], - }); - s2.setOptions({ - style: { - layoutWidthType: 'compact', - }, + s2options, + ); + s2.render(); }); - s2.render(); - // 有formatter - const { colLeafNodes } = s2.facet.layoutResult; - expect(Math.round(colLeafNodes[0].width)).toBe(62); + test('get correct width in layoutWidthType compact mode', () => { + s2.setOptions({ + style: { + layoutWidthType: 'compact', + }, + }); + s2.render(); + + const { colLeafNodes } = s2.facet.layoutResult; + + // price 列,列头标签比表身数据更长 + expect(Math.round(colLeafNodes[0].width)).toBe(46); + // cost 列,表身数据比列头更长(格式化) + expect(Math.round(colLeafNodes[1].width)).toBe(168); + }); }); }); diff --git a/packages/s2-core/__tests__/unit/facet/table-facet-spec.ts b/packages/s2-core/__tests__/unit/facet/table-facet-spec.ts index 72545b734e..035993e669 100644 --- a/packages/s2-core/__tests__/unit/facet/table-facet-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/table-facet-spec.ts @@ -80,6 +80,7 @@ jest.mock('@/data-set/table-data-set', () => { getDimensionValues: jest.fn(), getDisplayDataSet: jest.fn(() => data), getCellData: () => 1, + getFieldFormatter: jest.fn(), }; }), }; diff --git a/packages/s2-core/__tests__/unit/interaction/base-interaction/click/data-cell-click-spec.ts b/packages/s2-core/__tests__/unit/interaction/base-interaction/click/data-cell-click-spec.ts index 6f6495b6c1..8d6a51ff14 100644 --- a/packages/s2-core/__tests__/unit/interaction/base-interaction/click/data-cell-click-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/base-interaction/click/data-cell-click-spec.ts @@ -7,11 +7,13 @@ import type { GEvent } from '@/index'; import type { SpreadSheet } from '@/sheet-type'; import { HOVER_FOCUS_DURATION, + type InteractionCellSelectedHighlightType, InteractionName, InteractionStateName, InterceptType, S2Event, } from '@/common/constant'; +import type { Node } from '@/facet/layout/node'; import { CustomRect } from '@/engine'; jest.mock('@/interaction/event-controller'); @@ -51,6 +53,7 @@ describe('Interaction Data Cell Click Tests', () => { expect(s2.interaction.getState()).toEqual({ cells: [mockCellInfo.mockCellMeta], stateName: InteractionStateName.SELECTED, + onUpdateCells: expect.any(Function), }); expect(s2.showTooltipWithInfo).toHaveBeenCalled(); }); @@ -69,6 +72,7 @@ describe('Interaction Data Cell Click Tests', () => { expect(s2.interaction.getState()).toEqual({ cells: [mockCellInfo.mockCellMeta], stateName: InteractionStateName.SELECTED, + onUpdateCells: expect.any(Function), }); }); @@ -112,6 +116,7 @@ describe('Interaction Data Cell Click Tests', () => { expect(linkFieldJump).toHaveBeenCalledWith({ field: mockCellData.valueField, + cellData: mockCellData, record: mockCellData.data, }); expect(s2.showTooltipWithInfo).not.toHaveBeenCalled(); @@ -166,4 +171,66 @@ describe('Interaction Data Cell Click Tests', () => { expect(s2.interaction.isHoverFocusState()).toBeFalsy(); expect(clearHoverTimerSpy).toHaveBeenCalledTimes(2); }); + + test('should highlight the column header cell when data cell clicked', () => { + const headerCellId0 = 'header-0'; + const headerCellId1 = 'header-1'; + const columnNode: Array> = [ + { + belongsCell: { + getMeta: () => ({ + id: headerCellId0, + colIndex: -1, + rowIndex: -1, + }), + } as any, + id: headerCellId0, + }, + { + belongsCell: { + getMeta: () => ({ + id: headerCellId1, + colIndex: -1, + rowIndex: -1, + }), + } as any, + id: headerCellId1, + }, + ]; + s2.getColumnNodes = jest.fn(() => columnNode) as any; + s2.getRowNodes = jest.fn(() => []); + + const firstDataCellInfo = createMockCellInfo( + `${headerCellId0}[&]first-data-cell`, + ); + s2.getCell = () => firstDataCellInfo.mockCell as any; + + s2.setOptions({ + interaction: { + selectedCellHighlight: { + colHeader: true, + } as InteractionCellSelectedHighlightType, + }, + }); + + const mockHeaderCellInfo = createMockCellInfo(headerCellId0, { + colIndex: columnNode[0].belongsCell!.getMeta().colIndex, + rowIndex: columnNode[0].belongsCell!.getMeta().rowIndex, + }); + + s2.interaction.getAllColHeaderCells = jest.fn(); + s2.interaction.updateCells = jest.fn(); + + s2.emit(S2Event.DATA_CELL_CLICK, { + stopPropagation() {}, + } as unknown as GEvent); + + expect(s2.interaction.getState()).toEqual({ + cells: [firstDataCellInfo.mockCellMeta, mockHeaderCellInfo.mockCellMeta], + stateName: InteractionStateName.SELECTED, + onUpdateCells: expect.any(Function), + }); + expect(s2.interaction.getAllColHeaderCells).toHaveBeenCalled(); + expect(s2.interaction.updateCells).toHaveBeenCalled(); + }); }); diff --git a/packages/s2-core/__tests__/unit/interaction/brush-selection/data-brush-selection-spec.ts b/packages/s2-core/__tests__/unit/interaction/brush-selection/data-brush-selection-spec.ts index 8829f1000a..78387a6248 100644 --- a/packages/s2-core/__tests__/unit/interaction/brush-selection/data-brush-selection-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/brush-selection/data-brush-selection-spec.ts @@ -97,6 +97,12 @@ describe('Interaction Data Cell Brush Selection Tests', () => { mockSpreadSheetInstance.showTooltipWithInfo = jest.fn(); mockRootInteraction.getPanelGroupAllDataCells = () => panelGroupAllDataCells; + mockRootInteraction.getSelectedCellHighlight = () => ({ + rowHeader: false, + colHeader: false, + currentRow: false, + currentCol: false, + }); mockSpreadSheetInstance.interaction = mockRootInteraction; mockSpreadSheetInstance.render(); mockSpreadSheetInstance.facet.foregroundGroup = new Group(); @@ -138,6 +144,12 @@ describe('Interaction Data Cell Brush Selection Tests', () => { mockSpreadSheetInstance.setOptions({ interaction: { selectedCellHighlight: true }, }); + mockRootInteraction.getSelectedCellHighlight = () => ({ + rowHeader: true, + colHeader: true, + currentRow: false, + currentCol: false, + }); brushSelectionInstance.getBrushRange = () => { return { diff --git a/packages/s2-core/__tests__/unit/interaction/brush-selection/row-brush-selection-spec.ts b/packages/s2-core/__tests__/unit/interaction/brush-selection/row-brush-selection-spec.ts index 1949e45ed3..e97caaae2e 100644 --- a/packages/s2-core/__tests__/unit/interaction/brush-selection/row-brush-selection-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/brush-selection/row-brush-selection-spec.ts @@ -243,7 +243,7 @@ describe('Interaction Row Cell Brush Selection Tests', () => { }, }, }); - const rowCells = map(new Array(8), (a, i) => { + const currentRow = map(new Array(8), (a, i) => { const customY = 30 * i + 30; return { @@ -260,7 +260,7 @@ describe('Interaction Row Cell Brush Selection Tests', () => { } as RowCell; }); - mockRootInteraction.getAllRowHeaderCells = () => rowCells; + mockRootInteraction.getAllRowHeaderCells = () => currentRow; mockSpreadSheetInstance.interaction = mockRootInteraction; mockSpreadSheetInstance.facet.setScrollOffset({ hRowScrollX: 100, @@ -301,7 +301,7 @@ describe('Interaction Row Cell Brush Selection Tests', () => { ); // get brush range selected cells expect(brushSelectionInstance.brushRangeCells).toHaveLength( - rowCells.length / 2, + currentRow.length / 2, ); }); }); diff --git a/packages/s2-core/__tests__/unit/interaction/data-cell-multi-selection-spec.ts b/packages/s2-core/__tests__/unit/interaction/data-cell-multi-selection-spec.ts index 599671a2c0..4b3dc55df3 100644 --- a/packages/s2-core/__tests__/unit/interaction/data-cell-multi-selection-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/data-cell-multi-selection-spec.ts @@ -101,6 +101,7 @@ describe('Interaction Data Cell Multi Selection Tests', () => { expect(s2.interaction.getState()).toEqual({ cells: [mockCellA.mockCellMeta, mockCellB.mockCellMeta], stateName: InteractionStateName.SELECTED, + onUpdateCells: expect.any(Function), }); expect( @@ -143,6 +144,7 @@ describe('Interaction Data Cell Multi Selection Tests', () => { expect(s2.interaction.getState()).toEqual({ cells: [mockCellB.mockCellMeta], stateName: InteractionStateName.SELECTED, + onUpdateCells: expect.any(Function), }); expect( diff --git a/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts b/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts index 3838f695e2..c7e400abe5 100644 --- a/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts +++ b/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts @@ -485,6 +485,19 @@ describe('PivotSheet Tests', () => { expect(afterRender).toHaveBeenCalledTimes(1); }); + test('should emit after real dataCell render', () => { + const afterRealDataCellRender = jest.fn(); + const sheet = new PivotSheet(container, dataCfg, s2Options); + + sheet.on( + S2Event.LAYOUT_AFTER_REAL_DATA_CELL_RENDER, + afterRealDataCellRender, + ); + sheet.render(); + + expect(afterRealDataCellRender).toHaveBeenCalledTimes(1); + }); + test('should updatePagination', () => { s2.updatePagination({ current: 2, diff --git a/packages/s2-core/__tests__/unit/utils/__snapshots__/g-mini-charts-spec.ts.snap b/packages/s2-core/__tests__/unit/utils/__snapshots__/g-mini-charts-spec.ts.snap new file mode 100644 index 0000000000..bc371868c4 --- /dev/null +++ b/packages/s2-core/__tests__/unit/utils/__snapshots__/g-mini-charts-spec.ts.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`drawInterval Test should get right condition interval when only set fill 1`] = ` +Array [ + 100, + 28.128313891834573, + 48.14422057264051, + 54.30805938494168, + 19.591728525980912, + 20.90402969247084, + 22.50795334040297, + 27.637857900318135, + 67.57688229056204, + 5.129904559915165, + 92.64316012725344, + 7.807529162248144, + 29.241781548250266, + 26.497879109225874, + 27.67762460233298, + 29.16224814422057, + 9.278897136797456, + 14.03764581124072, + 11.930010604453871, + 15.734358430540826, + 27.704135737009544, + 0, + 29.32131495227996, + 29.334570519618243, + 14.554612937433722, + 14.700424178154826, + 16.940615058324497, + 18.411983032873806, + 49.82767762460234, + 37.53976670201485, + 43.82290562036055, + 1.4183457051961823, +] +`; diff --git a/packages/s2-core/__tests__/unit/utils/cell/table-col-cell.spec.ts b/packages/s2-core/__tests__/unit/utils/cell/table-col-cell-spec.ts similarity index 98% rename from packages/s2-core/__tests__/unit/utils/cell/table-col-cell.spec.ts rename to packages/s2-core/__tests__/unit/utils/cell/table-col-cell-spec.ts index 0839773cfe..7281b986d0 100644 --- a/packages/s2-core/__tests__/unit/utils/cell/table-col-cell.spec.ts +++ b/packages/s2-core/__tests__/unit/utils/cell/table-col-cell-spec.ts @@ -40,7 +40,7 @@ describe('Table col cell util test', () => { }, }, }), - ).toBe(80); + ).toBe(81); }); test('should return right occupied width for table col with actionIcons', () => { @@ -86,6 +86,6 @@ describe('Table col cell util test', () => { }, }, }), - ).toBe(105); + ).toBe(106); }); }); diff --git a/packages/s2-core/__tests__/unit/utils/color-spec.ts b/packages/s2-core/__tests__/unit/utils/color-spec.ts index 99d8107eab..38d46ef9db 100644 --- a/packages/s2-core/__tests__/unit/utils/color-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/color-spec.ts @@ -1,4 +1,4 @@ -import { getPalette } from '@/utils'; +import { getPalette, shouldReverseFontColor } from '@/utils'; import { generatePalette, generateStandardColors } from '@/utils/color'; const expectThemeColor = '#F1535F'; @@ -51,14 +51,16 @@ describe('Theme Color Tests', () => { function renderEmptyPalette() { generatePalette(); } - expect(renderEmptyPalette).not.toThrow(); + + expect(renderEmptyPalette).not.toThrowError(); }); test('should not throw error when receive empty brand color', () => { function renderStandardColors() { generateStandardColors(undefined as unknown as string); } - expect(renderStandardColors).not.toThrow(); + + expect(renderStandardColors).not.toThrowError(); }); test('should get standard color if brand color is empty', () => { @@ -80,4 +82,33 @@ describe('Theme Color Tests', () => { ); expect(generateStandardColors('')).toEqual(colors); }); + + // 主要测试一些主题色和亮度中间区域的颜色,保证修改后自带主题色不受影响 + test('should use reverse font color when background colors are these', () => { + const backgroundColors = [ + '#4174f0', + '#999999', + '#7F7F7F', + '#404040', + '#000000', + ]; + + backgroundColors.forEach((color) => { + expect(shouldReverseFontColor(color)).toBeTruthy(); + }); + }); + + test('should use default font color when background colors are these', () => { + const backgroundColors = [ + '#c4e0fa', + '#f2f2f2', + '#ffffff', + '#C0C0C0', + '#e1e9fb', + '#f0f2f4', + ]; + backgroundColors.forEach((color) => { + expect(shouldReverseFontColor(color)).toBeFalsy(); + }); + }); }); diff --git a/packages/s2-core/__tests__/unit/utils/export/copy-spec.ts b/packages/s2-core/__tests__/unit/utils/export/copy-spec.ts index a19587d79c..ffaca6fce3 100644 --- a/packages/s2-core/__tests__/unit/utils/export/copy-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/export/copy-spec.ts @@ -1,3 +1,4 @@ +import type { S2DataConfig } from '@antv/s2'; import { assembleDataCfg, assembleOptions, TOTALS_OPTIONS } from 'tests/util'; import { getContainer } from 'tests/util/helpers'; import { data as originalData, totalData } from 'tests/data/mock-dataset.json'; @@ -324,6 +325,27 @@ describe('List Table Core Data Process', () => { const data = getSelectedData(sss); expect(data).toBe(convertString(newLineText)); }); + + it('should copy row data when select data row cell', () => { + s2.setOptions({ + interaction: { + selectedCellHighlight: { + currentRow: true, + }, + }, + }); + + const cell = s2.interaction + .getAllCells() + .filter(({ cellType }) => cellType === CellTypes.DATA_CELL)[0]; + + s2.interaction.changeState({ + cells: [getCellMeta(cell)], + stateName: InteractionStateName.SELECTED, + }); + + expect(getSelectedData(s2).split('\t').length).toBe(5); + }); }); describe('Pivot Table Core Data Process', () => { @@ -336,24 +358,28 @@ describe('Pivot Table Core Data Process', () => { // 2 = ['province', 'city'].length 列头宽度 const ROW_HEADER_WIDTH = 2; - const s2 = new PivotSheet( - getContainer(), - assembleDataCfg({ + function getDataCfg() { + return assembleDataCfg({ meta: [], fields: { columns: ['type', 'sub_type'], rows: ['province', 'city'], values: ['number'], }, - }), - assembleOptions({ + }); + } + + function getOptions() { + return assembleOptions({ hierarchyType: 'grid', interaction: { enableCopy: true, }, totals: TOTALS_OPTIONS, - }), - ); + }); + } + + const s2 = new PivotSheet(getContainer(), getDataCfg(), getOptions()); s2.render(); it('should copy no data in grid mode', () => { @@ -409,14 +435,7 @@ describe('Pivot Table Core Data Process', () => { it('should copy row data in grid mode', () => { const ss = new PivotSheet( getContainer(), - assembleDataCfg({ - meta: [], - fields: { - columns: ['type', 'sub_type'], - rows: ['province', 'city'], - values: ['number'], - }, - }), + getDataCfg(), assembleOptions({ hierarchyType: 'grid', interaction: { @@ -724,6 +743,269 @@ describe('Pivot Table Core Data Process', () => { const data = getSelectedData(s2New); expect(data).toBe(convertString(`7789\n元`)); }); + + it('should get correct data with hideMeasureColumn is true', () => { + const ss = new PivotSheet(getContainer(), getDataCfg(), getOptions()); + ss.setOptions({ + style: { + colCell: { + hideValue: true, + }, + }, + }); + ss.render(); + const cells = ss.interaction + .getAllCells() + .filter(({ cellType }) => cellType === CellTypes.DATA_CELL); + ss.interaction.changeState({ + cells: map(cells, getCellMeta), + stateName: InteractionStateName.SELECTED, + }); + const data = getSelectedData(ss); + expect(data).toMatchInlineSnapshot(` + "7789 5343 13132 945 1343 + 2367 632 2999 1304 1354 + 3877 7234 11111 1145 1523 + 4342 834 5176 1432 1634 + 18375 14043 32418 4826 5854 + 1723 2451 4174 2335 4004 + 1822 2244 4066 245 3077 + 1943 2333 4276 2457 3551 + 2330 2445 4775 2458 352 + 7818 9473 17291 7495 10984 + 26193 23516 49709 12321 16838" + `); + }); + + // https://github.com/antvis/S2/issues/1955 + it('should get correct data with hideMeasureColumn、showSeriesNumber and copyWithHeader are all true', () => { + const ss = new PivotSheet(getContainer(), getDataCfg(), getOptions()); + ss.setOptions({ + style: { + colCell: { + hideValue: true, + }, + }, + interaction: { + enableCopy: true, + copyWithHeader: true, + }, + showSeriesNumber: true, + }); + ss.render(); + const cells = ss.interaction + .getAllCells() + .filter(({ cellType }) => cellType === CellTypes.DATA_CELL); + ss.interaction.changeState({ + cells: map(cells, getCellMeta), + stateName: InteractionStateName.SELECTED, + }); + const data = getSelectedData(ss); + expect(data).toMatchInlineSnapshot(` + " 家具 家具 家具 办公用品 + 桌子 沙发 小计 笔 + 浙江省 杭州市 7789 5343 13132 945 + 浙江省 绍兴市 2367 632 2999 1304 + 浙江省 宁波市 3877 7234 11111 1145 + 浙江省 舟山市 4342 834 5176 1432 + 浙江省 小计 18375 14043 32418 4826 + 四川省 成都市 1723 2451 4174 2335 + 四川省 绵阳市 1822 2244 4066 245 + 四川省 南充市 1943 2333 4276 2457 + 四川省 乐山市 2330 2445 4775 2458 + 四川省 小计 7818 9473 17291 7495 + 总计 26193 23516 49709 12321" + `); + }); +}); + +describe('Tree Table Core Data Process', () => { + let s2: PivotSheet; + + function setSelectedVisibleCell() { + const cell = s2.interaction + .getAllCells() + .filter(({ cellType }) => cellType === CellTypes.DATA_CELL); + + s2.interaction.changeState({ + cells: map(cell, getCellMeta), + stateName: InteractionStateName.SELECTED, + }); + } + + beforeEach(() => { + s2 = new PivotSheet( + getContainer(), + assembleDataCfg({ + meta: [], + fields: { + columns: ['type', 'sub_type'], + rows: ['province', 'city'], + values: ['number'], + }, + }), + assembleOptions({ + hierarchyType: 'tree', + interaction: { + enableCopy: true, + }, + totals: TOTALS_OPTIONS, + }), + ); + s2.render(); + }); + + it('should copy no data in tree mode', () => { + s2.interaction.changeState({ + cells: [], + stateName: InteractionStateName.SELECTED, + }); + const data = getSelectedData(s2); + expect(data).toBe(''); + }); + + it('should copy normal data in tree mode', () => { + setSelectedVisibleCell(); + + expect(getSelectedData(s2)).toMatchInlineSnapshot(` + "18375 14043 32418 4826 5854 + 7789 5343 13132 945 1343 + 2367 632 2999 1304 1354 + 3877 7234 11111 1145 1523 + 4342 834 5176 1432 1634 + 7818 9473 17291 7495 10984 + 1723 2451 4174 2335 4004 + 1822 2244 4066 245 3077 + 1943 2333 4276 2457 3551 + 2330 2445 4775 2458 352 + 26193 23516 49709 12321 16838" + `); + }); + + it('should copy col data in grid tree', () => { + const cell = s2.interaction + .getAllCells() + .filter(({ cellType }) => cellType === CellTypes.COL_CELL)[0]; + + s2.interaction.changeState({ + cells: [getCellMeta(cell)], + stateName: InteractionStateName.SELECTED, + }); + + expect(getSelectedData(s2)).toMatchInlineSnapshot(` + "18375 14043 32418 + 7789 5343 13132 + 2367 632 2999 + 3877 7234 11111 + 4342 834 5176 + 7818 9473 17291 + 1723 2451 4174 + 1822 2244 4066 + 1943 2333 4276 + 2330 2445 4775 + 26193 23516 49709" + `); + }); + + it('should copy row data in grid tree', () => { + const cell = s2.interaction + .getAllCells() + .filter(({ cellType }) => cellType === CellTypes.ROW_CELL)[0]; + + s2.interaction.changeState({ + cells: [getCellMeta(cell)], + stateName: InteractionStateName.SELECTED, + }); + + expect(getSelectedData(s2)).toMatchInlineSnapshot(` + "18375 14043 32418 4826 5854 10680 43098 + 7789 5343 13132 945 1343 2288 15420 + 2367 632 2999 1304 1354 2658 5657 + 3877 7234 11111 1145 1523 2668 13779 + 4342 834 5176 1432 1634 3066 8242" + `); + }); + + it('should copy all data in tree mode', () => { + s2.interaction.changeState({ + stateName: InteractionStateName.ALL_SELECTED, + }); + + expect(getSelectedData(s2)).toMatchInlineSnapshot(` + "18375 14043 32418 4826 5854 10680 43098 + 7789 5343 13132 945 1343 2288 15420 + 2367 632 2999 1304 1354 2658 5657 + 3877 7234 11111 1145 1523 2668 13779 + 4342 834 5176 1432 1634 3066 8242 + 7818 9473 17291 7495 10984 18479 35770 + 1723 2451 4174 2335 4004 6339 10513 + 1822 2244 4066 245 3077 3322 7388 + 1943 2333 4276 2457 3551 6008 10284 + 2330 2445 4775 2458 352 2810 7585 + 26193 23516 49709 12321 16838 29159 78868" + `); + }); + + it('should copy all data in tree mode with format', () => { + s2.setDataCfg({ + meta: [{ field: 'number', formatter: (v) => v + '元' }], + fields: { + columns: ['type', 'sub_type'], + rows: ['province', 'city'], + values: ['number'], + }, + } as S2DataConfig); + s2.setOptions({ + interaction: { + copyWithFormat: true, + }, + }); + s2.render(); + + setSelectedVisibleCell(); + + expect(getSelectedData(s2)).toMatchInlineSnapshot(` + "18375元 14043元 32418 4826元 5854元 + 7789元 5343元 13132 945元 1343元 + 2367元 632元 2999 1304元 1354元 + 3877元 7234元 11111 1145元 1523元 + 4342元 834元 5176 1432元 1634元 + 7818元 9473元 17291 7495元 10984元 + 1723元 2451元 4174 2335元 4004元 + 1822元 2244元 4066 245元 3077元 + 1943元 2333元 4276 2457元 3551元 + 2330元 2445元 4775 2458元 352元 + 26193元 23516元 49709 12321元 16838元" + `); + }); + + it('should copy normal data with header in tree mode', () => { + s2.setOptions({ + interaction: { + copyWithHeader: true, + }, + }); + s2.render(); + + setSelectedVisibleCell(); + + expect(getSelectedData(s2)).toMatchInlineSnapshot(` + " 家具 家具 家具 办公用品 办公用品 + 桌子 沙发 小计 笔 纸张 + number number number number + 浙江省 18375 14043 32418 4826 5854 + 浙江省 7789 5343 13132 945 1343 + 浙江省 2367 632 2999 1304 1354 + 浙江省 3877 7234 11111 1145 1523 + 浙江省 4342 834 5176 1432 1634 + 四川省 7818 9473 17291 7495 10984 + 四川省 1723 2451 4174 2335 4004 + 四川省 1822 2244 4066 245 3077 + 四川省 1943 2333 4276 2457 3551 + 四川省 2330 2445 4775 2458 352 + 总计 26193 23516 49709 12321 16838" + `); + }); }); describe('List Table getCopyData', () => { diff --git a/packages/s2-core/__tests__/unit/utils/g-mini-charts-spec.ts b/packages/s2-core/__tests__/unit/utils/g-mini-charts-spec.ts index 709adbf592..e6e06f1932 100644 --- a/packages/s2-core/__tests__/unit/utils/g-mini-charts-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/g-mini-charts-spec.ts @@ -1,10 +1,18 @@ +import { assembleDataCfg, assembleOptions } from 'tests/util'; +import { getContainer } from 'tests/util/helpers'; +import { forEach, map } from 'lodash'; +import { data } from 'tests/data/mock-dataset.json'; import type { RangeColors } from '../../../src/common/interface/theme'; +import { PivotSheet } from '@/sheet-type'; +import { CellTypes, MiniChartTypes, type S2CellType } from '@/common'; import { getBulletRangeColor, transformRatioToPercent, scale, + drawInterval, } from '@/utils/g-mini-charts'; -import { MiniChartTypes, type S2CellType } from '@/common'; +import type { DataCell } from '@/cell'; +import { getTheme } from '@/theme'; describe('MiniCharts Utils Tests', () => { const padding = { @@ -254,9 +262,9 @@ describe('MiniCharts Utils Tests', () => { expect(getBulletRangeColor(0.09788888, 0.19788888, rangeColors)).toEqual( 'green', ); - expect(getBulletRangeColor('测试', '牛批', rangeColors)).toEqual('red'); + expect(getBulletRangeColor('测试', '牛啊', rangeColors)).toEqual('red'); expect(getBulletRangeColor('测试', 0.2, rangeColors)).toEqual('red'); - expect(getBulletRangeColor(0.2, '牛批', rangeColors)).toEqual('red'); + expect(getBulletRangeColor(0.2, '牛啊', rangeColors)).toEqual('red'); // toFixed(2) 四舍五入精度问题 expect(getBulletRangeColor(0.09775, 0.1978, rangeColors)).toEqual('yellow'); @@ -268,7 +276,7 @@ describe('MiniCharts Utils Tests', () => { expect(transformRatioToPercent(0.2)).toEqual('20%'); expect(transformRatioToPercent('0.2')).toEqual('20%'); expect(transformRatioToPercent('test')).toEqual('test'); - expect(transformRatioToPercent('牛批')).toEqual('牛批'); + expect(transformRatioToPercent('牛啊')).toEqual('牛啊'); expect(transformRatioToPercent(0.02)).toEqual('2%'); expect(transformRatioToPercent(0.02, 2)).toEqual('2.00%'); expect(transformRatioToPercent(-122.2)).toEqual('-12220%'); @@ -302,3 +310,123 @@ describe('MiniCharts Utils Tests', () => { expect(transformRatioToPercent(0.09, 2)).toEqual('9.00%'); }); }); + +describe('drawInterval Test', () => { + const dataCfg = assembleDataCfg({ + meta: [], + fields: { + columns: ['type', 'sub_type'], + rows: ['province', 'city'], + values: ['number'], + }, + data, + }); + + const horizontalBorderWidth = + getTheme({})?.dataCell?.cell?.horizontalBorderWidth ?? 1; + const options = assembleOptions({ + style: { + dataCell: { + // 计算条形图的宽度时需要去掉 border width + width: 100 + horizontalBorderWidth, + }, + }, + conditions: {}, + }); + + const s2 = new PivotSheet(getContainer(), dataCfg, options); + + beforeEach(() => { + s2.render(); + }); + + test('should get right condition interval when only set fill', () => { + s2.setOptions({ + conditions: { + interval: [ + { + field: 'number', + mapping() { + return { + fill: 'pink', + }; + }, + }, + ], + }, + }); + s2.render(); + + const cells = s2.interaction + .getAllCells() + .filter(({ cellType }) => cellType === CellTypes.DATA_CELL); + + const allIntervalWidth = map( + cells, + (cell) => drawInterval(cell as DataCell)?.style?.width ?? 0, + ); + + expect(allIntervalWidth).toMatchSnapshot(); + }); + + test('should get right condition interval when minValue and maxValue is custom', () => { + s2.setOptions({ + conditions: { + interval: [ + { + field: 'number', + mapping() { + return { + fill: 'pink', + isCompare: true, + minValue: 0, + maxValue: 400, + }; + }, + }, + ], + }, + }); + s2.render(); + + const cells = s2.interaction + .getAllCells() + .filter(({ cellType }) => cellType === CellTypes.DATA_CELL); + + const firstIntervalInfo = drawInterval(cells[0] as DataCell); + const lastIntervalInfo = drawInterval(cells[cells.length - 1] as DataCell); + + expect(firstIntervalInfo?.style.width).toEqual(undefined); + expect(lastIntervalInfo?.style.width).toEqual(88); + }); + + test('should get right condition interval when filedValue is custom', () => { + s2.setOptions({ + conditions: { + interval: [ + { + field: 'number', + mapping() { + return { + isCompare: true, + minValue: 0, + maxValue: 400, + fieldValue: 200, + fill: 'pink', + }; + }, + }, + ], + }, + }); + s2.render(); + + const cells = s2.interaction + .getAllCells() + .filter(({ cellType }) => cellType === CellTypes.DATA_CELL); + forEach(cells, (cell) => { + const intervalInfo = drawInterval(cell as DataCell); + expect(intervalInfo?.style.width).toEqual(50); + }); + }); +}); diff --git a/packages/s2-core/__tests__/util/helpers.ts b/packages/s2-core/__tests__/util/helpers.ts index fbae523885..d9ae0a2a52 100644 --- a/packages/s2-core/__tests__/util/helpers.ts +++ b/packages/s2-core/__tests__/util/helpers.ts @@ -155,13 +155,14 @@ export function getGradient( export const createMockCellInfo = ( cellId: string, - { colIndex = 0, rowIndex = 0, level = 0 } = {}, + { colIndex = 0, rowIndex = 0, colId = '0', level = 0 } = {}, ) => { const mockCellViewMeta: Partial = { id: cellId, field: cellId, colIndex, rowIndex, + colId, level, type: undefined, x: 0, @@ -190,6 +191,7 @@ export const createMockCellInfo = ( 'spreadsheet', 'level', 'field', + 'colId', ]); const mockCell = { ...mockCellViewMeta, diff --git a/packages/s2-core/src/cell/base-cell.ts b/packages/s2-core/src/cell/base-cell.ts index fdc5ea2454..614b87b375 100644 --- a/packages/s2-core/src/cell/base-cell.ts +++ b/packages/s2-core/src/cell/base-cell.ts @@ -103,7 +103,9 @@ export abstract class BaseCell extends Group { this.theme = spreadsheet.theme; this.conditions = this.spreadsheet.options.conditions!; this.handleRestOptions(...restOptions); - this.initCell(); + if (this.shouldInit()) { + this.initCell(); + } } public getMeta(): T { @@ -188,6 +190,11 @@ export abstract class BaseCell extends Group { /* common functions that will be used in subtype */ /* -------------------------------------------------------------------------- */ + protected shouldInit() { + const { width, height } = this.meta; + return width > 0 && height > 0; + } + public getStyle( name?: K, ): DefaultCellTheme | S2Theme[K] { diff --git a/packages/s2-core/src/cell/data-cell.ts b/packages/s2-core/src/cell/data-cell.ts index 0c60ec6461..8f1247ef95 100644 --- a/packages/s2-core/src/cell/data-cell.ts +++ b/packages/s2-core/src/cell/data-cell.ts @@ -1,6 +1,5 @@ import type { PointLike } from '@antv/g'; import { find, findLast, first, get, isEmpty, isEqual } from 'lodash'; -import tinycolor from 'tinycolor2'; import { BaseCell } from '../cell/base-cell'; import { CellTypes, @@ -23,16 +22,20 @@ import type { ViewMetaIndexType, } from '../common/interface'; import { getMaxTextWidth } from '../utils/cell/cell'; -import { includeCell } from '../utils/cell/data-cell'; +import { + includeCell, + shouldUpdateBySelectedCellsHighlight, + updateBySelectedCellsHighlight, +} from '../utils/cell/data-cell'; import { getIconPositionCfg } from '../utils/condition/condition'; import { updateShapeAttr } from '../utils/g-renders'; import { EMPTY_PLACEHOLDER } from '../common/constant/basic'; import { drawInterval } from '../utils/g-mini-charts'; import { DEFAULT_FONT_COLOR, - FONT_COLOR_BRIGHTNESS_THRESHOLD, REVERSE_FONT_COLOR, } from '../common/constant/condition'; +import { shouldReverseFontColor } from '../utils/color'; import { getFieldValueOfViewMetaData } from '../data-set/cell-data'; import type { RawData } from './../common/interface/s2DataConfig'; @@ -87,6 +90,7 @@ export class DataCell extends BaseCell { protected handleSelect(cells: CellMeta[]) { const currentCellType = cells?.[0]?.type; + switch (currentCellType) { // 列多选 case CellTypes.COL_CELL: @@ -98,7 +102,9 @@ export class DataCell extends BaseCell { break; // 单元格单选/多选 case CellTypes.DATA_CELL: - if (includeCell(cells, this)) { + if (shouldUpdateBySelectedCellsHighlight(this.spreadsheet)) { + updateBySelectedCellsHighlight(cells, this, this.spreadsheet); + } else if (includeCell(cells, this)) { this.updateByState(InteractionStateName.SELECTED); } else if ( this.spreadsheet.options.interaction?.selectedCellsSpotlight @@ -141,6 +147,7 @@ export class DataCell extends BaseCell { public update() { const stateName = this.spreadsheet.interaction.getCurrentStateName(); + // 获取当前 interaction 记录的 Cells 元信息列表,不仅仅是数据单元格,也可能是行头或者列头。 const cells = this.spreadsheet.interaction.getCells(); if (stateName === InteractionStateName.ALL_SELECTED) { @@ -202,13 +209,9 @@ export class DataCell extends BaseCell { const { backgroundColor, intelligentReverseTextColor } = this.getBackgroundColor(); - const isMoreThanThreshold = - tinycolor(backgroundColor).getBrightness() <= - FONT_COLOR_BRIGHTNESS_THRESHOLD; - // text 默认为黑色,当背景颜色亮度过低时,修改 text 为白色 if ( - isMoreThanThreshold && + shouldReverseFontColor(backgroundColor as string) && textStyle.fill === DEFAULT_FONT_COLOR && intelligentReverseTextColor ) { diff --git a/packages/s2-core/src/cell/header-cell.ts b/packages/s2-core/src/cell/header-cell.ts index 213ce197cf..693115b4dc 100644 --- a/packages/s2-core/src/cell/header-cell.ts +++ b/packages/s2-core/src/cell/header-cell.ts @@ -310,6 +310,7 @@ export abstract class HeaderCell extends BaseCell { if (includeCell(cells, this)) { this.updateByState(InteractionStateName.SELECTED); } + const selectedNodeIds = map(nodes, 'id'); if (includes(selectedNodeIds, this.meta.id)) { this.updateByState(InteractionStateName.SELECTED); @@ -363,8 +364,10 @@ export abstract class HeaderCell extends BaseCell { public update() { const { interaction } = this.spreadsheet; const stateInfo = interaction?.getState(); - const cells = interaction?.getCells(); - + const cells = interaction?.getCells([ + CellTypes.COL_CELL, + CellTypes.ROW_CELL, + ]); if (!first(cells)) { return; } diff --git a/packages/s2-core/src/cell/row-cell.ts b/packages/s2-core/src/cell/row-cell.ts index a2e9306c01..2e7bbe1606 100644 --- a/packages/s2-core/src/cell/row-cell.ts +++ b/packages/s2-core/src/cell/row-cell.ts @@ -203,7 +203,7 @@ export class RowCell extends HeaderCell { const { linkTextFill } = this.getTextStyle(); super.drawLinkFieldShape( - linkFields.includes(this.meta.field), + (linkFields as string[]).includes(this.meta.field), linkTextFill!, ); } diff --git a/packages/s2-core/src/cell/table-data-cell.ts b/packages/s2-core/src/cell/table-data-cell.ts index e0ed53399c..7cf548395a 100644 --- a/packages/s2-core/src/cell/table-data-cell.ts +++ b/packages/s2-core/src/cell/table-data-cell.ts @@ -14,6 +14,8 @@ import { getOrCreateResizeAreaGroupById, getResizeAreaAttrs, } from '../utils/interaction/resize'; +import { checkIsLinkField } from '../utils/interaction/link-field'; + import { CustomRect } from '../engine'; import { BaseCell } from './base-cell'; @@ -27,10 +29,9 @@ export class TableDataCell extends DataCell { const { linkFields = [] } = this.spreadsheet.options.interaction!; const linkTextFill = this.theme.rowCell!.text!.linkTextFill!; - super.drawLinkFieldShape( - linkFields.includes(this.meta.valueField), - linkTextFill, - ); + const isLinkField = checkIsLinkField(linkFields, this.meta); + + super.drawLinkFieldShape(isLinkField, linkTextFill); } public override drawBorders() { diff --git a/packages/s2-core/src/common/constant/condition.ts b/packages/s2-core/src/common/constant/condition.ts index ffbb51c2a5..5196c1651b 100644 --- a/packages/s2-core/src/common/constant/condition.ts +++ b/packages/s2-core/src/common/constant/condition.ts @@ -1,9 +1,5 @@ export const VALUE_RANGES_KEY = 'valueRanges'; export const DEFAULT_VALUE_RANGES = {}; -/** - * 亮度范围 0~255 - * @see https://github.com/bgrins/TinyColor#getbrightness - */ -export const FONT_COLOR_BRIGHTNESS_THRESHOLD = 220; + export const DEFAULT_FONT_COLOR = '#000000'; export const REVERSE_FONT_COLOR = '#FFFFFF'; diff --git a/packages/s2-core/src/common/constant/events/basic.ts b/packages/s2-core/src/common/constant/events/basic.ts index f33e1f14b1..35074ce8c6 100644 --- a/packages/s2-core/src/common/constant/events/basic.ts +++ b/packages/s2-core/src/common/constant/events/basic.ts @@ -70,6 +70,7 @@ export enum S2Event { LAYOUT_CELL_SCROLL = 'layout:cell-scroll', LAYOUT_CELL_MOUNTED = 'layout:cell-mounted', LAYOUT_PAGINATION = 'layout:pagination', + LAYOUT_AFTER_REAL_DATA_CELL_RENDER = 'layout:after-real-data-cell-render', LAYOUT_AFTER_RENDER = 'layout:after-render', LAYOUT_BEFORE_RENDER = 'layout:before-render', LAYOUT_DESTROY = 'layout:destroy', diff --git a/packages/s2-core/src/common/constant/interaction.ts b/packages/s2-core/src/common/constant/interaction.ts index 5dc6b355c4..756a871944 100644 --- a/packages/s2-core/src/common/constant/interaction.ts +++ b/packages/s2-core/src/common/constant/interaction.ts @@ -112,3 +112,10 @@ export enum ScrollDirection { LEADING = 'leading', TRAILING = 'trailing', } + +export interface InteractionCellSelectedHighlightType { + rowHeader?: boolean; // 高亮行头 + colHeader?: boolean; // 高亮列头 + currentRow?: boolean; // 高亮选中单元格所在行 + currentCol?: boolean; // 高亮选中单元格所在列 +} diff --git a/packages/s2-core/src/common/icons/gui-icon.ts b/packages/s2-core/src/common/icons/gui-icon.ts index 88b1b1aaf6..f7f6508a65 100644 --- a/packages/s2-core/src/common/icons/gui-icon.ts +++ b/packages/s2-core/src/common/icons/gui-icon.ts @@ -107,6 +107,10 @@ export class GuiIcon extends Group { } else { this.getImage(name, cacheKey, fill) .then((value: HTMLImageElement) => { + // 加载完成后,当前 Cell 可能已经销毁了 + if (this.destroyed) { + return; + } image.attr('img', value); this.appendChild(image); }) diff --git a/packages/s2-core/src/common/interface/basic.ts b/packages/s2-core/src/common/interface/basic.ts index d5172efda3..aff1fbf133 100644 --- a/packages/s2-core/src/common/interface/basic.ts +++ b/packages/s2-core/src/common/interface/basic.ts @@ -348,6 +348,7 @@ export interface ViewMeta { label?: string; value?: string | number; query?: Query; + [key: string]: unknown; } @@ -414,3 +415,5 @@ export interface Point { x: number; y: number; } + +export type RowData = Data | CellData[]; diff --git a/packages/s2-core/src/common/interface/condition.ts b/packages/s2-core/src/common/interface/condition.ts index b2899370d2..aa017becd8 100644 --- a/packages/s2-core/src/common/interface/condition.ts +++ b/packages/s2-core/src/common/interface/condition.ts @@ -20,6 +20,11 @@ export interface MappingResult extends ValueRange { * @version 1.34.0 */ intelligentReverseTextColor?: boolean; + /** + * @description custom the interval condition's width + * @version 1.38.0 + */ + fieldValue?: number; } export type MappingFunction = ( diff --git a/packages/s2-core/src/common/interface/emitter.ts b/packages/s2-core/src/common/interface/emitter.ts index 61f3353e7f..b1a5e79dbe 100644 --- a/packages/s2-core/src/common/interface/emitter.ts +++ b/packages/s2-core/src/common/interface/emitter.ts @@ -1,4 +1,5 @@ import type { FederatedPointerEvent as CanvasEvent } from '@antv/g'; +import type { SpreadSheet } from '../../sheet-type'; import type { DataCell } from '../../cell/data-cell'; import type { RowCell } from '../../cell/row-cell'; import type { ColCell } from '../../cell/col-cell'; @@ -6,6 +7,7 @@ import type { S2Event } from '../../common/constant'; import type { CellMeta, CellScrollPosition, + Data, HiddenColumnsInfo, LayoutResult, RowCellCollapsedParams, @@ -59,8 +61,9 @@ export interface EmitterType { /** ================ Cell ================ */ [S2Event.GLOBAL_LINK_FIELD_JUMP]: (data: { + cellData: Node; field: string; - record: RawData; + record: Data; }) => void; /** ================ Date Cell ================ */ @@ -130,6 +133,11 @@ export interface EmitterType { current: number; }) => void; [S2Event.LAYOUT_AFTER_HEADER_LAYOUT]: (data: LayoutResult) => void; + [S2Event.LAYOUT_AFTER_REAL_DATA_CELL_RENDER]: (options: { + add: [number, number][]; + remove: [number, number][]; + spreadsheet: SpreadSheet; + }) => void; /** @deprecated 请使用 S2Event.GLOBAL_SCROLL 代替 */ [S2Event.LAYOUT_CELL_SCROLL]: (position: CellScrollPosition) => void; [S2Event.LAYOUT_CELL_MOUNTED]: (cell: S2CellType) => void; diff --git a/packages/s2-core/src/common/interface/interaction.ts b/packages/s2-core/src/common/interface/interaction.ts index 63c6a17ae4..3962e1a789 100644 --- a/packages/s2-core/src/common/interface/interaction.ts +++ b/packages/s2-core/src/common/interface/interaction.ts @@ -4,6 +4,7 @@ import type { CellTypes, InterceptType, ScrollbarPositionType, + InteractionCellSelectedHighlightType, } from '../constant'; import type { BaseCell, @@ -41,6 +42,7 @@ export interface CellMeta { colIndex: number; rowIndex: number; type: CellTypes; + rowQuery?: Record; [key: string]: unknown; } @@ -134,7 +136,7 @@ export interface BrushSelectionInfo { export interface InteractionOptions { // record which row/col field need extra link info - linkFields?: string[]; + linkFields?: string[] | ((meta: Node | ViewMeta) => boolean); // focus selected cell, like the spotlight selectedCellsSpotlight?: boolean; // highlight all row header cells and column header cells to which the hovered cell belongs @@ -168,7 +170,7 @@ export interface InteractionOptions { // https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener eventListenerOptions?: boolean | AddEventListenerOptions; // highlight col and row header for selected cell - selectedCellHighlight?: boolean; + selectedCellHighlight?: boolean | InteractionCellSelectedHighlightType; // https://developer.mozilla.org/en-US/docs/Web/CSS/overscroll-behavior overscrollBehavior?: 'auto' | 'none' | 'contain' | null; /** ***********CUSTOM INTERACTION HOOKS**************** */ diff --git a/packages/s2-core/src/data-set/base-data-set.ts b/packages/s2-core/src/data-set/base-data-set.ts index 7ec2ed49df..a37cf32b86 100644 --- a/packages/s2-core/src/data-set/base-data-set.ts +++ b/packages/s2-core/src/data-set/base-data-set.ts @@ -30,7 +30,8 @@ import { getValueRangeState, setValueRangeState, } from '../utils/condition/state-controller'; -import { CellTypes, type CustomHeaderField } from '../common'; +import { CellTypes } from '../common'; +import type { CellMeta, RowData, CustomHeaderField } from '../common'; import type { Query, TotalSelectionsOfMultiData } from './interface'; import type { CellData } from './cell-data'; import type { CellDataParams } from './index'; @@ -257,4 +258,10 @@ export abstract class BaseDataSet { public moreThanOneValue() { return this.fields?.values?.length! > 1; } + + /** + * get a row cells data including cell + * @param cells + */ + public abstract getRowData(cells: CellMeta): RowData; } diff --git a/packages/s2-core/src/data-set/pivot-data-set.ts b/packages/s2-core/src/data-set/pivot-data-set.ts index a1f1ae614e..9d3823b870 100644 --- a/packages/s2-core/src/data-set/pivot-data-set.ts +++ b/packages/s2-core/src/data-set/pivot-data-set.ts @@ -38,6 +38,7 @@ import type { S2DataConfig, TotalsStatus, ViewMeta, + RowData, } from '../common/interface'; import { Node } from '../facet/layout/node'; import { @@ -59,6 +60,7 @@ import { import { calcActionByType } from '../utils/number-calculate'; import { handleSortAction } from '../utils/sort-action'; import { DataSelectType } from '../common/constant/total'; +import type { CellMeta } from '../common'; import { CellData } from './cell-data'; import { BaseDataSet } from './base-data-set'; import type { @@ -615,4 +617,8 @@ export class PivotDataSet extends BaseDataSet { private isCustomMeasuresPosition(customValueOrder?: number) { return isNumber(customValueOrder); } + + public getRowData(cell: CellMeta): RowData { + return this.getMultiData(cell.rowQuery); + } } diff --git a/packages/s2-core/src/data-set/table-data-set.ts b/packages/s2-core/src/data-set/table-data-set.ts index c525a4a406..a032238ed8 100644 --- a/packages/s2-core/src/data-set/table-data-set.ts +++ b/packages/s2-core/src/data-set/table-data-set.ts @@ -1,6 +1,8 @@ import { each, orderBy, filter, includes, isFunction, isObject } from 'lodash'; import { isAscSort, isDescSort } from '..'; import type { S2DataConfig, RawData, Data } from '../common/interface'; +import type { CellMeta } from '../common'; +import type { RowData } from '../common/interface/basic'; import type { CellDataParams, Query } from './interface'; import { BaseDataSet } from './base-data-set'; @@ -163,4 +165,8 @@ export class TableDataSet extends BaseDataSet { public getMultiData(): Data[] { return this.displayData as Data[]; } + + public getRowData(cell: CellMeta): RowData { + return this.getCellData({ query: { rowIndex: cell.rowIndex } }); + } } diff --git a/packages/s2-core/src/facet/base-facet.ts b/packages/s2-core/src/facet/base-facet.ts index 90db7b3766..457a3a4a38 100644 --- a/packages/s2-core/src/facet/base-facet.ts +++ b/packages/s2-core/src/facet/base-facet.ts @@ -72,9 +72,11 @@ import { CornerBBox } from './bbox/cornerBBox'; import { PanelBBox } from './bbox/panelBBox'; import { ColHeader, + type ColHeaderConfig, CornerHeader, Frame, RowHeader, + type RowHeaderConfig, SeriesNumberHeader, } from './header'; import type { ViewCellHeights } from './layout/interface'; @@ -946,12 +948,12 @@ export abstract class BaseFacet { }; /** - 在当前表格滚动分两种情况: - 1. 当前表格无滚动条: 无需阻止外部容器滚动 - 2. 当前表格有滚动条: - - 未滚动到顶部或底部: 当前表格滚动, 阻止外部容器滚动 - - 滚动到顶部或底部: 恢复外部容器滚动 - */ + 在当前表格滚动分两种情况: + 1. 当前表格无滚动条: 无需阻止外部容器滚动 + 2. 当前表格有滚动条: + - 未滚动到顶部或底部: 当前表格滚动, 阻止外部容器滚动 + - 滚动到顶部或底部: 恢复外部容器滚动 + */ isScrollOverTheViewport = (scrollOffset: CellScrollOffset) => { const { deltaY, deltaX, offsetY } = scrollOffset; const isScrollOverTheHeader = offsetY <= this.cornerBBox.maxY; @@ -983,13 +985,13 @@ export abstract class BaseFacet { }; /** - https://developer.mozilla.org/zh-CN/docs/Web/CSS/overscroll-behavior - 阻止外部容器滚动: 表格是虚拟滚动, 这里按照标准模拟浏览器的 [overscroll-behavior] 实现 - 1. auto => 只有在滚动到表格顶部或底部时才触发外部容器滚动 - 1. contain => 默认的滚动边界行为不变(“触底”效果或者刷新),但是临近的滚动区域不会被滚动链影响到 - 2. none => 临近滚动区域不受到滚动链影响,而且默认的滚动到边界的表现也被阻止 - 所以只要不为 `auto`, 或者表格内, 都需要阻止外部容器滚动 - */ + https://developer.mozilla.org/zh-CN/docs/Web/CSS/overscroll-behavior + 阻止外部容器滚动: 表格是虚拟滚动, 这里按照标准模拟浏览器的 [overscroll-behavior] 实现 + 1. auto => 只有在滚动到表格顶部或底部时才触发外部容器滚动 + 1. contain => 默认的滚动边界行为不变(“触底”效果或者刷新),但是临近的滚动区域不会被滚动链影响到 + 2. none => 临近滚动区域不受到滚动链影响,而且默认的滚动到边界的表现也被阻止 + 所以只要不为 `auto`, 或者表格内, 都需要阻止外部容器滚动 + */ private stopScrollChainingIfNeeded = (event: WheelEvent) => { const { interaction } = this.spreadsheet.options; @@ -1157,6 +1159,11 @@ export abstract class BaseFacet { ); }); this.preCellIndexes = indexes; + this.spreadsheet.emit(S2Event.LAYOUT_AFTER_REAL_DATA_CELL_RENDER, { + add, + remove, + spreadsheet: this.spreadsheet, + }); }; protected init() { @@ -1261,7 +1268,7 @@ export abstract class BaseFacet { position: { x: seriesNumberWidth, y }, data: this.layoutResult.rowNodes, spreadsheet: this.spreadsheet, - }); + } as unknown as RowHeaderConfig); } return this.rowHeader; } @@ -1279,7 +1286,7 @@ export abstract class BaseFacet { data: this.layoutResult.colNodes, sortParam: this.spreadsheet.store.get('sortParam'), spreadsheet: this.spreadsheet, - }); + } as unknown as ColHeaderConfig); } return this.columnHeader; } @@ -1420,4 +1427,8 @@ export abstract class BaseFacet { detail.hideColumnNodes.some((node) => node.id === columnNode.id), ); } + + public getCornerNodes(): Node[] { + return this.cornerHeader?.getNodes() || []; + } } diff --git a/packages/s2-core/src/facet/bbox/cornerBBox.ts b/packages/s2-core/src/facet/bbox/cornerBBox.ts index b6ced1407f..e60890e5ce 100644 --- a/packages/s2-core/src/facet/bbox/cornerBBox.ts +++ b/packages/s2-core/src/facet/bbox/cornerBBox.ts @@ -12,9 +12,23 @@ export class CornerBBox extends BaseBBox { this.maxY = height; } - private getCornerBBoxHeight() { + private getCornerBBoxOriginalHeight() { const { colsHierarchy } = this.layoutResult; - this.originalHeight = Math.floor(colsHierarchy.height); + const { colCell } = this.spreadsheet.options.style!; + + /** + * 角头分为两部分, 一部分对应行头, 一部分对应列头, 角头的叶子节点对应行头节点, 以下极端情况让没有列头的时候也能 [行头对应的角头] 正常显示 + * 1. 只配置了 rows, 而 columns, values 都为空, 此时列头为空 + * 2. 配置了 rows, values, 此时存在一级列头 (即 EXTRA_FIELD 数值节点), 但是隐藏了数值 (hideMeasureColumn), 此时列头为空 + */ + if (!colsHierarchy.sampleNodeForLastLevel) { + return colCell?.height; + } + return Math.floor(colsHierarchy.height); + } + + private getCornerBBoxHeight() { + this.originalHeight = this.getCornerBBoxOriginalHeight() as number; return this.originalHeight; } diff --git a/packages/s2-core/src/facet/header/corner.ts b/packages/s2-core/src/facet/header/corner.ts index ff9a959419..730fa25bbf 100644 --- a/packages/s2-core/src/facet/header/corner.ts +++ b/packages/s2-core/src/facet/header/corner.ts @@ -64,7 +64,7 @@ export class CornerHeader extends BaseHeader { viewportHeight: height, seriesNumberWidth, spreadsheet, - }); + } as CornerHeaderConfig); } public static getTreeCornerText(options: BaseCornerOptions) { @@ -107,6 +107,7 @@ export class CornerHeader extends BaseHeader { options; const { rowsHierarchy, colsHierarchy } = layoutResult; const { rows = [], columns = [] } = spreadsheet?.dataSet?.fields || {}; + const { colCell } = spreadsheet.options.style!; const cornerNodes: Node[] = []; const leafNode = colsHierarchy?.sampleNodeForLastLevel; // check if show series number node @@ -130,53 +131,50 @@ export class CornerHeader extends BaseHeader { } // spreadsheet type tree mode - if (leafNode) { - if (spreadsheet.isHierarchyTreeType()) { - const cornerText = this.getTreeCornerText(options); + if (spreadsheet.isHierarchyTreeType()) { + const cornerText = this.getTreeCornerText(options); + const cornerNode: Node = new Node({ + id: '', + field: '', + value: cornerText, + }); + cornerNode.x = position.x + seriesNumberWidth; + cornerNode.y = leafNode?.y! ?? 0; + // cNode should subtract series width + cornerNode.width = width - seriesNumberWidth; + cornerNode.height = leafNode?.height! ?? (colCell?.height as number); + cornerNode.seriesNumberWidth = seriesNumberWidth; + cornerNode.isPivotMode = true; + cornerNode.spreadsheet = spreadsheet; + cornerNode.cornerType = CornerNodeType.Row; + cornerNodes.push(cornerNode); + } else { + const rowNodes = rowsHierarchy.sampleNodesForAllLevels || []; + const isCustomRow = spreadsheet.isCustomRowFields(); + + // spreadsheet type grid mode + rowNodes.forEach((rowNode) => { + // 自定义行头直接取采样的行头 field 值即可, 可通过 s2DataCfg.meta.name 自定义名称 + const field = isCustomRow + ? rowNode.field + : (rows[rowNode.level] as string); + + const value = spreadsheet.dataSet.getFieldName(field); const cornerNode: Node = new Node({ id: '', - field: '', - value: cornerText, + field, + value, }); - cornerNode.x = position.x + seriesNumberWidth; - cornerNode.y = colsHierarchy?.sampleNodeForLastLevel?.y!; - // cNode should subtract series width - cornerNode.width = width - seriesNumberWidth; - cornerNode.height = colsHierarchy?.sampleNodeForLastLevel?.height!; - cornerNode.seriesNumberWidth = seriesNumberWidth; + + cornerNode.x = rowNode.x + seriesNumberWidth; + cornerNode.y = leafNode?.y ?? 0; + cornerNode.width = rowNode.width; + cornerNode.height = leafNode?.height! ?? (colCell?.height as number); cornerNode.isPivotMode = true; - cornerNode.spreadsheet = spreadsheet; cornerNode.cornerType = CornerNodeType.Row; + cornerNode.spreadsheet = spreadsheet; cornerNodes.push(cornerNode); - } else { - const rowNodes = rowsHierarchy.sampleNodesForAllLevels || []; - const isCustomRow = spreadsheet.isCustomRowFields(); - - // spreadsheet type grid mode - rowNodes.forEach((rowNode) => { - // 自定义行头直接取采样的行头 field 值即可, 可通过 s2DataCfg.meta.name 自定义名称 - const field = isCustomRow - ? rowNode.field - : (rows[rowNode.level] as string); - - const value = spreadsheet.dataSet.getFieldName(field); - - const cornerNode: Node = new Node({ - id: '', - field, - value, - }); - - cornerNode.x = rowNode.x + seriesNumberWidth; - cornerNode.y = leafNode?.y; - cornerNode.width = rowNode.width; - cornerNode.height = leafNode?.height; - cornerNode.isPivotMode = true; - cornerNode.cornerType = CornerNodeType.Row; - cornerNode.spreadsheet = spreadsheet; - cornerNodes.push(cornerNode); - }); - } + }); } const columnNodes = colsHierarchy.sampleNodesForAllLevels || []; @@ -280,4 +278,8 @@ export class CornerHeader extends BaseHeader { }, }); } + + public getNodes(): Node[] { + return this.headerConfig.data || []; + } } diff --git a/packages/s2-core/src/facet/header/interface.ts b/packages/s2-core/src/facet/header/interface.ts index d6b450287d..97a77b1855 100644 --- a/packages/s2-core/src/facet/header/interface.ts +++ b/packages/s2-core/src/facet/header/interface.ts @@ -1,7 +1,12 @@ import type { PointLike } from '@antv/g'; -import type { LayoutResult, SortParam } from '../../common/interface'; +import type { + LayoutResult, + S2Options, + SortParam, +} from '../../common/interface'; import type { SpreadSheet } from '../../sheet-type'; import type { Node } from '../layout/node'; +import type { ViewMeta } from '../../common/interface'; /** * Base header config interface @@ -31,6 +36,10 @@ export interface BaseHeaderConfig { spreadsheet: SpreadSheet; // leaf node sort params sortParam?: SortParam; + // field ids that click to navigate (todo: 合并master 时加入的) + linkFields: string[] | ((meta: Node | ViewMeta) => boolean); + // type of hierarchy + hierarchyType: S2Options['hierarchyType']; } export interface ColHeaderConfig extends BaseHeaderConfig { diff --git a/packages/s2-core/src/facet/header/row.ts b/packages/s2-core/src/facet/header/row.ts index f746690bb2..eada6d33b9 100644 --- a/packages/s2-core/src/facet/header/row.ts +++ b/packages/s2-core/src/facet/header/row.ts @@ -1,6 +1,6 @@ import { Rect } from '@antv/g'; import { each, isEmpty } from 'lodash'; -import { RowCell } from '../../cell/row-cell'; +import { RowCell } from '../../cell'; import type { S2CellType } from '../../common/interface'; import type { Node } from '../layout/node'; import { translateGroup } from '../utils'; diff --git a/packages/s2-core/src/facet/header/series-number.ts b/packages/s2-core/src/facet/header/series-number.ts index 4686c7f7b2..3a4d4688c7 100644 --- a/packages/s2-core/src/facet/header/series-number.ts +++ b/packages/s2-core/src/facet/header/series-number.ts @@ -45,7 +45,7 @@ export class SeriesNumberHeader extends BaseHeader { spreadsheet, ), spreadsheet, - }); + } as BaseHeaderConfig); } constructor(cfg: BaseHeaderConfig) { diff --git a/packages/s2-core/src/facet/header/table-col.ts b/packages/s2-core/src/facet/header/table-col.ts index 9758308a3b..2d07eca37c 100644 --- a/packages/s2-core/src/facet/header/table-col.ts +++ b/packages/s2-core/src/facet/header/table-col.ts @@ -154,7 +154,7 @@ export class TableColHeader extends ColHeader { } public getScrollGroupClipBBox = () => { - const { width, height, scrollX = 0, spreadsheet } = this.headerConfig; + const { width, height, spreadsheet } = this.headerConfig; const colLeafNodes = spreadsheet.facet?.layoutResult.colLeafNodes; const frozenWidth = getFrozenColWidth( @@ -162,7 +162,7 @@ export class TableColHeader extends ColHeader { spreadsheet.options.frozen!, ); return { - x: scrollX + frozenWidth.frozenColWidth, + x: frozenWidth.frozenColWidth, y: 0, width: width - frozenWidth.frozenColWidth - frozenWidth.frozenTrailingColWidth, diff --git a/packages/s2-core/src/facet/pivot-facet.ts b/packages/s2-core/src/facet/pivot-facet.ts index cb0f44e52f..3fc0997ddb 100644 --- a/packages/s2-core/src/facet/pivot-facet.ts +++ b/packages/s2-core/src/facet/pivot-facet.ts @@ -5,6 +5,7 @@ import { isArray, isEmpty, isNil, + isNumber, keys, last, map, @@ -235,13 +236,13 @@ export class PivotFacet extends BaseFacet { const cellDraggedWidth = this.getColCellDraggedWidth(col); // 1. 拖拽后的宽度优先级最高 - if (cellDraggedWidth) { + if (isNumber(cellDraggedWidth)) { return cellDraggedWidth; } // 2. 其次是自定义, 返回 null 则使用默认宽度 const cellCustomWidth = this.getCellCustomSize(col, colCell?.width!); - if (!isNil(cellCustomWidth)) { + if (isNumber(cellCustomWidth)) { return cellCustomWidth; } @@ -648,12 +649,12 @@ export class PivotFacet extends BaseFacet { const cellDraggedWidth = this.getRowCellDraggedWidth(node); - if (cellDraggedWidth) { + if (isNumber(cellDraggedWidth)) { return cellDraggedWidth; } const cellCustomWidth = this.getCellCustomSize(node, rowCell?.width!); - if (!isNil(cellCustomWidth)) { + if (isNumber(cellCustomWidth)) { return cellCustomWidth; } diff --git a/packages/s2-core/src/facet/table-facet.ts b/packages/s2-core/src/facet/table-facet.ts index 1017e5f146..1e280a755c 100644 --- a/packages/s2-core/src/facet/table-facet.ts +++ b/packages/s2-core/src/facet/table-facet.ts @@ -1,5 +1,14 @@ import { Group, Rect } from '@antv/g'; -import { get, isBoolean, isEmpty, isNil, keys, last, maxBy, set } from 'lodash'; +import { + get, + isBoolean, + isEmpty, + isNumber, + keys, + last, + maxBy, + set, +} from 'lodash'; import { TableDataCell } from '../cell'; import { FRONT_GROUND_GROUP_COL_FROZEN_Z_INDEX, @@ -47,7 +56,7 @@ import type { Indexes, PanelIndexes } from '../utils/indexes'; import { getValidFrozenOptions } from '../utils/layout/frozen'; import { BaseFacet } from './base-facet'; import { CornerBBox } from './bbox/cornerBBox'; -import { Frame, type SeriesNumberHeader } from './header'; +import { type ColHeaderConfig, Frame, type SeriesNumberHeader } from './header'; import type { ColHeader } from './header/col'; import { TableColHeader } from './header/table-col'; import { buildHeaderHierarchy } from './layout/build-header-hierarchy'; @@ -242,7 +251,7 @@ export class TableFacet extends BaseFacet { const getCellMeta: GetCellMeta = (rowIndex, colIndex) => { const { showSeriesNumber } = this.spreadsheet.options; - const col = colLeafNodes[colIndex]; + const colNode = colLeafNodes[colIndex]; const cellHeight = this.getCellHeightByRowIndex(rowIndex); const cellRange = this.getCellRange(); @@ -255,7 +264,7 @@ export class TableFacet extends BaseFacet { let data; - const x = col.x; + const x = colNode.x; let y = this.viewCellHeights.getCellOffsetY(rowIndex); if ( @@ -266,12 +275,12 @@ export class TableFacet extends BaseFacet { this.getTotalHeightForRange(rowIndex, cellRange.end); } - if (showSeriesNumber && col.field === SERIES_NUMBER_FIELD) { + if (showSeriesNumber && colNode.field === SERIES_NUMBER_FIELD) { data = rowIndex + 1; } else { data = this.spreadsheet.dataSet.getCellData({ query: { - col: col.field, + col: colNode.field, rowIndex, }, }); @@ -280,19 +289,19 @@ export class TableFacet extends BaseFacet { spreadsheet: this.spreadsheet, x, y, - width: col.width, + width: colNode.width, height: cellHeight, data: { - [col.field]: data, + [colNode.field]: data, }, rowIndex, colIndex, isTotals: false, - colId: col.id, + colId: colNode.id, rowId: String(rowIndex), - valueField: col.field, + valueField: colNode.field, fieldValue: data, - id: getDataCellId(String(rowIndex), col.id), + id: getDataCellId(String(rowIndex), colNode.id), } as ViewMeta; }; @@ -439,61 +448,67 @@ export class TableFacet extends BaseFacet { colNode: Node, adaptiveColWidth: number, ): number { - const { colCell } = this.spreadsheet.options.style!; - const layoutWidthType = this.spreadsheet.getLayoutWidthType(); + const { spreadsheet } = this; + const { dataSet } = spreadsheet; + const { colCell } = spreadsheet.options.style!; + const layoutWidthType = spreadsheet.getLayoutWidthType(); const cellDraggedWidth = this.getColCellDraggedWidth(colNode); // 1. 拖拽后的宽度优先级最高 - if (cellDraggedWidth) { + if (isNumber(cellDraggedWidth)) { return cellDraggedWidth; } // 2. 其次是自定义, 返回 null 则使用默认宽度 const cellCustomWidth = this.getCellCustomSize(colNode, colCell?.width); - if (!isNil(cellCustomWidth)) { + if (isNumber(cellCustomWidth)) { return cellCustomWidth; } let colWidth: number; if (layoutWidthType === LayoutWidthTypes.Compact) { - const datas = this.spreadsheet.dataSet.getDisplayDataSet(); - const colLabel = colNode.value; - - const allLabels = - datas?.map((data) => `${data[colNode.field]}`)?.slice(0, 50) || []; // 采样取了前50 - allLabels.push(colLabel); - const maxLabel = maxBy(allLabels, (label) => - this.spreadsheet.measureTextWidthRoughly(label), + const datas = dataSet.getDisplayDataSet(); + const formatter = dataSet.getFieldFormatter(colNode.field); + + // 采样前50,找出表身最长的数据 + const maxLabel = maxBy( + datas + ?.slice(0, 50) + .map( + (data) => + `${formatter?.(data[colNode.field]) ?? data[colNode.field]}`, + ), + (label) => spreadsheet.measureTextWidthRoughly(label), ); - const { bolderText: colCellTextStyle } = this.spreadsheet.theme.colCell!; - const { text: dataCellTextStyle, cell: cellStyle } = - this.spreadsheet.theme.dataCell!; - DebuggerUtil.getInstance().logger( 'Max Label In Col:', colNode.field, maxLabel, ); - // 最长的 Label 如果是列名,按列名的标准计算宽度 - if (colLabel === maxLabel) { - colWidth = - this.spreadsheet.measureTextWidth(maxLabel, colCellTextStyle) + - getOccupiedWidthForTableCol( - this.spreadsheet, - colNode, - this.spreadsheet.theme.colCell!, - ); - } else { - // 额外添加一像素余量,防止 maxLabel 有多个同样长度情况下,一些 label 不能展示完全 - const EXTRA_PIXEL = 1; - colWidth = - this.spreadsheet.measureTextWidth(maxLabel, dataCellTextStyle) + - cellStyle!.padding!.left! + - cellStyle!.padding!.right! + - EXTRA_PIXEL; - } + const { bolderText: colCellTextStyle } = spreadsheet.theme.colCell!; + const { text: dataCellTextStyle, cell: cellStyle } = + spreadsheet.theme.dataCell!; + + // 额外添加一像素余量,防止 maxLabel 有多个同样长度情况下,一些 label 不能展示完全 + const EXTRA_PIXEL = 1; + const maxLabelWidth = + spreadsheet.measureTextWidth(maxLabel, dataCellTextStyle) + + cellStyle!.padding!.left! + + cellStyle!.padding!.right! + + EXTRA_PIXEL; + + // 计算表头 label+icon 占用的空间 + const colHeaderNodeWidth = + spreadsheet.measureTextWidth(colNode.value, colCellTextStyle) + + getOccupiedWidthForTableCol( + this.spreadsheet, + colNode, + spreadsheet.theme.colCell!, + ); + + colWidth = Math.max(colHeaderNodeWidth, maxLabelWidth); } else { colWidth = adaptiveColWidth; } @@ -972,7 +987,7 @@ export class TableFacet extends BaseFacet { data: this.layoutResult.colNodes, sortParam: this.spreadsheet.store.get('sortParam'), spreadsheet: this.spreadsheet, - }); + } as unknown as ColHeaderConfig); } return this.columnHeader; } 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 8c28165d74..aa13ee3edc 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 @@ -12,7 +12,8 @@ import type { } from '../../../common/interface'; import { getCellMeta, - updateRowColCells, + getInteractionCells, + afterSelectDataCells, } from '../../../utils/interaction/select-event'; import { getTooltipOptions, @@ -44,7 +45,7 @@ export class DataCellClick extends BaseEvent implements BaseEventImplement { event.stopPropagation(); - const { interaction, options } = this.spreadsheet; + const { interaction } = this.spreadsheet; interaction.clearHoverTimer(); if (interaction.hasIntercepts([InterceptType.CLICK])) { @@ -75,15 +76,12 @@ export class DataCellClick extends BaseEvent implements BaseEventImplement { } interaction.changeState({ - cells: [getCellMeta(cell)], + cells: getInteractionCells(getCellMeta(cell), this.spreadsheet), stateName: InteractionStateName.SELECTED, + onUpdateCells: afterSelectDataCells, }); this.spreadsheet.emit(S2Event.GLOBAL_SELECTED, [cell]); this.showTooltip(event, meta); - - if (options.interaction!.selectedCellHighlight) { - updateRowColCells(meta); - } }); } @@ -134,6 +132,7 @@ export class DataCellClick extends BaseEvent implements BaseEventImplement { const { valueField: field, data: record } = cellData!; this.spreadsheet.emit(S2Event.GLOBAL_LINK_FIELD_JUMP, { + cellData: cellData!, field, record: Object.assign({ rowIndex: cellData!.rowIndex }, record), }); diff --git a/packages/s2-core/src/interaction/base-interaction/click/row-text-click.ts b/packages/s2-core/src/interaction/base-interaction/click/row-text-click.ts index e273e4ce83..47f578a1c6 100644 --- a/packages/s2-core/src/interaction/base-interaction/click/row-text-click.ts +++ b/packages/s2-core/src/interaction/base-interaction/click/row-text-click.ts @@ -4,6 +4,7 @@ import type { RawData } from '../../../common/interface'; import { getFieldValueOfViewMetaData } from '../../../data-set/cell-data'; import type { Node } from '../../../facet/layout/node'; import { BaseEvent, type BaseEventImplement } from '../../base-event'; +import type { Data } from '../../../common/interface'; /** * Row header click navigation interaction @@ -29,7 +30,8 @@ export class RowTextClick extends BaseEvent implements BaseEventImplement { this.spreadsheet.emit(S2Event.GLOBAL_LINK_FIELD_JUMP, { field, - record: rowData, + cellData: cellData!, + record: rowData as Data, }); }); } diff --git a/packages/s2-core/src/interaction/brush-selection/data-cell-brush-selection.ts b/packages/s2-core/src/interaction/brush-selection/data-cell-brush-selection.ts index 02f0e78400..b0ecb5c85b 100644 --- a/packages/s2-core/src/interaction/brush-selection/data-cell-brush-selection.ts +++ b/packages/s2-core/src/interaction/brush-selection/data-cell-brush-selection.ts @@ -8,7 +8,8 @@ import { InteractionStateName, } from '../../common/constant/interaction'; import type { BrushRange, CellMeta, ViewMeta } from '../../common/interface'; -import { updateRowColCells } from '../../utils'; +import { getInteractionCellsBySelectedCells } from '../../utils'; +import { afterSelectDataCells } from '../../utils/interaction/select-event'; import { BaseBrushSelection } from './base-brush-selection'; /** @@ -42,8 +43,8 @@ export class DataCellBrushSelection extends BaseBrushSelection { this.clearAutoScroll(); if (!this.isPointInCanvas(pointInCanvas)) { - const deltaX = pointInCanvas.x - this.endBrushPoint.x; - const deltaY = pointInCanvas.y - this.endBrushPoint.y; + const deltaX = pointInCanvas?.x - this.endBrushPoint?.x; + const deltaY = pointInCanvas?.y - this.endBrushPoint?.y; this.handleScroll(deltaX, deltaY); return; } @@ -102,22 +103,20 @@ export class DataCellBrushSelection extends BaseBrushSelection { // 最终刷选的cell protected updateSelectedCells() { - const { interaction, options } = this.spreadsheet; + const { interaction } = this.spreadsheet; const brushRange = this.getBrushRange(); const selectedCellMetas = this.getSelectedCellMetas(brushRange); interaction.changeState({ - cells: selectedCellMetas, + cells: getInteractionCellsBySelectedCells( + selectedCellMetas, + this.spreadsheet, + ), stateName: InteractionStateName.SELECTED, + onUpdateCells: afterSelectDataCells, }); - if (options.interaction?.selectedCellHighlight) { - selectedCellMetas.forEach((meta) => { - updateRowColCells(meta as unknown as ViewMeta); - }); - } - const scrollBrushRangeCells = this.getScrollBrushRangeCells(selectedCellMetas); diff --git a/packages/s2-core/src/interaction/data-cell-multi-selection.ts b/packages/s2-core/src/interaction/data-cell-multi-selection.ts index a990831973..3aaa9d6bde 100644 --- a/packages/s2-core/src/interaction/data-cell-multi-selection.ts +++ b/packages/s2-core/src/interaction/data-cell-multi-selection.ts @@ -2,6 +2,7 @@ import type { FederatedPointerEvent as Event } from '@antv/g'; import { isEmpty } from 'lodash'; import type { DataCell } from '../cell'; import { + CellTypes, InteractionStateName, InterceptType, S2Event, @@ -10,8 +11,10 @@ import type { CellMeta, S2CellType, ViewMeta } from '../common/interface'; import { getCellMeta, isMultiSelectionKey, + getInteractionCellsBySelectedCells, } from '../utils/interaction/select-event'; import { getCellsTooltipData } from '../utils/tooltip'; +import { afterSelectDataCells } from '../utils/interaction/select-event'; import { BaseEvent, type BaseEventImplement } from './base-interaction'; export class DataCellMultiSelection @@ -54,7 +57,7 @@ export class DataCellMultiSelection private getSelectedCells(cell: S2CellType) { const id = cell.getMeta().id; const { interaction } = this.spreadsheet; - let selectedCells = interaction.getCells(); + let selectedCells = interaction.getCells([CellTypes.DATA_CELL]); let cells: CellMeta[] = []; if (interaction.getCurrentStateName() !== InteractionStateName.SELECTED) { selectedCells = []; @@ -86,9 +89,14 @@ export class DataCellMultiSelection interaction.addIntercepts([InterceptType.CLICK, InterceptType.HOVER]); this.spreadsheet.hideTooltip(); + interaction.changeState({ - cells: selectedCells, + cells: getInteractionCellsBySelectedCells( + selectedCells, + this.spreadsheet, + ), stateName: InteractionStateName.SELECTED, + onUpdateCells: afterSelectDataCells, }); this.spreadsheet.emit( S2Event.GLOBAL_SELECTED, diff --git a/packages/s2-core/src/interaction/root.ts b/packages/s2-core/src/interaction/root.ts index 31e218eb81..2cdae56bc6 100644 --- a/packages/s2-core/src/interaction/root.ts +++ b/packages/s2-core/src/interaction/root.ts @@ -7,6 +7,7 @@ import { InteractionStateName, InterceptType, S2Event, + type InteractionCellSelectedHighlightType, } from '../common/constant'; import type { BrushSelection, @@ -166,9 +167,13 @@ export class RootInteraction { } // 获取当前 interaction 记录的 Cells 元信息列表,包括不在可视区域内的格子 - public getCells(): CellMeta[] { + public getCells(cellType?: CellTypes[]): CellMeta[] { const currentState = this.getState(); - return currentState?.cells || []; + const cells = currentState?.cells || []; + if (isNil(cellType)) { + return cells; + } + return cells.filter((cell) => cellType.includes(cell.type)); } // 获取 cells 中在可视区域内的实例列表 @@ -517,10 +522,13 @@ export class RootInteraction { this.setState(interactionStateInfo); // 更新单元格 + const update = () => { + this.updatePanelGroupAllDataCells(); + }; if (onUpdateCells) { - onUpdateCells(this, () => this.updatePanelGroupAllDataCells()); + onUpdateCells(this, update); } else { - this.updatePanelGroupAllDataCells(); + update(); } this.draw(); } @@ -564,4 +572,33 @@ export class RootInteraction { public getHoverTimer() { return this.hoverTimer; } + + public getSelectedCellHighlight(): InteractionCellSelectedHighlightType { + const { selectedCellHighlight } = this.spreadsheet.options.interaction!; + + if (isBoolean(selectedCellHighlight)) { + return { + rowHeader: selectedCellHighlight, + colHeader: selectedCellHighlight, + currentRow: false, + currentCol: false, + }; + } + + // @ts-ignore + const { + rowHeader = false, + colHeader = false, + currentRow = false, + currentCol = false, + } = (selectedCellHighlight as unknown as InteractionCellSelectedHighlightType) ?? + {}; + + return { + rowHeader, + colHeader, + currentRow, + currentCol, + }; + } } diff --git a/packages/s2-core/src/utils/cell/data-cell.ts b/packages/s2-core/src/utils/cell/data-cell.ts index 0167b54f8b..e67e21fad4 100644 --- a/packages/s2-core/src/utils/cell/data-cell.ts +++ b/packages/s2-core/src/utils/cell/data-cell.ts @@ -1,4 +1,11 @@ -import { isEqual } from 'lodash'; +import { isEqual, forEach } from 'lodash'; +import type { DataCell } from '../../cell'; +import type { SpreadSheet } from '../../sheet-type'; +import { + InteractionStateName, + CellTypes, + EMPTY_PLACEHOLDER, +} from '../../common/constant'; import type { CellMeta, S2CellType } from '../../common/interface'; /** @@ -14,5 +21,91 @@ export const includeCell = (cells: CellMeta[], currentCell: S2CellType) => { }; export const getDataCellId = (rowIndex: string, colIndex: string) => { - return `${rowIndex}-${colIndex}`; + return `${rowIndex}${EMPTY_PLACEHOLDER}${colIndex}`; +}; + +export const shouldUpdateBySelectedCellsHighlight = (s2: SpreadSheet) => { + const { currentRow, currentCol, rowHeader, colHeader } = + s2.interaction.getSelectedCellHighlight(); + return currentRow || currentCol || rowHeader || colHeader; +}; + +export const isDataCell = (cell: CellMeta) => { + return cell.type === CellTypes.DATA_CELL; +}; + +/** + * highlight cells of the row + * @param cells cells selected + * @param dataCell cell to render + */ +export const updateCurrentRowCellState = ( + cells: CellMeta[], + dataCell: DataCell, +) => { + forEach(cells, (cell) => { + if (isDataCell(cell) && cell.rowIndex === dataCell.getMeta().rowIndex) { + dataCell.updateByState(InteractionStateName.SELECTED); + } + }); +}; + +/** + * highlight cells of the column + * @param cells cells selected + * @param dataCell cell to render + */ +export const updateCurrentColumnCellState = ( + cells: CellMeta[], + dataCell: DataCell, +) => { + forEach(cells, (cell) => { + if (isDataCell(cell) && cell.colIndex === dataCell.getMeta().colIndex) { + dataCell.updateByState(InteractionStateName.SELECTED); + } + }); +}; + +/** + * highlight cells + * @param cells cells selected + * @param dataCell cell to render + */ +export const updateCurrentCellState = ( + cells: CellMeta[], + dataCell: DataCell, +) => { + forEach(cells, (cell) => { + if ( + isDataCell(cell) && + cell.rowIndex === dataCell.getMeta().rowIndex && + cell.colIndex === dataCell.getMeta().colIndex + ) { + dataCell.updateByState(InteractionStateName.SELECTED); + } + }); +}; + +export const updateBySelectedCellsHighlight = ( + cells: CellMeta[], + dataCell: DataCell, + s2: SpreadSheet, +) => { + const { rowHeader, colHeader, currentRow, currentCol } = + s2.interaction.getSelectedCellHighlight(); + + const isRowCell = dataCell.cellType === CellTypes.ROW_CELL; + // 高亮序号 + const showSNWhenRowHeaderHighlight = + s2.isTableMode() && s2.options.showSeriesNumber && rowHeader && isRowCell; + + if (currentRow || showSNWhenRowHeaderHighlight) { + updateCurrentRowCellState(cells, dataCell); + } + if (currentCol) { + updateCurrentColumnCellState(cells, dataCell); + } + if (rowHeader || colHeader) { + updateCurrentCellState(cells, dataCell); + } }; diff --git a/packages/s2-core/src/utils/cell/table-col-cell.ts b/packages/s2-core/src/utils/cell/table-col-cell.ts index 1bb52da47e..86d1508e7d 100644 --- a/packages/s2-core/src/utils/cell/table-col-cell.ts +++ b/packages/s2-core/src/utils/cell/table-col-cell.ts @@ -63,6 +63,7 @@ export const getOccupiedWidthForTableCol = ( style: DefaultCellTheme, ) => { const padding = get(style, 'cell.padding'); + const horizontalBorderWidth = style?.cell?.horizontalBorderWidth ?? 1; const expandIconPadding = getExtraPaddingForExpandIcon(s2, meta.field, style); const iconsWidth = getTableColIconsWidth( s2, @@ -76,6 +77,7 @@ export const getOccupiedWidthForTableCol = ( padding.right + iconsWidth + expandIconPadding.left + - expandIconPadding.right + expandIconPadding.right + + horizontalBorderWidth ); }; diff --git a/packages/s2-core/src/utils/color.ts b/packages/s2-core/src/utils/color.ts index 15436d6b81..a80931f7d9 100644 --- a/packages/s2-core/src/utils/color.ts +++ b/packages/s2-core/src/utils/color.ts @@ -3,7 +3,6 @@ import tinycolor from 'tinycolor2'; import type { Palette, PaletteMeta } from '../common/interface/theme'; import { DEFAULT_FONT_COLOR, - FONT_COLOR_BRIGHTNESS_THRESHOLD, REVERSE_FONT_COLOR, } from '../common/constant/condition'; @@ -19,6 +18,14 @@ const STANDARD_COLOR_MIX_PERCENT = [95, 85, 75, 30, 15, 0, 15, 30, 45, 60, 80]; */ const BASIC_COLOR_COUNT = 15; +/** + * 智能反色使用 + * @param color + */ +export const shouldReverseFontColor = (color: string) => { + return tinycolor(color).getLuminance() <= 0.5; +}; + const FONT_COLOR_RELATIONS: Array<{ fontColorIndex: number; bgColorIndex: number; @@ -92,11 +99,11 @@ export const generatePalette = ( // 根据背景明暗设置字体颜色 FONT_COLOR_RELATIONS.forEach(({ fontColorIndex, bgColorIndex }) => { - basicColors[fontColorIndex] = - tinycolor(basicColors[bgColorIndex]).getBrightness() > - FONT_COLOR_BRIGHTNESS_THRESHOLD - ? DEFAULT_FONT_COLOR - : REVERSE_FONT_COLOR; + basicColors[fontColorIndex] = shouldReverseFontColor( + basicColors[bgColorIndex], + ) + ? REVERSE_FONT_COLOR + : DEFAULT_FONT_COLOR; }); return { diff --git a/packages/s2-core/src/utils/data-set-operate.ts b/packages/s2-core/src/utils/data-set-operate.ts index e083ef8301..9035d7a0df 100644 --- a/packages/s2-core/src/utils/data-set-operate.ts +++ b/packages/s2-core/src/utils/data-set-operate.ts @@ -1,4 +1,4 @@ -import { every, flatMap, has, isArray } from 'lodash'; +import { every, flatMap, get, has, isArray, keys } from 'lodash'; import { DataSelectType, DEFAULT_TOTAL_SELECTIONS, @@ -137,3 +137,46 @@ export function flattenIndexesData( return dimensionData.slice(startIdx, length).filter(Boolean); }); } + +export const flatten = (data: Record[] | Record) => { + const result = []; + + if (Array.isArray(data)) { + // 总计小计在数组里面,以 undefine作为key, 直接forEach的话会漏掉总计小计 + const containsTotal = 'undefined' in data; + const itemLength = data.length + (containsTotal ? 1 : 0); + + let i = 0; + while (i < itemLength) { + // eslint-disable-next-line dot-notation + const current = + i === data.length ? data['undefined' as unknown as number] : data[i]; + i++; + + if (current && 'undefined' in current) { + keys(current).forEach((ki) => { + result.push(current[ki]); + }); + } else if (Array.isArray(current)) { + result.push(...current); + } else { + result.push(current); + } + } + } else { + result.push(data); + } + return result; +}; + +export const flattenDeep = (data: Record[] | Record) => + keys(data)?.reduce((pre, next) => { + const item = get(data, next); + if (Array.isArray(item)) { + pre = pre.concat(flattenDeep(item)); + } else { + pre?.push(item as unknown as never); + } + + return pre; + }, []); diff --git a/packages/s2-core/src/utils/export/copy.ts b/packages/s2-core/src/utils/export/copy.ts index bc85ecdd3d..7c61ec73a2 100644 --- a/packages/s2-core/src/utils/export/copy.ts +++ b/packages/s2-core/src/utils/export/copy.ts @@ -5,8 +5,11 @@ import { filter, forEach, isEmpty, + isNil, map, max, + orderBy, + reduce, repeat, zip, } from 'lodash'; @@ -16,15 +19,21 @@ import { CopyType, EMPTY_PLACEHOLDER, NODE_ID_SEPARATOR, + EXTRA_FIELD, InteractionStateName, VALUE_FIELD, type Data, type DataItem, + SERIES_NUMBER_FIELD, + type RowData, + type CustomHeaderField, } from '../../common'; import type { Node } from '../../facet/layout/node'; import type { SpreadSheet } from '../../sheet-type'; import { copyToClipboard } from '../../utils/export'; import type { ColCell, RowCell } from '../../cell'; +import { getEmptyPlaceholder } from '../text'; +import { flattenDeep } from '../data-set-operate'; import { type Copyable, CopyMIMEType, @@ -78,6 +87,23 @@ const getFormat = (colIndex: number | undefined, spreadsheet: SpreadSheet) => { return (value: DataItem) => value; }; +/** + * 兼容 hideMeasureColumn 方案:hideMeasureColumn 的隐藏实现是通过截取掉度量(measure)数据,但是又只截取了 Node 中的,像 pivotMeta 中的又是完整的。导致复制时,无法通过 Node 找出正确路径。 + * https://github.com/antvis/S2/issues/1955 + * @param spreadsheet + */ +const compatibleHideMeasureColumn = (spreadsheet: SpreadSheet) => { + const isHideValue = + spreadsheet.options?.style?.colCell?.hideValue && + spreadsheet.isValueInCols(); + // 被 hideMeasureColumn 隐藏的 度量(measure) 值,手动添加上。 + return isHideValue + ? { + [EXTRA_FIELD]: spreadsheet.dataCfg.fields.values?.[0], + } + : {}; +}; + const getValueFromMeta = ( meta: CellMeta, displayData: Data[], @@ -85,10 +111,13 @@ const getValueFromMeta = ( ) => { if (spreadsheet.isPivotMode()) { const [rowNode, colNode] = getHeaderNodeFromMeta(meta, spreadsheet); + const measureQuery = compatibleHideMeasureColumn(spreadsheet); + const cell = spreadsheet.dataSet.getCellData({ query: { ...rowNode?.query, ...colNode?.query, + ...measureQuery, }, rowNode, isTotals: @@ -97,7 +126,7 @@ const getValueFromMeta = ( colNode?.isTotals || colNode?.isTotalMeasure, }); - return cell?.[VALUE_FIELD]; + return cell?.[VALUE_FIELD] ?? ''; } const fieldId = getFiledIdFromMeta(meta.colIndex, spreadsheet); return displayData[meta.rowIndex]?.[fieldId!]; @@ -342,7 +371,10 @@ const getDataMatrix = ( colNode.isTotals || colNode.isTotalMeasure, }); - return getFormat(colNode.colIndex, spreadsheet)(cellData?.[VALUE_FIELD]); + return getFormat( + colNode.colIndex, + spreadsheet, + )(cellData?.[VALUE_FIELD] ?? ''); }); }); }; @@ -394,7 +426,7 @@ const processPivotColSelected = ( ): Copyable => { const allRowLeafNodes = spreadsheet .getRowNodes() - .filter((node) => node.isLeaf); + .filter((node) => node.isLeaf || spreadsheet.isHierarchyTreeType()); const allColLeafNodes = spreadsheet .getColumnNodes() .filter((node) => node.isLeaf); @@ -449,7 +481,7 @@ const processPivotRowSelected = ( ): Copyable => { const allRowLeafNodes = spreadsheet .getRowNodes() - .filter((node) => node.isLeaf); + .filter((node) => node.isLeaf || spreadsheet.isHierarchyTreeType()); const allColLeafNodes = spreadsheet .getColumnNodes() .filter((node) => node.isLeaf); @@ -642,6 +674,61 @@ function getBrushHeaderCopyable( ]; } +const tilePivotData = ( + data: any, + columnOrdered: CustomHeaderField[], + defaultDataValue?: string, +): Array => { + // @ts-ignore + return map(columnOrdered, (field) => data[field] ?? defaultDataValue); +}; + +export const getDataByRowData = ( + spreadsheet: SpreadSheet, + rowData: RowData, +): Copyable => { + const { + options: { placeholder }, + dataCfg: { + fields: { rows = [], columns = [], values = [] }, + }, + } = spreadsheet; + const defaultDataValue = getEmptyPlaceholder(spreadsheet, placeholder); + const column = spreadsheet.getColumnLeafNodes(); + let datas: string[][] = []; + + if (spreadsheet.isTableMode()) { + const columnWithoutSeriesNumber = filter( + column, + (node) => node.field !== SERIES_NUMBER_FIELD, + ); + // 按列头顺序复制 + datas = map(rowData, (rowDataItem) => { + return map( + columnWithoutSeriesNumber, + // @ts-ignore + (node) => rowDataItem?.[node.field] ?? defaultDataValue, + ); + }); + } else if (spreadsheet.isPivotMode()) { + // 透视表的数据加上行头、列头才有意义,这里会以行头、列头、数据值的顺序将每一个单元格构造成一行 + const columnOrdered = [...rows, ...columns, ...values]; + const rowDataFlatten = flattenDeep(rowData as unknown as Array); + // 去掉小计 + const rowDataFlattenWithoutTotal = rowDataFlatten.filter((data) => + [...rows, ...columns].every((field) => !isNil(data[field as string])), + ); + datas = reduce( + rowDataFlattenWithoutTotal, + (ret: unknown[], data) => { + return [...ret, tilePivotData(data, columnOrdered, defaultDataValue)]; + }, + [], + ) as string[][]; + } + return matrixPlainTextTransformer(datas); +}; + function getDataCellCopyable( spreadsheet: SpreadSheet, cells: CellMeta[], @@ -653,10 +740,6 @@ function getDataCellCopyable( const displayData = spreadsheet.dataSet.getDisplayDataSet(); - if (spreadsheet.isPivotMode() && spreadsheet.isHierarchyTreeType()) { - // 树状模式透视表之后实现 - return; - } if ( spreadsheet.interaction.getCurrentStateName() === InteractionStateName.ALL_SELECTED @@ -672,8 +755,14 @@ function getDataCellCopyable( } // normal selected const selectedCellsMeta = getSelectedCellsMeta(cells); + const { currentRow } = spreadsheet.interaction.getSelectedCellHighlight(); - if (spreadsheet.options.interaction?.copyWithHeader) { + if (currentRow) { + const rowData = orderBy(cells, 'rowIndex', 'asc').map((cell) => + spreadsheet.dataSet.getRowData(cell), + ); + data = getDataByRowData(spreadsheet, rowData as unknown as RowData); + } else if (spreadsheet.options.interaction?.copyWithHeader) { data = getDataWithHeaderMatrix( selectedCellsMeta, displayData as Data[], diff --git a/packages/s2-core/src/utils/export/index.ts b/packages/s2-core/src/utils/export/index.ts index 753a6db072..a7265a1b3f 100644 --- a/packages/s2-core/src/utils/export/index.ts +++ b/packages/s2-core/src/utils/export/index.ts @@ -265,9 +265,15 @@ const getPlaceholder = ( */ const processColHeaders = (headers: any[][]) => { const result = headers.map((header) => - header.map((item) => - isArray(item) ? item : [item, ...new Array(header[0].length - 1)], - ), + header.map((item) => { + if (isArray(item)) { + return item; + } + if (isArray(header[0])) { + return [item, ...new Array(header[0].length - 1)]; + } + return item; + }), ); return result; }; diff --git a/packages/s2-core/src/utils/g-mini-charts.ts b/packages/s2-core/src/utils/g-mini-charts.ts index d7e83a7e25..142770de0a 100644 --- a/packages/s2-core/src/utils/g-mini-charts.ts +++ b/packages/s2-core/src/utils/g-mini-charts.ts @@ -3,7 +3,7 @@ * https://github.com/antvis/g */ -import { get, isEmpty, map, max, min } from 'lodash'; +import { get, isEmpty, isNil, map, max, min } from 'lodash'; import { CellClipBox, type BaseChartData, @@ -23,7 +23,7 @@ import { CellTypes, MiniChartTypes } from '../common/constant'; import { parseNumberWithPrecision } from '../utils/formatter'; import { getIntervalScale } from '../utils/condition/condition'; import type { DataCell } from '..'; -import { getEllipsisText } from './text'; +import { getEllipsisText, getEmptyPlaceholder } from './text'; interface FractionDigitsOptions { min: number; @@ -258,9 +258,9 @@ export const drawInterval = (cell: DataCell) => { const minValue = parseNumberWithPrecision(valueRange.minValue!); const maxValue = parseNumberWithPrecision(valueRange.maxValue!); - const fieldValue = parseNumberWithPrecision( - cell.getMeta().fieldValue as number, - ); + const fieldValue = isNil(attrs?.fieldValue) + ? parseNumberWithPrecision(cell.getMeta().fieldValue as number) + : parseNumberWithPrecision(attrs?.fieldValue); // 对于超出设定范围的值不予显示 if (fieldValue < minValue || fieldValue > maxValue) { return; @@ -293,13 +293,22 @@ export const drawInterval = (cell: DataCell) => { * 绘制单元格内的 mini子弹图 */ export const drawBullet = (value: BulletValue, cell: S2CellType) => { + const dataCellStyle = cell.getStyle(CellTypes.DATA_CELL); + const { x, y, height, width, spreadsheet } = cell.getMeta(); + if (isEmpty(value)) { + renderText( + cell, + [], + x + width - dataCellStyle.cell.padding.right, + y + height / 2, + getEmptyPlaceholder(cell, spreadsheet.options.placeholder) as string, + dataCellStyle.text, + ); return; } - const dataCellStyle = cell.getStyle(CellTypes.DATA_CELL); const bulletStyle = dataCellStyle.miniChart.bullet; - const { x, y, height, width, spreadsheet } = cell.getMeta(); const { progressBar, comparativeMeasure, rangeColors, backgroundColor } = bulletStyle; diff --git a/packages/s2-core/src/utils/interaction/index.ts b/packages/s2-core/src/utils/interaction/index.ts index bd8d7532af..8121164064 100644 --- a/packages/s2-core/src/utils/interaction/index.ts +++ b/packages/s2-core/src/utils/interaction/index.ts @@ -1,5 +1,6 @@ export * from './formatter'; export * from './hover-event'; +export * from './link-field'; export * from './merge-cell'; export * from './select-event'; export * from './state-controller'; diff --git a/packages/s2-core/src/utils/interaction/link-field.ts b/packages/s2-core/src/utils/interaction/link-field.ts new file mode 100644 index 0000000000..638da267ce --- /dev/null +++ b/packages/s2-core/src/utils/interaction/link-field.ts @@ -0,0 +1,14 @@ +import type { Node } from '../../facet/layout/node'; +import type { ViewMeta } from '../../common/interface/basic'; + +export const checkIsLinkField = ( + linkFields: string[] | ((meta: Node | ViewMeta) => boolean), + meta: Node | ViewMeta, +): boolean => { + return typeof linkFields === 'function' + ? linkFields(meta) + : linkFields.some( + (field) => + field === meta.key || field === meta.id || field === meta.valueField, + ); +}; diff --git a/packages/s2-core/src/utils/interaction/select-event.ts b/packages/s2-core/src/utils/interaction/select-event.ts index 1be6ccb38c..c31336608d 100644 --- a/packages/s2-core/src/utils/interaction/select-event.ts +++ b/packages/s2-core/src/utils/interaction/select-event.ts @@ -1,4 +1,4 @@ -import { forEach } from 'lodash'; +import { forEach, reduce, uniqBy } from 'lodash'; import { ColCell, RowCell, TableSeriesCell } from '../../cell'; import { getDataCellId } from '../cell/data-cell'; import { @@ -14,6 +14,12 @@ import { getActiveHoverRowColCells, updateAllColHeaderCellState, } from './hover-event'; +import type { RootInteraction } from './../../interaction'; + +type HeaderGetter = { + getter: typeof getRowHeaderByCellId; + shouldGet?: boolean; +}; export const isMultiSelectionKey = (e: KeyboardEvent) => { return [InteractionKeyboardKey.META, InteractionKeyboardKey.CONTROL].includes( @@ -21,14 +27,14 @@ export const isMultiSelectionKey = (e: KeyboardEvent) => { ); }; -export const getCellMeta = (cell: S2CellType) => { +export const getCellMeta = (cell: S2CellType): CellMeta => { const meta = cell.getMeta(); - const { id, colIndex, rowIndex } = meta; - + const { id, colIndex, rowIndex, rowQuery } = meta; return { id, colIndex, rowIndex, + rowQuery, type: cell instanceof TableSeriesCell ? CellTypes.ROW_CELL : cell.cellType, }; }; @@ -110,3 +116,73 @@ export function updateRowColCells(meta: ViewMeta) { }); } } + +export const getRowHeaderByCellId = ( + cellId: string, + s2: SpreadSheet, +): Node[] => { + return s2.getRowNodes().filter((node: Node) => cellId.includes(node.id)); +}; + +export const getColHeaderByCellId = ( + cellId: string, + s2: SpreadSheet, +): Node[] => { + return s2.getColumnNodes().filter((node: Node) => cellId.includes(node.id)); +}; + +export const getInteractionCells = ( + cell: CellMeta, + s2: SpreadSheet, +): Array => { + const { colHeader, rowHeader } = s2.interaction.getSelectedCellHighlight(); + + const headerGetters: HeaderGetter[] = [ + { + shouldGet: rowHeader, + getter: getRowHeaderByCellId, + }, + { + shouldGet: colHeader, + getter: getColHeaderByCellId, + }, + ]; + + const selectedHeaderCells = headerGetters + .filter((item) => item.shouldGet) + .reduce((acc: Node[], i) => [...acc, ...i.getter(cell.id, s2)], []) + .filter((node) => !!node.belongsCell) + .map((node) => getCellMeta(node.belongsCell!)); + + return [cell, ...selectedHeaderCells]; +}; + +export const getInteractionCellsBySelectedCells = ( + selectedCells: CellMeta[], + s2: SpreadSheet, +): Array => { + const headerSelectedCell: CellMeta[] = reduce( + selectedCells, + (_cells: CellMeta[], selectedCell) => { + return [..._cells, ...getInteractionCells(selectedCell, s2)]; + }, + [], + ); + + // headerSelectedCell 会有重复的 cell,在这里统一去重 + return uniqBy([...selectedCells, ...headerSelectedCell], 'id'); +}; + +export const afterSelectDataCells = ( + root: RootInteraction, + updateDataCells: () => void, +) => { + const { colHeader, rowHeader } = root.getSelectedCellHighlight(); + if (colHeader) { + root.updateCells(root.getAllColHeaderCells()); + } + if (rowHeader) { + root.updateCells(root.getAllRowHeaderCells()); + } + updateDataCells(); +}; diff --git a/packages/s2-react/CHANGELOG.md b/packages/s2-react/CHANGELOG.md index 52d756a912..dcb86a76c2 100644 --- a/packages/s2-react/CHANGELOG.md +++ b/packages/s2-react/CHANGELOG.md @@ -1,5 +1,30 @@ # [@antv/s2-react-v2.0.0-next.2](https://github.com/antvis/S2/compare/@antv/s2-react-v2.0.0-next.1...@antv/s2-react-v2.0.0-next.2) (2022-12-07) + +### Bug Fixes + +* 列头label存在数组,复制导出列头层级补齐错误 ([#1990](https://github.com/antvis/S2/issues/1990)) ([ec62409](https://github.com/antvis/S2/commit/ec62409b688c5dd5e39a93f5b292d909496ed830)) + + +### Features + +* selected cell highlight ([#1878](https://github.com/antvis/S2/issues/1878)) ([3e11a37](https://github.com/antvis/S2/commit/3e11a37bf94f758379ba2819ec5d8b3251708814)) + +# [@antv/s2-react-v1.34.0](https://github.com/antvis/S2/compare/@antv/s2-react-v1.33.0...@antv/s2-react-v1.34.0) (2022-12-09) + + +### Bug Fixes + +* **tooltip:** 修复自定义操作菜单传入自定义 ReactNode 不显示 ([#1969](https://github.com/antvis/S2/issues/1969)) ([3eff993](https://github.com/antvis/S2/commit/3eff9932438cc95093686c03510b57648ff44391)) + + +### Features + +* 暴露afterRealCellRender,这样能够更灵活的使用datacell ([#1970](https://github.com/antvis/S2/issues/1970)) ([66c5ab9](https://github.com/antvis/S2/commit/66c5ab9992c51b475be8acaf9a198d49f3114a49)) + +# [@antv/s2-react-v1.33.0](https://github.com/antvis/S2/compare/@antv/s2-react-v1.32.0...@antv/s2-react-v1.33.0) (2022-12-02) + + ### Bug Fixes * **interaction:** 修复趋势分析表选中高亮效果无效 close [#1960](https://github.com/antvis/S2/issues/1960) ([#1961](https://github.com/antvis/S2/issues/1961)) ([5140b60](https://github.com/antvis/S2/commit/5140b6060d03b0290ddb4b314e7892520038f369)) diff --git a/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/__snapshots__/index-spec.tsx.snap b/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/__snapshots__/index-spec.tsx.snap index 2643d30db3..609a09d441 100644 --- a/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/__snapshots__/index-spec.tsx.snap +++ b/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/__snapshots__/index-spec.tsx.snap @@ -1,8 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[` Tests StrategySheet Export Tests should export correct data 1`] = ` -" \\"日期\\" \\"2022-09\\" \\"2022-10\\" \\"2022-11\\" \\"2021年净增完成度\\" \\"趋势\\" \\"2022\\" - \\"数值\\" \\"数值\\" \\"环比\\" \\"同比\\" \\"数值\\" \\"环比\\" \\"数值\\" \\"环比\\" \\"同比\\" \\"净增完成度\\" \\"趋势\\" \\"数值\\" \\"环比\\" +" \\"日期\\" \\"2022-09\\" \\"2022-10\\" \\"2022-11\\" \\"2021年净增完成度\\" \\"趋势\\" \\"2022\\" + \\"数值\\" \\"数值\\" \\"环比\\" \\"同比\\" \\"数值\\" \\"环比\\" \\"数值\\" \\"环比\\" \\"同比\\" \\"净增完成度\\" \\"趋势\\" \\"数值\\" \\"环比\\" \\"自定义节点A\\" \\"自定义节点A\\" \\"指标A\\" \\"377\\" \\"3877\\" \\"4324\\" \\"0.42\\" \\"377\\" \\"自定义节点A\\" \\"指标A\\" \\"指标B\\" \\"377\\" \\"324\\" \\"377\\" \\"324\\" \\"-0.02\\" \\"377\\" \\"324\\" @@ -16,8 +16,8 @@ exports[` Tests StrategySheet Export Tests should export correct `; exports[` Tests StrategySheet Export Tests should export correct data for empty cell 1`] = ` -" \\"日期\\" \\"2022-09\\" \\"2022-10\\" \\"2022-11\\" \\"2021年净增完成度\\" \\"趋势\\" \\"2022\\" - \\"数值\\" \\"数值\\" \\"环比\\" \\"同比\\" \\"数值\\" \\"环比\\" \\"数值\\" \\"环比\\" \\"同比\\" \\"净增完成度\\" \\"趋势\\" \\"数值\\" \\"环比\\" +" \\"日期\\" \\"2022-09\\" \\"2022-10\\" \\"2022-11\\" \\"2021年净增完成度\\" \\"趋势\\" \\"2022\\" + \\"数值\\" \\"数值\\" \\"环比\\" \\"同比\\" \\"数值\\" \\"环比\\" \\"数值\\" \\"环比\\" \\"同比\\" \\"净增完成度\\" \\"趋势\\" \\"数值\\" \\"环比\\" \\"自定义节点A\\" \\"自定义节点A\\" \\"指标A\\" \\"377\\" \\"3877\\" \\"4324\\" \\"0.42\\" \\"377\\" \\"自定义节点A\\" \\"指标A\\" \\"指标B\\" \\"377\\" \\"324\\" \\"377\\" \\"324\\" \\"-0.02\\" \\"377\\" \\"324\\" @@ -31,8 +31,8 @@ exports[` Tests StrategySheet Export Tests should export correct `; exports[` Tests StrategySheet Export Tests should export correct data for multi different cycle compare data 1`] = ` -" \\"日期\\" \\"2022-09\\" \\"2022-10\\" \\"2022-11\\" \\"2021年净增完成度\\" \\"趋势\\" \\"2022\\" - \\"数值\\" \\"数值\\" \\"环比\\" \\"同比\\" \\"数值\\" \\"环比\\" \\"数值\\" \\"环比\\" \\"同比\\" \\"净增完成度\\" \\"趋势\\" \\"数值\\" \\"环比\\" +" \\"日期\\" \\"2022-09\\" \\"2022-10\\" \\"2022-11\\" \\"2021年净增完成度\\" \\"趋势\\" \\"2022\\" + \\"数值\\" \\"数值\\" \\"环比\\" \\"同比\\" \\"数值\\" \\"环比\\" \\"数值\\" \\"环比\\" \\"同比\\" \\"净增完成度\\" \\"趋势\\" \\"数值\\" \\"环比\\" \\"自定义节点A\\" \\"自定义节点A\\" \\"指标A\\" \\"377\\" \\"3877\\" \\"4324\\" \\"0.42\\" \\"377\\" \\"自定义节点A\\" \\"指标A\\" \\"指标B\\" \\"377\\" \\"324\\" \\"377\\" \\"324\\" \\"-0.02\\" \\"377\\" \\"324\\" diff --git a/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/index-spec.tsx b/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/index-spec.tsx index 8083c0aff2..efd35fbb28 100644 --- a/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/index-spec.tsx +++ b/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/index-spec.tsx @@ -273,6 +273,21 @@ describe(' Tests', () => { expect(result).toMatchSnapshot(); expect(detailRow).toEqual([`"自定义节点A"`, `"指标A"`, '', '', '']); }); + + test('should export correct headers when label have array and string', () => { + const result = copyData(s2, '\t'); + const rows = result.split('\n'); + expect(rows[0].split('\t')[8]).toEqual('"2022-11"'); + expect(rows[0].split('\t')[11]).toEqual('"2021年净增完成度"'); + expect(rows[0].split('\t')[12]).toEqual('"趋势"'); + expect(rows[0].split('\t')[13]).toEqual('"2022"'); + expect(rows[1].split('\t')[8]).toEqual('"数值"'); + expect(rows[1].split('\t')[9]).toEqual('"环比"'); + expect(rows[1].split('\t')[10]).toEqual('"同比"'); + expect(rows[1].split('\t')[11]).toEqual('"净增完成度"'); + expect(rows[1].split('\t')[12]).toEqual('"趋势"'); + expect(rows[1].split('\t')[13]).toEqual('"数值"'); + }); }); describe('StrategySheet Interaction Tests', () => { diff --git a/packages/s2-react/playground/config.ts b/packages/s2-react/playground/config.ts index 472339d1d8..e28f87428e 100644 --- a/packages/s2-react/playground/config.ts +++ b/packages/s2-react/playground/config.ts @@ -121,6 +121,7 @@ export const s2Options: SheetComponentOptions = { height: 50, }, colCell: { + hideValue: true, width: 140, height: 30, }, diff --git a/packages/s2-react/playground/index.tsx b/packages/s2-react/playground/index.tsx index 411a60d50b..a23c1bbbdd 100644 --- a/packages/s2-react/playground/index.tsx +++ b/packages/s2-react/playground/index.tsx @@ -3,7 +3,6 @@ /* eslint-disable no-console */ import { customMerge, - DEFAULT_STYLE, generatePalette, getDefaultSeriesNumberText, getLang, @@ -17,6 +16,8 @@ import { type TargetCellInfo, type ThemeCfg, type TooltipAutoAdjustBoundary, + DEFAULT_STYLE, + type InteractionCellSelectedHighlightType, } from '@antv/s2'; import type { Adaptive, SheetType } from '@antv/s2-shared'; import corePkg from '@antv/s2/package.json'; @@ -40,7 +41,7 @@ import { Divider, } from 'antd'; import 'antd/dist/antd.min.css'; -import { debounce, isEmpty } from 'lodash'; +import { debounce, isEmpty, isBoolean } from 'lodash'; import React from 'react'; import { ChromePicker } from 'react-color'; import ReactDOM from 'react-dom'; @@ -56,6 +57,7 @@ import { defaultOptions, pivotSheetDataCfg, s2ConditionsOptions, + s2Options, sliderOptions, tableSheetDataCfg, TableSheetFrozenOptions, @@ -584,20 +586,43 @@ function MainLayout() { }); }} /> - { - updateOptions({ - style: { - colCell: { - height: checked ? 0 : DEFAULT_STYLE.colCell?.height, + + { + updateOptions({ + style: { + colCell: { + height: checked + ? 0 + : s2Options?.style?.colCell?.height ?? + DEFAULT_STYLE.colCell?.height, + }, }, - }, - }); - }} - /> + }); + }} + /> + + + { + setDataCfg( + customMerge(dataCfg, { + fields: { + columns: checked + ? [] + : pivotSheetDataCfg.fields.columns, + }, + }), + ); + }} + /> + - - { + + void; // ============== Auto 自动生成的 ================ + onLayoutAfterRealDataCellRender?: (options: { + add: [number, number][]; + remove: [number, number][]; + spreadsheet: SpreadSheet; + }) => void; onRowCellBrushSelection?: (event: GEvent) => void; onColCellBrushSelection?: (event: GEvent) => void; } diff --git a/packages/s2-vue/CHANGELOG.md b/packages/s2-vue/CHANGELOG.md index 7e18233d0b..8163af7b68 100644 --- a/packages/s2-vue/CHANGELOG.md +++ b/packages/s2-vue/CHANGELOG.md @@ -1,3 +1,15 @@ +# [@antv/s2-vue-v1.5.0](https://github.com/antvis/S2/compare/@antv/s2-vue-v1.4.0...@antv/s2-vue-v1.5.0) (2022-12-09) + + +### Bug Fixes + +* **tooltip:** 修复自定义操作菜单传入自定义 ReactNode 不显示 ([#1969](https://github.com/antvis/S2/issues/1969)) ([3eff993](https://github.com/antvis/S2/commit/3eff9932438cc95093686c03510b57648ff44391)) + + +### Features + +* 暴露afterRealCellRender,这样能够更灵活的使用datacell ([#1970](https://github.com/antvis/S2/issues/1970)) ([66c5ab9](https://github.com/antvis/S2/commit/66c5ab9992c51b475be8acaf9a198d49f3114a49)) + # [@antv/s2-vue-v2.0.0-next.3](https://github.com/antvis/S2/compare/@antv/s2-vue-v2.0.0-next.2...@antv/s2-vue-v2.0.0-next.3) (2022-12-16) diff --git a/packages/s2-vue/src/hooks/useEvents.ts b/packages/s2-vue/src/hooks/useEvents.ts index 03fd18e444..2a73125683 100644 --- a/packages/s2-vue/src/hooks/useEvents.ts +++ b/packages/s2-vue/src/hooks/useEvents.ts @@ -299,6 +299,12 @@ export const useEvents = ( useS2Event(s2Ref, emit, S2Event.GLOBAL_SCROLL, 'scroll'); // ============== Auto 自动生成的 ================ + useS2Event( + s2Ref, + emit, + S2Event.LAYOUT_AFTER_REAL_DATA_CELL_RENDER, + 'layoutAfterRealDataCellRender', + ); useS2Event( s2Ref, emit, diff --git a/packages/s2-vue/src/utils/initPropAndEmits.ts b/packages/s2-vue/src/utils/initPropAndEmits.ts index d7358dff31..4144ec47e8 100644 --- a/packages/s2-vue/src/utils/initPropAndEmits.ts +++ b/packages/s2-vue/src/utils/initPropAndEmits.ts @@ -177,6 +177,7 @@ export const initBaseSheetEmits = () => { 'scroll', 'hover', // ============== Auto 自动生成的 ================ + 'layoutAfterRealDataCellRender', 'rowCellBrushSelection', 'colCellBrushSelection', ]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 199e8620b1..050e6979ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -327,7 +327,7 @@ importers: '@antv/dumi-theme-antv': 0.3.5_dumi@2.1.1 '@antv/g-canvas': 0.5.13 '@antv/s2': link:../packages/s2-core - '@antv/s2-react': 2.0.0-next.2_ekq5dgpvi24jylo2hahsfpc3h4 + '@antv/s2-react': link:../packages/s2-react antd: 4.24.7 copy-to-clipboard: 3.3.3 dumi: 2.1.1 @@ -835,26 +835,6 @@ packages: tslib: 2.4.1 dev: false - /@antv/s2-react/2.0.0-next.2_ekq5dgpvi24jylo2hahsfpc3h4: - resolution: {integrity: sha512-v2/sHhMSp9uZHPKB1oRdmJLm45Vrtt0xYoqWnBrnCvLwgN2A4l/sMDz2v9xv696Sk9KG3x09dHkPZn8xdxvqAQ==} - peerDependencies: - '@ant-design/icons': '>=4.7.0' - '@antv/s2': '>=1.0.0' - antd: '>=4.16.13' - react: '>=16.8.0' - react-dom: '>=16.8.0' - dependencies: - '@ant-design/icons': 4.8.0 - '@antv/s2': link:packages/s2-core - ahooks: 3.7.4 - antd: 4.24.7 - classnames: 2.3.2 - lodash: 4.17.21 - react-beautiful-dnd: 13.1.1 - transitivePeerDependencies: - - react-native - dev: false - /@antv/util/2.0.17: resolution: {integrity: sha512-o6I9hi5CIUvLGDhth0RxNSFDRwXeywmt6ExR4+RmVAzIi48ps6HUy+svxOCayvrPBN37uE6TAc2KDofRo0nK9Q==} dependencies: @@ -5159,23 +5139,6 @@ packages: resolution: {integrity: sha512-V7uUvAwnimu6eh/PED4mCDjE7tokeZQLKlxg9lCTMPhN+NjsSbtdacByVlR1oluXQzD3MOw55wylDmQo4+S9ZQ==} dev: false - /ahooks/3.7.4: - resolution: {integrity: sha512-hvgdqzPUKXn95mK3cGlDCi/ZZqv+FRibCUCFT8zW3hCwLGvixVfnHrIW2/2lgzPdLo8mLjp/XOdIJvcPvE2lgQ==} - engines: {node: '>=8.0.0'} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - dependencies: - '@types/js-cookie': 2.2.7 - ahooks-v3-count: 1.0.0 - dayjs: 1.11.7 - intersection-observer: 0.12.2 - js-cookie: 2.2.1 - lodash: 4.17.21 - resize-observer-polyfill: 1.5.1 - screenfull: 5.2.0 - tslib: 2.4.1 - dev: false - /ahooks/3.7.4_react@17.0.2: resolution: {integrity: sha512-hvgdqzPUKXn95mK3cGlDCi/ZZqv+FRibCUCFT8zW3hCwLGvixVfnHrIW2/2lgzPdLo8mLjp/XOdIJvcPvE2lgQ==} engines: {node: '>=8.0.0'} @@ -18937,23 +18900,6 @@ packages: minimist: 1.2.7 strip-json-comments: 2.0.1 - /react-beautiful-dnd/13.1.1: - resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==} - peerDependencies: - react: ^16.8.5 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0 - dependencies: - '@babel/runtime': 7.20.7 - css-box-model: 1.2.1 - memoize-one: 5.2.1 - raf-schd: 4.0.3 - react-redux: 7.2.9 - redux: 4.2.0 - use-memo-one: 1.1.3 - transitivePeerDependencies: - - react-native - dev: false - /react-beautiful-dnd/13.1.1_sfoxds7t5ydpegc3knd667wn6m: resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==} peerDependencies: @@ -19159,26 +19105,6 @@ packages: resolution: {integrity: sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==} dev: false - /react-redux/7.2.9: - resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} - peerDependencies: - react: ^16.8.3 || ^17 || ^18 - react-dom: '*' - react-native: '*' - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - dependencies: - '@babel/runtime': 7.20.7 - '@types/react-redux': 7.1.25 - hoist-non-react-statics: 3.3.2 - loose-envify: 1.4.0 - prop-types: 15.8.1 - react-is: 17.0.2 - dev: false - /react-redux/7.2.9_sfoxds7t5ydpegc3knd667wn6m: resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} peerDependencies: @@ -22172,12 +22098,6 @@ packages: react: 18.1.0 dev: false - /use-memo-one/1.1.3: - resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - dev: false - /use-memo-one/1.1.3_react@17.0.2: resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} peerDependencies: diff --git a/s2-site/.dumirc.ts b/s2-site/.dumirc.ts index c0358316cb..523dd4942a 100644 --- a/s2-site/.dumirc.ts +++ b/s2-site/.dumirc.ts @@ -34,9 +34,9 @@ export default defineConfig({ docsearchOptions: { // 头部搜索框配置 versionV3: true, - apiKey: '90c9a5dbf6e5ea7058cc32bcde8e94b2', - indexName: 's2-antv-vision', - appId: 'D73DOU8RXD', + apiKey: '74b99a09199729fd4ac472746ada8456', + indexName: 's2-antv-antgroup', + appId: 'LWCKDMVZ87', }, internalSite: { url: 'https://s2.antv.antgroup.com', diff --git a/s2-site/docs/api/basic-class/base-facet.zh.md b/s2-site/docs/api/basic-class/base-facet.zh.md index fd44258e45..7c024d0632 100644 --- a/s2-site/docs/api/basic-class/base-facet.zh.md +++ b/s2-site/docs/api/basic-class/base-facet.zh.md @@ -80,6 +80,7 @@ s2.facet.getRealWidth() | frozenTopGroup | 顶部冻结区域 group | [Group](https://g.antv.vision/zh/docs/api/group) | | | frozenBottomGroup | 底部冻结区域 group | [Group](https://g.antv.vision/zh/docs/api/group) | | | getHiddenColumnsInfo | 匹配当前节点是否是隐藏节点,如果是,则返回该节点隐藏信息 | (node: [Node](/docs/api/basic-class/node)) => [HiddenColumnsInfo](/docs/api/basic-class/store#hiddencolumnsinfo) \| `null` | `1.34.1` | +| getCornerNodes | 获取角头节点 | () => [Node](/docs/api/basic-class/node)[] | `1.40.0` | ### CellScrollOffset diff --git a/s2-site/docs/api/components/sheet-component.zh.md b/s2-site/docs/api/components/sheet-component.zh.md index 44dac1c967..38332df3c4 100644 --- a/s2-site/docs/api/components/sheet-component.zh.md +++ b/s2-site/docs/api/components/sheet-component.zh.md @@ -95,7 +95,7 @@ order: 0 | onMouseUp | 表格鼠标松开事件 | (event: CanvasEvent) => void | | | | onSelected | 单元格选中事件 | (cells: ( [Cell](/docs/api/basic-class/base-cell)[] ) => void | | | | onReset | 交互状态重置事件 | (event: KeyboardEvent) => void | | | -| onLinkFieldJump | 链接字段跳转事件 | (data: { key: string; record: [Data](/docs/api/general/S2DataConfig#data) }) => void | | | +| onLinkFieldJump | 链接字段跳转事件 (cellData: @antv/s2 1.37.0 新增) | (data: { key: string; cellData: [Node](/docs/api/basic-class/node); record: [Data](/docs/api/general/S2DataConfig#data) }) => void | | | | onScroll | 单元格滚动事件 (含行头和数值单元格) | ({position: [CellScrollPosition](#cellscrollposition)} ) => void; | | | | onColCellBrushSelection | 批量选中刷选范围内的列头单元格,刷选过程中,显示刷选范围提示蒙层,刷选完成后,弹出 tooltip, 展示被刷选单元格信息(仅支持透视表) | (cells: [ColCell](/docs/api/basic-class/base-cell)[]) => void; | | | | onRowCellBrushSelection | 批量选中刷选范围内的行头单元格,刷选过程中,显示刷选范围提示蒙层,刷选完成后,弹出 tooltip, 展示被刷选单元格信息(仅支持透视表) | (cells: [RowCell](/docs/api/basic-class/base-cell)[]) => void; | | | @@ -216,7 +216,7 @@ type SheetComponentOptions = S2Options< | mouseUp | 表格鼠标松开事件 | (event: CanvasEvent) => void | | | | selected | 单元格选中事件 | ( cells: ([Cell](/docs/api/basic-class/base-cell)[] ) => void | | | | reset | 交互状态重置事件 | (event: KeyboardEvent) => void | | | -| linkFieldJump | 链接字段跳转事件 | (data: { key: string; record: [Data](/docs/api/general/S2DataConfig#data) }) => void | | | +| linkFieldJump | 链接字段跳转事件 (cellData: @antv/s2 1.37.0 新增) | (data: { key: string; cellData: [Node](/docs/api/basic-class/node); record: [Data](/docs/api/general/S2DataConfig#data) }) => void | | | | scroll | 单元格滚动事件 (含行头和数值单元格) | ({position: [CellScrollPosition](#cellscrollposition)} ) => void; | | | | colCellBrushSelection | 批量选中刷选范围内的列头单元格,刷选过程中,显示刷选范围提示蒙层,刷选完成后,弹出 tooltip, 展示被刷选单元格信息(仅支持透视表) | (cells: ColCell[]) => void; | | | | rowCellBrushSelection | 批量选中刷选范围内的行头单元格,刷选过程中,显示刷选范围提示蒙层,刷选完成后,弹出 tooltip, 展示被刷选单元格信息(仅支持透视表) | (cells: RowCell[]) => void; | | | diff --git a/s2-site/docs/api/general/S2DataConfig.zh.md b/s2-site/docs/api/general/S2DataConfig.zh.md index 430fefee5e..a1c3679bdc 100644 --- a/s2-site/docs/api/general/S2DataConfig.zh.md +++ b/s2-site/docs/api/general/S2DataConfig.zh.md @@ -115,7 +115,7 @@ object **必选**,_default:null_ | field | 字段 id | `string` | | | | name | 字段名称 | `string`| | | | description | 字段描述,会显示在行头、列头、单元格对应的 tooltip 中 | `string`| | | -| formatter | 格式化
单元格、行头和列头支持格式化,角头不支持格式化。只有单元格存在第二个参数。
数值字段:一般用于格式化数字单位
文本字段:一般用于做字段枚举值的别名
第二个参数在以下情况会传入:data cell 格式化,复制/导出,tooltip 展示(**且仅在选择多个单元格时,data 类型为数组**) | `(value: unknown, data?: Data | Data[], meta?: Node | ViewMeta) => string` | | | +| formatter | 格式化
单元格、行头和列头支持格式化,角头不支持格式化。只有单元格存在第二个参数。
数值字段:一般用于格式化数字单位
文本字段:一般用于做字段枚举值的别名
第二个参数在以下情况会传入:data cell 格式化,复制/导出,tooltip 展示(**且仅在选择多个单元格时,data 类型为数组**) | `(value: unknown, data?: Data \| Data[], meta?: Node \| ViewMeta) => string` | | | ### MiniChartData @@ -129,8 +129,8 @@ object **必选**,_default:null_ | 配置项名称 | 说明 | 类型 | 默认值 | 必选 | | ------------- | ----------------- | --------- | ----- | --- | -| values | 格式化后的数据,直接展示在 dataCfg 中 | `(string/number)[][]` | ✓ | -| originalValues | 原始数据,用于原始数据导出 | `(string/number)[][]` | | | +| values | 格式化后的数据,直接展示在 dataCfg 中 | `(string \| number)[][]` | ✓ | +| originalValues | 原始数据,用于原始数据导出 | `(string \| number)[][]` | | | | label | 用作单元格小标题,单独占一行展示 | `string` | | | | [key: string] | 其他透传字段,用于自定义单元格的定制化展示 | `unknown` | `` | | @@ -149,4 +149,4 @@ object **必选**,_default:null_ | 属性名称 | 说明 | 类型 | 默认值 | 必选 | | ------- | ---------| -------| ------|------| | key | 列字段 id 或分组 id | string | | ✓ | -| children | 分组下面的子级 | `Array\` | | | +| children | 分组下面的子级 | `Array` | | | diff --git a/s2-site/docs/api/general/S2Event.zh.md b/s2-site/docs/api/general/S2Event.zh.md index 47f189ae41..d313e7ce6b 100644 --- a/s2-site/docs/api/general/S2Event.zh.md +++ b/s2-site/docs/api/general/S2Event.zh.md @@ -91,6 +91,7 @@ s2.on(S2Event.ROW_CELL_CLICK, (event) => { | 名称 | 事件名 | 描述 | | ----------------------- | --------------------------------- | ------------------------------------------ | | 表头布局完成 | `S2Event.LAYOUT_AFTER_HEADER_LAYOUT` | 行头和列头布局完成后触发 | +| 数值单元格布局完成 | `S2Event.LAYOUT_AFTER_REAL_DATA_CELL_RENDER` | 当前可视范围数值单元格渲染完成后触发 | | 单元格虚拟滚动 | `S2Event.LAYOUT_CELL_SCROLL` | 已废弃,请使用 `S2Event.GLOBAL_SCROLL` 替代 | | 分页 | `S2Event.LAYOUT_PAGINATION` | 分页事件 | | 列头展开 | `S2Event.COL_CELL_EXPANDED` | 列头展开时触发 | diff --git a/s2-site/docs/common/interaction.zh.md b/s2-site/docs/common/interaction.zh.md index ebbf87a421..2f72c19f88 100644 --- a/s2-site/docs/common/interaction.zh.md +++ b/s2-site/docs/common/interaction.zh.md @@ -7,12 +7,12 @@ order: 5 功能描述:表格内置交互配置 -| 参数 | 说明 | 类型 | 默认值 | 必选 | -| -------- | ----------- | -------------- | -------- | ---------------- | -| linkFields | 标记字段为链接样式,用于外链跳转 | `string[]` | | | -| selectedCellsSpotlight | 是否开启选中高亮聚光灯效果 | `boolean` | `false` | | +| 参数 | 说明 | 类型 | 默认值 | 必选 | +| -------- | ----------- |-------------------------------------------| -------- | ---------------- | +| linkFields | 标记字段为链接样式,用于外链跳转 | `string[]` \ | (meta: [Node](/docs/api/basic-class/node) \| ViewMeta) => boolean | | | +| selectedCellsSpotlight | 是否开启选中高亮聚光灯效果 | `boolean` | `false` | | | hoverHighlight | 鼠标悬停时高亮当前单元格,以及所对应的行头,列头 | `boolean` | `true` | | -| hoverFocus | 鼠标悬停在当前单元格超过默认 800ms 后,保持当前高亮,显示 tooltip,悬停时间通过设置 `duration` 来控制 | `boolean \| {duration: number}` | `true` | | +| hoverFocus | 鼠标悬停在当前单元格超过默认 800ms 后,保持当前高亮,显示 tooltip,悬停时间通过设置 `duration` 来控制 | `boolean \ | {duration: number}` | `true` | | | hiddenColumnFields | 用于配置默认隐藏的列,透视表需要配置列头唯一 id, 明细表配置列头 field 字段即可 | `string[]` | | | | enableCopy | 是否允许复制 | `boolean` | `false` | | | copyWithHeader | 复制数据是否带表头信息 | `boolean` | `false` | | @@ -20,14 +20,14 @@ order: 5 | customInteractions | 自定义交互 [详情](/docs/manual/advanced/interaction/custom) | [CustomInteraction[]](#custominteraction) | | | | scrollSpeedRatio | 用于控制滚动速率,分水平和垂直两个方向,默认为 1 | [ScrollSpeedRatio](#scrollspeedratio) | | | | autoResetSheetStyle | 用于控制点击表格外区域和按下 esc 键时是否重置交互状态 | `boolean` | `true` | | -| resize | 用于控制 resize 热区是否显示 | `boolean` \| [ResizeInteractionOptions](#resizeinteractionoptions) | `true` | | -| brushSelection | 是否允许单元格(包含行头,列头,数值单元格)刷选。行头,列头刷选只支持透视表 | `boolean` \| [BrushSelection](#brushSelection) | `true` | | 1.29.0 后支持 [BrushSelection](#brushSelection) | +| resize | 用于控制 resize 热区是否显示 | `boolean` \ | [ResizeInteractionOptions](#resizeinteractionoptions) | `true` | | +| brushSelection | 是否允许单元格(包含行头,列头,数值单元格)刷选。行头,列头刷选只支持透视表 | `boolean` \ | [BrushSelection](#brushSelection) | `true` | | 1.29.0 后支持 [BrushSelection](#brushSelection) | | multiSelection | 是否允许多选 (包含行头,列头,数值单元格) | `boolean` | `true` | | | rangeSelection | 是否允许区间快捷多选 | `boolean` | `true` | | -| scrollbarPosition | 用于控制滚动条展示在内容区边缘还是画布边缘 | `content \| canvas` | `content` | | +| scrollbarPosition | 用于控制滚动条展示在内容区边缘还是画布边缘 | `content \ | canvas` | `content` | | | eventListenerOptions | 事件监听函数 `addEventListener` 的 [可选项配置](https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener), 可控制事件从冒泡阶段还是捕获阶段触发 | `false` | | -| selectedCellHighlight | 是否高亮选中格子所在的行列头 | `boolean` | `false` | | -| overscrollBehavior | 控制滚动至边界的行为,可禁用浏览器的默认滚动行为。[详情](/docs/manual/advanced/interaction/basic/#修改滚动至边界行为) | `auto \| contain \| none \| null` | `auto` | +| selectedCellHighlight | 选中格子后的高亮行为
rowHeader:是否高亮选中格子所在行头
colHeader:是否高亮选中格子所在列头
rowCells:是否高亮选中格子所在行
colCells:是否高亮选中格子所在列
true:同{rowHeader: true, colHeader: true} | `boolean \| { rowHeader?: boolean, colHeader?: boolean, rowCells?: boolean, colCells?: boolean }` | | `false` | | +| overscrollBehavior | 控制滚动至边界的行为,可禁用浏览器的默认滚动行为。[详情](/docs/manual/advanced/interaction/basic/#修改滚动至边界行为) | `auto \ | contain \| none \| null` | `auto` | ### CustomInteraction diff --git a/s2-site/docs/common/mini-chart.zh.md b/s2-site/docs/common/mini-chart.zh.md index 1a839f2cc4..c0cbed655b 100644 --- a/s2-site/docs/common/mini-chart.zh.md +++ b/s2-site/docs/common/mini-chart.zh.md @@ -17,7 +17,7 @@ order: 6 | 参数 | 说明 | 类型 | 必选 | 默认值 | | --- | ---- | --- | ---- | ------ | -| type | mini 图类型 | `string` (line/bar) | ✓ | | +| type | mini 图类型 | `line \| bar` | ✓ | | | encode | 编码方式,声明 x 位置通道或 y 位置通道绑定的对应的列数据| `{x: string; y: string}` | ✓ | | | data | 原始数据 | [Data[]](#data) | | ✓ | | [key: string] | 其他透传字段,用于自定义单元格的定制化展示 | `unknown` | | | @@ -28,7 +28,7 @@ order: 6 | 参数 | 说明 | 类型 | 必选 | 默认值 | | --- | ---- | --- | ---- | ------ | -| type | mini 图类型 | `string` (bullet) | ✓ | bullet | -| measure | 当前指标 | `number/string` | ✓ | | -| target | 目标值 | `number/string` | ✓ | | +| type | mini 图类型 | `bullet` | ✓ | `bullet` | +| measure | 当前指标 | `number \| string` | ✓ | | +| target | 目标值 | `number \| string` | ✓ | | | [key: string] | 其他透传字段,用于自定义单元格的定制化展示 | `unknown` | | | diff --git a/s2-site/docs/common/sort-param.zh.md b/s2-site/docs/common/sort-param.zh.md index 12fcf74fb1..0acd59d8bc 100644 --- a/s2-site/docs/common/sort-param.zh.md +++ b/s2-site/docs/common/sort-param.zh.md @@ -10,7 +10,7 @@ order: 7 | 参数 | 说明 | 类型 | 默认值 | 必选 | | ------------ | ------------------------------------------ | ------------------------------------ | ------ | --- | | sortFieldId | 度量 Id,即要被排序的 Id | `string` | - | ✓ | -| sortMethod | 排序方式 | `ASC` \| `DESC` \| `asc` \| `desc` | - | | +| sortMethod | 排序方式 | `ASC \| DESC \| asc \| desc` | - | | | sortBy | 自定义排序列表 | `string[]` | - | | | sortByMeasure | 按照度量值(数值)排序(透视表适用) | `string` | - | | | query | 筛选条件,缩小排序范围 如 :`{city:'白山'}` | `Record` | - | | @@ -24,9 +24,9 @@ order: 7 | 参数 | 说明 | 类型 | 默认值 | 必选 | | ------------ | ------------------------------------------ | ------------------------------------ | ------ | --- | | sortFieldId | 度量 Id,即要被排序的 Id | `string` | - | ✓ | -| sortMethod | 排序方式 | `ASC` \| `DESC` \| `asc` \| `desc` | - | | +| sortMethod | 排序方式 | `ASC \| DESC \| asc \| desc` | - | | | sortBy | 自定义排序列表 | `string[]` | - | | | sortByMeasure | 按照度量值(数值)排序(透视表适用) | `string` | - | | | query | 筛选条件,缩小排序范围 如 :`{city:'白山'}` | `Record` | - | | | type | 组内排序用来显示 icon(透视表适用) | `string` | - | | -| data | 当前排序数据列表 | `Array>` | - | | +| data | 当前排序数据列表 | `Array>` | - | | diff --git a/s2-site/docs/manual/advanced/interaction/basic.zh.md b/s2-site/docs/manual/advanced/interaction/basic.zh.md index c7ae5fc6e5..4f496c0f06 100644 --- a/s2-site/docs/manual/advanced/interaction/basic.zh.md +++ b/s2-site/docs/manual/advanced/interaction/basic.zh.md @@ -185,11 +185,25 @@ const s2Options = { preview ```ts +// selectedCellHighlight 的类型为 boolean | { rowHeader: boolean, colHeader: boolean, rowCells: boolean, colCells: boolean } +// 当 selectedCellHighlight 为 boolean 时 const s2Options = { interaction: { - selectedCellHighlight: true // 默认 false + selectedCellHighlight: true // 默认 false, 当 selectedCellsSpotlight 为 true 时,会高亮 rowHeader 和 colHeader (兼容未拓展类型前的设计) } }; + +// 同时还可以分别配置 selectedCellHighlight 中 header 和 cells 的高亮 +const S2Options = { + interaction: { + selectedCellHighlight: { + rowHeader: true, // 选中单元格时,高亮行头 + colHeader: true, // 选中单元格时,高亮列头 + rowCells: false, // 选中单元格时,高亮当前行 + colCells: false, // 选中单元格时,高亮当前列 + }, + }, +}; ``` ### 悬停聚焦 diff --git a/s2-site/docs/manual/basic/conditions.zh.md b/s2-site/docs/manual/basic/conditions.zh.md index 83d0dc4004..8d1a5dda2c 100644 --- a/s2-site/docs/manual/basic/conditions.zh.md +++ b/s2-site/docs/manual/basic/conditions.zh.md @@ -93,13 +93,14 @@ const s2Options = { | fieldValue | 单元格对应字段的值 | `number` | `string` | `null` | - | | | data | 单元格对应的​整行数据 | `object` | - | ✓ | -| 返回值 | 说明 | 类型 | 默认值 | 必选 | -| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------ | ---- | +| 返回值 | 说明 | 类型 | 默认值 | 必选 | +| --------- |---------------------------------------------------------------------------------------------------------------------------| --------- | ------ | ---- | | fill | 当作用于文本字段标记时,代表**文字填充颜色**
当作用于背景字段标记时,代表**单元格背景填充颜色**
当作用于柱状图字段标记时,代表**柱状图填充颜色**
当作用于图标字段标记时,代表**图标填充颜色**
| `string` | - | ✓ | -| icon | 仅用于**图标**字段标记,指定图标类型 | `string` | - | | -| isCompare | 仅用于**柱状图**字段标记,当为 `true` 时,可以定制柱状图的最大最小值 | `boolean` | - | | -| minValue | 仅用于**柱状图**字段标记且 `isCompare` 为 `true` 时,定制柱状图最小值 | `number` | - | | -| maxValue | 仅用于**柱状图**字段标记且 `isCompare` 为 `true` 时,定制柱状图最大值 | `number` | - | | +| icon | 仅用于**图标**字段标记,指定图标类型 | `string` | - | | +| isCompare | 仅用于**柱状图**字段标记,当为 `true` 时,可以定制柱状图的最大最小值 | `boolean` | - | | +| minValue | 仅用于**柱状图**字段标记且 `isCompare` 为 `true` 时,定制柱状图最小值 | `number` | - | | +| maxValue | 仅用于**柱状图**字段标记且 `isCompare` 为 `true` 时,定制柱状图最大值 | `number` | - | | +| fieldValue | 仅用于**柱状图**字段标记且 `isCompare` 为 `true` 时,定制柱状图的长度 | `number` | - | | > 如果`mapping`函数返回值为空,则表明不渲染该单元格的字段标记 diff --git a/s2-site/docs/manual/faq.zh.md b/s2-site/docs/manual/faq.zh.md index 125da5a197..23159575d3 100644 --- a/s2-site/docs/manual/faq.zh.md +++ b/s2-site/docs/manual/faq.zh.md @@ -25,7 +25,10 @@ order: 8 ### 官网访问有点慢,或打不开,有国内镜像吗? -有,国内镜像部署在 `gitee` 上面。[点击访问](https://antv-s2.gitee.io/) +原国内镜像 [https://antv-s2.gitee.io](https://antv-s2.gitee.io/) 不再维护,推荐访问新版官网 [https://s2.antv.antgroup.com](https://s2.antv.antgroup.com/) 速度更快。 + +[旧版官网](https://s2.antv.vision/) +[新版官网](https://s2.antv.antgroup.com/) ### 父级元素使用了 `transform: scale` 后,图表鼠标坐标响应不正确 diff --git a/s2-site/examples/analysis/conditions/demo/interval.ts b/s2-site/examples/analysis/conditions/demo/interval.ts index 31c8530a76..2dce6f6e74 100644 --- a/s2-site/examples/analysis/conditions/demo/interval.ts +++ b/s2-site/examples/analysis/conditions/demo/interval.ts @@ -17,13 +17,14 @@ fetch( interval: [ { field: 'number', - mapping() { + mapping(value) { return { fill: '#80BFFF', // 自定义柱状图范围 isCompare: true, maxValue: 8000, minValue: 300, + fieldValue: value > 7900 ? 10 : value, }; }, }, diff --git a/s2-site/examples/basic/table/demo/meta.json b/s2-site/examples/basic/table/demo/meta.json index 7742dc74ee..57336fa764 100644 --- a/s2-site/examples/basic/table/demo/meta.json +++ b/s2-site/examples/basic/table/demo/meta.json @@ -16,9 +16,9 @@ "filename": "table-column-group.ts", "title": { "zh": "列分组", - "en": "Column Grouping" + "en": "Column Group" }, - "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/jWifHNLOsB/08db1064-bb09-4d44-b42b-26aed1766545.png" + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*DgnhTYveL1AAAAAAAAAAAAAADmJ7AQ/original" } ] } diff --git a/s2-site/examples/interaction/advanced/demo/custom-tree-link-jump.ts b/s2-site/examples/interaction/advanced/demo/custom-tree-link-jump.ts new file mode 100644 index 0000000000..357099ed67 --- /dev/null +++ b/s2-site/examples/interaction/advanced/demo/custom-tree-link-jump.ts @@ -0,0 +1,61 @@ +import { S2Event, PivotSheet } from '@antv/s2'; + +fetch( + 'https://gw.alipayobjects.com/os/bmw-prod/af54ea12-01d7-4696-a51c-c4d5e4ede28e.json', +) + .then((res) => res.json()) + .then((res) => { + const container = document.getElementById('container'); + const s2DataConfig = { + fields: { + rows: [], + columns: ['type', 'sub_type'], + values: [ + 'measure-a', + 'measure-b', + 'measure-c', + 'measure-d', + 'measure-e', + 'measure-f', + ], + customTreeItems: res.customTreeItem, + valueInCols: false, + }, + data: res.data, + }; + const s2Options = { + width: 600, + height: 480, + hierarchyType: 'customTree', + interaction: { + linkFields: [ + 'custom-node-1', + 'custom-node-2', + 'custom-node-3', + 'custom-node-4', + 'custom-node-5', + 'measure-a', + 'measure-b', + 'measure-c', + 'measure-d', + 'measure-e', + 'measure-f', + ], + }, + }; + const s2 = new PivotSheet(container, s2DataConfig, s2Options); + + s2.on(S2Event.GLOBAL_LINK_FIELD_JUMP, (data) => { + console.log(data); + + const { key, record } = data; + const value = record[key]; + const a = document.createElement('a'); + a.target = '_blank'; + a.href = `https://antv-s2.gitee.io/zh/docs/manual/introduction?${key}=${value}`; + a.click(); + a.remove(); + }); + + s2.render(); + }); diff --git a/s2-site/examples/interaction/advanced/demo/meta.json b/s2-site/examples/interaction/advanced/demo/meta.json index e63563664f..d68eaec408 100644 --- a/s2-site/examples/interaction/advanced/demo/meta.json +++ b/s2-site/examples/interaction/advanced/demo/meta.json @@ -28,6 +28,14 @@ }, "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/aygNlbA0fu/link-table.gif" }, + { + "filename": "custom-tree-link-jump.ts", + "title": { + "zh": "链接跳转 - 自定义目录树", + "en": "Link jump" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*f6e6S4OUSdMAAAAAAAAAAAAADmJ7AQ/original" + }, { "filename": "pivot-hide-columns.ts", "title": { diff --git a/s2-site/examples/layout/custom/demo/hide-value.ts b/s2-site/examples/layout/custom/demo/hide-value.ts new file mode 100644 index 0000000000..c0d82a1d68 --- /dev/null +++ b/s2-site/examples/layout/custom/demo/hide-value.ts @@ -0,0 +1,52 @@ +import { S2DataConfig, S2Options } from '@antv/s2'; +import { PivotSheet } from '@antv/s2'; + +fetch('https://assets.antv.antgroup.com/s2/basic.json') + .then((res) => res.json()) + .then((data) => { + const container = document.getElementById('container'); + const s2DataConfig: S2DataConfig = { + fields: { + rows: ['province', 'city'], + columns: ['type'], + values: ['price', 'cost'], + }, + meta: [ + { + field: 'province', + name: '省份', + }, + { + field: 'city', + name: '城市', + }, + { + field: 'type', + name: '商品类别', + }, + { + field: 'price', + name: '价格', + }, + { + field: 'cost', + name: '成本', + }, + ], + data, + }; + + const s2Options: S2Options = { + width: 600, + height: 480, + style: { + colCfg: { + // 隐藏数值 (数值挂列头时生效, 即 s2DataConfig.fields.values) + hideMeasureColumn: true, + }, + }, + }; + const s2 = new PivotSheet(container, s2DataConfig, s2Options); + + s2.render(); + }); diff --git a/s2-site/examples/layout/custom/demo/meta.json b/s2-site/examples/layout/custom/demo/meta.json index 87f8e295da..6859d445e3 100644 --- a/s2-site/examples/layout/custom/demo/meta.json +++ b/s2-site/examples/layout/custom/demo/meta.json @@ -51,6 +51,22 @@ "en": "Hide Columns" }, "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/7uLsxYg0N/b85dc985-6a07-40fd-a73e-4f0f17e63b72.png" + }, + { + "filename": "hide-value.ts", + "title": { + "zh": "数值挂列头时隐藏数值", + "en": "Hide Value" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*dmQ5R401jjAAAAAAAAAAAAAADmJ7AQ/original" + }, + { + "filename": "only-show-row-header.ts", + "title": { + "zh": "只显示行头", + "en": "Only Show Row Header" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*pezZTI9lp50AAAAAAAAAAAAADmJ7AQ/original" } ] } diff --git a/s2-site/examples/layout/custom/demo/only-show-row-header.ts b/s2-site/examples/layout/custom/demo/only-show-row-header.ts new file mode 100644 index 0000000000..0f0386fd29 --- /dev/null +++ b/s2-site/examples/layout/custom/demo/only-show-row-header.ts @@ -0,0 +1,43 @@ +import { S2DataConfig, S2Options } from '@antv/s2'; +import { PivotSheet } from '@antv/s2'; + +fetch('https://assets.antv.antgroup.com/s2/basic.json') + .then((res) => res.json()) + .then((data) => { + const container = document.getElementById('container'); + const s2DataConfig: S2DataConfig = { + fields: { + rows: ['province', 'city'], + values: ['price'], + }, + meta: [ + { + field: 'province', + name: '省份', + }, + { + field: 'city', + name: '城市', + }, + { + field: 'price', + name: '价格', + }, + ], + data, + }; + + const s2Options: S2Options = { + width: 600, + height: 480, + style: { + colCfg: { + // 当只有行头和数值时, 隐藏数值, 可以让表格只显示行头 + hideMeasureColumn: true, + }, + }, + }; + const s2 = new PivotSheet(container, s2DataConfig, s2Options); + + s2.render(); + }); diff --git a/s2-site/examples/layout/custom/index.zh.md b/s2-site/examples/layout/custom/index.zh.md index 9ef4c1ad7c..2c43364e8c 100644 --- a/s2-site/examples/layout/custom/index.zh.md +++ b/s2-site/examples/layout/custom/index.zh.md @@ -1,4 +1,4 @@ --- -title: 自定义行列宽高 +title: 自定义布局 / 行列宽高 order: 1 --- diff --git a/s2-site/examples/theme/custom/demo/custom-schema.ts b/s2-site/examples/theme/custom/demo/custom-schema.ts index d79b72f57b..135cf95452 100644 --- a/s2-site/examples/theme/custom/demo/custom-schema.ts +++ b/s2-site/examples/theme/custom/demo/custom-schema.ts @@ -83,35 +83,6 @@ fetch( right: 'rgba(0,0,0,0)', }, }, - rowCell: { - text: { - fill: '#fff', - opacity: 0.4, - fontSize: 15, - fontWeight: 'bold', - }, - cell: { - backgroundColor: HEADER_BACK_COLOR, - padding: { - top: 12, - right: 8, - bottom: 12, - left: 8, - }, - interactionState: { - hover: { - backgroundColor: CELL_ACTIVE_BACK_COLOR, - backgroundOpacity: 1, - }, - selected: { - backgroundColor: 'rgb(63, 69, 97)', - }, - }, - horizontalBorderColor: BORDER_COLOR, - verticalBorderColor: BORDER_COLOR, - verticalBorderWidth: 1, - }, - }, colCell: { cell: { horizontalBorderColor: BORDER_COLOR,