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:
- Accept the standard
LaneVisualProps+ (when data-driven)LaneDataProps<T>. - Read the viewport via
useTimelineViewport()and convert every horizontal pixel withtimeToX(t, v)so the lane stays in sync with ruler / playhead / other lanes. - Optionally consult the injected
dtypespec — the container spreadsdtype.defaultsinto props before your component sees them, and also passes the resolved spec as adtypeprop 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 leftOverriding 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 isSample,AnnotationEntry, or your own shape. - Call
assertSrcOrData(props, context)if the lane is data-driven. - Split into
Src/Datainner components so hooks are unconditional. - Use
useTimelineViewport()+timeToXfor every horizontal coordinate. - Don't
setStateper clock tick — subscribe toclock.on('tick')and mutate DOM imperatively (see the built-inPlayheadfor the pattern). - Respect
visible === falseby skipping heavy work (fetches, canvas paints). - Supply
defaultHeightin theviewsentry — absence falls back to 40 px. - Register a matching
DtypeSpecviaregisterDtype()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.