Pivot Filters
Filter pivot rows using specific labels or predicate conditions.
Note
This guide assumes familiarity with general and client data source filtering. Before continuing, read:
Demos in this guide show a measure summing Profit. Rows are pivoted by Country and Product, columns by Age Group.
Filtering Pivots
Pivoting data always creates group rows because pivots aggregate data.
To apply filters to pivoted data, set the filter property on the pivot model. The filter
property accepts an array of HavingFilterFn functions.
The demo below demonstrates having filter functions. You can update the filter applied to the pivot column by clicking the funnel icon in the column header.
Having filters are applied to rows at a specific depth; in this case, the child rows of countries will be filtered. Refer to the Client Row Having Filters guide for more details.
Filtering Pivots
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 {5 computeField,6 Grid,7 useClientDataSource,8 usePiece,9 type PieceWritable,10} from "@1771technologies/lytenyte-pro";11import {12 AgeGroup,13 CostCell,14 CountryCell,15 DateCell,16 GenderCell,17 NumberCell,18 ProfitCell,19} from "./components.jsx";20import { sum } from "es-toolkit";21import { useMemo, useState } from "react";22import { Header } from "./filter.jsx";23import { RowGroupCell } from "@1771technologies/lytenyte-pro/components";24
25export interface GridSpec {26 readonly data: SaleDataItem;27 readonly api: {28 readonly filterModel: PieceWritable<Record<string, GridFilter>>;29 };30}31
32export type FilterNumberOperator =33 | "greater_than"34 | "greater_than_or_equals"35 | "less_than"36 | "less_than_or_equals"37 | "equals"38 | "not_equals";39
40export interface FilterNumber {41 readonly operator: FilterNumberOperator;42 readonly value: number;43}44
45export interface GridFilter {46 readonly left: FilterNumber;47}48
49export const columns: Grid.Column<GridSpec>[] = [50 { id: "date", name: "Date", cellRenderer: DateCell, width: 110 },51 { id: "age", name: "Age", type: "number", width: 80 },52 { id: "ageGroup", name: "Age Group", cellRenderer: AgeGroup, width: 110 },53 { id: "customerGender", name: "Gender", cellRenderer: GenderCell, width: 80 },54 { id: "country", name: "Country", cellRenderer: CountryCell, width: 150 },55 { id: "orderQuantity", name: "Quantity", type: "number", width: 60 },56 { id: "unitPrice", name: "Price", type: "number", width: 80, cellRenderer: NumberCell },57 { id: "cost", name: "Cost", width: 80, type: "number", cellRenderer: CostCell },58 { id: "revenue", name: "Revenue", width: 80, type: "number", cellRenderer: ProfitCell },59 { id: "profit", name: "Profit", width: 80, type: "number", cellRenderer: ProfitCell },60 { id: "state", name: "State", width: 150 },61 { id: "product", name: "Product", width: 160 },62 { id: "productCategory", name: "Category", width: 120 },63 { id: "subCategory", name: "Sub-Category", width: 160 },64];65
66const base: Grid.ColumnBase<GridSpec> = { width: 120 };67
68const group: Grid.RowGroupColumn<GridSpec> = {69 cellRenderer: RowGroupCell,70 width: 200,71 pin: "start",72};73
74const aggSum: Grid.T.Aggregator<GridSpec["data"]> = (field, data) => {75 const values = data.map((x) => computeField<number>(field, x));76 return sum(values);77};78
79export default function PivotDemo() {80 const [filter, setFilter] = useState<Record<string, GridFilter>>({});81 const filterModel = usePiece(filter, setFilter);82
83 const filterFn = useMemo<(Grid.T.HavingFilterFn | null)[]>(() => {84 const entries = Object.entries(filter);85
86 const evaluateNumberFilter = (operator: FilterNumberOperator, compare: number, value: number) => {87 if (operator === "equals") return value === compare;88 if (operator === "greater_than") return compare > value;89 if (operator === "greater_than_or_equals") return compare >= value;90 if (operator === "less_than") return compare < value;91 if (operator === "less_than_or_equals") return compare <= value;92 if (operator === "not_equals") return value !== compare;93
94 return false;95 };96
97 return [98 null,99 (row) => {100 for (const [column, filter] of entries) {101 const value = row.data[column as keyof GridSpec["data"]];102
103 // We are only working with number filters, so lets filter out none number104 if (typeof value !== "number") return false;105
106 const compareValue = value;107
108 if (!evaluateNumberFilter(filter.left.operator, compareValue, filter.left.value)) return false;109 }110
111 return true;112 },113 ];114 }, [filter]);115
116 const ds = useClientDataSource<GridSpec>({117 data: salesData,118 pivotMode: true,119 pivotModel: {120 columns: [{ id: "ageGroup" }],121 rows: [{ id: "country" }, { id: "productCategory" }],122 filter: filterFn,123 measures: [124 {125 dim: {126 id: "profit",127 name: "Profit",128 type: "number",129 cellRenderer: ProfitCell,130 width: 180,131 headerRenderer: Header,132 },133 fn: "sum",134 },135 ],136 },137 rowGroupDefaultExpansion: true,138 aggregateFns: { sum: aggSum },139 });140
141 const pivotProps = ds.usePivotProps();142 return (143 <>144 <div className="ln-grid" style={{ height: 500 }}>145 <Grid146 apiExtension={useMemo(() => ({ filterModel }), [filterModel])}147 columns={columns}148 rowSource={ds}149 columnBase={base}150 rowGroupColumn={group}151 {...pivotProps}152 />153 </div>154 </>155 );156}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: 2,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 formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);83
84 return (85 <div86 className={tw(87 "flex h-full w-full items-center justify-end tabular-nums",88 field < 0 && "text-red-600 dark:text-red-300",89 field > 0 && "text-green-600 dark:text-green-300",90 )}91 >92 {formatted}93 </div>94 );95}96
97export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {98 const field = api.columnField(column, row);99
100 if (typeof field !== "number") return "-";101
102 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);103
104 return <div className={"flex h-full w-full items-center justify-end tabular-nums"}>{formatted}</div>;105}106
107export function CostCell({ 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 (115 <div116 className={tw(117 "flex h-full w-full items-center justify-end tabular-nums text-red-600 dark:text-red-300",118 )}119 >120 {formatted}121 </div>122 );123}124
125export function CountryCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {126 const field = api.columnField(column, row);127
128 const flag = countryFlags[field as keyof typeof countryFlags];129 if (!flag) return "-";130
131 return (132 <div className="flex h-full w-full items-center gap-2">133 <img className="size-4" src={flag} alt={`country flag of ${field}`} />134 <span>{String(field ?? "-")}</span>135 </div>136 );137}1import "@1771technologies/lytenyte-pro/components.css";2import { type Grid } from "@1771technologies/lytenyte-pro";3import type { FilterNumberOperator, GridFilter, GridSpec } from "./demo";4import { CheckIcon, ChevronDownIcon } from "@radix-ui/react-icons";5import { useState } from "react";6import { twMerge } from "tailwind-merge";7import clsx, { type ClassValue } from "clsx";8import { Popover, SmartSelect } from "@1771technologies/lytenyte-pro/components";9
10function tw(...c: ClassValue[]) {11 return twMerge(clsx(...c));12}13
14export function Header({ api, column }: Grid.T.HeaderParams<GridSpec>) {15 const label = column.name ?? column.id;16
17 const model = api.filterModel.useValue();18 const hasFilter = !!model[column.id];19
20 return (21 <div22 className="group relative flex h-full w-full cursor-pointer items-center text-sm transition-colors"23 onClick={() => {}}24 >25 <div26 className={tw(27 "sort-button relative flex w-full items-center gap-1 rounded py-1 transition-colors",28 column.type === "number" && "flex-row-reverse",29 )}30 >31 {column.name ?? column.id}32
33 <Popover>34 <Popover.Trigger35 data-ln-button="secondary"36 data-ln-icon37 data-ln-size="xs"38 className="bg-ln-bg-ui-panel/80 hover:bg-ln-bg absolute -left-1"39 >40 <div className="sr-only">Filter the {label}</div>41 <svg42 xmlns="http://www.w3.org/2000/svg"43 width="16"44 height="16"45 fill="currentcolor"46 viewBox="0 0 256 256"47 >48 <path d="M230.6,49.53A15.81,15.81,0,0,0,216,40H40A16,16,0,0,0,28.19,66.76l.08.09L96,139.17V216a16,16,0,0,0,24.87,13.32l32-21.34A16,16,0,0,0,160,194.66V139.17l67.74-72.32.08-.09A15.8,15.8,0,0,0,230.6,49.53ZM40,56h0Zm106.18,74.58A8,8,0,0,0,144,136v58.66L112,216V136a8,8,0,0,0-2.16-5.47L40,56H216Z"></path>49 </svg>50 {hasFilter && <div className="bg-ln-primary-50 absolute right-px top-px size-2 rounded-full" />}51 </Popover.Trigger>52 <Popover.Container>53 <Popover.Arrow />54 <Popover.Title className="sr-only">Filter {label}</Popover.Title>55 <Popover.Description className="sr-only">Filter the numbers in the{label}</Popover.Description>56 <NumberFilterControl57 column={column}58 filter={model[column.id] ?? null}59 api={api}60 ></NumberFilterControl>61 </Popover.Container>62 </Popover>63 </div>64 </div>65 );66}67
68const selectOptions = [69 { id: "equals", label: "Equals" },70 { id: "not_equals", label: "Does Not Equal" },71 { id: "separator", selectable: false },72 { id: "greater_than", label: "Greater Than" },73 { id: "greater_than_or_equals", label: "Greater Than or Equal To" },74 { id: "less_than", label: "Less Than" },75 { id: "less_than_or_equals", label: "Less Than or Equal To" },76];77
78type DeepPartial<T> = T extends object79 ? {80 -readonly [P in keyof T]?: DeepPartial<T[P]>;81 }82 : T;83
84function NumberFilterControl({85 api,86 filter: initialFilter,87 column,88}: {89 api: Grid.API<GridSpec>;90 filter: GridFilter | null;91 column: Grid.Column<GridSpec>;92}) {93 const [filter, setFilter] = useState<DeepPartial<GridFilter> | null>(initialFilter);94
95 const leftValue = selectOptions.find((x) => x.id === filter?.left?.operator) ?? null;96
97 const canSubmit = filter?.left?.value != null && filter.left?.operator;98
99 const popoverControls = Popover.useControls();100 return (101 <form102 className="grid grid-cols-1 gap-2 md:grid-cols-2"103 onSubmit={(e) => {104 if (!canSubmit) return;105 e.preventDefault();106
107 const finalFilter: DeepPartial<GridFilter> = {};108 if (filter?.left?.value != null && filter?.left.operator) finalFilter.left = filter.left;109
110 api.filterModel.set((prev) => ({ ...prev, [column.id]: finalFilter as GridFilter }));111 popoverControls.openChange(false);112 }}113 >114 <div className="text-ln-text hidden ps-2 text-sm md:block">Operator</div>115 <div className="text-ln-text hidden ps-2 text-sm md:block">Values</div>116
117 <SmartSelect118 options={selectOptions}119 value={filter ? (selectOptions.find((x) => x.id === filter?.left?.operator) ?? null) : null}120 onOptionChange={(option) => {121 if (!option) return;122
123 setFilter((prev) => {124 if (!prev) return { left: { operator: option.id as FilterNumberOperator } };125 return { ...prev, left: { ...prev.left, operator: option.id as FilterNumberOperator } };126 });127 }}128 kind="basic"129 trigger={130 <SmartSelect.BasicTrigger131 type="button"132 data-ln-input133 className="flex min-w-40 cursor-pointer items-center justify-between"134 >135 <div>{leftValue?.label ?? "Select..."}</div>136 <div>137 <ChevronDownIcon />138 </div>139 </SmartSelect.BasicTrigger>140 }141 >142 {(p) => {143 if (p.option.id.startsWith("separator")) {144 return <div role="separator" className="bg-ln-gray-40 my-1 h-px w-full" />;145 }146
147 return (148 <SmartSelect.Option key={p.option.id} {...p} className="flex items-center justify-between">149 {p.option.label}150 {p.selected && <CheckIcon className="text-ln-primary-50" />}151 </SmartSelect.Option>152 );153 }}154 </SmartSelect>155
156 <div>157 <label>158 <span className="sr-only">Value for the first filter</span>159 <input160 data-ln-input161 value={filter?.left?.value ?? ""}162 className="w-full"163 type="number"164 onChange={(e) => {165 setFilter((prev) => {166 if (!prev) return { left: { value: Number.parseFloat(e.target.value) } };167
168 return { ...prev, left: { ...prev.left, value: Number.parseFloat(e.target.value) } };169 });170 }}171 />172 </label>173 </div>174
175 <div className="flex items-center justify-between gap-4 md:col-span-2 md:grid md:grid-cols-subgrid">176 <div className="pt-2">177 <button178 data-ln-button="tertiary"179 data-ln-size="sm"180 type="button"181 className="hover:bg-ln-gray-30"182 onClick={() => popoverControls.openChange(false)}183 >184 Cancel185 </button>186 </div>187 <div className="flex justify-end gap-2 pt-2">188 <button189 data-ln-button="secondary"190 data-ln-size="sm"191 type="button"192 className="hover:bg-ln-bg-button-light"193 onClick={() => {194 api.filterModel.set((prev) => {195 const next = { ...prev };196 delete next[column.id];197
198 return next;199 });200 popoverControls.openChange(false);201 }}202 >203 Clear204 </button>205 <button data-ln-button="primary" data-ln-size="sm" disabled={!canSubmit}>206 Apply Filters207 </button>208 </div>209 </div>210 </form>211 );212}The API extends the grid with a filter model. This model’s state is converted into a filter function and applied to the client data source’s pivot model, as shown below.
1const [filter, setFilter] = useState<Record<string, GridFilter>>({});2const filterModel = usePiece(filter, setFilter);3
4const filterFn = useMemo<Grid.T.HavingFilterFn>(() => {27 collapsed lines
5 const entries = Object.entries(filter);6
7 const evaluateNumberFilter = (operator: FilterNumberOperator, compare: number, value: number) => {8 if (operator === "equals") return value === compare;9 if (operator === "greater_than") return compare > value;10 if (operator === "greater_than_or_equals") return compare >= value;11 if (operator === "less_than") return compare < value;12 if (operator === "less_than_or_equals") return compare <= value;13 if (operator === "not_equals") return value !== compare;14
15 return false;16 };17
18 return (row) => {19 for (const [column, filter] of entries) {20 const value = row.data[column as keyof GridSpec["data"]];21
22 // This example only supports number filters, so exclude non-number values23 if (typeof value !== "number") return false;24
25 const compareValue = value;26
27 if (!evaluateNumberFilter(filter.left.operator, compareValue, filter.left.value)) return false;28 }29
30 return true;31 };32}, [filter]);33
34const ds = useClientDataSource<GridSpec>({35 data: salesData,36 pivotMode: true,37 pivotModel: {38 columns: [{ id: "ageGroup" }],39 rows: [{ id: "country" }, { id: "productCategory" }],40 filter: filterFn,41 measures: [12 collapsed lines
42 {43 dim: {44 id: "profit",45 name: "Profit",46 type: "number",47 cellRenderer: ProfitCell,48 width: 140,49 headerRenderer: Header,50 },51 fn: "sum",52 },53 ],54 },55 rowGroupDefaultExpansion: true,56 aggregateFns: { sum: aggSum },57});Pre-Pivot Row Filtering
By default, LyteNyte Grid does not apply standard row filters when pivoting and uses all rows to create pivot columns.
To apply standard row filters before pivoting,
set pivotApplyExistingFilter. The grid filters rows first,
then generates pivoted data.
In the demo below, the grid filters rows to include only entries where the Country is the United States or Germany. For row filtering details, see the Client Row Filtering guide.
Filter Before Pivot
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} from "./components.jsx";14import { sum } from "es-toolkit";15import { useCallback } from "react";16import { RowGroupCell } from "@1771technologies/lytenyte-pro/components";17
18export interface GridSpec {19 readonly data: SaleDataItem;20}21
22export const columns: Grid.Column<GridSpec>[] = [23 { id: "date", name: "Date", cellRenderer: DateCell, width: 110 },24 { id: "age", name: "Age", type: "number", width: 80 },25 { id: "ageGroup", name: "Age Group", cellRenderer: AgeGroup, width: 110 },26 { id: "customerGender", name: "Gender", cellRenderer: GenderCell, width: 80 },27 { id: "country", name: "Country", cellRenderer: CountryCell, width: 150 },28 { id: "orderQuantity", name: "Quantity", type: "number", width: 60 },29 { id: "unitPrice", name: "Price", type: "number", width: 80, cellRenderer: NumberCell },30 { id: "cost", name: "Cost", width: 80, type: "number", cellRenderer: CostCell },31 { id: "revenue", name: "Revenue", width: 80, type: "number", cellRenderer: ProfitCell },32 { id: "profit", name: "Profit", width: 80, type: "number", cellRenderer: ProfitCell },33 { id: "state", name: "State", width: 150 },34 { id: "product", name: "Product", width: 160 },35 { id: "productCategory", name: "Category", width: 120 },36 { id: "subCategory", name: "Sub-Category", width: 160 },37];38
39const base: Grid.ColumnBase<GridSpec> = { width: 120, widthFlex: 1 };40
41const group: Grid.RowGroupColumn<GridSpec> = {42 cellRenderer: RowGroupCell,43 width: 200,44 pin: "start",45};46
47const aggSum: Grid.T.Aggregator<GridSpec["data"]> = (field, data) => {48 const values = data.map((x) => computeField<number>(field, x));49 return sum(values);50};51
52export default function PivotDemo() {53 const ds = useClientDataSource<GridSpec>({54 data: salesData,55 pivotMode: true,56 pivotModel: {57 columns: [{ id: "ageGroup" }],58 rows: [{ id: "country" }, { id: "productCategory" }],59 measures: [60 {61 dim: {62 id: "profit",63 name: "Profit",64 type: "number",65 cellRenderer: ProfitCell,66 width: 140,67 },68 fn: "sum",69 },70 ],71 },72 pivotApplyExistingFilter: true,73 filter: useCallback<Grid.T.FilterFn<GridSpec["data"]>>(74 (row) => row.data.country === "United States" || row.data.country === "Germany",75 [],76 ),77 rowGroupDefaultExpansion: true,78 aggregateFns: { sum: aggSum },79 });80
81 const pivotProps = ds.usePivotProps();82 return (83 <>84 <div className="ln-grid" style={{ height: 500 }}>85 <Grid columns={columns} rowSource={ds} columnBase={base} rowGroupColumn={group} {...pivotProps} />86 </div>87 </>88 );89}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: 2,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 formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);83
84 return (85 <div86 className={tw(87 "flex h-full w-full items-center justify-end tabular-nums",88 field < 0 && "text-red-600 dark:text-red-300",89 field > 0 && "text-green-600 dark:text-green-300",90 )}91 >92 {formatted}93 </div>94 );95}96
97export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {98 const field = api.columnField(column, row);99
100 if (typeof field !== "number") return "-";101
102 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);103
104 return <div className={"flex h-full w-full items-center justify-end tabular-nums"}>{formatted}</div>;105}106
107export function CostCell({ 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 (115 <div116 className={tw(117 "flex h-full w-full items-center justify-end tabular-nums text-red-600 dark:text-red-300",118 )}119 >120 {formatted}121 </div>122 );123}124
125export function CountryCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {126 const field = api.columnField(column, row);127
128 const flag = countryFlags[field as keyof typeof countryFlags];129 if (!flag) return "-";130
131 return (132 <div className="flex h-full w-full items-center gap-2">133 <img className="size-4" src={flag} alt={`country flag of ${field}`} />134 <span>{String(field ?? "-")}</span>135 </div>136 );137}Label Filters
Pivoting creates labels for rows and columns. Use rowLabelFilter and colLabelFilter
to filter pivot results by row and column labels.
Label filters allow you to include or exclude specific identifiers from the pivot output. For details, see the Client Row Label Filters guide.
The demo below applies two label filters. The row label filter keeps only the United States and Germany. The column label filter excludes Youth (<25) age group.
Pivot Label Filters
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} from "./components.jsx";14import { sum } from "es-toolkit";15import { RowGroupCell } from "@1771technologies/lytenyte-pro/components";16
17export interface GridSpec {18 readonly data: SaleDataItem;19}20
21export const columns: Grid.Column<GridSpec>[] = [22 { id: "date", name: "Date", cellRenderer: DateCell, width: 110 },23 { id: "age", name: "Age", type: "number", width: 80 },24 { id: "ageGroup", name: "Age Group", cellRenderer: AgeGroup, width: 110 },25 { id: "customerGender", name: "Gender", cellRenderer: GenderCell, width: 80 },26 { id: "country", name: "Country", cellRenderer: CountryCell, width: 150 },27 { id: "orderQuantity", name: "Quantity", type: "number", width: 60 },28 { id: "unitPrice", name: "Price", type: "number", width: 80, cellRenderer: NumberCell },29 { id: "cost", name: "Cost", width: 80, type: "number", cellRenderer: CostCell },30 { id: "revenue", name: "Revenue", width: 80, type: "number", cellRenderer: ProfitCell },31 { id: "profit", name: "Profit", width: 80, type: "number", cellRenderer: ProfitCell },32 { id: "state", name: "State", width: 150 },33 { id: "product", name: "Product", width: 160 },34 { id: "productCategory", name: "Category", width: 120 },35 { id: "subCategory", name: "Sub-Category", width: 160 },36];37
38const base: Grid.ColumnBase<GridSpec> = { width: 120, widthFlex: 1 };39
40const group: Grid.RowGroupColumn<GridSpec> = {41 cellRenderer: RowGroupCell,42 width: 200,43 pin: "start",44};45
46const aggSum: Grid.T.Aggregator<GridSpec["data"]> = (field, data) => {47 const values = data.map((x) => computeField<number>(field, x));48 return sum(values);49};50
51export default function PivotDemo() {52 const ds = useClientDataSource<GridSpec>({53 data: salesData,54 pivotMode: true,55 pivotModel: {56 colLabelFilter: [57 (s) => {58 return s !== "Youth (<25)";59 },60 ],61 rowLabelFilter: [62 (s) => {63 return s === "Germany" || s === "United States";64 },65 ],66 columns: [{ id: "ageGroup" }],67 rows: [{ id: "country" }, { id: "productCategory" }],68 measures: [69 {70 dim: {71 id: "profit",72 name: "Profit",73 type: "number",74 cellRenderer: ProfitCell,75 width: 140,76 },77 fn: "sum",78 },79 ],80 },81 rowGroupDefaultExpansion: true,82 aggregateFns: { sum: aggSum },83 });84
85 const pivotProps = ds.usePivotProps();86 return (87 <>88 <div className="ln-grid" style={{ height: 500 }}>89 <Grid columns={columns} rowSource={ds} columnBase={base} rowGroupColumn={group} {...pivotProps} />90 </div>91 </>92 );93}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: 2,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 formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);83
84 return (85 <div86 className={tw(87 "flex h-full w-full items-center justify-end tabular-nums",88 field < 0 && "text-red-600 dark:text-red-300",89 field > 0 && "text-green-600 dark:text-green-300",90 )}91 >92 {formatted}93 </div>94 );95}96
97export function NumberCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {98 const field = api.columnField(column, row);99
100 if (typeof field !== "number") return "-";101
102 const formatted = field < 0 ? `-$${formatter.format(Math.abs(field))}` : "$" + formatter.format(field);103
104 return <div className={"flex h-full w-full items-center justify-end tabular-nums"}>{formatted}</div>;105}106
107export function CostCell({ 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 (115 <div116 className={tw(117 "flex h-full w-full items-center justify-end tabular-nums text-red-600 dark:text-red-300",118 )}119 >120 {formatted}121 </div>122 );123}124
125export function CountryCell({ api, row, column }: Grid.T.CellRendererParams<GridSpec>) {126 const field = api.columnField(column, row);127
128 const flag = countryFlags[field as keyof typeof countryFlags];129 if (!flag) return "-";130
131 return (132 <div className="flex h-full w-full items-center gap-2">133 <img className="size-4" src={flag} alt={`country flag of ${field}`} />134 <span>{String(field ?? "-")}</span>135 </div>136 );137}Label filters are always applied as an array. Each item in the array filters labels at the depth corresponding to its index. This means the first item filters the top-level labels, the second item filters the next level, and so on. The code below shows the label filters used in the demo.
1const ds = useClientDataSource<GridSpec>({2 data: salesData,3 pivotMode: true,4 pivotModel: {5 colLabelFilter: [(s) => s !== "Under 25"],6 rowLabelFilter: [(s) => s === "Germany" || s === "United States"],7 columns: [{ id: "ageGroup" }],8 rows: [{ id: "country" }, { id: "productCategory" }],12 collapsed lines
9 measures: [10 {11 dim: {12 id: "profit",13 name: "Profit",14 type: "number",15 cellRenderer: ProfitCell,16 width: 140,17 },18 fn: "sum",19 },20 ],21 },22 rowGroupDefaultExpansion: true,23 aggregateFns: { sum: aggSum },24});Next Steps
- Pivot Sorting: Customize the sort order of pivoted row groups.
- Row & Column Pivots: Configure row and column dimensions for pivot view.
- Measures: Aggregate and summarize row data when pivoting.
