D
DreamLake

Timeline

Timeline

<TimelineContainer> is the multi-track editor-style surface that composes every vuer-m3u view into a single, synchronized document. Where individual views render a single visualization (a video pane, a 3D gizmo, a chart), the timeline renders many horizontal tracks sharing one ruler, one clock, one zoom/pan viewport, and one interaction model.

Think of it as an NLE (non-linear editor) for arbitrary time-segmented data: video on row 1, IMU chart on row 2, a ribbon of action labels on row 3, operator narration on row 4 — all scrubbable together, all zooming together, all seeking the same clock when you click.

Loading timeline…
import {
  TimelineContainer,
  defaultTimelineViews,
  type TimelineConfig,
} from '@vuer-ai/vuer-m3u';

const config: TimelineConfig = {
  container: { id: 'demo_overview', name: 'Demo episode', duration: 15 },
  groups: {
    cams:   { name: 'Cameras',     color: 'green',  icon: 'video' },
    robot:  { name: 'Robot state', color: 'blue',   icon: 'robot' },
    events: { name: 'Events',      color: 'purple', icon: 'caption' },
  },
  tracks: [
    { id: 'wrist_cam', path: 'cams/wrist_cam',
      dtype: 'video', color: 'green',
      src: '/vuer-m3u-demo/video/playlist.m3u8' },

    { id: 'joints', path: 'robot/joints',
      dtype: 'joint_angles', color: 'blue',
      src: '/vuer-m3u-demo/joints/playlist.m3u8' },
    { id: 'imu', path: 'robot/imu',
      dtype: 'imu_6dof', color: 'blue',
      src: '/vuer-m3u-demo/imu/playlist.m3u8' },

    { id: 'phases', path: 'events/phases',
      dtype: 'action_label', color: 'purple',
      data: [
        { ts: 0.5, te: 3.2, label: 'reach' },
        { ts: 3.2, te: 4.0, label: 'approach' },
        { ts: 4.0, te: 6.8, label: 'grasp', color: 'green' },
        { ts: 6.8, te: 9.5, label: 'lift', color: 'orange' },
        { ts: 9.5, te: 12, label: 'retry', kind: 'attempt' },
        { ts: 12, te: 14, label: 'halted', kind: 'halted' },
      ] },
    { id: 'contacts', path: 'events/contacts',
      dtype: 'marker_event', color: 'orange',
      data: [
        { ts: 1.2, label: 'sensor_zero' },
        { ts: 4.1, label: 'contact' },
        { ts: 6.9, label: 'released' },
        { ts: 11.3, label: 'fault' },
      ] },
  ],
};

export default function Demo() {
  return <TimelineContainer config={config} views={defaultTimelineViews} />;
}

Try it: scrub by clicking on any lane, shift+click to drop a persistent T1/T2/ marker, cmd/ctrl+wheel to zoom at the cursor, shift+wheel (or horizontal trackpad swipe) to pan, and drag the duration readout at the bottom to zoom by gesture.

What lives here vs. in views/

ViewsTimeline lanes
Shape of outputAn arbitrary React component — video pane, 3D gizmo, tile of charts. Shape is whatever the view wants.A horizontal strip, height fixed by the row, width driven by the shared viewport.
LayoutYou place it anywhere in the page.Stacked inside <TimelineContainer>, left-side tree + right-side scrolling body.
Zoom / panNot involved. Views render "current time" only.Painted against useTimelineViewport() — every pixel is timeToX(t, v).
ClockFrom <ClockProvider>.Same — TimelineContainer itself creates and provides the clock.
Data contractFree-form. Each view documents its own schema.Canonical — dispatched by dtype.
CompositionStandalone.Via a views: Record<DtypeId, lane> map supplied on <TimelineContainer>.
Intended forDomain-specific panels (pose gizmo, joint-angle stick figure).Overview "at-a-glance" rows that line up against each other in time.

A lane is a view — but a view that agrees to play by the timeline's layout + viewport rules. Any view can be re-skinned as a lane; the reverse is not always true (a lane's horizontal strip is a stricter layout than a free-form view).

Dispatch by dtype

Rows on the timeline declare dtype (data identity). The <TimelineContainer views> map decides which lane renders each dtype. Same data, different apps, different presentations — all without editing the config.

<TimelineContainer
  config={config}
  views={defaultTimelineViews}
/>

The stock defaultTimelineViews covers all 13 built-in dtypes. Override a subset:

views={{ ...defaultTimelineViews, joint_angles: { lane: MyQposLane } }}

See Dtypes for the registry + per-dtype schemas, and Config vs JSX authoring for the views map in detail.

The four built-in lanes

Lanes are classified by display pattern, not by domain meaning. The four built-in lanes handle every row shape in the default wiring; each one serves multiple dtypes.

LaneRender patternDefault dtype(s)
VideoLaneSegment-card film strip (or VTT thumbnails)video
LineChartLaneCanvas multi-channel line plotscalar, vector, imu_6dof, joint_angles, pose_6dof
PillLaneInterval pills with start-dot + duration labelsubtitle, action_label, ribbon_state
MarkerLaneDiamond glyphs at instantsmarker_event, detection_2d

For dtypes that ship without a default lane (audio, image), register your own — see Custom lanes.

Canonical data shapes

Every dtype's JSONL line shape is documented on its per-dtype page. Two general patterns cover nearly everything:

Continuous — one sample per timestamp:

{"ts": 0.00, "data": 12.3}
{"ts": 0.00, "data": [0.25, -0.15, 0.45]}

Discrete — events, intervals, markers:

{"ts": 1.2, "te": 2.5, "label": "grasp", "object": "cup"}
{"ts": 4.8, "label": "contact"}

ts / te are the canonical time fields everywhere in vuer-m3u. Extra fields pass through; lanes ignore what they don't consume.

Anatomy of a <TimelineContainer>

┌ TimelineHeader ─────────────────────────────────────────────────────┐
│ episode title       ⏮ ▶ ⏭ ⊕  4.2s / 30s        [Uniform│Expanded]   │
├──────────────────┬──────────────────────────────────────────────────┤
│ TreeSearch bar   │ TimeRuler  0ms   5s   10s   15s   20s   25s  30s
├──────────────────┼──────────────────────────────────────────────────┤
│ ▶ Cameras        │                                                  │
│   ▸ wrist_cam    │ ████ ████ ████ ████ ████ ████   (VideoLane)      │
│ ▼ Robot state    │                                                  │
│   arm / qpos     │ ╭─╮╱╲/╲_╱⎺⎻⎺╲_╱⎺╲__                 (LineChart) │
│ ▼ Narration      │                                                  │
│   operator       │   ●━━━ reach ━━╮      ●━━━ grasp ━━╮  (PillLane) │
│   milestones     │          ◆        ◆       ◆          (MarkerLane)│
├──────────────────┴──────────────────────────────────────────────────┤
│                          ◀  5.3s  ▶         (NavigationControls)    │
└─────────────────────────────────────────────────────────────────────┘

Layout pieces, all wired by <TimelineContainer>:

  • TimelineHeader — three-column strip. Left: episode title (name ?? id, truncated). Center: transport (⏮ ▶ ⏭), "center on playhead" (⊕), and a live mm:ss readout (current-time span is fixed width so the transport never jitters). Right: row-density toggle (Uniform / Expanded), always pinned to the right edge.
  • TreeColumn — left side. Search (Aa case-sensitive + .* regex), collapsible groups, hide-eye per row, color-coded icons.
  • TimeRuler — top ticks with pill labels. Tick step auto-picks for ~80 px label spacing at the current zoom. Event snap dots sit underneath.
  • LaneColumn — right side. Virtualized (OVERSCAN=6); off-screen rows unmount.
  • Playhead — red vertical line + draggable triangle, driven by clock.on('tick' | 'seek') imperatively so the React tree doesn't re-render at 60 Hz.
  • TimelineCursor — hover cursor with readout pill; snaps to nearby event times (within 8 px) and shows a magnet icon while snapped.
  • NavigationControls — bottom capsule: ◀ pan · duration (drag horizontally to zoom) · ▶ pan.

How interaction works

All pointer + wheel input converges through three hooks installed on the lane area:

  • useSeekOnPointer — click / drag anywhere in the lane area seeks the clock. Children that own a click (a pill's detail popover, say) call stopPropagation.
  • useTimelineWheelcmd/ctrl + wheel zooms at the cursor, shift + wheel or trackpad horizontal swipe pans, plain vertical wheel falls through to the outer row scroller.
  • useAutoFollow — while clock.playing, when the playhead exits the viewport, scroll so it re-enters with an 8 % left-edge margin. Paused users who panned away are not fought.

One reducer-backed viewport (TimelineViewportProvider) owns pxPerSecond, scrollSec, containerWidth, and duration. Every producer of a pixel — ruler, playhead, every lane, every interaction — reads timeToX from coords.ts. Drift between producers is the classic timeline-editor bug; centralizing the formula prevents it.

Two authoring paths

Primary: a serializable TimelineConfig handed to the container. Secondary: JSX composition (useful for prototyping + per-lane normalize functions that don't JSON-serialize).

// Config path — server-driven documents
<TimelineContainer config={timelineConfig} views={defaultTimelineViews} />

See Config vs JSX authoring for the full comparison and migration guidance.

Quick start

Loading…
import {
  TimelineContainer,
  defaultTimelineViews,
  type TimelineConfig,
} from '@vuer-ai/vuer-m3u';

const config: TimelineConfig = {
  container: { id: 'episode_001', duration: 15 },
  tracks: [
    { id: 'cam',  path: 'cam',    dtype: 'video',        color: 'green',
      src: '/vuer-m3u-demo/video/playlist.m3u8' },
    { id: 'qpos', path: 'qpos',   dtype: 'joint_angles', color: 'blue',
      src: '/vuer-m3u-demo/joints/playlist.m3u8' },
    { id: 'ops',  path: 'phases', dtype: 'action_label', color: 'purple',
      data: [
        { ts: 0.5, te: 3.0,  label: 'reach' },
        { ts: 3.0, te: 6.5,  label: 'grasp',   color: 'green' },
        { ts: 6.5, te: 10.0, label: 'place',   color: 'orange' },
        { ts: 10.0, te: 13.5, label: 'release' },
      ] },
  ],
};

export default function Demo() {
  return <TimelineContainer config={config} views={defaultTimelineViews} />;
}

That's everything — the container creates a TimelineClock, wraps its children in <ClockProvider>, sets up the viewport, and dispatches each track through the views map.

Next

  • Dtypes — the 13 built-in data types with JSONL schemas, sample data, and compatible lanes / standalone views
  • Lanes reference — every lane primitive with its rendering pattern and props
  • Config vs JSX authoring — dtype + path + groups + the views map in detail
  • Custom lanes — registering your own lane / tree cell, lane-authoring checklist