diff --git a/packages/s2-core/README.md b/packages/s2-core/README.md index 3f32e3aa91..aae2db40f2 100644 --- a/packages/s2-core/README.md +++ b/packages/s2-core/README.md @@ -142,7 +142,7 @@ const s2DataConfig = { ```ts const s2Options = { width: 600, - height: 600, + height: 600 } ``` @@ -214,9 +214,6 @@ yarn site:start - - -
## 👬 Contributors diff --git a/packages/s2-core/__tests__/spreadsheet/empty-dataset-spec.ts b/packages/s2-core/__tests__/spreadsheet/empty-dataset-spec.ts new file mode 100644 index 0000000000..b8229ca944 --- /dev/null +++ b/packages/s2-core/__tests__/spreadsheet/empty-dataset-spec.ts @@ -0,0 +1,64 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { getContainer } from 'tests/util/helpers'; +import { PivotSheet, TableSheet } from '@/sheet-type'; +import type { S2Options } from '@/common/interface/s2Options'; + +const s2Options: S2Options = { + width: 400, + height: 400, + hierarchyType: 'grid', +}; + +describe('Empty Dataset Structure Tests', () => { + test('should generate placeholder for pivot mode with single dimension', () => { + const container = getContainer(); + + const s2DataCfg = { + fields: { + rows: ['province', 'city'], + columns: ['type'], + values: ['price'], + valueInCols: true, + }, + data: [], + }; + const s2 = new PivotSheet(container, s2DataCfg, s2Options); + s2.render(); + + // @ts-ignore + expect(s2.facet.panelScrollGroupIndexes).toEqual([0, 0, 0, 0]); + }); + + test('should generate placeholder for pivot mode with two dimensions', () => { + const container = getContainer(); + + const s2DataCfg = { + fields: { + rows: ['province', 'city'], + columns: ['type'], + values: ['price', 'cost'], + valueInCols: true, + }, + data: [], + }; + const s2 = new PivotSheet(container, s2DataCfg, s2Options); + s2.render(); + // @ts-ignore + expect(s2.facet.panelScrollGroupIndexes).toEqual([0, 1, 0, 0]); + }); + + test(`shouldn't generate placeholder for table mode`, () => { + const container = getContainer(); + + const s2DataCfg = { + fields: { + columns: ['province', 'city', 'type', 'price', 'cost'], + }, + data: [], + }; + const s2 = new TableSheet(container, s2DataCfg, s2Options); + s2.render(); + // @ts-ignore + expect(s2.facet.panelScrollGroupIndexes).toEqual([]); + }); +}); diff --git a/packages/s2-core/__tests__/unit/cell/row-cell-spec.ts b/packages/s2-core/__tests__/unit/cell/row-cell-spec.ts index 1dca138ff3..9b4c9f3822 100644 --- a/packages/s2-core/__tests__/unit/cell/row-cell-spec.ts +++ b/packages/s2-core/__tests__/unit/cell/row-cell-spec.ts @@ -1,7 +1,7 @@ import { get } from 'lodash'; import { createPivotSheet } from 'tests/util/helpers'; -import type { RowCell } from '@antv/s2'; import type { Group } from '@antv/g-canvas'; +import type { RowCell } from '../../../src/cell'; import type { SpreadSheet } from '@/sheet-type'; import type { TextAlign } from '@/common'; diff --git a/packages/s2-core/__tests__/unit/facet/header/frozen-row-spec.ts b/packages/s2-core/__tests__/unit/facet/header/frozen-row-spec.ts index 5297b77ad9..da3537e447 100644 --- a/packages/s2-core/__tests__/unit/facet/header/frozen-row-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/header/frozen-row-spec.ts @@ -1,14 +1,15 @@ import { createPivotSheet } from 'tests/util/helpers'; import { get } from 'lodash'; +import { RowCell } from '../../../../src/cell/row-cell'; import { DEFAULT_OPTIONS } from '@/common'; -import { FrozenRowCell, SeriesNumberCell } from '@/cell'; +import { SeriesNumberCell } from '@/cell'; import { PivotRowHeader } from '@/facet/header'; import { SeriesNumberHeader } from '@/facet/header/series-number'; const s2 = createPivotSheet( { ...DEFAULT_OPTIONS, - frozenFirstRowPivot: true, + frozenRowHeader: true, totals: { row: { showGrandTotals: true, reverseLayout: true } }, showSeriesNumber: true, }, @@ -30,13 +31,13 @@ describe('Frozen Row Header Test', () => { expect(rowHeader.frozenHeadGroup.getChildren()).toHaveLength(1); const frozenRowCell = rowHeader.frozenHeadGroup.getChildren()[0]; - expect(frozenRowCell instanceof FrozenRowCell).toBeTrue(); + expect(frozenRowCell instanceof RowCell).toBeTrue(); expect(get(frozenRowCell, 'meta.height')).toEqual(30); expect(rowHeader.scrollGroup.getChildren()).toHaveLength(10); const scrollCell = rowHeader.scrollGroup.getChildren()[0]; - expect(scrollCell instanceof FrozenRowCell).toBeTrue(); + expect(scrollCell instanceof RowCell).toBeTrue(); expect(get(frozenRowCell, 'meta.height')).toEqual(30); expect(rowHeader.getFrozenFirstRowHeight()).toBe(30); diff --git a/packages/s2-core/__tests__/unit/facet/pivot-facet-frozen-spec.ts b/packages/s2-core/__tests__/unit/facet/pivot-facet-frozen-spec.ts index 23e8fdf5e9..91d6431543 100644 --- a/packages/s2-core/__tests__/unit/facet/pivot-facet-frozen-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/pivot-facet-frozen-spec.ts @@ -1,22 +1,21 @@ /** * pivot mode pivot test. */ -import { createPivotSheet } from 'tests/util/helpers'; import type { IGroup } from '@antv/g-canvas'; import { get } from 'lodash'; - -import type { PivotSheet, SpreadSheet } from '@antv/s2'; -import { FrozenRowCell, SeriesNumberCell } from '@/cell'; -import { getFrozenRowCfgPivot } from '@/facet/utils'; +import { createPivotSheet } from 'tests/util/helpers'; +import { type PivotSheet, RowCell, SeriesNumberCell } from '../../../src'; import { FrozenGroup, KEY_GROUP_ROW_HEADER_FROZEN, KEY_GROUP_ROW_SCROLL, + type S2Options, } from '@/common'; import type { FrozenFacet } from '@/facet/frozen-facet'; +import { getFrozenRowCfgPivot } from '@/facet/utils'; -const defaultOptions = { - frozenFirstRowPivot: true, +const defaultS2Options: S2Options = { + frozenFirstRow: true, totals: { row: { showGrandTotals: true, @@ -24,6 +23,7 @@ const defaultOptions = { }, }, }; + const enableFrozenFistRowOption = { frozenRowCount: 1, frozenColCount: 0, @@ -32,6 +32,7 @@ const enableFrozenFistRowOption = { enableFrozenFirstRow: true, frozenRowHeight: 30, }; + const disableFrozenFistRowOption = { ...enableFrozenFistRowOption, frozenRowCount: 0, @@ -43,7 +44,7 @@ let s2: PivotSheet; describe('test getFrozenRowCfgPivot', () => { beforeEach(() => { - s2 = createPivotSheet(defaultOptions, { useSimpleData: false }); + s2 = createPivotSheet(defaultS2Options, { useSimpleData: false }); }); afterEach(() => { @@ -76,7 +77,7 @@ describe('test getFrozenRowCfgPivot in tree', () => { beforeEach(() => { s2 = createPivotSheet( { - ...defaultOptions, + ...defaultS2Options, hierarchyType: 'tree', pagination: { pageSize: 0, @@ -106,7 +107,7 @@ describe('test getFrozenRowCfgPivot in tree', () => { test('showSeriesNumber has totals', () => { s2.setOptions({ showSeriesNumber: true, - ...defaultOptions, + ...defaultS2Options, }); s2.render(); @@ -118,7 +119,7 @@ describe('test getFrozenRowCfgPivot in tree', () => { describe('test cell XYIndexes frozen first row', () => { beforeEach(() => { - s2 = createPivotSheet(defaultOptions, { useSimpleData: false }); + s2 = createPivotSheet(defaultS2Options, { useSimpleData: false }); s2.render(); }); @@ -338,7 +339,7 @@ describe('test frozen group', () => { beforeEach(() => { s2 = createPivotSheet( { - ...defaultOptions, + ...defaultS2Options, showSeriesNumber: true, }, { useSimpleData: false }, @@ -369,12 +370,10 @@ describe('test frozen group', () => { scrollHeaderGroup as IGroup ).getChildren(); expect(frozenRowGroupChildren).toHaveLength(1); - expect(frozenRowGroupChildren[0] instanceof FrozenRowCell).toBe(true); + expect(frozenRowGroupChildren[0] instanceof RowCell).toBeTruthy(); expect(get(frozenRowGroupChildren[0], 'meta.value')).toBe('总计'); expect(scrollRowHeaderGroupChildren).toHaveLength(10); - expect(scrollRowHeaderGroupChildren[0] instanceof FrozenRowCell).toBe( - true, - ); + expect(scrollRowHeaderGroupChildren[0] instanceof RowCell).toBeTruthy(); expect(get(scrollRowHeaderGroupChildren[0], 'meta.value')).toBe('浙江省'); // serial number header 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 65827b8434..22c452bef1 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 @@ -4,8 +4,7 @@ import { sleep, } from 'tests/util/helpers'; import type { Event as GEvent } from '@antv/g-canvas'; -import type { InteractionCellHighlight } from '@antv/s2'; -import type { S2Options } from '@/common/interface'; +import type { InteractionCellHighlight, S2Options } from '@/common/interface'; import type { SpreadSheet } from '@/sheet-type'; import { HOVER_FOCUS_DURATION, @@ -49,6 +48,7 @@ describe('Interaction Data Cell Click Tests', () => { s2.emit(S2Event.DATA_CELL_CLICK, { stopPropagation() {}, } as unknown as GEvent); + expect(s2.interaction.getState()).toEqual({ cells: [mockCellInfo.mockCellMeta], stateName: InteractionStateName.SELECTED, @@ -82,9 +82,29 @@ describe('Interaction Data Cell Click Tests', () => { s2.emit(S2Event.DATA_CELL_CLICK, { stopPropagation() {}, } as unknown as GEvent); + expect(selected).toHaveBeenCalledWith([mockCellInfo.mockCell]); }); + // https://github.com/antvis/S2/issues/2447 + test('should emit cell selected event when cell unselected', () => { + jest + .spyOn(s2.interaction, 'isSelectedCell') + .mockImplementationOnce(() => true); + + const selected = jest.fn(); + s2.on(S2Event.GLOBAL_SELECTED, selected); + + s2.emit(S2Event.DATA_CELL_CLICK, { + stopPropagation() {}, + originalEvent: { + detail: 1, + }, + } as unknown as GEvent); + + expect(selected).toHaveBeenCalledWith([]); + }); + test('should emit link field jump event when link field text click and not show tooltip', () => { const linkFieldJump = jest.fn(); diff --git a/packages/s2-core/__tests__/unit/interaction/event-controller-spec.ts b/packages/s2-core/__tests__/unit/interaction/event-controller-spec.ts index 0659ebe1ee..31b382b261 100644 --- a/packages/s2-core/__tests__/unit/interaction/event-controller-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/event-controller-spec.ts @@ -506,8 +506,11 @@ describe('Interaction Event Controller Tests', () => { maxY: 100, } as BBox, } as BaseFacet; + + const selected = jest.fn(); const reset = jest.fn(); spreadsheet.on(S2Event.GLOBAL_RESET, reset); + spreadsheet.on(S2Event.GLOBAL_SELECTED, selected); window.dispatchEvent( new MouseEvent('click', { @@ -516,6 +519,7 @@ describe('Interaction Event Controller Tests', () => { } as MouseEventInit), ); + expect(selected).toHaveBeenCalledWith([]); expect(reset).toHaveBeenCalled(); expect(spreadsheet.interaction.reset).toHaveBeenCalled(); }); @@ -527,13 +531,17 @@ describe('Interaction Event Controller Tests', () => { maxY: 100, } as BBox, } as BaseFacet; + + const selected = jest.fn(); const reset = jest.fn(); spreadsheet.on(S2Event.GLOBAL_RESET, reset); + spreadsheet.on(S2Event.GLOBAL_SELECTED, selected); window.dispatchEvent( new KeyboardEvent('keydown', { key: InteractionKeyboardKey.ESC }), ); + expect(selected).toHaveBeenCalledWith([]); expect(reset).toHaveBeenCalled(); expect(spreadsheet.interaction.reset).toHaveBeenCalled(); }); diff --git a/packages/s2-core/__tests__/unit/interaction/root-spec.ts b/packages/s2-core/__tests__/unit/interaction/root-spec.ts index 047935b86d..8869724d41 100644 --- a/packages/s2-core/__tests__/unit/interaction/root-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/root-spec.ts @@ -26,6 +26,7 @@ import { DataCellBrushSelection, ColBrushSelection, RowBrushSelection, + S2Event, } from '@/index'; import { RootInteraction } from '@/interaction/root'; import { mergeCell, unmergeCell } from '@/utils/interaction/merge-cell'; @@ -170,9 +171,9 @@ describe('RootInteraction Tests', () => { // https://github.com/antvis/S2/issues/1243 test('should multi selected header cells', () => { - const isEqualStateNameSpy = jest + jest .spyOn(rootInteraction, 'isEqualStateName') - .mockImplementation(() => false); + .mockImplementationOnce(() => false); const mockCellA = createMockCellInfo('test-A').mockCell; const mockCellB = createMockCellInfo('test-B').mockCell; @@ -204,8 +205,6 @@ describe('RootInteraction Tests', () => { // 取消选中 expect(rootInteraction.getState().cells).toEqual([getCellMeta(mockCellA)]); - - isEqualStateNameSpy.mockRestore(); }); test('should call merge cells', () => { diff --git a/packages/s2-core/__tests__/unit/utils/cell/header-cell-spec.ts b/packages/s2-core/__tests__/unit/utils/cell/header-cell-spec.ts index 63498e33c8..a5aa9b6295 100644 --- a/packages/s2-core/__tests__/unit/utils/cell/header-cell-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/cell/header-cell-spec.ts @@ -1,4 +1,4 @@ -import type { Node } from '@antv/s2'; +import type { Node } from '../../../../src'; import { CellTypes, type HeaderActionIcon } from '@/common'; import { getActionIconConfig } from '@/utils/cell/header-cell'; 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 eb39fc36d9..5ff1a0bb37 100644 --- a/packages/s2-core/__tests__/unit/utils/export/copy-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/export/copy-spec.ts @@ -1,10 +1,9 @@ -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'; import { map } from 'lodash'; +import type { S2DataConfig } from '../../../../src/common'; import { TableSheet, PivotSheet } from '@/sheet-type'; - import { CellTypes, InteractionStateName, diff --git a/packages/s2-core/__tests__/unit/utils/interaction/state-controller-spec.ts b/packages/s2-core/__tests__/unit/utils/interaction/state-controller-spec.ts index 02535a8f4a..a1610f0a9a 100644 --- a/packages/s2-core/__tests__/unit/utils/interaction/state-controller-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/interaction/state-controller-spec.ts @@ -1,5 +1,5 @@ +import type { RowCell } from '../../../../src/cell'; import { getCellMeta } from '@/utils/interaction/select-event'; -import type { RowCell } from '@/cell/row-cell'; import { CellTypes, InteractionStateName } from '@/common/constant/interaction'; import type { S2Options } from '@/common/interface'; import { Store } from '@/common/store'; diff --git a/packages/s2-core/src/cell/base-row-cell.ts b/packages/s2-core/src/cell/base-row-cell.ts new file mode 100644 index 0000000000..911b0a4475 --- /dev/null +++ b/packages/s2-core/src/cell/base-row-cell.ts @@ -0,0 +1,501 @@ +import type { Point } from '@antv/g-canvas'; +import { GM } from '@antv/g-gesture'; +import { find, get, isEmpty } from 'lodash'; +import type { SimpleBBox } from '@antv/g-canvas'; +import { + CellTypes, + KEY_GROUP_ROW_RESIZE_AREA, + ResizeAreaEffect, + ResizeDirectionType, + S2Event, +} from '../common/constant'; +import { CellBorderPosition, type ViewMeta } from '../common/interface'; +import type { RowHeaderConfig } from '../facet/header/row'; +import { + getBorderPositionAndStyle, + getTextAndFollowingIconPosition, +} from '../utils/cell/cell'; +import { + renderCircle, + renderLine, + renderRect, + renderTreeIcon, +} from '../utils/g-renders'; +import { getAllChildrenNodeHeight } from '../utils/get-all-children-node-height'; +import { + getOrCreateResizeAreaGroupById, + getResizeAreaAttrs, +} from '../utils/interaction/resize'; +import { isMobile } from '../utils/is-mobile'; +import { getAdjustPosition } from '../utils/text-absorption'; +import { shouldAddResizeArea } from '../utils/interaction/resize'; +import { HeaderCell } from './header-cell'; + +export class BaseRowCell extends HeaderCell { + protected declare headerConfig: RowHeaderConfig; + + protected gm: GM; + + public get cellType() { + return CellTypes.ROW_CELL; + } + + public destroy(): void { + super.destroy(); + this.gm?.destroy(); + } + + protected initCell() { + super.initCell(); + // 绘制单元格背景 + this.drawBackgroundShape(); + // 绘制交互背景 + this.drawInteractiveBgShape(); + // 绘制交互边框 + this.drawInteractiveBorderShape(); + // 绘制单元格文本 + this.drawTextShape(); + // 绘制字段标记 -- icon + this.drawConditionIconShapes(); + // 绘制树状模式收起展开的 icon + this.drawTreeIcon(); + // 绘制树状模式下子节点层级占位圆点 + this.drawTreeLeafNodeAlignDot(); + // 绘制单元格边框 + this.drawRectBorder(); + // 绘制 resize 热区 + this.drawResizeAreaInLeaf(); + // 绘制 action icons + this.drawActionIcons(); + this.update(); + } + + public getBackgroundColor() { + const { backgroundColor, backgroundColorOpacity } = + this.getCrossBackgroundColor(this.meta.rowIndex); + return this.getBackgroundColorByCondition( + backgroundColor, + backgroundColorOpacity, + ); + } + + /** + * 绘制hover悬停,刷选的外框 + */ + protected drawInteractiveBorderShape() { + // 往内缩一个像素,避免和外边框重叠 + const margin = 2; + + this.stateShapes.set( + 'interactiveBorderShape', + renderRect(this, this.getInteractiveBorderShapeStyle(margin), { + visible: false, + }), + ); + } + + // 交互使用的背景色 + protected drawInteractiveBgShape() { + this.stateShapes.set( + 'interactiveBgShape', + renderRect( + this, + { + ...this.getCellArea(), + }, + { + visible: false, + }, + ), + ); + } + + protected showTreeIcon() { + return this.spreadsheet.isHierarchyTreeType() && !this.meta.isLeaf; + } + + protected showTreeLeafNodeAlignDot() { + return ( + this.spreadsheet.options.style?.showTreeLeafNodeAlignDot && + this.spreadsheet.isHierarchyTreeType() + ); + } + + // 获取树状模式下叶子节点的父节点收起展开 icon 图形属性 + protected getParentTreeIconCfg() { + if ( + !this.showTreeLeafNodeAlignDot() || + !this.spreadsheet.isHierarchyTreeType() || + !this.meta.isLeaf + ) { + return; + } + + return get(this.meta, 'parent.belongsCell.treeIcon.cfg'); + } + + // draw tree icon + protected drawTreeIcon() { + if (!this.showTreeIcon()) { + return; + } + + const { isCollapsed, id, hierarchy } = this.meta; + const { x } = this.getContentArea(); + const { fill } = this.getTextStyle(); + const { size } = this.getStyle().icon; + + const contentIndent = this.getContentIndent(); + + const iconX = x + contentIndent; + const iconY = this.getIconYPosition(); + + this.treeIcon = renderTreeIcon( + this, + { + x: iconX, + y: iconY, + width: size, + height: size, + }, + fill, + isCollapsed, + () => { + if (isMobile()) { + return; + } + // 折叠行头时因scrollY没变,导致底层出现空白 + if (!isCollapsed) { + const oldScrollY = this.spreadsheet.store.get('scrollY'); + // 可视窗口高度 + const viewportHeight = this.headerConfig.viewportHeight || 0; + // 被折叠项的高度 + const deleteHeight = getAllChildrenNodeHeight(this.meta); + // 折叠后真实高度 + const realHeight = hierarchy.height - deleteHeight; + if (oldScrollY > 0 && oldScrollY + viewportHeight > realHeight) { + const currentScrollY = realHeight - viewportHeight; + this.spreadsheet.store.set( + 'scrollY', + currentScrollY > 0 ? currentScrollY : 0, + ); + } + } + this.spreadsheet.emit(S2Event.ROW_CELL_COLLAPSE_TREE_ROWS, { + id, + isCollapsed: !isCollapsed, + node: this.meta, + }); + }, + ); + + // in mobile, we use this cell + if (isMobile()) { + this.gm = new GM(this, { + gestures: ['Tap'], + }); + this.gm.on('tap', () => { + this.spreadsheet.emit(S2Event.ROW_CELL_COLLAPSE_TREE_ROWS, { + id, + isCollapsed: !isCollapsed, + node: this.meta, + }); + }); + } + } + + protected drawTreeLeafNodeAlignDot() { + const parentTreeIconCfg = this.getParentTreeIconCfg(); + if (!parentTreeIconCfg) { + return; + } + const { size, margin } = this.getStyle().icon; + const x = parentTreeIconCfg.x + size + margin.right; + const textY = this.getTextPosition().y; + + const { fill, fontSize } = this.getTextStyle(); + const r = size / 5; // 半径,暂时先写死,后面看是否有这个点点的定制需求 + this.treeLeafNodeAlignDot = renderCircle(this, { + x: x + size / 2, // 和收起展开 icon 保持居中对齐 + y: textY + (fontSize - r) / 2, + r, + fill, + fillOpacity: 0.3, // 暂时先写死,后面看是否有这个点点的定制需求 + }); + } + + protected isBolderText() { + // 非叶子节点、小计总计,均为粗体 + const { isLeaf, isTotals, level } = this.meta; + return (!isLeaf && level === 0) || isTotals; + } + + // draw text + protected drawTextShape() { + super.drawTextShape(); + this.drawLinkField(this.meta); + } + + protected drawRectBorder() { + const { x } = this.getCellArea(); + + const contentIndent = this.getContentIndent(); + const finalX = this.spreadsheet.isHierarchyTreeType() + ? x + : x + contentIndent; + [CellBorderPosition.BOTTOM, CellBorderPosition.LEFT].forEach((type) => { + const { position, style } = getBorderPositionAndStyle( + type, + { + ...this.getCellArea(), + x: finalX, + }, + this.getStyle().cell, + ); + renderLine(this, position, style); + }); + } + + protected getResizeClipAreaBBox(): SimpleBBox { + const { width, viewportHeight } = this.headerConfig; + return { + x: 0, + y: 0, + width, + height: viewportHeight, + }; + } + + protected drawResizeAreaInLeaf() { + if ( + !this.meta.isLeaf || + !this.shouldDrawResizeAreaByType('rowCellVertical', this) + ) { + return; + } + + const { x, y, width, height } = this.getCellArea(); + const resizeStyle = this.getResizeAreaStyle(); + const resizeArea = getOrCreateResizeAreaGroupById( + this.spreadsheet, + KEY_GROUP_ROW_RESIZE_AREA, + ); + + const { + position, + seriesNumberWidth, + width: headerWidth, + scrollX, + scrollY, + } = this.headerConfig; + + const resizeAreaBBox = { + // fix: When scrolling without the entire frozen header horizontally, the resize area would be removed permanently. + x: x + seriesNumberWidth, + y: y + height - resizeStyle.size / 2, + width, + height: resizeStyle.size, + }; + + const resizeClipAreaBBox = this.getResizeClipAreaBBox(); + + if ( + !shouldAddResizeArea(resizeAreaBBox, resizeClipAreaBBox, { + scrollX, + scrollY, + }) + ) { + return; + } + + const offsetX = position?.x + x - scrollX + seriesNumberWidth; + const offsetY = position?.y + y - scrollY; + + const resizeAreaWidth = this.spreadsheet.isFrozenRowHeader() + ? headerWidth - seriesNumberWidth - (x - scrollX) + : width; + + resizeArea.addShape('rect', { + attrs: { + ...getResizeAreaAttrs({ + id: this.meta.id, + theme: resizeStyle, + type: ResizeDirectionType.Vertical, + effect: ResizeAreaEffect.Cell, + offsetX, + offsetY, + width, + height, + meta: this.meta, + }), + x: offsetX, + y: offsetY + height - resizeStyle.size / 2, + width: resizeAreaWidth, + }, + }); + } + + protected getContentIndent() { + if (!this.spreadsheet.isHierarchyTreeType()) { + return 0; + } + const { icon, cell } = this.getStyle(); + const iconWidth = icon.size + icon.margin.right; + + let parent = this.meta.parent; + let sum = 0; + while (parent) { + if (parent.height !== 0) { + sum += iconWidth; + } + parent = parent.parent; + } + if (this.showTreeLeafNodeAlignDot()) { + sum += this.isTreeLevel() ? 0 : cell.padding.right + icon.margin.right; + } + + return sum; + } + + protected getTextIndent() { + const { size, margin } = this.getStyle().icon; + const contentIndent = this.getContentIndent(); + const treeIconWidth = + this.showTreeIcon() || + (this.isTreeLevel() && this.showTreeLeafNodeAlignDot()) + ? size + margin.right + : 0; + return contentIndent + treeIconWidth; + } + + // 判断当前节点的兄弟节点是否叶子节点 + protected isTreeLevel() { + return find( + get(this.meta, 'parent.children'), + (cell: ViewMeta) => !cell.isLeaf, + ); + } + + protected getIconPosition() { + // 不同 textAlign 下,对应的文字绘制点 x 不同 + const { x, y, textAlign } = this.textShape.cfg.attrs; + const iconMarginLeft = this.getStyle().icon.margin.left; + + if (textAlign === 'left') { + /** + * attrs.x + * | + * v + * +---------+ +----+ + * | text |--|icon| + * +---------+ +----+ + */ + return { + x: x + this.actualTextWidth + iconMarginLeft, + y, + }; + } + if (textAlign === 'right') { + /** + * attrs.x + * | + * v + * +---------+ +----+ + * | text |--|icon| + * +---------+ +----+ + */ + return { + x: x + iconMarginLeft, + y, + }; + } + + /** + * attrs.x + * | + * v + * +---------+ +----+ + * | text |--|icon| + * +---------+ +----+ + */ + return { + x: x + this.actualTextWidth / 2 + iconMarginLeft, + y, + }; + } + + protected getMaxTextWidth(): number { + const { width } = this.getContentArea(); + return width - this.getTextIndent() - this.getActionIconsWidth(); + } + + protected getTextArea(): SimpleBBox { + const content = this.getContentArea(); + const textIndent = this.getTextIndent(); + return { + ...content, + x: content.x + textIndent, + width: content.width - textIndent, + }; + } + + protected getAdjustTextAreaHeight( + textArea: SimpleBBox, + scrollY: number, + viewportHeight: number, + ): number { + let adjustTextAreaHeight = textArea.height; + if ( + !this.spreadsheet.facet.vScrollBar && + textArea.y + textArea.height > scrollY + viewportHeight + ) { + adjustTextAreaHeight = scrollY + viewportHeight - textArea.y; + } + return adjustTextAreaHeight; + } + + protected calculateTextY({ + textArea, + adjustTextAreaHeight, + }: { + textArea: SimpleBBox; + adjustTextAreaHeight: number; + }): number { + const { scrollY, viewportHeight } = this.headerConfig; + const { fontSize } = this.getTextStyle(); + return getAdjustPosition( + textArea.y, + adjustTextAreaHeight, + scrollY, + viewportHeight, + fontSize, + ); + } + + protected getTextPosition(): Point { + const textArea = this.getTextArea(); + const { scrollY, viewportHeight } = this.headerConfig; + + const adjustTextAreaHeight = this.getAdjustTextAreaHeight( + textArea, + scrollY, + viewportHeight, + ); + const textY = this.calculateTextY({ textArea, adjustTextAreaHeight }); + const textX = getTextAndFollowingIconPosition( + textArea, + this.getTextStyle(), + 0, + this.getIconStyle(), + this.getActionIconsCount(), + ).text.x; + return { x: textX, y: textY }; + } + + protected getIconYPosition() { + const textY = this.getTextPosition().y; + const { size } = this.getStyle().icon; + const { fontSize } = this.getTextStyle(); + return textY + (fontSize - size) / 2; + } +} diff --git a/packages/s2-core/src/cell/frozen-row-cell.ts b/packages/s2-core/src/cell/frozen-row-cell.ts deleted file mode 100644 index 3728072c39..0000000000 --- a/packages/s2-core/src/cell/frozen-row-cell.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { SimpleBBox } from '@antv/g-canvas'; -import { getAdjustPosition } from '../utils/text-absorption'; -import { getFrozenRowCfgPivot } from '../facet/utils'; -import type { BaseHeaderConfig } from '../facet/header/base'; -import { RowCell } from './row-cell'; - -/** - * Adapting the frozen first row for cells pivot table - */ -export class FrozenRowCell extends RowCell { - /** - * To indicate whether the current node is a frozen node - * - * PS: It is a specific config for the cell node, so it should not be extended in the headerConfig. - */ - protected frozenRowCell: boolean; - - protected handleRestOptions( - ...[headerConfig, ...options]: [BaseHeaderConfig, boolean] - ) { - super.handleRestOptions(headerConfig, options); - this.frozenRowCell = options[0]; - } - - protected getAdjustTextAreaHeight( - textArea: SimpleBBox, - scrollY: number, - viewportHeight: number, - ): number { - const correctY = textArea.y - this.getFrozenFirstRowHeight(); - let adjustTextAreaHeight = textArea.height; - if ( - !this.spreadsheet.facet.vScrollBar && - correctY + textArea.height > scrollY + viewportHeight - ) { - adjustTextAreaHeight = scrollY + viewportHeight - correctY; - } - return adjustTextAreaHeight; - } - - protected calculateTextY({ - textArea, - adjustTextAreaHeight, - }: { - textArea: SimpleBBox; - adjustTextAreaHeight: number; - }): number { - const { scrollY, viewportHeight } = this.headerConfig; - const { fontSize } = this.getTextStyle(); - return getAdjustPosition( - textArea.y, - adjustTextAreaHeight, - // viewportLeft: start at the frozen row position - scrollY + this.getFrozenFirstRowHeight(), - viewportHeight, - fontSize, - ); - } - - protected getResizeClipAreaBBox(): SimpleBBox { - return { - ...super.getResizeClipAreaBBox(), - y: this.getFrozenFirstRowHeight(), - }; - } - - private getFrozenFirstRowHeight(): number { - if (this.frozenRowCell) { - // frozen row cell - return 0; - } - const { spreadsheet } = this.headerConfig; - const { facet } = spreadsheet; - const { frozenRowHeight } = getFrozenRowCfgPivot( - spreadsheet.options, - facet?.layoutResult?.rowNodes, - ); - return frozenRowHeight; - } -} diff --git a/packages/s2-core/src/cell/index.ts b/packages/s2-core/src/cell/index.ts index 54eb1eafba..8503b33e5d 100644 --- a/packages/s2-core/src/cell/index.ts +++ b/packages/s2-core/src/cell/index.ts @@ -4,12 +4,12 @@ import { CornerCell } from './corner-cell'; import { DataCell } from './data-cell'; import { HeaderCell } from './header-cell'; import { MergedCell } from './merged-cell'; -import { RowCell } from './row-cell'; import { TableColCell } from './table-col-cell'; import { TableCornerCell } from './table-corner-cell'; import { TableDataCell } from './table-data-cell'; import { TableSeriesCell } from './table-series-cell'; -import { FrozenRowCell } from './frozen-row-cell'; +import { BaseRowCell } from './base-row-cell'; +import { RowCell } from './row-cell'; import { SeriesNumberCell } from './series-number-cell'; export { @@ -18,12 +18,12 @@ export { TableSeriesCell, TableDataCell, RowCell, + BaseRowCell, ColCell, DataCell, MergedCell, CornerCell, BaseCell, HeaderCell, - FrozenRowCell, SeriesNumberCell, }; diff --git a/packages/s2-core/src/cell/row-cell.ts b/packages/s2-core/src/cell/row-cell.ts index f26f76ccb7..2994cf72c0 100644 --- a/packages/s2-core/src/cell/row-cell.ts +++ b/packages/s2-core/src/cell/row-cell.ts @@ -1,442 +1,25 @@ -import type { Point } from '@antv/g-canvas'; -import { GM } from '@antv/g-gesture'; -import { find, get, isEmpty } from 'lodash'; import type { SimpleBBox } from '@antv/g-canvas'; -import { - CellTypes, - KEY_GROUP_ROW_RESIZE_AREA, - ResizeAreaEffect, - ResizeDirectionType, - S2Event, -} from '../common/constant'; -import { CellBorderPosition, type ViewMeta } from '../common/interface'; -import type { RowHeaderConfig } from '../facet/header/row'; -import { - getBorderPositionAndStyle, - getTextAndFollowingIconPosition, -} from '../utils/cell/cell'; -import { - renderCircle, - renderLine, - renderRect, - renderTreeIcon, -} from '../utils/g-renders'; -import { getAllChildrenNodeHeight } from '../utils/get-all-children-node-height'; -import { - getOrCreateResizeAreaGroupById, - getResizeAreaAttrs, -} from '../utils/interaction/resize'; -import { isMobile } from '../utils/is-mobile'; import { getAdjustPosition } from '../utils/text-absorption'; -import { shouldAddResizeArea } from './../utils/interaction/resize'; -import { HeaderCell } from './header-cell'; - -export class RowCell extends HeaderCell { - protected declare headerConfig: RowHeaderConfig; - - protected gm: GM; - - public get cellType() { - return CellTypes.ROW_CELL; - } - - public destroy(): void { - super.destroy(); - this.gm?.destroy(); - } - - protected initCell() { - super.initCell(); - // 绘制单元格背景 - this.drawBackgroundShape(); - // 绘制交互背景 - this.drawInteractiveBgShape(); - // 绘制交互边框 - this.drawInteractiveBorderShape(); - // 绘制单元格文本 - this.drawTextShape(); - // 绘制字段标记 -- icon - this.drawConditionIconShapes(); - // 绘制树状模式收起展开的 icon - this.drawTreeIcon(); - // 绘制树状模式下子节点层级占位圆点 - this.drawTreeLeafNodeAlignDot(); - // 绘制单元格边框 - this.drawRectBorder(); - // 绘制 resize 热区 - this.drawResizeAreaInLeaf(); - // 绘制 action icons - this.drawActionIcons(); - this.update(); - } - - public getBackgroundColor() { - const { backgroundColor, backgroundColorOpacity } = - this.getCrossBackgroundColor(this.meta.rowIndex); - return this.getBackgroundColorByCondition( - backgroundColor, - backgroundColorOpacity, - ); - } - +import { getFrozenRowCfgPivot } from '../facet/utils'; +import type { BaseHeaderConfig } from '../facet/header/base'; +import { BaseRowCell } from './base-row-cell'; + +/** + * Adapting the frozen first row for cells pivot table + */ +export class RowCell extends BaseRowCell { /** - * 绘制hover悬停,刷选的外框 + * To indicate whether the current node is a frozen node + * + * PS: It is a specific config for the cell node, so it should not be extended in the headerConfig. */ - protected drawInteractiveBorderShape() { - // 往内缩一个像素,避免和外边框重叠 - const margin = 2; - - this.stateShapes.set( - 'interactiveBorderShape', - renderRect(this, this.getInteractiveBorderShapeStyle(margin), { - visible: false, - }), - ); - } - - // 交互使用的背景色 - protected drawInteractiveBgShape() { - this.stateShapes.set( - 'interactiveBgShape', - renderRect( - this, - { - ...this.getCellArea(), - }, - { - visible: false, - }, - ), - ); - } - - protected showTreeIcon() { - return this.spreadsheet.isHierarchyTreeType() && !this.meta.isLeaf; - } - - protected showTreeLeafNodeAlignDot() { - return ( - this.spreadsheet.options.style?.showTreeLeafNodeAlignDot && - this.spreadsheet.isHierarchyTreeType() - ); - } + protected frozenRowCell: boolean; - // 获取树状模式下叶子节点的父节点收起展开 icon 图形属性 - protected getParentTreeIconCfg() { - if ( - !this.showTreeLeafNodeAlignDot() || - !this.spreadsheet.isHierarchyTreeType() || - !this.meta.isLeaf - ) { - return; - } - - return get(this.meta, 'parent.belongsCell.treeIcon.cfg'); - } - - // draw tree icon - protected drawTreeIcon() { - if (!this.showTreeIcon()) { - return; - } - - const { isCollapsed, id, hierarchy } = this.meta; - const { x } = this.getContentArea(); - const { fill } = this.getTextStyle(); - const { size } = this.getStyle().icon; - - const contentIndent = this.getContentIndent(); - - const iconX = x + contentIndent; - const iconY = this.getIconYPosition(); - - this.treeIcon = renderTreeIcon( - this, - { - x: iconX, - y: iconY, - width: size, - height: size, - }, - fill, - isCollapsed, - () => { - if (isMobile()) { - return; - } - // 折叠行头时因scrollY没变,导致底层出现空白 - if (!isCollapsed) { - const oldScrollY = this.spreadsheet.store.get('scrollY'); - // 可视窗口高度 - const viewportHeight = this.headerConfig.viewportHeight || 0; - // 被折叠项的高度 - const deleteHeight = getAllChildrenNodeHeight(this.meta); - // 折叠后真实高度 - const realHeight = hierarchy.height - deleteHeight; - if (oldScrollY > 0 && oldScrollY + viewportHeight > realHeight) { - const currentScrollY = realHeight - viewportHeight; - this.spreadsheet.store.set( - 'scrollY', - currentScrollY > 0 ? currentScrollY : 0, - ); - } - } - this.spreadsheet.emit(S2Event.ROW_CELL_COLLAPSE_TREE_ROWS, { - id, - isCollapsed: !isCollapsed, - node: this.meta, - }); - }, - ); - - // in mobile, we use this cell - if (isMobile()) { - this.gm = new GM(this, { - gestures: ['Tap'], - }); - this.gm.on('tap', () => { - this.spreadsheet.emit(S2Event.ROW_CELL_COLLAPSE_TREE_ROWS, { - id, - isCollapsed: !isCollapsed, - node: this.meta, - }); - }); - } - } - - protected drawTreeLeafNodeAlignDot() { - const parentTreeIconCfg = this.getParentTreeIconCfg(); - if (!parentTreeIconCfg) { - return; - } - const { size, margin } = this.getStyle().icon; - const x = parentTreeIconCfg.x + size + margin.right; - const textY = this.getTextPosition().y; - - const { fill, fontSize } = this.getTextStyle(); - const r = size / 5; // 半径,暂时先写死,后面看是否有这个点点的定制需求 - this.treeLeafNodeAlignDot = renderCircle(this, { - x: x + size / 2, // 和收起展开 icon 保持居中对齐 - y: textY + (fontSize - r) / 2, - r, - fill, - fillOpacity: 0.3, // 暂时先写死,后面看是否有这个点点的定制需求 - }); - } - - protected isBolderText() { - // 非叶子节点、小计总计,均为粗体 - const { isLeaf, isTotals, level } = this.meta; - return (!isLeaf && level === 0) || isTotals; - } - - // draw text - protected drawTextShape() { - super.drawTextShape(); - this.drawLinkField(this.meta); - } - - protected drawRectBorder() { - const { x } = this.getCellArea(); - - const contentIndent = this.getContentIndent(); - const finalX = this.spreadsheet.isHierarchyTreeType() - ? x - : x + contentIndent; - [CellBorderPosition.BOTTOM, CellBorderPosition.LEFT].forEach((type) => { - const { position, style } = getBorderPositionAndStyle( - type, - { - ...this.getCellArea(), - x: finalX, - }, - this.getStyle().cell, - ); - renderLine(this, position, style); - }); - } - - protected getResizeClipAreaBBox(): SimpleBBox { - const { width, viewportHeight } = this.headerConfig; - return { - x: 0, - y: 0, - width, - height: viewportHeight, - }; - } - - protected drawResizeAreaInLeaf() { - if ( - !this.meta.isLeaf || - !this.shouldDrawResizeAreaByType('rowCellVertical', this) - ) { - return; - } - - const { x, y, width, height } = this.getCellArea(); - const resizeStyle = this.getResizeAreaStyle(); - const resizeArea = getOrCreateResizeAreaGroupById( - this.spreadsheet, - KEY_GROUP_ROW_RESIZE_AREA, - ); - - const { - position, - seriesNumberWidth, - width: headerWidth, - scrollX, - scrollY, - } = this.headerConfig; - - const resizeAreaBBox = { - // fix: When scrolling without the entire frozen header horizontally, the resize area would be removed permanently. - x: x + seriesNumberWidth, - y: y + height - resizeStyle.size / 2, - width, - height: resizeStyle.size, - }; - - const resizeClipAreaBBox = this.getResizeClipAreaBBox(); - - if ( - !shouldAddResizeArea(resizeAreaBBox, resizeClipAreaBBox, { - scrollX, - scrollY, - }) - ) { - return; - } - - const offsetX = position?.x + x - scrollX + seriesNumberWidth; - const offsetY = position?.y + y - scrollY; - - const resizeAreaWidth = this.spreadsheet.isFrozenRowHeader() - ? headerWidth - seriesNumberWidth - (x - scrollX) - : width; - - resizeArea.addShape('rect', { - attrs: { - ...getResizeAreaAttrs({ - id: this.meta.id, - theme: resizeStyle, - type: ResizeDirectionType.Vertical, - effect: ResizeAreaEffect.Cell, - offsetX, - offsetY, - width, - height, - meta: this.meta, - }), - x: offsetX, - y: offsetY + height - resizeStyle.size / 2, - width: resizeAreaWidth, - }, - }); - } - - protected getContentIndent() { - if (!this.spreadsheet.isHierarchyTreeType()) { - return 0; - } - const { icon, cell } = this.getStyle(); - const iconWidth = icon.size + icon.margin.right; - - let parent = this.meta.parent; - let sum = 0; - while (parent) { - if (parent.height !== 0) { - sum += iconWidth; - } - parent = parent.parent; - } - if (this.showTreeLeafNodeAlignDot()) { - sum += this.isTreeLevel() ? 0 : cell.padding.right + icon.margin.right; - } - - return sum; - } - - protected getTextIndent() { - const { size, margin } = this.getStyle().icon; - const contentIndent = this.getContentIndent(); - const treeIconWidth = - this.showTreeIcon() || - (this.isTreeLevel() && this.showTreeLeafNodeAlignDot()) - ? size + margin.right - : 0; - return contentIndent + treeIconWidth; - } - - // 判断当前节点的兄弟节点是否叶子节点 - protected isTreeLevel() { - return find( - get(this.meta, 'parent.children'), - (cell: ViewMeta) => !cell.isLeaf, - ); - } - - protected getIconPosition() { - // 不同 textAlign 下,对应的文字绘制点 x 不同 - const { x, y, textAlign } = this.textShape.cfg.attrs; - const iconMarginLeft = this.getStyle().icon.margin.left; - - if (textAlign === 'left') { - /** - * attrs.x - * | - * v - * +---------+ +----+ - * | text |--|icon| - * +---------+ +----+ - */ - return { - x: x + this.actualTextWidth + iconMarginLeft, - y, - }; - } - if (textAlign === 'right') { - /** - * attrs.x - * | - * v - * +---------+ +----+ - * | text |--|icon| - * +---------+ +----+ - */ - return { - x: x + iconMarginLeft, - y, - }; - } - - /** - * attrs.x - * | - * v - * +---------+ +----+ - * | text |--|icon| - * +---------+ +----+ - */ - return { - x: x + this.actualTextWidth / 2 + iconMarginLeft, - y, - }; - } - - protected getMaxTextWidth(): number { - const { width } = this.getContentArea(); - return width - this.getTextIndent() - this.getActionIconsWidth(); - } - - protected getTextArea(): SimpleBBox { - const content = this.getContentArea(); - const textIndent = this.getTextIndent(); - return { - ...content, - x: content.x + textIndent, - width: content.width - textIndent, - }; + protected handleRestOptions( + ...[headerConfig, ...options]: [BaseHeaderConfig, boolean] + ) { + super.handleRestOptions(headerConfig, options); + this.frozenRowCell = options[0]; } protected getAdjustTextAreaHeight( @@ -444,12 +27,13 @@ export class RowCell extends HeaderCell { scrollY: number, viewportHeight: number, ): number { + const correctY = textArea.y - this.getFrozenFirstRowHeight(); let adjustTextAreaHeight = textArea.height; if ( !this.spreadsheet.facet.vScrollBar && - textArea.y + textArea.height > scrollY + viewportHeight + correctY + textArea.height > scrollY + viewportHeight ) { - adjustTextAreaHeight = scrollY + viewportHeight - textArea.y; + adjustTextAreaHeight = scrollY + viewportHeight - correctY; } return adjustTextAreaHeight; } @@ -466,36 +50,31 @@ export class RowCell extends HeaderCell { return getAdjustPosition( textArea.y, adjustTextAreaHeight, - scrollY, + // viewportLeft: start at the frozen row position + scrollY + this.getFrozenFirstRowHeight(), viewportHeight, fontSize, ); } - protected getTextPosition(): Point { - const textArea = this.getTextArea(); - const { scrollY, viewportHeight } = this.headerConfig; - - const adjustTextAreaHeight = this.getAdjustTextAreaHeight( - textArea, - scrollY, - viewportHeight, - ); - const textY = this.calculateTextY({ textArea, adjustTextAreaHeight }); - const textX = getTextAndFollowingIconPosition( - textArea, - this.getTextStyle(), - 0, - this.getIconStyle(), - this.getActionIconsCount(), - ).text.x; - return { x: textX, y: textY }; + protected getResizeClipAreaBBox(): SimpleBBox { + return { + ...super.getResizeClipAreaBBox(), + y: this.getFrozenFirstRowHeight(), + }; } - protected getIconYPosition() { - const textY = this.getTextPosition().y; - const { size } = this.getStyle().icon; - const { fontSize } = this.getTextStyle(); - return textY + (fontSize - size) / 2; + private getFrozenFirstRowHeight(): number { + if (this.frozenRowCell) { + // frozen row cell + return 0; + } + const { spreadsheet } = this.headerConfig; + const { facet } = spreadsheet; + const { frozenRowHeight } = getFrozenRowCfgPivot( + spreadsheet.options, + facet?.layoutResult?.rowNodes, + ); + return frozenRowHeight; } } diff --git a/packages/s2-core/src/cell/series-number-cell.ts b/packages/s2-core/src/cell/series-number-cell.ts index 99193ef260..eefc400b7f 100644 --- a/packages/s2-core/src/cell/series-number-cell.ts +++ b/packages/s2-core/src/cell/series-number-cell.ts @@ -1,8 +1,8 @@ import type { Point } from '@antv/g-canvas'; import type { Condition, IconTheme, MappingResult, TextTheme } from '../common'; -import { FrozenRowCell } from './frozen-row-cell'; +import { RowCell } from './row-cell'; -export class SeriesNumberCell extends FrozenRowCell { +export class SeriesNumberCell extends RowCell { protected initCell(): void { this.drawBackgroundShape(); this.drawRectBorder(); diff --git a/packages/s2-core/src/common/icons/gui-icon.ts b/packages/s2-core/src/common/icons/gui-icon.ts index c04c23cfa2..5429ee6b15 100644 --- a/packages/s2-core/src/common/icons/gui-icon.ts +++ b/packages/s2-core/src/common/icons/gui-icon.ts @@ -1,8 +1,9 @@ /** - * @Description: 请严格要求 svg 的 viewBox,若设计产出的 svg 不是此规格,请叫其修改为 '0 0 1024 1024' + * @description: 请严格要求 svg 的 viewBox,若设计产出的 svg 不是此规格,请叫其修改为 '0 0 1024 1024' */ import { Group, Shape, type ShapeAttrs } from '@antv/g-canvas'; import { omit, clone } from 'lodash'; +import { DebuggerUtil, type S2CellType } from '..'; import { getIcon } from './factory'; const STYLE_PLACEHOLDER = '