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 DingTalk - - qq -

## 👬 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 = ' { - // 加载完成后,当前 Cell 可能已经销毁了 - if (this.destroyed) { + // 异步加载完成后,当前 Cell 可能已经销毁了 + const canvas = (this.getParent() as S2CellType)?.getMeta?.() + .spreadsheet?.container; + + // G 底层 refreshElements 默认是个数组, 销毁时获取不到, 没有兜底 https://github.com/antvis/S2/issues/2435 + if (this.destroyed || (canvas && !canvas.get('refreshElements'))) { + DebuggerUtil.getInstance().logger(`GuiIcon ${name} destroyed.`); return; } + image.attr('img', value); this.addShape('image', image); }) - .catch((event: Event) => { + .catch((event: string | Event) => { + // 如果是 TypeError, 则是 G 底层渲染有问题, 其他场景才报加载异常的错误 + if (event instanceof TypeError) { + // eslint-disable-next-line no-console + console.warn(`GuiIcon ${name} destroyed:`, event); + return; + } // eslint-disable-next-line no-console - console.error(`GuiIcon ${name} load failed`, event); + console.error(`GuiIcon ${name} load failed:`, event); }); } } diff --git a/packages/s2-core/src/common/interface/emitter.ts b/packages/s2-core/src/common/interface/emitter.ts index 58c42b7079..800f878f95 100644 --- a/packages/s2-core/src/common/interface/emitter.ts +++ b/packages/s2-core/src/common/interface/emitter.ts @@ -1,7 +1,5 @@ import type { Event as CanvasEvent } from '@antv/g-canvas'; -import type { ColCell } from '../../cell/col-cell'; -import type { DataCell } from '../../cell/data-cell'; -import type { RowCell } from '../../cell/row-cell'; +import type { ColCell, DataCell, RowCell } from '../../cell'; import type { S2Event } from '../../common/constant'; import type { CellMeta, diff --git a/packages/s2-core/src/common/interface/interaction.ts b/packages/s2-core/src/common/interface/interaction.ts index 4bba56f0ef..1f67b44ae2 100644 --- a/packages/s2-core/src/common/interface/interaction.ts +++ b/packages/s2-core/src/common/interface/interaction.ts @@ -1,6 +1,7 @@ import type { SimpleBBox } from '@antv/g-canvas'; import type { BaseCell, + BaseRowCell, ColCell, CornerCell, DataCell, @@ -27,6 +28,7 @@ export type S2CellType = | ColCell | CornerCell | RowCell + | BaseRowCell | MergedCell | BaseCell; diff --git a/packages/s2-core/src/common/interface/s2Options.ts b/packages/s2-core/src/common/interface/s2Options.ts index 2b0e1f134a..4b8d236493 100644 --- a/packages/s2-core/src/common/interface/s2Options.ts +++ b/packages/s2-core/src/common/interface/s2Options.ts @@ -128,7 +128,8 @@ export interface S2TableSheetOptions { // Pivot sheet options export interface S2PivotSheetOptions { // pivot sheet type: frozen head row, default false - frozenFirstRowPivot?: boolean; + // TODO: 2.0 版本统一在 frozen: { ... } 命名空间下 + frozenFirstRow?: boolean; } export interface S2Options< diff --git a/packages/s2-core/src/data-set/base-data-set.ts b/packages/s2-core/src/data-set/base-data-set.ts index 57351ef1b6..d166a0cc47 100644 --- a/packages/s2-core/src/data-set/base-data-set.ts +++ b/packages/s2-core/src/data-set/base-data-set.ts @@ -26,6 +26,7 @@ import { setValueRangeState, } from '../utils/condition/state-controller'; import { generateExtraFieldMeta } from '../utils/dataset/pivot-data-set'; +import type { Indexes } from '../utils/indexes'; import type { CellDataParams, DataType, MultiDataParams, Query } from './index'; export abstract class BaseDataSet { @@ -122,6 +123,11 @@ export abstract class BaseDataSet { return isEmpty(this.getDisplayDataSet()); } + // https://github.com/antvis/S2/issues/2255 + public getEmptyViewIndexes(): Indexes { + return []; + } + public getValueRangeByField(field: string): ValueRange { const cacheRange = getValueRangeState(this.spreadsheet, field); if (cacheRange) { diff --git a/packages/s2-core/src/facet/frozen-facet.ts b/packages/s2-core/src/facet/frozen-facet.ts index 52c278bc83..6f9c17e2dd 100644 --- a/packages/s2-core/src/facet/frozen-facet.ts +++ b/packages/s2-core/src/facet/frozen-facet.ts @@ -199,17 +199,17 @@ export abstract class FrozenFacet extends BaseFacet { } } - // https://github.com/antvis/S2/issues/2255 - const indexes = this.spreadsheet.dataSet?.isEmpty?.() - ? ([] as unknown as Indexes) - : calculateInViewIndexes( - scrollX, - scrollY, - this.viewCellWidths, - this.viewCellHeights, - finalViewport, - this.getRealScrollX(this.cornerBBox.width), - ); + const indexes = + this.spreadsheet.isTableMode() && this.spreadsheet.dataSet?.isEmpty?.() + ? this.spreadsheet.dataSet.getEmptyViewIndexes() + : calculateInViewIndexes( + scrollX, + scrollY, + this.viewCellWidths, + this.viewCellHeights, + finalViewport, + this.getRealScrollX(this.cornerBBox.width), + ); this.panelScrollGroupIndexes = indexes; diff --git a/packages/s2-core/src/facet/header/base-frozen-row.ts b/packages/s2-core/src/facet/header/base-frozen-row.ts index d7f34ef3a2..e3b2fe6c8b 100644 --- a/packages/s2-core/src/facet/header/base-frozen-row.ts +++ b/packages/s2-core/src/facet/header/base-frozen-row.ts @@ -98,7 +98,7 @@ export class BaseFrozenRowHeader extends RowHeader { }); } - public isFrozenRow(item: Node): boolean { + public isFrozenRow(node: Node): boolean { const { spreadsheet } = this.headerConfig; const { facet } = spreadsheet; const { frozenRowCount } = getFrozenRowCfgPivot( @@ -106,7 +106,7 @@ export class BaseFrozenRowHeader extends RowHeader { facet.layoutResult?.rowNodes, ); return ( - frozenRowCount > 0 && item.rowIndex >= 0 && item.rowIndex < frozenRowCount + frozenRowCount > 0 && node.rowIndex >= 0 && node.rowIndex < frozenRowCount ); } diff --git a/packages/s2-core/src/facet/header/pivot-row.ts b/packages/s2-core/src/facet/header/pivot-row.ts index 493bd13772..917f321678 100644 --- a/packages/s2-core/src/facet/header/pivot-row.ts +++ b/packages/s2-core/src/facet/header/pivot-row.ts @@ -1,13 +1,14 @@ import type { Node } from '../layout/node'; -import { FrozenRowCell, RowCell } from '../../cell'; +import { RowCell } from '../../cell/row-cell'; import { BaseFrozenRowHeader } from './base-frozen-row'; export class PivotRowHeader extends BaseFrozenRowHeader { - protected createCellInstance(item: Node): RowCell { + public createCellInstance(node: Node): RowCell { const { spreadsheet, scrollY } = this.headerConfig; - const frozenRow = this.isFrozenRow(item); - return new FrozenRowCell( - item, + const frozenRow = this.isFrozenRow(node); + + return new RowCell( + node, spreadsheet, { ...this.headerConfig, diff --git a/packages/s2-core/src/facet/header/row.ts b/packages/s2-core/src/facet/header/row.ts index 753f5ff5cc..47ce5743db 100644 --- a/packages/s2-core/src/facet/header/row.ts +++ b/packages/s2-core/src/facet/header/row.ts @@ -46,11 +46,11 @@ export class RowHeader extends BaseHeader { ); // right } - protected createCellInstance(item: Node) { - return new RowCell(item, this.headerConfig.spreadsheet, this.headerConfig); + public createCellInstance(node: Node) { + return new RowCell(node, this.headerConfig.spreadsheet, this.headerConfig); } - protected getCellGroup(item: Node): IGroup { + protected getCellGroup(node: Node): IGroup { return this; } diff --git a/packages/s2-core/src/facet/header/series-number.ts b/packages/s2-core/src/facet/header/series-number.ts index b46410ca96..09f541735b 100644 --- a/packages/s2-core/src/facet/header/series-number.ts +++ b/packages/s2-core/src/facet/header/series-number.ts @@ -72,10 +72,10 @@ export class SeriesNumberHeader extends BaseFrozenRowHeader { }); } - protected createCellInstance(item: Node): RowCell { - const frozenRow = this.isFrozenRow(item); - const cell = new SeriesNumberCell( - item, + public createCellInstance(node: Node): RowCell { + const frozenRow = this.isFrozenRow(node); + return new SeriesNumberCell( + node, this.headerConfig.spreadsheet, { ...this.headerConfig, @@ -83,6 +83,5 @@ export class SeriesNumberHeader extends BaseFrozenRowHeader { }, frozenRow, ); - return cell; } } diff --git a/packages/s2-core/src/facet/utils.ts b/packages/s2-core/src/facet/utils.ts index a9946b47ff..12febd56a2 100644 --- a/packages/s2-core/src/facet/utils.ts +++ b/packages/s2-core/src/facet/utils.ts @@ -542,19 +542,19 @@ export const areAllFieldsEmpty = (fields: Fields) => { export const getFrozenRowCfgPivot = ( options: Pick< S2Options, - 'frozenFirstRowPivot' | 'pagination' | 'hierarchyType' | 'showSeriesNumber' + 'frozenFirstRow' | 'pagination' | 'hierarchyType' | 'showSeriesNumber' >, rowNodes: Node[], ): S2TableSheetOptions & { frozenRowHeight: number; enableFrozenFirstRow: boolean; } => { - const { pagination, frozenFirstRowPivot, hierarchyType, showSeriesNumber } = + const { pagination, frozenFirstRow, hierarchyType, showSeriesNumber } = options; const enablePagination = pagination && pagination.pageSize; let enableFrozenFirstRow = false; const headNode = rowNodes?.[0]; - if (!enablePagination && frozenFirstRowPivot) { + if (!enablePagination && frozenFirstRow) { // first node no children: entire row enableFrozenFirstRow = headNode?.children?.length === 0; const treeMode = hierarchyType === 'tree' || hierarchyType === 'customTree'; diff --git a/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts b/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts index f1b351d15c..5fe4305a07 100644 --- a/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts +++ b/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts @@ -1,7 +1,6 @@ import type { Event as CanvasEvent } from '@antv/g-canvas'; import { forEach } from 'lodash'; import type { DataCell } from '../../../cell/data-cell'; -import type { RowCell } from '../../../cell/row-cell'; import { InteractionStateName, InterceptType, @@ -56,9 +55,15 @@ export class DataCellClick extends BaseEvent implements BaseEventImplement { interaction.addIntercepts([InterceptType.HOVER]); if (interaction.isSelectedCell(cell)) { - // https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail,使用 detail属性来判断是否是双击,双击时不触发选择态reset + // https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail,使用 detail 属性来判断是否是双击,双击时不触发选择态 reset if ((event.originalEvent as UIEvent)?.detail === 1) { interaction.reset(); + + // https://github.com/antvis/S2/issues/2447 + this.spreadsheet.emit( + S2Event.GLOBAL_SELECTED, + interaction.getActiveCells(), + ); } return; } @@ -87,8 +92,8 @@ export class DataCellClick extends BaseEvent implements BaseEventImplement { meta, spreadsheet, ); - forEach(allRowHeaderCells, (cell: RowCell) => { - cell.updateByState(InteractionStateName.SELECTED); + forEach(allRowHeaderCells, (rowCell) => { + rowCell.updateByState(InteractionStateName.SELECTED); }); } } diff --git a/packages/s2-core/src/interaction/brush-selection/row-brush-selection.ts b/packages/s2-core/src/interaction/brush-selection/row-brush-selection.ts index 57e842c95d..91df80b66a 100644 --- a/packages/s2-core/src/interaction/brush-selection/row-brush-selection.ts +++ b/packages/s2-core/src/interaction/brush-selection/row-brush-selection.ts @@ -1,6 +1,6 @@ import type { Point } from '@antv/g-canvas'; import { isNil, last, map } from 'lodash'; -import { RowCell } from '../../cell'; +import { BaseRowCell, RowCell } from '../../cell'; import { InterceptType, S2Event } from '../../common/constant'; import { InteractionBrushSelectionStage, @@ -129,7 +129,7 @@ export class RowBrushSelection extends BaseBrushSelection { } // TODO: 先暂时不考虑自定义单元格的情况, next 分支把这些单元格 (包括自定义单元格) 都放在了 s2.options.rowCell 里 - return new RowCell(node, this.spreadsheet); + return this.spreadsheet.facet.rowHeader.createCellInstance(node); }); } diff --git a/packages/s2-core/src/interaction/data-cell-multi-selection.ts b/packages/s2-core/src/interaction/data-cell-multi-selection.ts index b1ae87c6c7..5b2249209a 100644 --- a/packages/s2-core/src/interaction/data-cell-multi-selection.ts +++ b/packages/s2-core/src/interaction/data-cell-multi-selection.ts @@ -86,7 +86,7 @@ export class DataCellMultiSelection event.stopPropagation(); const cell: DataCell = this.spreadsheet.getCell(event.target); const meta = cell.getMeta(); - const { interaction, options } = this.spreadsheet; + const { interaction } = this.spreadsheet; if (this.isMultiSelection && meta) { const selectedCells = this.getSelectedCells(cell); @@ -94,6 +94,10 @@ export class DataCellMultiSelection if (isEmpty(selectedCells)) { interaction.clearState(); this.spreadsheet.hideTooltip(); + this.spreadsheet.emit( + S2Event.GLOBAL_SELECTED, + interaction.getActiveCells(), + ); return; } diff --git a/packages/s2-core/src/interaction/event-controller.ts b/packages/s2-core/src/interaction/event-controller.ts index a1258710b1..1632255142 100644 --- a/packages/s2-core/src/interaction/event-controller.ts +++ b/packages/s2-core/src/interaction/event-controller.ts @@ -186,8 +186,12 @@ export class EventController { return; } - this.spreadsheet.emit(S2Event.GLOBAL_RESET, event); interaction.reset(); + this.spreadsheet.emit(S2Event.GLOBAL_RESET, event); + this.spreadsheet.emit( + S2Event.GLOBAL_SELECTED, + interaction.getActiveCells(), + ); } private isMouseEvent(event: Event): event is MouseEvent { @@ -299,7 +303,7 @@ export class EventController { if (this.isResizeArea(event)) { this.spreadsheet.emit(S2Event.LAYOUT_RESIZE_MOUSE_DOWN, event); - // 仅捕获在canvas之外触发的事件 https://github.com/antvis/S2/issues/1592 + // 仅捕获在 canvas 之外触发的事件 https://github.com/antvis/S2/issues/1592 const resizeMouseMoveCapture = (mouseEvent: MouseEvent) => { if (!this.spreadsheet.getCanvasElement()) { return false; diff --git a/packages/s2-react/README.md b/packages/s2-react/README.md index 43cd3837b0..0a2719872f 100644 --- a/packages/s2-react/README.md +++ b/packages/s2-react/README.md @@ -105,7 +105,7 @@ const s2DataConfig = { ```ts const s2Options = { width: 600, - height: 480, + height: 480 } ``` @@ -117,18 +117,17 @@ const s2Options = {
``` -```ts +```tsx +import ReactDOM from 'react-dom' import { SheetComponent } from '@antv/s2-react'; import '@antv/s2-react/dist/style.min.css'; -const container = document.getElementById('container'); - ReactDOM.render( , - document.getElementById('container'), + document.getElementById('container') ); ``` diff --git a/packages/s2-react/__tests__/data/strategy-data.ts b/packages/s2-react/__tests__/data/strategy-data.ts index 4a9f7a6d48..e63defac4b 100644 --- a/packages/s2-react/__tests__/data/strategy-data.ts +++ b/packages/s2-react/__tests__/data/strategy-data.ts @@ -389,6 +389,7 @@ export const StrategyOptions: SheetComponentOptions = { width: 800, height: 800, cornerText: '指标', + // frozenFirstRow: true, placeholder: (v) => { const placeholder = v?.fieldValue ? '-' : ''; return placeholder; diff --git a/packages/s2-react/playground/config.ts b/packages/s2-react/playground/config.ts index d4a2919710..4b89787962 100644 --- a/packages/s2-react/playground/config.ts +++ b/packages/s2-react/playground/config.ts @@ -80,6 +80,7 @@ export const s2Options: SheetComponentOptions = { debug: true, width: 600, height: 400, + frozenFirstRow: false, showSeriesNumber: false, interaction: { enableCopy: true, diff --git a/packages/s2-react/playground/index.tsx b/packages/s2-react/playground/index.tsx index 1fbb458750..290dcc68a4 100644 --- a/packages/s2-react/playground/index.tsx +++ b/packages/s2-react/playground/index.tsx @@ -837,6 +837,24 @@ function MainLayout() { }} disabled={sheetType === 'table'} /> + + { + updateOptions({ + frozenFirstRow: checked, + }); + }} + disabled={ + sheetType === 'table' || + (mergedOptions.hierarchyType === 'grid' && + (!mergedOptions?.totals?.row?.showGrandTotals || + !mergedOptions?.totals?.row?.reverseLayout)) + } + /> + ``` -​📊 查看 [React 版下钻 demo](/examples/react-component/drill-dwon#for-pivot) +​📊 查看 [React 版下钻 demo](/examples/react-component/drill-down#for-pivot) ## Vue 下钻组件 diff --git a/s2-site/docs/api/general/S2Options.zh.md b/s2-site/docs/api/general/S2Options.zh.md index 79a72b2f94..dc4049f4e0 100644 --- a/s2-site/docs/api/general/S2Options.zh.md +++ b/s2-site/docs/api/general/S2Options.zh.md @@ -32,9 +32,9 @@ const s2Options = { | style | [Style](#style) | | | 单元格样式设置,比如布局类型,宽高,边距,是否隐藏数值列头等 | | frozenRowCount | `number` | | | 冻结行的数量,从顶部开始计数 (明细表有效) | | frozenColCount | `number` | | | 冻结列的数量,从左侧开始计数 (明细表有效) | -| frozenTrailingRowCount | `number` | | | 冻结行数量,从底部开始计数 (明细表有效) | -| frozenTrailingColCount | `number` | | | 冻结列的数量,从右侧开始计数 (明细表有效) | -| frozenFirstRowPivot | `boolean` | | `false` | 首行不存在子节点时, 冻结首行, 适用于聚合模式总计置于顶部冻结总计行, 树状模式冻结首行等场景 (透视表有效) | `@antv/s2@1.53.0` | +| frozenTrailingRowCount | `number` | | | 冻结行数量,从底部开始计数(明细表有效) | +| frozenTrailingColCount | `number` | | | 冻结列的数量,从右侧开始计数(明细表有效) | +| frozenFirstRow | `boolean` | | `false` | 首行不存在子节点时,冻结首行(透视表有效), 适用于聚合模式总计置于顶部冻结总计行,树状模式冻结首行等场景。| `@antv/s2@^1.53.0` | | hdAdapter | `boolean` | | `true` | 是否开启高清屏适配,解决多屏切换,高清视网膜屏字体渲染模糊的问题。[查看更多](/manual/advanced/hd-adapter) | | mergedCellsInfo | [MergedCellInfo[][]](#mergedcellinfo) | | | 合并单元格信息 | | placeholder | `string \| (meta: Record) => string` | | | 空单元格的填充内容 | @@ -52,8 +52,8 @@ const s2Options = { | layoutDataPosition | [layoutDataPosition](#layoutdataposition) | | | 自定义数据 | | filterDisplayDataItem | [FilterDataItemCallback](#filterdataitemcallback) | | | 过滤数据 | | mappingDisplayDataItem | [MappingDataItemCallback](#mappingdataitemcallback) | | | 转换数据,用于 tooltip 显示 | -| dataSet | [DataSet](#dataset) | | | 自定义数据集 | -| supportCSSTransform | `boolean` | | `false` | 开启后支持 CSS transform, 解决父元素设置 `transform` 后,鼠标坐标响应不正确的问题 | +| dataSet | [DataSet](#dataset) | | | 自定义数据集 | +| supportCSSTransform | `boolean` | | `false` | 开启后支持 CSS transform, 解决父元素设置 `transform` 后,鼠标坐标响应不正确的问题 | | devicePixelRatio | `number` | | `window.devicePixelRatio` | 自定义设备像素比 | | frozenEntireHeadRowPivot | `boolean` | | `false` | 交叉表模式冻结首行(整行) | diff --git a/s2-site/docs/manual/basic/analysis/drill-down.zh.md b/s2-site/docs/manual/basic/analysis/drill-down.zh.md index 920aa4f718..8152efbf74 100644 --- a/s2-site/docs/manual/basic/analysis/drill-down.zh.md +++ b/s2-site/docs/manual/basic/analysis/drill-down.zh.md @@ -3,7 +3,6 @@ title: 维度下钻 order: 13 --- - S2 提供的「维度下钻」的能力,可以为你挖掘不同维度下更详细的数据,让你的数据洞察变得更清晰。 preview @@ -88,24 +87,25 @@ const PartDrillDown = { ```jsx import React from 'react'; -import ReactDOM from 'react-dom'; import { SheetComponent } from '@antv/s2-react'; +import '@antv/s2-react/dist/style.min.css'; const s2Options = { hierarchyType: 'tree', // 树形结构 }; -ReactDOM.render( - , - document.getElementById('container'), -); +const App = () => { + return ( + + ) +} ``` - + ## 使用场景 diff --git a/s2-site/docs/manual/basic/analysis/editable-mode.zh.md b/s2-site/docs/manual/basic/analysis/editable-mode.zh.md index f368ffc1ca..813be1d588 100644 --- a/s2-site/docs/manual/basic/analysis/editable-mode.zh.md +++ b/s2-site/docs/manual/basic/analysis/editable-mode.zh.md @@ -3,21 +3,25 @@ title: 编辑表 order: 3 --- -## 明细表简介 +## 简介 -编辑表是 `S2` 明细表的衍生形态之一。在提供完整的明细表的分析功能之外,还支持对数据的修改操作。 +编辑表是 `S2` 明细表的衍生形态之一,基于 `React` 版本的明细表封装,在提供完整的明细表的分析功能之外,还支持对数据的修改操作。 editable-mode ## 使用 +:::warning{title="注意"} +编辑表的原理本质上是在 `Canvas` 表格上增加一个 `div` 蒙层,来实现对数据的编辑,如果想在 `@antv/s2` 和 `@antv/s2-vue` 中使用,请自行参考 [React 版本的实现](https://github.com/antvis/S2/blob/b81b7957b9e8b8e1fbac9ebc6cacdf45a14e5412/packages/s2-react/src/components/sheets/editable-sheet/index.tsx#L7) 进行封装。 +::: + + + ```html
``` -### React 组件方式 - -```typescript +```tsx import React from "react"; import ReactDOM from "react-dom"; import { SheetComponent } from '@antv/s2-react'; @@ -160,17 +164,19 @@ const s2Options = { // 4, 渲染 ReactDOM.render( { + console.log('onDataCellEditEnd', meta); + }} />, document.getElementById('container') ); ``` -## 特性 +## 效果 -效果如图: -preview +[查看示例](/examples/react-component/sheet#editable) -[playground 地址](/examples/react-component/sheet#editable) +preview diff --git a/s2-site/docs/manual/basic/analysis/strategy.zh.md b/s2-site/docs/manual/basic/analysis/strategy.zh.md index 0ca2bdf96f..3be1512ec3 100644 --- a/s2-site/docs/manual/basic/analysis/strategy.zh.md +++ b/s2-site/docs/manual/basic/analysis/strategy.zh.md @@ -16,7 +16,7 @@ order: 9
点击查看趋势分析表 options 配置 -```js +```ts const s2Options = { width: 600, height: 480, @@ -56,21 +56,20 @@ const s2Options = {
-```ts +```tsx import React from "react"; -import ReactDOM from "react-dom"; import { SheetComponent } from "@antv/s2-react"; import '@antv/s2-react/dist/style.min.css'; -ReactDOM.render( - , - document.getElementById('container'), -); - +const App = () => { + return ( + + ) +} ``` diff --git a/s2-site/docs/manual/basic/analysis/switcher.zh.md b/s2-site/docs/manual/basic/analysis/switcher.zh.md index a4d604f42a..9a4e505757 100644 --- a/s2-site/docs/manual/basic/analysis/switcher.zh.md +++ b/s2-site/docs/manual/basic/analysis/switcher.zh.md @@ -32,18 +32,17 @@ const switcherFields = { ```js import React from "react"; -import ReactDOM from "react-dom"; import { Switcher } from "@antv/s2-react"; const onSubmit = (result) => { console.log("result:", result); }; -ReactDOM.render( - , - document.getElementById("container") -); - +const App = () => { + return ( + + ) +} ``` diff --git a/s2-site/docs/manual/basic/sheet-type/pivot-mode.en.md b/s2-site/docs/manual/basic/sheet-type/pivot-mode.en.md index 79ebcab4c8..f37806e1b5 100644 --- a/s2-site/docs/manual/basic/sheet-type/pivot-mode.en.md +++ b/s2-site/docs/manual/basic/sheet-type/pivot-mode.en.md @@ -102,11 +102,11 @@ pivotSheet.render(); Translation: Currently, only the ability to freeze the first row is provided, which is different from freezing rows and columns in a detail table. Due to the complex layout caused by the grouping feature in a pivot table, and to ensure reasonable interaction, the following limitations are in place: The first row does not have any child nodes (suitable for scenarios where the total is placed at the top or for tree-like structures). -Pagination scenarios are not currently supported. To enable freezing of the first row, set frozenFirstRowPivot in s2Options configuration. +Pagination scenarios are not currently supported. To enable freezing of the first row, set frozenFirstRow in s2Options configuration. ```ts const s2Options = { - frozenFirstRowPivot: boolean; + frozenFirstRow: boolean; totals: { row: { showGrandTotals: true, diff --git a/s2-site/docs/manual/basic/sheet-type/pivot-mode.zh.md b/s2-site/docs/manual/basic/sheet-type/pivot-mode.zh.md index 0cebeac18d..581bb302c6 100644 --- a/s2-site/docs/manual/basic/sheet-type/pivot-mode.zh.md +++ b/s2-site/docs/manual/basic/sheet-type/pivot-mode.zh.md @@ -83,7 +83,7 @@ ReactDOM.render( ​📊 查看 [React 版本透视表示例](/examples/react-component/sheet#pivot) 和 [API 文档](/api/components/sheet-component)。 -### 类方式 +### PivotSheet 类方式 如果不打算依赖 `React`,可以在上面第三步之后直接调用: @@ -97,23 +97,52 @@ s2.render(); ​📊 查看 [类方式透视表示例](/examples/basic/pivot#grid) 和 [API 文档](/api/general/s2options)。 +## 特性 + +### 展示形态 + +默认支持 [平铺模式](/zh/examples/basic/pivot/#grid) 和 [树状模式](/zh/examples/basic/pivot/#tree) 两种展示形态。 + +### 数据汇总 + +支持 [小计/总计](/manual/basic/totals) 的透视能力。 + +### 冻结行头 + +当行头固定时,行头会有一个独立的可滚动区域,如果关闭冻结行头,则滚动区域为整个表格。 + + + +
+ +```ts +const s2Options = { + frozenRowHeader: false, // 默认开启 +} +``` + +preview + ### 冻结首行 @antv/s2@^1.53.0 新增 -:::info{title=""} +:::info{title="注意"} -目前仅提供**冻结首行**能力,和[明细表行列冻结](https://s2.antv.antgroup.com/manual/basic/sheet-type/table-mode#%E8%A1%8C%E5%88%97%E5%86%BB%E7%BB%93)不同, 透视表由于带有分组的特性, 布局比较复杂, 考虑到交互合理性, 目前有如下限制 +目前仅提供**冻结首行**能力,和 [明细表行列冻结](https://s2.antv.antgroup.com/manual/basic/sheet-type/table-mode#%E8%A1%8C%E5%88%97%E5%86%BB%E7%BB%93) 不同,透视表由于带有分组的特性,布局比较复杂,考虑到交互合理性,目前有如下限制: -- 首行不存在子节点 (适用于总计置于顶部, 树状模式等场景)。 +- 首行不存在子节点(适用于总计置于顶部,只有单个维值,树状模式等场景)。 - 分页场景暂不支持。 -`s2Options` 中配置 `frozenFirstRowPivot` 开启首行冻结能力 + +`s2Options` 中配置 `frozenFirstRow` 开启首行冻结能力 ::: +#### 平铺模式 + ```ts const s2Options = { - // 是否开启冻结首行 - frozenFirstRowPivot: boolean; - // 平铺模式,需要开启行总计 & 位置置顶 + frozenFirstRow: true, + hierarchyType: 'grid', + // 需要开启行总计 & 总计行置于顶部 totals: { row: { showGrandTotals: true, @@ -123,10 +152,19 @@ const s2Options = { } ``` -#### 平铺模式 - +
+ #### 树状模式 +```ts +const s2Options = { + frozenFirstRow: true, + hierarchyType: 'tree', +} +``` + + +
diff --git a/s2-site/docs/manual/basic/sheet-type/table-mode.zh.md b/s2-site/docs/manual/basic/sheet-type/table-mode.zh.md index 2cbfb4e5ec..611c394559 100644 --- a/s2-site/docs/manual/basic/sheet-type/table-mode.zh.md +++ b/s2-site/docs/manual/basic/sheet-type/table-mode.zh.md @@ -9,7 +9,7 @@ order: 2 pivot-mode -明细表和透视表共享[基础交互](/manual/advanced/interaction/basic)、[主题](/manual/basic/theme) 、[复制](/manual/basic/analysis/export)、[自定义单元格](/manual/advanced/custom/hook) 等能力。除此之外,明细表还支持 [行列冻结](/examples/interaction/basic#froze) 等特色功能。在海量明细数据渲染场景下,明细表可以替换基于 `DOM` 的表格组件,来提升性能和用户体验。 +明细表和透视表共享 [基础交互](/manual/advanced/interaction/basic)、[主题](/manual/basic/theme) 、[复制](/manual/basic/analysis/export)、[自定义单元格](/manual/advanced/custom/hook) 等能力。除此之外,明细表还支持 [行列冻结](/examples/interaction/basic#froze) 等特色功能。在海量明细数据渲染场景下,明细表可以替换基于 `DOM` 的表格组件,来提升性能和用户体验。 ## 使用 @@ -101,11 +101,12 @@ s2.render(); ### 序号 -在 `s2Options` 中传入 `showSeriesNumber` 即可展示内置的序号。[查看 demo](/examples/basic/table#table) +在 `s2Options` 中传入 `showSeriesNumber` 即可展示内置的序号,可以自定义序号列标题。[查看 demo](/examples/basic/table#table) ```ts const s2Options = { - showSeriesNumber: true + showSeriesNumber: true, + seriesNumberText: '自定义序号标题' // 默认 "序号" } ``` @@ -113,6 +114,10 @@ const s2Options = { 行列冻结让特定行列在滚动时保持固定,从而一直保持在视口范围内,提供信息的对照和参考。[查看 demo](/examples/interaction/basic#frozen) + + +
+ 行列冻结通过在 `s2Options` 中传入这些属性控制: ```ts @@ -127,5 +132,3 @@ const s2Options = { 效果如图: preview - - diff --git a/s2-site/docs/manual/basic/sort/advanced.zh.md b/s2-site/docs/manual/basic/sort/advanced.zh.md index 5a89885de1..69e9130be0 100644 --- a/s2-site/docs/manual/basic/sort/advanced.zh.md +++ b/s2-site/docs/manual/basic/sort/advanced.zh.md @@ -11,15 +11,14 @@ order: 1 使用 `@antv/s2-react` 的 `SheetComponent` 组件 ,并给 `header` 配置 `advancedSortCfg` ,配置具体信息可查看 [AdvancedSortCfgProps](/docs/api/components/advanced-sort#advancedsortcfgprops) -```ts -import React, { useState } from 'react'; -import ReactDOM from 'react-dom'; +```tsx +import React from 'react'; import { SortParams } from '@antv/s2'; import { SheetComponent } from '@antv/s2-react'; import '@antv/s2-react/dist/style.min.css'; -const AdvancedSortDemo = () => { - const [dataCfg, setDataCfg] = useState(s2DataConfig); +export const AdvancedSortDemo = () => { + const [dataCfg, setDataCfg] = React.useState(s2DataConfig); return ( { /> ); }; - -ReactDOM.render(, document.getElementById('container')); - ``` ## 配置 ### 显示 -```ts -advancedSortCfg: { - open: true, -} +```tsx + ``` -row +row ### 提交 通过 `onSortConfirm` 函数透出所选规则数据 `ruleValues` 和处理成表可直接用的数据 `sortParams` ```ts -advancedSortCfg: { - open: true, - onSortConfirm: (ruleValues: RuleValue[], sortParams: SortParams) => { - console.log(ruleValues, sortParams) - }, -}, + { + console.log(ruleValues, sortParams) + } + }, + }} +/> ``` @@ -79,7 +83,7 @@ advancedSortCfg: { | 参数 | 说明 | 类型 | 默认值 | 必选 | | --------------- | ------------------ | ---------------------- | ------ | ---- | | className | class 类名称 | `string` | - | | -| icon | 排序按钮图标 | `React.ReactNode` | - | | +| icon | 排序按钮图标 | `ReactNode` | - | | | text | 排序按钮名称 | `ReactNode` | - | | | ruleText | 规则描述 | `string` | - | | @@ -107,7 +111,7 @@ advancedSortCfg: { 支持自定义规则配置列表,不配置默认为:`首字母、手动排序、其他字段` ->注意:如果这里自定义,则需在 onSortConfirm 中通过 ruleValues 自定义 sortParams +> 注意:如果这里自定义,则需在 onSortConfirm 中通过 ruleValues 自定义 sortParams | 属性 | 类型 | 必选 | 默认值 | 功能描述 | | ------- | ------------------------------------------ | --- | ----- | --------- | diff --git a/s2-site/docs/manual/basic/totals.zh.md b/s2-site/docs/manual/basic/totals.zh.md index cffaf101cb..4d07c9910f 100644 --- a/s2-site/docs/manual/basic/totals.zh.md +++ b/s2-site/docs/manual/basic/totals.zh.md @@ -5,7 +5,7 @@ order: 5 ## 简介 -小计总计属于表的透视功能,可以给行和列分别配置小计总计。 +小计总计属于表的透视功能,可以给行头和列头分别配置小计总计。 ### 小计 @@ -15,13 +15,17 @@ order: 5 平铺模式下,给当前维度额外增加一行/列 -row +row + +
#### 形式二:挂靠节点 树状模式下,挂靠到当前节点所在行/列中 -row +row + +
### 总计 @@ -33,35 +37,43 @@ order: 5 row +
+ 树状: -row +row + +
#### 2. 多度量值 平铺: -row +row + +
树状: -row +row + +
### 分组汇总 -按维度进行 小计/总计 的汇总计算,用于进行某一维度的数据对比分析等。 +按维度进行 `小计/总计` 的汇总计算,用于进行某一维度的数据对比分析等。 - +#### 行总计/行小计分组 -#### 行总计小计分组 + -行总计按 “类别” 分组,行小计按 “类别”,“子类别” 分组: +
-row +#### 列总计/列小计分组 -#### 列总计小计分组 + -col +
## 使用 @@ -75,8 +87,8 @@ object **必选**,_default:null_ 功能描述: 小计总计配置 | 参数 | 说明 | 类型 | 默认值 | 必选 | | ---- | ------ | --------------------------------------------- | ------ | ---- | -| row | 列总计 | [Total](/docs/api/general/S2Options#total) | {} | | -| col | 行总计 | [Total](/docs/api/general/S2Options#total) | {} | | +| row | 列总计 | [Total](/docs/api/general/S2Options#total) | - | | +| col | 行总计 | [Total](/docs/api/general/S2Options#total) | - | | #### Total @@ -106,7 +118,7 @@ const s2Options = { reverseSubLayout: true, subTotalsDimensions: ['province'], totalsGroupDimensions: ['city'], - subTotalsGroupDimensions: ['type', 'sub_type'], + subTotalsGroupDimensions: ['type', 'sub_type'], }, col: { showGrandTotals: true, @@ -123,36 +135,36 @@ const s2Options = { #### 1. 数据传入 -数据根据行/列位置以及 key 值传入,维度 key 值没有包含所有行、列的 key,举例如下: +数据根据行/列位置以及 `key` 值传入,维度 `key` 值没有包含所有行、列的 `key`,举例如下: ```typescript [ - // 总计/总计 - { - price: '15.5', - }, - // 浙江/总计 - { - province: '浙江', - price: '5.5', - }, - // 浙江-杭州/总计 - { - province: '浙江', - city: '杭州', - price: '3', - }, - // 总计/笔 - { - type: '笔', - price: '10', - }, - // 浙江-小计/笔 - { - province: "浙江", - type: "笔", - price: "3" - }, + // 总计/总计 + { + price: '15.5', + }, + // 浙江/总计 + { + province: '浙江', + price: '5.5', + }, + // 浙江-杭州/总计 + { + province: '浙江', + city: '杭州', + price: '3', + }, + // 总计/笔 + { + type: '笔', + price: '10', + }, + // 浙江-小计/笔 + { + province: "浙江", + type: "笔", + price: "3" + }, ] ``` @@ -174,7 +186,7 @@ const s2DataConfig = { }, ], ... -}; +} ``` ##### 方式二:传入 totalData @@ -194,87 +206,29 @@ const s2DataConfig = { { price: '15.5', }, - ], -}; + ] +} ``` #### 2. 计算出数据 可以给 `totals` 下的 `row` 、 `col` 分别配置属性 `calcTotals` 、 `calcSubTotals` 来实现计算汇总数据 -##### 1. 配置聚合方式 +##### 2.1. 配置聚合方式 -通过配置 `aggregation` 来实现,聚合方式目前支持 `SUM` (求和)、 `MIN` (最小值)、 `MAX` (最大值)和 `AVG` (算术平均) 。 +通过配置 `aggregation` 来实现,聚合方式目前支持 `SUM` (求和)、 `MIN` (最小值)、 `MAX` (最大值)和 `AVG` (算术平均)。 [查看示例](https://s2.antv.antgroup.com/zh/examples/analysis/totals/#calculate) -```ts -const s2Options = { - totals: { - row: { - showGrandTotals: true, - showSubTotals: true, - reverseLayout: true, - reverseSubLayout: true, - subTotalsDimensions: ['province'], - calcTotals: { - aggregation: 'SUM', - }, - calcSubTotals: { - aggregation: 'SUM', - }, - }, - col: { - showGrandTotals: true, - showSubTotals: true, - reverseLayout: true, - reverseSubLayout: true, - subTotalsDimensions: ['type'], - calcTotals: { - aggregation: 'SUM', - }, - calcSubTotals: { - aggregation: 'SUM', - }, - }, - }, -}; -``` + -##### 2. 配置自定义方法 +
-通过配置 `calcFunc: (query: Record, arr: Record[]) => number` 来实现 +##### 2.2. 配置自定义方法 -```ts -const s2Options = { - totals: { - row: { - showGrandTotals: true, - showSubTotals: true, - reverseLayout: true, - reverseSubLayout: true, - subTotalsDimensions: ['province'], - calcTotals: { - calcFunc: (query, data) => {}, - }, - calcSubTotals: { - calcFunc: (query, data) => {}, - }, - }, - col: { - showGrandTotals: true, - showSubTotals: true, - reverseLayout: true, - reverseSubLayout: true, - subTotalsDimensions: ['type'], - calcTotals: { - calcFunc: (query, data) => {}, - }, - calcSubTotals: { - calcFunc: (query, data) => {}, - }, - }, - }, -}; -``` +通过配置 `calcFunc: (query: Record, arr: Record[]) => number` 来实现。[查看示例](https://s2.antv.antgroup.com/zh/examples/analysis/totals/#custom) + + + +
### 优先级 diff --git a/s2-site/docs/manual/getting-started.zh.md b/s2-site/docs/manual/getting-started.zh.md index 4f23674fd9..b3ae12a29b 100644 --- a/s2-site/docs/manual/getting-started.zh.md +++ b/s2-site/docs/manual/getting-started.zh.md @@ -195,7 +195,7 @@ s2.render(); #### 表格组件使用 -```ts +```tsx import React from 'react'; import ReactDOM from 'react-dom'; import { SheetComponent } from '@antv/s2-react'; diff --git a/s2-site/examples/basic/pivot/demo/grid.ts b/s2-site/examples/basic/pivot/demo/grid.ts index bd3947c201..0dfa6ca497 100644 --- a/s2-site/examples/basic/pivot/demo/grid.ts +++ b/s2-site/examples/basic/pivot/demo/grid.ts @@ -10,7 +10,10 @@ fetch( const s2Options = { width: 600, height: 480, + // 冻结行头 + // frozenRowHeader: true }; + const s2 = new PivotSheet(container, dataCfg, s2Options); s2.render(); diff --git a/s2-site/examples/basic/pivot/demo/tree.ts b/s2-site/examples/basic/pivot/demo/tree.ts index 063f99c7f5..35e710662f 100644 --- a/s2-site/examples/basic/pivot/demo/tree.ts +++ b/s2-site/examples/basic/pivot/demo/tree.ts @@ -1,23 +1,31 @@ import { PivotSheet } from '@antv/s2'; fetch( - 'https://gw.alipayobjects.com/os/bmw-prod/2a5dbbc8-d0a7-4d02-b7c9-34f6ca63cff6.json', + 'https://gw.alipayobjects.com/os/bmw-prod/2a5dbbc8-d0a7-4d02-b7c9-34f6ca63cff6.json', ) - .then((res) => res.json()) - .then((dataCfg) => { - const container = document.getElementById('container'); + .then((res) => res.json()) + .then((dataCfg) => { + const container = document.getElementById('container'); - const s2Options = { - width: 600, - height: 480, - hierarchyType: 'tree', - style: { - collapsedRows: { - 'root[&]浙江省': true, // 折叠浙江省下面所有的城市 - } - } - }; - const s2 = new PivotSheet(container, dataCfg, s2Options); + const s2Options = { + width: 600, + height: 480, + hierarchyType: 'tree', + style: { + // 折叠全部 + // hierarchyCollapse: true, - s2.render(); - }); + // 折叠浙江省下面所有的城市 + collapsedRows: { + 'root[&]浙江省': true, + }, + }, + + // 冻结行头 + // frozenRowHeader: true + }; + + const s2 = new PivotSheet(container, dataCfg, s2Options); + + s2.render(); + }); diff --git a/s2-site/examples/basic/table/demo/table.ts b/s2-site/examples/basic/table/demo/table.ts index 3d2eb303d7..e522932e47 100644 --- a/s2-site/examples/basic/table/demo/table.ts +++ b/s2-site/examples/basic/table/demo/table.ts @@ -1,8 +1,6 @@ import { TableSheet } from '@antv/s2'; -fetch( - 'https://assets.antv.antgroup.com/s2/basic-table-mode.json', -) +fetch('https://assets.antv.antgroup.com/s2/basic-table-mode.json') .then((res) => res.json()) .then((data) => { const container = document.getElementById('container'); @@ -39,7 +37,9 @@ fetch( width: 600, height: 480, showSeriesNumber: true, + // seriesNumberText: '自定义序号标题', }; + const s2 = new TableSheet(container, s2DataConfig, s2Options); s2.render(); diff --git a/s2-site/examples/interaction/advanced/demo/frozen-pivot-grid.ts b/s2-site/examples/interaction/advanced/demo/frozen-pivot-grid.ts index b5f352bf28..4c44b20818 100644 --- a/s2-site/examples/interaction/advanced/demo/frozen-pivot-grid.ts +++ b/s2-site/examples/interaction/advanced/demo/frozen-pivot-grid.ts @@ -10,7 +10,7 @@ fetch( const s2Options = { width: 600, height: 300, - frozenFirstRowPivot: true, + frozenFirstRow: true, totals: { row: { showGrandTotals: true, diff --git a/s2-site/examples/interaction/advanced/demo/frozen-pivot-tree.ts b/s2-site/examples/interaction/advanced/demo/frozen-pivot-tree.ts index b5bd94ae5f..2f3b6cc674 100644 --- a/s2-site/examples/interaction/advanced/demo/frozen-pivot-tree.ts +++ b/s2-site/examples/interaction/advanced/demo/frozen-pivot-tree.ts @@ -11,7 +11,7 @@ fetch( width: 600, height: 300, hierarchyType: 'tree', - frozenFirstRowPivot: true, + frozenFirstRow: true, }; const s2 = new PivotSheet(container, dataCfg, s2Options); diff --git a/s2-site/examples/interaction/basic/demo/frozen-row-header.ts b/s2-site/examples/interaction/basic/demo/frozen-row-header.ts new file mode 100644 index 0000000000..e51fb43708 --- /dev/null +++ b/s2-site/examples/interaction/basic/demo/frozen-row-header.ts @@ -0,0 +1,38 @@ +import { PivotSheet, S2Event, S2Options } from '@antv/s2'; + +fetch( + 'https://gw.alipayobjects.com/os/bmw-prod/2a5dbbc8-d0a7-4d02-b7c9-34f6ca63cff6.json', +) + .then((res) => res.json()) + .then((dataCfg) => { + const container = document.getElementById('container'); + + const s2Options: S2Options = { + width: 600, + height: 480, + hierarchyType: 'tree', // 'tree' | 'grid' + // 默认开启行头冻结, 关闭后滚动区域为整个表格 + frozenRowHeader: true, + style: { + rowCfg: { + treeRowsWidth: 400, + width: 200, + }, + colCfg: { + width: 200, + }, + }, + }; + + const s2 = new PivotSheet(container, dataCfg, s2Options); + + s2.on(S2Event.GLOBAL_SCROLL, (e) => { + console.log('scroll', e); + }); + + s2.on(S2Event.ROW_CELL_SCROLL, (e) => { + console.log('row cell scroll', e); + }); + + s2.render(); + }); diff --git a/s2-site/examples/interaction/basic/demo/meta.json b/s2-site/examples/interaction/basic/demo/meta.json index f3a6593ce6..3be5a6e3d1 100644 --- a/s2-site/examples/interaction/basic/demo/meta.json +++ b/s2-site/examples/interaction/basic/demo/meta.json @@ -63,11 +63,19 @@ { "filename": "frozen.ts", "title": { - "zh": "行列冻结", - "en": "Freeze Rows And Cols" + "zh": "明细表 - 行列冻结", + "en": "TableSheet - Frozen Rows And Cols" }, "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/%24a16EsOCR8/frozeb.gif" }, + { + "filename": "frozen-row-header.ts", + "title": { + "zh": "透视表 - 行头冻结", + "en": "PivotSheet - Frozen Row Header" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*kk0ETbbbnOsAAAAAAAAAAAAADmJ7AQ/original" + }, { "filename": "auto-reset-sheet-style.ts", "title": { diff --git a/s2-site/examples/react-component/drill-dwon/API.en.md b/s2-site/examples/react-component/drill-down/API.en.md similarity index 100% rename from s2-site/examples/react-component/drill-dwon/API.en.md rename to s2-site/examples/react-component/drill-down/API.en.md diff --git a/s2-site/examples/react-component/drill-dwon/API.zh.md b/s2-site/examples/react-component/drill-down/API.zh.md similarity index 100% rename from s2-site/examples/react-component/drill-dwon/API.zh.md rename to s2-site/examples/react-component/drill-down/API.zh.md diff --git a/s2-site/examples/react-component/drill-dwon/demo/basic-panel.tsx b/s2-site/examples/react-component/drill-down/demo/basic-panel.tsx similarity index 100% rename from s2-site/examples/react-component/drill-dwon/demo/basic-panel.tsx rename to s2-site/examples/react-component/drill-down/demo/basic-panel.tsx diff --git a/s2-site/examples/react-component/drill-dwon/demo/for-pivot.tsx b/s2-site/examples/react-component/drill-down/demo/for-pivot.tsx similarity index 100% rename from s2-site/examples/react-component/drill-dwon/demo/for-pivot.tsx rename to s2-site/examples/react-component/drill-down/demo/for-pivot.tsx diff --git a/s2-site/examples/react-component/drill-dwon/demo/meta.json b/s2-site/examples/react-component/drill-down/demo/meta.json similarity index 100% rename from s2-site/examples/react-component/drill-dwon/demo/meta.json rename to s2-site/examples/react-component/drill-down/demo/meta.json diff --git a/s2-site/examples/react-component/drill-dwon/index.en.md b/s2-site/examples/react-component/drill-down/index.en.md similarity index 100% rename from s2-site/examples/react-component/drill-dwon/index.en.md rename to s2-site/examples/react-component/drill-down/index.en.md diff --git a/s2-site/examples/react-component/drill-dwon/index.zh.md b/s2-site/examples/react-component/drill-down/index.zh.md similarity index 100% rename from s2-site/examples/react-component/drill-dwon/index.zh.md rename to s2-site/examples/react-component/drill-down/index.zh.md