Row & Column Pivots
Configure pivot row and column fields in the client data source to create specific views of your data.
Note
To focus on row and column configuration, all demos in this guide use a single measure column: sum of Profit. For measure configuration, see the Measures guide.
All demos also use the Pill Manager to manage pivot configuration. This component is optional; you can substitute any UI that fits your application.
Pivot Columns
Set the columns property on the pivotModel of the client data source to configure
which columns are used to dynamically generate pivot columns. The values in the
columns property are used to create the pivot column definitions.
The demo below lets you select pivot columns. Try different column combinations to see how pivoting behaves.
Column Pivots
1import "@1771technologies/lytenyte-pro/pill-manager.css";2import "@1771technologies/lytenyte-pro/components.css";3import "@1771technologies/lytenyte-pro/light-dark.css";4import { salesData, type SaleDataItem } from "@1771technologies/grid-sample-data/sales-data";5import { computeField, Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";6import {7 AgeGroup,8 CostCell,9 CountryCell,10 DateCell,11 GenderCell,12 NumberCell,13 ProfitCell,14 StickGroupHeader,15 style,16} from "./components.jsx";17import { sum } from "es-toolkit";18import { useMemo, useState } from "react";19import { PillManager, RowGroupCell } from "@1771technologies/lytenyte-pro/components";20
21export interface GridSpec {22 readonly data: SaleDataItem;23 readonly column: {24 pivotable?: boolean;25 };26}27
28export const columns: Grid.Column<GridSpec>[] = [29 { id: "date", name: "Date", cellRenderer: DateCell, width: 110 },30 { id: "age", name: "Age", type: "number", width: 80 },31 { id: "ageGroup", name: "Age Group", cellRenderer: AgeGroup, width: 110, pivotable: true },32 { id: "customerGender", name: "Gender", cellRenderer: GenderCell, width: 80, pivotable: true },33 { id: "country", name: "Country", cellRenderer: CountryCell, width: 150, pivotable: true },34 { id: "orderQuantity", name: "Quantity", type: "number", width: 60 },35 { id: "unitPrice", name: "Price", type: "number", width: 80, cellRenderer: NumberCell },36 { id: "cost", name: "Cost", width: 80, type: "number", cellRenderer: CostCell },37 { id: "revenue", name: "Revenue", width: 80, type: "number", cellRenderer: ProfitCell },38 { id: "profit", name: "Profit", width: 80, type: "number", cellRenderer: ProfitCell },39 { id: "state", name: "State", width: 150, pivotable: true },40 { id: "product", name: "Product", width: 160, pivotable: true },41 { id: "productCategory", name: "Category", width: 120, pivotable: true },42 { id: "subCategory", name: "Sub-Category", width: 160, pivotable: true },43];44
45const base: Grid.ColumnBase<GridSpec> = { width: 120, resizable: true };46
47const group: Grid.RowGroupColumn<GridSpec> = {48 cellRenderer: RowGroupCell,49 width: 200,50 pin: "start",51};52
53const aggSum: Grid.T.Aggregator<GridSpec["data"]> = (field, data) => {54 const values = data.map((x) => computeField<number>(field, x));55 return sum(values);56};57
58export default function PivotDemo() {59 const [pivots, setPivots] = useState<PillManager.T.PillItem[]>(() =>60 columns61 .filter((x) => x.pivotable)62 .map((x) => ({ id: x.id, name: x.name ?? x.id, active: x.id === "ageGroup" })),63 );64
65 const ds = useClientDataSource<GridSpec>({66 data: salesData,67 pivotMode: true,68 pivotModel: {69 columns: pivots.filter((x) => x.active),70 measures: [71 {72 dim: { id: "profit", name: "Profit", type: "number", cellRenderer: ProfitCell, width: 120 },73 fn: "sum",74 },75 ],76 },77 aggregateFns: { sum: aggSum },78 });79
80 const pillRows = useMemo(() => {81 const pivotPills = pivots.map((x) => {82 return {83 id: x.id,84 active: x.active,85 movable: x.active,86 name: x.name ?? x.id,87 data: x,88 removable: true,89 };90 });91 return [{ id: "column-pivots", label: "Column Pivots", type: "column-pivots", pills: pivotPills }];92 }, [pivots]);93
94 const pivotProps = ds.usePivotProps();95
96 const [groupColumn, setGroupColumn] = useState(group);97
98 return (99 <>100 <div>101 <PillManager102 rows={pillRows}103 onPillItemActiveChange={(p) => {104 setPivots((prev) => {105 const next = prev.map((x) =>106 x.id === p.item.id ? { ...x, active: p.item.active && p.row.id === "column-pivots" } : x,107 );108
109 return [...next.filter((x) => x.active), ...next.filter((x) => !x.active)];110 });111 }}112 onPillRowChange={(ev) => {113 const activeFirst = ev.changed[0].pills.filter((x) => x.active);114 const nonActive = ev.changed[0].pills.filter((x) => !x.active);115 setPivots([...activeFirst, ...nonActive]);116 }}117 />118 </div>119 <div className="ln-grid" style={{ height: 500 }}>120 <Grid121 columns={columns}122 rowSource={ds}123 columnBase={base}124 rowGroupColumn={groupColumn}125 onRowGroupColumnChange={setGroupColumn}126 {...pivotProps}127 styles={style}128 columnGroupRenderer={StickGroupHeader}129 />130 </div>131 </>132 );133}1import type { Grid } from "@1771technologies/lytenyte-pro";2import type { GridSpec } from "./demo";3import { format, isValid, parse } from "date-fns";4import { countryFlags } from "@1771technologies/grid-sample-data/sales-data";5import { clsx, type ClassValue } from "clsx";6import { twMerge } from "tailwind-merge";7
8// Need to set header group overflow to unset to allow sticky header labels.9export const style: Grid.Style = { headerGroup: { style: { overflow: "unset" } } };10export function StickGroupHeader(props: Grid.T.HeaderGroupParams<GridSpec>) {11 return (12 <div style={{ position: "sticky", insetInlineStart: "calc(var(--ln-start-offset) + 8px)" }}>13 {props.groupPath.at(-1)}14 </div>15 );16}17
18export function DateCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {19 const field = api.columnField(column, row);20
21 if (typeof field !== "string") return "-";22
23 const dateField = parse(field as string, "MM/dd/yyyy", new Date());24
25 if (!isValid(dateField)) return "-";26
27 const niceDate = format(dateField, "yyyy MMM dd");28 return <div className="flex h-full w-full items-center tabular-nums">{niceDate}</div>;29}30
31export function AgeGroupPivotHeader({ column }: Grid.T.HeaderParams<GridSpec>) {32 const field = column.name ?? column.id;33
34 if (field === "Under 25") return <div className="text-[#944cec] dark:text-[#B181EB]">Under 25</div>;35 if (field === "25-34") return <div className="text-[#aa6c1a] dark:text-[#E5B474]">25-34</div>;36 if (field === "35-64") return <div className="text-[#0f7d4c] dark:text-[#52B086]">35-64</div>;37
38 if (field === "Grand Total") return "Grand Total";39
40 return "Other";41}42
43export function AgeGroup({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {44 const field = api.columnField(column, row);45
46 if (field === "Under 25") return <div className="text-[#944cec] dark:text-[#B181EB]">Under 25</div>;47 if (field === "25-34") return <div className="text-[#aa6c1a] dark:text-[#E5B474]">25-34</div>;48 if (field === "35-64") return <div className="text-[#0f7d4c] dark:text-[#52B086]">35-64</div>;49
50 return "-";51}52
53export function GenderCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {54 const field = api.columnField(column, row);55
56 if (field === "M")57 return (58 <div className="flex h-full w-full items-center gap-2">59 <div className="flex size-6 items-center justify-center rounded-full bg-blue-500/50">60 <span className="iconify ph--gender-male-bold size-4" />61 </div>62 M63 </div>64 );65
66 if (field === "F")67 return (68 <div className="flex h-full w-full items-center gap-2">69 <div className="flex size-6 items-center justify-center rounded-full bg-pink-500/50">70 <span className="iconify ph--gender-female-bold size-4" />71 </div>72 F73 </div>74 );75
76 return "-";77}78
79function tw(...c: ClassValue[]) {80 return twMerge(clsx(...c));81}82
83const formatter = new Intl.NumberFormat("en-US", {84 maximumFractionDigits: 2,85 minimumFractionDigits: 0,86});87export function ProfitCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {88 const field = api.columnField(column, row);89
90 if (typeof field !== "number") return "-";91
92 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);93
94 return (95 <div96 className={tw(97 "flex h-full w-full items-center justify-end tabular-nums",98 field < 0 && "text-red-600 dark:text-red-300",99 field > 0 && "text-green-600 dark:text-green-300",100 )}101 >102 {formatted}103 </div>104 );105}106
107export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {108 const field = api.columnField(column, row);109
110 if (typeof field !== "number") return "-";111
112 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);113
114 return <div className={"flex h-full w-full items-center justify-end tabular-nums"}>{formatted}</div>;115}116
117export function CostCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {118 const field = api.columnField(column, row);119
120 if (typeof field !== "number") return "-";121
122 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);123
124 return (125 <div126 className={tw(127 "flex h-full w-full items-center justify-end tabular-nums text-red-600 dark:text-red-300",128 )}129 >130 {formatted}131 </div>132 );133}134
135export function CountryCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {136 const field = api.columnField(column, row);137
138 const flag = countryFlags[field as keyof typeof countryFlags];139 if (!flag) return "-";140
141 return (142 <div className="flex h-full w-full items-center gap-2">143 <img className="size-4" src={flag} alt={`country flag of ${field}`} />144 <span>{String(field ?? "-")}</span>145 </div>146 );147}Each pivot you add deepens the column hierarchy, and the number of generated columns can grow exponentially. LyteNyte Grid imposes no limits on the number of pivots you can apply, so your application should limit this based on your data requirements.
Without pivot rows, LyteNyte Grid aggregates all rows into a single totals row. See the Pivot Rows section of this guide to add pivot rows.
Pivot Column Group Expansions
Defining multiple pivot columns creates a column group hierarchy. Since the grid generates these dynamically, they are not explicitly defined in your application state.
The client data source manages column group expansion state internally. You can
collapse column groups using the grid’s
api.columnToggleGroup method.
The demo below shows this behavior. The grid is pivoted on Age Group and Gender. A custom header group renderer allows column groups to be collapsed. Notice that the Total columns remain visible even when the group is collapsed.
Pivot Column Groups
1import "@1771technologies/lytenyte-pro/pill-manager.css";2import "@1771technologies/lytenyte-pro/components.css";3import "@1771technologies/lytenyte-pro/light-dark.css";4import { salesData, type SaleDataItem } from "@1771technologies/grid-sample-data/sales-data";5import { computeField, Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";6import {7 AgeGroup,8 CostCell,9 CountryCell,10 DateCell,11 GenderCell,12 HeaderGroupCell,13 NumberCell,14 ProfitCell,15 style,16} from "./components.jsx";17import { sum } from "es-toolkit";18import { RowGroupCell } from "@1771technologies/lytenyte-pro/components";19
20export interface GridSpec {21 readonly data: SaleDataItem;22 readonly column: {23 pivotable?: boolean;24 };25}26
27export const columns: Grid.Column<GridSpec>[] = [28 { id: "date", name: "Date", cellRenderer: DateCell, width: 110 },29 { id: "age", name: "Age", type: "number", width: 80 },30 { id: "ageGroup", name: "Age Group", cellRenderer: AgeGroup, width: 110, pivotable: true },31 { id: "customerGender", name: "Gender", cellRenderer: GenderCell, width: 80, pivotable: true },32 { id: "country", name: "Country", cellRenderer: CountryCell, width: 150, pivotable: true },33 { id: "orderQuantity", name: "Quantity", type: "number", width: 60 },34 { id: "unitPrice", name: "Price", type: "number", width: 80, cellRenderer: NumberCell },35 { id: "cost", name: "Cost", width: 80, type: "number", cellRenderer: CostCell },36 { id: "revenue", name: "Revenue", width: 80, type: "number", cellRenderer: ProfitCell },37 { id: "profit", name: "Profit", width: 80, type: "number", cellRenderer: ProfitCell },38 { id: "state", name: "State", width: 150, pivotable: true },39 { id: "product", name: "Product", width: 160, pivotable: true },40 { id: "productCategory", name: "Category", width: 120, pivotable: true },41 { id: "subCategory", name: "Sub-Category", width: 160, pivotable: true },42];43
44const base: Grid.ColumnBase<GridSpec> = { width: 120 };45
46const group: Grid.RowGroupColumn<GridSpec> = {47 cellRenderer: RowGroupCell,48 width: 200,49 pin: "start",50};51
52const aggSum: Grid.T.Aggregator<GridSpec["data"]> = (field, data) => {53 const values = data.map((x) => computeField<number>(field, x));54 return sum(values);55};56
57export default function PivotDemo() {58 const ds = useClientDataSource<GridSpec>({59 data: salesData,60 pivotMode: true,61 pivotModel: {62 columns: [{ id: "ageGroup" }, { id: "customerGender" }],63 measures: [64 {65 dim: { id: "profit", name: "Profit", type: "number", cellRenderer: ProfitCell, width: 130 },66 fn: "sum",67 },68 ],69 },70 aggregateFns: { sum: aggSum },71 });72
73 const pivotProps = ds.usePivotProps();74
75 return (76 <div className="ln-grid" style={{ height: 500 }}>77 <Grid78 columns={columns}79 rowSource={ds}80 columnBase={base}81 rowGroupColumn={group}82 columnGroupRenderer={HeaderGroupCell}83 styles={style}84 {...pivotProps}85 />86 </div>87 );88}1import type { Grid } from "@1771technologies/lytenyte-pro";2import type { GridSpec } from "./demo";3import { format, isValid, parse } from "date-fns";4import { countryFlags } from "@1771technologies/grid-sample-data/sales-data";5import { clsx, type ClassValue } from "clsx";6import { twMerge } from "tailwind-merge";7import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons";8
9// Need to set header group overflow to unset to allow sticky header labels.10export const style: Grid.Style = { headerGroup: { style: { overflow: "unset" } } };11
12export function DateCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {13 const field = api.columnField(column, row);14
15 if (typeof field !== "string") return "-";16
17 const dateField = parse(field as string, "MM/dd/yyyy", new Date());18
19 if (!isValid(dateField)) return "-";20
21 const niceDate = format(dateField, "yyyy MMM dd");22 return <div className="flex h-full w-full items-center tabular-nums">{niceDate}</div>;23}24
25export function AgeGroupPivotHeader({ column }: Grid.T.HeaderParams<GridSpec>) {26 const field = column.name ?? column.id;27
28 if (field === "Under 25") return <div className="text-[#944cec] dark:text-[#B181EB]">Under 25</div>;29 if (field === "25-34") return <div className="text-[#aa6c1a] dark:text-[#E5B474]">25-34</div>;30 if (field === "35-64") return <div className="text-[#0f7d4c] dark:text-[#52B086]">35-64</div>;31
32 if (field === "Grand Total") return "Grand Total";33
34 return "Other";35}36
37export function AgeGroup({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {38 const field = api.columnField(column, row);39
40 if (field === "Under 25") return <div className="text-[#944cec] dark:text-[#B181EB]">Under 25</div>;41 if (field === "25-34") return <div className="text-[#aa6c1a] dark:text-[#E5B474]">25-34</div>;42 if (field === "35-64") return <div className="text-[#0f7d4c] dark:text-[#52B086]">35-64</div>;43
44 return "-";45}46
47export function GenderCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {48 const field = api.columnField(column, row);49
50 if (field === "M")51 return (52 <div className="flex h-full w-full items-center gap-2">53 <div className="flex size-6 items-center justify-center rounded-full bg-blue-500/50">54 <span className="iconify ph--gender-male-bold size-4" />55 </div>56 M57 </div>58 );59
60 if (field === "F")61 return (62 <div className="flex h-full w-full items-center gap-2">63 <div className="flex size-6 items-center justify-center rounded-full bg-pink-500/50">64 <span className="iconify ph--gender-female-bold size-4" />65 </div>66 F67 </div>68 );69
70 return "-";71}72
73function tw(...c: ClassValue[]) {74 return twMerge(clsx(...c));75}76
77const formatter = new Intl.NumberFormat("en-US", {78 maximumFractionDigits: 2,79 minimumFractionDigits: 0,80});81export function ProfitCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {82 const field = api.columnField(column, row);83
84 if (typeof field !== "number") return "-";85
86 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);87
88 return (89 <div90 className={tw(91 "flex h-full w-full items-center justify-end tabular-nums",92 field < 0 && "text-red-600 dark:text-red-300",93 field > 0 && "text-green-600 dark:text-green-300",94 )}95 >96 {formatted}97 </div>98 );99}100
101export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {102 const field = api.columnField(column, row);103
104 if (typeof field !== "number") return "-";105
106 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);107
108 return <div className={"flex h-full w-full items-center justify-end tabular-nums"}>{formatted}</div>;109}110
111export function CostCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {112 const field = api.columnField(column, row);113
114 if (typeof field !== "number") return "-";115
116 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);117
118 return (119 <div120 className={tw(121 "flex h-full w-full items-center justify-end tabular-nums text-red-600 dark:text-red-300",122 )}123 >124 {formatted}125 </div>126 );127}128
129export function CountryCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {130 const field = api.columnField(column, row);131
132 const flag = countryFlags[field as keyof typeof countryFlags];133 if (!flag) return "-";134
135 return (136 <div className="flex h-full w-full items-center gap-2">137 <img className="size-4" src={flag} alt={`country flag of ${field}`} />138 <span>{String(field ?? "-")}</span>139 </div>140 );141}142
143export function HeaderGroupCell({144 groupPath,145 api,146 collapsible,147 collapsed,148}: Grid.T.HeaderGroupParams<GridSpec>) {149 return (150 <>151 <div style={{ position: "sticky", insetInlineStart: "calc(var(--ln-start-offset) + 8px)" }}>152 {groupPath.at(-1)}153 </div>154 <div className="flex-1" />155 <button156 data-ln-button="secondary"157 data-ln-icon158 data-ln-size="sm"159 className="bg-ln-bg-ui-panel hover:bg-ln-bg absolute right-1"160 onClick={() => api.columnToggleGroup(groupPath)}161 >162 {collapsible && collapsed && <ChevronRightIcon />}163 {collapsible && !collapsed && <ChevronLeftIcon />}164 </button>165 </>166 );167}Pivot Column Updates
The client data source dynamically generates pivot columns and manages their column state. This lets you move and resize these generated pivot columns as shown in the demo below.
Update Pivot Columns
1import "@1771technologies/lytenyte-pro/pill-manager.css";2import "@1771technologies/lytenyte-pro/components.css";3import "@1771technologies/lytenyte-pro/light-dark.css";4import { salesData, type SaleDataItem } from "@1771technologies/grid-sample-data/sales-data";5import { computeField, Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";6import {7 AgeGroup,8 CostCell,9 CountryCell,10 DateCell,11 GenderCell,12 NumberCell,13 ProfitCell,14 StickGroupHeader,15 style,16} from "./components.jsx";17import { sum } from "es-toolkit";18import { useState } from "react";19import { RowGroupCell } from "@1771technologies/lytenyte-pro/components";20
21export interface GridSpec {22 readonly data: SaleDataItem;23 readonly column: {24 pivotable?: boolean;25 };26}27
28export const columns: Grid.Column<GridSpec>[] = [29 { id: "date", name: "Date", cellRenderer: DateCell, width: 110 },30 { id: "age", name: "Age", type: "number", width: 80 },31 { id: "ageGroup", name: "Age Group", cellRenderer: AgeGroup, width: 110, pivotable: true },32 { id: "customerGender", name: "Gender", cellRenderer: GenderCell, width: 80, pivotable: true },33 { id: "country", name: "Country", cellRenderer: CountryCell, width: 150, pivotable: true },34 { id: "orderQuantity", name: "Quantity", type: "number", width: 60 },35 { id: "unitPrice", name: "Price", type: "number", width: 80, cellRenderer: NumberCell },36 { id: "cost", name: "Cost", width: 80, type: "number", cellRenderer: CostCell },37 { id: "revenue", name: "Revenue", width: 80, type: "number", cellRenderer: ProfitCell },38 { id: "profit", name: "Profit", width: 80, type: "number", cellRenderer: ProfitCell },39 { id: "state", name: "State", width: 150, pivotable: true },40 { id: "product", name: "Product", width: 160, pivotable: true },41 { id: "productCategory", name: "Category", width: 120, pivotable: true },42 { id: "subCategory", name: "Sub-Category", width: 160, pivotable: true },43];44
45const base: Grid.ColumnBase<GridSpec> = { width: 120, resizable: true, movable: true };46
47const group: Grid.RowGroupColumn<GridSpec> = {48 cellRenderer: RowGroupCell,49 width: 200,50 pin: "start",51};52
53const aggSum: Grid.T.Aggregator<GridSpec["data"]> = (field, data) => {54 const values = data.map((x) => computeField<number>(field, x));55 return sum(values);56};57
58export default function PivotDemo() {59 const ds = useClientDataSource<GridSpec>({60 data: salesData,61 pivotMode: true,62 pivotModel: {63 columns: [{ id: "ageGroup" }, { id: "customerGender" }],64 measures: [65 {66 dim: { id: "profit", name: "Profit", type: "number", cellRenderer: ProfitCell, width: 120 },67 fn: "sum",68 },69 ],70 },71 aggregateFns: { sum: aggSum },72 });73
74 const pivotProps = ds.usePivotProps();75
76 const [groupColumn, setGroupColumn] = useState(group);77
78 return (79 <div className="ln-grid" style={{ height: 500 }}>80 <Grid81 columns={columns}82 rowSource={ds}83 columnBase={base}84 rowGroupColumn={groupColumn}85 onRowGroupColumnChange={setGroupColumn}86 {...pivotProps}87 styles={style}88 columnGroupRenderer={StickGroupHeader}89 />90 </div>91 );92}1import type { Grid } from "@1771technologies/lytenyte-pro";2import type { GridSpec } from "./demo";3import { format, isValid, parse } from "date-fns";4import { countryFlags } from "@1771technologies/grid-sample-data/sales-data";5import { clsx, type ClassValue } from "clsx";6import { twMerge } from "tailwind-merge";7
8// Need to set header group overflow to unset to allow sticky header labels.9export const style: Grid.Style = { headerGroup: { style: { overflow: "unset" } } };10export function StickGroupHeader(props: Grid.T.HeaderGroupParams<GridSpec>) {11 return (12 <div style={{ position: "sticky", insetInlineStart: "calc(var(--ln-start-offset) + 8px)" }}>13 {props.groupPath.at(-1)}14 </div>15 );16}17
18export function DateCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {19 const field = api.columnField(column, row);20
21 if (typeof field !== "string") return "-";22
23 const dateField = parse(field as string, "MM/dd/yyyy", new Date());24
25 if (!isValid(dateField)) return "-";26
27 const niceDate = format(dateField, "yyyy MMM dd");28 return <div className="flex h-full w-full items-center tabular-nums">{niceDate}</div>;29}30
31export function AgeGroupPivotHeader({ column }: Grid.T.HeaderParams<GridSpec>) {32 const field = column.name ?? column.id;33
34 if (field === "Under 25") return <div className="text-[#944cec] dark:text-[#B181EB]">Under 25</div>;35 if (field === "25-34") return <div className="text-[#aa6c1a] dark:text-[#E5B474]">25-34</div>;36 if (field === "35-64") return <div className="text-[#0f7d4c] dark:text-[#52B086]">35-64</div>;37
38 if (field === "Grand Total") return "Grand Total";39
40 return "Other";41}42
43export function AgeGroup({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {44 const field = api.columnField(column, row);45
46 if (field === "Under 25") return <div className="text-[#944cec] dark:text-[#B181EB]">Under 25</div>;47 if (field === "25-34") return <div className="text-[#aa6c1a] dark:text-[#E5B474]">25-34</div>;48 if (field === "35-64") return <div className="text-[#0f7d4c] dark:text-[#52B086]">35-64</div>;49
50 return "-";51}52
53export function GenderCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {54 const field = api.columnField(column, row);55
56 if (field === "M")57 return (58 <div className="flex h-full w-full items-center gap-2">59 <div className="flex size-6 items-center justify-center rounded-full bg-blue-500/50">60 <span className="iconify ph--gender-male-bold size-4" />61 </div>62 M63 </div>64 );65
66 if (field === "F")67 return (68 <div className="flex h-full w-full items-center gap-2">69 <div className="flex size-6 items-center justify-center rounded-full bg-pink-500/50">70 <span className="iconify ph--gender-female-bold size-4" />71 </div>72 F73 </div>74 );75
76 return "-";77}78
79function tw(...c: ClassValue[]) {80 return twMerge(clsx(...c));81}82
83const formatter = new Intl.NumberFormat("en-US", {84 maximumFractionDigits: 2,85 minimumFractionDigits: 0,86});87export function ProfitCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {88 const field = api.columnField(column, row);89
90 if (typeof field !== "number") return "-";91
92 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);93
94 return (95 <div96 className={tw(97 "flex h-full w-full items-center justify-end tabular-nums",98 field < 0 && "text-red-600 dark:text-red-300",99 field > 0 && "text-green-600 dark:text-green-300",100 )}101 >102 {formatted}103 </div>104 );105}106
107export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {108 const field = api.columnField(column, row);109
110 if (typeof field !== "number") return "-";111
112 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);113
114 return <div className={"flex h-full w-full items-center justify-end tabular-nums"}>{formatted}</div>;115}116
117export function CostCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {118 const field = api.columnField(column, row);119
120 if (typeof field !== "number") return "-";121
122 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);123
124 return (125 <div126 className={tw(127 "flex h-full w-full items-center justify-end tabular-nums text-red-600 dark:text-red-300",128 )}129 >130 {formatted}131 </div>132 );133}134
135export function CountryCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {136 const field = api.columnField(column, row);137
138 const flag = countryFlags[field as keyof typeof countryFlags];139 if (!flag) return "-";140
141 return (142 <div className="flex h-full w-full items-center gap-2">143 <img className="size-4" src={flag} alt={`country flag of ${field}`} />144 <span>{String(field ?? "-")}</span>145 </div>146 );147}Pivot Column Modification
LyteNyte Grid dynamically creates pivot columns based on the provided column
configuration. Set the pivotColumnProcessor property on the
useClientDataSource hook to modify these columns before they are rendered.
Using pivotColumnProcessor, you can modify dynamically created columns in any way
that suits your application. You can reorder or remove columns, or change any column
property.
The demo below uses pivotColumnProcessor to remove the Total columns created by
the column pivots.
Modifying Pivot Columns
1import "@1771technologies/lytenyte-pro/pill-manager.css";2import "@1771technologies/lytenyte-pro/components.css";3import "@1771technologies/lytenyte-pro/light-dark.css";4import { salesData, type SaleDataItem } from "@1771technologies/grid-sample-data/sales-data";5import { computeField, Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";6import {7 AgeGroup,8 CostCell,9 CountryCell,10 DateCell,11 GenderCell,12 NumberCell,13 ProfitCell,14 StickGroupHeader,15 style,16} from "./components.jsx";17import { sum } from "es-toolkit";18import { RowGroupCell } from "@1771technologies/lytenyte-pro/components";19
20export interface GridSpec {21 readonly data: SaleDataItem;22 readonly column: {23 pivotable?: boolean;24 };25}26
27export const columns: Grid.Column<GridSpec>[] = [28 { id: "date", name: "Date", cellRenderer: DateCell, width: 110 },29 { id: "age", name: "Age", type: "number", width: 80 },30 { id: "ageGroup", name: "Age Group", cellRenderer: AgeGroup, width: 110, pivotable: true },31 { id: "customerGender", name: "Gender", cellRenderer: GenderCell, width: 80, pivotable: true },32 { id: "country", name: "Country", cellRenderer: CountryCell, width: 150, pivotable: true },33 { id: "orderQuantity", name: "Quantity", type: "number", width: 60 },34 { id: "unitPrice", name: "Price", type: "number", width: 80, cellRenderer: NumberCell },35 { id: "cost", name: "Cost", width: 80, type: "number", cellRenderer: CostCell },36 { id: "revenue", name: "Revenue", width: 80, type: "number", cellRenderer: ProfitCell },37 { id: "profit", name: "Profit", width: 80, type: "number", cellRenderer: ProfitCell },38 { id: "state", name: "State", width: 150, pivotable: true },39 { id: "product", name: "Product", width: 160, pivotable: true },40 { id: "productCategory", name: "Category", width: 120, pivotable: true },41 { id: "subCategory", name: "Sub-Category", width: 160, pivotable: true },42];43
44const base: Grid.ColumnBase<GridSpec> = { width: 120, widthFlex: 1 };45
46const group: Grid.RowGroupColumn<GridSpec> = {47 cellRenderer: RowGroupCell,48 width: 200,49 pin: "start",50};51
52const aggSum: Grid.T.Aggregator<GridSpec["data"]> = (field, data) => {53 const values = data.map((x) => computeField<number>(field, x));54 return sum(values);55};56
57export default function PivotDemo() {58 const ds = useClientDataSource<GridSpec>({59 data: salesData,60 pivotMode: true,61 pivotColumnProcessor: (columns) => {62 return columns.filter((x) => !x.id.includes("total"));63 },64 pivotModel: {65 columns: [{ id: "ageGroup" }, { id: "customerGender" }],66 measures: [67 {68 dim: { id: "profit", name: "Profit", type: "number", cellRenderer: ProfitCell, width: 120 },69 fn: "sum",70 },71 ],72 },73 aggregateFns: { sum: aggSum },74 });75
76 const pivotProps = ds.usePivotProps();77
78 return (79 <div className="ln-grid" style={{ height: 500 }}>80 <Grid81 columns={columns}82 rowSource={ds}83 columnBase={base}84 rowGroupColumn={group}85 {...pivotProps}86 styles={style}87 columnGroupRenderer={StickGroupHeader}88 />89 </div>90 );91}1import type { Grid } from "@1771technologies/lytenyte-pro";2import type { GridSpec } from "./demo";3import { format, isValid, parse } from "date-fns";4import { countryFlags } from "@1771technologies/grid-sample-data/sales-data";5import { clsx, type ClassValue } from "clsx";6import { twMerge } from "tailwind-merge";7
8// Need to set header group overflow to unset to allow sticky header labels.9export const style: Grid.Style = { headerGroup: { style: { overflow: "unset" } } };10export function StickGroupHeader(props: Grid.T.HeaderGroupParams<GridSpec>) {11 return (12 <div style={{ position: "sticky", insetInlineStart: "calc(var(--ln-start-offset) + 8px)" }}>13 {props.groupPath.at(-1)}14 </div>15 );16}17
18export function DateCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {19 const field = api.columnField(column, row);20
21 if (typeof field !== "string") return "-";22
23 const dateField = parse(field as string, "MM/dd/yyyy", new Date());24
25 if (!isValid(dateField)) return "-";26
27 const niceDate = format(dateField, "yyyy MMM dd");28 return <div className="flex h-full w-full items-center tabular-nums">{niceDate}</div>;29}30
31export function AgeGroupPivotHeader({ column }: Grid.T.HeaderParams<GridSpec>) {32 const field = column.name ?? column.id;33
34 if (field === "Under 25") return <div className="text-[#944cec] dark:text-[#B181EB]">Under 25</div>;35 if (field === "25-34") return <div className="text-[#aa6c1a] dark:text-[#E5B474]">25-34</div>;36 if (field === "35-64") return <div className="text-[#0f7d4c] dark:text-[#52B086]">35-64</div>;37
38 if (field === "Grand Total") return "Grand Total";39
40 return "Other";41}42
43export function AgeGroup({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {44 const field = api.columnField(column, row);45
46 if (field === "Under 25") return <div className="text-[#944cec] dark:text-[#B181EB]">Under 25</div>;47 if (field === "25-34") return <div className="text-[#aa6c1a] dark:text-[#E5B474]">25-34</div>;48 if (field === "35-64") return <div className="text-[#0f7d4c] dark:text-[#52B086]">35-64</div>;49
50 return "-";51}52
53export function GenderCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {54 const field = api.columnField(column, row);55
56 if (field === "M")57 return (58 <div className="flex h-full w-full items-center gap-2">59 <div className="flex size-6 items-center justify-center rounded-full bg-blue-500/50">60 <span className="iconify ph--gender-male-bold size-4" />61 </div>62 M63 </div>64 );65
66 if (field === "F")67 return (68 <div className="flex h-full w-full items-center gap-2">69 <div className="flex size-6 items-center justify-center rounded-full bg-pink-500/50">70 <span className="iconify ph--gender-female-bold size-4" />71 </div>72 F73 </div>74 );75
76 return "-";77}78
79function tw(...c: ClassValue[]) {80 return twMerge(clsx(...c));81}82
83const formatter = new Intl.NumberFormat("en-US", {84 maximumFractionDigits: 2,85 minimumFractionDigits: 0,86});87export function ProfitCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {88 const field = api.columnField(column, row);89
90 if (typeof field !== "number") return "-";91
92 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);93
94 return (95 <div96 className={tw(97 "flex h-full w-full items-center justify-end tabular-nums",98 field < 0 && "text-red-600 dark:text-red-300",99 field > 0 && "text-green-600 dark:text-green-300",100 )}101 >102 {formatted}103 </div>104 );105}106
107export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {108 const field = api.columnField(column, row);109
110 if (typeof field !== "number") return "-";111
112 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);113
114 return <div className={"flex h-full w-full items-center justify-end tabular-nums"}>{formatted}</div>;115}116
117export function CostCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {118 const field = api.columnField(column, row);119
120 if (typeof field !== "number") return "-";121
122 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);123
124 return (125 <div126 className={tw(127 "flex h-full w-full items-center justify-end tabular-nums text-red-600 dark:text-red-300",128 )}129 >130 {formatted}131 </div>132 );133}134
135export function CountryCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {136 const field = api.columnField(column, row);137
138 const flag = countryFlags[field as keyof typeof countryFlags];139 if (!flag) return "-";140
141 return (142 <div className="flex h-full w-full items-center gap-2">143 <img className="size-4" src={flag} alt={`country flag of ${field}`} />144 <span>{String(field ?? "-")}</span>145 </div>146 );147}The code for the pivot column processor is shown below. The processor returns a new array of columns based on the columns generated by the grid.
1const ds = useClientDataSource<GridSpec>({2 data: salesData,3 pivotMode: true,4 pivotColumnProcessor: (columns) => {5 return columns.filter((x) => !x.id.includes("total"));6 },7 pivotModel: {8 columns: [{ id: "ageGroup" }, { id: "customerGender" }],9 measures: [10 {11 dim: { id: "profit", name: "Profit", type: "number", cellRenderer: ProfitCell, width: 120 },12 fn: "sum",13 },14 ],15 },16 aggregateFns: { sum: aggSum },17});Pivot Rows
Add pivot row groups to extend the pivot view by another dimension. Set the rows property
on the pivotModel to group rows before measurement. Note that, unlike standard
row grouping, the final level of a pivot row group represents an
aggregated result and cannot be expanded.
The demo below shows pivot rows. Each pivot row adds a grouping level. Since pivoting rotates data dimensions, row and column definitions are interchangeable. Click the Swap button to invert the current configuration.
Row Pivots
1import "@1771technologies/lytenyte-pro/pill-manager.css";2import "@1771technologies/lytenyte-pro/components.css";3import "@1771technologies/lytenyte-pro/light-dark.css";4import { salesData, type SaleDataItem } from "@1771technologies/grid-sample-data/sales-data";5import { computeField, Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";6import {7 AgeGroup,8 CostCell,9 CountryCell,10 DateCell,11 GenderCell,12 NumberCell,13 ProfitCell,14 StickGroupHeader,15 style,16} from "./components.jsx";17import { sum } from "es-toolkit";18import { useMemo, useState } from "react";19import { PillManager, RowGroupCell } from "@1771technologies/lytenyte-pro/components";20
21export interface GridSpec {22 readonly data: SaleDataItem;23 readonly column: {24 pivotable?: boolean;25 };26}27
28export const columns: Grid.Column<GridSpec>[] = [29 { id: "date", name: "Date", cellRenderer: DateCell, width: 110 },30 { id: "age", name: "Age", type: "number", width: 80 },31 { id: "ageGroup", name: "Age Group", cellRenderer: AgeGroup, width: 110, pivotable: true },32 { id: "customerGender", name: "Gender", cellRenderer: GenderCell, width: 80, pivotable: true },33 { id: "country", name: "Country", cellRenderer: CountryCell, width: 150, pivotable: true },34 { id: "orderQuantity", name: "Quantity", type: "number", width: 60 },35 { id: "unitPrice", name: "Price", type: "number", width: 80, cellRenderer: NumberCell },36 { id: "cost", name: "Cost", width: 80, type: "number", cellRenderer: CostCell },37 { id: "revenue", name: "Revenue", width: 80, type: "number", cellRenderer: ProfitCell },38 { id: "profit", name: "Profit", width: 80, type: "number", cellRenderer: ProfitCell },39 { id: "state", name: "State", width: 150, pivotable: true },40 { id: "product", name: "Product", width: 160, pivotable: true },41 { id: "productCategory", name: "Category", width: 120, pivotable: true },42 { id: "subCategory", name: "Sub-Category", width: 160, pivotable: true },43];44
45const base: Grid.ColumnBase<GridSpec> = { width: 120, resizable: true };46
47const group: Grid.RowGroupColumn<GridSpec> = {48 cellRenderer: RowGroupCell,49 width: 200,50 pin: "start",51};52
53const aggSum: Grid.T.Aggregator<GridSpec["data"]> = (field, data) => {54 const values = data.map((x) => computeField<number>(field, x));55 return sum(values);56};57
58export default function PivotDemo() {59 const [colPivots, setColPivots] = useState<PillManager.T.PillItem[]>(() =>60 columns61 .filter((x) => x.pivotable)62 .map((x) => ({ id: x.id, name: x.name ?? x.id, active: x.id === "ageGroup" })),63 );64 const [rowPivots, setRowPivots] = useState<PillManager.T.PillItem[]>(() => {65 const pivotables = columns66 .filter((x) => x.pivotable)67 .map((x) => ({68 id: x.id,69 name: x.name ?? x.id,70 active: x.id === "country",71 }));72
73 const active = [...pivotables.filter((x) => x.active)];74 const notActive = [...pivotables.filter((x) => !x.active)];75
76 return [...active, ...notActive];77 });78
79 const ds = useClientDataSource<GridSpec>({80 data: salesData,81 pivotMode: true,82 pivotModel: {83 columns: colPivots.filter((x) => x.active),84 rows: rowPivots.filter((x) => x.active),85 measures: [86 {87 dim: { id: "profit", name: "Profit", type: "number", cellRenderer: ProfitCell, width: 120 },88 fn: "sum",89 },90 ],91 },92 aggregateFns: { sum: aggSum },93 });94
95 const pillRows = useMemo<PillManager.T.PillRow[]>(() => {96 const colPivotPills = colPivots.map((x) => {97 return {98 id: x.id,99 active: x.active,100 movable: x.active,101 name: x.name ?? x.id,102 data: x,103 removable: true,104 };105 });106 const rowPivotPills = rowPivots.map((x) => {107 return {108 id: x.id,109 active: x.active,110 movable: x.active,111 name: x.name ?? x.id,112 data: x,113 removable: true,114 };115 });116 return [117 {118 id: "column-pivots",119 label: "Column Pivots",120 type: "column-pivots",121 pills: colPivotPills,122 accepts: ["row-pivots"],123 },124 {125 id: "row-pivots",126 label: "Row Pivots",127 type: "row-pivots",128 pills: rowPivotPills,129 accepts: ["column-pivots"],130 },131 ];132 }, [colPivots, rowPivots]);133
134 const pivotProps = ds.usePivotProps();135 const [groupColumn, setGroupColumn] = useState(group);136 return (137 <>138 <div className="@container">139 <PillManager140 rows={pillRows}141 onPillItemActiveChange={(p) => {142 setColPivots((prev) => {143 const next = prev.map((x) =>144 x.id === p.item.id ? { ...x, active: p.item.active && p.row.id === "column-pivots" } : x,145 );146 return [...next.filter((x) => x.active), ...next.filter((x) => !x.active)];147 });148 setRowPivots((prev) => {149 const next = prev.map((x) =>150 x.id === p.item.id ? { ...x, active: p.item.active && p.row.id === "row-pivots" } : x,151 );152 return [...next.filter((x) => x.active), ...next.filter((x) => !x.active)];153 });154 }}155 onPillRowChange={(ev) => {156 for (const changed of ev.changed) {157 const activeFirst = changed.pills.filter((x) => x.active);158 const nonActive = changed.pills.filter((x) => !x.active);159 if (changed.id === "column-pivots") {160 setColPivots([...activeFirst, ...nonActive]);161 }162 if (changed.id === "row-pivots") {163 setRowPivots([...activeFirst, ...nonActive]);164 }165 }166 }}167 >168 {(row) => {169 return (170 <PillManager.Row row={row} className="relative">171 {row.id === "column-pivots" && (172 <SwapPivots173 onSwap={() => {174 setColPivots(rowPivots);175 setRowPivots(colPivots);176 }}177 />178 )}179 <PillManager.Label row={row} />180 <PillManager.Container>181 {row.pills.map((x) => {182 return <PillManager.Pill item={x} key={x.id} />;183 })}184 </PillManager.Container>185 <PillManager.Expander />186 </PillManager.Row>187 );188 }}189 </PillManager>190 </div>191 <div className="ln-grid" style={{ height: 500 }}>192 <Grid193 columns={columns}194 rowSource={ds}195 columnBase={base}196 rowGroupColumn={groupColumn}197 onRowGroupColumnChange={setGroupColumn}198 {...pivotProps}199 columnGroupRenderer={StickGroupHeader}200 styles={style}201 />202 </div>203 </>204 );205}206
207function SwapPivots({ onSwap }: { onSwap: () => void }) {208 return (209 <button210 className="bg-ln-gray-02 border-ln-border-strong text-ln-primary-50 hover:bg-ln-gray-10 z-1 @2xl:-bottom-2.5 @2xl:left-7 absolute -bottom-2 left-3 flex cursor-pointer items-center gap-1 rounded-2xl border px-1 py-0.5 text-[10px]"211 onClick={() => onSwap()}212 >213 <span>214 <svg width="12" height="11" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">215 <path216 d="M13.4819 6.09082L12.0422 7.53048L10.6025 6.09082"217 stroke="currentcolor"218 strokeWidth="1.5"219 strokeLinecap="round"220 strokeLinejoin="round"221 />222 <path223 d="M0.749587 7.42017L2.18925 5.98051L3.62891 7.42017"224 stroke="currentcolor"225 strokeWidth="1.5"226 strokeLinecap="round"227 strokeLinejoin="round"228 />229 <path230 d="M12.0962 7.34067C12.1038 7.22805 12.1077 7.11441 12.1077 6.99985C12.1077 4.261 9.88744 2.04072 7.14859 2.04072C5.7388 2.04072 4.46641 2.62899 3.5635 3.57345M2.23753 6.30658C2.20584 6.53312 2.18945 6.76457 2.18945 6.99985C2.18945 9.73871 4.40973 11.959 7.14859 11.959C8.68732 11.959 10.0624 11.2582 10.972 10.1583"231 stroke="currentcolor"232 strokeWidth="1.5"233 strokeLinecap="round"234 strokeLinejoin="round"235 />236 </svg>237 </span>238 <span className="sr-only">Swap row and column pivots</span>239 <span className="@2xl:block hidden">SWAP</span>240 </button>241 );242}1import type { Grid } from "@1771technologies/lytenyte-pro";2import type { GridSpec } from "./demo";3import { format, isValid, parse } from "date-fns";4import { countryFlags } from "@1771technologies/grid-sample-data/sales-data";5import { clsx, type ClassValue } from "clsx";6import { twMerge } from "tailwind-merge";7
8// Need to set header group overflow to unset to allow sticky header labels.9export const style: Grid.Style = { headerGroup: { style: { overflow: "unset" } } };10export function StickGroupHeader(props: Grid.T.HeaderGroupParams<GridSpec>) {11 return (12 <div style={{ position: "sticky", insetInlineStart: "calc(var(--ln-start-offset) + 8px)" }}>13 {props.groupPath.at(-1)}14 </div>15 );16}17
18export function DateCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {19 const field = api.columnField(column, row);20
21 if (typeof field !== "string") return "-";22
23 const dateField = parse(field as string, "MM/dd/yyyy", new Date());24
25 if (!isValid(dateField)) return "-";26
27 const niceDate = format(dateField, "yyyy MMM dd");28 return <div className="flex h-full w-full items-center tabular-nums">{niceDate}</div>;29}30
31export function AgeGroupPivotHeader({ column }: Grid.T.HeaderParams<GridSpec>) {32 const field = column.name ?? column.id;33
34 if (field === "Under 25") return <div className="text-[#944cec] dark:text-[#B181EB]">Under 25</div>;35 if (field === "25-34") return <div className="text-[#aa6c1a] dark:text-[#E5B474]">25-34</div>;36 if (field === "35-64") return <div className="text-[#0f7d4c] dark:text-[#52B086]">35-64</div>;37
38 if (field === "Grand Total") return "Grand Total";39
40 return "Other";41}42
43export function AgeGroup({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {44 const field = api.columnField(column, row);45
46 if (field === "Under 25") return <div className="text-[#944cec] dark:text-[#B181EB]">Under 25</div>;47 if (field === "25-34") return <div className="text-[#aa6c1a] dark:text-[#E5B474]">25-34</div>;48 if (field === "35-64") return <div className="text-[#0f7d4c] dark:text-[#52B086]">35-64</div>;49
50 return "-";51}52
53export function GenderCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {54 const field = api.columnField(column, row);55
56 if (field === "M")57 return (58 <div className="flex h-full w-full items-center gap-2">59 <div className="flex size-6 items-center justify-center rounded-full bg-blue-500/50">60 <span className="iconify ph--gender-male-bold size-4" />61 </div>62 M63 </div>64 );65
66 if (field === "F")67 return (68 <div className="flex h-full w-full items-center gap-2">69 <div className="flex size-6 items-center justify-center rounded-full bg-pink-500/50">70 <span className="iconify ph--gender-female-bold size-4" />71 </div>72 F73 </div>74 );75
76 return "-";77}78
79function tw(...c: ClassValue[]) {80 return twMerge(clsx(...c));81}82
83const formatter = new Intl.NumberFormat("en-US", {84 maximumFractionDigits: 2,85 minimumFractionDigits: 0,86});87export function ProfitCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {88 const field = api.columnField(column, row);89
90 if (typeof field !== "number") return "-";91
92 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);93
94 return (95 <div96 className={tw(97 "flex h-full w-full items-center justify-end tabular-nums",98 field < 0 && "text-red-600 dark:text-red-300",99 field > 0 && "text-green-600 dark:text-green-300",100 )}101 >102 {formatted}103 </div>104 );105}106
107export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {108 const field = api.columnField(column, row);109
110 if (typeof field !== "number") return "-";111
112 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);113
114 return <div className={"flex h-full w-full items-center justify-end tabular-nums"}>{formatted}</div>;115}116
117export function CostCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {118 const field = api.columnField(column, row);119
120 if (typeof field !== "number") return "-";121
122 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);123
124 return (125 <div126 className={tw(127 "flex h-full w-full items-center justify-end tabular-nums text-red-600 dark:text-red-300",128 )}129 >130 {formatted}131 </div>132 );133}134
135export function CountryCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {136 const field = api.columnField(column, row);137
138 const flag = countryFlags[field as keyof typeof countryFlags];139 if (!flag) return "-";140
141 return (142 <div className="flex h-full w-full items-center gap-2">143 <img className="size-4" src={flag} alt={`country flag of ${field}`} />144 <span>{String(field ?? "-")}</span>145 </div>146 );147}The demo uses the Pill Manager component to configure pivots. React state stores the pivot configuration and updates it when pills are clicked or dragged. The updated pivot state is then mapped to the client data source, as shown in the code below.
1const [colPivots, setColPivots] = useState<PillManager.T.PillItem[]>(initialPivots);2const [rowPivots, setRowPivots] = useState<PillManager.T.PillItem[]>(initialColPivots);3
4const ds = useClientDataSource<GridSpec>({5 data: salesData,6 pivotMode: true,7 pivotModel: {8 columns: colPivots.filter((x) => x.active),9 rows: rowPivots.filter((x) => x.active),10 measures: [11 {12 dim: { id: "profit", name: "Profit", type: "number", cellRenderer: ProfitCell, width: 120 },13 fn: "sum",14 },15 ],16 },17 aggregateFns: { sum: aggSum },18});Next Steps
- Measures: Aggregate and summarize row data when pivoting.
- Pivot Filters: Filter pivots by labels or predicate conditions.
- Grand Totals: Display a grand total across all pivot measures.
