Measures
Configure measures to aggregate values across pivot rows and columns.
To create a measure, define a custom function that receives an array of leaf rows and a field value, then returns a summarized result. The return value can be any type, but it’s typically numeric.
Note
This guide assumes familiarity with row aggregations. See the Client Row Aggregations guide for details.
Defining Measures
Define a measure by setting the measure property on the pivot model. LyteNyte Grid uses a measure
for two purposes:
- Aggregate row data based on the row pivot and column pivot dimensions.
- If the measure dimension is a column, use it as a reference for the columns created from the pivot configuration.
The demo below shows a measure that sums Profit. Rows are pivoted by Country and columns by Age Group, creating a profit breakdown per country and age demographic.
Sum of Profit Measure
1import "@1771technologies/lytenyte-pro/components.css";2import "@1771technologies/lytenyte-pro/light-dark.css";3import { salesData, type SaleDataItem } from "@1771technologies/grid-sample-data/sales-data";4import { computeField, Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";5import {6 AgeGroup,7 CostCell,8 CountryCell,9 DateCell,10 GenderCell,11 NumberCell,12 ProfitCell,13 StickGroupHeader,14 style,15} from "./components.jsx";16import { sum } from "es-toolkit";17import { RowGroupCell } from "@1771technologies/lytenyte-pro/components";18
19export interface GridSpec {20 readonly data: SaleDataItem;21 readonly column: {22 pivotable?: boolean;23 };24}25
26export const columns: Grid.Column<GridSpec>[] = [27 { id: "date", name: "Date", cellRenderer: DateCell, width: 110 },28 { id: "age", name: "Age", type: "number", width: 80 },29 { id: "ageGroup", name: "Age Group", cellRenderer: AgeGroup, width: 110, pivotable: true },30 { id: "customerGender", name: "Gender", cellRenderer: GenderCell, width: 80, pivotable: true },31 { id: "country", name: "Country", cellRenderer: CountryCell, width: 150, pivotable: true },32 { id: "orderQuantity", name: "Quantity", type: "number", width: 60 },33 { id: "unitPrice", name: "Price", type: "number", width: 80, cellRenderer: NumberCell },34 { id: "cost", name: "Cost", width: 80, type: "number", cellRenderer: CostCell },35 { id: "revenue", name: "Revenue", width: 80, type: "number", cellRenderer: ProfitCell },36 { id: "profit", name: "Profit", width: 80, type: "number", cellRenderer: ProfitCell },37 { id: "state", name: "State", width: 150, pivotable: true },38 { id: "product", name: "Product", width: 160, pivotable: true },39 { id: "productCategory", name: "Category", width: 120, pivotable: true },40 { id: "subCategory", name: "Sub-Category", width: 160, pivotable: true },41];42
43const base: Grid.ColumnBase<GridSpec> = { width: 120, widthFlex: 1 };44
45const group: Grid.RowGroupColumn<GridSpec> = {46 cellRenderer: RowGroupCell,47 width: 200,48 pin: "start",49};50
51const aggSum: Grid.T.Aggregator<GridSpec["data"]> = (field, data) => {52 const values = data.map((x) => computeField<number>(field, x));53 return sum(values);54};55
56export default function PivotDemo() {57 const ds = useClientDataSource<GridSpec>({58 data: salesData,59 pivotMode: true,60 pivotModel: {61 columns: [{ id: "ageGroup" }],62 rows: [{ id: "country" }],63 measures: [64 {65 dim: { id: "profit", name: "Profit", type: "number", cellRenderer: ProfitCell, width: 120 },66 fn: "sum",67 },68 ],69 },70 aggregateFns: { sum: aggSum },71 });72
73 const pivotProps = ds.usePivotProps();74 return (75 <div className="ln-grid" style={{ height: 500 }}>76 <Grid77 columns={columns}78 rowSource={ds}79 columnBase={base}80 rowGroupColumn={group}81 {...pivotProps}82 styles={style}83 columnGroupRenderer={StickGroupHeader}84 />85 </div>86 );87}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 below shows how to apply the measure. This demo uses a predefined measure function called
"sum", but you can also provide a function inline if that better suits your application’s
use case.
1const aggSum: Grid.T.Aggregator<GridSpec["data"]> = (field, data) => {2 const values = data.map((x) => computeField<number>(field, x));3 return sum(values);4};5
6const ds = useClientDataSource<GridSpec>({7 data: salesData,8 pivotMode: true,9 pivotModel: {10 columns: [{ id: "ageGroup" }],11 rows: [{ id: "country" }],12 measures: [13 {14 dim: { id: "profit", name: "Profit", type: "number", cellRenderer: ProfitCell, width: 120 },15 fn: "sum",16 },17 ],18 },19 aggregateFns: { sum: aggSum },20});Configuring Multiple Measures
You can apply multiple measures to the pivot configuration by providing additional dimensions. The order of measures does not affect aggregation behavior, but it does influence the initial column order when pivots are applied.
The demo below demonstrates multiple measures in action. You can also update the measure function by clicking the function name in the pill and selecting a different measure for that column.
Multiple Measures
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 DecimalCell,12 GenderCell,13 NumberCell,14 ProfitCell,15} from "./components.jsx";16import { sum } from "es-toolkit";17import { useMemo, useState } from "react";18import { Menu, PillManager, RowGroupCell } from "@1771technologies/lytenyte-pro/components";19
20export interface GridSpec {21 readonly data: SaleDataItem;22 readonly column: {23 measurable?: 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 },31 { id: "customerGender", name: "Gender", cellRenderer: GenderCell, width: 80 },32 { id: "country", name: "Country", cellRenderer: CountryCell, width: 150 },33
34 { id: "revenue", name: "Revenue", width: 80, type: "number", cellRenderer: ProfitCell, measurable: true },35 { id: "profit", name: "Profit", width: 100, type: "number", cellRenderer: ProfitCell, measurable: true },36 { id: "unitPrice", name: "Price", type: "number", width: 80, cellRenderer: NumberCell, measurable: true },37 { id: "cost", name: "Cost", width: 80, type: "number", cellRenderer: CostCell, measurable: true },38 {39 id: "orderQuantity",40 name: "Quantity",41 type: "number",42 width: 80,43 measurable: true,44 cellRenderer: DecimalCell,45 },46
47 { id: "state", name: "State", width: 150 },48 { id: "product", name: "Product", width: 160 },49 { id: "productCategory", name: "Category", width: 120 },50 { id: "subCategory", name: "Sub-Category", width: 160 },51];52
53const base: Grid.ColumnBase<GridSpec> = { width: 130, widthFlex: 1 };54
55const group: Grid.RowGroupColumn<GridSpec> = {56 cellRenderer: RowGroupCell,57 width: 200,58 pin: "start",59};60
61const aggSum: Grid.T.Aggregator<GridSpec["data"]> = (field, data) => {62 const values = data.map((x) => computeField<number>(field, x));63 return sum(values);64};65const aggAvg: Grid.T.Aggregator<GridSpec["data"]> = (field, data) => {66 const values = data.map((x) => computeField<number>(field, x));67 return sum(values) / values.length;68};69const aggMin: Grid.T.Aggregator<GridSpec["data"]> = (field, data) => {70 const values = data.map((x) => computeField<number>(field, x));71 return Math.min(...values);72};73const aggMax: Grid.T.Aggregator<GridSpec["data"]> = (field, data) => {74 const values = data.map((x) => computeField<number>(field, x));75 return Math.max(...values);76};77
78export default function PivotDemo() {79 const [measures, setMeasures] = useState<PillManager.T.PillItem[]>(() => {80 return columns81 .filter((x) => x.measurable)82 .map<PillManager.T.PillItem>((x) => ({83 id: x.id,84 active: x.id === "profit" || x.id === "revenue",85 movable: true,86 name: x.name,87 }));88 });89
90 const ds = useClientDataSource<GridSpec>({91 data: salesData,92 pivotMode: true,93 pivotModel: {94 columns: [{ id: "ageGroup" }],95 rows: [{ id: "country" }],96 measures: measures97 .filter((x) => x.active)98 .map((x) => {99 const column = columns.find((c) => x.id === c.id)!;100 return {101 dim: column,102 fn: (x.data as string) ?? "sum",103 };104 }),105 },106 aggregateFns: { sum: aggSum, avg: aggAvg, min: aggMin, max: aggMax },107 });108
109 const pillRows = useMemo<PillManager.T.PillRow[]>(() => {110 const measurePills = measures.map((x) => {111 return {112 id: x.id,113 active: x.active,114 name: x.name ?? x.id,115 data: x.data,116 removable: true,117 };118 });119 return [120 {121 id: "measures",122 label: "Measures",123 type: "measures",124 pills: measurePills,125 },126 ];127 }, [measures]);128
129 const pivotProps = ds.usePivotProps();130 return (131 <>132 <PillManager133 rows={pillRows}134 onPillItemActiveChange={(p) => {135 setMeasures((prev) => {136 const next = prev.map((x) =>137 x.id === p.item.id ? { ...x, active: p.item.active && p.row.id === "measures" } : x,138 );139 return [...next.filter((x) => x.active), ...next.filter((x) => !x.active)];140 });141 }}142 onPillRowChange={(ev) => {143 for (const changed of ev.changed) {144 const activeFirst = changed.pills.filter((x) => x.active);145 const nonActive = changed.pills.filter((x) => !x.active);146 if (changed.id === "measures") {147 setMeasures([...activeFirst, ...nonActive]);148 }149 }150 }}151 >152 {(row) => {153 return (154 <PillManager.Row row={row}>155 <PillManager.Label row={row} />156 <PillManager.Container>157 {row.pills.map((x) => {158 return (159 <PillManager.Pill160 item={x}161 key={x.id}162 elementEnd={163 x.active ? (164 <div className="text-ln-primary-50 ms-1">165 <Menu>166 <Menu.Trigger167 onClick={(e) => e.stopPropagation()}168 className="text-ln-primary-50 hover:bg-ln-primary-30 cursor-pointer rounded-lg px-0.5 py-0.5 text-[10px]"169 >170 ({(x.data as string) ?? "sum"})171 </Menu.Trigger>172 <Menu.Popover>173 <Menu.Container>174 <Menu.Arrow />175
176 {["sum", "avg", "max", "min"].map((agg) => {177 return (178 <Menu.Item179 key={agg}180 onAction={() => {181 setMeasures((prev) => {182 const next = [...prev];183 const nexIndex = prev.findIndex((m) => m.id === x.id);184 if (nexIndex === -1) return prev;185
186 next[nexIndex] = { ...next[nexIndex], data: agg.toLowerCase() };187 return next;188 });189 }}190 >191 {agg}192 </Menu.Item>193 );194 })}195 </Menu.Container>196 </Menu.Popover>197 </Menu>198 </div>199 ) : null200 }201 />202 );203 })}204 </PillManager.Container>205 <PillManager.Expander />206 </PillManager.Row>207 );208 }}209 </PillManager>210 <div className="ln-grid" style={{ height: 500 }}>211 <Grid212 columns={columns}213 rowSource={ds}214 columnBase={base}215 rowGroupColumn={group}216 {...pivotProps}217 styles={{218 headerGroup: {219 style: { position: "sticky", insetInlineStart: "var(--ln-start-offset)", overflow: "unset" },220 },221 }}222 />223 </div>224 </>225 );226}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
8export function DateCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {9 const field = api.columnField(column, row);10
11 if (typeof field !== "string") return "-";12
13 const dateField = parse(field as string, "MM/dd/yyyy", new Date());14
15 if (!isValid(dateField)) return "-";16
17 const niceDate = format(dateField, "yyyy MMM dd");18 return <div className="flex h-full w-full items-center tabular-nums">{niceDate}</div>;19}20
21export function AgeGroupPivotHeader({ column }: Grid.T.HeaderParams<GridSpec>) {22 const field = column.name ?? column.id;23
24 if (field === "Under 25") return <div className="text-[#944cec] dark:text-[#B181EB]">Under 25</div>;25 if (field === "25-34") return <div className="text-[#aa6c1a] dark:text-[#E5B474]">25-34</div>;26 if (field === "35-64") return <div className="text-[#0f7d4c] dark:text-[#52B086]">35-64</div>;27
28 if (field === "Grand Total") return "Grand Total";29
30 return "Other";31}32
33export function AgeGroup({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {34 const field = api.columnField(column, row);35
36 if (field === "Under 25") return <div className="text-[#944cec] dark:text-[#B181EB]">Under 25</div>;37 if (field === "25-34") return <div className="text-[#aa6c1a] dark:text-[#E5B474]">25-34</div>;38 if (field === "35-64") return <div className="text-[#0f7d4c] dark:text-[#52B086]">35-64</div>;39
40 return "-";41}42
43export function GenderCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {44 const field = api.columnField(column, row);45
46 if (field === "M")47 return (48 <div className="flex h-full w-full items-center gap-2">49 <div className="flex size-6 items-center justify-center rounded-full bg-blue-500/50">50 <span className="iconify ph--gender-male-bold size-4" />51 </div>52 M53 </div>54 );55
56 if (field === "F")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-pink-500/50">60 <span className="iconify ph--gender-female-bold size-4" />61 </div>62 F63 </div>64 );65
66 return "-";67}68
69function tw(...c: ClassValue[]) {70 return twMerge(clsx(...c));71}72
73const formatter = new Intl.NumberFormat("en-US", {74 maximumFractionDigits: 0,75 minimumFractionDigits: 0,76});77export function ProfitCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {78 const field = api.columnField(column, row);79
80 if (typeof field !== "number") return "-";81
82 const prefix = "$";83 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : prefix + formatter.format(field);84
85 return (86 <div87 className={tw(88 "flex h-full w-full items-center justify-end tabular-nums",89 prefix && field < 0 && "text-red-600 dark:text-red-300",90 prefix && field > 0 && "text-green-600 dark:text-green-300",91 )}92 >93 {formatted}94 </div>95 );96}97
98export function DecimalCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {99 const field = api.columnField(column, row);100
101 if (typeof field !== "number") return "-";102
103 const formatted = formatter.format(field);104
105 return <div className={"flex h-full w-full items-center justify-end tabular-nums"}>{formatted}</div>;106}107
108export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {109 const field = api.columnField(column, row);110
111 if (typeof field !== "number") return "-";112
113 const prefix = "$";114 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : prefix + formatter.format(field);115
116 return <div className={"flex h-full w-full items-center justify-end tabular-nums"}>{formatted}</div>;117}118
119export function CostCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {120 const field = api.columnField(column, row);121
122 if (typeof field !== "number") return "-";123
124 const prefix = "$";125 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : prefix + formatter.format(field);126
127 return (128 <div129 className={tw(130 "flex h-full w-full items-center justify-end tabular-nums text-red-600 dark:text-red-300",131 )}132 >133 {formatted}134 </div>135 );136}137
138export function CountryCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {139 const field = api.columnField(column, row);140
141 const flag = countryFlags[field as keyof typeof countryFlags];142 if (!flag) return "-";143
144 return (145 <div className="flex h-full w-full items-center gap-2">146 <img className="size-4" src={flag} alt={`country flag of ${field}`} />147 <span>{String(field ?? "-")}</span>148 </div>149 );150}Be aware that measures multiply your column count. If a pivot generates 20 columns, two measures produce 40 columns, and three produce 60.
Multiple Measures Per Column
You can measure the same column multiple times to apply different measure functions.
The demo below measures Profit with both sum and avg. The grid treats each measure as distinct, even when both reference the same column.
Column Multiple Measures
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 measurable?: 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 },31 { id: "customerGender", name: "Gender", cellRenderer: GenderCell, width: 80 },32 { id: "country", name: "Country", cellRenderer: CountryCell, width: 150 },33
34 { id: "profit", name: "Profit", width: 100, type: "number", cellRenderer: ProfitCell, measurable: true },35 { id: "orderQuantity", name: "Quantity", type: "number", width: 60, measurable: true },36 { id: "unitPrice", name: "Price", type: "number", width: 80, cellRenderer: NumberCell, measurable: true },37 { id: "cost", name: "Cost", width: 80, type: "number", cellRenderer: CostCell, measurable: true },38 { id: "revenue", name: "Revenue", width: 80, type: "number", cellRenderer: ProfitCell, measurable: true },39
40 { id: "state", name: "State", width: 150 },41 { id: "product", name: "Product", width: 160 },42 { id: "productCategory", name: "Category", width: 120 },43 { id: "subCategory", name: "Sub-Category", width: 160 },44];45
46const base: Grid.ColumnBase<GridSpec> = { width: 130, widthFlex: 1 };47
48const group: Grid.RowGroupColumn<GridSpec> = {49 cellRenderer: RowGroupCell,50 width: 200,51 pin: "start",52};53
54const aggSum: Grid.T.Aggregator<GridSpec["data"]> = (field, data) => {55 const values = data.map((x) => computeField<number>(field, x));56 return sum(values);57};58const aggAvg: Grid.T.Aggregator<GridSpec["data"]> = (field, data) => {59 const values = data.map((x) => computeField<number>(field, x));60 return sum(values) / values.length;61};62
63export default function PivotDemo() {64 const ds = useClientDataSource<GridSpec>({65 data: salesData,66 pivotMode: true,67 pivotModel: {68 columns: [{ id: "ageGroup" }],69 rows: [{ id: "country" }],70 measures: [71 {72 dim: {73 ...columns.find((x) => x.id === "profit")!,74 id: "Profit (sum)",75 field: "profit",76 name: "Profit (sum)",77 width: 100,78 },79 fn: "sum",80 },81 {82 dim: {83 ...columns.find((x) => x.id === "profit")!,84 id: "Profit (avg)",85 field: "profit",86 name: "Profit (avg)",87 width: 100,88 },89 fn: "avg",90 },91 ],92 },93 aggregateFns: { sum: aggSum, avg: aggAvg },94 });95
96 const pivotProps = ds.usePivotProps();97 return (98 <>99 <div className="ln-grid" style={{ height: 500 }}>100 <Grid101 columns={columns}102 rowSource={ds}103 columnBase={base}104 rowGroupColumn={group}105 {...pivotProps}106 styles={style}107 columnGroupRenderer={StickGroupHeader}108 />109 </div>110 </>111 );112}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: 0,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}LyteNyte Grid identifies measures by id. When you apply multiple measures to the same column,
you must provide a unique ID for each measure. The example below shows how to configure this:
1const ds = useClientDataSource<GridSpec>({2 pivotModel: {3 columns: [{ id: "ageGroup" }],4 rows: [{ id: "country" }],5 measures: [6 {7 dim: {8 ...columns.find((x) => x.id === "profit")!,9 id: "Profit (sum)",10 field: "profit",11 name: "Profit (sum)",12 width: 100,13 },14 fn: "sum",15 },16 {17 dim: {18 ...columns.find((x) => x.id === "profit")!,19 id: "Profit (avg)",20 field: "profit",21 name: "Profit (avg)",22 width: 100,23 },24 fn: "avg",25 },26 ],27 },28 aggregateFns: { sum: aggSum, avg: aggAvg },29});Next Steps
- Row & Column Pivots: Configure row and column dimensions for pivot view.
- Grand Totals: Display a grand total across all pivot measures.
- Pivot Sorting: Customize the sort order of pivoted row groups.
