Timeline
Config vs JSX Authoring
<TimelineContainer> has two authoring styles:
- Config path — a serializable
TimelineConfigobject. Use when the document comes from a server / database, or when the same file is shared across a backend CLI and the frontend UI. This is the primary path. - JSX path — lane components as children. Use when the document is hand-written in code or when a lane needs a custom
normalizedecoder that isn't JSON-serializable.
Both paths share the same validation and the same dtype-driven dispatch. A config always round-trips to JSX; JSX round-trips to config except for normalize.
The two axes — dtype and views
The key split: the track declares what it IS (a dtype), the app declares how it RENDERS (a views map). These are two different config surfaces.
const config = { // ← what the data is
tracks: [
{ id: 'q', path: 'robot/arm/qpos', dtype: 'joint_angles', src: '/q.m3u8' },
],
}
<TimelineContainer
config={config}
views={defaultTimelineViews} // ← how to draw it
/>Same config rendered by a different app can pass a different views map and get a completely different visualization — without touching the data or the track description.
Config path
import {
TimelineContainer,
defaultTimelineViews,
type TimelineConfig,
} from '@vuer-ai/vuer-m3u'
const config: TimelineConfig = {
container: {
id: 'teleop_run_037',
name: 'teleop_run_037',
duration: 30,
},
groups: {
cams: { name: 'Cameras', color: 'green', icon: 'video' },
robot: { name: 'Robot state', color: 'blue', icon: 'robot' },
narr: { name: 'Narration', color: 'purple', icon: 'caption' },
},
tracks: [
{ id: 'wrist_cam',
path: 'cams/wrist_cam',
dtype: 'video',
src: 'video/wrist_cam/playlist.m3u8',
color: 'green',
props: { thumbnails: 'video/wrist_cam/thumbnails.vtt' } },
{ id: 'arm_qpos',
path: 'robot/arm/qpos',
dtype: 'joint_angles',
src: 'track/arm_qpos/playlist.m3u8',
color: 'blue',
// shape, range, unit come from the joint_angles dtype defaults —
// only channel names are episode-specific.
props: {
channelNames: ['shoulder_pan', 'shoulder_lift', 'upper_arm_roll',
'elbow_flex', 'forearm_roll', 'wrist_flex', 'wrist_roll'],
} },
{ id: 'operator',
path: 'narr/operator',
dtype: 'action_label',
src: 'text/operator/playlist.m3u8',
color: 'purple' },
],
}
export default function Demo() {
return <TimelineContainer config={config} views={defaultTimelineViews} />
}TimelineConfig shape
interface TimelineConfig {
container: TimelineMeta
tracks: TrackRow[]
/** Per-group styling overrides, keyed by path prefix. Optional — prefixes
* without an entry use defaults (path-leaf as label, no icon, expanded). */
groups?: Record<string, GroupConfig>
}
interface TimelineMeta {
id: string
name?: string
description?: string
duration: number // seconds — required, positive
t0?: number // wall-clock anchor (unix seconds)
fps?: number // default frame rate; lanes may override
meta?: Record<string, unknown>
}
interface TrackRow {
id: string // required — React key
path: string // required — hierarchy + position ("robot/arm/qpos")
dtype: DtypeId // required — registered dtype id ("joint_angles")
src?: string // m3u8 URL
data?: unknown[] // inline data (mutually exclusive with src)
name?: string // tree-label override; default = last segment of path
visible?: boolean
height?: number
color?: string
icon?: string
props?: Record<string, unknown> // merged on top of dtype.defaults
}
interface GroupConfig {
name?: string // overrides path-leaf default
color?: string
icon?: string
expanded?: boolean // initial collapsed state
}Path-based hierarchy
Hierarchy comes from the /-separated segments in track.path — no parentId, no explicit parent references. Matches the server-side Track.path convention (see the dreamlake-py SDK).
For path: "robot/arm/qpos":
robot/becomes a root grouprobot/arm/becomes a nested group underrobot- the track itself is a leaf under
robot/arm
Both intermediate prefixes appear in the tree automatically. Add groups: { "robot/arm": { name: "Arm", icon: "arm" } } to style the nested level.
Validation rules
validateTimelineConfig throws on the first issue and names the offending track:
container.idnon-empty;container.duration > 0.- Every track has
id,path,dtype. pathhas no leading/trailing/and no empty segments.dtypeis registered (callregisterDtypeat bootstrap for custom ones).- Every track has exactly one of
srcordata. - No duplicate
idor duplicatepath. groupsentries whose prefix doesn't match any track's path emit a dev-mode warning.
Authoring tips
propsoften shrinks to nothing. Dtype defaults cover most presentation fields (shape, range, unit, channelGroups). Per-trackpropsshould only carry episode-specific details likechannelNames.- Inline
dataparticipates in snap points. Anyts/tein inline tracks automatically becomes a cursor-snap point. Live-loaded tracks (src) don't contribute snap points at render time. - Relative URLs are your responsibility. The container doesn't rewrite paths. When loading a config from
/episodes/ep1/config.json, prepend the episode base to everysrcbefore handing it in.
The views map
Required. Maps DtypeId → lane component + optional tree-cell override.
interface TimelineViewEntry {
lane: LaneComponent<any>
treeCell?: FC<TreeCellProps>
icon?: string
defaultHeight?: number
}
type TimelineViews = Record<DtypeId, TimelineViewEntry>defaultTimelineViews ships wiring for all 13 built-in dtypes:
| Dtype | Default lane |
|---|---|
video | VideoLane |
subtitle | PillLane |
scalar, vector, imu_6dof, joint_angles, pose_6dof | LineChartLane |
action_label, ribbon_state | PillLane |
marker_event, detection_2d | MarkerLane |
(audio and image are reserved — no stock lane.)
Override a subset:
<TimelineContainer
config={config}
views={{
...defaultTimelineViews,
joint_angles: { lane: MyQposLane, icon: 'robot', defaultHeight: 100 },
}}
/>If a track's dtype has no views entry, the lane renders as PlaceholderLane with a visible "no view registered" message.
JSX path
The config path is the primary path. JSX is for prototyping + escape-hatch use — you need a lane with a custom normalize decoder, or you want to hand-compose a small demo in code without round-tripping through JSON.
import {
TimelineContainer,
VideoLane,
LineChartLane,
PillLane,
MarkerLane,
} from '@vuer-ai/vuer-m3u'
// Coming in a follow-up — JSX composition parses children into the same
// TrackRow + groups shape the config path uses. Currently the primary
// authoring path is config; document + demo the JSX path in the next pass.JSX-only capability: normalize
Exactly one capability doesn't round-trip to config: a per-lane normalize function for custom chunk shapes. Functions aren't JSON-serializable, so they live only on the JSX form of a lane prop. For most cases you can register the custom decoder globally instead:
import { registerDecoder } from '@vuer-ai/vuer-m3u'
registerDecoder('mpk', (raw) => decodeMsgpack(raw))Then the config path works fine.
Frequently-asked
What happened to view: 'LineChartLane'?
Removed. Tracks used to declare the lane-component name directly — that mixed "what the data is" with "how to draw it." Now tracks carry dtype (data identity) and apps supply a views map (presentation).
What happened to parentId?
Removed. Hierarchy now comes from path. Matches the server-side Track.path convention (1:1).
What happened to the Group view?
Removed. Groups aren't tracks — they're synthesized client-side from path prefixes. Style a group via groups[prefix]; see the config example above.
Can a track render under a header that doesn't match its path?
Not with the stock wiring — path prefix equals visible hierarchy. If you need to group tracks whose paths don't share a prefix, rewrite paths client-side before passing the config to <TimelineContainer>.
Do I have to use defaultTimelineViews?
No — you can construct your own map from scratch. But the 13 built-ins cover every case dreamlake ships with, and overriding a subset via { ...defaultTimelineViews, myDtype: ... } is usually what you want.