Skip to content

Commit 9d3ca29

Browse files
committed
adaptative channel metadata
factorize get channel values dynamic visible channel type range
1 parent 2a31fc8 commit 9d3ca29

12 files changed

Lines changed: 412 additions & 78 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* A function to compare to objects of the same type.
3+
*/
4+
export type Comparator<T> = (a: T, b: T) => boolean;
5+
6+
/**
7+
* A dependency-aware key-value mutable cache.
8+
*
9+
* Only a single value is cached per key, when querying the cache, that value
10+
* is recomputed it the dependencies of that entry have changed.
11+
*/
12+
export default class MutableKeyDepCache<K, D, V> {
13+
private cache = new Map<K, {deps: D; value: V}>();
14+
private comparator: Comparator<D>;
15+
16+
/**
17+
* Create a new mutable key dependency cache with the given dependency
18+
* comparator.
19+
*/
20+
constructor(comparator: Comparator<D> = Object.is) {
21+
this.comparator = comparator;
22+
}
23+
24+
/**
25+
* Query the cache for a value with the given key and dependencies.
26+
*/
27+
get(key: K, deps: D, compute: () => V): V {
28+
const entry = this.cache.get(key);
29+
30+
// If the key is present and its dependencies have not changed, return the
31+
// existing value.
32+
if (entry && this.comparator(entry.deps, deps)) {
33+
return entry.value;
34+
}
35+
36+
// Otherwise, recompute, cache, and return the new value.
37+
const value = compute();
38+
this.cache.set(key, {deps, value});
39+
return value;
40+
}
41+
}

modules/electrophysiology_browser/jsx/react-series-data-viewer/src/eeglab/EEGLabSeriesProvider.tsx

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import React, {Component, createRef} from 'react';
1+
import React, {
2+
Component, createContext, createRef, useState, useEffect,
3+
} from 'react';
24
import {tsvParse} from 'd3-dsv';
35
import {applyMiddleware, createStore, Store} from 'redux';
46
import {Provider} from 'react-redux';
@@ -23,7 +25,7 @@ import {
2325
setCoordinateSystem, setElectrodes,
2426
} from '../series/store/state/montage';
2527
import {
26-
ChannelInfos, EventMetadata, HEDSchemaElement,
28+
ChannelInfo, ChannelInfos, ChannelMetadata, EventMetadata, HEDSchemaElement,
2729
} from '../series/store/types';
2830
import TriggerableModal from 'jsx/TriggerableModal';
2931
import DatasetTagger from '../series/components/DatasetTagger';
@@ -62,16 +64,66 @@ const MenuOption = {
6264
};
6365

6466
/**
65-
* EEGLabSeriesProvider component
67+
* The channel informaton context, which provides the BIDS information about\
68+
* the channels present in the acquisition, if available.
6669
*/
67-
class EEGLabSeriesProvider extends Component<CProps, any> {
70+
export const ChannelInfosContext = createContext<ChannelInfo[]>([]);
71+
72+
/**
73+
* The channel metadata context, which provides the metadata about the channels
74+
* present in the acquisition.
75+
*/
76+
export const ChannelMetasContext = createContext<ChannelMetadata[]>([]);
77+
78+
/**
79+
* Function wrapper around the older `EEGLabSeriesProviderClass` class
80+
* component.
81+
*/
82+
function EEGLabSeriesProvider(props: CProps) {
83+
const [channelInfos, setChannelInfos] = useState<ChannelInfo[]>([]);
84+
const [channelMetas, setChannelMetas] = useState<ChannelMetadata[]>([]);
85+
86+
// Fetch the channel BIDS information from the API.
87+
useEffect(() => {
88+
fetchJSON(props.channelsURL).then((json: ChannelInfos) => {
89+
setChannelInfos(json.Channels);
90+
});
91+
}, [props.channelsURL]);
92+
93+
return (
94+
<ChannelInfosContext.Provider value={channelInfos}>
95+
<ChannelMetasContext.Provider value={channelMetas}>
96+
<EEGLabSeriesProviderClass
97+
{...props}
98+
setChannelMetas={setChannelMetas}
99+
/>
100+
</ChannelMetasContext.Provider>
101+
</ChannelInfosContext.Provider>
102+
);
103+
}
104+
105+
/**
106+
* Props for the `EEGLabSeriesProviderClass` component, which extend the props
107+
* of the functional component.
108+
*/
109+
type CClassProps = CProps & {
110+
/**
111+
* Setter for the channel metadata context lifted to the functional component.
112+
*/
113+
setChannelMetas: (_: ChannelMetadata[]) => void,
114+
};
115+
116+
/**
117+
* EEGLabSeriesProviderClass component
118+
*/
119+
class EEGLabSeriesProviderClass extends Component<CClassProps, any> {
68120
private store: Store;
69121

70122
/**
71123
* @class
72124
* @param {object} props - React Component properties
73125
*/
74-
constructor(props: CProps) {
126+
constructor(props: CClassProps) {
75127
super(props);
76128
const epicMiddleware = createEpicMiddleware();
77129

@@ -102,6 +154,7 @@ class EEGLabSeriesProvider extends Component<CProps, any> {
102154
eegMontageName,
103155
recordingHasHED,
104156
t,
157+
setChannelMetas,
105158
} = props;
106159

107160
if (!window.EEGLabSeriesProviderStore) {
@@ -181,18 +234,13 @@ class EEGLabSeriesProvider extends Component<CProps, any> {
181234
}
182235
};
183236

184-
fetchJSON(props.channelsURL).then((json: ChannelInfos) => {
185-
this.store.dispatch(setDatasetMetadata({
186-
bidsChannels: json.Channels,
187-
}));
188-
});
189-
190237
Promise.race(racers(fetchJSON, chunksURL, '/index.json')).then(
191238
({json, url}) => {
192239
if (json) {
193240
const {
194241
channelMetadata, shapes, timeInterval, seriesRange, validSamples,
195242
} = json;
243+
setChannelMetas(channelMetadata);
196244
this.store.dispatch(
197245
setDatasetMetadata({
198246
chunksURL: url,

modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/AnnotationForm.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, {useEffect, useState} from 'react';
1+
import React, {useContext, useEffect, useState} from 'react';
22
import {ChannelMetadata, Epoch as EpochType, HEDSchemaElement, HEDTag, RightPanel,} from '../store/types';
33
import {connect} from 'react-redux';
44
import {setTimeSelection} from '../store/state/timeSelection';
@@ -21,6 +21,7 @@ import swal from 'sweetalert2';
2121
import {InfoIcon} from "./components";
2222
import {colorOrder} from "../../color";
2323
import {useTranslation} from "react-i18next";
24+
import {ChannelMetasContext} from '../../eeglab/EEGLabSeriesProvider';
2425

2526

2627
type CProps = {
@@ -40,7 +41,6 @@ type CProps = {
4041
hedSchema: HEDSchemaElement[],
4142
datasetTags: any,
4243
channelDelimiter: string,
43-
channelMetadata: ChannelMetadata[],
4444
panelIsDirty: boolean,
4545
setPanelIsDirty: (_: boolean) => void,
4646
eventChannels: string[],
@@ -65,7 +65,6 @@ type CProps = {
6565
* @param root0.hedSchema
6666
* @param root0.datasetTags
6767
* @param root0.channelDelimiter
68-
* @param root0.channelMetadata
6968
* @param root0.panelIsDirty
7069
* @param root0.setPanelIsDirty
7170
* @param root0.eventChannels
@@ -87,13 +86,13 @@ const AnnotationForm = ({
8786
hedSchema,
8887
datasetTags,
8988
channelDelimiter,
90-
channelMetadata,
9189
panelIsDirty,
9290
setPanelIsDirty,
9391
eventChannels,
9492
setEventChannels,
9593
}: CProps) => {
9694
const {t} = useTranslation();
95+
const channelMetadata = useContext(ChannelMetasContext);
9796
const [eventInterval, setEventInterval] = useState<(number | string)[]>(
9897
timeSelection ?? ['', '']
9998
);
@@ -1636,7 +1635,6 @@ export default connect(
16361635
hedSchema: state.dataset.hedSchema,
16371636
datasetTags: state.dataset.datasetTags,
16381637
channelDelimiter: state.dataset.channelDelimiter,
1639-
channelMetadata: state.dataset.channelMetadata,
16401638
channels: state.channels,
16411639
}),
16421640
(dispatch: (any) => void) => ({

modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/Epoch.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import React from 'react';
1+
import React, {useContext} from 'react';
22
import {vec2} from 'gl-matrix';
33
import {MIN_EPOCH_WIDTH} from '../../vector';
44
import {ScaleLinear} from 'd3-scale';
55
import {connect} from "react-redux";
66
import {RootState} from "../store";
7-
import {Channel, ChannelMetadata} from "../store/types";
7+
import {Channel} from "../store/types";
8+
import {ChannelMetasContext} from '../../eeglab/EEGLabSeriesProvider';
89

910
type CProps = {
1011
key: string,
@@ -20,7 +21,6 @@ type CProps = {
2021
minWidth: number,
2122
epochChannels?: string[],
2223
channels: Channel[],
23-
channelMetadata: ChannelMetadata[],
2424
};
2525

2626
/**
@@ -35,7 +35,6 @@ type CProps = {
3535
* @param root0.minWidth
3636
* @param root0.epochChannels
3737
* @param root0.channels
38-
* @param root0.channelMetadata
3938
*/
4039
const Epoch = (
4140
{
@@ -49,8 +48,9 @@ const Epoch = (
4948
minWidth,
5049
epochChannels,
5150
channels,
52-
channelMetadata,
5351
}: CProps) => {
52+
const channelMetadata = useContext(ChannelMetasContext);
53+
5454
onset = isNaN(onset) ? 0 : onset;
5555
duration = isNaN(duration) ? 0 : duration;
5656

@@ -121,5 +121,4 @@ Epoch.defaultProps = {
121121
export default connect(
122122
(state: RootState)=> ({
123123
channels: state.channels,
124-
channelMetadata: state.dataset.channelMetadata,
125124
}))(Epoch);

modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/EventManager.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, {useState, useEffect} from 'react';
1+
import React, {useState, useEffect, useContext} from 'react';
22
import {setCurrentAnnotation} from '../store/state/currentAnnotation';
33
import {MAX_RENDERED_EPOCHS} from '../../vector';
44
import {
@@ -15,7 +15,6 @@ import {
1515
HEDTag,
1616
HEDSchemaElement,
1717
RightPanel,
18-
ChannelMetadata,
1918
Channel
2019
} from '../store/types';
2120
import {connect} from 'react-redux';
@@ -26,6 +25,7 @@ import {RootState} from '../store';
2625
import {setFilteredEpochs} from '../store/state/dataset';
2726
import {CheckboxElement} from './Form';
2827
import {useTranslation, Trans} from "react-i18next";
28+
import {ChannelMetasContext} from '../../eeglab/EEGLabSeriesProvider';
2929

3030
type CProps = {
3131
timeSelection?: [number, number],
@@ -46,7 +46,6 @@ type CProps = {
4646
datasetTags: any,
4747
channelDelimiter: string,
4848
channels: Channel[],
49-
channelMetadata: ChannelMetadata[],
5049
canEdit: boolean,
5150
tagsHaveChanges: boolean,
5251
};
@@ -70,7 +69,6 @@ type CProps = {
7069
* @param root0.hedSchema
7170
* @param root0.channelDelimiter
7271
* @param root0.channels
73-
* @param root0.channelMetadata
7472
* @param root0.datasetTags
7573
* @param root0.tagsHaveChanges
7674
*/
@@ -92,11 +90,11 @@ const EventManager = ({
9290
datasetTags,
9391
channelDelimiter,
9492
channels,
95-
channelMetadata,
9693
canEdit,
9794
tagsHaveChanges,
9895
}: CProps) => {
9996
const {t} = useTranslation();
97+
const channelMetadata = useContext(ChannelMetasContext);
10098
const [epochsInRange, setEpochsInRange] = useState(getEpochsInRange(epochs, interval));
10199
const [allEpochsVisible, setAllEpochsVisibility] = useState(() => {
102100
if (epochsInRange.length < MAX_RENDERED_EPOCHS) {
@@ -769,7 +767,6 @@ export default connect(
769767
datasetTags: state.dataset.datasetTags,
770768
channelDelimiter: state.dataset.channelDelimiter,
771769
channels: state.channels, // TODO: merge with below and pass?
772-
channelMetadata: state.dataset.channelMetadata,
773770
tagsHaveChanges: state.dataset.tagsHaveChanges,
774771
}),
775772
(dispatch: (_: any) => void) => ({

modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/SeriesCursor.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import {bisector} from 'd3-array';
33
import {colorOrder} from '../../color';
44
import {Channel, ChannelMetadata, Epoch} from '../store/types';
55
import {connect} from 'react-redux';
6-
import {MAX_RENDERED_EPOCHS, SIGNAL_SCALE, SIGNAL_UNIT} from '../../vector';
6+
import {MAX_RENDERED_EPOCHS} from '../../vector';
77
import {MutableRefObject, useEffect} from 'react';
88
import {RootState} from '../store';
99
import {getEpochsInRange} from '../store/logic/filterEpochs';
1010
import {useTranslation} from "react-i18next";
11+
import {getChannelUnit, useChannelInfo} from '../store/logic/channels';
12+
import {normalizeUnit, normalizeValueUnit} from '../../utils';
1113

1214
type CursorContentProps = {
1315
time: number,
@@ -158,8 +160,11 @@ const SeriesCursor = (
158160
chunk.interval[1] >= time
159161
);
160162
if (!hoveredChunk) return;
163+
const rawUnit = getChannelUnit(useChannelInfo(hoveredChannel));
161164
const chunkValue = computeValue(hoveredChunk, time);
162165
const channelColor = colorOrder(channelIndex.toString()).toString();
166+
const unit = normalizeUnit(rawUnit);
167+
const value = normalizeValueUnit(chunkValue, unit);
163168
return (
164169
<div
165170
key={channelIndex.toString()}
@@ -168,7 +173,7 @@ const SeriesCursor = (
168173
width: '100px',
169174
}}
170175
>
171-
{channelName}: {Math.round(chunkValue)} {SIGNAL_UNIT}
176+
{channelName}: {value} {unit}
172177
</div>
173178
);
174179
})}
@@ -267,7 +272,7 @@ const computeValue = (chunk, time) => {
267272
const idx = bisectTime(indices, time);
268273
const value = chunk.values[idx-1];
269274

270-
return value * SIGNAL_SCALE;
275+
return value;
271276
};
272277

273278
/**
@@ -290,6 +295,8 @@ const CursorContent = (
290295
channelMetadata,
291296
}: CursorContentProps
292297
) => {
298+
const rawUnit = getChannelUnit(useChannelInfo(channel));
299+
const unit = normalizeUnit(rawUnit);
293300
return (
294301
<div style={{margin: '0 5px', width: '120px'}}>
295302
{channel.traces.map((trace, i) => {
@@ -313,7 +320,7 @@ const CursorContent = (
313320
}}
314321
>
315322
{channelMetadata[channel.index].name}:&nbsp;
316-
{chunk && Math.round(computeValue(chunk, time))} {SIGNAL_UNIT}
323+
{chunk && normalizeValueUnit(computeValue(chunk, time), unit)} {unit}
317324
</div>
318325
);
319326
})}

0 commit comments

Comments
 (0)