Kanzo UI
Analytics

Dashboard

A dashboard over any relation for finding what it holds — a filter bar and tiles (figures, charts, tables), chosen from the relation's stats, edited where they sit, and kept as a JSON spec the host saves.

Dashboard is the charts grammar composed for a relation you have not seen. It reads the relation's fields from one DuckDB SUMMARIZE, draws a dashboard from them — or from a spec you saved — and lets the reader edit every tile of it. The host brings three things: a MosaicProvider, the relation, and somewhere to keep the spec. The relation can be a table or a join over a graph of types, and the dashboard cannot tell the two apart.

The references are Grafana's panel model — tiles of a kind in an ordered layout, each with a width — edited in place the way Notion and Linear edit a block, Metabase's filter bar drawn as Linear's chips, Metabase's X-ray for the dashboard a relation gets before anybody has edited one, and Looker's explores and Malloy's sources for a relation that joins. What it leaves out is their query layer: every tile reads the one relation the Dashboard is given, under one crossfilter.

Usage

import { Dashboard, MosaicProvider, type DashboardSpec } from "@kanzo-tech/ui/analytics";

function Sightings({ coordinator, saved, save }: Props) {
  return (
    <MosaicProvider coordinator={coordinator}>
      <Dashboard table="sightings" value={saved} onChange={save} rowNoun="sightings" />
    </MosaicProvider>
  );
}
  • value is undefined until somebody edits: the automatic dashboard is drawn, and a host persists nothing for a relation nobody has touched.
  • onChange makes it editable. It is called with the whole next spec — the automatic parts included, the first time — so the host stores what it is handed and never merges. Reset to automatic calls it with undefined: the host deletes what it stored, and the dashboard follows the stats again — undefined meaning automatic in both directions, as Metabase's X-ray stays unsaved until it is saved.
  • Without onChange it is read-only: no pencils, no Add tile, no remove buttons. Filters, picks and brushes still work; reading is not editing.

Wiring that to a backend — a debounced save, a read-only role, your own labels and colours — is the host recipe.

Anatomy

Dashboard                 — reads the fields, draws the spec or the automatic one
├── DashboardFilters      — the chip bar, its readout, "Clear", "Add tile"
├── DashboardStat         — a `stat` tile
├── ChartCard             — a `chart` tile
├── DetailTable           — inside a `table` tile: the rows under the selection, paged by DuckDB
└── TileEditor            — the popover a tile is added and edited in, anchored to the tile

The figures come first, as one band whose row they share equally, wrapping when they run out of room: a figure is one number and its trend, and beside a chart it would stand as tall as the chart. Charts and tables flow left to right through a three-column grid beneath it, each as wide as its span. Every part is exported and works on its own inside a MosaicProvider; Dashboard is what they make when the spec decides the arrangement. Arranging the parts yourself is the other way.

How a relation is read

useFieldStats(table) runs one SUMMARIZE — over SELECT * FROM the relation, built with mosaic-sql, so a join is summarized like a table — and classifies every column twice — by kind, from its DuckDB type through Mosaic's own jsType (so a column is numeric here exactly when it is numeric to every mark), and by role, from its name and its approximate distinct count.

KindDuckDB types
numericevery integer width up to HUGEINT, signed or not; DOUBLE, FLOAT, REAL, DECIMAL
temporalDATE, TIME, TIMESTAMP, TIMESTAMP_MS, TIMESTAMP_NS, TIMESTAMP WITH TIME ZONE
categoricalVARCHAR, ENUM, UUID, JSON, BOOLEAN
— not a fieldlists, arrays, structs, maps, blobs, GEOMETRY, and types Mosaic does not know (INTERVAL, BIT, UHUGEINT, TIMESTAMP_S, TIME WITH TIME ZONE)

The last three read as a number and two times, and are still not fields: every vgplot mark runs Mosaic's jsType on its columns before it queries, and throws on them. Cast such a column in the relation you hand over — ts::TIMESTAMP — and it is a field like any other.

RoleWhenUsed as
identifiernamed id, *_id, *Id, uri or iri; or text with more than 50 values and at least half as many values as rowssearched, never grouped
measureevery other numberaggregated, binned, either side of a scatter
dimensionevery time; text with ≤ 50 values, or fewer values than half the rowsgrouped, filtered, a series, a panel

The filter control and the charts a field can be follow from those two words alone:

FieldFilter controlCharts it can be
A timetimeline — a brushable histogram over time, first in the rowthe axis of a line, an area or a histogram; a tile's trend
A number (measure)ChartSlider, a rangean axis, a measure, either side of a scatter; a tile's trend
A category (dimension)ChartFilter, a facet filter with countsbars; series (2–8 values); panels (2–6 values)
A key (identifier text)ChartSearch, a match over what you type—
A numeric key (dense_id)none—

Past eight series a chart wants to be a table, not more hues; past six panels small multiples stop being comparable. Those are the dataviz limits, and the editors enforce them.

A relation with a time, and one control of every kind — the timeline over closed, facet filters, a search over id and a slider over reward:

A column named like a plot channel — x, y, x1, x2, y1, y2, fx, fy, z, fill, stroke — is never a field, and the plots read the relation without it. A layout's coordinates on a graph corpus are the usual case. Why, and what it costs.

The automatic dashboard

autoDashboard(fields) is the spec drawn while value is undefined — Metabase's X-ray, from the stats alone and nothing else:

  • Filters (at most six): the first time, up to three categories, the first text key, up to two measures.
  • Figures (the band): a row count and the mean of up to two measures, each trending along the first time when there is one.
  • Charts (at most six): the first six that recommend proposes for an overview.
  • A table, full width: the first eight fields — categories, then times, measures and keys.

The last tile of a row is widened so no row ends in a hole.

It is exported for a host that wants the automatic spec without the component — to seed a saved dashboard, or to diff against one.

recommend

recommend(fields, intent?) is every chart a table of rules proposes for those fields, heaviest first. It is CompassQL's and Draco's pipeline — enumerate what every rule matches, rank it, explain each — with the weights set by hand and no solver, since a rule reads at most two fields. Each answer is { spec, rationale, rule }: a card valid for the fields, one sentence saying why, and the rule that proposed it. Its ids are derived from the rule and the fields, so the same fields always give the same answer.

The rules are not the library's own: each is a case of Show Me (Mackinlay, Hanrahan and Stolte, 2007), the automatic presentation behind Tableau's chart picker, and a rule no Show Me case covers does not belong in the table. The weights are the one part with no published value.

ruleShow MeReadsDrawsoverviewanswerAt mostRationale
time-measureLines (continuous)a time and a measureline of the measure's total—1003day is a time and n a measure, so a line of total n over time
category-measureBarsa category and a measurebar of the measure's total—903type has 3 values and n is a measure, so a bar of total n per value
timeLines (continuous)a timeline of counts, two thirds wide100501seen is a time, so a line of counts along it
categoryBarsa category of 2 or more valuesbar of counts80403type has 3 values, so a bar of counts per value
measureHistograma measurehistogram60302bounty is a measure, so a histogram of how its values spread
measure-pairScatter plot, with a trend linetwo measuresregression of one on the other, two thirds wide40701bounty and leagues are both measures, so a fit of one against the other
  • intent is "overview" (the default) for a dashboard over raw rows — what each field does on its own — or "answer" for one chart of a result set, whose rows are usually aggregated already, so a measure beside what it was grouped by comes first. A rule with no weight under an intent is not proposed for it.
  • Ties keep the table's order, and a rule takes fields in the relation's order, up to its at most. A key is never drawn, and neither is a category with one value.
  • Add tile opens the editor on the best chart of the first field a rule draws on its own, preferring a field the dashboard does not chart yet.

A card says why it is there. autoDashboard records the rule behind each card it proposes as the card's origin ({ rule }), and while a card has one, ChartCard puts that rule's rationale before the interaction hint and an Automatic badge beside the title. Edit the type, a field, the measure, a series, a panel or the title and the origin goes, and both with it; the width is layout and does not count. A card added by hand has no origin, whatever the rules would say of it. The origin is part of the saved spec, and the sentence is read from the rule over the card's own fields on every render.

The parts

DashboardFilters

One filter bar above everything it scopes — Metabase's filter bar, drawn as Linear's chips. Each filters entry names a field and is a chip reading region: Saltmere ▾, or region: Any ▾ while it filters nothing. The chip opens its control, chosen from that field's stats and never written in the spec: a category is a facet list, a key a search, a measure a range slider, a time a brushable timeline. A field that has no control (a numeric key) or no longer exists is skipped. A control stays mounted while its popover is shut, because a control that unmounts retracts its clause.

After the chips, the readout: 12 of 40 sightings (rowNoun is the plural), and a quiet Clear while anything on the page filters, which is useMosaic().reset(). Children trail it — it is where Dashboard puts Add tile. With onChange, each chip gets a remove button and + Filter lists the fields that have a control and no filter yet. A clause something else published — a pick on a chart, a lasso — counts in the readout and goes with Clear; FilterChips is the part that lists every clause, for a host that wants them named.

DashboardStat

A stat tile: a Stat whose figure is ChartStat — the measure, re-queried under the crossfilter. With a trend it adds the same measure along that field as a StatTrend sparkline, and a StatDelta comparing the last step with the one before.

  • A time trend is binned into about two dozen steps; a number with at most 60 values is its own step (the hours of a day are 24 already), and a longer one is binned like a histogram.
  • The delta is in the step's own unit — vs previous closed, vs previous hour — because a relation without dates has no "last quarter" to compare with. A share measure's delta is in pp.
  • goodWhenUp: false paints a rise as bad news: a hoax rate, an error count.

Figures format by measure: a share as 12.5%, everything else to at most one decimal, compact past 10,000.

ChartCard

A chart tile, drawn. The chart is a ChartRoot with the marks the type calls for and the interactor its scale allows — pick on a band, brush on a range, the rule that keeps a brush off a band scale.

typeLabelDrawsxyInteraction
barBarhorizontal bars, the 12 largest groupsa categorya measureclick a bar
lineLinea line; a time or a number with > 60 values is binneda time or a numbera measuredrag a range
areaAreaan area, stacked by seriesa time or a numbera measuredrag a range
histogramHistogrambinned columnsa time or a numbera measuredrag a range
dotScatterone dot per rowa numbera number (op: "value")drag a box
regressionFitthe dots, a least-squares line and its 95% banda numbera number (op: "value")drag a box

Series (color) is a category of 2–8 values, on every type but regression. Panels (facet) is a category of 2–6 values, on every type but bar: a facet is an encoding, not a chart type — any line, area, histogram or scatter splits into small multiples sharing one pair of scales, which is how Vega-Lite models it too. The domains are chosen over the whole relation — the top bars by the measure, the top series and panels by count — so a filter never reorders a bar or repaints a survivor.

Without a series, the whole relation stays drawn behind the selection in grey: the context a filtered chart otherwise loses. A card with an origin shows why, under an Automatic badge. A card that names a field the relation no longer has draws The relation has no hall any more. in place of the chart.

With onEdit, the header carries a pencil, and so do a figure's and a table's: what it opens is the host's, and in Dashboard it is the TileEditor.

DetailTable

The rows behind the charts: one page at a time, sorted and counted by DuckDB, so a relation of any size costs one page of DOM. A header click cycles ascending, descending, unsorted; a new selection returns to page one. Its columns are a table tile's, chosen in the editor.

It is the root barrel's Table, not /table's: TanStack's row models sort and page in the browser, and here the browser never holds more than the page.

TileEditor

The one editor — a popover anchored to the tile it edits, the way Notion and Linear edit a block where it sits. Add tile and every tile's pencil open the same popover, in this order: the kind as cards with an icon, like a chart's mark (Figure, Chart, Table — a kind the relation cannot draw is disabled), the fields that kind reads, the title, and — for a chart or a table — the width (a third, two thirds, full), with the position among the figures or among the grid's tiles. The tile is the preview: it draws the draft in its own slot under the page's crossfilter, and grows or shrinks with the width; a tile being added gets a slot at the end of its band or grid, brought into view as the popover opens. Nothing reaches the dashboard until Add or Save; Cancel, Escape and a click outside drop the draft. A new position applies on save, so the tile never moves under the popover.

TileEditor draws no tile. Hand it the draft as tile with onChange, draw that draft in the tile's own view, and pass the view's element as anchor; mount the editor while it is open. The view stays mounted from the pencil to Save, so opening the editor does not rebuild its plot. Dashboard loads it the first time somebody edits, so a read-only dashboard never fetches the editor's code.

  • A chart asks for its mark — a mark the relation cannot draw is disabled — x, the measure, series and panels. Every change goes through the same normalisation: an encoding the new mark can keep is kept, one it cannot is replaced by the first field that fits, and an optional one that fits nothing is dropped. A written title describes the encodings it was written for, so changing them retires it.
  • A figure asks for the measure and the field it trends along.
  • A table asks for its columns; a newly ticked column goes last.
  • Changing the kind carries what carries over — a chart's aggregate becomes the figure, a figure's measure the chart's — and keeps the id, and the width between a chart and a table. A figure that becomes a chart moves from the band to the grid, and back.

Each kind is a row in three tables — how one is made, titled and converted (plain TypeScript, no React), how it is drawn, and what its editor asks for — each mapped over the kinds, so a kind missing from any of them does not compile and neither the board nor the editor switches on a kind.

FilterChips and useClauses

useClauses(selection) is a selection's live clause list. FilterChips draws it as removable badges — the crossfilter by default — and renders nothing while it is empty. A chip's label is read from the clause: hour 2 – 5 for a range, region Saltmere for one value, beast · 3 selected for more. Removing one calls retract, so the pick that made it un-highlights too.

Because they read Selection.clauses, they report filters they did not publish: a brush, a bar someone clicked, a lasso on a graph canvas. A semi-join wears its publisher's label — Lasso · 13 selected, a rule's name — so <FilterChips /> in a dock's header is the page's scope: every clause each client there is filtered by, each removable where it was published.

useFieldStats, queryFieldStats and fieldStats

useFieldStats(table, { exclude }) returns { fields, columns, error }: fields are the classified FieldStats, columns every column of the relation in its own order, fields or not. Both are null until the summary lands, and it re-asks when the relation or exclude changes — compared by value, so an inline array is fine. error is the thrown value, whole and typed unknown, and it is also handed to the provider's onFailure; narrow it before reading .message.

queryFieldStats(coordinator, table, options) is the same read as a promise resolving to { fields, columns }, for a host outside React or one that needs the fields before it renders.

fieldStats(rows, options) is the classification on its own: SUMMARIZE rows in, the same { fields, columns } out, nothing queried. queryFieldStats is SUMMARIZE on the coordinator and then this. Reach for it when the host runs the summary itself — on its own connection, or with an AbortSignal the coordinator does not take. A fossil corpus answers with its rows as arrays, so they are zipped with the column names first:

const summary = await corpus.sql(`SUMMARIZE ${relation}`, { signal });
const rows = summary.rows.map((row) => Object.fromEntries(summary.columns.map((name, i) => [name, row[i]])));
const { fields, columns } = fieldStats(rows as SummarizeRow[], { exclude: ["dense_id"] });

A row is a plain object under DuckDB's column names — column_name, column_type, approx_unique, min, max, count — with values as strings, numbers or bigints, the way any DuckDB client returns them.

plotRelation

plotRelation(table, { fields, columns }) returns { table, fields } as the plots can read them: the relation itself, or a projection without the columns named like a plot channel, and the fields without those. Dashboard calls it on its own summary; a host placing the parts over a relation with a layout's x/y calls it on useFieldStats' answer and hands the parts both halves — why.

exclude is for the host's bookkeeping — a key, an internal identity, a layout's coordinates under names that are not channels. An excluded column is still in the relation (a clause on it still applies); it is just never offered as a field.

Arranging the parts yourself

The parts take the same table and fields. A tile is editable when it is given an onEdit, and what that opens is the host's — here, the same TileEditor Dashboard uses, on the bar chart alone:

Dashboard does two things the parts do not: it drops the channel-named columns from the relation and from the fields, and it owns the spec. A host composing parts over a relation with an x column does the first itself — how.

Interaction

Every part is on the provider's crossfilter. A pick on a bar chart filters every other tile, and dims the rest of its own bars instead of filtering itself away. A brush on a line, a histogram or the timeline filters by a range. The readout counts what survives. A clause published by something else on the page — a ChartFilter you placed above the dashboard, a lasso on a graph — is applied the same way, and Clear retracts it too.

With publish, the tiles crossfilter each other in a selection of the dashboard's own, and the page sees one clause instead: the one publish maps them to. Everything above still holds inside the dashboard, and the page's clauses still reach every tile — see what the page sees.

Relations

The table can be any relation, and a relation that joins is one more TableExpr. A relation is a root type and the hops taken from it — Looker's explore, Malloy's source — over a join graph: types, each a table with a key, and the edge tables between them. A type on its own is a relation with no hops, so there is no second path for it.

import { Dashboard, RelationPicker, relationIdentities, relationKey, relationQuery } from "@kanzo-tech/ui/analytics";

<RelationPicker graph={graph} value={relation} onValueChange={setRelation} />
<Dashboard
  key={relationKey(graph, relation)}
  table={relationQuery(graph, relation)}
  exclude={relationIdentities(graph, relation).map((i) => i.column)}
  value={byRelation[relationKey(graph, relation)]}
/>
  • relationQuery(graph, relation) is one mosaic-sql Query: every type along the path joined through its edge tables, every column named by its step — Person.country, and Person2.country when the path visits a type twice. The root's key keeps its own name.
  • relationKey(graph, relation) is the canonical identity a saved dashboard is keyed by: Person, Person>knows>Person, Person<hasCreator<Post.
  • relationIdentities(graph, relation) is each step's key column and the type it identifies: what a clause on the relation crosses to anything else keyed the same way with, as a semi-join on identity. The root's key is the relation's own, so a clauseSemiJoin on it — a lasso on a graph — filters a joined relation exactly as it filters the root's table.
  • relationHops(graph, type) lists the hops that leave a type, out-edges then in-edges. RelationPicker is built on it: the root as a select, each hop as label → type, the last hop removable, and + Hop offering the edges that leave the last type.

What the page sees

A tile's clause on a relation names the relation's columns — "Person.country" IN ('ES') — and nothing else on the page has them: a graph beside the dashboard would refuse it as graph/unfilterable, and another relation's dashboard would fail to bind it. So a relation's dashboard publishes in the page's vocabulary instead, a semi-join on the root's key:

import { semiJoinOf } from "@kanzo-tech/ui/analytics";

const table = useMemo(() => relationQuery(graph, relation), [graph, relation]);
const publish = useMemo(() => semiJoinOf(relationIdentities(graph, relation)[0].column, table, { label: key }), [graph, relation, table, key]);

<Dashboard key={key} table={table} publish={publish} … />

The tiles publish into the dashboard's own crossfilter and filter by it, so they crossfilter each other on the relation's columns exactly as before. The page gets one clause for all of them, dense_id IN (SELECT dense_id FROM <relation> WHERE <every tile's clause>), whose source is the dashboard: the graph greys out every vertex but the roots of the rows the tiles keep, another relation's dashboard is filtered to the same vertices through its own root key, and FilterChips names it by label. The clauses are mapped together, not one by one — on a joined relation, a person with a row where a and b is not a person with a row where a, and one where b.

Retracting goes both ways. A tile that lets go withdraws the page's clause; removing the page's clause — its chip, the page's Clear — clears the tiles' brushes and picks. In the other direction, a lasso on the graph reaches every tile as itself, so the graph stays exempt from its own pick and is not filtered by it back through the dashboard.

semiJoinOf is one map among any: publish takes a ClauseMap, (clauses, source) => clause, and the bridge that carries it is @kanzo-tech/mosaic's bridgeSelection(inner, outer, map), built on mosaic-core's public Selection API alone. Without publish, a dashboard's clauses are the page's, which is right for a dashboard over one table beside charts over the same table.

The JoinGraph is plain data a reader builds from its own catalog — JoinType (name, table, key, columns) and JoinEdge (name, label, source, destination, table, and the src and dst columns holding each end's key). For a fossil corpus, @kanzo-tech/graph's readJoinGraph builds it from the catalog the graph already reads.

Series vocabulary

config maps a field to a ChartConfig, for a field drawn as color that has meanings of its own — a status column wearing the status colours and their icons:

<Dashboard
  table="sightings"
  config={{
    verdict: {
      confirmed: { label: "Confirmed", color: "var(--success)", icon: CheckCircle2Icon },
      hoax: { label: "Hoax", color: "var(--destructive)", icon: TriangleAlertIcon },
    },
  }}
/>

A configured field is drawn in the config's order and colours, with its labels in the legend. A field without one takes the categorical scheme in order of frequency over the whole relation, so a filter never repaints a survivor.

When the relation cannot be read

A failed SUMMARIZE — a relation that does not exist, a catalog that is not attached — draws an alert, The relation could not be summarized, with DuckDB's message, and reports the error to the provider's onFailure. A failed card, tile or input says so in its own frame and reports the same way. Nothing fails blank.

API Reference

Dashboard

PropTypeDefault
tableTableExpr — a name, a mosaic-sql node for a relation in another catalog, or a relationQuery—
valueDashboardSpecthe automatic dashboard
onChange(spec: DashboardSpec) => void— (read-only without it)
excludereadonly string[]—
configReadonly<Record<string, ChartConfig>>—
rowNounstring"rows"
publishClauseMap — what the page sees of the tiles' clauses; semiJoinOf for a relation— (the tiles' clauses are the page's)

Every other prop goes to the root div (data-slot="dashboard").

DashboardFilters

PropTypeDefault
tableTableExpr—
fieldsreadonly FieldStat[]—
filtersreadonly DashboardFilterSpec[]—
onChange(filters: DashboardFilterSpec[]) => void— (no remove, no + Filter)
rowNounstring"rows"
childrenReactNode— trailing controls, after Clear

DashboardStat

PropTypeDefault
tableTableExpr—
fieldsreadonly FieldStat[]—
statStatTile—
onEdit() => void— (no pencil)

ChartCard

PropTypeDefault
tableTableExpr—
fieldsreadonly FieldStat[]—
cardChartTile—
configReadonly<Record<string, ChartConfig>>—
onEdit() => void— (no pencil)

DetailTable

PropTypeDefault
tableTableExpr—
fieldsreadonly FieldStat[]—
columnsreadonly string[]— in order; a name that is not a field is skipped
pageSizenumber25

TileEditor

PropTypeDefault
tableTableExpr— what the tile reads
fieldsreadonly FieldStat[]—
tileTile— the tile to edit, or to add when tiles does not hold it. Mounted is open
tilesreadonly Tile[][] — where the tile sits, and what a change of kind avoids repeating
configReadonly<Record<string, ChartConfig>>—
onSave(tile: Tile, index: number) => void—
onRemove() => void— (no Remove)
onClose() => void— Cancel, Escape and a click outside

RelationPicker

PropTypeDefault
graphJoinGraph—
valueRelation—
onValueChange(relation: Relation) => void—
function relationQuery(graph: JoinGraph, relation: Relation): Query;
function relationKey(graph: Pick<JoinGraph, "edges">, relation: Relation): string;
function relationIdentities(graph: JoinGraph, relation: Relation): { column: string; type: string }[];
function relationHops(graph: Pick<JoinGraph, "edges">, type: string): RelationHop[];

interface Relation { root: string; path: readonly Hop[] }
interface Hop { edge: string; direction: "out" | "in" }
interface RelationHop { hop: Hop; label: string; to: string }
interface JoinGraph { types: readonly JoinType[]; edges: readonly JoinEdge[] }
interface JoinType { name: string; table: TableExpr; key: string; columns: readonly string[] }
interface JoinEdge { name: string; label: string; source: string; destination: string; table: TableExpr; src: string; dst: string }

FilterChips

PropTypeDefault
selectionSelectionthe provider's crossfilter

useClauses(selection: Selection): readonly SelectionClause[].

Field stats

function useFieldStats(table: TableExpr, options?: FieldStatsOptions): FieldStatsState;
function queryFieldStats(coordinator: Coordinator, table: TableExpr, options?: FieldStatsOptions): Promise<FieldStats>;
function fieldStats(rows: readonly SummarizeRow[], options?: FieldStatsOptions): FieldStats;
function autoDashboard(fields: readonly FieldStat[]): DashboardSpec;
function recommend(fields: readonly FieldStat[], intent?: RecommendIntent): Recommendation[];

type RecommendIntent = "overview" | "answer"; // default "overview"
interface Recommendation { spec: ChartTile; rationale: string; rule: string }

interface FieldStat {
  name: string;
  type: string;        // DuckDB's type, as SUMMARIZE reports it
  kind: FieldKind;     // "numeric" | "temporal" | "categorical"
  role: FieldRole;     // "dimension" | "measure" | "identifier"
  distinct: number;    // approx_unique: an estimate
  min?: number;        // the extent of a numeric or temporal field —
  max?: number;        //   a temporal one in epoch milliseconds
}
interface FieldStatsOptions { exclude?: readonly string[] }
interface FieldStats { fields: FieldStat[]; columns: string[] }
interface FieldStatsState { fields: FieldStat[] | null; columns: string[] | null; error: unknown }
interface SummarizeRow {
  column_name: unknown; column_type: unknown; approx_unique: unknown;
  min: unknown; max: unknown; count: unknown;
}

The spec's types — DashboardSpec, Tile, TileKind, TileSpan, StatTile, ChartTile, TableTile, DashboardFilterSpec, DashboardMeasure, DashboardChartType and DashboardAggregate — and Dashboards with parseDashboard and parseDashboards are exported and documented field by field on Dashboard spec. The props types are exported as DashboardProps, DashboardFiltersProps, DashboardStatProps, ChartCardProps, DetailTableProps, TileEditorProps, RelationPickerProps and FilterChipsProps.

On this page