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: AngularColumnDef[] = [{ 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: 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" } }],}}/>AngularCopypivotConfig = {rows: ["region"],columns: ["quarter"],values: [{ accessor: "sales", aggregation: { type: "sum" } }],};<simple-table[columns]="headers"[rows]="flatRows"[pivot]="pivotConfig"></simple-table>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>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
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.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
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 type { PivotFact } from "./pivot.demo-data";6import "@simple-table/react/styles.css";78const INITIAL_PIVOT: PivotConfig<PivotFact> = {9 rows: ["region", "product"],10 columns: ["quarter"],11 values: [{ accessor: "sales", aggregation: { type: "sum" } }],12};1314const PivotDemo = ({15 height = "500px",16 theme,17}: {18 height?: string | number;19 theme?: Theme;20}) => {21 const [pivotEnabled, setPivotEnabled] = useState(true);22 const [pivot, setPivot] = useState<PivotConfig<PivotFact> | null>(INITIAL_PIVOT);2324 const handlePivotEnabledChange = (enabled: boolean) => {25 setPivotEnabled(enabled);26 if (enabled && pivot === null) {27 setPivot(INITIAL_PIVOT);28 }29 };3031 return (32 <div>33 <label34 style={{35 display: "flex",36 alignItems: "center",37 gap: 8,38 marginBottom: 12,39 fontSize: 14,40 color: "#374151",41 }}42 >43 <input44 type="checkbox"45 checked={pivotEnabled}46 onChange={(e) => handlePivotEnabledChange(e.target.checked)}47 aria-label="Pivot mode"48 />49 Pivot mode50 </label>51 <SimpleTable52 columns={pivotDemoConfig.headers}53 rows={pivotDemoConfig.rows}54 autoExpandColumns55 columnResizing56 enableColumnEditor57 enableColumnEditorInitOpen58 enablePivotPanel={pivotEnabled}59 height={height}60 pivot={pivotEnabled ? pivot : null}61 onPivotChange={setPivot}62 selectableCells63 theme={theme}64 getRowId={({ row }) => (row?.id == null ? undefined : String(row.id))}65 />66 </div>67 );68};6970export default PivotDemo;
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<PivotFact> = {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<PivotFact> | 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<PivotFact> | 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<PivotFact>;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<PivotFact> = pivotPresets[0].pivot;260261export const pivotDemoConfig = {262 headers: pivotHeaders,263 rows: pivotRows,264 tableProps: { pivot: pivotConfig },265 presets: pivotPresets,266};267
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<PivotFact> = {52 rows: ["region", "product"],53 columns: ["quarter"],54 values: [{ accessor: "sales", aggregation: { type: "sum" } }],55};5657const pivotEnabled = ref(true);58const pivot = ref<PivotConfig<PivotFact> | 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>
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<PivotFact> = {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<PivotFact> | 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>
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 type { PivotFact } from "./pivot.demo-data";6import "@simple-table/solid/styles.css";78const INITIAL_PIVOT: PivotConfig<PivotFact> = {9 rows: ["region", "product"],10 columns: ["quarter"],11 values: [{ accessor: "sales", aggregation: { type: "sum" } }],12};1314export default function PivotDemo(props: {15 height?: string | number;16 theme?: Theme;17}) {18 const [pivotEnabled, setPivotEnabled] = createSignal(true);19 const [pivot, setPivot] = createSignal<PivotConfig<PivotFact> | null>(INITIAL_PIVOT);2021 const handlePivotEnabledChange = (enabled: boolean) => {22 setPivotEnabled(enabled);23 if (enabled && pivot() === null) {24 setPivot(INITIAL_PIVOT);25 }26 };2728 return (29 <div>30 <label31 style={{32 display: "flex",33 "align-items": "center",34 gap: "8px",35 "margin-bottom": "12px",36 "font-size": "14px",37 color: "#374151",38 }}39 >40 <input41 type="checkbox"42 checked={pivotEnabled()}43 onChange={(e) => handlePivotEnabledChange(e.currentTarget.checked)}44 aria-label="Pivot mode"45 />46 Pivot mode47 </label>48 <SimpleTable49 columns={pivotDemoConfig.headers}50 rows={pivotDemoConfig.rows}51 autoExpandColumns52 columnResizing53 enableColumnEditor54 enableColumnEditorInitOpen55 enablePivotPanel={pivotEnabled()}56 height={props.height ?? "500px"}57 pivot={pivotEnabled() ? pivot() : null}58 onPivotChange={setPivot}59 selectableCells60 theme={props.theme}61 getRowId={({ row }) => (row?.id == null ? undefined : String(row.id))}62 />63 </div>64 );65}
1import { SimpleTableVanilla } from "simple-table-core";2import type { PivotConfig, Theme } from "simple-table-core";3import { pivotDemoConfig } from "./pivot.demo-data";4import type { PivotFact } from "./pivot.demo-data";5import "simple-table-core/styles.css";67const INITIAL_PIVOT: PivotConfig<PivotFact> = {8 rows: ["region", "product"],9 columns: ["quarter"],10 values: [{ accessor: "sales", aggregation: { type: "sum" } }],11};1213export function renderPivotDemo(14 container: HTMLElement,15 options?: { height?: string | number; theme?: Theme }16): SimpleTableVanilla<PivotFact> {17 let pivotEnabled = true;18 let pivot: PivotConfig<PivotFact> | null = INITIAL_PIVOT;19 let table: SimpleTableVanilla<PivotFact> | null = null;2021 const root = document.createElement("div");2223 const label = document.createElement("label");24 label.style.cssText =25 "display:flex;align-items:center;gap:8px;margin-bottom:12px;font-size:14px;color:#374151";2627 const checkbox = document.createElement("input");28 checkbox.type = "checkbox";29 checkbox.checked = true;30 checkbox.setAttribute("aria-label", "Pivot mode");31 checkbox.addEventListener("change", () => {32 pivotEnabled = checkbox.checked;33 if (pivotEnabled && pivot === null) {34 pivot = INITIAL_PIVOT;35 }36 table?.updateConfig({37 pivot: pivotEnabled ? pivot : null,38 enablePivotPanel: pivotEnabled,39 });40 });4142 const text = document.createElement("span");43 text.textContent = "Pivot mode";44 label.append(checkbox, text);4546 const tableHost = document.createElement("div");47 tableHost.style.width = "100%";48 root.append(label, tableHost);49 container.replaceChildren(root);5051 table = new SimpleTableVanilla(tableHost, {52 columns: pivotDemoConfig.headers,53 rows: pivotDemoConfig.rows,54 pivot,55 autoExpandColumns: true,56 columnResizing: true,57 enableColumnEditor: true,58 enableColumnEditorInitOpen: true,59 enablePivotPanel: true,60 height: options?.height ?? "500px",61 selectableCells: true,62 theme: options?.theme,63 getRowId: ({ row }) => (row?.id == null ? undefined : String(row.id)),64 onPivotChange: (next) => {65 pivot = next;66 },67 });6869 return table;70}717273// pivot.demo-data.ts74import type { PivotConfig, ColumnDef } from "simple-table-core";7576export interface PivotFact {77 id: string;78 region: string;79 country: string;80 category: string;81 product: string;82 channel: string;83 year: number;84 quarter: string;85 sales: number;86 units: number;87 cost: number;88}8990export const pivotHeaders: ColumnDef<PivotFact>[] = [91 { accessor: "region", label: "Region", width: 110, type: "string" },92 { accessor: "country", label: "Country", width: 100, type: "string" },93 { accessor: "category", label: "Category", width: 110, type: "string" },94 { accessor: "product", label: "Product", width: 120, type: "string" },95 { accessor: "channel", label: "Channel", width: 100, type: "string" },96 { accessor: "year", label: "Year", width: 80, type: "number" },97 { accessor: "quarter", label: "Quarter", width: 80, type: "string" },98 {99 accessor: "sales",100 label: "Sales",101 width: 100,102 type: "number",103 align: "right",104 valueFormatter: ({ value }) =>105 typeof value === "number" ? `$${value.toLocaleString()}` : "",106 },107 {108 accessor: "units",109 label: "Units",110 width: 80,111 type: "number",112 align: "right",113 },114 {115 accessor: "cost",116 label: "Cost",117 width: 100,118 type: "number",119 align: "right",120 valueFormatter: ({ value }) =>121 typeof value === "number" ? `$${value.toLocaleString()}` : "",122 },123];124125const COUNTRIES = {126 West: ["USA", "Canada"],127 East: ["USA", "UK"],128 North: ["Canada", "Sweden"],129 South: ["Brazil", "Australia"],130};131const PRODUCTS = {132 Hardware: ["Widget", "Gadget", "Sensor"],133 Software: ["License", "Subscription"],134};135const CHANNELS = ["Direct", "Partner", "Online"];136const YEARS = [2024, 2025];137const QUARTERS = ["Q1", "Q2", "Q3", "Q4"];138139/** Sparse multi-dimension fact cube (~150–250 rows). */140export function generatePivotRows(): PivotFact[] {141 const rows: PivotFact[] = [];142 let id = 1;143 for (const [region, countries] of Object.entries(COUNTRIES)) {144 for (const country of countries) {145 for (const [category, products] of Object.entries(PRODUCTS)) {146 for (const product of products) {147 for (const channel of CHANNELS) {148 for (const year of YEARS) {149 for (const quarter of QUARTERS) {150 // Sparse facts — skip ~1/3 of combinations151 if ((id + year + quarter.charCodeAt(1) + channel.length) % 5 !== 0) {152 id++;153 continue;154 }155 const base = 40 + ((id * 17) % 90);156 rows.push({157 id: `r${id}`,158 region,159 country,160 category,161 product,162 channel,163 year,164 quarter,165 sales: base * 100,166 units: base,167 cost: Math.round(base * 55),168 });169 id++;170 }171 }172 }173 }174 }175 }176 }177 return rows;178}179180export const pivotRows: PivotFact[] = generatePivotRows();181182export type PivotPreset = {183 id: string;184 label: string;185 pivot: PivotConfig<PivotFact>;186};187188export const pivotPresets: PivotPreset[] = [189 {190 id: "region-quarter",191 label: "Region × Quarter",192 pivot: {193 rows: ["region"],194 columns: ["quarter"],195 values: [{ accessor: "sales", aggregation: { type: "sum" } }],196 },197 },198 {199 id: "multi-rows",200 label: "Region × Product",201 pivot: {202 rows: ["region", "product"],203 columns: ["quarter"],204 values: [{ accessor: "sales", aggregation: { type: "sum" } }],205 },206 },207 {208 id: "category-year-quarter",209 label: "Category × Year → Quarter",210 pivot: {211 rows: ["category"],212 columns: ["year", "quarter"],213 values: [{ accessor: "sales", aggregation: { type: "sum" } }],214 },215 },216 {217 id: "channel-quarter",218 label: "Channel × Quarter",219 pivot: {220 rows: ["channel"],221 columns: ["quarter"],222 values: [223 { accessor: "sales", aggregation: { type: "sum" }, label: "Sales" },224 { accessor: "units", aggregation: { type: "sum" }, label: "Units" },225 ],226 },227 },228 {229 id: "country-category",230 label: "Country × Category",231 pivot: {232 rows: ["country"],233 columns: ["category"],234 values: [{ accessor: "sales", aggregation: { type: "average" } }],235 showColumnTotals: false,236 },237 },238 {239 id: "values-only",240 label: "Values only",241 pivot: {242 rows: ["region", "category"],243 columns: [],244 values: [245 { accessor: "sales", aggregation: { type: "sum" } },246 { accessor: "cost", aggregation: { type: "sum" } },247 ],248 },249 },250];251252export const pivotConfig: PivotConfig<PivotFact> = pivotPresets[0].pivot;253254export const pivotDemoConfig = {255 headers: pivotHeaders,256 rows: pivotRows,257 tableProps: { pivot: pivotConfig },258 presets: pivotPresets,259};260
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). |