useChartQuery
Read a relation from the same coordinator the charts use, under the same crossfilter — for anything that is not a plot.
Two readings of one relation: the first under the crossfilter, the second deliberately outside it
(filterBy={null}), so the pair reads as "N of M". Click a region bar — only the first number moves.
Usage
import { Query, count } from "@kanzo-tech/mosaic";
import { useChartQuery } from "@kanzo-tech/ui/analytics";The /analytics peers are optional — nothing installs them unless you import this subpath.
const { row } = useChartQuery({
query: (filter) => Query.from("telemetry").select({ n: count() }).where(filter),
});It needs a MosaicProvider above it, and
nothing else — no ChartRoot. A readout, a KPI, a table or a canvas is a peer of the plots, not a
child of one.
The failure it exists to prevent
A dashboard always has something that is not a plot, and it has to move with the brush like
everything else. The shortcut — engine.query() inside a useEffect — looks identical on the
first paint and then sits there reporting unfiltered totals beside filtered charts. Nobody reads
that as a stale widget; they read it as a broken crossfilter, three components away.
This hook connects a real MosaicClient instead. That is the whole protocol: declare a query, get
re-asked whenever any clause anyone publishes changes the predicate. ChartStat is this hook
wrapped around StatValue, and it is why a stat in one card follows a brush in another.
A panel as a client of the page's selection
Any panel that reads rows — a count, a summary, a list of findings — is this hook over the page's crossfilter: its query filtered by the live selection, re-run when it changes. Nothing else is needed, and nothing else exists for it.
// How many of a rule's findings are in scope — a lasso on the graph, a brush, a chip.
const { row } = useChartQuery({
query: (filter) => Query.from(findings).select({ n: count() }).where(filter),
deps: [findings],
});A clause names the columns it filters, so the panel's relation has to answer it: a semi-join on an
identity its rows carry always does — findings above keeps each vertex's dense_id, and the
graph's pick is dense_id IN (…). A column clause published on another relation reaches the panel
crossed into the one it says it reads — its relation — by the key the provider's joins share, or
keeps none of it where they share none (across relations).
filterBy
| Value | What it reads |
|---|---|
| omitted | the page's selection — the usual case |
null | the full relation, ignoring every clause |
a Selection | that one, for a widget wired to its own scope |
deps, and the query you pass
The builder is read from a ref, so an inline arrow does not reconnect the client — which matters,
because reconnecting re-runs the query and blanks rows back to null. The consequence is that a
value captured in the closure will not, on its own, re-ask the question: put it in deps.
const { rows } = useChartQuery({
query: (filter) => Query.from(table).select({ n: count() }).where(filter),
deps: [table],
});Return null from query to ask for nothing at all — the honest answer while a table name is still
being resolved.
API Reference
function useChartQuery(options: ChartQueryOptions): ChartQueryResult;Options
| Option | Type | Default |
|---|---|---|
query | (filter: FilterExpr) => Query | null | — |
filterBy | Selection | null | the page's selection; null reads the full relation |
relation | string | Relation | null — the relation the query reads, so a clause published on another reaches it crossed (across relations) | the enclosing MosaicClients's; with none, every clause as published |
deps | readonly unknown[] | [] |
Returns
| Field | Type | What it is |
|---|---|---|
rows | readonly ChartQueryRow[] | null | the rows, or null while the first query is in flight |
row | ChartQueryRow | undefined | rows[0], for the common single-aggregate case |
ChartQueryRow is Record<string, unknown> — Arrow's values arrive untyped, so coerce at the edge
(Number(row?.n ?? 0)) rather than casting the row.
Query is @kanzo-tech/mosaic's, so a query is built without a direct @uwdata/mosaic-sql import;
so are the aggregates (count, sum, avg, min, max, median, quantile, stddev, mode,
sql) and the two sort directions .orderby() takes, asc and desc — a bare column already
sorts ascending, so orderby(desc(col)) is how the other one is spelled.