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.
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/
| Views | Timeline lanes | |
|---|---|---|
| Shape of output | An 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. |
| Layout | You place it anywhere in the page. | Stacked inside <TimelineContainer>, left-side tree + right-side scrolling body. |
| Zoom / pan | Not involved. Views render "current time" only. | Painted against useTimelineViewport() — every pixel is timeToX(t, v). |
| Clock | From <ClockProvider>. | Same — TimelineContainer itself creates and provides the clock. |
| Data contract | Free-form. Each view documents its own schema. | Canonical — dispatched by dtype. |
| Composition | Standalone. | Via a views: Record<DtypeId, lane> map supplied on <TimelineContainer>. |
| Intended for | Domain-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.
| Lane | Render pattern | Default dtype(s) |
|---|---|---|
VideoLane | Segment-card film strip (or VTT thumbnails) | video |
LineChartLane | Canvas multi-channel line plot | scalar, vector, imu_6dof, joint_angles, pose_6dof |
PillLane | Interval pills with start-dot + duration label | subtitle, action_label, ribbon_state |
MarkerLane | Diamond glyphs at instants | marker_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 livemm:ssreadout (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 (Aacase-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 byclock.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) callstopPropagation.useTimelineWheel—cmd/ctrl + wheelzooms at the cursor,shift + wheelor trackpad horizontal swipe pans, plain vertical wheel falls through to the outer row scroller.useAutoFollow— whileclock.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
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
viewsmap in detail - Custom lanes — registering your own lane / tree cell, lane-authoring checklist