Skip to content

Commit 6836cc8

Browse files
committed
[IMP] pivot: add tabular form
This commit adds a new setting for pivot: whether to use tabular form. In tabular form, each level of row header is displayed in a separate column. Task: 4794334
1 parent 49d5142 commit 6836cc8

File tree

13 files changed

+441
-44
lines changed

13 files changed

+441
-44
lines changed

packages/o-spreadsheet-engine/src/functions/module_lookup.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -906,14 +906,19 @@ export const PIVOT = {
906906
"include_measure_titles (boolean, default=TRUE)",
907907
_t("Whether to include the measure titles row or not.")
908908
),
909+
arg(
910+
"tabular_form (boolean, default=FALSE)",
911+
_t("Whether to display the pivot in a tabular form.")
912+
),
909913
],
910914
compute: function (
911915
pivotFormulaId: Maybe<FunctionResultObject>,
912916
rowCount: Maybe<FunctionResultObject>,
913917
includeTotal: Maybe<FunctionResultObject>,
914918
includeColumnHeaders: Maybe<FunctionResultObject>,
915919
columnCount: Maybe<FunctionResultObject>,
916-
includeMeasureTitles: Maybe<FunctionResultObject>
920+
includeMeasureTitles: Maybe<FunctionResultObject>,
921+
tabularForm: Maybe<FunctionResultObject>
917922
) {
918923
const _pivotFormulaId = toString(pivotFormulaId);
919924
const pivotId = getPivotId(_pivotFormulaId, this.getters);
@@ -927,6 +932,7 @@ export const PIVOT = {
927932
includeColumnHeaders,
928933
columnCount,
929934
includeMeasureTitles,
935+
tabularForm,
930936
this.locale
931937
);
932938

@@ -961,7 +967,10 @@ export const PIVOT = {
961967
if (tableHeight === 0) {
962968
return [[{ value: pivotTitle }]];
963969
}
964-
const tableWidth = Math.min(1 + pivotStyle.numberOfColumns, cells.length);
970+
const tableWidth = Math.min(
971+
table.getRowHeadersWidth(pivotStyle) + pivotStyle.numberOfColumns,
972+
cells.length
973+
);
965974
const result: Matrix<FunctionResultObject> = [];
966975
for (const col of range(0, tableWidth)) {
967976
result[col] = [];
@@ -981,10 +990,16 @@ export const PIVOT = {
981990
case "VALUE":
982991
result[col].push(pivot.getPivotCellValueAndFormat(pivotCell.measure, pivotCell.domain));
983992
break;
993+
case "ROW_GROUP_NAME":
994+
result[col].push(pivot.getPivotRowGroupName(pivotCell.groupByIndex));
995+
break;
984996
}
985997
}
986998
}
987-
if (pivotStyle.displayColumnHeaders || pivotStyle.displayMeasuresRow) {
999+
if (
1000+
(pivotStyle.displayColumnHeaders || pivotStyle.displayMeasuresRow) &&
1001+
cells[0][0].type === "EMPTY"
1002+
) {
9881003
result[0][0] = { value: pivotTitle };
9891004
}
9901005
return result;

packages/o-spreadsheet-engine/src/helpers/pivot/pivot_helpers.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const DEFAULT_PIVOT_STYLE: Required<PivotStyle> = {
4040
displayMeasuresRow: true,
4141
numberOfRows: Number.MAX_VALUE,
4242
numberOfColumns: Number.MAX_VALUE,
43+
tabularForm: false,
4344
};
4445

4546
const AGGREGATOR_NAMES = {
@@ -463,6 +464,7 @@ export function getPivotStyleFromFnArgs(
463464
includeColumnHeadersArg: Maybe<FunctionResultObject | CellValue>,
464465
columnCountArg: Maybe<FunctionResultObject | CellValue>,
465466
includeMeasuresRowArg: Maybe<FunctionResultObject | CellValue>,
467+
tabularFormArg: Maybe<FunctionResultObject | CellValue>,
466468
locale: Locale
467469
): Required<PivotStyle> {
468470
const style = definition.style;
@@ -489,5 +491,17 @@ export function getPivotStyleFromFnArgs(
489491
? toBoolean(includeMeasuresRowArg)
490492
: style?.displayMeasuresRow ?? DEFAULT_PIVOT_STYLE.displayMeasuresRow;
491493

492-
return { numberOfRows, numberOfColumns, displayTotals, displayColumnHeaders, displayMeasuresRow };
494+
const tabularForm =
495+
tabularFormArg !== undefined
496+
? toBoolean(tabularFormArg)
497+
: style?.tabularForm ?? DEFAULT_PIVOT_STYLE.tabularForm;
498+
499+
return {
500+
numberOfRows,
501+
numberOfColumns,
502+
displayTotals,
503+
displayColumnHeaders,
504+
displayMeasuresRow,
505+
tabularForm,
506+
};
493507
}

packages/o-spreadsheet-engine/src/helpers/pivot/spreadsheet_pivot/spreadsheet_pivot.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,11 @@ export class SpreadsheetPivot implements Pivot<SpreadsheetPivotRuntimeDefinition
263263
return finalCell;
264264
}
265265

266+
getPivotRowGroupName(groupByIndex: number): FunctionResultObject {
267+
const rowDimension = this.definition.rows[groupByIndex];
268+
return { value: rowDimension?.displayName || "" };
269+
}
270+
266271
getPivotCellValueAndFormat(measureId: string, domain: PivotDomain): FunctionResultObject {
267272
const dataEntries = this.filterDataEntriesFromDomain(this.dataEntries, domain);
268273
if (dataEntries.length === 0) {

packages/o-spreadsheet-engine/src/helpers/pivot/table_spreadsheet_pivot.ts

Lines changed: 83 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ interface CollapsiblePivotTableColumn extends PivotTableColumn {
6464
export class SpreadsheetPivotTable {
6565
readonly columns: CollapsiblePivotTableColumn[][];
6666
rows: PivotTableRow[];
67+
readonly numberOfRowGroupings: number;
6768
readonly measures: string[];
6869
readonly fieldsType: Record<string, string | undefined>;
6970
readonly maxIndent: number;
@@ -87,9 +88,7 @@ export class SpreadsheetPivotTable {
8788
columns = this.removeCollapsedColumns(columns, measures, collapsedDomains.COL);
8889
}
8990
this.columns = columns.map((cols) => {
90-
// offset in the pivot table
91-
// starts at 1 because the first column is the row title
92-
let offset = 1;
91+
let offset = 0;
9392
return cols.map((col) => {
9493
col = { ...col, offset };
9594
offset += col.width;
@@ -98,6 +97,7 @@ export class SpreadsheetPivotTable {
9897
});
9998

10099
this.rows = rows.filter((row) => !this.isParentCollapsed(collapsedDomains.ROW, row));
100+
this.numberOfRowGroupings = Math.max(...rows.map((row) => row.fields.length));
101101
this.maxIndent = Math.max(...this.rows.map((row) => row.indent));
102102
this.rowTree = lazy(() => this.buildRowsTree());
103103
this.colTree = lazy(() => this.buildColumnsTree());
@@ -159,13 +159,23 @@ export class SpreadsheetPivotTable {
159159

160160
private getSkippedRows(pivotStyle: Required<PivotStyle>) {
161161
const skippedRows: Set<number> = new Set();
162+
const colHeadersHeight = this.getColHeadersHeight();
162163
if (!pivotStyle.displayColumnHeaders) {
163-
for (let i = 0; i < this.columns.length - 1; i++) {
164+
for (let i = 0; i < colHeadersHeight - 1; i++) {
164165
skippedRows.add(i);
165166
}
166167
}
167168
if (!pivotStyle.displayMeasuresRow) {
168-
skippedRows.add(this.columns.length - 1);
169+
skippedRows.add(colHeadersHeight - 1);
170+
}
171+
// Skip sub-total rows in tabular form
172+
if (pivotStyle.tabularForm) {
173+
for (let i = 0; i < this.rows.length; i++) {
174+
const indent = this.rows[i].indent;
175+
if (indent !== 0 && indent !== this.maxIndent) {
176+
skippedRows.add(i + colHeadersHeight);
177+
}
178+
}
169179
}
170180
return skippedRows;
171181
}
@@ -176,8 +186,8 @@ export class SpreadsheetPivotTable {
176186
const { displayTotals } = pivotStyle;
177187
const numberOfDataRows = this.rows.length;
178188
const numberOfDataColumns = this.getNumberOfDataColumns();
179-
let pivotHeight = this.columns.length + numberOfDataRows;
180-
let pivotWidth = 1 /*(row headers)*/ + numberOfDataColumns;
189+
let pivotHeight = numberOfDataRows + this.getColHeadersHeight();
190+
let pivotWidth = numberOfDataColumns + this.getRowHeadersWidth(pivotStyle);
181191
if (!displayTotals && numberOfDataRows !== 1) {
182192
pivotHeight -= 1;
183193
}
@@ -192,7 +202,10 @@ export class SpreadsheetPivotTable {
192202
if (skippedRows.has(row)) {
193203
continue;
194204
}
195-
domainArray[col].push(this.getPivotCell(col, row, displayTotals));
205+
const cell = pivotStyle.tabularForm
206+
? this.getTabularFormPivotCell(col, row, pivotStyle)
207+
: this.getPivotCell(col, row, pivotStyle);
208+
domainArray[col].push(cell);
196209
}
197210
}
198211
this.pivotCells[key] = domainArray;
@@ -212,38 +225,72 @@ export class SpreadsheetPivotTable {
212225
return this.rows[index].indent !== this.maxIndent;
213226
}
214227

215-
private getPivotCell(col: number, row: number, includeTotal = true): PivotTableCell {
216-
const colHeadersHeight = this.columns.length;
217-
if (col > 0 && row === colHeadersHeight - 1) {
218-
const domain = this.getColHeaderDomain(col, row);
228+
private getPivotCell(col: number, row: number, pivotStyle: Required<PivotStyle>): PivotTableCell {
229+
const colHeadersHeight = this.getColHeadersHeight();
230+
const rowHeadersWidth = this.getRowHeadersWidth(pivotStyle);
231+
232+
const isColHeader = row < colHeadersHeight - 1 && col >= rowHeadersWidth;
233+
const isMeasureHeader = row === colHeadersHeight - 1 && col >= rowHeadersWidth;
234+
const isRowHeader = row > colHeadersHeight - 1 && col < rowHeadersWidth;
235+
const isPivotValue = row > colHeadersHeight - 1 && col >= rowHeadersWidth;
236+
237+
if (isMeasureHeader) {
238+
const colIndex = col - rowHeadersWidth;
239+
const domain = this.getColHeaderDomain(colIndex, row);
219240
if (!domain) {
220241
return EMPTY_PIVOT_CELL;
221242
}
222243
const measure = domain.at(-1)?.value?.toString() || "";
223244
return { type: "MEASURE_HEADER", domain: domain.slice(0, -1), measure };
224-
} else if (row <= colHeadersHeight - 1) {
225-
const domain = this.getColHeaderDomain(col, row);
245+
} else if (isColHeader) {
246+
const colIndex = col - rowHeadersWidth;
247+
const domain = this.getColHeaderDomain(colIndex, row);
226248
return domain ? { type: "HEADER", domain, dimension: "COL" } : EMPTY_PIVOT_CELL;
227-
} else if (col === 0) {
249+
} else if (isRowHeader) {
228250
const rowIndex = row - colHeadersHeight;
229251
const domain = this.getDomain(this.rows[rowIndex]);
230252
return { type: "HEADER", domain, dimension: "ROW" };
231-
} else {
253+
} else if (isPivotValue) {
232254
const rowIndex = row - colHeadersHeight;
233-
if (!includeTotal && this.isTotalRow(rowIndex)) {
255+
const colIndex = col - rowHeadersWidth;
256+
if (!pivotStyle.displayTotals && this.isTotalRow(rowIndex)) {
234257
return EMPTY_PIVOT_CELL;
235258
}
236-
const domain = [...this.getDomain(this.rows[rowIndex]), ...this.getColDomain(col)];
237-
const measure = this.getColMeasure(col);
259+
const domain = [...this.getDomain(this.rows[rowIndex]), ...this.getColDomain(colIndex)];
260+
const measure = this.getColMeasure(colIndex);
238261
return { type: "VALUE", domain, measure };
239262
}
263+
264+
return EMPTY_PIVOT_CELL;
240265
}
241266

242-
private getColHeaderDomain(col: number, row: number) {
243-
if (col === 0) {
244-
return undefined;
267+
private getTabularFormPivotCell(
268+
col: number,
269+
row: number,
270+
pivotStyle: Required<PivotStyle>
271+
): PivotTableCell {
272+
const colHeadersHeight = this.getColHeadersHeight();
273+
const rowHeadersWidth = this.getRowHeadersWidth(pivotStyle);
274+
275+
const isRowHeader = row > colHeadersHeight - 1 && col < rowHeadersWidth;
276+
const isRowGroupName = row === colHeadersHeight - 1 && col < rowHeadersWidth;
277+
278+
if (isRowHeader) {
279+
const rowIndex = row - colHeadersHeight;
280+
const domain = this.getDomain(this.rows[rowIndex]).slice(0, col + 1);
281+
if (domain.length === 0 && col !== 0) {
282+
return EMPTY_PIVOT_CELL;
283+
}
284+
return { type: "HEADER", domain, dimension: "ROW" };
285+
} else if (isRowGroupName) {
286+
return { type: "ROW_GROUP_NAME", groupByIndex: col };
245287
}
246-
const pivotCol = this.columns[row].find((pivotCol) => pivotCol.offset === col);
288+
289+
return this.getPivotCell(col, row, pivotStyle);
290+
}
291+
292+
private getColHeaderDomain(colIndex: number, row: number) {
293+
const pivotCol = this.columns[row].find((pivotCol) => pivotCol.offset === colIndex);
247294
if (!pivotCol || pivotCol.collapsedHeader) {
248295
return undefined;
249296
}
@@ -273,20 +320,28 @@ export class SpreadsheetPivotTable {
273320
});
274321
}
275322

276-
private getColDomain(col: number) {
277-
const domain = this.getColHeaderDomain(col, this.columns.length - 1);
323+
private getColDomain(colIndex: number) {
324+
const domain = this.getColHeaderDomain(colIndex, this.getColHeadersHeight() - 1);
278325
return domain ? domain.slice(0, -1) : []; // slice: remove measure and value
279326
}
280327

281-
private getColMeasure(col: number) {
282-
const domain = this.getColHeaderDomain(col, this.columns.length - 1);
328+
private getColMeasure(colIndex: number) {
329+
const domain = this.getColHeaderDomain(colIndex, this.getColHeadersHeight() - 1);
283330
const measure = domain?.at(-1)?.value;
284331
if (measure === undefined || measure === null) {
285332
throw new Error("Measure is missing");
286333
}
287334
return measure.toString();
288335
}
289336

337+
getRowHeadersWidth(pivotStyle: Required<PivotStyle>) {
338+
return pivotStyle.tabularForm ? this.numberOfRowGroupings : 1;
339+
}
340+
341+
private getColHeadersHeight() {
342+
return this.columns.length;
343+
}
344+
290345
buildRowsTree(): DimensionTree {
291346
const tree: DimensionTree = [];
292347
let depth = 0;
@@ -406,7 +461,7 @@ export class SpreadsheetPivotTable {
406461
}
407462

408463
getColumnDomainsAtDepth(depth: number) {
409-
if (depth < 0 || depth >= this.columns.length - 1) {
464+
if (depth < 0 || depth >= this.getColHeadersHeight() - 1) {
410465
return [];
411466
}
412467
return this.columns[depth].map((col) => this.getDomain(col)).filter((d) => d.length);

packages/o-spreadsheet-engine/src/plugins/ui_core_views/pivot_ui.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ export class PivotUIPlugin extends CoreViewPlugin {
226226
toScalar(args[3]),
227227
toScalar(args[4]),
228228
toScalar(args[5]),
229+
toScalar(args[6]),
229230
this.getters.getLocale()
230231
);
231232
const pivotCells = pivot.getCollapsedTableStructure().getPivotCells(pivotStyle);
@@ -292,7 +293,12 @@ export class PivotUIPlugin extends CoreViewPlugin {
292293
getPivotCellSortDirection(position: CellPosition): SortDirection | "none" | undefined {
293294
const pivotId = this.getters.getPivotIdFromPosition(position);
294295
const pivotCell = this.getters.getPivotCellFromPosition(position);
295-
if (pivotCell.type === "EMPTY" || pivotCell.type === "HEADER" || !pivotId) {
296+
if (
297+
pivotCell.type === "EMPTY" ||
298+
pivotCell.type === "HEADER" ||
299+
pivotCell.type === "ROW_GROUP_NAME" ||
300+
!pivotId
301+
) {
296302
return undefined;
297303
}
298304
const pivot = this.getters.getPivot(pivotId);

packages/o-spreadsheet-engine/src/registries/icons_on_cell_registry.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
PIVOT_INDENT,
1919
} from "../constants";
2020
import { deepEquals } from "../helpers/misc";
21-
import { togglePivotCollapse } from "../helpers/pivot/pivot_helpers";
21+
import { DEFAULT_PIVOT_STYLE, togglePivotCollapse } from "../helpers/pivot/pivot_helpers";
2222
import { computeTextFontSizeInPixels } from "../helpers/text_helper";
2323
import { Getters } from "../types/getters";
2424
import { ImageSVG } from "../types/image";
@@ -139,14 +139,15 @@ iconsOnCellRegistry.add("conditional_formatting", (getters, position) => {
139139
});
140140

141141
iconsOnCellRegistry.add("pivot_collapse", (getters, position) => {
142-
if (!getters.isSpillPivotFormula(position)) {
142+
const pivotId = getters.getPivotIdFromPosition(position);
143+
if (!getters.isSpillPivotFormula(position) || !pivotId) {
143144
return undefined;
144145
}
145146
const pivotCell = getters.getPivotCellFromPosition(position);
146-
const pivotId = getters.getPivotIdFromPosition(position);
147+
const definition = getters.getPivotCoreDefinition(pivotId);
148+
const tabularForm = definition.style?.tabularForm ?? DEFAULT_PIVOT_STYLE.tabularForm;
147149

148-
if (pivotCell.type === "HEADER" && pivotId && pivotCell.domain.length) {
149-
const definition = getters.getPivotCoreDefinition(pivotId);
150+
if (!tabularForm && pivotCell.type === "HEADER" && pivotId && pivotCell.domain.length) {
150151
const isDashboard = getters.isDashboard();
151152

152153
const fields = pivotCell.dimension === "COL" ? definition.columns : definition.rows;

packages/o-spreadsheet-engine/src/types/pivot.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ export interface PivotHeaderCell {
156156
dimension: Dimension;
157157
}
158158

159+
export interface PivotGroupNameCell {
160+
type: "ROW_GROUP_NAME";
161+
groupByIndex: number;
162+
}
163+
159164
export interface PivotMeasureHeaderCell {
160165
type: "MEASURE_HEADER";
161166
domain: PivotDomain;
@@ -174,6 +179,7 @@ export interface PivotEmptyCell {
174179

175180
export type PivotTableCell =
176181
| PivotHeaderCell
182+
| PivotGroupNameCell
177183
| PivotMeasureHeaderCell
178184
| PivotValueCell
179185
| PivotEmptyCell;
@@ -246,4 +252,5 @@ export interface PivotStyle {
246252
displayTotals?: boolean;
247253
displayColumnHeaders?: boolean;
248254
displayMeasuresRow?: boolean;
255+
tabularForm?: boolean;
249256
}

packages/o-spreadsheet-engine/src/types/pivot_runtime.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface Pivot<T = PivotRuntimeDefinition> {
2626
getPivotHeaderValueAndFormat(domain: PivotDomain): FunctionResultObject;
2727
getPivotCellValueAndFormat(measure: string, domain: PivotDomain): FunctionResultObject;
2828
getPivotMeasureValue(measure: string, domain: PivotDomain): FunctionResultObject;
29+
getPivotRowGroupName(groupByIndex: number): FunctionResultObject;
2930

3031
getMeasure: (id: string) => PivotMeasure;
3132

0 commit comments

Comments
 (0)