# TimeSeries Tag – Developer Guide
This document explains how the **TimeSeries** component in Label Studio is built, how its synchronisation with other media works (video / audio), and how the playback cursor logic is implemented.
> The goal is to give both humans and LLMs enough context to confidently extend or debug the code.
---
## 1. High-level architecture
```
TimeSeries (MST model + React container)
├── Channels (one per data column) – individual SVG plots
│ ├── Hover tracker (grey) – shows XY under mouse
│ └── Playhead line (customizable) – current playback position
├── MultiChannel (grouped visualization) – multiple channels in single plot
│ ├── Channel Legend – interactive channel visibility controls
│ ├── TimeSeriesVisualizer – unified D3 rendering component
│ └── Automatic color palette – consistent channel colors
├── Overview (small plot at bottom) – brush for zoom / pan
│ └── Playhead line (customizable)
└── Region brushes – user labelled time ranges
```
* **Model** – `TimeSeriesModel` (MobX-state-tree). Holds data, view state and actions. Mixin order:
`SyncableMixin → ObjectBase → PersistentStateMixin → AnnotationMixin → Model`.
* **View** – `HtxTimeSeriesViewRTS` (React) renders overview + channel children.
* **Channels** – legacy rendering via `ChannelD3` (being replaced by TimeSeriesVisualizer).
* **MultiChannel** – `MultiChannelModel` + `HtxMultiChannel` component that groups channels together.
* **TimeSeriesVisualizer** – unified D3 rendering component that replaces `ChannelD3` for both single and multi-channel views.
* **ChannelLegend** – interactive legend with visibility controls and hover highlighting.
* **Overview** – rendered inside the same file; supplies brush that changes `brushRange`.
### Important reactive fields
| Field | Type | Meaning |
|----------------------|---------------------|---------|
| `brushRange` | `[number, number]` | Visible time window (native units) |
| `cursorTime` | `number \| null` | Current playhead position (native) |
| `seekTo` | `number \| null` | One–shot instruction for overview to centre view |
| `scale` | `number` | Cached zoom factor (forces rerender) |
| `canvasWidth` | `number` | Cached width in px for correct math |
| `isPlaying` & co. | … | Playback loop state |
| `cursorcolor` | `string` | Hex/colour string for playhead (default: `--color-neutral-inverted-surface`) |
| `suppressSync` | `boolean` | Temporarily disable sync events (during overview drag) |
Native units = *ms* when `timeformat` is a date, otherwise raw numeric indices/seconds.
---
## 2. MultiChannel functionality
The **MultiChannel** tag enables grouping multiple data channels in a single visualization with the following features:
### 2.1 Channel Legend
Interactive legend component that allows users to:
* Toggle channel visibility by clicking on legend items
* Highlight channels on hover for better visual focus
* Automatically assigns colors from a predefined palette
### 2.2 Color Palette System
* Automatic color assignment based on channel index
* Colors from design system: grape, mango, kale, persimmon, sand, kiwi, canteloupe, fig, plum, blueberry
* Optimized for contrast and accessibility
* Located in `palette.js` with `getChannelColor(index)` function
### 2.3 TimeSeriesVisualizer
Unified D3-based rendering component that **replaces the legacy `ChannelD3`** approach:
* Supports both single channel and multichannel visualizations
* Handles brush interactions for region creation
* Manages playhead cursor positioning
* Provides consistent rendering logic across MultiChannel and Channel
* Eliminates code duplication between single and multi-channel rendering
### 2.4 Usage Example
```xml
```
---
## 3. Synchronisation between object tags
The **SyncableMixin** provides a small intra-tab message bus.
* Each tag may specify `sync="groupA"`. All tags with the same name join one `SyncManager`.
* Supported events: `play`, `pause`, `seek`, `speed`.
* A 100 ms **SYNC_WINDOW** prevents infinite feedback loops – the originator is "locked" and only its events propagate during that window.
* **TimeSeries** registers handlers for `seek / play / pause` when FF `FF_TIMESERIES_SYNC` is on.
* Outgoing events are emitted via `syncSend`:
* Overview brush drag → `emitSeekSync()` (fires on `updateTR`).
* Manual click on main plot → handled in `handleMainAreaClick`.
* **Note**: Overview dragging temporarily sets `suppressSync = true` to prevent cursor jumps.
* Incoming events call `_handleSeek`, `_handlePlay`, `_handlePause` which in turn
* move cursor (`cursorTime`),
* optionally restart playback loop,
* update view by calling `_updateViewForTime` **only if needed**.
Audio tags honour `mute` logic so that only one sound is audible.
---
## 4. Playback cursor implementation
### 4.1 Data flow
1. **Source of truth** – `cursorTime` in the model (native units).
2. `cursorTime` is written by:
* Playback loop (`playbackLoop`),
* `_handleSeek` (remote seek),
* `_updateViewForTime` (local scroll),
* Manual click (`setCursor`) when inside current view.
3. Channels and Overview subscribe through React `useEffect`s and D3 – whenever `item.cursorTime` changes they reposition their SVG playhead line.
### 4.2 Channel playhead (`TimeSeriesVisualizer` / legacy `ChannelD3`)
```js
this.playhead = this.main.append('line')
.attr('stroke', parent.cursorcolor)
.attr('x1/x2', this.x(cursorTime))
```
`updatePlayhead(time)` hides the line if the time is outside current `x.domain()` or `null`.
**Note**: In modern MultiChannel components, this logic is handled by `TimeSeriesVisualizer` which provides unified cursor management across all channels.
### 4.3 Overview playhead
Identical logic but uses scaled brush coordinate.
### 4.4 Click without recentering
* If click time is **inside** `brushRange` we call `setCursor(time)` – only cursor moves.
* If outside – `_updateViewForTime` recentres view and may emit sync.
### 4.5 Click during playback synchronization
* **Problem solved**: Previously, clicking during video playback would seek the video but TimeSeries cursor would continue from its old position.
* **Solution**: When `isPlaying` is true and user clicks, the `restartPlaybackFromTime(time)` action:
* Cancels current animation frame
* Converts clicked time to relative seconds for playback state
* Updates `playStartPosition` and `playStartTime` to the clicked position
* Restarts the playback loop from the new position
* **Result**: Both video and TimeSeries cursor jump to clicked position and continue playing synchronously.
### 4.6 Shared click handling logic
* `handleTimeSeriesMainAreaClick()` in `helpers.js` provides unified click handling for both:
* TimeSeries main component (`TimeSeries.jsx`)
* TimeSeriesVisualizer component (`TimeSeriesVisualizer.jsx` for MultiChannel)
* Eliminates code duplication (~80 lines reduced to single function call)
* Handles coordinate calculation, boundary checks, cursor updates, and playback restart logic
* Uses proper MobX-state-tree actions to avoid protection errors
### 4.7 Overview dragging behavior
* When user starts dragging the overview brush (`brushstarted`), `suppressSync` is set to `true`.
* This prevents `emitSeekSync()` from firing during the drag, keeping cursor fixed.
* On `brushended`, `suppressSync` is reset to `false` (with 0ms delay to let range settle).
* Result: dragging overview changes viewport without moving playhead or syncing with video/audio.
---
## 5. Important actions
| Action | Purpose |
|------------------------|---------|
| `updateTR(range)` | Central method to change visible window; triggers rerender + optional sync |
| `scrollToRegion(r)` | Ensure a labelled region is visible (may expand brush) |
| `setCursorAndSeek(t)` | Update both `cursorTime` & `seekTo` (internal only) |
| `setCursor(t)` | Update only `cursorTime` (no brush movement) |
| `restartPlaybackFromTime(t)` | Restart playback loop from specific time (handles click during playback) |
| `_updateViewForTime(t)`| Convert time → pixels & adjust brush + cursor |
| `setSuppressSync(flag)`| Temporarily disable sync emissions |
---
## 6. Helper functions
The `helpers.js` file contains shared utility functions used across TimeSeries components:
### 6.1 Click handling
* **`handleTimeSeriesMainAreaClick(event, timeSeriesItem, mainDisplayElement)`** – Unified click handling logic used by both:
* TimeSeries main component
* TimeSeriesVisualizer (for MultiChannel)
* Handles coordinate calculation, boundary checks, view updates, and playback synchronization
* Automatically calls appropriate actions (`setCursor`, `_updateViewForTime`, `restartPlaybackFromTime`)
* Ensures MobX-state-tree compliance by using proper actions instead of direct property modification
### 6.2 Other utilities
* **`sparseValues()`** – Data thinning for performance with large datasets
* **`getRegionColor()`** – Color calculation for labeled regions
* **`formatTrackerTime()`** – Time formatting for tracker display
* **`checkD3EventLoop()`** – D3 event loop prevention
---
## 7. Adding new functionality
* **New attributes** – extend `TagAttrs` with MST `types.optional`, then read `item.` in views.
* **New MultiChannel features** – modify `MultiChannelModel` actions or extend `ChannelLegend` component.
* **Color customization** – extend `palette.js` or add channel-specific color attributes.
* **Styling** – prefer Tailwind utility classes or inline SVG attributes.
* **Performance** – huge datasets are thinned with `sparseValues()`; thresholds controlled by `zoomStep`.
* **Visualization** – extend `TimeSeriesVisualizer` for custom D3 rendering behaviors.
---
## 8. Glossary
| Term | Meaning |
|---------------|---------|
| **Native units** | Raw numeric time values used in dataset (ms for dates, seconds/indices otherwise) |
| **Relative seconds** | Seconds offset from dataset start – format used in sync messages |
| **Brush** | D3 brush in Overview controlling visible window (`brushRange`) |
| **MultiChannel** | Component that groups multiple data channels in a single visualization |
| **Channel Legend** | Interactive legend component for controlling channel visibility and highlighting |
| **TimeSeriesVisualizer** | Unified D3-based rendering component that replaces legacy `ChannelD3` |
| **ChannelD3** | Legacy D3 rendering component (being replaced by TimeSeriesVisualizer) |
| **Color Palette** | Predefined set of colors automatically assigned to channels |
| **Playback Sync** | Real-time synchronization between video/audio playback and TimeSeries cursor position |
| **Click Handling** | Unified logic for processing user clicks on TimeSeries visualizations |