Skip to content

feat: BROS-44: Add MultiChannel tag support for TimeSeries #7669

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 38 commits into from
Jun 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
2e54686
feat: BROS-44: Add MultiChannel tag support for TimeSeries
Gondragos Jun 2, 2025
4a05ea5
test: Add small smoke test
Gondragos Jun 2, 2025
7491567
Linting
Gondragos Jun 2, 2025
98ad467
feat: Implement dynamic color assignment for channels
Gondragos Jun 2, 2025
b793e95
Merge branch 'develop' into fb-BROS-44/multichannel
niklub Jun 3, 2025
487274f
Merge branch 'develop' into 'fb-BROS-44/multichannel'
Gondragos Jun 5, 2025
8eb5596
fix: Fix TS visualizer for single channel
Gondragos Jun 6, 2025
2c04735
fix: Correct TimeSeries signal generation logic
Gondragos Jun 6, 2025
e8f53e2
fix: Prevent ChannelLegend from triggering seek action
Gondragos Jun 6, 2025
4304fd0
fix: Ensure default valueType is "url" in TimeSeries
Gondragos Jun 6, 2025
19b08b8
fix: Respect `fixedscale` property in MultiChannel
Gondragos Jun 6, 2025
dae8818
fix: Replace equality check with `is None`
Gondragos Jun 6, 2025
83f06c3
docs: Add documentation for MultiChannel tag
Gondragos Jun 6, 2025
165e47e
docs: Add params
Gondragos Jun 6, 2025
233132b
Merge branch 'develop' of ssh://github.com/heartexlabs/label-studio i…
Gondragos Jun 6, 2025
7d84c3e
fix: Add playhead rendering to TimeSeriesVisualizer component
Gondragos Jun 6, 2025
7dd53c6
docs: Update documentation for MultiChannel tag
Gondragos Jun 6, 2025
a739bcf
ci: Build tag docs
robot-ci-heartex Jun 6, 2025
f949d10
docs: Consolidate MultiChannel documentation into TimeSeries
Gondragos Jun 6, 2025
75612f1
Merge remote-tracking branch 'origin/fb-BROS-44/multichannel' into fb…
Gondragos Jun 6, 2025
9827554
docs: Fix docs
Gondragos Jun 6, 2025
450d1ea
ci: Build tag docs
robot-ci-heartex Jun 6, 2025
8b7d2ae
fix: Adjust default valueType behavior in secure mode
Gondragos Jun 6, 2025
7bab1f9
Merge remote-tracking branch 'origin/fb-BROS-44/multichannel' into fb…
Gondragos Jun 6, 2025
82998a2
docs: Expand TimeSeries documentation for MultiChannel
Gondragos Jun 6, 2025
86f858f
Fix margin that caused shift in cursor position while manual navigation
makseq Jun 7, 2025
5f1c1ac
Fix for video and timeseries navigation while playback
makseq Jun 7, 2025
c784b07
Linter
makseq Jun 8, 2025
7cea25d
Revert unnecessary changes
Gondragos Jun 8, 2025
895eec2
Revert "Fix margin that caused shift in cursor position while manual …
Gondragos Jun 8, 2025
11155b1
feat: Add plot click handling to TimeSeries
Gondragos Jun 9, 2025
7856287
Merge branch 'develop' into 'fb-BROS-44/multichannel'
Gondragos Jun 9, 2025
6b759ec
Add delta to generate_time_series_json and set second as default delta
makseq Jun 9, 2025
a692780
Apply suggestions from code review
Gondragos Jun 9, 2025
2c27446
Apply pre-commit linters
Gondragos Jun 9, 2025
99317d3
Add new generation for timeseries
makseq Jun 9, 2025
ddc2962
Merge branch 'fb-BROS-44/multichannel' of github.com:heartexlabs/labe…
makseq Jun 9, 2025
e6fc826
fix: Correct import path for FF_MULTICHANNEL_TS
Gondragos Jun 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/source/includes/tags/channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
| [legend] | <code>string</code> | | display name of the channel |
| [units] | <code>string</code> | | display units name |
| [displayFormat] | <code>string</code> | | format string for the values, uses d3-format:<br/> `[,][.precision][f\|%]`<br/> `,` - group thousands with separator (from locale): `,` (12345.6 -> 12,345.6) `,.2f` (12345.6 -> 12,345.60)<br/> `.precision` - precision for `f\|%` type, significant digits for empty type:<br/> `.3f` (12.3456 -> 12.345, 1000 -> 1000.000)<br/> `.3` (12.3456 -> 12.3, 1.2345 -> 1.23, 12345 -> 1.23e+4)<br/> `f` - treat as float, default precision is .6: `f` (12 -> 12.000000) `.2f` (12 -> 12.00) `.0f` (12.34 -> 12)<br/> `%` - treat as percents and format accordingly: `%.0` (0.128 -> 13%) `%.1` (1.2345 -> 123.4%) |
| [height] | <code>number</code> | | height of the plot |
| [height] | <code>number</code> | <code>200</code> | height of the plot |
| [strokeColor] | <code>string</code> | <code>&quot;#f48a42&quot;</code> | plot stroke color, expects hex value |
| [strokeWidth] | <code>number</code> | <code>1</code> | plot stroke width |
| [markerColor] | <code>string</code> | <code>&quot;#f48a42&quot;</code> | plot stroke color, expects hex value |
Expand Down
9 changes: 9 additions & 0 deletions docs/source/includes/tags/multichannel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
### Parameters

| Param | Type | Default | Description |
| --- | --- | --- | --- |
| [height] | <code>number</code> | <code>200</code> | height of the plot |
| [showAxis] | <code>boolean</code> | <code>true</code> | whether to show both axes |
| [showYAxis] | <code>boolean</code> | <code>true</code> | whether to show the y-axis |
| [fixedScale] | <code>boolean</code> | | whether to use a fixed scale for all channels. If not set, inherits from parent TimeSeries tag |

36 changes: 36 additions & 0 deletions docs/source/tags/timeseries.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,39 @@ Labeling configuration for time series data stored in the task field `ts` in Lab
</TimeSeries>
</View>
```

## MultiChannel

The `MultiChannel` tag is used to group multiple channels together in a time series visualization. Use this tag within a `TimeSeries` tag to organize and display multiple data channels in a single view.

{% insertmd includes/tags/multichannel.md %}

### Example

Labeling configuration for time series data with multiple channels grouped together:

```html
<View>
<TimeSeries name="ts" value="$timeseries" valuetype="json"
timeColumn="time"
timeFormat="%Y-%m-%d %H:%M:%S.%f"
timeDisplayFormat="%Y-%m-%d"
overviewChannels="velocity">
<MultiChannel>
<Channel column="velocity"
units="miles/h"
displayFormat=",.1f"
legend="Velocity"/>

<Channel column="acceleration"
units="miles/h^2"
displayFormat=",.1f"
legend="Acceleration"/>
</MultiChannel>
</TimeSeries>
<TimeSeriesLabels name="label" toName="ts">
<Label value="Run" background="red"/>
<Label value="Walk" background="green"/>
</TimeSeriesLabels>
</View>
```
6 changes: 3 additions & 3 deletions docs/source/templates/timeseries_audio_video.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ To specify a time-based time series, use the following format:

<!-- {
"video": "https://app.heartex.ai/static/samples/opossum_snow.mp4",
"accel_data": "https://app.heartex.ai/samples/time-series.csv?time=time&values=accel_x%2Caccel_y&sep=%2C&tf=%H:%m:%d.%f",
"gyro_data": "https://app.heartex.ai/samples/time-series.csv?time=time&values=gyro_x%2Cgyro_y&sep=%2C&tf=%H:%m:%d.%f"
"accel_data": "https://app.heartex.ai/samples/time-series.csv?time=time&values=accel_x%2Caccel_y&sep=%2C&tf=%H:%M:%S.%f",
"gyro_data": "https://app.heartex.ai/samples/time-series.csv?time=time&values=gyro_x%2Cgyro_y&sep=%2C&tf=%H:%M:%S.%f"
}
-->
```
Expand Down Expand Up @@ -180,7 +180,7 @@ To specify an index-based time series, use the following format:

#### Labeling configuration

<br><br>
<br>

{% details <b>Index-based TimeSeries (no timestamps at X axis)</b> %}

Expand Down
60 changes: 53 additions & 7 deletions label_studio/core/label_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,14 @@

# detect secured mode - objects served as URLs
value_type = p.get('valueType') or p.get('valuetype')
only_urls = secure_mode or value_type == 'url'

only_urls = value_type == 'url'
if secure_mode and p.tag in ['Paragraphs', 'HyperText', 'Text']:

Check warning on line 278 in label_studio/core/label_config.py

View check run for this annotation

Codecov / codecov/patch

label_studio/core/label_config.py#L277-L278

Added lines #L277 - L278 were not covered by tests
# In secure mode default valueType for Paragraphs and RichText is "url"
only_urls = only_urls or value_type is None
if p.tag == 'TimeSeries':

Check warning on line 281 in label_studio/core/label_config.py

View check run for this annotation

Codecov / codecov/patch

label_studio/core/label_config.py#L280-L281

Added lines #L280 - L281 were not covered by tests
# for TimeSeries default valueType is "url"
only_urls = only_urls or value_type is None

Check warning on line 283 in label_studio/core/label_config.py

View check run for this annotation

Codecov / codecov/patch

label_studio/core/label_config.py#L283

Added line #L283 was not covered by tests

example_from_field_name = examples.get('$' + value)
if example_from_field_name:
Expand All @@ -300,10 +307,10 @@
# TimeSeries special case - generate signals on-the-fly
time_column = p.get('timeColumn')
value_columns = []
for ts_child in p:
if ts_child.tag != 'Channel':
continue
value_columns.append(ts_child.get('column'))
if hasattr(p, 'findall'):
channels = p.findall('.//Channel[@column]')
for ts_child in channels:
value_columns.append(ts_child.get('column'))

Check warning on line 313 in label_studio/core/label_config.py

View check run for this annotation

Codecov / codecov/patch

label_studio/core/label_config.py#L310-L313

Added lines #L310 - L313 were not covered by tests
sep = p.get('sep')
time_format = p.get('timeFormat')

Expand Down Expand Up @@ -362,6 +369,29 @@
return '%' in s


def _get_smallest_time_freq(time_format):
"""Determine the smallest time component in strftime format and return pandas frequency"""
if not time_format:
return 'D' # default to daily

Check warning on line 375 in label_studio/core/label_config.py

View check run for this annotation

Codecov / codecov/patch

label_studio/core/label_config.py#L374-L375

Added lines #L374 - L375 were not covered by tests

# Order from smallest to largest (we want the smallest one)
time_components = [

Check warning on line 378 in label_studio/core/label_config.py

View check run for this annotation

Codecov / codecov/patch

label_studio/core/label_config.py#L378

Added line #L378 was not covered by tests
('%S', 's'), # second
('%M', 'min'), # minute
('%H', 'h'), # hour
('%d', 'D'), # day
('%m', 'MS'), # month start
('%Y', 'YS'), # year start
]

# Find the smallest time component present in the format
for strftime_code, pandas_freq in time_components:
if strftime_code in time_format:
return pandas_freq

Check warning on line 390 in label_studio/core/label_config.py

View check run for this annotation

Codecov / codecov/patch

label_studio/core/label_config.py#L388-L390

Added lines #L388 - L390 were not covered by tests

return 'D' # default to daily if no time components found

Check warning on line 392 in label_studio/core/label_config.py

View check run for this annotation

Codecov / codecov/patch

label_studio/core/label_config.py#L392

Added line #L392 was not covered by tests


def generate_time_series_json(time_column, value_columns, time_format=None):
"""Generate sample for time series"""
n = 100
Expand All @@ -372,10 +402,26 @@
if time_format is None:
times = np.arange(n).tolist()
else:
times = pd.date_range('2020-01-01', periods=n, freq='D').strftime(time_format).tolist()
# Automatically determine the appropriate frequency based on time format
freq = _get_smallest_time_freq(time_format)
times = pd.date_range('2020-01-01', periods=n, freq=freq)
new_times = []
prev_time_str = None
for time in times:
time_str = time.strftime(time_format)

Check warning on line 411 in label_studio/core/label_config.py

View check run for this annotation

Codecov / codecov/patch

label_studio/core/label_config.py#L406-L411

Added lines #L406 - L411 were not covered by tests

# Check if formatted string is monotonic (to handle cycling due to format truncation)
if prev_time_str is not None and time_str <= prev_time_str:
break

Check warning on line 415 in label_studio/core/label_config.py

View check run for this annotation

Codecov / codecov/patch

label_studio/core/label_config.py#L414-L415

Added lines #L414 - L415 were not covered by tests

new_times.append(time_str)
prev_time_str = time_str # Update prev_time_str for next iteration

Check warning on line 418 in label_studio/core/label_config.py

View check run for this annotation

Codecov / codecov/patch

label_studio/core/label_config.py#L417-L418

Added lines #L417 - L418 were not covered by tests

times = new_times

Check warning on line 420 in label_studio/core/label_config.py

View check run for this annotation

Codecov / codecov/patch

label_studio/core/label_config.py#L420

Added line #L420 was not covered by tests

ts = {time_column: times}
for value_col in value_columns:
ts[value_col] = np.random.randn(n).tolist()
ts[value_col] = np.random.randn(len(times)).tolist()
return ts


Expand Down
5 changes: 5 additions & 0 deletions web/libs/core/src/lib/utils/feature-flags/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,8 @@ export const FF_THEME_TOGGLE = "fflag_feat_front_optic_1217_theme_toggle_short";
* Enables the summary view for annotations
*/
export const FF_SUMMARY = "fflag_feat_front_leap_2036_annotations_summary";

/**
* TimeSeries Multi-channel functionality
*/
export const FF_MULTICHANNEL_TS = "fflag_feat_front_bros58_timeseries_multichannel_short";
Loading
Loading