# DreamLake — Full documentation > @dreamlake/viz visualizes robot-learning datasets in the browser: one `.dreamrc` file at a dataset root renders every episode — LeRobot / zarr / MCAP / plain folders, cameras, depth, point clouds, time series, annotations, and 3D reconstructions, all read in place over HTTP. Generated from https://viz.dreamlake.ai. 25 pages. --- Source: https://viz.dreamlake.ai # Quick start `@dreamlake/viz` visualizes robot-learning datasets in the browser. One `.dreamrc` file at a dataset root renders every episode in it — LeRobot / zarr / MCAP / plain folders; cameras, depth maps, point clouds, time series, annotations and 3D reconstructions — all read **in place** over HTTP range requests, never downloaded whole. The design in one sentence: pre-built view components each declare an input contract, format adapters normalize whatever is on disk into those contracts, and the `.dreamrc` states which fields feed which views — the program never decides what your data means. The full story: [the architecture](/dataset-viz/overview.md). ## The no-code path: one file on your dataset If your dataset is public (a HuggingFace repo, any CORS-enabled bucket), you never need to install anything. Write a `.dreamrc` at its root: ```yaml file=".dreamrc" version: 1 dataset: format: lerobot # lerobot | folder | umi | mcap episodes: auto views: - view: videoStack cameras: ["observation.images.*"] - view: lineChart series: - { field: [action, "*"] } ``` Check it from a shell before you ship it — with no config it prints the dataset's field inventory, which is how you find out what to bind: ```bash file="terminal" npx tsx scripts/check-dreamrc.mts hf:your-name/your-dataset ``` Then open the dataset in the DreamLake app, or compare against the [gallery](/dataset-viz/gallery.md) — every entry there is a complete `.dreamrc` over a real public dataset. Copyable starting points: [templates](/dataset-viz/templates.md). The grammar: [the .dreamrc file](/dataset-viz/spec.md). ## The library path: render it yourself For hosts embedding the viewer. The library is YAML-free (parse upstream) and credential-free (storage drivers carry identifiers only): ```bash file="terminal" pnpm add @dreamlake/viz react react-dom ``` ```tsx file="app.tsx" const rc = validateDreamrc(parse(dreamrcText)) const { episodes, warnings } = await resolveDataset(rc, { // For a file found at a dataset root, inject that root; a file declaring // its own storage: resolves alone and the declaration wins. rootStorage: { driver: 'hf', repo: 'live9080/dreamlake-ceramics' }, }) // One episode → one player. Render a list by mapping; wrap it in // and mount lazily for long datasets. export const App = () => ``` `validateDreamrc` throws errors written to be fixed mechanically — the offending key, the allowed values, a did-you-mean — because a `.dreamrc` is often authored by an agent in a write → validate → fix loop. Hosts extend every axis at runtime: `registerStorage` for an authorized backend, `registerFormat` for a dataset layout, `registerComponent` for a view of their own ([TypeScript API](/dataset-viz/spec.md#typescript-api)). ## What's in this package | export | what it is | | --- | --- | | `@dreamlake/viz/dataset-viz` | the `.dreamrc` engine: validate, resolve, render ([docs](/dataset-viz/overview.md)) | | `@dreamlake/viz/episode-*`, `…/media-overlay` | the underlying episode components — video stack, line chart, timeline, frame stack, 3D scene ([docs](/components/episode-video-stack.md)) | | `@dreamlake/viz/file-preview` | single-file preview used by the DreamLake file browser | | `@dreamlake/viz/schema-viz` | the previous-generation, schema-driven viewer the platform still ships | ## Where to go next | you want | read | | --- | --- | | to understand the design | [the architecture](/dataset-viz/overview.md) | | to write a `.dreamrc` | [the .dreamrc file](/dataset-viz/spec.md) · [view components](/dataset-viz/views.md) · [reference](/dataset-viz/reference.md) | | to prepare a dataset | [what your data must look like](/dataset-viz/requirements.md) | | working examples | [templates](/dataset-viz/templates.md) · [gallery](/dataset-viz/gallery.md) | | these docs, for your agent | [LLM-readable docs](/llm-readable.md) | --- Source: https://viz.dreamlake.ai/dataset-viz/overview # The architecture **A viewer cannot generate itself.** We do not produce bespoke visualization code per dataset — the system ships **pre-built view components** (a video wall, a line chart, a timeline, an animated 3D scene) and one config file composes them over your data. Everything on these pages follows from what that takes: 1. A pre-built view is only possible because its **input is known**. Every component declares a contract — the structure it consumes, per binding slot. Without that contract, a dataset could contain hand keypoints, you could know they are hand keypoints, and the viewer still could not draw them: nothing would say what structure to hand the view. 2. Datasets do not arrive in that structure, and never will — so **format adapters** normalize what is on disk into it. 3. Nothing about the bytes says which fields should feed which views — so a config, the **`.dreamrc`**, states it. Meaning is written down once, by a person or an agent who read the dataset; the program never guesses it. ## The three layers ``` bytes on disk in memory on screen ┌───────────────────┐ ┌──────────────────────┐ ┌───────────────────┐ │ existing formats │ │ payload contracts │ │ view components │ │ LeRobot · zarr │ │ video · series · │ │ videoStack · │ │ MCAP · folders │ → │ keypoints · segments│ → │ lineChart · │ │ WebVTT · COCO · │ │ pointcloud · mesh3d │ │ timeline · │ │ glTF · Parquet │ │ · … (closed set) │ │ recon3d · … │ └───────────────────┘ └──────────────────────┘ └───────────────────┘ always theirs ours — in memory only composed by .dreamrc └── adapters normalize ──┘└── views consume contracts only ──┘ ``` The middle layer is the narrow waist: N formats × M components costs **N + M, not N × M**. A new adapter works with every component the day it lands; a new component works with every format. That holds only while the contract set stays closed — adding a payload kind is a deliberate act in the library, never something an adapter does on the side. ## Views declare what they read Every registered component states, per binding slot, the payload kinds it consumes. That declaration **is** the input contract: | view | slot | contract it asks for | | --- | --- | --- | | `timeline` | `tracks` | `segments` — `{ start, end, label }` spans | | `pointCloud` | `cloud` | `pointcloud` — per-frame `xyz` (+ optional `rgb`) | | `videoStack` | `overlays` | `keypoints` (a skeleton) or `segments` (captions) | | `recon3d` | `geometry` + `tracks` | `mesh3d` geometry moved by `transform3d` / `vertices3d` / `pose3d` | One declaration is enforced three ways: it is what the decoder is called with, what a `*` glob is filtered by, and what an author's `as:` is validated against. Bind a field to a slot whose contract its bytes cannot satisfy and the panel says so by name — `observation.state is float32 [6]; keypoints needs [J,2] or [J,3]` — instead of drawing something wrong. The full tables: [reference](/dataset-viz/reference.md#kinds--the-catalogs-and-the-views). ## The contract lives in memory, never on disk The payload kinds are the **only** place a "DreamLake shape" exists, and they exist only as in-memory structures — a TypeScript union the components are typed against. Nothing on disk is ever asked to look like them. There is no DreamLake file format, no required directory layout, no conversion step: a public dataset renders with exactly one file added (the `.dreamrc`), and deleting that file leaves the dataset byte-identical to how it was. ## Where the bytes come from — the standards ladder Each contract is fed from the wire in a fixed order of preference: 1. **The container's own idiom.** Data that lives inside a dataset is expressed the way that container already expresses things: a LeRobot feature with its `dtype`/`shape`/`names`, a zarr array with its codec, an MCAP channel with its schema. A `[J,2]` float feature is already a skeleton's worth of numbers; an int column plus a label table is already LeRobot's own task pattern, and `segments` reads it as found. 2. **An established standard beside the data**, when the container cannot hold it or is not yours to write into: WebVTT/SRT for labelled time spans, COCO for 2D keypoints, glTF for geometry (and, animated, for its motion), Parquet for per-frame numbers. Every one of them predates us. 3. **A structure of our own, defined deliberately.** Where no existing standard fits, we define one — that is the ladder's last rung, not a failure of it, and it is how a dataset gets guaranteed a correct visualization when the ecosystem offers nothing to lean on. It has an admission bar, because the thing to prevent is the casual, unexamined format: survey the existing implementations first (LeRobot, Rerun, Foxglove, glTF, COCO, … — absorb their experience before writing a line), give the result its own spec section and a version, and keep it normalizable to and from neighbouring standards where possible. There is currently one on disk — the motion-track Parquet profile v1 ([which format to write](/dataset-viz/requirements.md#which-format-to-write)) — and one in memory: the payload contracts themselves, versioned with the library and [published in full](/dataset-viz/reference.md#payload-contracts-in-full). 4. **A format we do not read yet gets an adapter, not a rule.** Nobody is asked to convert: `registerFormat` adds a reader that normalizes the foreign layout into the same payloads, and until one exists, a custom component can bind the raw file and parse its own wire format — [the extension path](/dataset-viz/reference.md#data-the-library-does-not-know). A shape that proves general is then promoted into the contract set. The result is that the requirements on your data are small and mostly not ours: [what your data must look like](/dataset-viz/requirements.md) is two rules plus the shape each contract demands — which is not a rule we impose, but what a skeleton, a point cloud, or a depth map *is*. ## The `.dreamrc` states meaning The last piece is the wiring. A field's catalog entry records how its bytes are addressed (a video stream, numbers with a shape, a file with an extension) and draws no conclusion; the binding in the `.dreamrc` is where a human or an agent states what the field *is*, by choosing the slot — and, where a slot reads more than one contract, saying which with `as:`. ```yaml views: - view: videoStack cameras: [observation.images.ego] # decoded as video overlays: - { field: observation.keypoints_2d.left.ego, as: keypoints } - view: pointCloud cloud: [observation.environment_state] # the author knows ``` `observation.environment_state` is a `[512,6]` float tensor whose name says nothing. The program never decides what it means; the config's author read the inventory once and wrote it down. That one sentence is the design — the rest is [grammar](/dataset-viz/spec.md). ## What this buys - **Pre-built views over arbitrary datasets.** The contracts are the reason no per-dataset code is ever generated. - **Configs port.** A `.dreamrc` written for one dataset works on another by changing field names, because components only ever see contracts. - **Wrong renders become named errors.** Every binding is checked against a declared contract, so a mistake fails with the fix in the message instead of drawing a silently wrong picture. - **AI-authorable.** Meaning is stated in one small file with a closed vocabulary, and every validation error names the offending key and the allowed values — a write → validate → fix loop an agent can run alone. ## Where to go | you are | read | | --- | --- | | writing a `.dreamrc` | [the .dreamrc file](/dataset-viz/spec.md), then [view components](/dataset-viz/views.md) | | preparing or publishing a dataset | [what your data must look like](/dataset-viz/requirements.md) | | looking up a name or a config key | [reference](/dataset-viz/reference.md) | | wanting working examples | [templates](/dataset-viz/templates.md) · [gallery](/dataset-viz/gallery.md) | | changing the library | [library internals](/dataset-viz/internals.md) | --- Source: https://viz.dreamlake.ai/components/episode-timeline # EpisodeTimeline A zoomable episode-detail timeline: time ruler on top, a frame thumbnail strip beneath it, and N rows of labelled track blocks under that. The cursor is **hover-driven** — the user's pointer position is the single source of truth for the highlighted time. An optional `time` prop provides a fallback position to display when the pointer leaves (e.g., for syncing with a running video clock). Pair with [EpisodeVideoStack](/components/episode-video-stack.md) when you have multi-camera footage that should scrub against the same cursor. ## Basic usage Pure hover-driven: the cursor follows the pointer; when it leaves, the cursor disappears. ```tsx file="BasicSpec.tsx" export const BasicSpec = () => { const [t, setT] = useState(null) return ( setT(null)} /> ) } ``` ## Frames `frames` is an **ascending-time-sorted** list of `{ time, image }`. The component does not re-sort — sorting on every render would be wasteful and the contract is documented in the type. Frames at the same time deduplicate naturally because the greedy culler keeps the first one and drops any followers within `frameWidth + 4px`. When `frames` is empty, the strip and its footer label collapse entirely — there's no empty band and no `0 OF 0 FRAMES` readout; the track rows sit directly beneath the ruler. Each frame's **left edge** sits at its time on the axis (`fx = timeToX(f.time)`) — the frame visually represents the chunk starting at `time`. This keeps the `t = 0` cell fully visible at the timeline's left edge instead of half-clipped. ```ts type FrameSample = { time: number // seconds; 0 ≤ time ≤ duration image: string // URL alt?: string // reserved for future a11y surfacing } const frames: FrameSample[] = [ { time: 0, image: '/keyframes/000.jpg' }, { time: 0.5, image: '/keyframes/030.jpg' }, { time: 1.0, image: '/keyframes/060.jpg' }, ] ``` ### Greedy culling When the visible window is wide (low zoom), many frames would overlap in screen space. The component sweeps left-to-right and renders a frame only when its `time` is at least `(frameWidth + 4px) / pxPerSec` later than the previous rendered frame. Off-screen frames are skipped. As you zoom in, the visible time window shrinks and `pxPerSec` grows, so the stride between renderable frames shrinks too — more of the supplied frames qualify until eventually every one is shown. The footer readout `{visible} OF {total} FRAMES` updates live as you scrub. Time positions are exact — each frame's left edge sits at the X corresponding to its actual `time`, no quantization to evenly-spaced slots. ## Tracks and blocks Each track is a row; each block is a labelled span `{ start, end, label }`. Blocks outside the visible window are skipped entirely; blocks that straddle the edge render with a dashed border on the clipped side to signal "continues off-screen." ```ts type TrackBlock = { id?: string // stable key for hover + click callbacks start: number end: number label: string } type TimelineTrack = { id: string name?: string // accessibility only — not rendered blocks: TrackBlock[] } const tracks: TimelineTrack[] = [ { id: 'phases', blocks: [ { id: 'p1', start: 0, end: 2.4, label: 'idle' }, { id: 'p2', start: 2.4, end: 9.1, label: 'approach' }, ], }, ] ``` Provide stable `id` on each block to get reliable hover highlight + callback identity. ## Interaction model | Gesture | Effect | | --- | --- | | **Hover** | Bright accent cursor pinned to the pointer X; highlights every track block whose `[start, end]` contains the cursor time AND the frame whose visual extent contains it. Fires `onHover(time)` on every move, `onHoverEnd()` on leave. | | **Click** on empty area | `onSeek(time)` — separate "commit" event distinct from continuous hover. | | **Click** on a block | `onBlockClick(block, trackId)` — does NOT fire `onSeek`. | | **Drag** (≥ 4px) | Pans the viewport (when zoomed in). | | **Shift + drag** vertical | Drag-zoom: drag up to zoom in, down to zoom out. Anchors the time under the cursor. | | **Wheel + alt/⌘/ctrl** | Zoom at cursor (also fires on trackpad pinch — macOS dispatches `ctrlKey + wheel`). | | **Wheel** (horizontal-dominant) | Pan when zoomed in. Two-finger horizontal swipes are always swallowed, so a trackpad gesture on the timeline never triggers the browser's back/forward navigation. | | **ZoomBar** `‹` / `›` | Step zoom by ×1.4 / ÷1.4. | | **ZoomBar** drag readout | Continuous zoom via `exp(dx × 0.008)`. | ## Controlled vs uncontrolled viewport `zoom` and `panPct` are optional. Omit them → the component owns viewport state internally. Provide **either** one → the component flips into controlled mode for **both** (the omitted value falls back to its default, but the component no longer writes to its internal state). `onViewportChange` always fires when the user gestures, so the parent can persist the new values. ```tsx // Uncontrolled // Controlled — persist viewport in URL / localStorage const [vp, setVp] = useState({ zoom: 1, panPct: 0 }) ``` ```ts type ViewportState = { zoom: number // ≥ 1 panPct: number // 0 ≤ panPct ≤ 1 - 1/zoom } ``` ## Design rules User-visible behaviour the component enforces: - **0s strict left edge** — the ruler picks tick steps from a fixed ladder (`0.05, 0.1, 0.25, 0.5, 1, 5, 10, …` seconds) and starts ticking at `0`; the chart never shows negative time. The `0s` and duration labels stay visible at the very edges instead of getting clipped. - **Anchor zoom** — `⌘` (or `alt` / `ctrl`) + wheel zooms about the cursor: the time directly under the pointer stays fixed across the zoom step. - **Three-tier ruler** — `major / minor / micro`. As zoom changes, ticks cross-fade smoothly between tiers instead of popping. ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `duration` | `number` | — | Episode duration in seconds. Must be `> 0`. | | `frames` | `FrameSample[]` | — | Frame thumbnails, sorted ascending by time. | | `tracks` | `TimelineTrack[]` | — | Track rows, drawn top-to-bottom in array order. | | `time` | `number \| null` | `null` | Fallback cursor position when the user is NOT hovering. Hover always overrides. `null` → pure hover-driven cursor that disappears on leave. | | `onHover` | `(t: number) => void` | — | Called continuously on every hover move with the time at the cursor. | | `onHoverEnd` | `() => void` | — | Called when the cursor leaves the timeline. | | `onSeek` | `(t: number) => void` | — | Called on click in an empty area (single commit). Clicks on a track block fire `onBlockClick` instead and do NOT fire `onSeek`. | | `zoom` | `number` | `1` | Controlled zoom level. Clamped to `[minZoom, maxZoom]`. Pairing with `panPct` flips the component into controlled mode for both. | | `panPct` | `number` | `0` | Controlled pan as fraction of duration. Clamped to `[0, 1 - 1/zoom]`. | | `onViewportChange` | `(v: ViewportState) => void` | — | Fires when the user changes zoom or pan via wheel/drag. | | `onBlockClick` | `(block, trackId) => void` | — | Click on a track block (suppresses `onSeek`). | | `onBlockHover` | `(block \| null, trackId \| null) => void` | — | Hover enter/leave on a track block. Receives `null` on leave. | | `framesLabel` | `string` | `'FRAMES'` | Left-side uppercase caption under the frame strip. The right side is auto-generated as `{visibleCount} OF {totalCount} FRAMES`. | | `frameHeight` | `number` | auto | Frame thumbnail height in pixels (width auto from 16:9). When omitted, sized to fit 12 cells with 4px gaps. | | `trackRowHeight` | `number` | `28` | Track row height. | | `minZoom` | `number` | `1` | Minimum zoom factor. | | `maxZoom` | `number` | `20` | Maximum zoom factor. | | `className` | `string` | — | Extra classes on the root wrapper. | --- Source: https://viz.dreamlake.ai/dataset-viz/spec # The `.dreamrc` file **One `.dreamrc` at the root of a dataset renders every episode in it.** The file answers three questions — where the episodes are, how to parse them, and how to lay out the visualization — and nothing else. It is plain YAML, never contains credentials, and works unchanged on any storage backend. Why the system is shaped this way — views with declared input contracts, meaning stated in the config — is [the architecture](/dataset-viz/overview.md); this page is the grammar. ```yaml # .dreamrc — at the dataset root version: 1 name: Kitchen Manipulation v2 # optional display name dataset: # ── data entry + parsing ── format: lerobot # lerobot | folder | umi | mcap episodes: auto # ask the format adapter (default) views: # ── visualization: compose views ── - view: videoStack cameras: ["observation.images.*"] - view: lineChart series: - { field: [action, "*"] } ``` That is a complete, working file. Note what it does **not** say: where the dataset lives. A `.dreamrc` at its dataset's root inherits the storage it sits in — the app injects it. A *standalone* file (a demo, a config pointing at another bucket) adds one more block, `storage:` — see [below](#storage--where-the-dataset-lives). Everything else is the reference for the blocks. Gallery — 10 real datasets, one grammar LeRobot v2/v3 · depth maps · point clouds · UMI zarr · MCAP · annotations + 3D — a full-screen two-pane switcher of complete .dreamrc files ## Live example One file exercising most of the spec at once — glob enumeration, auto-discovered annotation tracks (COCO hand keypoints drawn over the video, WebVTT subtask cues on a timeline), and a real 3D hand–object reconstruction (glTF geometry + per-frame parquet tracks) driven by the shared cursor. It uses the `folder` format on purpose: the **zero-convention layout** — any files you can put in folders, no conversion, the easiest dataset there is to prepare. An *existing* LeRobot / zarr / MCAP dataset needs an even shorter file (`format:` + `episodes: auto` — see the gallery). The source pane is the complete standalone `.dreamrc`; copy it and it runs anywhere: ## `dataset:` — which episodes, parsed how `format` names the dataset format; `episodes` says how to enumerate the episodes. Both live here and nowhere else. **Which format is mine?** List the dataset root and match the signature: | you see at the root | `format` | `episodes` | | --- | --- | --- | | `meta/info.json` | `lerobot` | `auto` | | a `*.zarr.zip` or a `.zarr/` directory | `umi` | `auto` | | `*.mcap` files | `mcap` | `auto` | | one folder per recording | `folder` | a glob, e.g. `"episodes/*/"` | If none matches, `folder` is the escape hatch: it asks nothing of the layout beyond one directory per episode, and reads whatever standard files it finds. HDF5 (RoboMimic, ManiSkill, AgiBotWorld) and RLDS/TFRecord (Open X-Embodiment) have no adapter yet — most publishers also ship a LeRobot export, which does. | key | values | meaning | | --- | --- | --- | | `format` | `lerobot` \| `folder` \| `umi` \| `mcap` | the format adapter. One name per format — no aliases. | | `episodes` | `auto` (default) | ask the format adapter. Container formats know their own episode count (LeRobot `meta/info.json` `total_episodes`, zarr `episode_ends`). | | | `"episodes/*"` | a glob over storage paths — one episode per match. A trailing `/` matches directories only. Only `*` is supported, one path segment per star. | | | `{ glob, sort?, limit? }` | glob with options. `sort`: `name` (numeric-aware, default) \| `name-desc` \| `none`. `limit`: cap per pattern (default 1000). | | `annotations` | `{ : \| { path, kind? } }` | extra annotation tracks merged into every episode's catalog — see below. `kind` is the author stating the payload once in the declaration instead of at every binding. | | `labels` | `{ "": "Display name" }` | rename a field, or one dimension of one (`"observation.state[3]"`), when the dataset's own names are serial numbers, `null`, or absent. | Use `auto` for container formats (`lerobot`, `umi`) and a glob for folder-per-episode layouts (`folder`): ```yaml dataset: format: folder episodes: "episodes/*/" # each matched folder is one episode ``` In glob mode the matched path is handed to the adapter as the episode root — you never write per-episode config. `{ limit }` on its own (no glob) caps what a container format enumerated, which is how you preview a 300-episode dataset. **Writing `views:` for a dataset you have not seen** — start with `view: fieldsCatalog` and nothing else. It prints the inventory: every field's address plus the `dtype`, `shape` and `names` the container reported, and no conclusion drawn from them. Reading that listing is where the judgment happens — you are the one who knows that `observation.environment_state` is a point cloud — and the bindings you write next are where it gets recorded. ### `dataset.labels` — names the container got wrong A camera keyed by its serial number, a 14-dim state whose `names` is `null`: the data is right and only the label is unreadable. Patterns match field names (or a `feature[dim]` address), first match wins: ```yaml dataset: format: lerobot labels: "observation.images.cam_035622060973": Front camera "observation.state[3]": wrist_flex ``` `labels` changes what is **displayed** and nothing else; bindings still use the real names. There is no companion key for meaning — nothing to correct, because nothing was guessed. What a field is gets stated where it is bound. ### `dataset.annotations` — tracks that live beside the data Annotations belong **inside** your dataset's container — see [what your data must look like](/dataset-viz/requirements.md#annotations-belong-inside-the-container). This block is for the two cases that cannot: static geometry, which no container models, and a dataset you cannot write into. ```yaml dataset: format: lerobot annotations: scene: "recon/scene.glb" # a path, nothing more ``` The merge happens after the format adapter runs, so a declared track lands in the same catalog as the dataset's own fields, and a declaration **overrides** a native track of the same name. Paths, formats and the rest: [files beside the data](/dataset-viz/requirements.md#files-beside-the-data). ## `views:` — compose views Each entry names a **view** from the registry and binds fields to it. Everything that is not a binding key is passed through to the view as props — each view's reference section lists what it accepts. You own the layout: nest `split` nodes to build any arrangement. ```yaml views: - view: videoStack cameras: ["observation.images.*"] # binding — read as video overlays: - { field: observation.keypoints_2d.left.ego, as: keypoints } columns: 2 # passthrough prop - view: timeline tracks: [{ field: subtask_index, as: segments }] # joins its label table - split: row # layout node: row | column | grid children: - view: lineChart series: - { field: [action, left_waist], label: waist · cmd } - { field: [observation.state, left_waist], label: waist · actual, dash: "3 2" } - view: lineChart series: [{ field: [action, right_waist] }] ``` Rules, all of them: - **One binding style per view, and the slot's name says what it takes.** Camera views (`videoStack`, `frameStack`, `depthStack`) bind `cameras`, `lineChart` binds `series`, `timeline` binds `tracks`, `pointCloud` binds `cloud`; camera views also take `overlays`, and `recon3d` takes both `geometry` (the glTF) and `tracks` (motion). A bare string in `series`/`tracks`/`overlays` is shorthand for `{ field }`. A slot a view does not read is an error, never ignored. - **The slot says what the bytes become.** Binding a field to a slot is what decides how it is decoded — `series` reads numbers as traces, `tracks` on `timeline` reads them as spans, `cloud` on `pointCloud` reads them as a cloud. When the field's addressing kind leaves the slot only one possibility, nothing is written. When it leaves several, the entry says which with `as` (`overlays` draws a skeleton or captions; a `.json` could be either), and until it does the panel refuses and prints the choice ([the rule](/dataset-viz/reference.md#which-payloads-a-field-can-serve--and-when-you-write-as)). - **Field references** are `"feature"` or `[feature, dim]` — a feature name (never split on dots: `observation.state` is one name), optionally drilled into one named dimension. One glob rule: `*` matches within a name. - **Layout** is `split: row | column | grid` with `children`; nest freely. `grid` accepts `columns`. A **`row` is a fixed-height strip** (`height`, default 280): the layout never reflows as media loads — extra width overflows into a horizontal scrollbar instead, and that scroll **syncs across episodes** (the list renderer wraps episodes in `SyncScrollProvider`). Per child you choose what to keep: nothing — keep the row height (media takes its aspect-derived width, everything else stretches to share the leftover, `flex` weighting it and `minWidth` flooring it); `width` — a fixed-width box; `height` — that child's own strip height. `views` is **required**. There is no default layout: a dataset with no views is one nobody has described yet, and inventing an arrangement for it would be the program deciding what its data means. To find out what there is to bind, resolve the dataset with no config and read the inventory — `check-dreamrc` prints every field with its `dtype` and `shape`. ### The views The initial registry — it grows over time, and a host app can register its own with `registerComponent(spec)`: | view | renders | slot → payload it asks for | | --- | --- | --- | | `videoStack` | camera videos as a tile grid, with overlay support | `cameras` → `video` \| `image` · `overlays` → `keypoints` \| `segments` | | `frameStack` | per-frame image sequences (chunked cameras) | `cameras` → `frames` · `overlays` → `keypoints` \| `segments` | | `lineChart` | time series, styled per-dim traces, synced cursor | `series` → `series` | | `timeline` | ruler + labelled track blocks | `tracks` → `segments` | | `metaPanel` | episode name / duration / task strings header card | — (`note` prop) | | `fieldsCatalog` | the episode's inventory as a table | — | | `recon3d` | animated 3D scene: glTF geometry moved by per-frame tracks, plus point sets — orbit + cursor-driven playback | `geometry` → `mesh3d` · `tracks` → `transform3d` \| `vertices3d` \| `pose3d` | | `depthStack` | per-frame depth maps, turbo-colorized | `cameras` → `depth` · `overlays` → `keypoints` \| `segments` | | `trajectory2d` | planar series as a top-down xy path | `series` → `series` | | `bandTrack` | discrete series as categorical color bands | `series` → `series` | | `pointCloud` | per-frame 3D point clouds, orbitable | `cloud` → `pointcloud` | Every slot, its payload, and the shape that payload needs: [reference](/dataset-viz/reference.md#view-components). ## `storage:` — where the dataset lives Every path in the file is **relative to the dataset root**; `storage:` says where that root is. It is optional, and *who writes it* is the design: - **At the dataset root, omit it.** The app that found the file injects the storage it sits in — a DreamLake source, a project folder, any browsed directory. This is the normal, uploaded form: the file never repeats what its own location already says, and moving the dataset never breaks it. - **Standalone files declare it.** A docs example, a demo gallery, a config that points at another bucket — anything not sitting at its data's root names the root explicitly. Every live example on this page is this form: copy the YAML and it resolves the same data anywhere. - **A declaration wins.** If a file with `storage:` is opened inside the app, it renders as written (same rule as `dataset.annotations`: an explicit entry is user intent). Omission — not override — is what makes a file portable. ```yaml storage: { driver: hf, repo: lerobot/pusht } # public HuggingFace repo storage: { driver: http, url: https://my-cdn.example.com/kitchen-v2 } ``` Built-in drivers are `http` (`url` — the dataset root URL) and `hf` (`repo`, plus optional `root`, `revision`, `repoType`) — both credential-free. Host apps register more: DreamLake registers `dlSource` and `dlProject`, whose configs carry **identifiers only** — a `.dreamrc` never contains credentials; drivers that need auth get their tokens from the host at registration time. Per-driver keys: [reference](/dataset-viz/reference.md#storage-drivers). The whole storage contract is two methods — `list(path)` and `resolveUrl(path)` — which is why any backend can be a root. ## Validation `validateDreamrc(parsed)` checks the file before anything renders and throws errors written to be fixed mechanically — each names the offending key, the allowed values, and the episode where expansion failed. Typical messages: ``` .dreamrc: dataset.format 'lerobot3' is not a registered format (available: lerobot, folder, umi, mcap) .dreamrc: views[2].view 'lineChart2' is not registered (did you mean 'lineChart'?) .dreamrc: episodes glob "episodes/**" — '**' is not supported, use one '*' per path segment .dreamrc declares no 'storage:' and no host root storage was supplied — standalone files need storage: { driver, … } ``` ## TypeScript API For hosts and tests — the library is credential-free and YAML-free (parse upstream, pass the object): ```ts const rc = validateDreamrc(parseYaml(text)) // A self-contained file (declares storage:) resolves alone; for a file found // at a dataset root, inject that root — the file's own storage: would win. const { episodes, warnings } = await resolveDataset(rc, { rootStorage: { driver: 'http', url: 'https://…/my-dataset' }, }) // episodes: ResolvedEpisode[] — id, name, meta, and a ready-to-render handle // ``` --- Source: https://viz.dreamlake.ai/schema-viz/overview # Schema viz `@dreamlake/viz/schema-viz` renders a **dataset** as a **synchronized, multi-panel visualization** from a small **schema**. You describe _what_ you want to look at; it fetches lazily and draws — videos, charts, and timelines that scrub together. ## Quick start Hand `` a parsed schema. This one points the built-in `http` storage at a public LeRobot dataset and **omits `panels`** — so viz **auto-lays-out** the whole episode: a camera stack, a task timeline, and one chart per numeric field. The entire program: ```tsx file="AutoLayoutSpec.tsx" // Omit `panels` entirely → viz auto-lays-out the source: a camera stack, a task // timeline, and one chart per numeric field. The smallest possible schema that // still produces a full view — the "I don't know this dataset yet" workflow. const schema: VizSchema = { version: 1, sources: { ep: { adapter: 'lerobot', storage: { driver: 'http', basePath: 'https://huggingface.co/datasets/lerobot/aloha_static_coffee/resolve/main', }, episode: 0, }, }, // no `panels` → auto-layout runs against `ep` } export const AutoLayoutSpec = () => ``` A schema has two parts: - **`sources`** — each names an **[adapter](/schema-viz/adapters.md)** (_what format the data is_) and its **[storage](/schema-viz/storage.md)** (_where the bytes live_). - **`panels`** — each is a **[view](/schema-viz/views.md)** (`videoStack`, `lineChart`, `timeline`) over some fields. Omit `panels` and viz **auto-lays-out** the dataset. That is the entire surface area. Everything else is choosing the right adapter, storage, and views — and, when you need it, writing your own. ## How to read these docs 1. [Schema](/schema-viz/schema.md) — write a schema: sources, panels, field binding, and the auto-layout you get when you omit panels. 2. [Storage](/schema-viz/storage.md) — point at the bytes: the public `http` driver, and how a host app injects an **authorized** driver for private data. 3. [Adapters](/schema-viz/adapters.md) — which adapter for which dataset (LeRobot, Zarr/UMI, egocentric, loose folders), or write your own. Start here if you just want to point at a dataset. 4. [Views](/schema-viz/views.md) — the built-in panels, their options, writing your own panel, and how auto-layout works. 5. [Concept](/schema-viz/concept.md) — a short read on _why_ it is built in four layers. Skip it until you are curious. --- Source: https://viz.dreamlake.ai/dataset-viz/views # View components **Every registered component, each with a minimal `.dreamrc` and its live render** — this page answers "what do I *get* if I bind this?". Config keys and the payload each slot asks for are in the [reference](/dataset-viz/reference.md#view-components); full multi-component compositions in the [gallery](/dataset-viz/gallery.md). Each YAML below is complete and standalone — one component, one real public dataset, first episode only. **Interaction rule**: every component whose x-axis is time (videos, frames, depth, charts, bands, the timeline) scrubs the shared cursor on hover — hover any demo to move it. The 3D views leave the pointer to orbiting, so their demos pair a chart or timeline sibling as the time source. ## videoStack Camera videos as a tile grid, each tile at its video's own aspect ratio. `overlays` is the one slot that draws two different things, so each entry says which with `as` — here a COCO file `as: keypoints` becomes a hand skeleton and a WebVTT file `as: segments` becomes captions: ## frameStack Per-frame image sequences (cameras stored one frame per chunk) — each tile byte-ranges ONLY the frame under the shared cursor; scrub to step. JPEG-XL frames need Safari 17+ or Chrome's JXL flag: ## depthStack Per-frame depth maps colorized on the fly — turbo by default (`colormap: gray` for grayscale), each frame mapped over its own valid min/max unless pinned with `min`/`max`; 0/invalid readings stay transparent. The corner chip shows the mapped range in metres when the format knows the depth scale: ## pointCloud Per-frame 3D point clouds as an orbitable scene — per-point color when the data carries rgb, camera auto-fit from the first frame, playback follows the shared cursor. Default `up: z` (robot-lab convention); `up: y` for y-up clouds: ## lineChart Time series with a synced cursor — one `series` entry per trace, `[feature, dim]` drills into one dimension, `label` / `color` / `dash` style it: ## trajectory2d Planar series as a top-down xy path — for 2-dim position series (pusht's `action` target position) a spatial path reads far better than a line chart. Hover snaps the shared cursor to the nearest sample; the thick trail is the last 1.5 s; `invertY: false` flips to math convention: ## timeline Anything you bind here is read as `segments` — tasks, subtasks, actions, phases, a `.vtt` file, an index column with its label table — and drawn as labelled blocks on a ruler. Hover to scrub every panel in the episode: ## bandTrack Discrete series (gripper open/close, stage indices, success flags) as categorical color bands — one row per bound column, one colored rect per contiguous equal-value run, a value→color legend below. Columns busier than `maxLevels` (12 distinct values) get a one-line "use lineChart" note instead of a band: ## metaPanel The episode header: name, duration, frame count, fps, the dataset's task strings — plus a free-text `note`: ## fieldsCatalog The episode's inventory as a table — every field's address plus the `dtype`, `shape` and `names` the container reported, with nothing concluded from them. The exploration component: ship it first when you don't know what a dataset holds, decide what the columns are, write the bindings, then replace it: ## recon3d The animated 3D scene, bound through two slots: `geometry` is the static scene, and `tracks` is the per-frame motion that moves it — a track binds to the glTF node whose name matches its ref. All three track kinds are parquet tables of numbers, so each entry names the one it is. Orbit with the mouse; playback follows the shared cursor. The motion trail is the **future** — the next `trail.ahead` seconds (default 1) of each object's path, and nothing more: it runs out exactly when the clip does. In robot learning the question at time t is what is about to happen, so that is the segment that glows (`trail.behind` opts into a dim past tail): --- Host apps can grow this registry — `registerComponent({ name, component })` makes a new name available to every `.dreamrc` the app renders ([TypeScript API](/dataset-viz/spec.md#typescript-api)). --- Source: https://viz.dreamlake.ai/dataset-viz/requirements # What your data must look like This page is the **data side** of the [contract](/dataset-viz/overview.md): what your container must declare, the shape each payload demands, and where annotation tracks live. The program never decides what your data means — your `.dreamrc` does ([the architecture](/dataset-viz/overview.md)) — so the requirements are much smaller than a list of naming rules. There are only two: 1. **The container must say which bytes are encoded media**, so they can be addressed a frame at a time instead of being read as numbers. Every format already does this — LeRobot's `dtype`, a zarr array's codec, an MCAP channel's schema, a file's extension. 2. **A column bound to a view must have the shape that view decodes.** Ask for a skeleton and the numbers must be `[J,2]` or `[J,3]`. That is not a rule we impose; it is what a skeleton is. Everything else — which camera a hand belongs to, whether a `[7]` column is a pose or a gripper command, what a `.json` beside your video contains — is stated in the `.dreamrc`. You never rename a feature to make it render. ## Which datasets can be read Four formats, named in `dataset.format`. Each is somebody else's specification, read as published: | `format` | the dataset is | episodes come from | how you recognize yours | | --- | --- | --- | --- | | `lerobot` | a [LeRobot](https://huggingface.co/docs/lerobot/main/en/lerobot-dataset-v3) dataset, v2.0 / v2.1 / v3.0 | `meta/episodes` (v3) or `meta/episodes.jsonl` (v2) | there is a `meta/info.json` at the root | | `umi` | a [Zarr](https://zarr-specs.readthedocs.io/) store — a v2 `.zarr.zip` ReplayBuffer or a v3 `.zarr/` directory | `meta/episode_ends`, or the store's attributes | there is a `*.zarr` or `*.zarr.zip` | | `mcap` | [MCAP](https://mcap.dev/) v1 logs, one file per episode | one `*.mcap` file each, listed or globbed | there are `*.mcap` files | | `folder` | no container at all — directories of files | a glob you write, e.g. `"episodes/*/"` | none of the above; one directory per recording | `folder` is the escape hatch and asks nothing of the layout beyond one directory per episode. HDF5 (RoboMimic, ManiSkill, AgiBotWorld) and RLDS/TFRecord (Open X-Embodiment) have no adapter yet — most publishers also ship a LeRobot export, which does. A format we do not read gets an adapter, not a conversion demand ([the extension path](/dataset-viz/reference.md#data-the-library-does-not-know)). ## What the viewer sees before you configure anything The catalog is an **inventory**, not a classification. Point the checker at any dataset and it lists what exists, with the facts the container reported and no conclusions drawn from them: ``` episode_000000 (7 fields) video observation.images.ego h264 1080×1920 tensor observation.keypoints_2d.left.ego float32 [21,3] tensor observation.state float32 [6] names: shoulder_pan.pos, … tensor subtask_index int64 [1] text language_instruction file recon/scene.glb ext: glb ``` The kinds describe **how the bytes are addressed**, never what they mean: | kind | means | | --- | --- | | `video` | an encoded stream, seekable | | `frames` / `image` | encoded images, one per frame or one file | | `tensor` | numbers, with `dtype` and `shape` as the container reported them | | `text` | strings | | `file` | an address, with its extension | **That listing is what you write the config against** — it is the raw truth, and an agent authoring a `.dreamrc` reads exactly this. Binding a field to a view slot is what states its meaning, and asking for something the bytes cannot become fails by name (`observation.state is float32 [6]; keypoints needs [J,2] or [J,3]`) instead of rendering a guess. ## What each payload needs The shape requirements, in full. This is the whole contract on the data side: | ask for | the column must be | notes | | --- | --- | --- | | `series` | any numeric scalar or `[n]` | `names` in the container label the traces | | `keypoints` | float `[J,2]` or `[J,3]` | third component is a score; **NaN, not 0, for not-measured** | | `segments` | an int column **plus a label table**, or a string column, or a `.vtt` / `.srt` | equal consecutive values merge into one span | | `depth` | float `[H,W]` or `[H,W,1]` | metres or millimetres — declare the scale | | `pointcloud` | float `[N,3]` or `[N,6]` | xyz, or xyz + rgb | | `transform3d` | float `[7]` or `[N,7]` | translation + quaternion | | `vertices3d` | float `[V,3]` | topology comes from a bound glTF node | | `pose3d` | float `[J,3]` or a flat `[J*3]` | a point set per frame | | `frames` | encoded image bytes per row, or a per-frame path template | | | `mesh3d` | a `.glb` / `.gltf` / `.obj` | node names bind per-frame tracks | > **Note:** Zero is a real coordinate — the top-left pixel, the origin. Nothing downstream > can tell a fabricated zero from a measured one, so a frame written as zeros > draws a collapsed skeleton in the corner. Write NaN and it renders as nothing, > because it is nothing. ## Annotations belong inside the container A payload's preferred home is **inside** the dataset, expressed in the container's own idiom: a keypoint feature sits in the same parquet as your state and action columns, gets read by episode row range, and travels with the dataset. > **Note:** Measured on our own template: 150 frames of two hands cost **57.7 KB** as > parquet columns and **142.3 KB** as a JSON file next to them — and the > columns can be read one episode at a time while the file must be fetched and > parsed whole. One container, one round trip, no second thing to keep in sync. What that looks like per container, and the one thing each must get right: **LeRobot** — `meta/info.json` `features` gives every column its `dtype`, `shape` and `names`, and those appear verbatim in the listing. The one thing to get right: **cameras must be `dtype: video` or `dtype: image`**, so they are addressed as media. Episode boundaries come from `meta/episodes` — v3 packs many episodes into one parquet and one mp4 per camera, and each episode's row range and video window are read from there. A labelled span is an index column plus a label table (`meta/s.jsonl` — LeRobot's own `task_index` + `meta/tasks.jsonl` pattern, one level down); hand keypoints are a `[J,2]` or `[J,3]` float feature like any other. **MCAP** — every channel is listed with its schema name and encoding. Foxglove schemas (`PointCloud`, `FrameTransform`, `CompressedImage`, `SceneUpdate`, …) have decoders; `cdr` / `ros2msg` / `ros1msg` do not, so a plain ROS 2 bag currently yields nothing. **Zarr** — an array's codec says whether it holds encoded images or numbers. Episodes come from `meta/episode_ends` or the store's attributes. **A folder of files** — one directory per episode; a file's basename is its track name; `annotations/` is searched. Three placement conventions, and none of them says what a file contains. ## Files beside the data Two cases genuinely cannot go inside, and only two: 1. **The container cannot model it.** Static geometry is the real example — there is no LeRobot feature shape for a mesh. 2. **The dataset is not yours.** A public mirror you cannot write into, so any extra tracks (and the config itself) have to live elsewhere. For those, put a file beside the data **in an established format** and name it in the config. The original dataset is never modified — delete the extra files and it is untouched. ### Declaring a track `dataset.annotations` maps a track name to a path. The merge happens *after* the format adapter runs, so a declared track lands in the same catalog as the dataset's own fields and binds the same way: ```yaml dataset: format: lerobot episodes: auto annotations: subtasks: "annotations/subtasks/episode_{episode_index:06d}.vtt" hands: "annotations/hands/episode_{episode_index:06d}.json" scene: "recon/scene.glb" ``` - **Paths** are relative to the dataset root; templates use the same `{var}` / `{var:06d}` style LeRobot itself uses. Variables: `episode_index`, `episode_name`, `episode_path` (glob mode). - **A declaration is an address, not a claim.** The file lands in the catalog as `file` with its extension recorded, and the view that binds it says what to make of it. The extension gets its say at read time, where all it picks is a PARSER: asked for `segments`, a `.vtt` goes through the WebVTT reader and a `.json` through the COCO one. - **`{ path, kind }` is you saying it once, in the declaration** instead of at every binding — legitimate because it is the author speaking, not the library deducing. A binding's own `as:` still wins, and either way the decoder checks the bytes and fails loudly when they cannot produce what was asked. - **A declaration overrides a native track of the same name.** That is user intent: replacing a dataset's coarse task segments with a refined re-annotation is exactly what this is for. Undeclared native tracks stay. - The `folder` format auto-discovers anything in each episode's `annotations/` folder, so declaring is never required when files live there. ### Which format to write Use the standard that already exists for the job — none of these is ours: | for | write | spec | | --- | --- | --- | | labelled time spans | **WebVTT** `.vtt` (or SubRip `.srt`) | [W3C](https://www.w3.org/TR/webvtt1/) | | 2D keypoints | **COCO keypoints** `.json` | [COCO](https://cocodataset.org/#format-data) | | geometry, and its motion | **glTF 2.0** `.glb` | [Khronos](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html) | | per-frame numbers | **Apache Parquet** | [Parquet](https://parquet.apache.org/docs/file-format/) | **Time spans — WebVTT**, the web's own "time range → text" format: ```text file="subtasks.vtt" WEBVTT 1 00:00:00.000 --> 00:00:02.500 pick up shelf board ``` **2D keypoints — COCO**, stock, plus a top-level `fps` because COCO indexes images and a player needs seconds (our one documented extension to COCO): ```json file="hands.coco.json" { "fps": 30, "images": [{ "id": 0, "file_name": "frame_000000.jpg", "width": 1920, "height": 1080 }], "annotations": [ { "id": 1, "image_id": 0, "category_id": 1, "keypoints": [473.4, 714.2, 2, 532.2, 710.5, 2], "bbox": [418, 524, 277, 249], "score": 0.79 } ], "categories": [{ "id": 1, "name": "hand", "keypoints": ["wrist", "thumb_cmc"], "skeleton": [[1, 2]] }] } ``` Every key is COCO's, including the 1-based `skeleton` pairs. A frame with no detection simply has **no annotation** — absence, not a zero coordinate. **Geometry and motion — glTF.** Export one `.glb` with your objects as **named nodes**: the node name is the join key, so a node called `ruler` binds the per-frame pose track named `ruler`, and no side file describes the relationship. **If the objects move, animate the glTF** — per-node translation/rotation/scale channels with their own keyframe times are already standardized, so one animated `.glb` carries geometry AND motion in a file Blender, three.js and every other glTF tool reads. (The first clip's TRS channels are read; skinning, morph targets and multiple clips are not yet.) **Per-frame numbers — Parquet**, when the motion is not in a glTF: | track | columns | | --- | --- | | object poses | `frame`, `timestamp`, `object`, `tx,ty,tz` + a quaternion — the `object` column splits it into one track per object | | mesh vertices | `frame`, `timestamp`, and a **list-of-float** column of flat xyz | Quaternion order is read from the COLUMN ORDER: `qw` first means `w,x,y,z`, `qw` last means `x,y,z,w`. These column names are a **decoder's** requirement, consulted only once a binding has asked for `transform3d` or `vertices3d` — never evidence about what a file holds. > **Note:** Those column names are ours — Parquet is Apache's, but "`tx,ty,tz` plus a > quaternion plus a grouping column" is not written down anywhere else, so we > define it deliberately: the **motion-track Parquet profile v1**, currently > the one self-defined on-disk structure in the spec > ([when we define our own](/dataset-viz/overview.md#where-the-bytes-come-from--the-standards-ladder)). > An animated glTF carries the same information inside an existing standard > and opens in Blender and three.js; the profile is the columnar route when > your pipeline is already writing Parquet. Pick whichever it produces more > naturally. ## Cameras: the encoding that actually matters The single most common cause of a sluggish dataset, and it applies to every container. Scrubbing means seeking, and a seek decodes forward from the previous keyframe — keyframes 10 seconds apart feel stuck no matter how fast the machine is: ```bash file="transcode.sh" ffmpeg -i raw.mp4 \ -c:v libx264 -pix_fmt yuv420p \ -g 30 -keyint_min 30 -sc_threshold 0 \ # a keyframe every ~1 s at 30fps -movflags +faststart \ # index up front — playback starts immediately out.mp4 ``` H.264 + `yuv420p` decodes everywhere; HEVC, AV1 and MPEG-4 Part 2 do not. **Independent frames scrub better than any video** — one fetch per frame, no decode chain, exact at any file size. ## Verify ```bash npx tsx scripts/check-dreamrc.mts hf:your-name/your-dataset # a Hub dataset npx tsx scripts/check-dreamrc.mts https://bucket/…/my-dataset # any object storage npx tsx scripts/check-dreamrc.mts ./draft.dreamrc # before you upload ``` It resolves the dataset exactly as the app does: reads the `.dreamrc`, decodes every bound field, and prints what came back. With no config it prints the inventory instead — which is how you find out what to write. ## Checklist - [ ] Cameras are declared as media by the container (`dtype: video` / `dtype: image` / an image codec / a media extension) - [ ] Videos are H.264 `yuv420p`, `+faststart`, keyframes ≈ 1 s apart - [ ] Every column you intend to render has the shape its payload needs - [ ] Not-measured is NaN, not zero - [ ] Span index columns have their label table - [ ] Annotations live inside the container where it can hold them; sidecar files are established formats, declared in `dataset.annotations` - [ ] A `.dreamrc` at the dataset root, no `storage:` block — the app injects it ([spec](/dataset-viz/spec.md#storage--where-the-dataset-lives)) - [ ] Resolved once from a shell, with every binding reporting the payload you expected --- Source: https://viz.dreamlake.ai/dataset-viz/reference # Reference Every name a `.dreamrc` can use, with its config keys. The [architecture](/dataset-viz/overview.md) is why the system has this shape, the [spec](/dataset-viz/spec.md) is the grammar, and the [contract](/dataset-viz/requirements.md) says what your data must look like; this page is the lookup table — the names here are exactly the registries the validator checks, so anything not listed fails validation with a did-you-mean. For the pipeline and the registries in code, see [library internals](/dataset-viz/internals.md). ## Kinds — the catalog's, and the view's Both say "kind" about the same field, and they mean different things. Keeping the two apart is the whole design. ### Inventory kinds — what the catalog states `Field.kind` says how the BYTES ARE ADDRESSED and nothing about what they mean. Six values, closed, and nothing turns a name or a shape into one of them: | kind | the container said | `meta` carries | | --- | --- | --- | | `video` | an encoded stream, seekable | `codec` / dimensions when reported | | `frames` | encoded images, one per frame | `count`, `fps?`, `ext` | | `image` | one encoded image | `ext` | | `tensor` | numbers | `dtype`, `shape`, `names` — verbatim | | `text` | strings | | | `file` | an address, and nothing claimed | `ext` | For a loose file the extension picks the addressing and only that: `.mp4` / `.webm` → `video`, `.jpg` / `.png` → `image`, `.csv` / `.parquet` → `tensor`. **`.vtt`, `.glb` and `.json` are all `file`** — "labelled spans", "geometry" and "keypoints" are meanings, and meanings come from the `.dreamrc`. `meta` is raw facts, recorded because they are free: `dtype`, `shape`, `names`, `ext`, `schema`, `codec`. A decoder reads them to do its job — the schema name selects an MCAP decoder, the extension selects a parser — and nothing reads them to decide what a field is. ### Payload kinds — what a view asks for `read(ref, { as })` decodes into one of these. The `as` comes from the slot the field was bound to, so the interpretation is written in the `.dreamrc`: | payload | what comes back | the field must be | | --- | --- | --- | | `video` | `{ url, window? }` — streamable; `window` is this episode's span of a shared file | a `video` field | | `image` | `{ url }` | an `image` field | | `frames` | `{ count, fps?, frameAt(i) }` — lazy per-frame fetch | a `frames` field | | `series` | `{ timestamps, columns }` — per-dim numeric traces | any numeric scalar or `[n]`; `names` label the traces | | `segments` | `{ segments: [{ start, end, label }] }` — any time-range → label | an int column **plus its label table**, a string column, or a `.vtt` / `.srt` | | `keypoints` | `{ keypoints }` — pixel space, fps, sparse frames, skeleton | float `[J,2]` / `[J,3]`, or a COCO `.json` | | `depth` | `{ count, fps?, at(i) }` — raw values, never pre-colorized | float `[H,W]` or `[H,W,1]`, or `frames` of encoded 16-bit PNGs | | `pointcloud` | `{ count, fps?, at(i) }` | float `[N,3]` or `[N,6]` (xyz, or xyz + rgb) | | `transform3d` | `{ timestamps, values, layout }` — position + quaternion | float `[7]` or `[N,7]` | | `vertices3d` | `{ count, fps?, vertexCount, at(i) }` — lazy | float `[V,3]` | | `pose3d` | `{ timestamps, joints, shape }` — per-frame point sets | float `[J,3]` or a flat `[J*3]` | | `mesh3d` | `{ url, format }` — static geometry; its node names bind the tracks above | a `.glb` / `.gltf` / `.obj` | | `file` | `{ url, ext? }` — an address a component parses itself | anything | Ask for one the bytes cannot become and the read throws naming both sides (`observation.state is float32 [6]; keypoints needs [J,2] or [J,3]`) instead of drawing something wrong. ### Which payloads a field can serve — and when you write `as:` The bridge between the two tables. It reads the addressing kind and nothing else — no name, no shape, no extension: | addressed as | can be read as | | --- | --- | | `video` | `video` | | `image` | `image` | | `frames` | `frames`, `depth` — depth shipped as 16-bit PNGs is a codec question, not a meaning | | `tensor` | `series`, `keypoints`, `pose3d`, `transform3d`, `vertices3d`, `depth`, `pointcloud` | | `text` | `segments` | | `file` | `file`, `mesh3d`, `segments`, `keypoints` | Intersect a field's row with the payloads its slot reads ([per component](#view-components)): - **exactly one survives** — nothing to write; the container already settled it (a `video` bound to `videoStack.cameras`, a `tensor` bound to `pointCloud`); - **more than one** — the binding says which with `as:`, and until it does the panel refuses and prints the choice (a `.json` bound to `overlays` is a skeleton file or a caption track, and nothing about the bytes says which); - **none** — that slot cannot show that field, and it says so by name. A declared `as:` short-circuits the table entirely: it is the author speaking, and it is how a LeRobot `subtask_index` is bound `as: segments` and joins its label table. Where a payload comes from — a feature in the dataset's own container, or a standard file beside it (WebVTT/SRT, COCO, glTF, parquet) — is in the [contract](/dataset-viz/requirements.md#what-each-payload-needs). **No payload requires a format of our own** — where a DreamLake structure exists (the motion-track Parquet profile v1), an existing standard carries the same payload. ### Data the library does not know The payload set versions with the library, but shipping your own end to end needs no library change: 1. **Declare where it is** — `dataset.annotations: { gaze: "annotations/gaze_{episode_index:06d}.json" }`. The field lands in every episode's catalog as a `file` with its `ext` in `meta`. Nothing is claimed about the contents. 2. **Render it** — the host registers a component (`registerComponent({ name: 'gazePanel', component, reads: { fields: ['file'] } })`) whose slot reads `file`, fetches the URL and parses its own wire format. The `.dreamrc` writes `view: gazePanel` and binds the field — no schema change, no library change. 3. **Promote it** — once a payload proves general (the way `segments` covers tasks/subtasks/actions/phases), it graduates into the library: a `Payload` member, a decoder that accepts the wire shapes found in the wild (see `normalizeSegments`), and a built-in component with a slot that reads it. The same registries extend the other axes: `registerFormat` for a new dataset layout, `registerStorage` for a new backend — including *composite* backends (an overlay bucket layered over a read-only base is just another two-method `Storage` whose `list` merges and whose `resolveUrl` checks the overlay first). ## Payload contracts in full The exact structures a `read(ref, { as })` returns — what a component is handed, and therefore what a host-registered component should expect. These are **the** in-memory contract: our one self-defined schema layer, versioned with the library ([why it exists](/dataset-viz/overview.md#the-contract-lives-in-memory-never-on-disk)). Every type below is exported from `@dreamlake/viz/dataset-viz`, and the source (`dataset-viz/types.ts`) is the authority — this listing mirrors it. ```ts type Payload = // A camera stream. `window` marks the sub-range of `url` this episode // occupies (LeRobot v3 concatenates episodes into one file per camera). | { kind: 'video'; url: string; window?: { from: number; to: number } } | { kind: 'series'; timestamps: number[]; columns: Record } | { kind: 'image'; url: string } | { kind: 'frames'; count: number; fps?: number; frameAt: (i: number) => Promise } | { kind: 'file'; url: string; ext?: string } | { kind: 'keypoints'; keypoints: KeypointTrack } | { kind: 'segments'; segments: Segment[] } | { kind: 'pose3d'; timestamps: number[]; joints: number[][]; shape: number[]; layout?: string } // Static 3D geometry — node names inside it are the join key for // per-frame transform3d / vertices3d tracks. | { kind: 'mesh3d'; url: string; format: 'gltf' | 'glb' | 'obj' } | { kind: 'transform3d'; timestamps: number[]; values: Float32Array; layout: TransformLayout } // Per-frame vertex positions for a deforming mesh, fetched lazily; // topology comes from the paired mesh3d node. | { kind: 'vertices3d'; count: number; fps?: number; vertexCount: number; at: (i: number) => Promise } // Raw values, never pre-colorized — the component owns the colormap. | { kind: 'depth'; count: number; fps?: number; at: (i: number) => Promise } | { kind: 'pointcloud'; count: number; fps?: number; at: (i: number) => Promise } /** One labelled time span — the canonical shape for every segment-class * track (tasks, subtasks, actions, phases, subtitle files). */ interface Segment { start: number end: number label: string } /** Per-frame 2D keypoints, normalized out of whatever the source was (COCO * JSON, a per-frame tensor). Coordinates are pixels in width × height; * frames are sparse — a missing key means "no detection". */ interface KeypointTrack { width: number height: number /** Frame index → detections. Frame k is shown at k / fps seconds. */ frames: Map fps: number /** Index pairs drawn as bones. COCO-17 and Hand-21 presets are built in. */ skeleton?: [number, number][] jointNames?: string[] } interface KeypointDetection { /** [x, y] per joint, pixels. */ points: number[][] score?: number /** Free label — "left" / "right" / a class name. */ group?: string /** [x1, y1, x2, y2] pixels, when the source carries one. */ box?: [number, number, number, number] } /** One depth map. `data` is row-major width×height; `scale` converts a raw * value to metres (0.001 for millimetre uint16; omit when unknown — the * component then normalizes to the frame's own range). 0 = no reading. */ interface DepthFrame { width: number height: number data: Float32Array | Uint16Array | Uint8Array scale?: number } /** One cloud frame. `xyz` is N×3 metres; `rgb` optional N×3, either 0-255 * bytes or 0-1 floats. */ interface CloudFrame { xyz: Float32Array rgb?: Uint8Array | Float32Array } /** Component order of a transform3d row. Both orders occur in the wild * (glTF/three use xyzw; robotics and MANO exports use wxyz), so the * adapter states which it read instead of guessing downstream. */ type TransformLayout = 'txyz_qwxyz' | 'txyz_qxyzw' ``` ## Storage drivers Declared in a standalone file's `storage:` block, or injected by the host for a file at its dataset root ([who writes it](/dataset-viz/spec.md#storage--where-the-dataset-lives)). Configs carry identifiers only — never credentials. | driver | config keys | notes | | --- | --- | --- | | `http` | `url` **required** — the dataset root URL (absolute, or site-relative when co-hosted) | `resolveUrl` joins `url` + path. Listing reads a co-located `index.json` manifest per directory — a static host cannot enumerate itself, so glob enumeration needs the manifests; `episodes: auto` formats don't. | | `hf` | `repo` **required** (e.g. `lerobot/pusht`) · `root` subpath within the repo · `revision` (default `main`) · `repoType` (default `datasets`) | Public HuggingFace repos, credential-free. Listing walks the Hub tree API; resolved URLs support CORS + Range, so parquet/zarr reads work in-browser. | | `dlSource` | `slug` (namespace) · `sourceId` · `root` subpath | **Registered by the DreamLake app** — a source browsed in the platform (S3, GCS, …). Listing via the source browse API, URLs via presign; the session token is injected at registration, never configured. | | `dlProject` | `namespace` + `project`, or `root` (a node id) | **Registered by the DreamLake app** — a project folder as the dataset root. | Hosts add drivers with `registerStorage(name, factory)`; the whole contract is `list(path)` + `resolveUrl(path)`. (The legacy schema-viz pages use `basePath`/`id` for their own http/hf drivers — a different subsystem; a `.dreamrc` always uses the keys above.) ### The http `index.json` manifest A static host can't enumerate itself, so the `http` driver lists a directory by fetching `/index.json`. **Every directory a glob walks needs one** — for `episodes: "episodes/*/"` that is `episodes/index.json` plus one inside each episode folder (formats with `episodes: auto` fetch known paths and need none). The shape mirrors what `list()` returns: ```json { "entries": [ { "name": "run_a", "path": "episodes/run_a", "type": "dir" }, { "name": "cam_ego.mp4", "path": "episodes/run_a/cam_ego.mp4", "type": "file" } ] } ``` `name` + `type` (`"file"` | `"dir"`) are required; `path` is the storage-relative (or absolute) location — entries may point anywhere, which is how a manifest can reference files hosted elsewhere. Don't list `index.json` itself; `.dreamrc` need not be listed either (the app fetches it directly). ## Formats The `dataset.format` adapters. Common to all: the catalog is an inventory — each field's address plus the facts the container reported — and `read(ref, { as })` is where anything is decoded. `dataset.annotations` declares extra tracks ([spec](/dataset-viz/spec.md#datasetannotations--tracks-that-live-beside-the-data)), merged in by the core — never a per-format concern. ### `lerobot` LeRobot v2.0 / v2.1 / v3.0. The entry file is `meta/info.json`. | | | | --- | --- | | expected layout | `meta/info.json` + `meta/tasks*` + `data/…parquet` + `videos/…mp4` (the paths `info.json` itself declares) | | episodes | `auto` — `total_episodes` from `info.json`. A glob is never needed. | | config keys | none | The inventory is `meta/info.json`'s `features`, verbatim — one entry per feature, carrying its `dtype`, `shape` and `names` as found. `dtype` is the only thing read, and only to decide addressing: `video` → `video`, `image` → `frames`, `language` / `string` → `text`, any numeric dtype → `tensor`. Anything else is omitted with a warning naming it, including a depth VIDEO (the feature's own `video.is_depth_map` flag) — no browser decodes 12-bit log-quantized H.265. Exactly four bookkeeping columns are dropped — `timestamp`, `frame_index`, `episode_index`, `index` — and nothing else. `task_index` and `subtask_index` stay: an integer pointing into a label table is data, and a view binding one `as: segments` is what fetches `meta/tasks*` and joins it. Timeline: per-episode length at the dataset's `fps`. ### `folder` Folder-per-episode, no manifest — the directory layout is the contract. | | | | --- | --- | | expected layout | anything: each matched folder is one episode, its files are the fields | | episodes | a glob, e.g. `"episodes/*/"` — `auto` errors (a bare folder cannot enumerate itself) | | config keys | `fps` — the clock for numbered still runs and depth stills (default 30). A directory of JPEGs states no frame rate, so the author does; without it, tracks that carry a real clock (a COCO file's `fps`, parquet timestamps) drift away from the pictures. | The inventory is the directory listing and nothing more: one field per file, addressed by its basename, with `path`, `ext` and `size` in `meta`. The extension picks the ADDRESSING only — `mp4`/`webm` → `video`, `jpg`/`png` → `image`, `csv`/`parquet` → `tensor`, and **everything else — `.vtt`, `.glb`, `.json` — is `file`**. Consecutive numbered stills collapse into one `frames` field. `annotations/` is listed too, and a file there replaces a same-named one at the root. No file is opened and no name is read for meaning; a view binding `subtasks.vtt` as `segments` is what parses it, and the extension's second say — at read time — picks only which parser. Series tables take their x-axis from a `timestamp`-like column, row index otherwise. Timeline is `null` — media components probe durations themselves. ### `umi` Zarr stores, two modes detected from the store itself. | | | | --- | --- | | expected layout | a `*.zarr.zip` ReplayBuffer (UMI: `data/` arrays + `meta/episode_ends`) **or** a `*.zarr` v3 directory store with a root `zarr.json` manifest (EgoVerse-style, one episode per store) | | episodes | `auto` — `episode_ends` slices the ReplayBuffer; a directory store is a single episode | | config keys | `path` — store path when not at the root or ambiguous (default: first `*.zarr.zip` / `*.zarr` found) · `fps` — ReplayBuffer clock (default 60) | The inventory reads each array's own metadata. An **image codec** is the store declaring that one chunk is one encoded image, so that array is `frames` (byte-ranged a frame at a time — the store is never downloaded whole); numeric arrays are `tensor`, carrying `dtype`, `shape` and `codec`. Nothing is synthesized: a `names` list whose length does not match the array's width names nothing, and a `[T,J,3]` keypoint array is a tensor like any other until a view binds it `as: pose3d`. Arrays this reader cannot window (three dims or more) are omitted with a warning. Timeline from frame counts at `fps`. ### `mcap` MCAP v1 indexed containers — one episode per `*.mcap` file, read in place over HTTP range requests (a 512MB file costs ~130KB before any field is read). | | | | --- | --- | | expected layout | `*.mcap` files at the dataset root — indexed/chunked, lz4- or zstd-compressed chunks | | episodes | `auto` — one per `*.mcap` at the root, name-sorted (the `http` driver needs its `index.json` manifest to list); a glob (`"runs/*.mcap"`) also works | | config keys | `path` — a single `.mcap` at a subpath (skips listing) | The inventory is the channel list, each channel carrying its `schema` name and encoding in `meta`. Addressing follows what the schema says a message IS: json channels with numeric leaves → `tensor` (dot paths are the columns, e.g. `linear_accel.x`; dims come from the jsonschema or the first message), string-only leaves → `text`; Foxglove image schemas → `frames` (one message is one image, byte-ranged per frame); `FrameTransform` → one `tensor` per child frame; a `SceneUpdate` carrying a ModelPrimitive → `file`. The schema name is then a DECODER selector at read time — a `foxglove.PointCloud` channel bound `as: pointcloud`, a tf channel bound `as: transform3d` — never a conclusion at catalog time. Channels outside this v1 scope — ros1msg/cdr/ros2idl decoding, `Grid`, attachments, bz2 chunks — are **omitted from the catalog** with a console warning naming topic + encoding, never mislisted. Unindexed files fail with a clear error (repack with `mcap recover`). Timeline: message start→end from the summary statistics. ## View components Live example of each — a minimal `.dreamrc` and its render — on the [view components catalog](/dataset-viz/views.md). Here: the config surface. **A slot is where meaning is stated.** Each component declares, per binding slot, the payload kinds it reads — and that one declaration is what `read(ref, { as })` is called with, what a `*` glob is filtered by, and what an author's `as:` is checked against. Bind a field to `series` and it is read as traces; bind the same field to `tracks` and it is read as spans. A slot a component does not declare is rejected rather than ignored: `overlays:` on a `lineChart` is an author expecting to see something, and silence there is the failure mode this design removes. Whether a binding needs an `as:` follows from the field, not from the view — [the rule](#which-payloads-a-field-can-serve--and-when-you-write-as). The media/geometry slots (`cameras`, `cloud`, `geometry`) take bare refs with nowhere to write one, so they are always settled by the field's own addressing kind; `series`, `tracks` and `overlays` take `{ field, as }` entries. A `*` glob selects only the fields whose addressing kind can serve the slot — `cameras: ["*"]` on `videoStack` takes the cameras and leaves the tensors — while an explicit ref always passes and fails loudly if it cannot be read. Bindings are the keys the view interprets — its slot names (`cameras` / `cloud` / `geometry` / `series` / `tracks` / `overlays`); **every other key passes through as a prop** to the underlying `Episode*` component, so its documented props are all available. A `split: row` is a fixed-height strip (`height` on the split, default 280) — its sizing keys (`width` fixed box · `flex` stretch share · `minWidth` squish floor · child `height` override) are consumed by the layout, not the component; media sizes its width from its aspect, charts fill their slot, and overflow scrolls horizontally, synced across episodes under a `SyncScrollProvider`. "row" marks components that render in the compact `layout="row"` strips (dataset lists); the rest appear in grid layout only. | view | binds | slot → payload | keys of note | row | | --- | --- | --- | --- | --- | | `videoStack` | `cameras` (a `*` glob expands to the container's media fields) · `overlays: [ { field, as, on? } ]` — `as` picks skeleton or captions, `on` pins one to a specific camera | `cameras` → `video`, `image` · `overlays` → `keypoints`, `segments` | `columns` (default 3) · `tileAspect` — force one ratio; by default each tile uses its video's intrinsic ratio. Plus [EpisodeVideoStack](/components/episode-video-stack.md) props. Probes video durations when the format has no timeline. | ✓ | | `frameStack` | `cameras` · `overlays` (same form as `videoStack` — the tiles take the same layer) | `cameras` → `frames` · `overlays` → `keypoints`, `segments` | `columns` (default 3) | ✓ | | `depthStack` | `cameras` — name the depth columns; a bare `*` would ask every tensor in the episode for a depth map · `overlays` | `cameras` → `depth` · `overlays` → `keypoints`, `segments` | `colormap: turbo \| gray` (default `turbo`) · `min` / `max` (raw units) pin the color range; by default each frame maps its own min/max over valid readings (>0), invalid renders transparent · `columns` (default 3). Corner chip shows the mapped range — metres when the format knows the depth scale, raw units otherwise | ✓ | | `lineChart` | `series: [ ref \| { field, label?, color?, dash?, … } ]` — field is `feature` (all dims) or `[feature, dim]`; `*` globs work in both halves | `series` → `series` | `height` (default 180) sizes a standalone panel; in a `split: row` slot the chart fills the strip automatically. Plus `title`, `caption` + [EpisodeLineChart](/components/episode-line-chart.md) props | ✓ | | `trajectory2d` | `series: [ ref \| { field, label?, color? } ]` — each entry is one path; x/y dims are the columns named `x`/`y` (case-insensitive) or the first two | `series` → `series` | `invertY: false` — math convention (y up); default is image convention (top-left origin) · `window: { ahead?, behind? }` — pin the drawn path to the seconds around the cursor (defaults 1 / 0 when given; omitted, the full path draws). `height` (default 260) sizes a standalone panel; in a `split: row` slot the plot fills the strip automatically | ✓ | | `timeline` | `tracks` — required; nothing in an inventory says a column holds spans | `tracks` → `segments` | [EpisodeTimeline](/components/episode-timeline.md) props | — | | `bandTrack` | `series: [ ref \| { field, label? } ]` — same field addressing as `lineChart`; each resolved column becomes one band row | `series` → `series` | `maxLevels` (default 12) — a column is discrete when its unique values (rounded to 6 decimals) fit, busier columns get a one-line "use lineChart" note · `bandHeight` (default 18) per-band px. Runs of equal value become colored rects; value→color legend below; natural height. | — | | `metaPanel` | — (renders `EpisodeInfo`: name, duration, frames, fps, task strings) | — | `note` — a free-text line · `showTasks: false` hides the task strings (single-task datasets repeat one sentence per episode otherwise) | — | | `fieldsCatalog` | — | — (prints the inventory itself) | — | — | | `recon3d` | `geometry` — the geometry file(s) that make the scene · `tracks` — the per-frame motion, each entry naming its `as`; a track binds to the glTF node whose name matches its ref | `geometry` → `mesh3d` · `tracks` → `transform3d`, `vertices3d`, `pose3d` | `up` — gravity vector in the data's own frame (uprights the grid) · `trail: { ahead?, behind? }` — motion-trail window in seconds around the playhead (default `{ ahead: 1, behind: 0 }` — pure future, so the trail runs out exactly when the clip does; `behind` opts into a dim past tail; `false` turns it off) · `height` (default 360) sizes a standalone panel; in a `split: row` slot the scene fills the strip automatically | — | | `pointCloud` | `cloud` — name the column; the first bound field renders (one cloud per panel in v1) | `cloud` → `pointcloud` | `up: y \| z` (default `z`, robot-lab convention → −90° X rotation) · `height` (default 360) sizes a standalone panel; in a `split: row` slot the scene fills the strip automatically. Per-point color when the data carries rgb; camera auto-fits the first frame | — | Hosts add views with `registerComponent({ name, component, reads, slotNames?, rows? })` — `reads` is the slot → payload declaration above (canonical keys: `fields`, `series`, `tracks`, `overlays`), `slotNames` renames a slot's config-surface key the way `videoStack` surfaces `fields` as `cameras`, and a component that omits `reads` opts out of binding checks. A component receives `{ fields, info?, read, timeline, cursor, config, layout }`. --- Source: https://viz.dreamlake.ai/components/episode-video-stack # EpisodeVideoStack A grid of N camera-view tiles that share a cursor time with each other and (optionally) with a sibling [EpisodeTimeline](/components/episode-timeline.md). Hovering any tile updates one shared `time` value; every other tile re-renders its scrub line at that same time, with a **muted** tone so the actively-hovered tile stays visually unambiguous. The caller wires two pieces of state to make this work: a shared **`hover`** time, and an **`activeId`** marking which tile carries the accent ring. The component exposes both as controlled props — see [Combined with EpisodeTimeline](#combined-with-episodetimeline) for the canonical pattern. ## Tile anatomy ``` ┌──────────────────────────────────────────┐ │ REC · 00:10.602 ● LIVE │ │ │ │