Kanzo UI
Hooks

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

ValueWhat it reads
omittedthe page's selection — the usual case
nullthe full relation, ignoring every clause
a Selectionthat 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

OptionTypeDefault
query(filter: FilterExpr) => Query | null—
filterBySelection | nullthe page's selection; null reads the full relation
relationstring | 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
depsreadonly unknown[][]

Returns

FieldTypeWhat it is
rowsreadonly ChartQueryRow[] | nullthe rows, or null while the first query is in flight
rowChartQueryRow | undefinedrows[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.

On this page