annie.pages.timeline

The Event-annotation timeline — an inline-SVG track/event surface (UI).

A video-editor-style timeline: a time ruler, a vertical playhead across every lane, and one lane per track holding its event boxes. The surface is always the full width of its container; zooming does not widen it — it narrows the time window shown, so the ruler grows denser and boundaries are easier to place. The visible window is [view_start_frame, view_end_frame] (the task keeps it centred on the playhead); frames map to x within that window, and the gesture x-fraction maps back through the same window.

Within one track, events that overlap in time are stacked into sub-rows rather than drawn on top of each other, so every event stays clickable; a lane’s height grows with how many events are concurrent at once (see pack_lanes()).

Horizontal geometry is in percent. The SVG has no viewBox, so its user units are real pixels, while every x position and width is written as a percentage of the element’s width (x="12.5%"). The server therefore never needs to know the browser’s pixel width, nothing is scaled, and text is never stretched — the failure mode of a fixed viewBox squeezed to the container with preserveAspectRatio="none". Vertical measurements (lane heights, font size) are plain pixels.

Where the logic lives. Python owns the model and renders the SVG markup — this keeps the geometry testable (build_svg(), pack_lanes(), both pure) and mirrors how the segment-review layout keeps its logic server-side. JavaScript does two things only: it reports pointer gestures back (carrying the track and an x fraction of the window) so Python can convert them to frames, and it moves the single playhead line during playback without a full re-render (see annie.pages.videoclock.set_playhead_x()).

Attributes

Classes

TimelineEvent

One event to draw on a lane.

TimelineTrack

One lane: a track name and the events on it.

Functions

pack_lanes(→ list[int])

Assign each event a sub-row so overlapping events never share one.

build_svg(→ str)

Render the timeline as an inline-SVG string for the visible frame window.

gesture_script(→ str)

Return JS that reports a timeline click to the server via emitEvent.

scrollbar_html(→ str)

Return the pan scrollbar markup: a track holding a draggable thumb.

scrollbar_script(→ str)

Return JS that makes the scrollbar draggable and click-to-centre.

Module Contents

annie.pages.timeline.ROW_HEIGHT = 22
annie.pages.timeline.LANE_PAD = 6
annie.pages.timeline.RULER_HEIGHT = 22
annie.pages.timeline.LANE_GAP = 8
annie.pages.timeline.LANE_LABEL_HEIGHT = 16
annie.pages.timeline.REFERENCE_WIDTH = 1300
annie.pages.timeline.MIN_BOX_PERCENT = 0.25
annie.pages.timeline.FONT_SIZE = 12
annie.pages.timeline.EVENT_RADIUS = 4
class annie.pages.timeline.TimelineEvent[source]

One event to draw on a lane.

event_id

The event’s stable id (echoed back on click, to select it).

start_frame

Inclusive start frame.

end_frame

Inclusive end frame.

label

Text drawn inside the box (may be empty).

selected

Whether to draw this box in the selected style.

color

Per-event fill colour override (hex), or None to use the lane’s colour.

class annie.pages.timeline.TimelineTrack[source]

One lane: a track name and the events on it.

name

The track (category) name, shown as the lane label.

events

The events to draw, in any order.

color

Optional lane colour override (hex); None uses the palette by lane order.

annie.pages.timeline.pack_lanes(events: collections.abc.Sequence[TimelineEvent]) → list[int][source]

Assign each event a sub-row so overlapping events never share one.

A greedy interval-partitioning pass: events are considered in start-frame order and each is placed on the lowest sub-row whose last event has already ended. Two events overlap when one starts at or before the other ends (inclusive frames), so touching intervals are treated as overlapping and separated — clearer for the reviewer than a 1px seam.

Parameters:

events – The events on one track, in any order.

Returns:

A list of sub-row indices, parallel to events in their original order.

annie.pages.timeline.build_svg(tracks: collections.abc.Sequence[TimelineTrack], *, view_start: int, view_end: int, fps: float, svg_id: str, pending_start: int | None = None, pending_end: int | None = None) → str[source]

Render the timeline as an inline-SVG string for the visible frame window.

Pure and deterministic — no NiceGUI, no DOM — so the geometry (tick placement, event box extents, lane stacking) is unit-testable.

Parameters:
  • tracks – The lanes to draw, top to bottom.

  • view_start – First frame shown at the left edge.

  • view_end – Last frame shown at the right edge (> view_start).

  • fps – Frames per second, for converting the window to ruler seconds.

  • svg_id – DOM id for the <svg> (the playhead updater and gesture bridge use it).

  • pending_start – While the reviewer is mid-capture (start marked, end not yet), the start frame of the growing amber band; None when not capturing.

  • pending_end – The band’s right edge — the live playhead frame; defaults to pending_start (a minimal band) when omitted. Ignored if pending_start is None.

Returns:

A complete <svg>…</svg> markup string that scales to its container’s width.

annie.pages.timeline.gesture_script(svg_id: str, event_name: str, *, pannable: bool = False) → str[source]

Return JS that reports a timeline click to the server via emitEvent.

Attached once after the SVG is embedded. It listens for pointer down/up on the SVG and emits event_name with a detail of {kind, x, ...} where x is a [0, 1] fraction of the visible window:

  • kind = "select" when an existing event box is clicked (carries event_id);

  • kind = "seek" for a click anywhere else, or any drag.

The timeline is navigation-only — it never creates events — so there is no click/drag authoring gesture; boundaries come from the START/END EVENT buttons and the i/o keys.

Parameters:
  • svg_id – DOM id of the timeline <svg>.

  • event_name – The custom event name the NiceGUI handler listens for.

  • pannable – Whether the timeline is zoomed in. When true, a horizontal wheel/trackpad scroll (or Shift+wheel) pans the window and emits kind = "wheel" with dx (a fraction of the window width); the page’s own scroll is left alone otherwise.

Returns:

The JavaScript to run once via ui.run_javascript.

annie.pages.timeline.scrollbar_html(scroll_id: str, left: float, width: float) → str[source]

Return the pan scrollbar markup: a track holding a draggable thumb.

Parameters:
  • scroll_id – DOM id for the track element.

  • left – The thumb’s left edge as a [0, 1] fraction of the track.

  • width – The thumb’s width as a [0, 1] fraction of the track.

Returns:

An HTML string; positions are percentages so it fills any container width.

annie.pages.timeline.scrollbar_script(scroll_id: str, event_name: str) → str[source]

Return JS that makes the scrollbar draggable and click-to-centre.

Emits event_name with {kind: "pan", x} where x is the thumb’s position as a fraction of the pannable range (0 = window at the start, 1 = at the end), throttled to ~20 Hz while dragging.

Parameters:
  • scroll_id – DOM id of the scrollbar track.

  • event_name – The custom event name the NiceGUI handler listens for.

Returns:

The JavaScript to run once via ui.run_javascript.