D
DreamLake

Timeline

Config vs JSX Authoring

<TimelineContainer> has two authoring styles:

  • Config path — a serializable TimelineConfig object. 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 normalize decoder 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.pathno 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 group
  • robot/arm/ becomes a nested group under robot
  • 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:

  1. container.id non-empty; container.duration > 0.
  2. Every track has id, path, dtype.
  3. path has no leading/trailing / and no empty segments.
  4. dtype is registered (call registerDtype at bootstrap for custom ones).
  5. Every track has exactly one of src or data.
  6. No duplicate id or duplicate path.
  7. groups entries whose prefix doesn't match any track's path emit a dev-mode warning.

Authoring tips

  • props often shrinks to nothing. Dtype defaults cover most presentation fields (shape, range, unit, channelGroups). Per-track props should only carry episode-specific details like channelNames.
  • Inline data participates in snap points. Any ts / te in 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 every src before 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:

DtypeDefault lane
videoVideoLane
subtitlePillLane
scalar, vector, imu_6dof, joint_angles, pose_6dofLineChartLane
action_label, ribbon_statePillLane
marker_event, detection_2dMarkerLane

(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.