D
DreamLake

Timeline

Custom Lanes

<TimelineContainer> dispatches every track through the views prop — a map keyed by DtypeId. The stock defaultTimelineViews covers the 13 built-in dtypes; extend or override it to add custom lanes, tree-cell overrides, or to wire one of the type-only primitives (AudioLane, AreaChartLane, RibbonLane).

The views map

type TimelineViews = Record<DtypeId, TimelineViewEntry>
 
interface TimelineViewEntry {
  lane: LaneComponent<any>            // right-side lane renderer
  treeCell?: FC<TreeCellProps>         // optional left-side override
  icon?: string                        // tree-cell icon
  defaultHeight?: number               // used when TrackRow.height is absent
}

Lookup key: TrackRow.dtype. Missing entry → PlaceholderLane + dev warn.

Extending defaultTimelineViews

import {
  TimelineContainer,
  defaultTimelineViews,
  registerDtype,
  type TimelineViews,
} from '@vuer-ai/vuer-m3u'
import { QposLane } from './lanes/QposLane'
 
// Register the dtype once at app bootstrap (if it's a new one).
registerDtype({
  id: 'arm_qpos_extended',
  name: 'Arm qpos (extended)',
  defaults: { range: [-Math.PI, Math.PI], unit: 'rad' },
})
 
const views: TimelineViews = {
  ...defaultTimelineViews,
  arm_qpos_extended: { lane: QposLane, icon: 'robot', defaultHeight: 84 },
  // Or override an existing dtype to use a custom lane:
  joint_angles: { lane: QposLane, icon: 'robot', defaultHeight: 84 },
}
 
export default function Demo() {
  return <TimelineContainer config={config} views={views} />
}

Spreading defaultTimelineViews keeps the 13 built-ins; the entries you list after win on conflict.

Writing a lane component

A lane is a React component that renders into one row's right-side body. It follows three conventions:

  1. Accept the standard LaneVisualProps + (when data-driven) LaneDataProps<T>.
  2. Read the viewport via useTimelineViewport() and convert every horizontal pixel with timeToX(t, v) so the lane stays in sync with ruler / playhead / other lanes.
  3. Optionally consult the injected dtype spec — the container spreads dtype.defaults into props before your component sees them, and also passes the resolved spec as a dtype prop so you can introspect it.

Minimal example — a scalar-threshold lane

import type {
  LaneComponent,
  LaneDataProps,
  LaneVisualProps,
  Sample,
} from '@vuer-ai/vuer-m3u'
import {
  assertSrcOrData,
  useTimelineViewport,
  timeToX,
} from '@vuer-ai/vuer-m3u'
 
export interface ThresholdLaneProps
  extends LaneVisualProps,
    LaneDataProps<Sample> {
  threshold: number         // above → red tint, below → green tint
}
 
export const ThresholdLane: LaneComponent<ThresholdLaneProps> = (props) => {
  assertSrcOrData(props, `ThresholdLane "${props.id ?? ''}"`)
  // ...render: fetch or normalize data, draw canvas / divs per sample,
  // positioning horizontally via timeToX(sample.ts, v).
  return <div>...</div>
}

No static __viewName is needed — dispatch goes through the views map keyed on dtype, not on component identity.

Using data — src vs data

If you accept both, split into two inner components so hooks are unconditional:

export const ThresholdLane: LaneComponent<ThresholdLaneProps> = (props) => {
  assertSrcOrData(props, `ThresholdLane "${props.id ?? ''}"`)
  if (props.src) return <ThresholdFromSrc {...props} src={props.src} />
  return <ThresholdFromData {...props} data={props.data!} />
}

Pattern mirrors the built-in LineChartLane / PillLane / MarkerLane verbatim.

Using the viewport

const v = useTimelineViewport()
// v.pxPerSecond   — current zoom
// v.scrollSec     — time at x=0 in the lane area
// v.viewStart     — first visible second
// v.viewEnd       — last visible second
// v.duration      — total document duration
// v.containerWidth — measured lane-area pixel width
 
const x = timeToX(sample.ts, v)         // pixels from left

Overriding the tree cell

Per-dtype treeCell override in the views map:

import type { TreeCellProps } from '@vuer-ai/vuer-m3u'
 
function QposTreeCell({ row, expanded, onToggleHidden }: TreeCellProps) {
  // row is discriminated — check row.kind === 'track' to access .track,
  //   or row.kind === 'group' to access .path + .config.
  return <div className="flex items-center h-full px-2">…</div>
}
 
const views = {
  ...defaultTimelineViews,
  joint_angles: { lane: QposLane, treeCell: QposTreeCell, defaultHeight: 84 },
}

TreeCellProps carries the full TreeRow (track row or group row), depth/hasChildren/isLast via row, plus expanded/hidden/selected flags and the toggle callbacks.

Wiring the reserved primitives

AudioLane, AreaChartLane, and RibbonLane ship as TypeScript interfaces but not as views entries. Write the renderer, register it:

import type { AudioLaneProps, LaneComponent } from '@vuer-ai/vuer-m3u'
 
const AudioLane: LaneComponent<AudioLaneProps> = ({ src, gain }) => {
  // draw a waveform strip — peaks can come from a peaks.json sidecar
  // or be computed from the decoded audio
  return <div>...</div>
}
 
const views = {
  ...defaultTimelineViews,
  audio: { lane: AudioLane, icon: 'audio', defaultHeight: 56 },
}

The dtype id is the lookup key — pick a stable snake_case string.

Lane-authoring checklist

Before you register a new lane:

  • Accept LaneVisualProps (or equivalent).
  • Accept LaneDataProps<T> where T is Sample, AnnotationEntry, or your own shape.
  • Call assertSrcOrData(props, context) if the lane is data-driven.
  • Split into Src / Data inner components so hooks are unconditional.
  • Use useTimelineViewport() + timeToX for every horizontal coordinate.
  • Don't setState per clock tick — subscribe to clock.on('tick') and mutate DOM imperatively (see the built-in Playhead for the pattern).
  • Respect visible === false by skipping heavy work (fetches, canvas paints).
  • Supply defaultHeight in the views entry — absence falls back to 40 px.
  • Register a matching DtypeSpec via registerDtype() if your lane serves a new data type.
  • Document the dtype's JSONL schema on a /vuer-m3u/dtypes/<id>/ MDX page following the per-dtype template.

When a lane isn't the right shape

If your visualization is fundamentally non-horizontal (a detail pane, a 3D scene, a cluster of charts), don't make it a lane. Make it a standalone view and place it outside the timeline, sharing the clock via <ClockProvider>. Use a lane for the horizontal overview row and a view for the detailed inspection pane.