Lattice Grid Buy a licence

developer guide

Data Grid Facet Filters and Header Histograms

A header histogram shows what is actually in a column, and clicking a bar filters to it, the way a shopping facet works. The counts are recomputed against the filters already applied, so what a reader sees is what they will get.

Developer guideSorting, filtering and find › Data Grid Facet Filters and Header Histograms

Header histograms and facet filtering

The shape of a column, drawn in its heading, and clickable. Explore a dataset by clicking through headers instead of writing queries.

Turning them on

facets: { enabled: true }

// per column, layered over the grid's settings
{ field: 'price', type: 'number', facet: { strategy: 'quantile' } }
{ field: 'notes', facet: false }

The column being filtered is not counted against its own filter. Every other active filter applies; that column's own conditions are pruned out of the tree before counting. This is the whole of faceted browsing and it is the part that is easy to get subtly wrong, a self-filtered chart collapses to a single bar the moment you click one, and there is then no way to see what you excluded or to widen the selection. Getting it wrong does not degrade the feature, it removes it.

Pruning is not symmetric across operators. An and group narrows with each condition, so dropping one widens the result, the direction faceting wants. An or group widens with each branch, so dropping one would show fewer rows than the user's actual filter. There is no partial answer that is correct, so a disjunction naming the column is dropped whole.

Bucket edges are placed once and kept. They are computed against the unfiltered column and survive every filter change until the data is replaced. Not only an optimisation: bars that resized on every click would make the chart unusable as a control, because the thing you are pointing at would move as you pointed at it.

Each bar carries two readings. Its full height is the bucket's share of the unfiltered column; the solid fill inside is how much survives the current filters. Either alone misleads: scaling to the filtered maximum draws a full-height chart out of three surviving rows, and scaling everything down together flattens the whole chart into a few pixels the moment anyone filters anything.

The filters are ordinary filters. They go through the same filters.set as everything else, so they undo, serialise into saved views, and appear in whatever filter UI already exists. Nothing downstream can tell a filter made by clicking a bar from one typed into the filter panel. A drag emits a between range rather than a set of bucket indices, so it still means the same thing after the data is replaced and the edges move.

Selection is derived, never stored. Which buckets look selected is read back out of the filter tree. Remove the filter through the filter panel, an undo or a saved view and the chart is correct without anything having to tell it.

High-cardinality columns are refused, and it costs nothing to know. Text columns are dictionary-encoded in the store, so the distinct count is a property read rather than a scan. The first column anyone points this at is a name or an id, and one hairline per customer looks like a rendering fault rather than a distribution. Above cardinalityLimit the chart is suppressed, or shows a top-N with an aggregated remainder if you ask for aboveLimit: 'topN': aggregated rather than truncated, because silently dropping the tail would misrepresent the bars it did draw.

Nulls are never dropped. They land in a terminal bucket, always last, and the counts always sum to the row count. A column where nine thousand of ten thousand rows are empty is a fact about the data, and a chart that quietly showed the thousand would be lying about the shape. NaN joins them rather than forming its own bucket: it is the same answer to the same question.

Live streams suppress the charts. Constantly shifting distributions are unreadable, recounting on every batch is wasteful, and a filter control whose buckets move under the pointer is actively hostile. Filters already made stay applied, because they are ordinary filters. Pausing the stream brings the charts back, a paused stream is a still one: unless you set whilePaused: false.

Counting runs off the main thread above workerThreshold. A distribution was the first work the grid moved to a Worker: nothing waits on a histogram, which is what made it offloadable without an async pipeline. A portable sort - a built-in collation with no custom comparator - now also recomputes off-thread above the threshold, serving the prior order until the new one lands. Filtering and grouping still run on the main thread. The column is copied, or shared where cross-origin isolation makes SharedArrayBuffer available; it is never transferred, because transferring would detach the buffer the grid is still rendering from.

The Worker settings. useWorker and workerThreshold decide whether and when a distribution is offloaded. Two more control how the Worker is built, and both are settled when it is constructed: changing either discards the running Worker so the next offload builds a new one.

SettingWhat it does
workerUrlLoads the Worker from a URL you host instead of a blob:. Required under a Content-Security-Policy that forbids blob: workers: without it the Worker cannot be constructed at all on such a page, and compute stays on the main thread.
sharedMemoryOff by default. Passes columns to the Worker in a SharedArrayBuffer rather than copying them on every message, at the cost of retaining a shared copy of each column that crosses. Needs the page to be cross-origin isolated; where it is not, it falls back to copying and says so once.

grid.diagnostics.renders().worker reports what the Worker host is actually doing: whether one was spawned, how many calls ran locally versus remotely, and the threshold, sharedMemory and workerUrl it was built with.

Server-side sources need a provider, and its absence is silent. A grid holding one page of data cannot compute a distribution over the whole set. Supply a function and it receives the column, the pruned filter state and the bucketing settings, and returns counts. Without one the charts are simply absent, no error, because most deployments will never supply one. Be clear-eyed about the load: results are cached against the filter state, but this is one query per column per filter change, and a grid with eight faceted columns asks eight questions every time a filter moves.

Keyboard and screen reader. Focus enters the chart from the header and arrows move between buckets, so a column costs one tab stop rather than twenty. Enter toggles, Shift with arrows extends a range on ordered columns, Escape clears. Selected buckets carry an outline as well as a colour. Beyond per-bucket labels the chart carries a sentence describing the distribution's shape, because twenty bucket readings do not add up to "most of the mass is at the low end", and that shape is the entire value of the chart.