diff --git a/apps/marketing/public/txt-demos/angular/pivot.txt b/apps/marketing/public/txt-demos/angular/pivot.txt index fec6dbee2..84c84e7c0 100644 --- a/apps/marketing/public/txt-demos/angular/pivot.txt +++ b/apps/marketing/public/txt-demos/angular/pivot.txt @@ -11,26 +11,45 @@ import { pivotDemoConfig } from "./pivot.demo-data"; import type { PivotFact } from "./pivot.demo-data"; import "@simple-table/angular/styles.css"; +const INITIAL_PIVOT: PivotConfig = { + rows: ["region", "product"], + columns: ["quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], +}; + @Component({ selector: "pivot-demo", standalone: true, imports: [SimpleTableComponent], template: ` - +
+ + +
`, }) export class PivotDemoComponent { @@ -40,11 +59,15 @@ export class PivotDemoComponent { readonly rows: PivotFact[] = pivotDemoConfig.rows; readonly headers: AngularColumnDef[] = pivotDemoConfig.headers; - pivot: PivotConfig | null = { - rows: ["region", "product"], - columns: ["quarter"], - values: [{ accessor: "sales", aggregation: { type: "sum" } }], - }; + pivotEnabled = true; + pivot: PivotConfig | null = INITIAL_PIVOT; + + onPivotEnabledChange(enabled: boolean): void { + this.pivotEnabled = enabled; + if (enabled && this.pivot === null) { + this.pivot = INITIAL_PIVOT; + } + } onPivotChange = (next: PivotConfig | null) => { this.pivot = next; @@ -53,3 +76,192 @@ export class PivotDemoComponent { getRowId = ({ row }: GetRowIdParams) => row?.id == null ? undefined : String(row.id); } + + +// pivot.demo-data.ts +import type { PivotConfig, AngularColumnDef, ValueFormatterProps } from "@simple-table/angular"; + +export interface PivotFact { + id: string; + region: string; + country: string; + category: string; + product: string; + channel: string; + year: number; + quarter: string; + sales: number; + units: number; + cost: number; +} + +export const pivotHeaders: AngularColumnDef[] = [ + { accessor: "region", label: "Region", width: 110, type: "string" }, + { accessor: "country", label: "Country", width: 100, type: "string" }, + { accessor: "category", label: "Category", width: 110, type: "string" }, + { accessor: "product", label: "Product", width: 120, type: "string" }, + { accessor: "channel", label: "Channel", width: 100, type: "string" }, + { accessor: "year", label: "Year", width: 80, type: "number" }, + { accessor: "quarter", label: "Quarter", width: 80, type: "string" }, + { + accessor: "sales", + label: "Sales", + width: 100, + type: "number", + align: "right", + valueFormatter: ({ value }: ValueFormatterProps) => + typeof value === "number" ? `$${value.toLocaleString()}` : "", + }, + { + accessor: "units", + label: "Units", + width: 80, + type: "number", + align: "right", + }, + { + accessor: "cost", + label: "Cost", + width: 100, + type: "number", + align: "right", + valueFormatter: ({ value }: ValueFormatterProps) => + typeof value === "number" ? `$${value.toLocaleString()}` : "", + }, +]; + +const COUNTRIES = { + West: ["USA", "Canada"], + East: ["USA", "UK"], + North: ["Canada", "Sweden"], + South: ["Brazil", "Australia"], +}; +const PRODUCTS = { + Hardware: ["Widget", "Gadget", "Sensor"], + Software: ["License", "Subscription"], +}; +const CHANNELS = ["Direct", "Partner", "Online"]; +const YEARS = [2024, 2025]; +const QUARTERS = ["Q1", "Q2", "Q3", "Q4"]; + +/** Sparse multi-dimension fact cube (~150–250 rows). */ +export function generatePivotRows(): PivotFact[] { + const rows: PivotFact[] = []; + let id = 1; + for (const [region, countries] of Object.entries(COUNTRIES)) { + for (const country of countries) { + for (const [category, products] of Object.entries(PRODUCTS)) { + for (const product of products) { + for (const channel of CHANNELS) { + for (const year of YEARS) { + for (const quarter of QUARTERS) { + // Sparse facts — skip ~1/3 of combinations + if ((id + year + quarter.charCodeAt(1) + channel.length) % 5 !== 0) { + id++; + continue; + } + const base = 40 + ((id * 17) % 90); + rows.push({ + id: `r${id}`, + region, + country, + category, + product, + channel, + year, + quarter, + sales: base * 100, + units: base, + cost: Math.round(base * 55), + }); + id++; + } + } + } + } + } + } + } + return rows; +} + +export const pivotRows: PivotFact[] = generatePivotRows(); + +export type PivotPreset = { + id: string; + label: string; + pivot: PivotConfig; +}; + +export const pivotPresets: PivotPreset[] = [ + { + id: "region-quarter", + label: "Region × Quarter", + pivot: { + rows: ["region"], + columns: ["quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], + }, + }, + { + id: "multi-rows", + label: "Region × Product", + pivot: { + rows: ["region", "product"], + columns: ["quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], + }, + }, + { + id: "category-year-quarter", + label: "Category × Year → Quarter", + pivot: { + rows: ["category"], + columns: ["year", "quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], + }, + }, + { + id: "channel-quarter", + label: "Channel × Quarter", + pivot: { + rows: ["channel"], + columns: ["quarter"], + values: [ + { accessor: "sales", aggregation: { type: "sum" }, label: "Sales" }, + { accessor: "units", aggregation: { type: "sum" }, label: "Units" }, + ], + }, + }, + { + id: "country-category", + label: "Country × Category", + pivot: { + rows: ["country"], + columns: ["category"], + values: [{ accessor: "sales", aggregation: { type: "average" } }], + showColumnTotals: false, + }, + }, + { + id: "values-only", + label: "Values only", + pivot: { + rows: ["region", "category"], + columns: [], + values: [ + { accessor: "sales", aggregation: { type: "sum" } }, + { accessor: "cost", aggregation: { type: "sum" } }, + ], + }, + }, +]; + +export const pivotConfig: PivotConfig = pivotPresets[0].pivot; + +export const pivotDemoConfig = { + headers: pivotHeaders, + rows: pivotRows, + tableProps: { pivot: pivotConfig }, + presets: pivotPresets, +}; diff --git a/apps/marketing/public/txt-demos/react/pivot.txt b/apps/marketing/public/txt-demos/react/pivot.txt index 6f9c2885d..377fd69f7 100644 --- a/apps/marketing/public/txt-demos/react/pivot.txt +++ b/apps/marketing/public/txt-demos/react/pivot.txt @@ -1,3 +1,4 @@ +// PivotDemo.tsx import { useState } from "react"; import { SimpleTable } from "@simple-table/react"; import type { PivotConfig, Theme } from "@simple-table/react"; @@ -17,25 +18,242 @@ const PivotDemo = ({ height?: string | number; theme?: Theme; }) => { + const [pivotEnabled, setPivotEnabled] = useState(true); const [pivot, setPivot] = useState(INITIAL_PIVOT); + const handlePivotEnabledChange = (enabled: boolean) => { + setPivotEnabled(enabled); + if (enabled && pivot === null) { + setPivot(INITIAL_PIVOT); + } + }; + return ( - (row?.id == null ? undefined : String(row.id))} - /> +
+ + (row?.id == null ? undefined : String(row.id))} + /> +
); }; export default PivotDemo; + + +// pivot.demo-data.ts +import type { PivotConfig, ReactColumnDef } from "@simple-table/react"; + +export interface PivotFact { + id: string; + region: string; + country: string; + category: string; + product: string; + channel: string; + year: number; + quarter: string; + sales: number; + units: number; + cost: number; +} + +export const pivotHeaders: ReactColumnDef[] = [ + { accessor: "region", label: "Region", width: 110, type: "string" }, + { accessor: "country", label: "Country", width: 100, type: "string" }, + { accessor: "category", label: "Category", width: 110, type: "string" }, + { accessor: "product", label: "Product", width: 120, type: "string" }, + { accessor: "channel", label: "Channel", width: 100, type: "string" }, + { accessor: "year", label: "Year", width: 80, type: "number" }, + { accessor: "quarter", label: "Quarter", width: 80, type: "string" }, + { + accessor: "sales", + label: "Sales", + width: 100, + type: "number", + align: "right", + valueFormatter: ({ value }) => + typeof value === "number" ? `$${value.toLocaleString()}` : "", + }, + { + accessor: "units", + label: "Units", + width: 80, + type: "number", + align: "right", + }, + { + accessor: "cost", + label: "Cost", + width: 100, + type: "number", + align: "right", + valueFormatter: ({ value }) => + typeof value === "number" ? `$${value.toLocaleString()}` : "", + }, +]; + +const COUNTRIES = { + West: ["USA", "Canada"], + East: ["USA", "UK"], + North: ["Canada", "Sweden"], + South: ["Brazil", "Australia"], +}; +const PRODUCTS = { + Hardware: ["Widget", "Gadget", "Sensor"], + Software: ["License", "Subscription"], +}; +const CHANNELS = ["Direct", "Partner", "Online"]; +const YEARS = [2024, 2025]; +const QUARTERS = ["Q1", "Q2", "Q3", "Q4"]; + +/** Sparse multi-dimension fact cube (~150–250 rows). */ +export function generatePivotRows(): PivotFact[] { + const rows: PivotFact[] = []; + let id = 1; + for (const [region, countries] of Object.entries(COUNTRIES)) { + for (const country of countries) { + for (const [category, products] of Object.entries(PRODUCTS)) { + for (const product of products) { + for (const channel of CHANNELS) { + for (const year of YEARS) { + for (const quarter of QUARTERS) { + // Sparse facts — skip ~1/3 of combinations + if ((id + year + quarter.charCodeAt(1) + channel.length) % 5 !== 0) { + id++; + continue; + } + const base = 40 + ((id * 17) % 90); + rows.push({ + id: `r${id}`, + region, + country, + category, + product, + channel, + year, + quarter, + sales: base * 100, + units: base, + cost: Math.round(base * 55), + }); + id++; + } + } + } + } + } + } + } + return rows; +} + +export const pivotRows: PivotFact[] = generatePivotRows(); + +export type PivotPreset = { + id: string; + label: string; + pivot: PivotConfig; +}; + +export const pivotPresets: PivotPreset[] = [ + { + id: "region-quarter", + label: "Region × Quarter", + pivot: { + rows: ["region"], + columns: ["quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], + }, + }, + { + id: "multi-rows", + label: "Region × Product", + pivot: { + rows: ["region", "product"], + columns: ["quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], + }, + }, + { + id: "category-year-quarter", + label: "Category × Year → Quarter", + pivot: { + rows: ["category"], + columns: ["year", "quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], + }, + }, + { + id: "channel-quarter", + label: "Channel × Quarter", + pivot: { + rows: ["channel"], + columns: ["quarter"], + values: [ + { accessor: "sales", aggregation: { type: "sum" }, label: "Sales" }, + { accessor: "units", aggregation: { type: "sum" }, label: "Units" }, + ], + }, + }, + { + id: "country-category", + label: "Country × Category", + pivot: { + rows: ["country"], + columns: ["category"], + values: [{ accessor: "sales", aggregation: { type: "average" } }], + showColumnTotals: false, + }, + }, + { + id: "values-only", + label: "Values only", + pivot: { + rows: ["region", "category"], + columns: [], + values: [ + { accessor: "sales", aggregation: { type: "sum" } }, + { accessor: "cost", aggregation: { type: "sum" } }, + ], + }, + }, +]; + +export const pivotConfig: PivotConfig = pivotPresets[0].pivot; + +export const pivotDemoConfig = { + headers: pivotHeaders, + rows: pivotRows, + tableProps: { pivot: pivotConfig }, + presets: pivotPresets, +}; diff --git a/apps/marketing/public/txt-demos/solid/pivot.txt b/apps/marketing/public/txt-demos/solid/pivot.txt index 9477355c9..a8be660a7 100644 --- a/apps/marketing/public/txt-demos/solid/pivot.txt +++ b/apps/marketing/public/txt-demos/solid/pivot.txt @@ -1,3 +1,4 @@ +// PivotDemo.tsx import { createSignal } from "solid-js"; import { SimpleTable } from "@simple-table/solid"; import type { PivotConfig, Theme } from "@simple-table/solid"; @@ -14,23 +15,240 @@ export default function PivotDemo(props: { height?: string | number; theme?: Theme; }) { + const [pivotEnabled, setPivotEnabled] = createSignal(true); const [pivot, setPivot] = createSignal(INITIAL_PIVOT); + const handlePivotEnabledChange = (enabled: boolean) => { + setPivotEnabled(enabled); + if (enabled && pivot() === null) { + setPivot(INITIAL_PIVOT); + } + }; + return ( - (row?.id == null ? undefined : String(row.id))} - /> +
+ + (row?.id == null ? undefined : String(row.id))} + /> +
); } + + +// pivot.demo-data.ts +import type { PivotConfig, SolidColumnDef } from "@simple-table/solid"; + +export interface PivotFact { + id: string; + region: string; + country: string; + category: string; + product: string; + channel: string; + year: number; + quarter: string; + sales: number; + units: number; + cost: number; +} + +export const pivotHeaders: SolidColumnDef[] = [ + { accessor: "region", label: "Region", width: 110, type: "string" }, + { accessor: "country", label: "Country", width: 100, type: "string" }, + { accessor: "category", label: "Category", width: 110, type: "string" }, + { accessor: "product", label: "Product", width: 120, type: "string" }, + { accessor: "channel", label: "Channel", width: 100, type: "string" }, + { accessor: "year", label: "Year", width: 80, type: "number" }, + { accessor: "quarter", label: "Quarter", width: 80, type: "string" }, + { + accessor: "sales", + label: "Sales", + width: 100, + type: "number", + align: "right", + valueFormatter: ({ value }) => + typeof value === "number" ? `$${value.toLocaleString()}` : "", + }, + { + accessor: "units", + label: "Units", + width: 80, + type: "number", + align: "right", + }, + { + accessor: "cost", + label: "Cost", + width: 100, + type: "number", + align: "right", + valueFormatter: ({ value }) => + typeof value === "number" ? `$${value.toLocaleString()}` : "", + }, +]; + +const COUNTRIES = { + West: ["USA", "Canada"], + East: ["USA", "UK"], + North: ["Canada", "Sweden"], + South: ["Brazil", "Australia"], +}; +const PRODUCTS = { + Hardware: ["Widget", "Gadget", "Sensor"], + Software: ["License", "Subscription"], +}; +const CHANNELS = ["Direct", "Partner", "Online"]; +const YEARS = [2024, 2025]; +const QUARTERS = ["Q1", "Q2", "Q3", "Q4"]; + +/** Sparse multi-dimension fact cube (~150–250 rows). */ +export function generatePivotRows(): PivotFact[] { + const rows: PivotFact[] = []; + let id = 1; + for (const [region, countries] of Object.entries(COUNTRIES)) { + for (const country of countries) { + for (const [category, products] of Object.entries(PRODUCTS)) { + for (const product of products) { + for (const channel of CHANNELS) { + for (const year of YEARS) { + for (const quarter of QUARTERS) { + // Sparse facts — skip ~1/3 of combinations + if ((id + year + quarter.charCodeAt(1) + channel.length) % 5 !== 0) { + id++; + continue; + } + const base = 40 + ((id * 17) % 90); + rows.push({ + id: `r${id}`, + region, + country, + category, + product, + channel, + year, + quarter, + sales: base * 100, + units: base, + cost: Math.round(base * 55), + }); + id++; + } + } + } + } + } + } + } + return rows; +} + +export const pivotRows: PivotFact[] = generatePivotRows(); + +export type PivotPreset = { + id: string; + label: string; + pivot: PivotConfig; +}; + +export const pivotPresets: PivotPreset[] = [ + { + id: "region-quarter", + label: "Region × Quarter", + pivot: { + rows: ["region"], + columns: ["quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], + }, + }, + { + id: "multi-rows", + label: "Region × Product", + pivot: { + rows: ["region", "product"], + columns: ["quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], + }, + }, + { + id: "category-year-quarter", + label: "Category × Year → Quarter", + pivot: { + rows: ["category"], + columns: ["year", "quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], + }, + }, + { + id: "channel-quarter", + label: "Channel × Quarter", + pivot: { + rows: ["channel"], + columns: ["quarter"], + values: [ + { accessor: "sales", aggregation: { type: "sum" }, label: "Sales" }, + { accessor: "units", aggregation: { type: "sum" }, label: "Units" }, + ], + }, + }, + { + id: "country-category", + label: "Country × Category", + pivot: { + rows: ["country"], + columns: ["category"], + values: [{ accessor: "sales", aggregation: { type: "average" } }], + showColumnTotals: false, + }, + }, + { + id: "values-only", + label: "Values only", + pivot: { + rows: ["region", "category"], + columns: [], + values: [ + { accessor: "sales", aggregation: { type: "sum" } }, + { accessor: "cost", aggregation: { type: "sum" } }, + ], + }, + }, +]; + +export const pivotConfig: PivotConfig = pivotPresets[0].pivot; + +export const pivotDemoConfig = { + headers: pivotHeaders, + rows: pivotRows, + tableProps: { pivot: pivotConfig }, + presets: pivotPresets, +}; diff --git a/apps/marketing/public/txt-demos/svelte/pivot.txt b/apps/marketing/public/txt-demos/svelte/pivot.txt index ed9404869..54300388f 100644 --- a/apps/marketing/public/txt-demos/svelte/pivot.txt +++ b/apps/marketing/public/txt-demos/svelte/pivot.txt @@ -1,34 +1,247 @@ +// PivotDemo.svelte - (pivot = next)} - autoExpandColumns={true} - columnResizing={true} - enableColumnEditor={true} - enableColumnEditorInitOpen={true} - enablePivotPanel={true} - selectableCells={true} - {getRowId} - {height} - {theme} -/> +
+ + (pivot = next)} + autoExpandColumns={true} + columnResizing={true} + enableColumnEditor={true} + enableColumnEditorInitOpen={true} + enablePivotPanel={pivotEnabled} + selectableCells={true} + {getRowId} + {height} + {theme} + /> +
+ + +// pivot.demo-data.ts +import type { PivotConfig, SvelteColumnDef } from "@simple-table/svelte"; + +export interface PivotFact { + id: string; + region: string; + country: string; + category: string; + product: string; + channel: string; + year: number; + quarter: string; + sales: number; + units: number; + cost: number; +} + +export const pivotHeaders: SvelteColumnDef[] = [ + { accessor: "region", label: "Region", width: 110, type: "string" }, + { accessor: "country", label: "Country", width: 100, type: "string" }, + { accessor: "category", label: "Category", width: 110, type: "string" }, + { accessor: "product", label: "Product", width: 120, type: "string" }, + { accessor: "channel", label: "Channel", width: 100, type: "string" }, + { accessor: "year", label: "Year", width: 80, type: "number" }, + { accessor: "quarter", label: "Quarter", width: 80, type: "string" }, + { + accessor: "sales", + label: "Sales", + width: 100, + type: "number", + align: "right", + valueFormatter: ({ value }) => + typeof value === "number" ? `$${value.toLocaleString()}` : "", + }, + { + accessor: "units", + label: "Units", + width: 80, + type: "number", + align: "right", + }, + { + accessor: "cost", + label: "Cost", + width: 100, + type: "number", + align: "right", + valueFormatter: ({ value }) => + typeof value === "number" ? `$${value.toLocaleString()}` : "", + }, +]; + +const COUNTRIES = { + West: ["USA", "Canada"], + East: ["USA", "UK"], + North: ["Canada", "Sweden"], + South: ["Brazil", "Australia"], +}; +const PRODUCTS = { + Hardware: ["Widget", "Gadget", "Sensor"], + Software: ["License", "Subscription"], +}; +const CHANNELS = ["Direct", "Partner", "Online"]; +const YEARS = [2024, 2025]; +const QUARTERS = ["Q1", "Q2", "Q3", "Q4"]; + +/** Sparse multi-dimension fact cube (~150–250 rows). */ +export function generatePivotRows(): PivotFact[] { + const rows: PivotFact[] = []; + let id = 1; + for (const [region, countries] of Object.entries(COUNTRIES)) { + for (const country of countries) { + for (const [category, products] of Object.entries(PRODUCTS)) { + for (const product of products) { + for (const channel of CHANNELS) { + for (const year of YEARS) { + for (const quarter of QUARTERS) { + // Sparse facts — skip ~1/3 of combinations + if ((id + year + quarter.charCodeAt(1) + channel.length) % 5 !== 0) { + id++; + continue; + } + const base = 40 + ((id * 17) % 90); + rows.push({ + id: `r${id}`, + region, + country, + category, + product, + channel, + year, + quarter, + sales: base * 100, + units: base, + cost: Math.round(base * 55), + }); + id++; + } + } + } + } + } + } + } + return rows; +} + +export const pivotRows: PivotFact[] = generatePivotRows(); + +export type PivotPreset = { + id: string; + label: string; + pivot: PivotConfig; +}; + +export const pivotPresets: PivotPreset[] = [ + { + id: "region-quarter", + label: "Region × Quarter", + pivot: { + rows: ["region"], + columns: ["quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], + }, + }, + { + id: "multi-rows", + label: "Region × Product", + pivot: { + rows: ["region", "product"], + columns: ["quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], + }, + }, + { + id: "category-year-quarter", + label: "Category × Year → Quarter", + pivot: { + rows: ["category"], + columns: ["year", "quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], + }, + }, + { + id: "channel-quarter", + label: "Channel × Quarter", + pivot: { + rows: ["channel"], + columns: ["quarter"], + values: [ + { accessor: "sales", aggregation: { type: "sum" }, label: "Sales" }, + { accessor: "units", aggregation: { type: "sum" }, label: "Units" }, + ], + }, + }, + { + id: "country-category", + label: "Country × Category", + pivot: { + rows: ["country"], + columns: ["category"], + values: [{ accessor: "sales", aggregation: { type: "average" } }], + showColumnTotals: false, + }, + }, + { + id: "values-only", + label: "Values only", + pivot: { + rows: ["region", "category"], + columns: [], + values: [ + { accessor: "sales", aggregation: { type: "sum" } }, + { accessor: "cost", aggregation: { type: "sum" } }, + ], + }, + }, +]; + +export const pivotConfig: PivotConfig = pivotPresets[0].pivot; + +export const pivotDemoConfig = { + headers: pivotHeaders, + rows: pivotRows, + tableProps: { pivot: pivotConfig }, + presets: pivotPresets, +}; diff --git a/apps/marketing/public/txt-demos/vanilla/pivot.txt b/apps/marketing/public/txt-demos/vanilla/pivot.txt index 2429a2a26..bc3b437e2 100644 --- a/apps/marketing/public/txt-demos/vanilla/pivot.txt +++ b/apps/marketing/public/txt-demos/vanilla/pivot.txt @@ -14,11 +14,44 @@ export function renderPivotDemo( container: HTMLElement, options?: { height?: string | number; theme?: Theme } ): SimpleTableVanilla { - container.replaceChildren(); - return new SimpleTableVanilla(container, { + let pivotEnabled = true; + let pivot: PivotConfig | null = INITIAL_PIVOT; + let table: SimpleTableVanilla | null = null; + + const root = document.createElement("div"); + + const label = document.createElement("label"); + label.style.cssText = + "display:flex;align-items:center;gap:8px;margin-bottom:12px;font-size:14px;color:#374151"; + + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.checked = true; + checkbox.setAttribute("aria-label", "Pivot mode"); + checkbox.addEventListener("change", () => { + pivotEnabled = checkbox.checked; + if (pivotEnabled && pivot === null) { + pivot = INITIAL_PIVOT; + } + table?.updateConfig({ + pivot: pivotEnabled ? pivot : null, + enablePivotPanel: pivotEnabled, + }); + }); + + const text = document.createElement("span"); + text.textContent = "Pivot mode"; + label.append(checkbox, text); + + const tableHost = document.createElement("div"); + tableHost.style.width = "100%"; + root.append(label, tableHost); + container.replaceChildren(root); + + table = new SimpleTableVanilla(tableHost, { columns: pivotDemoConfig.headers, rows: pivotDemoConfig.rows, - pivot: INITIAL_PIVOT, + pivot, autoExpandColumns: true, columnResizing: true, enableColumnEditor: true, @@ -28,5 +61,199 @@ export function renderPivotDemo( selectableCells: true, theme: options?.theme, getRowId: ({ row }) => (row?.id == null ? undefined : String(row.id)), + onPivotChange: (next) => { + pivot = next; + }, }); + + return table; +} + + +// pivot.demo-data.ts +import type { PivotConfig, ColumnDef } from "simple-table-core"; + +export interface PivotFact { + id: string; + region: string; + country: string; + category: string; + product: string; + channel: string; + year: number; + quarter: string; + sales: number; + units: number; + cost: number; } + +export const pivotHeaders: ColumnDef[] = [ + { accessor: "region", label: "Region", width: 110, type: "string" }, + { accessor: "country", label: "Country", width: 100, type: "string" }, + { accessor: "category", label: "Category", width: 110, type: "string" }, + { accessor: "product", label: "Product", width: 120, type: "string" }, + { accessor: "channel", label: "Channel", width: 100, type: "string" }, + { accessor: "year", label: "Year", width: 80, type: "number" }, + { accessor: "quarter", label: "Quarter", width: 80, type: "string" }, + { + accessor: "sales", + label: "Sales", + width: 100, + type: "number", + align: "right", + valueFormatter: ({ value }) => + typeof value === "number" ? `$${value.toLocaleString()}` : "", + }, + { + accessor: "units", + label: "Units", + width: 80, + type: "number", + align: "right", + }, + { + accessor: "cost", + label: "Cost", + width: 100, + type: "number", + align: "right", + valueFormatter: ({ value }) => + typeof value === "number" ? `$${value.toLocaleString()}` : "", + }, +]; + +const COUNTRIES = { + West: ["USA", "Canada"], + East: ["USA", "UK"], + North: ["Canada", "Sweden"], + South: ["Brazil", "Australia"], +}; +const PRODUCTS = { + Hardware: ["Widget", "Gadget", "Sensor"], + Software: ["License", "Subscription"], +}; +const CHANNELS = ["Direct", "Partner", "Online"]; +const YEARS = [2024, 2025]; +const QUARTERS = ["Q1", "Q2", "Q3", "Q4"]; + +/** Sparse multi-dimension fact cube (~150–250 rows). */ +export function generatePivotRows(): PivotFact[] { + const rows: PivotFact[] = []; + let id = 1; + for (const [region, countries] of Object.entries(COUNTRIES)) { + for (const country of countries) { + for (const [category, products] of Object.entries(PRODUCTS)) { + for (const product of products) { + for (const channel of CHANNELS) { + for (const year of YEARS) { + for (const quarter of QUARTERS) { + // Sparse facts — skip ~1/3 of combinations + if ((id + year + quarter.charCodeAt(1) + channel.length) % 5 !== 0) { + id++; + continue; + } + const base = 40 + ((id * 17) % 90); + rows.push({ + id: `r${id}`, + region, + country, + category, + product, + channel, + year, + quarter, + sales: base * 100, + units: base, + cost: Math.round(base * 55), + }); + id++; + } + } + } + } + } + } + } + return rows; +} + +export const pivotRows: PivotFact[] = generatePivotRows(); + +export type PivotPreset = { + id: string; + label: string; + pivot: PivotConfig; +}; + +export const pivotPresets: PivotPreset[] = [ + { + id: "region-quarter", + label: "Region × Quarter", + pivot: { + rows: ["region"], + columns: ["quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], + }, + }, + { + id: "multi-rows", + label: "Region × Product", + pivot: { + rows: ["region", "product"], + columns: ["quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], + }, + }, + { + id: "category-year-quarter", + label: "Category × Year → Quarter", + pivot: { + rows: ["category"], + columns: ["year", "quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], + }, + }, + { + id: "channel-quarter", + label: "Channel × Quarter", + pivot: { + rows: ["channel"], + columns: ["quarter"], + values: [ + { accessor: "sales", aggregation: { type: "sum" }, label: "Sales" }, + { accessor: "units", aggregation: { type: "sum" }, label: "Units" }, + ], + }, + }, + { + id: "country-category", + label: "Country × Category", + pivot: { + rows: ["country"], + columns: ["category"], + values: [{ accessor: "sales", aggregation: { type: "average" } }], + showColumnTotals: false, + }, + }, + { + id: "values-only", + label: "Values only", + pivot: { + rows: ["region", "category"], + columns: [], + values: [ + { accessor: "sales", aggregation: { type: "sum" } }, + { accessor: "cost", aggregation: { type: "sum" } }, + ], + }, + }, +]; + +export const pivotConfig: PivotConfig = pivotPresets[0].pivot; + +export const pivotDemoConfig = { + headers: pivotHeaders, + rows: pivotRows, + tableProps: { pivot: pivotConfig }, + presets: pivotPresets, +}; diff --git a/apps/marketing/public/txt-demos/vue/pivot.txt b/apps/marketing/public/txt-demos/vue/pivot.txt index ea708541b..c8066147a 100644 --- a/apps/marketing/public/txt-demos/vue/pivot.txt +++ b/apps/marketing/public/txt-demos/vue/pivot.txt @@ -1,19 +1,40 @@ +// PivotDemo.vue + + +// pivot.demo-data.ts +import type { PivotConfig, VueColumnDef } from "@simple-table/vue"; + +export interface PivotFact { + id: string; + region: string; + country: string; + category: string; + product: string; + channel: string; + year: number; + quarter: string; + sales: number; + units: number; + cost: number; +} + +export const pivotHeaders: VueColumnDef[] = [ + { accessor: "region", label: "Region", width: 110, type: "string" }, + { accessor: "country", label: "Country", width: 100, type: "string" }, + { accessor: "category", label: "Category", width: 110, type: "string" }, + { accessor: "product", label: "Product", width: 120, type: "string" }, + { accessor: "channel", label: "Channel", width: 100, type: "string" }, + { accessor: "year", label: "Year", width: 80, type: "number" }, + { accessor: "quarter", label: "Quarter", width: 80, type: "string" }, + { + accessor: "sales", + label: "Sales", + width: 100, + type: "number", + align: "right", + valueFormatter: ({ value }) => + typeof value === "number" ? `$${value.toLocaleString()}` : "", + }, + { + accessor: "units", + label: "Units", + width: 80, + type: "number", + align: "right", + }, + { + accessor: "cost", + label: "Cost", + width: 100, + type: "number", + align: "right", + valueFormatter: ({ value }) => + typeof value === "number" ? `$${value.toLocaleString()}` : "", + }, +]; + +const COUNTRIES = { + West: ["USA", "Canada"], + East: ["USA", "UK"], + North: ["Canada", "Sweden"], + South: ["Brazil", "Australia"], +}; +const PRODUCTS = { + Hardware: ["Widget", "Gadget", "Sensor"], + Software: ["License", "Subscription"], +}; +const CHANNELS = ["Direct", "Partner", "Online"]; +const YEARS = [2024, 2025]; +const QUARTERS = ["Q1", "Q2", "Q3", "Q4"]; + +/** Sparse multi-dimension fact cube (~150–250 rows). */ +export function generatePivotRows(): PivotFact[] { + const rows: PivotFact[] = []; + let id = 1; + for (const [region, countries] of Object.entries(COUNTRIES)) { + for (const country of countries) { + for (const [category, products] of Object.entries(PRODUCTS)) { + for (const product of products) { + for (const channel of CHANNELS) { + for (const year of YEARS) { + for (const quarter of QUARTERS) { + // Sparse facts — skip ~1/3 of combinations + if ((id + year + quarter.charCodeAt(1) + channel.length) % 5 !== 0) { + id++; + continue; + } + const base = 40 + ((id * 17) % 90); + rows.push({ + id: `r${id}`, + region, + country, + category, + product, + channel, + year, + quarter, + sales: base * 100, + units: base, + cost: Math.round(base * 55), + }); + id++; + } + } + } + } + } + } + } + return rows; +} + +export const pivotRows: PivotFact[] = generatePivotRows(); + +export type PivotPreset = { + id: string; + label: string; + pivot: PivotConfig; +}; + +export const pivotPresets: PivotPreset[] = [ + { + id: "region-quarter", + label: "Region × Quarter", + pivot: { + rows: ["region"], + columns: ["quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], + }, + }, + { + id: "multi-rows", + label: "Region × Product", + pivot: { + rows: ["region", "product"], + columns: ["quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], + }, + }, + { + id: "category-year-quarter", + label: "Category × Year → Quarter", + pivot: { + rows: ["category"], + columns: ["year", "quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], + }, + }, + { + id: "channel-quarter", + label: "Channel × Quarter", + pivot: { + rows: ["channel"], + columns: ["quarter"], + values: [ + { accessor: "sales", aggregation: { type: "sum" }, label: "Sales" }, + { accessor: "units", aggregation: { type: "sum" }, label: "Units" }, + ], + }, + }, + { + id: "country-category", + label: "Country × Category", + pivot: { + rows: ["country"], + columns: ["category"], + values: [{ accessor: "sales", aggregation: { type: "average" } }], + showColumnTotals: false, + }, + }, + { + id: "values-only", + label: "Values only", + pivot: { + rows: ["region", "category"], + columns: [], + values: [ + { accessor: "sales", aggregation: { type: "sum" } }, + { accessor: "cost", aggregation: { type: "sum" } }, + ], + }, + }, +]; + +export const pivotConfig: PivotConfig = pivotPresets[0].pivot; + +export const pivotDemoConfig = { + headers: pivotHeaders, + rows: pivotRows, + tableProps: { pivot: pivotConfig }, + presets: pivotPresets, +}; diff --git a/apps/marketing/src/components/demos/PivotDemo.tsx b/apps/marketing/src/components/demos/PivotDemo.tsx index f917e9408..3a0cf85a7 100644 --- a/apps/marketing/src/components/demos/PivotDemo.tsx +++ b/apps/marketing/src/components/demos/PivotDemo.tsx @@ -135,11 +135,11 @@ const PivotDemo = ({ rows={rows} autoExpandColumns columnResizing - enableColumnEditor - enableColumnEditorInitOpen - enablePivotPanel={pivotEnabled} - height={height} - pivot={pivotEnabled ? pivot : null} + enableColumnEditor + enableColumnEditorInitOpen + enablePivotPanel={pivotEnabled} + height={height} + pivot={pivotEnabled ? pivot : null} onPivotChange={setPivot} selectableCells theme={theme} diff --git a/packages/examples/angular/src/demos/pivot/pivot-demo.component.ts b/packages/examples/angular/src/demos/pivot/pivot-demo.component.ts index d63fee259..fde588b35 100644 --- a/packages/examples/angular/src/demos/pivot/pivot-demo.component.ts +++ b/packages/examples/angular/src/demos/pivot/pivot-demo.component.ts @@ -1,40 +1,49 @@ import { Component, Input } from "@angular/core"; import { SimpleTableComponent } from "@simple-table/angular"; -import type { AngularColumnDef, GetRowIdParams, PivotConfig, Theme } from "@simple-table/angular"; -import { pivotDemoConfig, pivotPresets, type PivotPreset } from "./pivot.demo-data"; -import "@simple-table/angular/styles.css"; +import type { + AngularColumnDef, + GetRowIdParams, + PivotConfig, + Theme, +} from "@simple-table/angular"; +import { pivotDemoConfig } from "./pivot.demo-data"; import type { PivotFact } from "./pivot.demo-data"; +import "@simple-table/angular/styles.css"; + +const INITIAL_PIVOT: PivotConfig = { + rows: ["region", "product"], + columns: ["quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], +}; @Component({ selector: "pivot-demo", standalone: true, imports: [SimpleTableComponent], template: ` -
-
- @for (preset of presets; track preset.id) { - - } -
+
+ [] = pivotDemoConfig.headers; - readonly presets = pivotPresets; - activeId = pivotPresets[0].id; - pivot: PivotConfig = pivotPresets[0].pivot; + pivotEnabled = true; + pivot: PivotConfig | null = INITIAL_PIVOT; - selectPreset(preset: PivotPreset): void { - this.activeId = preset.id; - this.pivot = preset.pivot; + onPivotEnabledChange(enabled: boolean): void { + this.pivotEnabled = enabled; + if (enabled && this.pivot === null) { + this.pivot = INITIAL_PIVOT; + } } - getRowId = ({ row }: GetRowIdParams) => row.id; + onPivotChange = (next: PivotConfig | null) => { + this.pivot = next; + }; + + getRowId = ({ row }: GetRowIdParams) => + row?.id == null ? undefined : String(row.id); } diff --git a/packages/examples/react/src/demos/pivot/PivotDemo.tsx b/packages/examples/react/src/demos/pivot/PivotDemo.tsx index 7ec704e55..15710ab0e 100644 --- a/packages/examples/react/src/demos/pivot/PivotDemo.tsx +++ b/packages/examples/react/src/demos/pivot/PivotDemo.tsx @@ -1,54 +1,66 @@ import { useState } from "react"; import { SimpleTable } from "@simple-table/react"; -import type { Theme } from "@simple-table/react"; -import { pivotDemoConfig, pivotPresets } from "./pivot.demo-data"; +import type { PivotConfig, Theme } from "@simple-table/react"; +import { pivotDemoConfig } from "./pivot.demo-data"; import "@simple-table/react/styles.css"; +const INITIAL_PIVOT: PivotConfig = { + rows: ["region", "product"], + columns: ["quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], +}; + const PivotDemo = ({ - height = "400px", - theme + height = "500px", + theme, }: { height?: string | number; theme?: Theme; }) => { - const [activeId, setActiveId] = useState(pivotPresets[0].id); - const active = pivotPresets.find((p) => p.id === activeId) ?? pivotPresets[0]; + const [pivotEnabled, setPivotEnabled] = useState(true); + const [pivot, setPivot] = useState(INITIAL_PIVOT); + + const handlePivotEnabledChange = (enabled: boolean) => { + setPivotEnabled(enabled); + if (enabled && pivot === null) { + setPivot(INITIAL_PIVOT); + } + }; return ( -
-
- {pivotPresets.map((preset) => { - const selected = preset.id === activeId; - return ( - - ); - })} -
+
+ row.id} + getRowId={({ row }) => (row?.id == null ? undefined : String(row.id))} />
); diff --git a/packages/examples/solid/src/demos/pivot/PivotDemo.tsx b/packages/examples/solid/src/demos/pivot/PivotDemo.tsx index 0da4921c7..855736b93 100644 --- a/packages/examples/solid/src/demos/pivot/PivotDemo.tsx +++ b/packages/examples/solid/src/demos/pivot/PivotDemo.tsx @@ -1,54 +1,63 @@ -import { createMemo, createSignal, For } from "solid-js"; +import { createSignal } from "solid-js"; import { SimpleTable } from "@simple-table/solid"; -import type { Theme } from "@simple-table/solid"; -import { pivotDemoConfig, pivotPresets } from "./pivot.demo-data"; +import type { PivotConfig, Theme } from "@simple-table/solid"; +import { pivotDemoConfig } from "./pivot.demo-data"; import "@simple-table/solid/styles.css"; +const INITIAL_PIVOT: PivotConfig = { + rows: ["region", "product"], + columns: ["quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], +}; + export default function PivotDemo(props: { height?: string | number; theme?: Theme; }) { - const [activeId, setActiveId] = createSignal(pivotPresets[0].id); - const active = createMemo( - () => pivotPresets.find((p) => p.id === activeId()) ?? pivotPresets[0] - ); + const [pivotEnabled, setPivotEnabled] = createSignal(true); + const [pivot, setPivot] = createSignal(INITIAL_PIVOT); + + const handlePivotEnabledChange = (enabled: boolean) => { + setPivotEnabled(enabled); + if (enabled && pivot() === null) { + setPivot(INITIAL_PIVOT); + } + }; return ( -
-
- - {(preset) => { - const selected = () => preset.id === activeId(); - return ( - - ); - }} - -
+
+ row.id} rows={pivotDemoConfig.rows} - pivot={active().pivot} + autoExpandColumns columnResizing - height={props.height ?? "400px"} + enableColumnEditor + enableColumnEditorInitOpen + enablePivotPanel={pivotEnabled()} + height={props.height ?? "500px"} + pivot={pivotEnabled() ? pivot() : null} + onPivotChange={setPivot} selectableCells theme={props.theme} + getRowId={({ row }) => (row?.id == null ? undefined : String(row.id))} />
); diff --git a/packages/examples/svelte/src/demos/pivot/PivotDemo.svelte b/packages/examples/svelte/src/demos/pivot/PivotDemo.svelte index 0e51df5cc..2bf065ca6 100644 --- a/packages/examples/svelte/src/demos/pivot/PivotDemo.svelte +++ b/packages/examples/svelte/src/demos/pivot/PivotDemo.svelte @@ -1,39 +1,56 @@ -
-
- {#each pivotPresets as preset} - - {/each} -
+
+ (pivot = next)} + autoExpandColumns={true} columnResizing={true} + enableColumnEditor={true} + enableColumnEditorInitOpen={true} + enablePivotPanel={pivotEnabled} selectableCells={true} + {getRowId} {height} {theme} /> diff --git a/packages/examples/vanilla/src/demos/pivot/PivotDemo.ts b/packages/examples/vanilla/src/demos/pivot/PivotDemo.ts index 9328f4d77..7dcb76592 100644 --- a/packages/examples/vanilla/src/demos/pivot/PivotDemo.ts +++ b/packages/examples/vanilla/src/demos/pivot/PivotDemo.ts @@ -1,62 +1,69 @@ import { SimpleTableVanilla } from "simple-table-core"; -import type { PivotFact } from "./pivot.demo-data"; -import type { Theme, GetRowIdParams } from "simple-table-core"; -import { pivotDemoConfig, pivotPresets } from "./pivot.demo-data"; +import type { PivotConfig, Theme } from "simple-table-core"; +import { pivotDemoConfig } from "./pivot.demo-data"; import "simple-table-core/styles.css"; -const getRowId = ({ row }: GetRowIdParams) => row.id; +const INITIAL_PIVOT: PivotConfig = { + rows: ["region", "product"], + columns: ["quarter"], + values: [{ accessor: "sales", aggregation: { type: "sum" } }], +}; + export function renderPivotDemo( container: HTMLElement, options?: { height?: string | number; theme?: Theme } -): SimpleTableVanilla { - let activeId = pivotPresets[0].id; - let table: SimpleTableVanilla | null = null; +): SimpleTableVanilla { + let pivotEnabled = true; + let pivot: PivotConfig | null = INITIAL_PIVOT; + let table: SimpleTableVanilla | null = null; const root = document.createElement("div"); - root.style.cssText = "display:flex;flex-direction:column;gap:12px;width:100%"; - const buttons = document.createElement("div"); - buttons.style.cssText = "display:flex;flex-wrap:wrap;gap:8px"; + const label = document.createElement("label"); + label.style.cssText = + "display:flex;align-items:center;gap:8px;margin-bottom:12px;font-size:14px;color:#374151"; - const tableHost = document.createElement("div"); - tableHost.style.cssText = "width:100%"; - - const paintButtons = () => { - buttons.replaceChildren(); - for (const preset of pivotPresets) { - const btn = document.createElement("button"); - btn.type = "button"; - btn.textContent = preset.label; - const selected = preset.id === activeId; - btn.style.cssText = `padding:6px 12px;border-radius:6px;border:none;cursor:pointer;font-size:13px;font-weight:500;background:${ - selected ? "#2563eb" : "#e5e7eb" - };color:${selected ? "#fff" : "#374151"}`; - btn.addEventListener("click", () => { - activeId = preset.id; - paintButtons(); - const active = pivotPresets.find((p) => p.id === activeId) ?? pivotPresets[0]; - table?.updateConfig({ - pivot: active.pivot, - }); - }); - buttons.appendChild(btn); + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.checked = true; + checkbox.setAttribute("aria-label", "Pivot mode"); + checkbox.addEventListener("change", () => { + pivotEnabled = checkbox.checked; + if (pivotEnabled && pivot === null) { + pivot = INITIAL_PIVOT; } - }; + table?.updateConfig({ + pivot: pivotEnabled ? pivot : null, + enablePivotPanel: pivotEnabled, + }); + }); - paintButtons(); - root.append(buttons, tableHost); + const text = document.createElement("span"); + text.textContent = "Pivot mode"; + label.append(checkbox, text); + + const tableHost = document.createElement("div"); + tableHost.style.width = "100%"; + root.append(label, tableHost); container.replaceChildren(root); - const active = pivotPresets.find((p) => p.id === activeId) ?? pivotPresets[0]; table = new SimpleTableVanilla(tableHost, { - getRowId, columns: pivotDemoConfig.headers, rows: pivotDemoConfig.rows, - pivot: active.pivot, + pivot, + autoExpandColumns: true, columnResizing: true, - height: options?.height ?? "400px", + enableColumnEditor: true, + enableColumnEditorInitOpen: true, + enablePivotPanel: true, + height: options?.height ?? "500px", selectableCells: true, theme: options?.theme, + getRowId: ({ row }) => (row?.id == null ? undefined : String(row.id)), + onPivotChange: (next) => { + pivot = next; + }, }); + return table; } diff --git a/packages/examples/vue/src/demos/pivot/PivotDemo.vue b/packages/examples/vue/src/demos/pivot/PivotDemo.vue index 09e6a6cd1..8322f9d03 100644 --- a/packages/examples/vue/src/demos/pivot/PivotDemo.vue +++ b/packages/examples/vue/src/demos/pivot/PivotDemo.vue @@ -1,54 +1,69 @@