diff --git a/modules/electrophysiology_browser/jsx/electrophysiologySessionView.js b/modules/electrophysiology_browser/jsx/electrophysiologySessionView.js index 00c05c43ec3..024c351c3d1 100644 --- a/modules/electrophysiology_browser/jsx/electrophysiologySessionView.js +++ b/modules/electrophysiology_browser/jsx/electrophysiologySessionView.js @@ -358,6 +358,10 @@ class ElectrophysiologySessionView extends Component { eegMontage, } = this.state.database[i]; const file = this.state.database[i].file; + const channelsURL = `${loris.BaseURL}/api/v0.0.4-dev/candidates` + + `/${this.state.patient.info.pscid}` + + `/${this.state.patient.info.visit_label}/recordings/${file.name}` + + `/channels`; const splitPagination = []; for (const j of Array(file.splitData?.splitCount).keys()) { splitPagination.push( @@ -403,6 +407,7 @@ class ElectrophysiologySessionView extends Component { {EEG_VIS_ENABLED &&
{ } }; + fetchJSON(props.channelsURL).then((json: ChannelInfos) => { + this.store.dispatch(setDatasetMetadata({ + bidsChannels: json.Channels, + })); + }); + Promise.race(racers(fetchJSON, chunksURL, '/index.json')).then( ({json, url}) => { if (json) { diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/ChannelTypesSelector.tsx b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/ChannelTypesSelector.tsx new file mode 100644 index 00000000000..ecb41b4153a --- /dev/null +++ b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/ChannelTypesSelector.tsx @@ -0,0 +1,48 @@ +import {ChannelTypeState} from "./SeriesRenderer"; + +/** + * Component that displays the list of channel types present in the acquisition and + * allows to configure which ones should be displayed or not. + */ +const ChannelTypesSelector = ({channelTypes, setChannelTypes}: { + channelTypes: Record, + setChannelTypes: React.Dispatch>>, +}) => { + return ( +
+ +
    + {Object.entries(channelTypes).map(([name, {visible, channelsCount}]) => ( +
  • + {name} ({channelsCount}) + e.stopPropagation() + } + onChange={(e) => { + setChannelTypes((channelTypes) => ({ + ...channelTypes, + [name]: { + ...channelTypes[name], + visible: e.target.checked, + }, + })); + }} + /> +
  • + ))} +
+
+ ); +} + +export default ChannelTypesSelector; diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/Pagination.tsx b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/Pagination.tsx new file mode 100644 index 00000000000..8796a5f909f --- /dev/null +++ b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/Pagination.tsx @@ -0,0 +1,113 @@ +import React from 'react'; +import {useTranslation} from "react-i18next"; +import {CHANNEL_DISPLAY_OPTIONS} from "../../vector"; +import {RightPanel} from "../store/types"; + +/** + * Pagination component that provides controls for selecting how many channels + * should be displayed at once, and navigating through the paginated channels. + */ +function Pagination({ + limit, + selectedChannelsCount, + visibleChannelsCount, + offsetIndex, + setOffsetIndex, + displayedChannelsLimit, + setDisplayedChannelsLimit, + rightPanel, +}: { + limit: number, + selectedChannelsCount: number, + visibleChannelsCount: number, + offsetIndex: number, + setOffsetIndex: (_: number) => void, + displayedChannelsLimit: number, + setDisplayedChannelsLimit: (_: number) => void, + rightPanel: RightPanel, +}) { + const {t} = useTranslation(); + + const hardLimit = Math.min(offsetIndex + limit - 1, visibleChannelsCount); + + return ( +
+ + {t('Displaying: ', {ns: 'electrophysiology_browser'})} + +   + {t('Showing:', {ns: 'electrophysiology_browser'})} +   + { + const value = parseInt(e.target.value); + !isNaN(value) && setOffsetIndex(value); + }} + /> +   + {t('to {{channelsInView}} of {{totalChannels}}', { + ns: 'electrophysiology_browser', + channelsInView: hardLimit, + totalChannels: selectedChannelsCount + })} + +
+ setOffsetIndex(offsetIndex - limit)} + value='<<' + /> + setOffsetIndex(offsetIndex - 1)} + value='<' + /> + setOffsetIndex(offsetIndex + 1)} + value='>' + /> + setOffsetIndex(offsetIndex + limit)} + value='>>' + /> +
+
+ ); +} + +export default Pagination; diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/SeriesRenderer.tsx b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/SeriesRenderer.tsx index 39b6ba5e916..62015007c11 100644 --- a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/SeriesRenderer.tsx +++ b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/components/SeriesRenderer.tsx @@ -5,6 +5,7 @@ import React, { useRef, FunctionComponent, MutableRefObject, + useMemo, } from 'react'; import * as R from 'ramda'; import {vec2} from 'gl-matrix'; @@ -16,7 +17,6 @@ import {colorOrder} from '../../color'; import { MAX_RENDERED_EPOCHS, DEFAULT_MAX_CHANNELS, - CHANNEL_DISPLAY_OPTIONS, SIGNAL_UNIT, Vector2, DEFAULT_TIME_INTERVAL, @@ -32,11 +32,12 @@ import SeriesCursor from './SeriesCursor'; import LoadingBar from './LoadingBar'; import {setRightPanel} from '../store/state/rightPanel'; import {setDatasetMetadata} from '../store/state/dataset'; -import {setOffsetIndex} from '../store/logic/pagination'; +import {createChannelTypesDict, filterDisplayedChannels, filterSelectedChannels} from '../store/logic/channels'; import IntervalSelect from './IntervalSelect'; import EventManager from './EventManager'; import AnnotationForm from './AnnotationForm'; import {RootState} from '../store'; +import {createAction} from 'redux-actions'; import { setAmplitudesScale, @@ -64,6 +65,7 @@ import { Channel, Epoch as EpochType, RightPanel, EpochFilter, + ChannelInfo, } from '../store/types'; import {setCurrentAnnotation} from '../store/state/currentAnnotation'; import {setCursorInteraction} from '../store/logic/cursorInteraction'; @@ -72,6 +74,18 @@ import {getEpochsInRange, updateActiveEpoch} from '../store/logic/filterEpochs'; import HEDEndorsement from "./HEDEndorsement"; import {setTimeSelection} from "../store/state/timeSelection"; import {useTranslation} from "react-i18next"; +import ChannelTypesSelector from './ChannelTypesSelector'; +import Pagination from './Pagination'; +import {SET_CHANNELS} from '../store/state/channels'; +import {UPDATE_VIEWED_CHUNKS} from '../store/logic/fetchChunks'; + +/** + * The state of a channel type. + */ +export type ChannelTypeState = { + visible: boolean, + channelsCount: number, +} type CProps = { ref: MutableRefObject, @@ -91,8 +105,7 @@ type CProps = { epochs: EpochType[], filteredEpochs: EpochFilter, activeEpoch: number, - offsetIndex: number, - setOffsetIndex: (_: number) => void, + bidsChannels: ChannelInfo[], setAmplitudesScale: (_: number) => void, resetAmplitudesScale: (_: void) => void, setLowPassFilter: (_: string) => void, @@ -135,8 +148,7 @@ const SeriesRenderer: FunctionComponent = ({ epochs, filteredEpochs, activeEpoch, - offsetIndex, - setOffsetIndex, + bidsChannels: bidsChannels, setAmplitudesScale, resetAmplitudesScale, setLowPassFilter, @@ -160,6 +172,10 @@ const SeriesRenderer: FunctionComponent = ({ numDisplayedChannels, setNumDisplayedChannels, ] = useState(DEFAULT_MAX_CHANNELS); + + // The channel types are indexed by channel type name. + const [channelTypes, setChannelTypes] = useState>({}); + const [cursorEnabled, setCursorEnabled] = useState(false); const toggleCursor = () => setCursorEnabled((value) => !value); const [DCOffsetView, setDCOffsetView] = useState(true); @@ -174,6 +190,7 @@ const SeriesRenderer: FunctionComponent = ({ const [lowPass, setLowPass] = useState('none'); const [refNode, setRefNode] = useState(null); const [bounds, setBounds] = useState(null); + const [offsetIndex, setOffsetIndex] = useState(1); const getBounds = useCallback((domNode) => { if (domNode) { setRefNode(domNode); @@ -185,6 +202,11 @@ const SeriesRenderer: FunctionComponent = ({ const [eventChannels, setEventChannels] = useState([]); const {t} = useTranslation(); + // Initialize the channel types mapping once channels information is loaded. + useEffect(() => { + setChannelTypes(createChannelTypesDict(channelMetadata, bidsChannels)); + }, [channelMetadata, bidsChannels]); + window.onbeforeunload = function() { if (panelIsDirty) { return t( @@ -526,7 +548,36 @@ const SeriesRenderer: FunctionComponent = ({ .range([topLeft[1], bottomRight[1]]), ]; + // Selected channels are all the channels that should be currently available in the viewer, + // including those not currently displayed because of pagination. + const selectedChannels = useMemo(() => ( + filterSelectedChannels(channelMetadata, bidsChannels, channelTypes) + ), [bidsChannels, channelMetadata, channelTypes]); + + // Indexes of the selected channel indexes for comparison with previous renders. + const selectedChannelIndexes = JSON.stringify(selectedChannels.map((channel) => channel.index)); + + // Displayed channels are all the selected channels that are currently displayed on screen. + channels = useMemo(() => ( + filterDisplayedChannels(selectedChannels, offsetIndex, limit, channels) + ), [bidsChannels, selectedChannelIndexes, offsetIndex, limit, channels]); + + // Indexes of the displayed channel indexes for comparison with previous renders. + const displayedChannelIndexes = JSON.stringify(channels.map((channel) => channel.index)); + + // Hack to update the global store whenever displayed channels are updated. + useEffect(() => { + const store = window.EEGLabSeriesProviderStore[chunksURL]; + if (store === undefined) { + return; + } + + store.dispatch(createAction(SET_CHANNELS)(channels)); + store.dispatch(createAction(UPDATE_VIEWED_CHUNKS)()); + }, [displayedChannelIndexes]); + const filteredChannels = channels.filter((_, i) => !hidden.includes(i)); + const showAxisScaleLines = false; // Visibility state of y-axis scale lines /** @@ -819,8 +870,6 @@ const SeriesRenderer: FunctionComponent = ({ ); }; - const hardLimit = Math.min(offsetIndex + limit - 1, channelMetadata.length); - /** * */ @@ -845,10 +894,9 @@ const SeriesRenderer: FunctionComponent = ({ }; /** - * + * Handle a change to the limit of the number of displayed channels. */ - const handleChannelChange = (e) => { - const numChannels = parseInt(e.target.value, 10); + const handleChannelChange = (numChannels: number) => { setNumDisplayedChannels(numChannels); // This one is the frontend controller setDatasetMetadata({limit: numChannels}); // Will trigger re-render to the store setOffsetIndex(offsetIndex); // Will include new channels on limit increase @@ -959,6 +1007,10 @@ const SeriesRenderer: FunctionComponent = ({ > {t('Add Event', {ns: 'electrophysiology_browser'})} + { rightPanel === null && (
- -
- - {t('Displaying: ', {ns: 'electrophysiology_browser'})} - -   - {t('Showing:', {ns: 'electrophysiology_browser'})} -   - { - const value = parseInt(e.target.value); - !isNaN(value) && setOffsetIndex(value); - }} - /> -   - {t('to {{channelsInView}} of {{totalChannels}}', { - ns: 'electrophysiology_browser', - channelsInView: hardLimit, - totalChannels: channelMetadata.length - })} - -
- setOffsetIndex(offsetIndex - limit)} - value='<<' - /> - setOffsetIndex(offsetIndex - 1)} - value='<' - /> - setOffsetIndex(offsetIndex + 1)} - value='>' - /> - setOffsetIndex(offsetIndex + limit)} - value='>>' - /> -
-
+ @@ -1622,7 +1607,6 @@ SeriesRenderer.defaultProps = { epochs: [], hidden: [], channelMetadata: [], - offsetIndex: 1, limit: DEFAULT_MAX_CHANNELS, }; @@ -1636,12 +1620,12 @@ export default connect( timexSelection: state.timeSelection, chunksURL: state.dataset.chunksURL, channels: state.channels, + bidsChannels: state.dataset.bidsChannels, epochs: state.dataset.epochs, filteredEpochs: state.dataset.filteredEpochs, activeEpoch: state.dataset.activeEpoch, hidden: state.montage.hidden, channelMetadata: state.dataset.channelMetadata, - offsetIndex: state.dataset.offsetIndex, limit: state.dataset.limit, loadedChannels: state.dataset.loadedChannels, domain: state.bounds.domain, @@ -1649,10 +1633,6 @@ export default connect( hoveredChannels: state.cursor.hoveredChannels, }), (dispatch: (_: any) => void) => ({ - setOffsetIndex: R.compose( - dispatch, - setOffsetIndex - ), setInterval: R.compose( dispatch, setInterval diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/index.tsx b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/index.tsx index 1d424b56b18..c247636e591 100644 --- a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/index.tsx +++ b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/index.tsx @@ -12,7 +12,6 @@ import {channelsReducer} from './state/channels'; import {createDragBoundsEpic} from './logic/dragBounds'; import {createTimeSelectionEpic} from './logic/timeSelection'; import {createFetchChunksEpic} from './logic/fetchChunks'; -import {createPaginationEpic} from './logic/pagination'; import { createActiveEpochEpic, createFilterEpochsEpic, @@ -51,10 +50,6 @@ export const rootEpic = combineEpics( dataset, channels, })), - createPaginationEpic(({dataset, channels}) => { - const {limit, channelMetadata} = dataset; - return {limit, channelMetadata, channels}; - }), createScaleAmplitudesEpic(({bounds}) => { const {amplitudeScale} = bounds; return amplitudeScale; diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/logic/channels.tsx b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/logic/channels.tsx new file mode 100644 index 00000000000..5767c57d566 --- /dev/null +++ b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/logic/channels.tsx @@ -0,0 +1,109 @@ +import {ChannelTypeState} from '../../components/SeriesRenderer'; +import {Channel, ChannelInfo, ChannelMetadata} from '../types'; + +/** + * Create the channel types dictionary that maps each channel type found in the dataset + * to its state. + */ +export function createChannelTypesDict( + rawChannels: ChannelMetadata[], + bidsChannels: ChannelInfo[], +): Record { + const channelTypes: Record = {}; + + for (const rawChannel of rawChannels) { + const bidsChannel = findBidsChannel(rawChannel, bidsChannels); + const channelTypeName = bidsChannel?.ChannelType ?? 'Unknown'; + if (channelTypes[channelTypeName] === undefined) { + channelTypes[channelTypeName] = { + visible: true, + channelsCount: 0, + }; + } + + channelTypes[channelTypeName].channelsCount++; + } + + return channelTypes; +} +/** + * Filter the list of all channels to keep only those whose channel types are visible. + */ +export function filterSelectedChannels( + rawChannels: ChannelMetadata[], + bidsChannels: ChannelInfo[], + channelTypes: Record, +): ChannelMetadata[] { + // If no channel types are loaded, do not filter any channel out. + if (Object.keys(channelTypes).length === 0) { + return rawChannels; + } + + return rawChannels.filter((rawChannel) => { + const bidsChannel = findBidsChannel(rawChannel, bidsChannels); + + // If there is a mismatch between the BIDS channels and the raw channels, + // display the channel by default. + if (bidsChannel === undefined) { + return channelTypes['Unknown'].visible; + } + + const channelType = channelTypes[bidsChannel.ChannelType]; + if (channelType === undefined) { + return true; + } + + return channelTypes[bidsChannel.ChannelType].visible; + }); +} + +/** + * Find the BIDS channel corresponding to a raw channel among a list of BIDS channels. + */ +function findBidsChannel( + rawChannel: ChannelMetadata, + bidsChannels: ChannelInfo[] +): ChannelInfo | undefined { + return bidsChannels.find((bidsChannel) => + bidsChannel.ChannelName === rawChannel.name + ); +} + +/** + * Filter the list of selected channels to keep only those that should be displayed on the current + * page. + */ +export function filterDisplayedChannels( + selectedChannels: ChannelMetadata[], + offsetIndex: number, + pageLimit: number, + previousChannels: Channel[], +): Channel[] { + // Index of of the first displayed channel among the selected channels. + let selectedChannelIndex = offsetIndex - 1; + + const maxSelectedChannelIndex = Math.min( + offsetIndex + pageLimit - 1, + selectedChannels.length + ); + + const newChannels = []; + while (selectedChannelIndex < maxSelectedChannelIndex) { + // Get the channel index of the selected channel. + const channelIndex = selectedChannels[selectedChannelIndex].index; + + // Re-use previous channels if possible. + // TODO: need to handle multiple traces using shapes + const channel = previousChannels.find((pastChannel) => + pastChannel.index === channelIndex + ) ?? { + index: channelIndex, + traces: [{chunks: [], type: 'line'}], + }; + + newChannels.push(channel); + selectedChannelIndex++; + } + + return newChannels; +} diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/logic/fetchChunks.tsx b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/logic/fetchChunks.tsx index 116127ccf5a..c38bd0514cf 100644 --- a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/logic/fetchChunks.tsx +++ b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/logic/fetchChunks.tsx @@ -31,9 +31,11 @@ export const loadChunks = (chunksData: FetchedChunks[]) => { return (dispatch: (_: any) => void) => { const channels : Channel[] = []; - const filters: Filter[] - = window.EEGLabSeriesProviderStore[chunksData[0].chunksURL] - .getState().filters; + const filters: Filter[] = chunksData[0] !== undefined + ? window.EEGLabSeriesProviderStore[chunksData[0].chunksURL] + .getState().filters + : []; + for (let index = 0; index < chunksData.length; index++) { const {channelIndex, chunks} : { channelIndex: number, diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/logic/pagination.tsx b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/logic/pagination.tsx deleted file mode 100644 index 1659e91e4f4..00000000000 --- a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/logic/pagination.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import * as R from 'ramda'; -import {Observable} from 'rxjs'; -import * as Rx from 'rxjs/operators'; -import {ofType} from 'redux-observable'; -import {createAction} from 'redux-actions'; -import {Channel, ChannelMetadata} from '../types'; -import {setChannels} from '../state/channels'; -import {setDatasetMetadata} from '../state/dataset'; -import {updateViewedChunks} from './fetchChunks'; - -export const SET_OFFSET_INDEX = 'SET_OFFSET_INDEX'; -export const setOffsetIndex = createAction(SET_OFFSET_INDEX); - -export type Action = (_: (_: any) => void) => void; - -export type State = { - limit: number, - channelMetadata: ChannelMetadata[], - channels: Channel[] -}; - -/** - * createPaginationEpic - * - * @param {Function} fromState - A function to parse the current state - * @returns {Observable} - A stream of actions - */ -export const createPaginationEpic = (fromState: (_: any) => State) => ( - action$: Observable, - state$: Observable -): Observable => { - return action$.pipe( - ofType(SET_OFFSET_INDEX), - Rx.map(R.prop('payload')), - Rx.withLatestFrom(state$), - Rx.map<[number, State], any>(([payload, state]) => { - const {limit, channelMetadata, channels} = fromState(state); - - const offsetIndex = Math.min( - Math.max(payload, 1), - Math.max(channelMetadata.length - limit + 1, 1) - ); - - let channelIndex = offsetIndex - 1; - - const newChannels = []; - const hardLimit = Math.min( - offsetIndex + limit - 1, - channelMetadata.length - ); - while (channelIndex < hardLimit) { - // TODO: need to handle multiple traces using shapes - const channel = - channels.find( - R.pipe( - R.prop('index'), - R.equals(channelIndex) - ) - ) || { - index: channelIndex, - traces: [{chunks: [], type: 'line'}], - }; - - newChannels.push(channel); - channelIndex++; - } - - return (dispatch) => { - dispatch(setDatasetMetadata({offsetIndex})); - dispatch(setChannels(newChannels)); - dispatch(updateViewedChunks()); - }; - }) - ); -}; diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/state/dataset.tsx b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/state/dataset.tsx index 4b66e3d5ddd..050eacc6d69 100644 --- a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/state/dataset.tsx +++ b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/state/dataset.tsx @@ -1,6 +1,7 @@ import * as R from 'ramda'; import {createAction} from 'redux-actions'; import { + ChannelInfo, ChannelMetadata, Epoch, EpochFilter, @@ -52,6 +53,7 @@ export type Action = | { type: 'SET_DATASET_METADATA', payload: { + bidsChannels: ChannelInfo[], chunksURL: string, channelNames: string[], shapes: number[][], @@ -62,7 +64,6 @@ export type Action = loadedChannels: number, samplingFrequency: string, eegMontageName: string, - offsetIndex: number, channelDelimiter: string, tagsHaveChanges: boolean, recordingHasHED: boolean, @@ -70,9 +71,9 @@ export type Action = }; export type State = { + bidsChannels: ChannelInfo[], chunksURL: string, channelMetadata: ChannelMetadata[], - offsetIndex: number, channelDelimiter: string, limit: number, loadedChannels: number, @@ -104,6 +105,7 @@ export type State = { */ export const datasetReducer = ( state: State = { + bidsChannels: [], chunksURL: '', channelMetadata: [], epochs: [], @@ -114,7 +116,6 @@ export const datasetReducer = ( }, activeEpoch: null, physioFileID: null, - offsetIndex: 1, channelDelimiter: '', limit: DEFAULT_MAX_CHANNELS, loadedChannels: 0, diff --git a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/types.tsx b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/types.tsx index dd757ee8766..c453cccd481 100644 --- a/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/types.tsx +++ b/modules/electrophysiology_browser/jsx/react-series-data-viewer/src/series/store/types.tsx @@ -16,6 +16,7 @@ export type Trace = { }; export type ChannelMetadata = { + index: number, name: string, seriesRange: [number, number] }; @@ -114,3 +115,41 @@ export type HEDEndorsement = { EndorsementStatus: EndorsementStatus, EndorsementTime: string, } + +/** + * LORIS EEG API acquisition metadata. + */ +export type ChannelInfosMetadata = { + CandID: string; + Visit: string; + File: string; +} + +/** + * Channel information extracted from the BIDS `channels.tsv` file and obtained + * through the LORIS EEG acquisition channel API. + */ +export type ChannelInfo = { + ChannelName: string; + ChannelDescription: string; + ChannelType: string; + ChannelTypeDescription: string; + ChannelStatus: string; + StatusDescription: string; + SamplingFrequency: number; + LowCutoff: string; + HighCutoff: string; + ManualFlag: string; + Notch: string; + Reference: string; + Unit: string; + ChannelFilePath: string; +} + +/** + * LORIS EEG acquisition channels API data. + */ +export type ChannelInfos = { + Meta: ChannelInfosMetadata; + Channels: ChannelInfo[]; +}