diff --git a/package.json b/package.json index ae560037e2..8fcdb05722 100644 --- a/package.json +++ b/package.json @@ -170,5 +170,8 @@ "repository": { "type": "git", "url": "https://github.com/antvis/S2.git" + }, + "tnpm": { + "mode": "pnpm" } } diff --git a/packages/s2-core/__tests__/spreadsheet/interaction-brush-selection-scroll-spec.ts b/packages/s2-core/__tests__/spreadsheet/interaction-brush-selection-scroll-spec.ts index 7ccb885f6a..a1a8739ac9 100644 --- a/packages/s2-core/__tests__/spreadsheet/interaction-brush-selection-scroll-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/interaction-brush-selection-scroll-spec.ts @@ -143,6 +143,8 @@ const expectScrollBrush = async ( const allCells = s2.interaction.getCells(); const lastCell = allCells[allCells.length - 1]; + await sleep(500); + expect(s2.facet.getScrollOffset().scrollY).toBeGreaterThan(0); expect(brushRange.start.colIndex).toBe(allCells[0].colIndex); expect(brushRange.start.rowIndex).toBe(allCells[0].rowIndex); @@ -238,6 +240,7 @@ describe('PivotSheet Brush Selection Scroll Tests', () => { } as any); await emitBrushEvent(s2, 200, 200); + await sleep(500); expect(s2.facet.getScrollOffset().scrollY).toBeGreaterThan(0); expect(s2.interaction.getCells()).not.toBeEmpty(); @@ -280,6 +283,7 @@ describe('PivotSheet Brush Selection Scroll Tests', () => { // 只刷选 [省份] await emitBrushEvent(s2, 100, 200); + await sleep(500); // 只选中 [浙江省, 四川省] expect(s2.facet.getScrollOffset().scrollY).toBeGreaterThan(0); diff --git a/packages/s2-core/__tests__/spreadsheet/scroll-spec.ts b/packages/s2-core/__tests__/spreadsheet/scroll-spec.ts index 062dd59f38..34a09bb472 100644 --- a/packages/s2-core/__tests__/spreadsheet/scroll-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/scroll-spec.ts @@ -678,7 +678,7 @@ describe('Scroll Tests', () => { canvas.dispatchEvent(wheelEvent); - await sleep(200); + await sleep(1000); expect(onScroll).toHaveBeenCalled(); }); diff --git a/packages/s2-core/__tests__/spreadsheet/spread-sheet-spec.ts b/packages/s2-core/__tests__/spreadsheet/spread-sheet-spec.ts index 44537bec4c..955b611046 100644 --- a/packages/s2-core/__tests__/spreadsheet/spread-sheet-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/spread-sheet-spec.ts @@ -140,6 +140,8 @@ describe('SpreadSheet Tests', () => { s2.updateScrollOffset({ offsetX: { value: 30 }, }); + await sleep(500); + expect(s2.facet.hScrollBar.current()).toBeGreaterThan(0); expect(s2.facet.getScrollOffset()).toMatchInlineSnapshot(` Object { @@ -165,6 +167,8 @@ describe('SpreadSheet Tests', () => { s2.updateScrollOffset({ offsetY: { value: 20 }, }); + + await sleep(500); expect(s2.facet.vScrollBar.current()).toBeGreaterThan(0); expect(s2.facet.getScrollOffset()).toMatchInlineSnapshot(` Object { @@ -200,6 +204,8 @@ describe('SpreadSheet Tests', () => { s2.updateScrollOffset({ rowHeaderOffsetX: { value: 30 }, }); + + await sleep(500); expect(s2.facet.hRowScrollBar.current()).toBeGreaterThan(0); expect(s2.facet.getScrollOffset()).toMatchInlineSnapshot(` Object { @@ -230,6 +236,9 @@ describe('SpreadSheet Tests', () => { offsetX: { value: 30 }, rowHeaderOffsetX: { value: 40 }, }); + + await sleep(500); + expect(s2.facet.vScrollBar.current()).toBeGreaterThan(0); expect(s2.facet.hScrollBar.current()).toBeGreaterThan(0); expect(s2.facet.hRowScrollBar.current()).toBeGreaterThan(0); diff --git a/packages/s2-core/__tests__/unit/cell/__snapshots__/data-cell-spec.ts.snap b/packages/s2-core/__tests__/unit/cell/__snapshots__/data-cell-spec.ts.snap new file mode 100644 index 0000000000..1c3989532b --- /dev/null +++ b/packages/s2-core/__tests__/unit/cell/__snapshots__/data-cell-spec.ts.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Data Cell Tests Data Cell Formatter & Method Tests should get empty chart data and default options 1`] = ` +Object { + "autoFit": true, + "height": 83, + "theme": Object { + "type": "light", + }, + "width": 83, + "x": NaN, + "y": NaN, +} +`; + +exports[`Data Cell Tests Data Cell Formatter & Method Tests should get multiple chart data and all options 1`] = ` +Object { + "autoFit": true, + "data": Array [ + Object { + "genre": "Sports", + "sold": 275, + }, + ], + "encode": Object { + "color": "genre", + "x": "genre", + "y": "sold", + }, + "type": "interval", +} +`; + +exports[`Data Cell Tests Data Cell Formatter & Method Tests should get multiple chart data and all options 2`] = ` +Object { + "autoFit": true, + "data": Array [ + Object { + "genre": "Sports", + "sold": 275, + }, + ], + "encode": Object { + "color": "genre", + "x": "genre", + "y": "sold", + }, + "height": 83, + "theme": Object { + "type": "dark", + }, + "type": "interval", + "width": 83, + "x": 8, + "y": 208, +} +`; 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 e3a531b48d..096ccf74bf 100644 --- a/packages/s2-core/__tests__/unit/cell/data-cell-spec.ts +++ b/packages/s2-core/__tests__/unit/cell/data-cell-spec.ts @@ -99,7 +99,7 @@ describe('Data Cell Tests', () => { ); }); - describe('Data Cell Formatter Tests', () => { + describe('Data Cell Formatter & Method Tests', () => { beforeEach(() => { const container = document.createElement('div'); @@ -137,6 +137,64 @@ describe('Data Cell Tests', () => { expect(dataCell.getTextShape().attr('fill')).toEqual(DEFAULT_FONT_COLOR); }); + + test('should get empty chart data and default options', () => { + const dataCell = new DataCell(meta, s2); + + expect(dataCell.isMultiData()).toBeFalsy(); + expect(dataCell.isChartData()).toBeFalsy(); + expect(dataCell.getRenderChartData()).toBeUndefined(); + expect(dataCell.getRenderChartOptions()).toMatchSnapshot(); + }); + + test('should get correctly cell data status', () => { + const multipleMeta = { + fieldValue: { + values: [1, 2, 3], + }, + } as unknown as ViewMeta; + + const dataCell = new DataCell(multipleMeta, s2); + + expect(dataCell.isMultiData()).toBeTruthy(); + expect(dataCell.isChartData()).toBeFalsy(); + }); + + test('should get multiple chart data and all options', () => { + s2.setThemeCfg({ name: 'dark' }); + + const multipleMeta = { + fieldValue: { + values: { + type: 'interval', + autoFit: true, + data: [ + { + genre: 'Sports', + sold: 275, + }, + ], + encode: { + x: 'genre', + y: 'sold', + color: 'genre', + }, + }, + }, + value: 'value', + width: 100, + height: 100, + x: 0, + y: 200, + } as unknown as ViewMeta; + + const dataCell = new DataCell(multipleMeta, s2); + + expect(dataCell.isMultiData()).toBeTruthy(); + expect(dataCell.isChartData()).toBeTruthy(); + expect(dataCell.getRenderChartData()).toMatchSnapshot(); + expect(dataCell.getRenderChartOptions()).toMatchSnapshot(); + }); }); describe('Data Cell Shape Tests', () => { @@ -254,6 +312,7 @@ describe('Data Cell Tests', () => { expect(dataCell?.getTextShape().parsedStyle.fill).toBeColor('#D03050'); }); }); + describe('Condition Tests', () => { const s2 = createPivotSheet({ conditions: { @@ -530,8 +589,6 @@ describe('Data Cell Tests', () => { }); describe('Data Cell Interaction', () => { - // let s2: SpreadSheet; - beforeEach(async () => { s2 = createPivotSheet({ showSeriesNumber: true, @@ -541,6 +598,11 @@ describe('Data Cell Tests', () => { }); await s2.render(); }); + + afterEach(() => { + s2.destroy(); + }); + const emitEvent = (type: S2Event, event: Partial) => { s2.emit(type, { originalEvent: event, 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 1b02c7e310..35f4efca83 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 @@ -3,7 +3,7 @@ import { Canvas, CanvasEvent } from '@antv/g'; import { cloneDeep, get, last } from 'lodash'; import dataCfg from 'tests/data/simple-data.json'; import { waitForRender } from 'tests/util'; -import { getContainer } from 'tests/util/helpers'; +import { createPivotSheet, getContainer, sleep } from 'tests/util/helpers'; import type { BaseEvent, BaseTooltipOperatorMenuOptions, @@ -498,7 +498,7 @@ describe('PivotSheet Tests', () => { expect(afterRender).toHaveBeenCalledTimes(1); }); - test('should emit after real dataCell render', async () => { + test('should emit after real dataCell render event', async () => { const afterRealDataCellRender = jest.fn(); const sheet = new PivotSheet(container, dataCfg, s2Options); @@ -511,6 +511,36 @@ describe('PivotSheet Tests', () => { expect(afterRealDataCellRender).toHaveBeenCalledTimes(1); }); + test('should emit data cell render event', async () => { + const cornerCellRender = jest.fn(); + const rowCellRender = jest.fn(); + const colCellRender = jest.fn(); + const dataCellRender = jest.fn(); + const seriesNumberCellRender = jest.fn(); + const layoutCellRender = jest.fn(); + + const sheet = createPivotSheet( + { + ...s2Options, + showSeriesNumber: true, + }, + { useSimpleData: false }, + ); + + sheet.on(S2Event.CORNER_CELL_RENDER, cornerCellRender); + sheet.on(S2Event.ROW_CELL_RENDER, rowCellRender); + sheet.on(S2Event.COL_CELL_RENDER, colCellRender); + sheet.on(S2Event.DATA_CELL_RENDER, dataCellRender); + sheet.on(S2Event.SERIES_NUMBER_CELL_RENDER, seriesNumberCellRender); + sheet.on(S2Event.LAYOUT_CELL_RENDER, layoutCellRender); + + await sheet.render(); + await sleep(500); + + expect(dataCellRender).toHaveBeenCalledTimes(8); + expect(layoutCellRender).toHaveBeenCalledTimes(20); + }); + test('should updatePagination', () => { s2.updatePagination({ current: 2, diff --git a/packages/s2-core/__tests__/unit/utils/tooltip-spec.ts b/packages/s2-core/__tests__/unit/utils/tooltip-spec.ts index 93d4283a4e..d0a348b6a5 100644 --- a/packages/s2-core/__tests__/unit/utils/tooltip-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/tooltip-spec.ts @@ -34,6 +34,7 @@ import { PivotSheet, type S2DataConfig, type S2Options, + type TooltipOperatorMenuItems, } from '@/index'; import type { BaseFacet } from '@/facet/base-facet'; import type { BBox } from '@/engine'; @@ -256,6 +257,9 @@ describe('Tooltip Utils Tests', () => { options: { tooltip, }, + interaction: { + getInteractedCells: jest.fn(() => []), + }, } as unknown as SpreadSheet; expect(getTooltipOptions(spreadsheet, {} as Event)).toEqual({ @@ -306,6 +310,9 @@ describe('Tooltip Utils Tests', () => { options: { tooltip, }, + interaction: { + getInteractedCells: jest.fn(() => []), + }, } as unknown as SpreadSheet; const tooltipOptions = omit( @@ -330,16 +337,43 @@ describe('Tooltip Utils Tests', () => { }, ); + test('should use interacted cell type if cannot get current cell type', () => { + const mockInteractedCell = { cellType: CellType.DATA_CELL }; + + const tooltip: Tooltip = { + enable: true, + [CellType.DATA_CELL]: { + enable: false, + }, + content: '', + operation: {}, + }; + + const spreadsheet = { + getCellType: () => undefined, + options: { + tooltip, + }, + interaction: { + getInteractedCells: jest.fn(() => [mockInteractedCell]), + }, + } as unknown as SpreadSheet; + + const tooltipOptions = getTooltipOptions(spreadsheet, {} as Event); + + expect(tooltipOptions?.enable).toBeFalsy(); + }); + test('should filter not displayed tooltip operation menus', () => { const mockCell = { cellType: CellType.DATA_CELL, } as unknown as S2CellType; const onClick = jest.fn(); - const defaultMenus = [ + const defaultMenus: TooltipOperatorMenuItems = [ { key: 'default-menu', - text: 'default-menu', + label: 'default-menu', }, ]; diff --git a/packages/s2-core/package.json b/packages/s2-core/package.json index da5d919a4d..9dfa6c4ee2 100644 --- a/packages/s2-core/package.json +++ b/packages/s2-core/package.json @@ -60,10 +60,10 @@ "tsc": "tsc --noEmit" }, "dependencies": { - "@antv/g-lite": "^1.2.19", + "@antv/g-lite": "^1.2.20", "@antv/event-emitter": "^0.1.3", - "@antv/g": "^5.18.21", - "@antv/g-canvas": "^1.11.24", + "@antv/g": "^5.18.23", + "@antv/g-canvas": "^1.11.25", "d3-ease": "^3.0.1", "d3-interpolate": "^1.3.2", "d3-timer": "^1.0.9", @@ -72,12 +72,12 @@ }, "devDependencies": { "@testing-library/dom": "^9.3.3", - "@types/d3-dsv": "^3.0.0", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-timer": "^3.0.0", - "@types/tinycolor2": "^1.4.4", - "csstype": "^3.1.2", + "@types/d3-dsv": "^3.0.7", + "@types/d3-ease": "^3.0.2", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-timer": "^3.0.2", + "@types/tinycolor2": "^1.4.6", + "csstype": "^3.1.3", "d3-dsv": "^1.1.1", "tinycolor2": "^1.6.0" }, @@ -97,5 +97,8 @@ ], "publishConfig": { "access": "public" + }, + "tnpm": { + "mode": "pnpm" } } diff --git a/packages/s2-core/src/cell/base-cell.ts b/packages/s2-core/src/cell/base-cell.ts index 01d216bfcf..be85d5327d 100644 --- a/packages/s2-core/src/cell/base-cell.ts +++ b/packages/s2-core/src/cell/base-cell.ts @@ -317,7 +317,7 @@ export abstract class BaseCell extends Group { return resize?.[type]; } - public getBBoxByType(type = CellClipBox.BORDER_BOX) { + public getBBoxByType(type = CellClipBox.BORDER_BOX): SimpleBBox { const bbox: SimpleBBox = { x: this.meta.x, y: this.meta.y, diff --git a/packages/s2-core/src/cell/data-cell.ts b/packages/s2-core/src/cell/data-cell.ts index 31814695d3..aed4dc358c 100644 --- a/packages/s2-core/src/cell/data-cell.ts +++ b/packages/s2-core/src/cell/data-cell.ts @@ -1,28 +1,44 @@ import type { PointLike } from '@antv/g'; -import { find, findLast, first, get, isEmpty, isEqual, merge } from 'lodash'; +import { + find, + findLast, + first, + get, + isEmpty, + isEqual, + isObject, + isPlainObject, + merge, +} from 'lodash'; import { BaseCell } from '../cell/base-cell'; +import { G2_THEME_TYPE } from '../common'; +import { EMPTY_PLACEHOLDER } from '../common/constant/basic'; import { CellType, InteractionStateName, SHAPE_STYLE_MAP, } from '../common/constant/interaction'; -import { - CellBorderPosition, - CellClipBox, - type HeaderActionNameOptions, - type IconCondition, - type InteractionStateTheme, - type RenderTextShapeOptions, -} from '../common/interface'; import type { + BaseChartData, CellMeta, Condition, - FormatResult, ConditionMappingResult, + FormatResult, + MiniChartData, + MultiData, TextTheme, ViewMeta, ViewMetaIndexType, } from '../common/interface'; +import { + CellBorderPosition, + CellClipBox, + type HeaderActionNameOptions, + type IconCondition, + type InteractionStateTheme, + type RenderTextShapeOptions, +} from '../common/interface'; +import { getFieldValueOfViewMetaData } from '../data-set/cell-data'; import { getHorizontalTextIconPosition, getVerticalIconPosition, @@ -33,12 +49,10 @@ import { shouldUpdateBySelectedCellsHighlight, updateBySelectedCellsHighlight, } from '../utils/cell/data-cell'; +import { groupIconsByPosition } from '../utils/cell/header-cell'; import { getIconPosition } 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 { getFieldValueOfViewMetaData } from '../data-set/cell-data'; -import { groupIconsByPosition } from '../utils/cell/header-cell'; +import { updateShapeAttr } from '../utils/g-renders'; import type { RawData } from './../common/interface/s2DataConfig'; /** @@ -61,6 +75,39 @@ export class DataCell extends BaseCell { return CellType.DATA_CELL; } + public isMultiData() { + const fieldValue = this.getFieldValue(); + + return isObject(fieldValue); + } + + public isChartData() { + const fieldValue = this.getFieldValue(); + + return isPlainObject( + (fieldValue as unknown as MultiData)?.values, + ); + } + + public getRenderChartData(): BaseChartData { + const { fieldValue } = this.meta; + + return (fieldValue as MultiData)?.values as BaseChartData; + } + + public getRenderChartOptions() { + const chartData = this.getRenderChartData(); + const cellArea = this.getBBoxByType(CellClipBox.CONTENT_BOX); + const themeName = this.spreadsheet.getThemeName(); + + return { + autoFit: true, + theme: { type: G2_THEME_TYPE[themeName] }, + ...cellArea, + ...chartData, + }; + } + protected getBorderPositions(): CellBorderPosition[] { return [CellBorderPosition.BOTTOM, CellBorderPosition.RIGHT]; } diff --git a/packages/s2-core/src/common/constant/events/basic.ts b/packages/s2-core/src/common/constant/events/basic.ts index 10ed128684..fcda31b006 100644 --- a/packages/s2-core/src/common/constant/events/basic.ts +++ b/packages/s2-core/src/common/constant/events/basic.ts @@ -11,6 +11,8 @@ export enum S2Event { ROW_CELL_BRUSH_SELECTION = 'row-cell:brush-selection', ROW_CELL_COLLAPSED = 'row-cell:collapsed', ROW_CELL_ALL_COLLAPSED = 'row-cell:all-collapsed', + ROW_CELL_RENDER = 'row-cell:render', + // 内部用来通信的 event ROW_CELL_COLLAPSED__PRIVATE = 'row-cell:collapsed__private', ROW_CELL_ALL_COLLAPSED__PRIVATE = 'row-cell:all-collapsed__private', @@ -26,6 +28,7 @@ export enum S2Event { COL_CELL_BRUSH_SELECTION = 'col-cell:brush-selection', COL_CELL_EXPANDED = 'col-cell:expanded', COL_CELL_HIDDEN = 'col-cell:hidden', + COL_CELL_RENDER = 'col-cell:render', /** ================ Data Cell ================ */ DATA_CELL_HOVER = 'data-cell:hover', @@ -37,6 +40,7 @@ export enum S2Event { DATA_CELL_MOUSE_MOVE = 'data-cell:mouse-move', DATA_CELL_BRUSH_SELECTION = 'data-cell:brush-selection', DATA_CELL_SELECT_MOVE = 'data-cell:select-move', + DATA_CELL_RENDER = 'data-cell:render', /** ================ Corner Cell ================ */ CORNER_CELL_HOVER = 'corner-cell:hover', @@ -46,6 +50,7 @@ export enum S2Event { CORNER_CELL_MOUSE_DOWN = 'corner-cell:mouse-down', CORNER_CELL_MOUSE_UP = 'corner-cell:mouse-up', CORNER_CELL_MOUSE_MOVE = 'corner-cell:mouse-move', + CORNER_CELL_RENDER = 'corner-cell:render', /** ================ Merged Cells ================ */ MERGED_CELLS_HOVER = 'merged-cells:hover', @@ -55,6 +60,10 @@ export enum S2Event { MERGED_CELLS_MOUSE_DOWN = 'merged-cells:mouse-down', MERGED_CELLS_MOUSE_UP = 'merged-cells:mouse-up', MERGED_CELLS_MOUSE_MOVE = 'merged-cells:mouse-move', + MERGED_CELLS_RENDER = 'merged-cells:render', + + /** ================ SeriesNumber Cell ================ */ + SERIES_NUMBER_CELL_RENDER = 'series-number-cell:render', /** ================ Sort ================ */ RANGE_SORT = 'sort:range-sort', @@ -66,7 +75,7 @@ export enum S2Event { /** ================ Table Layout ================ */ LAYOUT_AFTER_HEADER_LAYOUT = 'layout:after-header-layout', - LAYOUT_CELL_MOUNTED = 'layout:cell-mounted', + LAYOUT_CELL_RENDER = 'layout:cell-render', LAYOUT_PAGINATION = 'layout:pagination', LAYOUT_AFTER_REAL_DATA_CELL_RENDER = 'layout:after-real-data-cell-render', LAYOUT_AFTER_RENDER = 'layout:after-render', diff --git a/packages/s2-core/src/common/constant/theme.ts b/packages/s2-core/src/common/constant/theme.ts index e20b3607ae..cf1a9e301e 100644 --- a/packages/s2-core/src/common/constant/theme.ts +++ b/packages/s2-core/src/common/constant/theme.ts @@ -2,6 +2,7 @@ import { paletteColorful } from '../../theme/palette/colorful'; import { paletteDefault } from '../../theme/palette/default'; import { paletteGray } from '../../theme/palette/gray'; import { paletteDark } from '../../theme/palette/dark'; +import type { ThemeName } from '../interface'; // Map of the theme export const PALETTE_MAP = { @@ -26,3 +27,14 @@ export const INTERVAL_BAR_HEIGHT = 12; * 注入主题 css 变量的节点名 */ export const STYLE_ELEMENT_ID = `antv-s2-core-vars`; + +/** + * 兼容 G2 主题: S2 和 G2 的主题名转换 + * https://g2.antv.antgroup.com/manual/core/theme + */ +export const G2_THEME_TYPE: Record = { + default: 'light', + colorful: 'light', + gray: 'light', + dark: 'dark', +}; diff --git a/packages/s2-core/src/common/interface/basic.ts b/packages/s2-core/src/common/interface/basic.ts index c7ad26e749..65b1d3ac64 100644 --- a/packages/s2-core/src/common/interface/basic.ts +++ b/packages/s2-core/src/common/interface/basic.ts @@ -1,5 +1,5 @@ -import type { FederatedPointerEvent as Event, Group } from '@antv/g'; -import type { MergedCell } from '../../cell'; +import type { FederatedPointerEvent as Event } from '@antv/g'; +import type { DataCell, MergedCell } from '../../cell'; import type { CustomTreeNode, Data, @@ -370,13 +370,13 @@ export interface InternalFullyHeaderActionIcon extends HeaderActionIcon { isSortIcon?: boolean; } -export type CellCallback = ( +export type CellCallback = ( node: Node, spreadsheet: SpreadSheet, headerConfig: T, -) => S2CellType; +) => K; -export type DataCellCallback = (viewMeta: ViewMeta) => S2CellType; +export type DataCellCallback = (viewMeta: ViewMeta) => DataCell; export type MergedCellCallback = ( spreadsheet: SpreadSheet, @@ -502,13 +502,6 @@ export interface GridInfo { rows: number[]; } -export type RenderHandler = ( - options: Record, - context: { - group: Group; - }, -) => void; - export interface Point { x: number; y: number; diff --git a/packages/s2-core/src/common/interface/emitter.ts b/packages/s2-core/src/common/interface/emitter.ts index fb150c90fa..9d22472eee 100644 --- a/packages/s2-core/src/common/interface/emitter.ts +++ b/packages/s2-core/src/common/interface/emitter.ts @@ -17,6 +17,7 @@ import type { FilterParam, SortParams, S2Style } from '../../common/interface'; import type { RawData } from '../../common/interface/s2DataConfig'; import type { CopyableList } from '../../utils/export/interface'; import type { Node } from '../../facet/layout/node'; +import type { CornerCell, MergedCell, SeriesNumberCell } from '../../cell'; import type { ResizeInfo } from './resize'; type CanvasEventHandler = (event: CanvasEvent) => void; @@ -78,6 +79,7 @@ export interface EmitterType { [S2Event.DATA_CELL_CONTEXT_MENU]: CanvasEventHandler; [S2Event.DATA_CELL_BRUSH_SELECTION]: (cells: (DataCell | CellMeta)[]) => void; [S2Event.DATA_CELL_SELECT_MOVE]: (metas: CellMeta[]) => void; + [S2Event.DATA_CELL_RENDER]: (cell: DataCell) => void; /** ================ Row Cell ================ */ [S2Event.ROW_CELL_MOUSE_DOWN]: CanvasEventHandler; @@ -93,6 +95,7 @@ export interface EmitterType { [S2Event.ROW_CELL_COLLAPSED__PRIVATE]: (data: RowCellCollapsedParams) => void; [S2Event.ROW_CELL_ALL_COLLAPSED]: (isCollapsed: boolean) => void; [S2Event.ROW_CELL_ALL_COLLAPSED__PRIVATE]: (isCollapsed: boolean) => void; + [S2Event.ROW_CELL_RENDER]: (cell: RowCell) => void; /** ================ Col Cell ================ */ [S2Event.COL_CELL_MOUSE_DOWN]: CanvasEventHandler; @@ -108,6 +111,7 @@ export interface EmitterType { currentHiddenColumnsInfo: HiddenColumnsInfo, hiddenColumnsDetail: HiddenColumnsInfo[], ) => void; + [S2Event.COL_CELL_RENDER]: (cell: ColCell) => void; /** ================ Corner Cell ================ */ [S2Event.CORNER_CELL_MOUSE_MOVE]: CanvasEventHandler; @@ -117,6 +121,7 @@ export interface EmitterType { [S2Event.CORNER_CELL_DOUBLE_CLICK]: CanvasEventHandler; [S2Event.CORNER_CELL_CONTEXT_MENU]: CanvasEventHandler; [S2Event.CORNER_CELL_MOUSE_UP]: CanvasEventHandler; + [S2Event.CORNER_CELL_RENDER]: (cell: CornerCell) => void; /** ================ Merged Cells ================ */ [S2Event.MERGED_CELLS_MOUSE_DOWN]: CanvasEventHandler; @@ -126,6 +131,10 @@ export interface EmitterType { [S2Event.MERGED_CELLS_CLICK]: CanvasEventHandler; [S2Event.MERGED_CELLS_CONTEXT_MENU]: CanvasEventHandler; [S2Event.MERGED_CELLS_DOUBLE_CLICK]: CanvasEventHandler; + [S2Event.MERGED_CELLS_RENDER]: (cell: MergedCell) => void; + + /** ================ SeriesNumber Cell ================ */ + [S2Event.SERIES_NUMBER_CELL_RENDER]: (cell: SeriesNumberCell) => void; /** ================ Layout ================ */ [S2Event.LAYOUT_PAGINATION]: (data: { @@ -140,7 +149,7 @@ export interface EmitterType { remove: [number, number][]; spreadsheet: SpreadSheet; }) => void; - [S2Event.LAYOUT_CELL_MOUNTED]: (cell: S2CellType) => void; + [S2Event.LAYOUT_CELL_RENDER]: (cell: T) => void; [S2Event.LAYOUT_BEFORE_RENDER]: () => void; [S2Event.LAYOUT_AFTER_RENDER]: () => void; [S2Event.LAYOUT_DESTROY]: () => void; diff --git a/packages/s2-core/src/common/interface/s2DataConfig.ts b/packages/s2-core/src/common/interface/s2DataConfig.ts index 4414f769bc..3ace8790ce 100644 --- a/packages/s2-core/src/common/interface/s2DataConfig.ts +++ b/packages/s2-core/src/common/interface/s2DataConfig.ts @@ -10,10 +10,12 @@ export interface BaseChartData { * 类型 */ type: MiniChartTypes; + /** * 数据 */ data: RawData[]; + /** * 坐标轴数据 */ @@ -21,6 +23,8 @@ export interface BaseChartData { x: keyof RawData; y: keyof RawData; }; + + [key: string]: unknown; } /* 子弹图数据结构 */ @@ -29,10 +33,12 @@ export interface BulletValue { * 类型 */ type: MiniChartTypes.Bullet; + /** * 当前值 */ measure: number | string; + /** * 目标值 */ diff --git a/packages/s2-core/src/common/interface/s2Options.ts b/packages/s2-core/src/common/interface/s2Options.ts index a238332d3c..8531a8ce5c 100644 --- a/packages/s2-core/src/common/interface/s2Options.ts +++ b/packages/s2-core/src/common/interface/s2Options.ts @@ -1,4 +1,5 @@ import type { CanvasConfig } from '@antv/g'; +import type { CornerCell, RowCell, SeriesNumberCell } from '../../cell'; import type { CellCallback, CornerHeaderCallback, @@ -27,7 +28,7 @@ import type { import type { SpreadSheet } from '../../sheet-type'; import type { CustomSVGIcon, HeaderActionIcon } from './basic'; import type { Conditions } from './condition'; -import type { InteractionOptions } from './interaction'; +import type { InteractionOptions, S2CellType } from './interaction'; import type { S2Style } from './style'; import type { BaseTooltipOperatorMenuOptions, @@ -164,25 +165,25 @@ export interface S2BasicOptions< * 自定义角头单元格 * @see https://s2.antv.antgroup.com/examples/custom/custom-cell#corner-cell */ - cornerCell?: CellCallback; + cornerCell?: CellCallback; /** * 自定义序号单元格 * @see https://s2.antv.antgroup.com/examples/custom/custom-cell#series-number-cell */ - seriesNumberCell?: CellCallback; + seriesNumberCell?: CellCallback; /** * 自定义行头单元格 * @see https://s2.antv.antgroup.com/examples/custom/custom-cell#row-cell */ - rowCell?: CellCallback; + rowCell?: CellCallback; /** * 自定义列头单元格 * @see https://s2.antv.antgroup.com/examples/custom/custom-cell#col-cell */ - colCell?: CellCallback; + colCell?: CellCallback; /** * 自定义合并单元格 diff --git a/packages/s2-core/src/facet/base-facet.ts b/packages/s2-core/src/facet/base-facet.ts index 106ff35aa5..e2d73d04b1 100644 --- a/packages/s2-core/src/facet/base-facet.ts +++ b/packages/s2-core/src/facet/base-facet.ts @@ -1275,24 +1275,29 @@ export abstract class BaseFacet { this.columnHeader.onColScroll(scrollX, KEY_GROUP_COL_RESIZE_AREA); } - addCell = (cell: S2CellType) => { + addDataCell = (cell: DataCell) => { this.panelScrollGroup?.appendChild(cell); - this.spreadsheet.emit(S2Event.LAYOUT_CELL_MOUNTED, cell); + + setTimeout(() => { + this.spreadsheet.emit(S2Event.DATA_CELL_RENDER, cell); + this.spreadsheet.emit(S2Event.LAYOUT_CELL_RENDER, cell); + }, 100); }; - realCellRender = (scrollX: number, scrollY: number) => { + realDataCellRender = (scrollX: number, scrollY: number) => { const indexes = this.calculateXYIndexes(scrollX, scrollY); DebuggerUtil.getInstance().logger( - 'realCellRender:', + 'realDataCellRender:', this.preCellIndexes, indexes, ); - const { add, remove } = diffPanelIndexes(this.preCellIndexes!, indexes); + const { add: willAddDataCells, remove: willRemoveDataCells } = + diffPanelIndexes(this.preCellIndexes!, indexes); DebuggerUtil.getInstance().debugCallback(DEBUG_VIEW_RENDER, () => { // add new cell in panelCell - each(add, ([i, j]) => { + each(willAddDataCells, ([i, j]) => { const viewMeta = this.getCellMeta(j, i); if (viewMeta) { @@ -1300,29 +1305,33 @@ export abstract class BaseFacet { // mark cell for removing cell.name = `${i}-${j}`; - this.addCell(cell); + this.addDataCell(cell); } }); - const allCells = getAllChildCells( - this.panelGroup.children, + const allDataCells = getAllChildCells( + this.panelGroup.children as DataCell[], DataCell, - ) as DataCell[]; + ); // remove cell from panelCell - each(remove, ([i, j]) => { - const findOne = find(allCells, (cell) => cell.name === `${i}-${j}`); + each(willRemoveDataCells, ([i, j]) => { + const mountedDataCell = find( + allDataCells, + (cell) => cell.name === `${i}-${j}`, + ); - findOne?.remove(); + mountedDataCell?.remove(); }); DebuggerUtil.getInstance().logger( - `Render Cell Panel: ${allCells?.length}, Add: ${add?.length}, Remove: ${remove?.length}`, + `Render Cell Panel: ${allDataCells?.length}, Add: ${willAddDataCells?.length}, Remove: ${willRemoveDataCells?.length}`, ); }); + this.preCellIndexes = indexes; this.spreadsheet.emit(S2Event.LAYOUT_AFTER_REAL_DATA_CELL_RENDER, { - add, - remove, + add: willAddDataCells, + remove: willRemoveDataCells, spreadsheet: this.spreadsheet, }); }; @@ -1549,7 +1558,7 @@ export abstract class BaseFacet { this.spreadsheet.hideTooltip(); this.spreadsheet.interaction.clearHoverTimer(); - this.realCellRender(scrollX, scrollY); + this.realDataCellRender(scrollX, scrollY); this.updatePanelScrollGroup(); this.translateRelatedGroups(scrollX, scrollY, rowHeaderScrollX); if (!skipScrollEvent) { diff --git a/packages/s2-core/src/facet/header/col.ts b/packages/s2-core/src/facet/header/col.ts index e043204c0a..6f5f2c9ab5 100644 --- a/packages/s2-core/src/facet/header/col.ts +++ b/packages/s2-core/src/facet/header/col.ts @@ -4,8 +4,8 @@ import { ColCell } from '../../cell/col-cell'; import { FRONT_GROUND_GROUP_COL_SCROLL_Z_INDEX, KEY_GROUP_COL_SCROLL, + S2Event, } from '../../common/constant'; -import type { S2CellType } from '../../common/interface'; import type { Node } from '../layout/node'; import { translateGroupX } from '../utils'; import { BaseHeader } from './base'; @@ -25,7 +25,7 @@ export class ColHeader extends BaseHeader { this.initScrollGroup(); } - protected getCellInstance(node: Node): S2CellType { + protected getCellInstance(node: Node) { const { spreadsheet } = this.getHeaderConfig(); const { colCell } = spreadsheet.options; @@ -97,7 +97,7 @@ export class ColHeader extends BaseHeader { } protected layout() { - const { nodes } = this.getHeaderConfig(); + const { nodes, spreadsheet } = this.getHeaderConfig(); each(nodes, (node) => { if (this.isColCellInRect(node)) { @@ -108,6 +108,8 @@ export class ColHeader extends BaseHeader { const group = this.getCellGroup(node); group?.appendChild(cell); + spreadsheet.emit(S2Event.COL_CELL_RENDER, cell as ColCell); + spreadsheet.emit(S2Event.LAYOUT_CELL_RENDER, cell); } }); } diff --git a/packages/s2-core/src/facet/header/corner.ts b/packages/s2-core/src/facet/header/corner.ts index 0309060a90..0b7cb4f848 100644 --- a/packages/s2-core/src/facet/header/corner.ts +++ b/packages/s2-core/src/facet/header/corner.ts @@ -7,6 +7,7 @@ import type { CornerBBox } from '../bbox/cornerBBox'; import type { PanelBBox } from '../bbox/panelBBox'; import { Node } from '../layout/node'; import { translateGroupX } from '../utils'; +import { S2Event } from '../../common'; import { getDefaultCornerText, getDefaultSeriesNumberText, @@ -22,7 +23,7 @@ export class CornerHeader extends BaseHeader { super(config); } - protected getCellInstance(node: Node): S2CellType { + protected getCellInstance(node: Node): CornerCell { const headerConfig = this.getHeaderConfig(); const { spreadsheet } = headerConfig; const { cornerCell } = spreadsheet.options; @@ -259,6 +260,8 @@ export class CornerHeader extends BaseHeader { const cell = this.getCellInstance(node); this.appendChild(cell); + spreadsheet.emit(S2Event.CORNER_CELL_RENDER, cell); + spreadsheet.emit(S2Event.LAYOUT_CELL_RENDER, cell); }); } diff --git a/packages/s2-core/src/facet/header/row.ts b/packages/s2-core/src/facet/header/row.ts index 149bbda705..dafbd5eaf2 100644 --- a/packages/s2-core/src/facet/header/row.ts +++ b/packages/s2-core/src/facet/header/row.ts @@ -1,9 +1,9 @@ import { Rect } from '@antv/g'; import { each, isEmpty } from 'lodash'; import { RowCell } from '../../cell'; -import type { S2CellType } from '../../common/interface'; import type { Node } from '../layout/node'; import { translateGroup } from '../utils'; +import { S2Event } from '../../common'; import { BaseHeader } from './base'; import type { RowHeaderConfig } from './interface'; @@ -15,7 +15,7 @@ export class RowHeader extends BaseHeader { super(config); } - protected getCellInstance(node: Node): S2CellType { + protected getCellInstance(node: Node): RowCell { const headerConfig = this.getHeaderConfig(); const { spreadsheet } = headerConfig; const { rowCell } = spreadsheet.options; @@ -54,7 +54,7 @@ export class RowHeader extends BaseHeader { each(nodes, (node) => { if (rowCellInRect(node) && node.height !== 0) { - let cell: S2CellType | null = null; + let cell: RowCell | null = null; // 首先由外部控制UI展示 if (rowCell) { @@ -70,6 +70,8 @@ export class RowHeader extends BaseHeader { if (cell) { this.appendChild(cell); + spreadsheet.emit(S2Event.ROW_CELL_RENDER, cell); + spreadsheet.emit(S2Event.LAYOUT_CELL_RENDER, cell); } } }); diff --git a/packages/s2-core/src/facet/header/series-number.ts b/packages/s2-core/src/facet/header/series-number.ts index cf09e628e6..768ee4a14e 100644 --- a/packages/s2-core/src/facet/header/series-number.ts +++ b/packages/s2-core/src/facet/header/series-number.ts @@ -1,12 +1,12 @@ import { Rect } from '@antv/g'; import { each } from 'lodash'; import { SeriesNumberCell } from '../../cell/series-number-cell'; -import type { S2CellType } from '../../common/interface'; import type { SpreadSheet } from '../../sheet-type/index'; import type { PanelBBox } from '../bbox/panelBBox'; import type { Hierarchy } from '../layout/hierarchy'; import type { Node } from '../layout/node'; import { translateGroup } from '../utils'; +import { S2Event } from '../../common'; import { BaseHeader } from './base'; import type { BaseHeaderConfig } from './interface'; import { getSeriesNumberNodes } from './util'; @@ -16,7 +16,7 @@ export class SeriesNumberHeader extends BaseHeader { super(config); } - protected getCellInstance(node: Node): S2CellType { + protected getCellInstance(node: Node) { const headerConfig = this.getHeaderConfig(); const { spreadsheet } = headerConfig; const { seriesNumberCell } = spreadsheet.options; @@ -78,7 +78,12 @@ export class SeriesNumberHeader extends BaseHeader { } public layout() { - const { nodes, scrollY = 0, viewportHeight } = this.getHeaderConfig(); + const { + nodes, + scrollY = 0, + viewportHeight, + spreadsheet, + } = this.getHeaderConfig(); each(nodes, (node) => { const { y, height: cellHeight } = node; @@ -97,6 +102,8 @@ export class SeriesNumberHeader extends BaseHeader { node.belongsCell = cell; this.appendChild(cell); + spreadsheet.emit(S2Event.SERIES_NUMBER_CELL_RENDER, cell); + spreadsheet.emit(S2Event.LAYOUT_CELL_RENDER, cell); }); } diff --git a/packages/s2-core/src/facet/pivot-facet.ts b/packages/s2-core/src/facet/pivot-facet.ts index 39c128a9e6..cfe52c155a 100644 --- a/packages/s2-core/src/facet/pivot-facet.ts +++ b/packages/s2-core/src/facet/pivot-facet.ts @@ -27,7 +27,7 @@ import { import { EXTRA_FIELD, LayoutWidthTypes, VALUE_FIELD } from '../common/constant'; import { CellType } from '../common/constant/interaction'; import { DebuggerUtil } from '../common/debug'; -import type { LayoutResult } from '../common/interface'; +import type { LayoutResult, SimpleData } from '../common/interface'; import type { PivotDataSet } from '../data-set/pivot-data-set'; import type { SpreadSheet } from '../sheet-type'; import { safeJsonParse } from '../utils'; @@ -763,7 +763,7 @@ export class PivotFacet extends BaseFacet { for (let j = 0; j < cellDataKeys.length; j++) { const dataValue: MultiData = get(cellData, cellDataKeys[j]); - const valueSize = size(get(dataValue?.values, '0')); + const valueSize = size(get(dataValue?.values as SimpleData[][], '0')); if (valueSize > maxLength) { // greater length diff --git a/packages/s2-core/src/facet/table-facet.ts b/packages/s2-core/src/facet/table-facet.ts index 9d3ed42d4a..e71683d64c 100644 --- a/packages/s2-core/src/facet/table-facet.ts +++ b/packages/s2-core/src/facet/table-facet.ts @@ -946,7 +946,7 @@ export class TableFacet extends BaseFacet { }; }; - addCell = (cell: S2CellType) => { + addDataCell = (cell: S2CellType) => { const { rowCount: frozenRowCount = 0, colCount: frozenColCount = 0, diff --git a/packages/s2-core/src/group/panel-scroll-group.ts b/packages/s2-core/src/group/panel-scroll-group.ts index db203ead07..315355e3b7 100644 --- a/packages/s2-core/src/group/panel-scroll-group.ts +++ b/packages/s2-core/src/group/panel-scroll-group.ts @@ -2,6 +2,7 @@ import { Group } from '@antv/g'; import type { GridInfo } from '../common/interface'; import type { GridGroupConstructorParameters } from '../common/interface/group'; import { updateMergedCells } from '../utils/interaction/merge-cell'; +import { S2Event } from '../common'; import type { MergedCell } from './../cell/merged-cell'; import { KEY_GROUP_MERGED_CELLS } from './../common/constant/basic'; import { GridGroup } from './grid-group'; @@ -36,8 +37,10 @@ export class PanelScrollGroup extends GridGroup { this.mergedCellsGroup.toFront(); } - addMergeCell(mergeCell: MergedCell) { - this.mergedCellsGroup?.appendChild(mergeCell); + addMergeCell(mergedCell: MergedCell) { + this.mergedCellsGroup?.appendChild(mergedCell); + this.s2.emit(S2Event.MERGED_CELLS_RENDER, mergedCell); + this.s2.emit(S2Event.LAYOUT_CELL_RENDER, mergedCell); } update(gridInfo: GridInfo) { diff --git a/packages/s2-core/src/sheet-type/spread-sheet.ts b/packages/s2-core/src/sheet-type/spread-sheet.ts index 472d06f9db..6aba27d534 100644 --- a/packages/s2-core/src/sheet-type/spread-sheet.ts +++ b/packages/s2-core/src/sheet-type/spread-sheet.ts @@ -154,8 +154,6 @@ export abstract class SpreadSheet extends EE { preventRender?: boolean, ): void; - // public abstract handleGroupSort(event: CanvasEvent, meta: Node): void; - public abstract groupSortByMethod(sortMethod: SortMethod, meta: Node): void; public constructor( diff --git a/packages/s2-core/src/utils/cell/cell.ts b/packages/s2-core/src/utils/cell/cell.ts index cb50d3f633..171de172bb 100644 --- a/packages/s2-core/src/utils/cell/cell.ts +++ b/packages/s2-core/src/utils/cell/cell.ts @@ -129,7 +129,7 @@ export const getCellBoxByType = ( borderPositions: CellBorderPosition[], cellStyle: CellTheme, boxType: CellClipBox, -) => { +): SimpleBBox => { if (boxType === CellClipBox.BORDER_BOX) { return bbox; } diff --git a/packages/s2-core/src/utils/custom-render.ts b/packages/s2-core/src/utils/custom-render.ts deleted file mode 100644 index edfebc2c2b..0000000000 --- a/packages/s2-core/src/utils/custom-render.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - type S2CellType, - type RenderHandler, - CellClipBox, -} from '../common/interface'; - -/** - * @description 将外部以 g5.0 作为底层渲染引擎绘制的图形渲染进已挂载的单元格中。 - */ - -export const renderToMountedCell = ( - cell: S2CellType, - render: RenderHandler, - renderOptions?: Record, -) => { - const { fieldValue } = cell.getMeta(); - const { x, y, width, height } = cell.getBBoxByType(CellClipBox.CONTENT_BOX); - - render( - { - x, - y, - width, - height, - ...fieldValue, - ...renderOptions, - }, - { group: cell }, - ); -}; diff --git a/packages/s2-core/src/utils/export/copy/pivot-data-cell-copy.ts b/packages/s2-core/src/utils/export/copy/pivot-data-cell-copy.ts index 8e5bcafe3c..1df16a9f85 100644 --- a/packages/s2-core/src/utils/export/copy/pivot-data-cell-copy.ts +++ b/packages/s2-core/src/utils/export/copy/pivot-data-cell-copy.ts @@ -1,16 +1,18 @@ -import { find, isEmpty, map, slice, zip } from 'lodash'; +import { find, isEmpty, isPlainObject, map, slice, zip } from 'lodash'; import { AsyncRenderThreshold, - type CellMeta, - type DataItem, EXTRA_FIELD, VALUE_FIELD, + type CellMeta, + type DataItem, + type MiniChartData, + type MultiData, } from '../../../common'; import type { Node } from '../../../facet/layout/node'; import type { SpreadSheet } from '../../../sheet-type'; import type { - CopyableList, CopyAllDataParams, + CopyableList, MeasureQuery, SheetCopyConstructorParams, } from '../interface'; @@ -22,6 +24,7 @@ import { getSelectedRows, } from '../method'; import type { BaseDataSet } from './../../../data-set/base-data-set'; +import { BaseDataCellCopy } from './base-data-cell-copy'; import { assembleMatrix, completeMatrix, @@ -30,7 +33,6 @@ import { getNodeFormatData, } from './common'; import { getHeaderNodeFromMeta } from './core'; -import { BaseDataCellCopy } from './base-data-cell-copy'; export class PivotDataCellCopy extends BaseDataCellCopy { protected leafRowNodes: Node[] = []; @@ -229,7 +231,13 @@ export class PivotDataCellCopy extends BaseDataCellCopy { dataSet, ); - return formatter(cellData?.[VALUE_FIELD] ?? ''); + const fieldValue = cellData?.[VALUE_FIELD]; + const isChartData = isPlainObject( + (fieldValue as MultiData)?.values, + ); + const value = isChartData ? '' : fieldValue; + + return formatter(value ?? ''); }; protected getCornerMatrix = (rowMatrix?: string[][]): string[][] => { diff --git a/packages/s2-core/src/utils/index.ts b/packages/s2-core/src/utils/index.ts index 9bb50659e0..def125c3d1 100644 --- a/packages/s2-core/src/utils/index.ts +++ b/packages/s2-core/src/utils/index.ts @@ -9,7 +9,6 @@ export * from './export/index'; export * from './export/copy'; export * from './interaction'; export * from './g-renders'; -export * from './custom-render'; export * from './g-mini-charts'; export * from './merge'; export * from './is-mobile'; diff --git a/packages/s2-core/src/utils/tooltip.ts b/packages/s2-core/src/utils/tooltip.ts index 473bbdb19d..f457d8c998 100644 --- a/packages/s2-core/src/utils/tooltip.ts +++ b/packages/s2-core/src/utils/tooltip.ts @@ -211,12 +211,12 @@ export const getListItem = ( const formatter = getFieldFormatter(spreadsheet, field); - // 暂时对 object 类型 data 不作处理,上层通过自定义 tooltip 的方式去自行定制 - let dataValue = getFieldValueOfViewMetaData(data, field); + // 非数值类型的 data 不展示 (趋势分析表/迷你图/G2 图表),上层通过自定义 tooltip 的方式去自行定制 + const dataValue = getFieldValueOfViewMetaData(data, field); + const displayDataValue = isObject(dataValue) ? null : dataValue; - dataValue = isObject(dataValue) ? JSON.stringify(dataValue) : dataValue; const value = formatter( - valueField || dataValue, + valueField || displayDataValue, useCompleteDataForFormatter ? data : undefined, ); @@ -704,9 +704,16 @@ export const getTooltipOptions = ( return null; } + const { options, interaction } = spreadsheet; const cellType = spreadsheet.getCellType?.(event?.target); - return getTooltipOptionsByCellType(spreadsheet.options.tooltip!, cellType!); + // 如果没有 cellType, 说明是刷选丢失 event target 的场景, 此时从产生过交互状态的单元格里取, 避免刷选读取不到争取 tooltip 配置的问题 + const sampleCell = interaction.getInteractedCells()[0]; + + return getTooltipOptionsByCellType( + options.tooltip!, + cellType || sampleCell?.cellType!, + ); }; export const getTooltipVisibleOperator = ( diff --git a/packages/s2-react/__tests__/data/data-g2-chart.ts b/packages/s2-react/__tests__/data/data-g2-chart.ts new file mode 100644 index 0000000000..0cdb61d75a --- /dev/null +++ b/packages/s2-react/__tests__/data/data-g2-chart.ts @@ -0,0 +1,889 @@ +import type { S2DataConfig } from '@antv/s2'; + +export const ChartDataConfig: S2DataConfig = { + fields: { + rows: ['province', 'city'], + columns: ['type', 'sub_type'], + values: ['number'], + valueInCols: true, + }, + meta: [ + { + field: 'number', + name: '数量', + description: '数量说明。。', + }, + { + field: 'province', + name: '省份', + description: '省份说明。。', + }, + { + field: 'city', + name: '城市', + description: '城市说明。。', + }, + { + field: 'type', + name: '类别', + description: '类别说明。。', + }, + { + field: 'sub_type', + name: '子类别', + description: '子类别说明。。', + }, + { + field: 'area', + name: '地区', + description: '地区说明。。', + }, + { + field: 'money', + name: '金额', + description: '金额说明。。', + }, + ], + data: [ + { + number: { + values: { + // 面积图 + type: 'area', + autoFit: true, + data: { + type: 'fetch', + value: 'https://assets.antv.antgroup.com/g2/aapl.json', + }, + encode: { + x: (d: Record) => new Date(d['date']), + y: 'close', + }, + }, + }, + province: '浙江省', + city: '杭州市', + type: '家具', + sub_type: '桌子', + }, + { + number: { + // 玉玦图 + values: { + type: 'interval', + autoFit: true, + data: [ + { question: '问题 1', percent: 0.21 }, + { question: '问题 2', percent: 0.4 }, + { question: '问题 3', percent: 0.49 }, + { question: '问题 4', percent: 0.52 }, + { question: '问题 5', percent: 0.53 }, + { question: '问题 6', percent: 0.84 }, + { question: '问题 7', percent: 1 }, + { question: '问题 8', percent: 1.2 }, + ], + encode: { x: 'question', y: 'percent', color: 'percent' }, + scale: { color: { range: '#BAE7FF-#1890FF-#0050B3' } }, + coordinate: { + type: 'radial', + innerRadius: 0.1, + endAngle: 3.141592653589793, + }, + style: { stroke: 'white' }, + animate: { enter: { type: 'waveIn', duration: 800 } }, + // animate: false, + legend: { + color: { + length: 400, + position: 'bottom', + layout: { justifyContent: 'center' }, + }, + }, + }, + }, + province: '浙江省', + city: '绍兴市', + type: '家具', + sub_type: '桌子', + }, + { + number: { + // 柱形图 + values: { + type: 'interval', + autoFit: true, + data: [ + { genre: 'Sports', sold: 275 }, + { genre: 'Strategy', sold: 115 }, + { genre: 'Action', sold: 120 }, + { genre: 'Shooter', sold: 350 }, + { genre: 'Other', sold: 150 }, + ], + scale: { + color: { + guide: { + position: 'right', + size: 80, + }, + }, + }, + encode: { + x: 'genre', + y: 'sold', + color: 'genre', + }, + }, + }, + province: '浙江省', + city: '宁波市', + type: '家具', + sub_type: '桌子', + }, + { + number: { + // 仪表盘 + values: { + type: 'gauge', + autoFit: true, + data: { + value: { + target: 159, + total: 400, + name: 'score', + thresholds: [100, 200, 400], + }, + }, + scale: { color: { range: ['#F4664A', '#FAAD14', 'green'] } }, + style: { + textContent: (target: number, total: number) => `得分:${target}\ + 占比:${(target / total) * 100}%`, + }, + legend: false, + }, + }, + province: '浙江省', + city: '舟山市', + type: '家具', + sub_type: '桌子', + }, + { + number: { + // 雷达图 + values: { + type: 'view', + autoFit: true, + data: [ + { item: 'Design', type: 'a', score: 70 }, + { item: 'Design', type: 'b', score: 30 }, + { item: 'Development', type: 'a', score: 60 }, + { item: 'Development', type: 'b', score: 70 }, + { item: 'Marketing', type: 'a', score: 50 }, + { item: 'Marketing', type: 'b', score: 60 }, + { item: 'Users', type: 'a', score: 40 }, + { item: 'Users', type: 'b', score: 50 }, + { item: 'Test', type: 'a', score: 60 }, + { item: 'Test', type: 'b', score: 70 }, + { item: 'Language', type: 'a', score: 70 }, + { item: 'Language', type: 'b', score: 50 }, + { item: 'Technology', type: 'a', score: 50 }, + { item: 'Technology', type: 'b', score: 40 }, + { item: 'Support', type: 'a', score: 30 }, + { item: 'Support', type: 'b', score: 40 }, + { item: 'Sales', type: 'a', score: 60 }, + { item: 'Sales', type: 'b', score: 40 }, + { item: 'UX', type: 'a', score: 50 }, + { item: 'UX', type: 'b', score: 60 }, + ], + scale: { x: { padding: 0.5, align: 0 }, y: { tickCount: 5 } }, + coordinate: { type: 'polar' }, + axis: { x: { grid: true }, y: { zIndex: 1, title: false } }, + interaction: { tooltip: { crosshairsLineDash: [4, 4] } }, + children: [ + { + type: 'area', + encode: { x: 'item', y: 'score', color: 'type', shape: 'smooth' }, + scale: { y: { domainMax: 80 } }, + style: { fillOpacity: 0.5 }, + }, + { + type: 'line', + encode: { x: 'item', y: 'score', color: 'type', shape: 'smooth' }, + style: { lineWidth: 2 }, + }, + ], + }, + }, + province: '浙江省', + city: '杭州市', + type: '家具', + sub_type: '沙发', + }, + { + number: { + // 面积图 + values: { + type: 'box', + autoFit: true, + data: [ + { x: 'Oceania', y: [1, 9, 16, 22, 24] }, + { x: 'East Europe', y: [1, 5, 8, 12, 16] }, + { x: 'Australia', y: [1, 8, 12, 19, 26] }, + { x: 'South America', y: [2, 8, 12, 21, 28] }, + { x: 'North Africa', y: [1, 8, 14, 18, 24] }, + { x: 'North America', y: [3, 10, 17, 28, 30] }, + { x: 'West Europe', y: [1, 7, 10, 17, 22] }, + { x: 'West Africa', y: [1, 6, 8, 13, 16] }, + ], + encode: { x: 'x', y: 'y', color: 'x' }, + scale: { + x: { paddingInner: 0.6, paddingOuter: 0.3 }, + y: { zero: true }, + }, + style: { stroke: 'black' }, + legend: false, + tooltip: { + items: [ + { name: 'min', channel: 'y' }, + { name: 'q1', channel: 'y1' }, + { name: 'q2', channel: 'y2' }, + { name: 'q3', channel: 'y3' }, + { name: 'max', channel: 'y4' }, + ], + }, + }, + }, + province: '浙江省', + city: '绍兴市', + type: '家具', + sub_type: '沙发', + }, + { + number: { + // 饼图 + values: { + type: 'view', + autoFit: true, + coordinate: { type: 'theta', innerRadius: 0.6 }, + children: [ + { + type: 'interval', + data: { + type: 'fetch', + value: + 'https://gw.alipayobjects.com/os/bmw-prod/79fd9317-d2af-4bc4-90fa-9d07357398fd.csv', + }, + encode: { y: 'value', color: 'name' }, + transform: [{ type: 'stackY' }], + scale: { + color: { + palette: 'spectral', + offset: (t: number) => t * 0.8 + 0.1, + }, + }, + legend: false, + }, + { + type: 'text', + style: { + text: 'Donut', + x: '50%', + y: '50%', + fontSize: 40, + fontWeight: 'bold', + textAlign: 'center', + }, + }, + { + type: 'text', + style: { + text: 'chart', + x: 304, + y: 360, + fontSize: 20, + fontWeight: 'bold', + textAlign: 'center', + }, + }, + ], + }, + }, + province: '浙江省', + city: '宁波市', + type: '家具', + sub_type: '沙发', + }, + { + number: { + // 折线图 + values: { + type: 'line', + autoFit: true, + data: { + type: 'fetch', + value: 'https://assets.antv.antgroup.com/g2/indices.json', + }, + encode: { + x: (d: Record) => new Date(d['Date']), + y: 'Close', + color: 'Symbol', + }, + transform: [{ type: 'normalizeY', basis: 'first', groupBy: 'color' }], + scale: { y: { type: 'log' } }, + axis: { y: { title: '↑ Change in price (%)' } }, + labels: [{ text: 'Symbol', selector: 'last', fontSize: 10 }], + tooltip: { items: [{ channel: 'y', valueFormatter: '.1f' }] }, + }, + }, + province: '浙江省', + city: '舟山市', + type: '家具', + sub_type: '沙发', + }, + { + number: { + // 柱形图 + values: { + type: 'interval', + autoFit: true, + data: [ + { genre: 'Sports', sold: 275 }, + { genre: 'Strategy', sold: 115 }, + { genre: 'Action', sold: 120 }, + { genre: 'Shooter', sold: 350 }, + { genre: 'Other', sold: 150 }, + ], + scale: { + color: { + guide: { + position: 'right', + size: 80, + }, + }, + }, + encode: { + x: 'genre', + y: 'sold', + color: 'genre', + }, + }, + }, + province: '浙江省', + city: '杭州市', + type: '办公用品', + sub_type: '笔', + }, + { + number: { + // 漏斗图 + values: { + type: 'interval', + autoFit: true, + data: [ + { action: '浏览网站', pv: 50000 }, + { action: '放入购物车', pv: 35000 }, + { action: '生成订单', pv: 25000 }, + { action: '支付订单', pv: 15000 }, + { action: '完成交易', pv: 8000 }, + ], + encode: { x: 'action', y: 'pv', color: 'action', shape: 'funnel' }, + transform: [{ type: 'symmetryY' }], + scale: { x: { padding: 0 } }, + coordinate: { transform: [{ type: 'transpose' }] }, + animate: { enter: { type: 'fadeIn' } }, + axis: false, + labels: [ + { + text: (d: Record) => `${d['action']}\ + ${d['pv']}`, + position: 'inside', + transform: [{ type: 'contrastReverse' }], + }, + ], + }, + }, + province: '浙江省', + city: '绍兴市', + type: '办公用品', + sub_type: '笔', + }, + { + number: { + // 出现顺序堆叠面积图 + values: { + type: 'view', + autoFit: true, + data: { + type: 'fetch', + value: + 'https://gw.alipayobjects.com/os/bmw-prod/f38a8ad0-6e1f-4bb3-894c-7db50781fdec.json', + }, + interaction: { + tooltip: { + filter: (d: Record) => + parseInt(d['value'], 10) > 0, + }, + }, + children: [ + { + type: 'area', + encode: { + x: (d: Record) => new Date(d['year']), + y: 'revenue', + series: 'format', + color: 'group', + shape: 'smooth', + }, + transform: [ + { type: 'stackY', orderBy: 'maxIndex', reverse: true }, + ], + axis: { y: { labelFormatter: '~s' } }, + tooltip: { items: [{ channel: 'y', valueFormatter: '.2f' }] }, + }, + { + type: 'line', + encode: { + x: (d: Record) => new Date(d['year']), + y: 'revenue', + series: 'format', + shape: 'smooth', + color: 'group', + }, + transform: [ + { type: 'stackY', orderBy: 'maxIndex', reverse: true, y: 'y1' }, + ], + style: { stroke: 'white' }, + tooltip: false, + }, + ], + }, + }, + province: '浙江省', + city: '宁波市', + type: '办公用品', + sub_type: '笔', + }, + { + number: { + // 密度热力图 + values: { + type: 'view', + autoFit: true, + padding: 0, + axis: false, + children: [ + { + type: 'image', + style: { + src: 'https://gw.alipayobjects.com/zos/rmsportal/NeUTMwKtPcPxIFNTWZOZ.png', + x: '50%', + y: '50%', + width: '100%', + height: '100%', + }, + tooltip: false, + }, + { + type: 'heatmap', + data: { + type: 'fetch', + value: 'https://assets.antv.antgroup.com/g2/heatmap.json', + }, + encode: { x: 'g', y: 'l', color: 'tmp' }, + style: { opacity: 0 }, + tooltip: false, + }, + ], + }, + }, + province: '浙江省', + city: '舟山市', + type: '办公用品', + sub_type: '笔', + }, + { + number: 1343, + province: '浙江省', + city: '杭州市', + type: '办公用品', + sub_type: '纸张', + }, + { + number: 1354, + province: '浙江省', + city: '绍兴市', + type: '办公用品', + sub_type: '纸张', + }, + { + number: 1523, + province: '浙江省', + city: '宁波市', + type: '办公用品', + sub_type: '纸张', + }, + { + number: 1634, + province: '浙江省', + city: '舟山市', + type: '办公用品', + sub_type: '纸张', + }, + { + number: 1723, + province: '四川省', + city: '成都市', + type: '家具', + sub_type: '桌子', + }, + { + number: 1822, + province: '四川省', + city: '绵阳市', + type: '家具', + sub_type: '桌子', + }, + { + number: 1943, + province: '四川省', + city: '南充市', + type: '家具', + sub_type: '桌子', + }, + { + number: 2330, + province: '四川省', + city: '乐山市', + type: '家具', + sub_type: '桌子', + }, + { + number: 2451, + province: '四川省', + city: '成都市', + type: '家具', + sub_type: '沙发', + }, + { + number: 2244, + province: '四川省', + city: '绵阳市', + type: '家具', + sub_type: '沙发', + }, + { + number: 2333, + province: '四川省', + city: '南充市', + type: '家具', + sub_type: '沙发', + }, + { + number: 2445, + province: '四川省', + city: '乐山市', + type: '家具', + sub_type: '沙发', + }, + { + number: 2335, + province: '四川省', + city: '成都市', + type: '办公用品', + sub_type: '笔', + }, + { + number: 245, + province: '四川省', + city: '绵阳市', + type: '办公用品', + sub_type: '笔', + }, + { + number: 2457, + province: '四川省', + city: '南充市', + type: '办公用品', + sub_type: '笔', + }, + { + number: 2458, + province: '四川省', + city: '乐山市', + type: '办公用品', + sub_type: '笔', + }, + { + number: 4004, + province: '四川省', + city: '成都市', + type: '办公用品', + sub_type: '纸张', + }, + { + number: 3077, + province: '四川省', + city: '绵阳市', + type: '办公用品', + sub_type: '纸张', + }, + { + number: 3551, + province: '四川省', + city: '南充市', + type: '办公用品', + sub_type: '纸张', + }, + { + number: 352, + province: '四川省', + city: '乐山市', + type: '办公用品', + sub_type: '纸张', + }, + ], + totalData: [ + { + number: 26193, + type: '家具', + sub_type: '桌子', + }, + { + number: 49709, + type: '家具', + }, + { + number: 23516, + type: '家具', + sub_type: '沙发', + }, + { + number: 29159, + type: '办公用品', + }, + { + number: 12321, + type: '办公用品', + sub_type: '笔', + }, + { + number: 16838, + type: '办公用品', + sub_type: '纸张', + }, + { + number: 18375, + province: '浙江省', + type: '家具', + sub_type: '桌子', + }, + { + number: 14043, + province: '浙江省', + type: '家具', + sub_type: '沙发', + }, + { + number: 4826, + province: '浙江省', + type: '办公用品', + sub_type: '笔', + }, + { + number: 5854, + province: '浙江省', + type: '办公用品', + sub_type: '纸张', + }, + { + number: 7818, + province: '四川省', + type: '家具', + sub_type: '桌子', + }, + { + number: 9473, + province: '四川省', + type: '家具', + sub_type: '沙发', + }, + { + number: 7495, + province: '四川省', + type: '办公用品', + sub_type: '笔', + }, + { + number: 10984, + province: '四川省', + type: '办公用品', + sub_type: '纸张', + }, + { + number: 13132, + province: '浙江省', + city: '杭州市', + type: '家具', + }, + { + number: 2288, + province: '浙江省', + city: '杭州市', + type: '办公用品', + }, + { + number: 15420, + province: '浙江省', + city: '杭州市', + }, + { + number: 2999, + province: '浙江省', + city: '绍兴市', + type: '家具', + }, + { + number: 2658, + province: '浙江省', + city: '绍兴市', + type: '办公用品', + }, + { + number: 5657, + province: '浙江省', + city: '绍兴市', + }, + { + number: 11111, + province: '浙江省', + city: '宁波市', + type: '家具', + }, + { + number: 2668, + province: '浙江省', + city: '宁波市', + type: '办公用品', + }, + { + number: 13779, + province: '浙江省', + city: '宁波市', + }, + { + number: 5176, + province: '浙江省', + city: '舟山市', + type: '家具', + }, + { + number: 3066, + province: '浙江省', + city: '舟山市', + type: '办公用品', + }, + { + number: 8242, + province: '浙江省', + city: '舟山市', + }, + { + number: 4174, + province: '四川省', + city: '成都市', + type: '家具', + }, + { + number: 6339, + province: '四川省', + city: '成都市', + type: '办公用品', + }, + { + number: 10513, + province: '四川省', + city: '成都市', + }, + { + number: 4066, + province: '四川省', + city: '绵阳市', + type: '家具', + }, + { + number: 3322, + province: '四川省', + city: '绵阳市', + type: '办公用品', + }, + { + number: 7388, + province: '四川省', + city: '绵阳市', + }, + { + number: 4276, + province: '四川省', + city: '南充市', + type: '家具', + }, + { + number: 6008, + province: '四川省', + city: '南充市', + type: '办公用品', + }, + { + number: 10284, + province: '四川省', + city: '南充市', + }, + { + number: 4775, + province: '四川省', + city: '乐山市', + type: '家具', + }, + { + number: 2810, + province: '四川省', + city: '乐山市', + type: '办公用品', + }, + { + number: 7585, + province: '四川省', + city: '乐山市', + }, + { + number: 32418, + province: '浙江省', + type: '家具', + }, + { + number: 10680, + province: '浙江省', + type: '办公用品', + }, + { + number: 43098, + province: '浙江省', + }, + { + number: 17291, + province: '四川省', + type: '家具', + }, + { + number: 18479, + province: '四川省', + type: '办公用品', + }, + { + number: 35770, + province: '四川省', + }, + { + number: 78868, + }, + ], +}; diff --git a/packages/s2-react/__tests__/spreadsheet/spread-sheet-spec.tsx b/packages/s2-react/__tests__/spreadsheet/spread-sheet-spec.tsx index ca339c07fc..dac18da612 100644 --- a/packages/s2-react/__tests__/spreadsheet/spread-sheet-spec.tsx +++ b/packages/s2-react/__tests__/spreadsheet/spread-sheet-spec.tsx @@ -60,6 +60,11 @@ describe('Spread Sheet Tests', () => { test('should only mount container once in strict mode for React 18', async () => { // eslint-disable-next-line no-console console.table(process.env); + + const containerId = 'mounted-react-18'; + + container.id = containerId; + const onMounted = jest.fn(); renderComponent( @@ -76,7 +81,11 @@ describe('Spread Sheet Tests', () => { await waitFor(() => { expect( - Array.from(document.querySelectorAll('.antv-s2-container canvas')), + Array.from( + document + .getElementById(containerId)! + .querySelectorAll('.antv-s2-container canvas'), + ), ).toHaveLength(1); expect(onMounted).toHaveBeenCalledTimes(1); }); diff --git a/packages/s2-react/__tests__/unit/components/advanced-sort/__snapshots__/index-spec.tsx.snap b/packages/s2-react/__tests__/unit/components/advanced-sort/__snapshots__/index-spec.tsx.snap index 1a242c3932..414d0f7286 100644 --- a/packages/s2-react/__tests__/unit/components/advanced-sort/__snapshots__/index-spec.tsx.snap +++ b/packages/s2-react/__tests__/unit/components/advanced-sort/__snapshots__/index-spec.tsx.snap @@ -6,7 +6,7 @@ exports[`AdvancedSort Component Tests should render component 1`] = ` class="antv-s2-advanced-sort test" >