Documentation
Pivot Tables
Turn flat rows into a matrix — row fields on the left, column fields as dynamic headers, values aggregated in each cell.
- 1
Define the field catalog
Pass source fields incolumns— labels, types, and formatters for the fact data. While pivot is active, the grid shows generated headers instead of this catalog as columns.TypeScriptCopyconst headers: ReactColumnDef[] = [{ accessor: "region", label: "Region", type: "string" },{ accessor: "quarter", label: "Quarter", type: "string" },{ accessor: "sales", label: "Sales", type: "number" },];TypeScriptCopyconst headers: VueColumnDef[] = [{ accessor: "region", label: "Region", type: "string" },{ accessor: "quarter", label: "Quarter", type: "string" },{ accessor: "sales", label: "Sales", type: "number" },];TypeScriptCopyconst headers: AngularColumnDef[] = [{ accessor: "region", label: "Region", type: "string" },{ accessor: "quarter", label: "Quarter", type: "string" },{ accessor: "sales", label: "Sales", type: "number" },];TypeScriptCopyconst headers: SvelteColumnDef[] = [{ accessor: "region", label: "Region", type: "string" },{ accessor: "quarter", label: "Quarter", type: "string" },{ accessor: "sales", label: "Sales", type: "number" },];TypeScriptCopyconst headers: SolidColumnDef[] = [{ accessor: "region", label: "Region", type: "string" },{ accessor: "quarter", label: "Quarter", type: "string" },{ accessor: "sales", label: "Sales", type: "number" },];TypeScriptCopyconst headers: ColumnDef[] = [{ accessor: "region", label: "Region", type: "string" },{ accessor: "quarter", label: "Quarter", type: "string" },{ accessor: "sales", label: "Sales", type: "number" },]; - 2
Provide flat rows
Use a flat fact table — one object per measure row, not a pre-nested tree.TypeScriptCopyconst flatRows = [{ region: "North", quarter: "Q1", sales: 120000 },{ region: "North", quarter: "Q2", sales: 145000 },{ region: "South", quarter: "Q1", sales: 98000 },];TypeScriptCopyconst flatRows = [{ region: "North", quarter: "Q1", sales: 120000 },{ region: "North", quarter: "Q2", sales: 145000 },{ region: "South", quarter: "Q1", sales: 98000 },];TypeScriptCopyconst flatRows = [{ region: "North", quarter: "Q1", sales: 120000 },{ region: "North", quarter: "Q2", sales: 145000 },{ region: "South", quarter: "Q1", sales: 98000 },];TypeScriptCopyconst flatRows = [{ region: "North", quarter: "Q1", sales: 120000 },{ region: "North", quarter: "Q2", sales: 145000 },{ region: "South", quarter: "Q1", sales: 98000 },];TypeScriptCopyconst flatRows = [{ region: "North", quarter: "Q1", sales: 120000 },{ region: "North", quarter: "Q2", sales: 145000 },{ region: "South", quarter: "Q1", sales: 98000 },];TypeScriptCopyconst flatRows = [{ region: "North", quarter: "Q1", sales: 120000 },{ region: "North", quarter: "Q2", sales: 145000 },{ region: "South", quarter: "Q1", sales: 98000 },]; - 3
Set pivot
Configurepivotwith row dims, column dims, and at least one value. ConsumerrowGroupingis ignored while pivot is on.React TSXCopy<SimpleTablecolumns={headers}rows={flatRows}pivot={{rows: ["region"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" } }],}}/>Vue SFCCopy<script setup>const pivotConfig = {rows: ["region"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" } }],};</script><template><SimpleTable:columns="headers":rows="flatRows":pivot="pivotConfig"/></template>AngularCopypivotConfig = {rows: ["region"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" } }],};<simple-table[columns]="headers"[rows]="flatRows"[pivot]="pivotConfig"></simple-table>SvelteCopy<SimpleTablecolumns={headers}rows={flatRows}pivot={{rows: ["region"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" } }],}}/>Solid TSXCopy<SimpleTablecolumns={headers}rows={flatRows()}pivot={{rows: ["region"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" } }],}}/>TypeScriptCopynew SimpleTableVanilla(container, {columns: headers,rows: flatRows,pivot: {rows: ["region"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" } }],},});
Patterns
Multiple row dimensions
Multiple pivot.rows fields produce one flat row per combination (e.g. region × product), not an expand/collapse tree.
{rows: ["region", "product"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" } }],}
{rows: ["region", "product"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" } }],}
{rows: ["region", "product"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" } }],}
{rows: ["region", "product"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" } }],}
{rows: ["region", "product"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" } }],}
{rows: ["region", "product"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" } }],}
Pivot Panel (column editor)
Set enablePivotPanel (with enableColumnEditor) to compose Available / Rows / Columns / Values in the side panel. The panel drives setPivot — pivot is active when Values has at least one measure.
enableColumnEditor: true,enablePivotPanel: true,
enableColumnEditor: true,enablePivotPanel: true,
enableColumnEditor: true,enablePivotPanel: true,
enableColumnEditor: true,enablePivotPanel: true,
enableColumnEditor: true,enablePivotPanel: true,
enableColumnEditor: true,enablePivotPanel: true,
Multiple measures
Add more entries to values. Optional label overrides the header. Aggregation types match aggregate functions.
{rows: ["channel"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" }, label: "Sales" },{ accessor: "units", aggregation: { type: "sum" }, label: "Units" },],}
{rows: ["channel"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" }, label: "Sales" },{ accessor: "units", aggregation: { type: "sum" }, label: "Units" },],}
{rows: ["channel"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" }, label: "Sales" },{ accessor: "units", aggregation: { type: "sum" }, label: "Units" },],}
{rows: ["channel"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" }, label: "Sales" },{ accessor: "units", aggregation: { type: "sum" }, label: "Units" },],}
{rows: ["channel"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" }, label: "Sales" },{ accessor: "units", aggregation: { type: "sum" }, label: "Units" },],}
{rows: ["channel"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" }, label: "Sales" },{ accessor: "units", aggregation: { type: "sum" }, label: "Units" },],}
Values only (no column dims)
Set columns: [] to group and aggregate without a matrix of dynamic headers.
{rows: ["region", "category"],columns: [],values: [{ accessor: "sales", aggregation: { type: "sum" } },{ accessor: "cost", aggregation: { type: "sum" } },],}
{rows: ["region", "category"],columns: [],values: [{ accessor: "sales", aggregation: { type: "sum" } },{ accessor: "cost", aggregation: { type: "sum" } },],}
{rows: ["region", "category"],columns: [],values: [{ accessor: "sales", aggregation: { type: "sum" } },{ accessor: "cost", aggregation: { type: "sum" } },],}
{rows: ["region", "category"],columns: [],values: [{ accessor: "sales", aggregation: { type: "sum" } },{ accessor: "cost", aggregation: { type: "sum" } },],}
{rows: ["region", "category"],columns: [],values: [{ accessor: "sales", aggregation: { type: "sum" } },{ accessor: "cost", aggregation: { type: "sum" } },],}
{rows: ["region", "category"],columns: [],values: [{ accessor: "sales", aggregation: { type: "sum" } },{ accessor: "cost", aggregation: { type: "sum" } },],}
Totals
showRowTotals, showColumnTotals, and showGrandTotal default to true. Turn them off as needed.
{rows: ["country"],columns: ["category"],values: [{ accessor: "sales", aggregation: { type: "average" } }],showColumnTotals: false,}
{rows: ["country"],columns: ["category"],values: [{ accessor: "sales", aggregation: { type: "average" } }],showColumnTotals: false,}
{rows: ["country"],columns: ["category"],values: [{ accessor: "sales", aggregation: { type: "average" } }],showColumnTotals: false,}
{rows: ["country"],columns: ["category"],values: [{ accessor: "sales", aggregation: { type: "average" } }],showColumnTotals: false,}
{rows: ["country"],columns: ["category"],values: [{ accessor: "sales", aggregation: { type: "average" } }],showColumnTotals: false,}
{rows: ["country"],columns: ["category"],values: [{ accessor: "sales", aggregation: { type: "average" } }],showColumnTotals: false,}
Programmatic pivot
Call setPivot to enable or update, getPivot to read the active config, or pass null to return to the source grid. onPivotChange fires for API-driven updates.
tableRef.current?.setPivot({rows: ["region"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" } }],});const active = tableRef.current?.getPivot();tableRef.current?.setPivot(null); // back to source grid
tableRef.value?.setPivot({rows: ["region"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" } }],});const active = tableRef.value?.getPivot();tableRef.value?.setPivot(null); // back to source grid
this.tableRef.getAPI()?.setPivot({rows: ["region"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" } }],});const active = this.tableRef.getAPI()?.getPivot();this.tableRef.getAPI()?.setPivot(null); // back to source grid
tableRef.getAPI()?.setPivot({rows: ["region"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" } }],});const active = tableRef.getAPI()?.getPivot();tableRef.getAPI()?.setPivot(null); // back to source grid
tableRef.setPivot({rows: ["region"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" } }],});const active = tableRef.getPivot();tableRef.setPivot(null); // back to source grid
table.setPivot({rows: ["region"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" } }],});const active = table.getPivot();table.setPivot(null); // back to source grid
Example
1import { useState } from "react";2import { SimpleTable } from "@simple-table/react";3import type { PivotConfig, Theme } from "@simple-table/react";4import { pivotDemoConfig } from "./pivot.demo-data";5import "@simple-table/react/styles.css";67const INITIAL_PIVOT: PivotConfig = {8 rows: ["region", "product"],9 columns: ["quarter"],10 values: [{ accessor: "sales", aggregation: { type: "sum" } }],11};1213const PivotDemo = ({14 height = "500px",15 theme,16}: {17 height?: string | number;18 theme?: Theme;19}) => {20 const [pivotEnabled, setPivotEnabled] = useState(true);21 const [pivot, setPivot] = useState<PivotConfig | null>(INITIAL_PIVOT);2223 const handlePivotEnabledChange = (enabled: boolean) => {24 setPivotEnabled(enabled);25 if (enabled && pivot === null) {26 setPivot(INITIAL_PIVOT);27 }28 };2930 return (31 <div>32 <label33 style={{34 display: "flex",35 alignItems: "center",36 gap: 8,37 marginBottom: 12,38 fontSize: 14,39 color: "#374151",40 }}41 >42 <input43 type="checkbox"44 checked={pivotEnabled}45 onChange={(e) => handlePivotEnabledChange(e.target.checked)}46 aria-label="Pivot mode"47 />48 Pivot mode49 </label>50 <SimpleTable51 columns={pivotDemoConfig.headers}52 rows={pivotDemoConfig.rows}53 autoExpandColumns54 columnResizing55 enableColumnEditor56 enableColumnEditorInitOpen57 enablePivotPanel={pivotEnabled}58 height={height}59 pivot={pivotEnabled ? pivot : null}60 onPivotChange={setPivot}61 selectableCells62 theme={theme}63 getRowId={({ row }) => (row?.id == null ? undefined : String(row.id))}64 />65 </div>66 );67};6869export default PivotDemo;707172// pivot.demo-data.ts73import type { PivotConfig, ReactColumnDef } from "@simple-table/react";7475export interface PivotFact {76 id: string;77 region: string;78 country: string;79 category: string;80 product: string;81 channel: string;82 year: number;83 quarter: string;84 sales: number;85 units: number;86 cost: number;87}8889export const pivotHeaders: ReactColumnDef<PivotFact>[] = [90 { accessor: "region", label: "Region", width: 110, type: "string" },91 { accessor: "country", label: "Country", width: 100, type: "string" },92 { accessor: "category", label: "Category", width: 110, type: "string" },93 { accessor: "product", label: "Product", width: 120, type: "string" },94 { accessor: "channel", label: "Channel", width: 100, type: "string" },95 { accessor: "year", label: "Year", width: 80, type: "number" },96 { accessor: "quarter", label: "Quarter", width: 80, type: "string" },97 {98 accessor: "sales",99 label: "Sales",100 width: 100,101 type: "number",102 align: "right",103 valueFormatter: ({ value }) =>104 typeof value === "number" ? `$${value.toLocaleString()}` : "",105 },106 {107 accessor: "units",108 label: "Units",109 width: 80,110 type: "number",111 align: "right",112 },113 {114 accessor: "cost",115 label: "Cost",116 width: 100,117 type: "number",118 align: "right",119 valueFormatter: ({ value }) =>120 typeof value === "number" ? `$${value.toLocaleString()}` : "",121 },122];123124const COUNTRIES = {125 West: ["USA", "Canada"],126 East: ["USA", "UK"],127 North: ["Canada", "Sweden"],128 South: ["Brazil", "Australia"],129};130const PRODUCTS = {131 Hardware: ["Widget", "Gadget", "Sensor"],132 Software: ["License", "Subscription"],133};134const CHANNELS = ["Direct", "Partner", "Online"];135const YEARS = [2024, 2025];136const QUARTERS = ["Q1", "Q2", "Q3", "Q4"];137138/** Sparse multi-dimension fact cube (~150–250 rows). */139export function generatePivotRows(): PivotFact[] {140 const rows: PivotFact[] = [];141 let id = 1;142 for (const [region, countries] of Object.entries(COUNTRIES)) {143 for (const country of countries) {144 for (const [category, products] of Object.entries(PRODUCTS)) {145 for (const product of products) {146 for (const channel of CHANNELS) {147 for (const year of YEARS) {148 for (const quarter of QUARTERS) {149 // Sparse facts — skip ~1/3 of combinations150 if ((id + year + quarter.charCodeAt(1) + channel.length) % 5 !== 0) {151 id++;152 continue;153 }154 const base = 40 + ((id * 17) % 90);155 rows.push({156 id: `r${id}`,157 region,158 country,159 category,160 product,161 channel,162 year,163 quarter,164 sales: base * 100,165 units: base,166 cost: Math.round(base * 55),167 });168 id++;169 }170 }171 }172 }173 }174 }175 }176 return rows;177}178179export const pivotRows: PivotFact[] = generatePivotRows();180181export type PivotPreset = {182 id: string;183 label: string;184 pivot: PivotConfig;185};186187export const pivotPresets: PivotPreset[] = [188 {189 id: "region-quarter",190 label: "Region × Quarter",191 pivot: {192 rows: ["region"],193 columns: ["quarter"],194 values: [{ accessor: "sales", aggregation: { type: "sum" } }],195 },196 },197 {198 id: "multi-rows",199 label: "Region × Product",200 pivot: {201 rows: ["region", "product"],202 columns: ["quarter"],203 values: [{ accessor: "sales", aggregation: { type: "sum" } }],204 },205 },206 {207 id: "category-year-quarter",208 label: "Category × Year → Quarter",209 pivot: {210 rows: ["category"],211 columns: ["year", "quarter"],212 values: [{ accessor: "sales", aggregation: { type: "sum" } }],213 },214 },215 {216 id: "channel-quarter",217 label: "Channel × Quarter",218 pivot: {219 rows: ["channel"],220 columns: ["quarter"],221 values: [222 { accessor: "sales", aggregation: { type: "sum" }, label: "Sales" },223 { accessor: "units", aggregation: { type: "sum" }, label: "Units" },224 ],225 },226 },227 {228 id: "country-category",229 label: "Country × Category",230 pivot: {231 rows: ["country"],232 columns: ["category"],233 values: [{ accessor: "sales", aggregation: { type: "average" } }],234 showColumnTotals: false,235 },236 },237 {238 id: "values-only",239 label: "Values only",240 pivot: {241 rows: ["region", "category"],242 columns: [],243 values: [244 { accessor: "sales", aggregation: { type: "sum" } },245 { accessor: "cost", aggregation: { type: "sum" } },246 ],247 },248 },249];250251export const pivotConfig: PivotConfig = pivotPresets[0].pivot;252253export const pivotDemoConfig = {254 headers: pivotHeaders,255 rows: pivotRows,256 tableProps: { pivot: pivotConfig },257 presets: pivotPresets,258};259
1<template>2 <div>3 <label4 style="5 display: flex;6 align-items: center;7 gap: 8px;8 margin-bottom: 12px;9 font-size: 14px;10 color: #374151;11 "12 >13 <input14 type="checkbox"15 :checked="pivotEnabled"16 aria-label="Pivot mode"17 @change="onPivotEnabledChange(($event.target as HTMLInputElement).checked)"18 />19 Pivot mode20 </label>21 <SimpleTable22 :columns="pivotDemoConfig.headers"23 :rows="pivotDemoConfig.rows"24 :pivot="pivotEnabled ? pivot : null"25 :onPivotChange="(next) => (pivot = next)"26 :auto-expand-columns="true"27 :column-resizing="true"28 :enable-column-editor="true"29 :enable-column-editor-init-open="true"30 :enable-pivot-panel="pivotEnabled"31 :height="height"32 :selectable-cells="true"33 :theme="theme"34 :get-row-id="getRowId"35 />36 </div>37</template>3839<script setup lang="ts">40import { ref } from "vue";41import { SimpleTable } from "@simple-table/vue";42import type { GetRowIdParams, PivotConfig, Theme } from "@simple-table/vue";43import { pivotDemoConfig } from "./pivot.demo-data";44import type { PivotFact } from "./pivot.demo-data";45import "@simple-table/vue/styles.css";4647withDefaults(defineProps<{ height?: string | number; theme?: Theme }>(), {48 height: "500px",49});5051const INITIAL_PIVOT: PivotConfig = {52 rows: ["region", "product"],53 columns: ["quarter"],54 values: [{ accessor: "sales", aggregation: { type: "sum" } }],55};5657const pivotEnabled = ref(true);58const pivot = ref<PivotConfig | null>(INITIAL_PIVOT);5960const onPivotEnabledChange = (enabled: boolean) => {61 pivotEnabled.value = enabled;62 if (enabled && pivot.value === null) {63 pivot.value = INITIAL_PIVOT;64 }65};6667const getRowId = ({ row }: GetRowIdParams<PivotFact>) =>68 row?.id == null ? undefined : String(row.id);69</script>707172// pivot.demo-data.ts73import type { PivotConfig, VueColumnDef } from "@simple-table/vue";7475export interface PivotFact {76 id: string;77 region: string;78 country: string;79 category: string;80 product: string;81 channel: string;82 year: number;83 quarter: string;84 sales: number;85 units: number;86 cost: number;87}8889export const pivotHeaders: VueColumnDef<PivotFact>[] = [90 { accessor: "region", label: "Region", width: 110, type: "string" },91 { accessor: "country", label: "Country", width: 100, type: "string" },92 { accessor: "category", label: "Category", width: 110, type: "string" },93 { accessor: "product", label: "Product", width: 120, type: "string" },94 { accessor: "channel", label: "Channel", width: 100, type: "string" },95 { accessor: "year", label: "Year", width: 80, type: "number" },96 { accessor: "quarter", label: "Quarter", width: 80, type: "string" },97 {98 accessor: "sales",99 label: "Sales",100 width: 100,101 type: "number",102 align: "right",103 valueFormatter: ({ value }) =>104 typeof value === "number" ? `$${value.toLocaleString()}` : "",105 },106 {107 accessor: "units",108 label: "Units",109 width: 80,110 type: "number",111 align: "right",112 },113 {114 accessor: "cost",115 label: "Cost",116 width: 100,117 type: "number",118 align: "right",119 valueFormatter: ({ value }) =>120 typeof value === "number" ? `$${value.toLocaleString()}` : "",121 },122];123124const COUNTRIES = {125 West: ["USA", "Canada"],126 East: ["USA", "UK"],127 North: ["Canada", "Sweden"],128 South: ["Brazil", "Australia"],129};130const PRODUCTS = {131 Hardware: ["Widget", "Gadget", "Sensor"],132 Software: ["License", "Subscription"],133};134const CHANNELS = ["Direct", "Partner", "Online"];135const YEARS = [2024, 2025];136const QUARTERS = ["Q1", "Q2", "Q3", "Q4"];137138/** Sparse multi-dimension fact cube (~150–250 rows). */139export function generatePivotRows(): PivotFact[] {140 const rows: PivotFact[] = [];141 let id = 1;142 for (const [region, countries] of Object.entries(COUNTRIES)) {143 for (const country of countries) {144 for (const [category, products] of Object.entries(PRODUCTS)) {145 for (const product of products) {146 for (const channel of CHANNELS) {147 for (const year of YEARS) {148 for (const quarter of QUARTERS) {149 // Sparse facts — skip ~1/3 of combinations150 if ((id + year + quarter.charCodeAt(1) + channel.length) % 5 !== 0) {151 id++;152 continue;153 }154 const base = 40 + ((id * 17) % 90);155 rows.push({156 id: `r${id}`,157 region,158 country,159 category,160 product,161 channel,162 year,163 quarter,164 sales: base * 100,165 units: base,166 cost: Math.round(base * 55),167 });168 id++;169 }170 }171 }172 }173 }174 }175 }176 return rows;177}178179export const pivotRows: PivotFact[] = generatePivotRows();180181export type PivotPreset = {182 id: string;183 label: string;184 pivot: PivotConfig;185};186187export const pivotPresets: PivotPreset[] = [188 {189 id: "region-quarter",190 label: "Region × Quarter",191 pivot: {192 rows: ["region"],193 columns: ["quarter"],194 values: [{ accessor: "sales", aggregation: { type: "sum" } }],195 },196 },197 {198 id: "multi-rows",199 label: "Region × Product",200 pivot: {201 rows: ["region", "product"],202 columns: ["quarter"],203 values: [{ accessor: "sales", aggregation: { type: "sum" } }],204 },205 },206 {207 id: "category-year-quarter",208 label: "Category × Year → Quarter",209 pivot: {210 rows: ["category"],211 columns: ["year", "quarter"],212 values: [{ accessor: "sales", aggregation: { type: "sum" } }],213 },214 },215 {216 id: "channel-quarter",217 label: "Channel × Quarter",218 pivot: {219 rows: ["channel"],220 columns: ["quarter"],221 values: [222 { accessor: "sales", aggregation: { type: "sum" }, label: "Sales" },223 { accessor: "units", aggregation: { type: "sum" }, label: "Units" },224 ],225 },226 },227 {228 id: "country-category",229 label: "Country × Category",230 pivot: {231 rows: ["country"],232 columns: ["category"],233 values: [{ accessor: "sales", aggregation: { type: "average" } }],234 showColumnTotals: false,235 },236 },237 {238 id: "values-only",239 label: "Values only",240 pivot: {241 rows: ["region", "category"],242 columns: [],243 values: [244 { accessor: "sales", aggregation: { type: "sum" } },245 { accessor: "cost", aggregation: { type: "sum" } },246 ],247 },248 },249];250251export const pivotConfig: PivotConfig = pivotPresets[0].pivot;252253export const pivotDemoConfig = {254 headers: pivotHeaders,255 rows: pivotRows,256 tableProps: { pivot: pivotConfig },257 presets: pivotPresets,258};259
1import { Component, Input } from "@angular/core";2import { SimpleTableComponent } from "@simple-table/angular";3import type {4 AngularColumnDef,5 GetRowIdParams,6 PivotConfig,7 Theme,8} from "@simple-table/angular";9import { pivotDemoConfig } from "./pivot.demo-data";10import type { PivotFact } from "./pivot.demo-data";11import "@simple-table/angular/styles.css";1213const INITIAL_PIVOT: PivotConfig = {14 rows: ["region", "product"],15 columns: ["quarter"],16 values: [{ accessor: "sales", aggregation: { type: "sum" } }],17};1819@Component({20 selector: "pivot-demo",21 standalone: true,22 imports: [SimpleTableComponent],23 template: `24 <div>25 <label26 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 14px; color: #374151"27 >28 <input29 type="checkbox"30 [checked]="pivotEnabled"31 aria-label="Pivot mode"32 (change)="onPivotEnabledChange($any($event.target).checked)"33 />34 Pivot mode35 </label>36 <simple-table37 [getRowId]="getRowId"38 [rows]="rows"39 [columns]="headers"40 [pivot]="pivotEnabled ? pivot : null"41 [onPivotChange]="onPivotChange"42 [autoExpandColumns]="true"43 [columnResizing]="true"44 [enableColumnEditor]="true"45 [enableColumnEditorInitOpen]="true"46 [enablePivotPanel]="pivotEnabled"47 [height]="height"48 [selectableCells]="true"49 [theme]="theme"50 ></simple-table>51 </div>52 `,53})54export class PivotDemoComponent {55 @Input() height: string | number = "500px";56 @Input() theme?: Theme;5758 readonly rows: PivotFact[] = pivotDemoConfig.rows;59 readonly headers: AngularColumnDef<PivotFact>[] = pivotDemoConfig.headers;6061 pivotEnabled = true;62 pivot: PivotConfig | null = INITIAL_PIVOT;6364 onPivotEnabledChange(enabled: boolean): void {65 this.pivotEnabled = enabled;66 if (enabled && this.pivot === null) {67 this.pivot = INITIAL_PIVOT;68 }69 }7071 onPivotChange = (next: PivotConfig | null) => {72 this.pivot = next;73 };7475 getRowId = ({ row }: GetRowIdParams<PivotFact>) =>76 row?.id == null ? undefined : String(row.id);77}787980// pivot.demo-data.ts81import type { PivotConfig, AngularColumnDef, ValueFormatterProps } from "@simple-table/angular";8283export interface PivotFact {84 id: string;85 region: string;86 country: string;87 category: string;88 product: string;89 channel: string;90 year: number;91 quarter: string;92 sales: number;93 units: number;94 cost: number;95}9697export const pivotHeaders: AngularColumnDef<PivotFact>[] = [98 { accessor: "region", label: "Region", width: 110, type: "string" },99 { accessor: "country", label: "Country", width: 100, type: "string" },100 { accessor: "category", label: "Category", width: 110, type: "string" },101 { accessor: "product", label: "Product", width: 120, type: "string" },102 { accessor: "channel", label: "Channel", width: 100, type: "string" },103 { accessor: "year", label: "Year", width: 80, type: "number" },104 { accessor: "quarter", label: "Quarter", width: 80, type: "string" },105 {106 accessor: "sales",107 label: "Sales",108 width: 100,109 type: "number",110 align: "right",111 valueFormatter: ({ value }: ValueFormatterProps<PivotFact>) =>112 typeof value === "number" ? `$${value.toLocaleString()}` : "",113 },114 {115 accessor: "units",116 label: "Units",117 width: 80,118 type: "number",119 align: "right",120 },121 {122 accessor: "cost",123 label: "Cost",124 width: 100,125 type: "number",126 align: "right",127 valueFormatter: ({ value }: ValueFormatterProps<PivotFact>) =>128 typeof value === "number" ? `$${value.toLocaleString()}` : "",129 },130];131132const COUNTRIES = {133 West: ["USA", "Canada"],134 East: ["USA", "UK"],135 North: ["Canada", "Sweden"],136 South: ["Brazil", "Australia"],137};138const PRODUCTS = {139 Hardware: ["Widget", "Gadget", "Sensor"],140 Software: ["License", "Subscription"],141};142const CHANNELS = ["Direct", "Partner", "Online"];143const YEARS = [2024, 2025];144const QUARTERS = ["Q1", "Q2", "Q3", "Q4"];145146/** Sparse multi-dimension fact cube (~150–250 rows). */147export function generatePivotRows(): PivotFact[] {148 const rows: PivotFact[] = [];149 let id = 1;150 for (const [region, countries] of Object.entries(COUNTRIES)) {151 for (const country of countries) {152 for (const [category, products] of Object.entries(PRODUCTS)) {153 for (const product of products) {154 for (const channel of CHANNELS) {155 for (const year of YEARS) {156 for (const quarter of QUARTERS) {157 // Sparse facts — skip ~1/3 of combinations158 if ((id + year + quarter.charCodeAt(1) + channel.length) % 5 !== 0) {159 id++;160 continue;161 }162 const base = 40 + ((id * 17) % 90);163 rows.push({164 id: `r${id}`,165 region,166 country,167 category,168 product,169 channel,170 year,171 quarter,172 sales: base * 100,173 units: base,174 cost: Math.round(base * 55),175 });176 id++;177 }178 }179 }180 }181 }182 }183 }184 return rows;185}186187export const pivotRows: PivotFact[] = generatePivotRows();188189export type PivotPreset = {190 id: string;191 label: string;192 pivot: PivotConfig;193};194195export const pivotPresets: PivotPreset[] = [196 {197 id: "region-quarter",198 label: "Region × Quarter",199 pivot: {200 rows: ["region"],201 columns: ["quarter"],202 values: [{ accessor: "sales", aggregation: { type: "sum" } }],203 },204 },205 {206 id: "multi-rows",207 label: "Region × Product",208 pivot: {209 rows: ["region", "product"],210 columns: ["quarter"],211 values: [{ accessor: "sales", aggregation: { type: "sum" } }],212 },213 },214 {215 id: "category-year-quarter",216 label: "Category × Year → Quarter",217 pivot: {218 rows: ["category"],219 columns: ["year", "quarter"],220 values: [{ accessor: "sales", aggregation: { type: "sum" } }],221 },222 },223 {224 id: "channel-quarter",225 label: "Channel × Quarter",226 pivot: {227 rows: ["channel"],228 columns: ["quarter"],229 values: [230 { accessor: "sales", aggregation: { type: "sum" }, label: "Sales" },231 { accessor: "units", aggregation: { type: "sum" }, label: "Units" },232 ],233 },234 },235 {236 id: "country-category",237 label: "Country × Category",238 pivot: {239 rows: ["country"],240 columns: ["category"],241 values: [{ accessor: "sales", aggregation: { type: "average" } }],242 showColumnTotals: false,243 },244 },245 {246 id: "values-only",247 label: "Values only",248 pivot: {249 rows: ["region", "category"],250 columns: [],251 values: [252 { accessor: "sales", aggregation: { type: "sum" } },253 { accessor: "cost", aggregation: { type: "sum" } },254 ],255 },256 },257];258259export const pivotConfig: PivotConfig = pivotPresets[0].pivot;260261export const pivotDemoConfig = {262 headers: pivotHeaders,263 rows: pivotRows,264 tableProps: { pivot: pivotConfig },265 presets: pivotPresets,266};267
1<script lang="ts">2 import { SimpleTable } from "@simple-table/svelte";3 import type { Theme, GetRowIdParams, PivotConfig } from "@simple-table/svelte";4 import { pivotDemoConfig } from "./pivot.demo-data";5 import type { PivotFact } from "./pivot.demo-data";6 import "@simple-table/svelte/styles.css";78 let { height = "500px", theme }: { height?: string | number; theme?: Theme } = $props();910 const INITIAL_PIVOT: PivotConfig = {11 rows: ["region", "product"],12 columns: ["quarter"],13 values: [{ accessor: "sales", aggregation: { type: "sum" } }],14 };1516 let pivotEnabled = $state(true);17 let pivot = $state<PivotConfig | null>(INITIAL_PIVOT);1819 const handlePivotEnabledChange = (enabled: boolean) => {20 pivotEnabled = enabled;21 if (enabled && pivot === null) {22 pivot = INITIAL_PIVOT;23 }24 };2526 const getRowId = ({ row }: GetRowIdParams<PivotFact>) =>27 row?.id == null ? undefined : String(row.id);28</script>2930<div>31 <label32 style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 14px; color: #374151"33 >34 <input35 type="checkbox"36 checked={pivotEnabled}37 aria-label="Pivot mode"38 onchange={(e) => handlePivotEnabledChange(e.currentTarget.checked)}39 />40 Pivot mode41 </label>42 <SimpleTable43 columns={pivotDemoConfig.headers}44 rows={pivotDemoConfig.rows}45 pivot={pivotEnabled ? pivot : null}46 onPivotChange={(next) => (pivot = next)}47 autoExpandColumns={true}48 columnResizing={true}49 enableColumnEditor={true}50 enableColumnEditorInitOpen={true}51 enablePivotPanel={pivotEnabled}52 selectableCells={true}53 {getRowId}54 {height}55 {theme}56 />57</div>585960// pivot.demo-data.ts61import type { PivotConfig, SvelteColumnDef } from "@simple-table/svelte";6263export interface PivotFact {64 id: string;65 region: string;66 country: string;67 category: string;68 product: string;69 channel: string;70 year: number;71 quarter: string;72 sales: number;73 units: number;74 cost: number;75}7677export const pivotHeaders: SvelteColumnDef<PivotFact>[] = [78 { accessor: "region", label: "Region", width: 110, type: "string" },79 { accessor: "country", label: "Country", width: 100, type: "string" },80 { accessor: "category", label: "Category", width: 110, type: "string" },81 { accessor: "product", label: "Product", width: 120, type: "string" },82 { accessor: "channel", label: "Channel", width: 100, type: "string" },83 { accessor: "year", label: "Year", width: 80, type: "number" },84 { accessor: "quarter", label: "Quarter", width: 80, type: "string" },85 {86 accessor: "sales",87 label: "Sales",88 width: 100,89 type: "number",90 align: "right",91 valueFormatter: ({ value }) =>92 typeof value === "number" ? `$${value.toLocaleString()}` : "",93 },94 {95 accessor: "units",96 label: "Units",97 width: 80,98 type: "number",99 align: "right",100 },101 {102 accessor: "cost",103 label: "Cost",104 width: 100,105 type: "number",106 align: "right",107 valueFormatter: ({ value }) =>108 typeof value === "number" ? `$${value.toLocaleString()}` : "",109 },110];111112const COUNTRIES = {113 West: ["USA", "Canada"],114 East: ["USA", "UK"],115 North: ["Canada", "Sweden"],116 South: ["Brazil", "Australia"],117};118const PRODUCTS = {119 Hardware: ["Widget", "Gadget", "Sensor"],120 Software: ["License", "Subscription"],121};122const CHANNELS = ["Direct", "Partner", "Online"];123const YEARS = [2024, 2025];124const QUARTERS = ["Q1", "Q2", "Q3", "Q4"];125126/** Sparse multi-dimension fact cube (~150–250 rows). */127export function generatePivotRows(): PivotFact[] {128 const rows: PivotFact[] = [];129 let id = 1;130 for (const [region, countries] of Object.entries(COUNTRIES)) {131 for (const country of countries) {132 for (const [category, products] of Object.entries(PRODUCTS)) {133 for (const product of products) {134 for (const channel of CHANNELS) {135 for (const year of YEARS) {136 for (const quarter of QUARTERS) {137 // Sparse facts — skip ~1/3 of combinations138 if ((id + year + quarter.charCodeAt(1) + channel.length) % 5 !== 0) {139 id++;140 continue;141 }142 const base = 40 + ((id * 17) % 90);143 rows.push({144 id: `r${id}`,145 region,146 country,147 category,148 product,149 channel,150 year,151 quarter,152 sales: base * 100,153 units: base,154 cost: Math.round(base * 55),155 });156 id++;157 }158 }159 }160 }161 }162 }163 }164 return rows;165}166167export const pivotRows: PivotFact[] = generatePivotRows();168169export type PivotPreset = {170 id: string;171 label: string;172 pivot: PivotConfig;173};174175export const pivotPresets: PivotPreset[] = [176 {177 id: "region-quarter",178 label: "Region × Quarter",179 pivot: {180 rows: ["region"],181 columns: ["quarter"],182 values: [{ accessor: "sales", aggregation: { type: "sum" } }],183 },184 },185 {186 id: "multi-rows",187 label: "Region × Product",188 pivot: {189 rows: ["region", "product"],190 columns: ["quarter"],191 values: [{ accessor: "sales", aggregation: { type: "sum" } }],192 },193 },194 {195 id: "category-year-quarter",196 label: "Category × Year → Quarter",197 pivot: {198 rows: ["category"],199 columns: ["year", "quarter"],200 values: [{ accessor: "sales", aggregation: { type: "sum" } }],201 },202 },203 {204 id: "channel-quarter",205 label: "Channel × Quarter",206 pivot: {207 rows: ["channel"],208 columns: ["quarter"],209 values: [210 { accessor: "sales", aggregation: { type: "sum" }, label: "Sales" },211 { accessor: "units", aggregation: { type: "sum" }, label: "Units" },212 ],213 },214 },215 {216 id: "country-category",217 label: "Country × Category",218 pivot: {219 rows: ["country"],220 columns: ["category"],221 values: [{ accessor: "sales", aggregation: { type: "average" } }],222 showColumnTotals: false,223 },224 },225 {226 id: "values-only",227 label: "Values only",228 pivot: {229 rows: ["region", "category"],230 columns: [],231 values: [232 { accessor: "sales", aggregation: { type: "sum" } },233 { accessor: "cost", aggregation: { type: "sum" } },234 ],235 },236 },237];238239export const pivotConfig: PivotConfig = pivotPresets[0].pivot;240241export const pivotDemoConfig = {242 headers: pivotHeaders,243 rows: pivotRows,244 tableProps: { pivot: pivotConfig },245 presets: pivotPresets,246};247
1import { createSignal } from "solid-js";2import { SimpleTable } from "@simple-table/solid";3import type { PivotConfig, Theme } from "@simple-table/solid";4import { pivotDemoConfig } from "./pivot.demo-data";5import "@simple-table/solid/styles.css";67const INITIAL_PIVOT: PivotConfig = {8 rows: ["region", "product"],9 columns: ["quarter"],10 values: [{ accessor: "sales", aggregation: { type: "sum" } }],11};1213export default function PivotDemo(props: {14 height?: string | number;15 theme?: Theme;16}) {17 const [pivotEnabled, setPivotEnabled] = createSignal(true);18 const [pivot, setPivot] = createSignal<PivotConfig | null>(INITIAL_PIVOT);1920 const handlePivotEnabledChange = (enabled: boolean) => {21 setPivotEnabled(enabled);22 if (enabled && pivot() === null) {23 setPivot(INITIAL_PIVOT);24 }25 };2627 return (28 <div>29 <label30 style={{31 display: "flex",32 "align-items": "center",33 gap: "8px",34 "margin-bottom": "12px",35 "font-size": "14px",36 color: "#374151",37 }}38 >39 <input40 type="checkbox"41 checked={pivotEnabled()}42 onChange={(e) => handlePivotEnabledChange(e.currentTarget.checked)}43 aria-label="Pivot mode"44 />45 Pivot mode46 </label>47 <SimpleTable48 columns={pivotDemoConfig.headers}49 rows={pivotDemoConfig.rows}50 autoExpandColumns51 columnResizing52 enableColumnEditor53 enableColumnEditorInitOpen54 enablePivotPanel={pivotEnabled()}55 height={props.height ?? "500px"}56 pivot={pivotEnabled() ? pivot() : null}57 onPivotChange={setPivot}58 selectableCells59 theme={props.theme}60 getRowId={({ row }) => (row?.id == null ? undefined : String(row.id))}61 />62 </div>63 );64}656667// pivot.demo-data.ts68import type { PivotConfig, SolidColumnDef } from "@simple-table/solid";6970export interface PivotFact {71 id: string;72 region: string;73 country: string;74 category: string;75 product: string;76 channel: string;77 year: number;78 quarter: string;79 sales: number;80 units: number;81 cost: number;82}8384export const pivotHeaders: SolidColumnDef<PivotFact>[] = [85 { accessor: "region", label: "Region", width: 110, type: "string" },86 { accessor: "country", label: "Country", width: 100, type: "string" },87 { accessor: "category", label: "Category", width: 110, type: "string" },88 { accessor: "product", label: "Product", width: 120, type: "string" },89 { accessor: "channel", label: "Channel", width: 100, type: "string" },90 { accessor: "year", label: "Year", width: 80, type: "number" },91 { accessor: "quarter", label: "Quarter", width: 80, type: "string" },92 {93 accessor: "sales",94 label: "Sales",95 width: 100,96 type: "number",97 align: "right",98 valueFormatter: ({ value }) =>99 typeof value === "number" ? `$${value.toLocaleString()}` : "",100 },101 {102 accessor: "units",103 label: "Units",104 width: 80,105 type: "number",106 align: "right",107 },108 {109 accessor: "cost",110 label: "Cost",111 width: 100,112 type: "number",113 align: "right",114 valueFormatter: ({ value }) =>115 typeof value === "number" ? `$${value.toLocaleString()}` : "",116 },117];118119const COUNTRIES = {120 West: ["USA", "Canada"],121 East: ["USA", "UK"],122 North: ["Canada", "Sweden"],123 South: ["Brazil", "Australia"],124};125const PRODUCTS = {126 Hardware: ["Widget", "Gadget", "Sensor"],127 Software: ["License", "Subscription"],128};129const CHANNELS = ["Direct", "Partner", "Online"];130const YEARS = [2024, 2025];131const QUARTERS = ["Q1", "Q2", "Q3", "Q4"];132133/** Sparse multi-dimension fact cube (~150–250 rows). */134export function generatePivotRows(): PivotFact[] {135 const rows: PivotFact[] = [];136 let id = 1;137 for (const [region, countries] of Object.entries(COUNTRIES)) {138 for (const country of countries) {139 for (const [category, products] of Object.entries(PRODUCTS)) {140 for (const product of products) {141 for (const channel of CHANNELS) {142 for (const year of YEARS) {143 for (const quarter of QUARTERS) {144 // Sparse facts — skip ~1/3 of combinations145 if ((id + year + quarter.charCodeAt(1) + channel.length) % 5 !== 0) {146 id++;147 continue;148 }149 const base = 40 + ((id * 17) % 90);150 rows.push({151 id: `r${id}`,152 region,153 country,154 category,155 product,156 channel,157 year,158 quarter,159 sales: base * 100,160 units: base,161 cost: Math.round(base * 55),162 });163 id++;164 }165 }166 }167 }168 }169 }170 }171 return rows;172}173174export const pivotRows: PivotFact[] = generatePivotRows();175176export type PivotPreset = {177 id: string;178 label: string;179 pivot: PivotConfig;180};181182export const pivotPresets: PivotPreset[] = [183 {184 id: "region-quarter",185 label: "Region × Quarter",186 pivot: {187 rows: ["region"],188 columns: ["quarter"],189 values: [{ accessor: "sales", aggregation: { type: "sum" } }],190 },191 },192 {193 id: "multi-rows",194 label: "Region × Product",195 pivot: {196 rows: ["region", "product"],197 columns: ["quarter"],198 values: [{ accessor: "sales", aggregation: { type: "sum" } }],199 },200 },201 {202 id: "category-year-quarter",203 label: "Category × Year → Quarter",204 pivot: {205 rows: ["category"],206 columns: ["year", "quarter"],207 values: [{ accessor: "sales", aggregation: { type: "sum" } }],208 },209 },210 {211 id: "channel-quarter",212 label: "Channel × Quarter",213 pivot: {214 rows: ["channel"],215 columns: ["quarter"],216 values: [217 { accessor: "sales", aggregation: { type: "sum" }, label: "Sales" },218 { accessor: "units", aggregation: { type: "sum" }, label: "Units" },219 ],220 },221 },222 {223 id: "country-category",224 label: "Country × Category",225 pivot: {226 rows: ["country"],227 columns: ["category"],228 values: [{ accessor: "sales", aggregation: { type: "average" } }],229 showColumnTotals: false,230 },231 },232 {233 id: "values-only",234 label: "Values only",235 pivot: {236 rows: ["region", "category"],237 columns: [],238 values: [239 { accessor: "sales", aggregation: { type: "sum" } },240 { accessor: "cost", aggregation: { type: "sum" } },241 ],242 },243 },244];245246export const pivotConfig: PivotConfig = pivotPresets[0].pivot;247248export const pivotDemoConfig = {249 headers: pivotHeaders,250 rows: pivotRows,251 tableProps: { pivot: pivotConfig },252 presets: pivotPresets,253};254
1import { SimpleTableVanilla } from "simple-table-core";2import type { PivotConfig, Theme } from "simple-table-core";3import { pivotDemoConfig } from "./pivot.demo-data";4import "simple-table-core/styles.css";56const INITIAL_PIVOT: PivotConfig = {7 rows: ["region", "product"],8 columns: ["quarter"],9 values: [{ accessor: "sales", aggregation: { type: "sum" } }],10};1112export function renderPivotDemo(13 container: HTMLElement,14 options?: { height?: string | number; theme?: Theme }15): SimpleTableVanilla {16 let pivotEnabled = true;17 let pivot: PivotConfig | null = INITIAL_PIVOT;18 let table: SimpleTableVanilla | null = null;1920 const root = document.createElement("div");2122 const label = document.createElement("label");23 label.style.cssText =24 "display:flex;align-items:center;gap:8px;margin-bottom:12px;font-size:14px;color:#374151";2526 const checkbox = document.createElement("input");27 checkbox.type = "checkbox";28 checkbox.checked = true;29 checkbox.setAttribute("aria-label", "Pivot mode");30 checkbox.addEventListener("change", () => {31 pivotEnabled = checkbox.checked;32 if (pivotEnabled && pivot === null) {33 pivot = INITIAL_PIVOT;34 }35 table?.updateConfig({36 pivot: pivotEnabled ? pivot : null,37 enablePivotPanel: pivotEnabled,38 });39 });4041 const text = document.createElement("span");42 text.textContent = "Pivot mode";43 label.append(checkbox, text);4445 const tableHost = document.createElement("div");46 tableHost.style.width = "100%";47 root.append(label, tableHost);48 container.replaceChildren(root);4950 table = new SimpleTableVanilla(tableHost, {51 columns: pivotDemoConfig.headers,52 rows: pivotDemoConfig.rows,53 pivot,54 autoExpandColumns: true,55 columnResizing: true,56 enableColumnEditor: true,57 enableColumnEditorInitOpen: true,58 enablePivotPanel: true,59 height: options?.height ?? "500px",60 selectableCells: true,61 theme: options?.theme,62 getRowId: ({ row }) => (row?.id == null ? undefined : String(row.id)),63 onPivotChange: (next) => {64 pivot = next;65 },66 });6768 return table;69}707172// pivot.demo-data.ts73import type { PivotConfig, ColumnDef } from "simple-table-core";7475export interface PivotFact {76 id: string;77 region: string;78 country: string;79 category: string;80 product: string;81 channel: string;82 year: number;83 quarter: string;84 sales: number;85 units: number;86 cost: number;87}8889export const pivotHeaders: ColumnDef<PivotFact>[] = [90 { accessor: "region", label: "Region", width: 110, type: "string" },91 { accessor: "country", label: "Country", width: 100, type: "string" },92 { accessor: "category", label: "Category", width: 110, type: "string" },93 { accessor: "product", label: "Product", width: 120, type: "string" },94 { accessor: "channel", label: "Channel", width: 100, type: "string" },95 { accessor: "year", label: "Year", width: 80, type: "number" },96 { accessor: "quarter", label: "Quarter", width: 80, type: "string" },97 {98 accessor: "sales",99 label: "Sales",100 width: 100,101 type: "number",102 align: "right",103 valueFormatter: ({ value }) =>104 typeof value === "number" ? `$${value.toLocaleString()}` : "",105 },106 {107 accessor: "units",108 label: "Units",109 width: 80,110 type: "number",111 align: "right",112 },113 {114 accessor: "cost",115 label: "Cost",116 width: 100,117 type: "number",118 align: "right",119 valueFormatter: ({ value }) =>120 typeof value === "number" ? `$${value.toLocaleString()}` : "",121 },122];123124const COUNTRIES = {125 West: ["USA", "Canada"],126 East: ["USA", "UK"],127 North: ["Canada", "Sweden"],128 South: ["Brazil", "Australia"],129};130const PRODUCTS = {131 Hardware: ["Widget", "Gadget", "Sensor"],132 Software: ["License", "Subscription"],133};134const CHANNELS = ["Direct", "Partner", "Online"];135const YEARS = [2024, 2025];136const QUARTERS = ["Q1", "Q2", "Q3", "Q4"];137138/** Sparse multi-dimension fact cube (~150–250 rows). */139export function generatePivotRows(): PivotFact[] {140 const rows: PivotFact[] = [];141 let id = 1;142 for (const [region, countries] of Object.entries(COUNTRIES)) {143 for (const country of countries) {144 for (const [category, products] of Object.entries(PRODUCTS)) {145 for (const product of products) {146 for (const channel of CHANNELS) {147 for (const year of YEARS) {148 for (const quarter of QUARTERS) {149 // Sparse facts — skip ~1/3 of combinations150 if ((id + year + quarter.charCodeAt(1) + channel.length) % 5 !== 0) {151 id++;152 continue;153 }154 const base = 40 + ((id * 17) % 90);155 rows.push({156 id: `r${id}`,157 region,158 country,159 category,160 product,161 channel,162 year,163 quarter,164 sales: base * 100,165 units: base,166 cost: Math.round(base * 55),167 });168 id++;169 }170 }171 }172 }173 }174 }175 }176 return rows;177}178179export const pivotRows: PivotFact[] = generatePivotRows();180181export type PivotPreset = {182 id: string;183 label: string;184 pivot: PivotConfig;185};186187export const pivotPresets: PivotPreset[] = [188 {189 id: "region-quarter",190 label: "Region × Quarter",191 pivot: {192 rows: ["region"],193 columns: ["quarter"],194 values: [{ accessor: "sales", aggregation: { type: "sum" } }],195 },196 },197 {198 id: "multi-rows",199 label: "Region × Product",200 pivot: {201 rows: ["region", "product"],202 columns: ["quarter"],203 values: [{ accessor: "sales", aggregation: { type: "sum" } }],204 },205 },206 {207 id: "category-year-quarter",208 label: "Category × Year → Quarter",209 pivot: {210 rows: ["category"],211 columns: ["year", "quarter"],212 values: [{ accessor: "sales", aggregation: { type: "sum" } }],213 },214 },215 {216 id: "channel-quarter",217 label: "Channel × Quarter",218 pivot: {219 rows: ["channel"],220 columns: ["quarter"],221 values: [222 { accessor: "sales", aggregation: { type: "sum" }, label: "Sales" },223 { accessor: "units", aggregation: { type: "sum" }, label: "Units" },224 ],225 },226 },227 {228 id: "country-category",229 label: "Country × Category",230 pivot: {231 rows: ["country"],232 columns: ["category"],233 values: [{ accessor: "sales", aggregation: { type: "average" } }],234 showColumnTotals: false,235 },236 },237 {238 id: "values-only",239 label: "Values only",240 pivot: {241 rows: ["region", "category"],242 columns: [],243 values: [244 { accessor: "sales", aggregation: { type: "sum" } },245 { accessor: "cost", aggregation: { type: "sum" } },246 ],247 },248 },249];250251export const pivotConfig: PivotConfig = pivotPresets[0].pivot;252253export const pivotDemoConfig = {254 headers: pivotHeaders,255 rows: pivotRows,256 tableProps: { pivot: pivotConfig },257 presets: pivotPresets,258};259
Props
Pivot props
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
pivotPivotConfig | null | Optional | Matrix pivot config. When set, flat rows are reshaped into dynamic columns (one row per row-dimension combination). Pass null to disable. Consumer rowGrouping is off while active. | |
onPivotChange(pivot: PivotConfig | null) => void | Optional | Fires when pivot changes via TableAPI.setPivot (not every prop sync from your app). | |
enablePivotPanelboolean | Optional | Adds a Pivot Panel to the column editor for placing fields into Rows / Columns / Values. Requires enableColumnEditor. Pivot activates when Values has ≥ 1 measure. |
PivotConfig
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
PivotConfig.rowsAccessor[] | Required | Row dimension accessors. Multiple fields → one flat row per combination. | |
PivotConfig.columnsAccessor[] | Required | Column dimension accessors. Distinct values become headers. Empty = values only. | |
PivotConfig.valuesPivotValueConfig[] | Required | Measures to aggregate (at least one). Optional label overrides the header. | |
PivotConfig.showRowTotalsboolean | Optional | Total column across column dims. Default true. Only when columns is non-empty. | |
PivotConfig.showColumnTotalsboolean | Optional | Total row across row dims. Default true. | |
PivotConfig.showGrandTotalboolean | Optional | Grand-total cells at the totals intersection. Default true. |
Pivot TableAPI methods
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
setPivot(config)(config: PivotConfig | null) => void | Optional | Enable, update, or clear pivot. Pass null for the source grid. | |
getPivot()() => PivotConfig | null | Optional | Active pivot config, or null when off. | |
getPivotHeaders()() => ColumnDef[] | Optional | Generated headers while pivot is active. | |
getPivotedRows()() => Row[] | Optional | Post-pivot rows while pivot is active (flat combination rows plus optional totals). |