Dashboards and interactive exploration
Cross-filtered charts over millions of rows — the Explore view and spec-driven Mosaic dashboards, computed in the workspace engine through Mosaic's pre-aggregation, never in the browser.
ZAANIX embeds Mosaic (@uwdata/vgplot, BSD-3) with the workspace engine as its data connector. Mosaic turns every interaction into SQL and, for repeated filtering, builds pixel-binned pre-aggregated tables, so brushing stays interactive on datasets far larger than a browser could hold. DuckDB-WASM is never loaded.
Explore view#
In the Data explorer (Explore tab) and as a results view in the workbench: every numeric or temporal column becomes a histogram, every low-cardinality text column a bar chart, with a lazily paged table underneath. Brush one chart to cross-filter all the others; click a bar to toggle it; double-click to clear. The view rebuilds whenever the workspace data epoch moves.
Mosaic dashboards#
Dashboards come in two kinds. Grid dashboards are widgets on a drag-and-drop layout; Mosaic dashboards are a Mosaic declarative spec — YAML or JSON — rendered live against the workspace and stored with the dashboard, so every member sees the same interactive view.
- Generate drafts a complete, cross-filtered spec from any table, data file or SELECT in one click. The draft is ordinary spec text.
- Edit opens a YAML/JSON editor next to a live preview that re-renders as you type (⌘S saves). Viewers see the rendered dashboard only.
- Any workspace table can be referenced with
from: table_name. Files, queries and inline rows are declared underdata:exactly as in the Mosaic docs; ZAANIX turns them into hidden source views inside the workspace jail. - Params, selections, inputs (
menu,search,slider,table), every mark, interactor and attribute, legends andhconcat/vconcatlayouts are Mosaic's own. - Datasets are materialised once into an in-memory database (up to
mosaic.materialize_max_rows;materialize: falseopts out), so brushing, menus and sliders answer in milliseconds even when the source is a large CSV.
meta: { title: Trips by hour }
data:
trips: { file: green_tripdata.parquet }
params:
brush: { select: crossfilter }
vconcat:
- plot:
- mark: rectY
data: { from: trips, filterBy: $brush }
x: { bin: lpep_pickup_datetime }
y: { count: null }
- select: intervalX
as: $brush
xDomain: Fixed
width: 640
- input: table
from: trips
filterBy: $brush
height: 300A complete example — 3.7M NYC taxi trips with menus, sliders, headline numbers, an hourly timeline, an hour × weekday heatmap, brushable histograms, a density raster, grouped lines, top zones and the filtered rows — comes with ZAANIX as examples/mosaic/nyc-yellow-taxi.yaml.
Agents and the AI#
Every spec is validated by the server before it is rendered or saved — structure in Mosaic's own vocabulary, then every dataset and table bound in the workspace with EXPLAIN — so a missing file or column comes back as a precise error instead of a blank chart.
- MCP:
create_mosaic_dashboard(spec | spec_text, validate_only?)creates or updates a dashboard and refuses invalid specs with the error list; the resourceduckdb://guides/mosaic-specand the promptbuild_mosaic_dashboardcarry the authoring rules. - The AI: a mission that asks for a chart or dashboard gets the same guide; every spec in its answer is validated against the workspace and becomes a card with Create dashboard — one click to save and open.
How it stays safe#
The browser talks to POST /api/workspaces/:id/mosaic. Chart queries (arrow / json) run through the normal query pipeline — role, sandbox, audit, result cache with ETag — with their own row ceiling (mosaic.max_rows). Mosaic's plumbing (exec) is admitted only in its exact shapes: creating the zaanix_mosaic schema, preagg_<hash> tables inside it, ZAANIX's zaanix_mosaic_src_<hash> source views and zaanix_mosaic_mem.src_<hash> in-memory tables, and dropping them again; every wrapped SELECT must be a single read-only statement. Anything else is rejected. Pre-aggregates and source views are derived data: viewers can create them, they never move the data epoch, and they are dropped — and rebuilt lazily — whenever the epoch moves. They are hidden from the catalog, the explorer and agents.
Configuration: mosaic.enabled, mosaic.schema, mosaic.max_rows. Metrics: zaanix_mosaic_exec_total{kind}.