Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ jest.mock('@/sheet-type', () => {
emit: jest.fn(),
isHierarchyTreeType: jest.fn(),
isHierarchyGridTreeType: jest.fn(),
isHierarchyGridTreeColType: jest.fn().mockReturnValue(false),
facet: {
getFreezeCornerDiffWidth: jest.fn(),
getColLeafNodes: jest.fn().mockReturnValue([]),
Expand Down
1 change: 1 addition & 0 deletions packages/s2-core/__tests__/util/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ export const createFakeSpreadSheet = (config?: {
s2.getCell = jest.fn();
s2.isHierarchyTreeType = jest.fn();
s2.isHierarchyGridTreeType = jest.fn();
s2.isHierarchyGridTreeColType = jest.fn().mockReturnValue(false);
s2.getCanvasElement = () =>
s2.container.getContextService().getDomElement() as any;
s2.getCanvasConfig = () => s2.container.getConfig();
Expand Down
124 changes: 123 additions & 1 deletion packages/s2-core/src/cell/col-cell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ import type { FrozenFacet } from '../facet';
import { Frame } from '../facet/header/frame';
import { type ColHeaderConfig } from '../facet/header/interface';
import {
getCellBoxByType,
getHorizontalTextIconPosition,
getVerticalIconPosition,
getVerticalTextPosition,
} from '../utils/cell/cell';
import { adjustTextIconPositionWhileScrolling } from '../utils/cell/text-scrolling';
import { renderIcon, renderLine } from '../utils/g-renders';
import { renderIcon, renderLine, renderTreeIcon } from '../utils/g-renders';
import { batchSetStyle } from '../utils/g-utils';
import {
getHiddenColumnContinuousSiblingNodes,
Expand All @@ -43,6 +44,7 @@ import {
getResizeAreaAttrs,
shouldAddResizeArea,
} from '../utils/interaction/resize';
import { isMobile } from '../utils/is-mobile';
import { normalizeTextAlign } from '../utils/normalize';
import { HeaderCell } from './header-cell';

Expand All @@ -59,6 +61,51 @@ export class ColCell extends HeaderCell<ColHeaderConfig> {
return [CellBorderPosition.TOP, CellBorderPosition.RIGHT];
}

/**
* column grid-tree 模式下,折叠的节点需要跨越子维度行形成合并单元格
* 通过重写 getBBoxByType 实现视觉上的跨行效果,不影响全局布局
*/
public getBBoxByType(type = CellClipBox.BORDER_BOX): SimpleBBox {
// 只在 column grid-tree 模式下的折叠节点才需要跨行
if (
!this.spreadsheet.isHierarchyGridTreeColType() ||
!this.meta.isCollapsed
) {
return super.getBBoxByType(type);
}

// 获取列头层级信息,计算需要跨越的高度
const { hierarchy } = this.meta;
const sampleNodes = hierarchy?.sampleNodesForAllLevels || [];
let spanHeight = 0;

// 从当前层级到最大层级的所有行高之和
for (let i = this.meta.level; i <= (hierarchy?.maxLevel ?? 0); i++) {
const levelSample = sampleNodes[i];

spanHeight += levelSample?.height ?? 0;
}

// 构造扩展后的 BORDER_BOX(保持 x, y 不变,只改变 height)
const expandedBorderBox: SimpleBBox = {
x: this.meta.x,
y: this.meta.y,
width: this.meta.width,
height: spanHeight || this.meta.height,
};

// 使用 getCellBoxByType 处理 padding/border,确保 CONTENT_BOX 正确计算
const cellStyle = (this.getStyle() ||
this.theme.dataCell) as DefaultCellTheme;

return getCellBoxByType(
expandedBorderBox,
this.getBorderPositions(),
cellStyle?.cell!,
type,
);
}

protected initCell() {
super.initCell();
// 1、draw rect background
Expand All @@ -74,6 +121,8 @@ export class ColCell extends HeaderCell<ColHeaderConfig> {
protected afterDrawText() {
// 绘制字段标记 -- icon
this.drawActionAndConditionIcons();
// 绘制树状模式收起展开的 icon (grid-tree 模式)
this.drawTreeIcon();
// draw borders
this.drawBorders();
// draw resize ares
Expand Down Expand Up @@ -117,6 +166,79 @@ export class ColCell extends HeaderCell<ColHeaderConfig> {
return false;
}

/**
* 是否显示列头树状模式的展开/折叠图标 (grid-tree 模式)
*/
protected showTreeIcon() {
// 只有 column grid-tree 模式需要显示展开/折叠图标
if (!this.spreadsheet.isHierarchyGridTreeColType()) {
return false;
}

// 已折叠的节点需要显示展开图标
if (this.meta.isCollapsed) {
return true;
}

// 未折叠的非叶子节点显示折叠图标
return !this.meta.isLeaf;
}

private onTreeIconClick() {
const { device } = this.spreadsheet.options;

if (isMobile(device)) {
return;
}

this.emitCollapseEvent();
}

private emitCollapseEvent() {
this.spreadsheet.emit(S2Event.COL_CELL_COLLAPSED__PRIVATE, {
isCollapsed: !this.meta.isCollapsed,
node: this.meta,
});
}

protected drawTreeIcon() {
if (!this.showTreeIcon()) {
return;
}

const { isCollapsed } = this.meta;
const { x } = this.getBBoxByType(CellClipBox.CONTENT_BOX);
const { fill } = this.getTextStyle();
const { size } = this.getStyle()!.icon!;

const iconX = x;
const iconY = this.getIconPosition().y;

this.treeIcon = renderTreeIcon({
group: this,
iconCfg: {
x: iconX,
y: iconY,
width: size,
height: size,
fill,
},
isCollapsed,
onClick: () => {
this.onTreeIconClick();
},
});

// 移动端, 点击热区为整个单元格
const { device } = this.spreadsheet.options;

if (isMobile(device)) {
this.addMobileTouchListener(() => {
this.emitCollapseEvent();
});
}
}

/**
* 计算文本位置时候需要,留给后代根据情况(固定列)覆盖
* @param viewport
Expand Down
6 changes: 6 additions & 0 deletions packages/s2-core/src/common/constant/events/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ export enum S2Event {
COL_CELL_HIDDEN = 'col-cell:hidden',
COL_CELL_RENDER = 'col-cell:render',
COL_CELL_SELECTED = 'col-cell:selected',
COL_CELL_COLLAPSED = 'col-cell:collapsed',
COL_CELL_ALL_COLLAPSED = 'col-cell:all-collapsed',

// 列头内部通信 event
COL_CELL_COLLAPSED__PRIVATE = 'col-cell:collapsed__private',
COL_CELL_ALL_COLLAPSED__PRIVATE = 'col-cell:all-collapsed__private',

/** ================ Data Cell ================ */
DATA_CELL_HOVER = 'data-cell:hover',
Expand Down
8 changes: 7 additions & 1 deletion packages/s2-core/src/common/interface/collapse.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import type { Node } from '../../facet/layout/node';
import type { RowCellStyle } from './style';
import type { ColCellStyle, RowCellStyle } from './style';

export type RowCellCollapsedParams = {
isCollapsed: boolean;
node: Node;
collapseFields?: RowCellStyle['collapseFields'];
};

export type ColCellCollapsedParams = {
isCollapsed: boolean;
node: Node;
collapseFields?: ColCellStyle['collapseFields'];
};
5 changes: 5 additions & 0 deletions packages/s2-core/src/common/interface/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { InteractionName, S2Event } from '../../common/constant';
import type {
CellMeta,
CellScrollPosition,
ColCellCollapsedParams,
Data,
FilterParam,
HiddenColumnsInfo,
Expand Down Expand Up @@ -154,6 +155,10 @@ export interface EmitterType {
) => void;
[S2Event.COL_CELL_RENDER]: (cell: ColCell) => void;
[S2Event.COL_CELL_SELECTED]: CellSelectedHandler;
[S2Event.COL_CELL_COLLAPSED]: (data: ColCellCollapsedParams) => void;
[S2Event.COL_CELL_COLLAPSED__PRIVATE]: (data: ColCellCollapsedParams) => void;
[S2Event.COL_CELL_ALL_COLLAPSED]: (isCollapsed: boolean) => void;
[S2Event.COL_CELL_ALL_COLLAPSED__PRIVATE]: (isCollapsed: boolean) => void;

/** ================ Corner Cell ================ */
[S2Event.CORNER_CELL_MOUSE_MOVE]: CanvasEventHandler;
Expand Down
8 changes: 8 additions & 0 deletions packages/s2-core/src/common/interface/s2Options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,14 @@ export interface S2PivotSheetOptions {
*/
hierarchyType?: HierarchyType;

/**
* 列头布局类型
* - grid: 平铺网格 (默认)
* - grid-tree: 树状平铺(平铺布局 + 展开折叠)
* @description 列头仅支持 grid 和 grid-tree 模式
*/
columnHierarchyType?: 'grid' | 'grid-tree';

/**
* 小计/总计配置
* @see https://s2.antv.antgroup.com/manual/basic/totals
Expand Down
20 changes: 20 additions & 0 deletions packages/s2-core/src/common/interface/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,26 @@ export interface ColCellStyle extends BaseCellStyle, CellTextWordWrapStyle {
* 数值挂列头时, 是否隐藏数值 (即 s2DataConfig.fields.values 只有一个数值时生效)
*/
hideValue?: boolean;

/**
* 收起所有列头节点 (grid-tree 模式生效)
* @description 优先级 `collapseFields` > `expandDepth` > `collapseAll`
*/
collapseAll?: boolean | null;

/**
* 折叠列头节点 (grid-tree 模式生效)
* id 级别: { ['root[&]家具']: true } 即 家具 对应的节点会被折叠
* field 级别: { type: true } : 即 所有 type 对应的维值都会被折叠
* @description 优先级 `collapseFields` > `expandDepth` > `collapseAll`
*/
collapseFields?: Record<string, boolean> | null;

/**
* 列头节点默认展开到第几层 (从 0 开始, grid-tree 模式生效)
* @description 优先级 `collapseFields` > `expandDepth` > `collapseAll`
*/
expandDepth?: number | null;
}

export interface CornerCellStyle extends CellTextWordWrapStyle {}
Expand Down
5 changes: 5 additions & 0 deletions packages/s2-core/src/data-set/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ export interface GetCellDataParams {
*/
rowNode?: Node;

/**
* 列头节点,用于 column grid-tree 模式
*/
colNode?: Node;

/**
* 是否是行头
*/
Expand Down
18 changes: 15 additions & 3 deletions packages/s2-core/src/data-set/pivot-data-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,13 @@ export class PivotDataSet extends BaseDataSet {
}

public getCellData(params: GetCellDataParams): ViewMetaData | undefined {
const { query = {}, rowNode, isTotals = false, totalStatus } = params || {};
const {
query = {},
rowNode,
colNode,
isTotals = false,
totalStatus,
} = params || {};

const { rows: originRows, columns } = this.fields;
let rows = originRows;
Expand Down Expand Up @@ -490,9 +496,15 @@ export class PivotDataSet extends BaseDataSet {

// grid-tree 模式下折叠的节点,使用小计聚合逻辑计算数据
// 折叠节点没有原始数据,需要聚合其子节点的数据
const isGridTree = this.spreadsheet?.isHierarchyGridTreeType();
const isRowGridTree = this.spreadsheet?.isHierarchyGridTreeType();
const isColGridTree = this.spreadsheet?.isHierarchyGridTreeColType();

if (isRowGridTree && rowNode?.isCollapsed) {
return this.getTotalValue(query, totalStatus);
}

if (isGridTree && rowNode?.isCollapsed) {
// column grid-tree 模式下折叠的列节点,使用小计聚合逻辑
if (isColGridTree && colNode?.isCollapsed) {
return this.getTotalValue(query, totalStatus);
}
}
Expand Down
Loading
Loading