Skip to content

Commit 75d219a

Browse files
Gondragosniklubrobot-ci-heartexmakseqbmartel
authored
feat: BROS-44: Add MultiChannel tag support for TimeSeries (#7669)
Co-authored-by: niklub <[email protected]> Co-authored-by: Gondragos <[email protected]> Co-authored-by: robot-ci-heartex <[email protected]> Co-authored-by: makseq <[email protected]> Co-authored-by: bmartel <[email protected]>
1 parent ca23499 commit 75d219a

File tree

16 files changed

+1740
-37
lines changed

16 files changed

+1740
-37
lines changed

docs/source/includes/tags/channel.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
| [legend] | <code>string</code> | | display name of the channel |
77
| [units] | <code>string</code> | | display units name |
88
| [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%) |
9-
| [height] | <code>number</code> | | height of the plot |
9+
| [height] | <code>number</code> | <code>200</code> | height of the plot |
1010
| [strokeColor] | <code>string</code> | <code>&quot;#f48a42&quot;</code> | plot stroke color, expects hex value |
1111
| [strokeWidth] | <code>number</code> | <code>1</code> | plot stroke width |
1212
| [markerColor] | <code>string</code> | <code>&quot;#f48a42&quot;</code> | plot stroke color, expects hex value |
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
### Parameters
2+
3+
| Param | Type | Default | Description |
4+
| --- | --- | --- | --- |
5+
| [height] | <code>number</code> | <code>200</code> | height of the plot |
6+
| [showAxis] | <code>boolean</code> | <code>true</code> | whether to show both axes |
7+
| [showYAxis] | <code>boolean</code> | <code>true</code> | whether to show the y-axis |
8+
| [fixedScale] | <code>boolean</code> | | whether to use a fixed scale for all channels. If not set, inherits from parent TimeSeries tag |
9+

docs/source/tags/timeseries.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,39 @@ Labeling configuration for time series data stored in the task field `ts` in Lab
4949
</TimeSeries>
5050
</View>
5151
```
52+
53+
## MultiChannel
54+
55+
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.
56+
57+
{% insertmd includes/tags/multichannel.md %}
58+
59+
### Example
60+
61+
Labeling configuration for time series data with multiple channels grouped together:
62+
63+
```html
64+
<View>
65+
<TimeSeries name="ts" value="$timeseries" valuetype="json"
66+
timeColumn="time"
67+
timeFormat="%Y-%m-%d %H:%M:%S.%f"
68+
timeDisplayFormat="%Y-%m-%d"
69+
overviewChannels="velocity">
70+
<MultiChannel>
71+
<Channel column="velocity"
72+
units="miles/h"
73+
displayFormat=",.1f"
74+
legend="Velocity"/>
75+
76+
<Channel column="acceleration"
77+
units="miles/h^2"
78+
displayFormat=",.1f"
79+
legend="Acceleration"/>
80+
</MultiChannel>
81+
</TimeSeries>
82+
<TimeSeriesLabels name="label" toName="ts">
83+
<Label value="Run" background="red"/>
84+
<Label value="Walk" background="green"/>
85+
</TimeSeriesLabels>
86+
</View>
87+
```

docs/source/templates/timeseries_audio_video.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ To specify a time-based time series, use the following format:
116116

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

181181
#### Labeling configuration
182182

183-
<br><br>
183+
<br>
184184

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

label_studio/core/label_config.py

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,14 @@ def generate_sample_task_without_check(label_config, mode='upload', secure_mode=
273273

274274
# detect secured mode - objects served as URLs
275275
value_type = p.get('valueType') or p.get('valuetype')
276-
only_urls = secure_mode or value_type == 'url'
276+
277+
only_urls = value_type == 'url'
278+
if secure_mode and p.tag in ['Paragraphs', 'HyperText', 'Text']:
279+
# In secure mode default valueType for Paragraphs and RichText is "url"
280+
only_urls = only_urls or value_type is None
281+
if p.tag == 'TimeSeries':
282+
# for TimeSeries default valueType is "url"
283+
only_urls = only_urls or value_type is None
277284

278285
example_from_field_name = examples.get('$' + value)
279286
if example_from_field_name:
@@ -300,10 +307,10 @@ def generate_sample_task_without_check(label_config, mode='upload', secure_mode=
300307
# TimeSeries special case - generate signals on-the-fly
301308
time_column = p.get('timeColumn')
302309
value_columns = []
303-
for ts_child in p:
304-
if ts_child.tag != 'Channel':
305-
continue
306-
value_columns.append(ts_child.get('column'))
310+
if hasattr(p, 'findall'):
311+
channels = p.findall('.//Channel[@column]')
312+
for ts_child in channels:
313+
value_columns.append(ts_child.get('column'))
307314
sep = p.get('sep')
308315
time_format = p.get('timeFormat')
309316

@@ -362,6 +369,29 @@ def _is_strftime_string(s):
362369
return '%' in s
363370

364371

372+
def _get_smallest_time_freq(time_format):
373+
"""Determine the smallest time component in strftime format and return pandas frequency"""
374+
if not time_format:
375+
return 'D' # default to daily
376+
377+
# Order from smallest to largest (we want the smallest one)
378+
time_components = [
379+
('%S', 's'), # second
380+
('%M', 'min'), # minute
381+
('%H', 'h'), # hour
382+
('%d', 'D'), # day
383+
('%m', 'MS'), # month start
384+
('%Y', 'YS'), # year start
385+
]
386+
387+
# Find the smallest time component present in the format
388+
for strftime_code, pandas_freq in time_components:
389+
if strftime_code in time_format:
390+
return pandas_freq
391+
392+
return 'D' # default to daily if no time components found
393+
394+
365395
def generate_time_series_json(time_column, value_columns, time_format=None):
366396
"""Generate sample for time series"""
367397
n = 100
@@ -372,10 +402,26 @@ def generate_time_series_json(time_column, value_columns, time_format=None):
372402
if time_format is None:
373403
times = np.arange(n).tolist()
374404
else:
375-
times = pd.date_range('2020-01-01', periods=n, freq='D').strftime(time_format).tolist()
405+
# Automatically determine the appropriate frequency based on time format
406+
freq = _get_smallest_time_freq(time_format)
407+
times = pd.date_range('2020-01-01', periods=n, freq=freq)
408+
new_times = []
409+
prev_time_str = None
410+
for time in times:
411+
time_str = time.strftime(time_format)
412+
413+
# Check if formatted string is monotonic (to handle cycling due to format truncation)
414+
if prev_time_str is not None and time_str <= prev_time_str:
415+
break
416+
417+
new_times.append(time_str)
418+
prev_time_str = time_str # Update prev_time_str for next iteration
419+
420+
times = new_times
421+
376422
ts = {time_column: times}
377423
for value_col in value_columns:
378-
ts[value_col] = np.random.randn(n).tolist()
424+
ts[value_col] = np.random.randn(len(times)).tolist()
379425
return ts
380426

381427

web/libs/core/src/lib/utils/feature-flags/flags.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,8 @@ export const FF_THEME_TOGGLE = "fflag_feat_front_optic_1217_theme_toggle_short";
8787
* Enables the summary view for annotations
8888
*/
8989
export const FF_SUMMARY = "fflag_feat_front_leap_2036_annotations_summary";
90+
91+
/**
92+
* TimeSeries Multi-channel functionality
93+
*/
94+
export const FF_MULTICHANNEL_TS = "fflag_feat_front_bros58_timeseries_multichannel_short";

0 commit comments

Comments
 (0)