D
DreamLake

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

PropTypeDescription
idstring?Stable identifier. If omitted the outer TrackRow.id supplies it.
namestring?Tree-side label. Falls back to id.
heightnumber?Row height in px. Overrides the registry's defaultHeight.
visibleboolean?Default true. Hidden rows skip data loading and render dimmed.
colorstring?Accent color: semantic name (blue, green, orange, purple, gray-light, gray-medium) or any CSS hex.
iconstring?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

PropTypeDescription
srcstring (required)HLS playlist URL
posterstring?Reserved for future use
thumbnailsstring?Optional WebVTT thumbnail track URL
…visual propssee 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,90

Thumbnail 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, pair VideoLane (timeline row) with <VideoPlayer> (elsewhere in the page). They share the same clock via <ClockProvider>.
  • No inline data form — video must be an HLS URL.

Preview

Loading…
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

PropTypeDescription
src / datamutually exclusiveOne required
normalize(decoded) => Sample[]?JSX-only, custom decoder
shapenumber[]?Sample layout. Omit to infer from the first sample.
channelNamesstring[]?Flat legend labels
channelGroupsstring[][]?Visually group channels (e.g. [['x','y','z'], ['qx','qy','qz','qw']])
range[number, number]?Y-axis clamp; auto-scaled if omitted
unitstring?Unit label (reserved for future legend)

Data format

Sample[]:

{ ts: number, data: number | number[] }
  • shape: []data is a scalar
  • shape: [N]data is a length-N vector
  • shape: [H, W]data is 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 useMergedTrack for src (playhead-centric loading) or columnar conversion for inline data (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

Loading…
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

PropTypeDescription
src / datamutually exclusiveOne required
normalize(decoded) => AnnotationEntry[]?JSX-only
textFieldstring?Field on the entry to show. Defaults to label.
…visual propscolor 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 te are skipped. Point events belong on MarkerLane.
  • Per-entry color must be a semantic name (blue / green / orange / purple / gray-light / gray-medium); arbitrary CSS strings fall back to the lane-level color.

Preview

Loading…
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

PropTypeDescription
src / datamutually exclusiveOne 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).
  • color resolution is identical to PillLane's.
  • Hover a diamond to see the cursor-pointer + hover:scale-[1.25] animation.

Preview

Loading…
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.

Loading…
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

ViewdefaultHeight
Group32 px
VideoLane56 px
LineChartLane84 px
PillLane32 px
MarkerLane32 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:

NameHexUse case
blue#3b82f6Robot state, defaults
green#22c55eSensors, video
orange#f97316Attention / warnings
purple#a855f7Narration, annotations
gray-light#d4d4d8De-emphasized
gray-medium#a1a1aaMetadata

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: false or 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 on visible === 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.