Timeline
Lanes Reference
Every row inside <TimelineContainer> is rendered by a lane component. Lanes are picked from the views prop — a map keyed by DtypeId — using TrackRow.dtype as the lookup key. Dtypes without a matching entry fall through to PlaceholderLane, which renders a visible "no view registered" message.
This page is the reference for the four built-in lane components shipped in defaultTimelineViews, plus the three type-only primitives (AudioLane, AreaChartLane, RibbonLane) that ship without a default renderer.
Groups are NOT lanes — they're synthesized client-side from track path prefixes. See the Path-based groups section below.
Contract every lane shares
Every lane gets three groups of props, the first two common to all:
LaneVisualProps — identity / presentation
| Prop | Type | Description |
|---|---|---|
id | string? | Stable identifier. If omitted the outer TrackRow.id supplies it. |
name | string? | Tree-side label. Falls back to id. |
height | number? | Row height in px. Overrides the registry's defaultHeight. |
visible | boolean? | Default true. Hidden rows skip data loading and render dimmed. |
color | string? | Accent color: semantic name (blue, green, orange, purple, gray-light, gray-medium) or any CSS hex. |
icon | string? | Tree-cell icon name (one of IconName) or URL. |
LaneDataProps<T> — src / data / normalize
interface LaneDataProps<T> {
src?: string; // m3u8 URL
data?: T[]; // inline array
normalize?: (decoded: unknown) => T[]; // JSX-only
}Mutual exclusion: exactly one of src or data must be provided. Both or neither throws at runtime with a message that names the offending track — the same error for the config path and the JSX path.
normalize is JSX-only: a function can't round-trip through JSON, so it's not representable in a TimelineConfig. Use it when your chunks have a wrapper envelope, a non-standard field name, or any shape that isn't already Sample / AnnotationEntry.
Per-lane props
Everything else is specific to the lane — shape on LineChartLane, textField on PillLane, and so on. On the config path these live in TrackRow.props; on the JSX path they're just regular JSX attributes.
VideoLane
Segment-card strip — one card per HLS segment, with an optional WebVTT thumbnail track.
Props
| Prop | Type | Description |
|---|---|---|
src | string (required) | HLS playlist URL |
poster | string? | Reserved for future use |
thumbnails | string? | Optional WebVTT thumbnail track URL |
| …visual props | see above |
Data format
Two modes, chosen by whether thumbnails is set:
Plain mode (default). VideoLane reads playlist.segments metadata only — never decodes a chunk. Each segment becomes an accent-bordered card with a timestamp label. Signals "video content" without pretending to show frames.
Thumbnail mode. Provide a WebVTT track (YouTube / Vimeo / Bitmovin convention). Each cue maps a time range to an image URL, optionally with an #xywh=x,y,w,h sprite fragment:
WEBVTT
00:00:00.000 --> 00:00:02.000
thumbs/000.jpg
00:00:02.000 --> 00:00:04.000
sprite.jpg#xywh=0,0,160,90
00:00:04.000 --> 00:00:06.000
sprite.jpg#xywh=160,0,160,90Thumbnail cadence is independent of m3u8 segment length — you can have coarser segments and finer thumbnails (or vice versa). Cues outside the 3 px visibility threshold are skipped.
Notes
<TimelineContainer>renders video timelines, not video frames. For the actual video pixels in a separate pane, pairVideoLane(timeline row) with<VideoPlayer>(elsewhere in the page). They share the same clock via<ClockProvider>.- No inline
dataform — video must be an HLS URL.
Preview
import {
TimelineContainer,
defaultTimelineViews,
type TimelineConfig,
} from '@vuer-ai/vuer-m3u';
const config: TimelineConfig = {
container: { id: 'video_demo', duration: 15 },
tracks: [
{ id: 'wrist', path: 'wrist_cam', dtype: 'video', color: 'green',
src: '/vuer-m3u-demo/video/playlist.m3u8' },
],
};
export default function Demo() {
return <TimelineContainer config={config} views={defaultTimelineViews} />;
}LineChartLane
Multi-channel line plot rendered onto a DPR-aware canvas. Repaints only when tracks or viewport change (not per tick), because the curve is static across the viewport; the playhead overlay is rendered separately.
Props
| Prop | Type | Description |
|---|---|---|
src / data | mutually exclusive | One required |
normalize | (decoded) => Sample[]? | JSX-only, custom decoder |
shape | number[]? | Sample layout. Omit to infer from the first sample. |
channelNames | string[]? | Flat legend labels |
channelGroups | string[][]? | Visually group channels (e.g. [['x','y','z'], ['qx','qy','qz','qw']]) |
range | [number, number]? | Y-axis clamp; auto-scaled if omitted |
unit | string? | Unit label (reserved for future legend) |
Data format
Sample[]:
{ ts: number, data: number | number[] }shape: []→datais a scalarshape: [N]→datais a length-N vectorshape: [H, W]→datais a flattened length-H·W row-major matrix
Example — 7-DoF arm joint angles at 100 Hz:
{"ts": 0.00, "data": [0.1, -0.2, 0.3, 0.4, -0.1, 0.0, 0.2]}
{"ts": 0.01, "data": [0.1, -0.2, 0.3, 0.4, -0.1, 0.0, 0.2]}Notes
- Uses
useMergedTrackforsrc(playhead-centric loading) or columnar conversion for inlinedata(fully loaded). The canvas sees the same internal shape either way. - Downsampling: never draws more samples than pixels. A 100 Hz track over a 1000 px lane renders at 1 sample / px; zoomed out, most samples are skipped.
- Channel colors come from the golden-angle hue sequence (
hsl((c * 137.5) % 360, 65%, 60%)). Stable per channel index.
Preview
import {
TimelineContainer,
defaultTimelineViews,
type TimelineConfig,
} from '@vuer-ai/vuer-m3u';
const config: TimelineConfig = {
container: { id: 'chart_demo', duration: 15 },
tracks: [
// dtype 'joint_angles' provides range + unit via its defaults;
// only episode-specific overrides stay in `props`.
{ id: 'joints', path: 'arm/qpos', dtype: 'joint_angles',
color: 'blue', icon: 'waves',
src: '/vuer-m3u-demo/joints/playlist.m3u8',
props: { channelNames: ['j0','j1','j2','j3','j4','j5','j6'] } },
],
};
export default function Demo() {
return <TimelineContainer config={config} views={defaultTimelineViews} />;
}PillLane
Interval pills in the waterfall style: rounded pill body, accent-bordered start-dot, centered duration label.
Props
| Prop | Type | Description |
|---|---|---|
src / data | mutually exclusive | One required |
normalize | (decoded) => AnnotationEntry[]? | JSX-only |
textField | string? | Field on the entry to show. Defaults to label. |
| …visual props | color picks the default pill tint; entries can override per-pill |
Data format
AnnotationEntry[] with te present:
{ ts: number, te: number, label?: string, color?: string, halted?: boolean, ... }Example — operator narration:
{"ts": 0.5, "te": 2.8, "label": "reach_for_cup"}
{"ts": 2.8, "te": 3.2, "label": "grasp"}
{"ts": 3.2, "te": 5.0, "label": "lift", "color": "orange"}Halted variant
Entries with halted: true, kind: 'halted', or state: 'halted' render as a dashed segment between two vertical caps with an orange "Halted" pill — matches the reference waterfall's halted-step treatment.
Striped variant
stripes: true or kind: 'attempt' adds a diagonal-stripe overlay on the pill body. Visually marks partial / retried segments.
Notes
- Entries without
teare skipped. Point events belong onMarkerLane. - Per-entry
colormust be a semantic name (blue/green/orange/purple/gray-light/gray-medium); arbitrary CSS strings fall back to the lane-levelcolor.
Preview
import {
TimelineContainer,
defaultTimelineViews,
type TimelineConfig,
} from '@vuer-ai/vuer-m3u';
const config: TimelineConfig = {
container: { id: 'pill_demo', duration: 15 },
tracks: [
{ id: 'phases', path: 'phases', dtype: 'action_label', color: 'blue',
data: [
// `createTime` → dashed wait-line before the execution bar
{ ts: 0.5, te: 2.8, label: 'reach', createTime: 0.0 },
{ ts: 2.8, te: 4.2, label: 'approach' },
{ ts: 4.2, te: 6.8, label: 'grasp', color: 'green' },
{ ts: 6.8, te: 9.0, label: 'lift', color: 'orange' },
// striped variant
{ ts: 9.0, te: 10.8, label: 'retry', kind: 'attempt' },
// dashed + orange "Halted" pill — smoothly shrinks as you zoom
{ ts: 10.8, te: 13.5, label: 'halted', kind: 'halted' },
] },
],
};
export default function Demo() {
return <TimelineContainer config={config} views={defaultTimelineViews} />;
}MarkerLane
Diamond glyphs for instantaneous events.
Props
| Prop | Type | Description |
|---|---|---|
src / data | mutually exclusive | One required |
normalize | (decoded) => AnnotationEntry[]? | JSX-only |
shape | 'diamond' | 'circle' | 'triangle' | Glyph shape. Defaults to diamond. |
Data format
AnnotationEntry[], typically without te:
{"ts": 1.2, "label": "contact"}
{"ts": 3.8, "label": "released"}
{"ts": 5.4, "label": "checkpoint", "color": "orange"}If te is present it is simply ignored — markers only care about ts.
Notes
- Markers are clipped at ±5 % past the viewport edges (not a hard clip, just a perf optimization — most off-screen markers don't mount).
colorresolution is identical toPillLane's.- Hover a diamond to see the cursor-pointer +
hover:scale-[1.25]animation.
Preview
import {
TimelineContainer,
defaultTimelineViews,
type TimelineConfig,
} from '@vuer-ai/vuer-m3u';
const config: TimelineConfig = {
container: { id: 'marker_demo', duration: 15 },
tracks: [
{ id: 'milestones', path: 'milestones',
dtype: 'marker_event', color: 'purple',
data: [
{ ts: 1.0, label: 'start' },
{ ts: 3.4, label: 'contact', color: 'orange' },
{ ts: 5.1, label: 'released' },
{ ts: 7.8, label: 'checkpoint', color: 'green' },
{ ts: 10.2, label: 'fault', color: 'orange' },
{ ts: 12.6, label: 'reset' },
{ ts: 14.2, label: 'end' },
] },
],
};
export default function Demo() {
return <TimelineContainer config={config} views={defaultTimelineViews} />;
}Path-based groups
Groups are not lanes — they're synthesized client-side from the /-separated prefixes in track paths. There's no GroupLane component, and no group dtype. The path "robot/arm/qpos" implies two levels of groups (robot, robot/arm); TimelineConfig.groups supplies per-prefix styling.
{
groups: {
'robot': { name: 'Robot state', color: 'blue', icon: 'robot' },
'robot/arm': { name: 'Arm', icon: 'arm' },
},
tracks: [
{ id: 'qpos', path: 'robot/arm/qpos', dtype: 'joint_angles', src: '/q.m3u8' },
],
}Even without an override entry, a prefix that has children appears in the tree — it just uses defaults (path-leaf as label, expanded, no icon).
Preview
Nested groups with L-shaped tree guides. Click a chevron or a group row to collapse/expand; hover a child to see the guide lines highlight the parent chain.
import {
TimelineContainer,
defaultTimelineViews,
type TimelineConfig,
} from '@vuer-ai/vuer-m3u';
const config: TimelineConfig = {
container: { id: 'group_demo', duration: 10 },
groups: {
'sensors': { name: 'Sensors', color: 'green', icon: 'waves' },
'sensors/cams': { name: 'Cameras', color: 'green', icon: 'video' },
'events': { name: 'Events', color: 'purple', icon: 'caption' },
},
tracks: [
{ id: 'cam_wrist', path: 'sensors/cams/wrist',
dtype: 'marker_event', color: 'green',
data: [1,3,5,7].map((ts) => ({ ts, label: 'shutter' })) },
{ id: 'cam_scene', path: 'sensors/cams/scene',
dtype: 'marker_event', color: 'green',
data: [2,4,6,8].map((ts) => ({ ts, label: 'shutter' })) },
{ id: 'imu', path: 'sensors/imu_tick',
dtype: 'marker_event', color: 'blue',
data: [0.5, 1.5, 2.5, 3.5, 4.5].map((ts) => ({ ts, label: 't' })) },
{ id: 'phases', path: 'events/phases',
dtype: 'action_label', color: 'purple',
data: [
{ ts: 0, te: 3, label: 'phase_a' },
{ ts: 3, te: 6, label: 'phase_b', color: 'green' },
{ ts: 6, te: 9, label: 'phase_c', color: 'orange' },
] },
],
};
export default function Demo() {
return <TimelineContainer config={config} views={defaultTimelineViews} />;
}Reserved primitives (type-only)
The types ship with three additional lane shapes that aren't wired in defaultTimelineViews yet. Prop types are stable; register a renderer via your own views map to use them.
AudioLane
interface AudioLaneProps extends LaneVisualProps {
src: string; // required: HLS audio playlist URL
gain?: number; // playback gain multiplier (1.0 = passthrough)
}Intended rendering: waveform strip. Data format: audio HLS segments.
AreaChartLane
interface AreaChartLaneProps extends LaneVisualProps, LaneDataProps<Sample> {
range?: [number, number];
unit?: string;
fill?: string;
}Intended rendering: single-channel filled area plot. Data format: Sample[], same contract as LineChartLane but restricted to scalar data.
RibbonLane
interface RibbonLaneProps extends LaneVisualProps, LaneDataProps<AnnotationEntry> {
stateColors?: Record<string, string>; // state key → CSS color
stateField?: string; // defaults to 'state'
}Intended rendering: colored state bands (e.g. execute → green, halted → orange). Data format: AnnotationEntry[] where entry[stateField] looks up into stateColors.
See Custom lanes for how to register any of these.
Default row heights
| View | defaultHeight |
|---|---|
Group | 32 px |
VideoLane | 56 px |
LineChartLane | 84 px |
PillLane | 32 px |
MarkerLane | 32 px |
Override per row with TrackRow.height (or height JSX prop). The "Uniform" row-mode toggle in the header overrides every row to 32 px regardless of the per-row value — useful when you want the density of a scannable list over the information density of per-lane sizing.
Color semantics
Lanes recognize a small palette of semantic color names and map them through matching Tailwind classes in both light and dark themes:
| Name | Hex | Use case |
|---|---|---|
blue | #3b82f6 | Robot state, defaults |
green | #22c55e | Sensors, video |
orange | #f97316 | Attention / warnings |
purple | #a855f7 | Narration, annotations |
gray-light | #d4d4d8 | De-emphasized |
gray-medium | #a1a1aa | Metadata |
Arbitrary CSS strings work for per-lane accents (used in inline styles like the VideoLane card border), but fall back to gray-medium for the wedge / pill variants that require a Tailwind class.
Hidden / filtered / selected state
- Hidden (
visible: falseor eye toggle) — row renders dimmed (opacity-30 saturate-50). Data loading is expected to be skipped by lane implementations; the built-ins still mount but you can short-circuit yours onvisible === false. - Filtered — the tree search hides non-matching rows; matches keep their ancestors and descendants visible. A row in filter mode behaves the same as normal — filter only changes what the tree flattens.
- Selected — left-side row gets an indigo-tinted background. Selection is tracked but currently informational.
- Hovered — both sides tint slightly; plumbed so a hover on the tree highlights the matching lane row and vice versa.