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:

Storage ──► Adapter ──► Model ──► View
(where)     (format)    (vocab)    (draw)

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.

ts
interface Model {
  fields(): Promise<Field[]> // what's here
  read(ref: string, query?: ReadQuery): Promise<Payload> // one field, lazy
  timeline(): Promise<Timeline | null> // shared clock, or null
}

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:

Field.kind ─ selects ─► the View that declares it renders that kind
     └─ determines ─► the Payload shape that read() returns

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:

kindWhat it isread() returns (Payload)Default view
videoa video clip{ url, start, end }videoStack
imagea still image{ url }videoStack
filean opaque / unrecognized file{ url, ext }videoStack
seriesnumeric time-series (multi-column){ timestamps, columns } (meta.dims)lineChart
cueslabeled 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

  • Storagenothing. Storage only knows Entry.type (file/dir); it never sees a kind. (A backend's own node.kind is a different thing — see the Storage page.)
  • Adapter — the producer. It assigns each field a kind in fields() and tags every Payload with the same kind in read() — where raw bytes become a typed modality (filesystem infers it from the file extension; lerobot from the feature type).
  • Model / Field — the contract. Field.kind answers "what is this"; Payload.kind is the discriminant of the read() 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 kind to pick default panels: media → one videoStack, cues → one timeline, series → a lineChart each.

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:

  • a new backend is one Storage,
  • a new format is one Adapter,
  • a new visualization is one View

each added independently, and the schema wires them together with no code.