Why four layers
You can use schema-viz without reading this page. It explains why the pieces are shaped the way they are — useful when you write your own adapter or view.
Robot datasets arrive in many formats (LeRobot's meta/info.json, a zarr root,
a bare folder of files) from many places (S3, GitHub, HuggingFace, DreamLake), but
you want to render them all with the same panels. schema-viz gets there by
splitting the problem into four layers, each meeting the next through one narrow
shape:
The Model is the pivot
The hard part of visualizing data is not drawing it — it is agreeing on what the
data is. So schema-viz defines one normalized shape, the Model: a list of
fields, each with a kind (video, series, image, cues, …) and a
lazy way to read it.
A view asks the Model only for the fields it can draw, by kind. Views speak only Model — they never learn where the data came from. An adapter is just a Model implementation for one format; a storage driver feeds the adapter bytes from one backend.
kind is the joint the system turns on
Every field carries a kind, and every read() result is discriminated by the
same kind. That one string couples three things that otherwise know nothing about
each other:
So a new modality is a new kind plus a view that renders it — no interface
changes. That is why the registry and the panels never grow when you add a format.
The built-in kinds
kind is an open set of strings — these five ship built in:
kind | What it is | read() returns (Payload) | Default view |
|---|---|---|---|
video | a video clip | { url, start, end } | videoStack |
image | a still image | { url } | videoStack |
file | an opaque / unrecognized file | { url, ext } | videoStack |
series | numeric time-series (multi-column) | { timestamps, columns } (meta.dims) | lineChart |
cues | labeled time intervals (segments) | { cues: Cue[] } | timeline |
Adding a sixth is additive: emit the new kind from an adapter and register a view
whose kinds includes it. Nothing else changes.
What each layer does with kind
- Storage — nothing. Storage only knows
Entry.type(file/dir); it never sees akind. (A backend's ownnode.kindis a different thing — see the Storage page.) - Adapter — the producer. It assigns each field a
kindinfields()and tags everyPayloadwith the samekindinread()— where raw bytes become a typed modality (filesysteminfers it from the file extension;lerobotfrom the feature type). - Model /
Field— the contract.Field.kindanswers "what is this";Payload.kindis the discriminant of theread()result union. - View — the consumer. Each view declares
kinds: string[](the kinds it can draw); the registry indexes views by kind, so any view renders any adapter's data as long as the kinds line up. - Auto-layout — groups a source's fields by
kindto pick default panels: media → onevideoStack,cues→ onetimeline,series→ alineCharteach.
In one line: the adapter produces kind, the view consumes it, and neither has
to know about the other.
Lazy by construction
Discovery is cheap (fields() is a catalog); data is fetched only for what is on
screen, and only for the window asked for. Where the bytes load is decided by
kind, so you never wire it by hand:
- Media (
video/image/file) →read()returns a URL and stops; the<video>/<img>streams the bytes itself, on play/seek. - Data (
series/cues) →read()resolves the URL, range-reads the window, parses it, and returns arrays. The view gets numbers and never learns the file format.
The payoff
Because each layer meets the next only through these shapes:
each added independently, and the schema wires them together with no code.