Skip to content

PivotViewer dimensions and filters

Dimensions define the values PivotViewer sorts by and groups by. The toolbar’s Sort by list shows one entry per dimension:

  • In collection view, the active dimension sorts the single card grid.
  • In grouped view, the active dimension splits the cards into columns. For a string or boolean dimension, clicking a column’s label below the grid shows only that column’s cards, and clicking it again shows all of them. For a numeric dimension, the click only highlights the label; the cards are not filtered.
interface PivotDimension<TItem> {
key: string; // Unique identifier; also the column name in PivotViewer's store
label: string; // Shown in the Sort by list
getValue: (item: TItem) => PivotPrimitive; // string | number | boolean | Date | null | undefined
formatValue?: (value: PivotPrimitive) => string; // Changes displayed group labels
sort?: (a: PivotGroup<TItem>, b: PivotGroup<TItem>) => number; // Orders groups in grouped view
}

How a dimension groups depends on the first non-missing (null or undefined) value returned by getValue:

ValueGrouped view
stringOne column per distinct value, sorted by value. Missing values appear as the string labels null and undefined.
numberTen equal-width numeric ranges between the smallest and largest value; missing values are excluded from the ranges.
DateConverted to a numeric timestamp, so it groups into ten ranges of milliseconds; missing values are excluded.
booleanSeparate false and true columns; missing values are not grouped with false.

In grouped view, items missing a numeric or boolean grouping value have no column, although the toolbar count still includes them. String dimensions instead show missing values in null and undefined columns. If every value is missing, the dimension uses string grouping. To group by exact numbers, years, or dates, return a string: String(item.priority) or String(item.createdAt.getFullYear()).

formatValue changes only the displayed group label, not the group’s key or filter value. For numeric and Date dimensions, the formatter receives the numeric lower bound of each range (a millisecond timestamp for Dates), not the original item value. sort orders groups before layout; its comparator receives PivotGroup objects with the original items, key, label, value, and count. Both callbacks run in the viewer after the engine returns, so they work the same whether grouping runs in a worker or the synchronous fallback. Without a custom sort, groups retain value order.

The snippets on this page use the Task type from the complete example, the Product type from the overview, and an illustrative Person type with a numeric age.

import type { PivotDimension } from '@cratis/components/PivotViewer';
const dimensions: PivotDimension<Task>[] = [
{ key: 'status', label: 'Status', getValue: (item) => item.status },
{ key: 'assignee', label: 'Assigned To', getValue: (item) => item.assignee },
{
key: 'month',
label: 'Created',
getValue: (item) =>
`${item.createdAt.getFullYear()}-${String(item.createdAt.getMonth() + 1).padStart(2, '0')}`,
},
];

The month dimension returns zero-padded text such as 2024-03, so its columns sort in calendar order. Boolean columns normally read false and true, while the filter panel labels those options No and Yes. To use the same labels in both places, set formatValue: value => value ? 'Yes' : 'No' on the boolean dimension.

Filters appear in the filter panel, opened from the filter button at the left of the toolbar. The button, and with it the panel’s search box, appears only when filters has at least one entry. A badge on the button counts the active selections.

  1. Categorical filter (default): a list of values with counts. Single-select unless you set multi: true.
  2. Range filter (type: 'number'): a histogram with a minimum/maximum range selection.

A filter and a dimension with the same key share one column in PivotViewer’s store, and the filter’s getValue wins. Give them the same getValue, or different keys. Categorical filters work on string and boolean columns; a categorical selection on a number column does not narrow the cards. This also applies when the numeric column’s first value is missing: the first non-missing value determines its type, so selecting a categorical option still does not narrow it.

The type union also contains 'date' and 'custom', and a filter can supply its own editor through renderEditor. This page covers the categorical and numeric filters that the PivotViewer stories exercise.

const categoryFilter: PivotFilter<Product> = {
key: 'category',
label: 'Category',
getValue: (item) => item.category,
};

Options and their counts are computed from data, most frequent first. Pass options to supply a fixed list, or sort to order the options yourself. Each option’s count reflects the other active filters.

Without multi, selecting a value replaces the previous selection and selecting it again clears it.

const priceFilter: PivotFilter<Product> = {
key: 'price',
label: 'Price',
type: 'number',
getValue: (item) => item.price,
buckets: 20,
};

buckets sets the number of histogram bars (default 20). getValue must return a number for every item that should take part in the range.

Set multi: true to let users select several values of one categorical filter. An item matches when its value is any of the selected values:

const statusFilter: PivotFilter<Task> = {
key: 'status',
label: 'Status',
getValue: (item) => item.status,
multi: true,
};

getValue returns one value per item. PivotViewer has no filter for array-valued properties such as tags: PivotFilter.getValue does not accept an array type, and a value that is not a primitive is converted to one string (['a', 'b'] becomes 'a,b'). To let users find items by tag, list the property in searchFields instead; see Search configuration.

import type { PivotDimension, PivotFilter } from '@cratis/components/PivotViewer';
interface Task {
id: string;
title: string;
description: string;
status: 'todo' | 'in-progress' | 'done';
priority: number;
assignee: string;
tags: string[];
estimatedHours: number;
createdAt: Date;
}
const taskDimensions: PivotDimension<Task>[] = [
{ key: 'status', label: 'Status', getValue: (item) => item.status },
{ key: 'assignee', label: 'Assignee', getValue: (item) => item.assignee },
{
key: 'priority-level',
label: 'Priority Level',
getValue: (item) => {
if (item.priority >= 8) return 'High';
if (item.priority >= 5) return 'Medium';
return 'Low';
},
},
{
key: 'year',
label: 'Year',
getValue: (item) => String(item.createdAt.getFullYear()),
},
{
key: 'quarter',
label: 'Quarter',
getValue: (item) => {
const quarter = Math.floor(item.createdAt.getMonth() / 3) + 1;
return `${item.createdAt.getFullYear()} Q${quarter}`;
},
},
];
const taskFilters: PivotFilter<Task>[] = [
{ key: 'status', label: 'Status', getValue: (item) => item.status, multi: true },
{ key: 'priority', label: 'Priority', type: 'number', getValue: (item) => item.priority, buckets: 10 },
{
key: 'estimatedHours',
label: 'Estimated Hours',
type: 'number',
getValue: (item) => item.estimatedHours,
buckets: 20,
},
{ key: 'assignee', label: 'Assigned To', getValue: (item) => item.assignee, multi: true },
];

Define these arrays outside the component, or memoize them. PivotViewer rebuilds its store whenever the array identities change.

Create dimensions from computed values. Return strings so each band becomes its own column:

const ageGroup: PivotDimension<Person> = {
key: 'age-group',
label: 'Age Group',
getValue: (item) => {
if (item.age < 18) return 'Under 18';
if (item.age < 35) return '18-34';
if (item.age < 55) return '35-54';
return '55+';
},
};

Columns are sorted in plain string order, so in this example 'Under 18' comes last, after '55+'. Supply a dimension sort comparator to change the order without changing the values.

A Date or a numeric year groups into ten numeric ranges, not one column per year. Convert dates to text:

const byYear: PivotDimension<Task> = {
key: 'year',
label: 'Year',
getValue: (item) => String(item.createdAt.getFullYear()),
};
const byMonth: PivotDimension<Task> = {
key: 'month',
label: 'Month',
getValue: (item) =>
`${item.createdAt.getFullYear()}-${String(item.createdAt.getMonth() + 1).padStart(2, '0')}`,
};
const byQuarter: PivotDimension<Task> = {
key: 'quarter',
label: 'Quarter',
getValue: (item) => `${item.createdAt.getFullYear()} Q${Math.floor(item.createdAt.getMonth() / 3) + 1}`,
};

Put the year first so the text sorts chronologically.

  1. Return strings for exact numeric or date categories: numbers and dates produce ranges, while booleans produce false and true columns
  2. Limit dimensions: every dimension and filter adds a column to the store PivotViewer builds on the main thread
  3. Use clear labels: the label is what users see in the Sort by list and the filter panel
  4. Consider cardinality: a dimension with hundreds of distinct values produces hundreds of columns
  5. Balance filters: provide both broad (status) and narrow (assignee) options, with multi: true where users compare several values
  6. Keep accessors cheap: getValue runs for every item whenever data, dimensions, or filters change, and again when filter options are recounted
  7. Provide defaults: set a sensible defaultDimensionKey