Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -403,6 +407,7 @@ class ElectrophysiologySessionView extends Component {
{EEG_VIS_ENABLED &&
<div className="react-series-data-viewer-scoped col-xs-12">
<EEGLabSeriesProvider
channelsURL={channelsURL}
chunksURL={
chunksURLs?.[file.splitData?.splitIndex] || chunksURLs
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* A function to compare to objects of the same type.
*/
export type Comparator<T> = (a: T, b: T) => boolean;

/**
* A dependency-aware key-value mutable cache.
*
* Only a single value is cached per key, when querying the cache, that value
* is recomputed it the dependencies of that entry have changed.
*/
export default class MutableKeyDepCache<K, D, V> {
private cache = new Map<K, {deps: D; value: V}>();
private comparator: Comparator<D>;

/**
* Create a new mutable key dependency cache with the given dependency
* comparator.
*/
constructor(comparator: Comparator<D> = Object.is) {
this.comparator = comparator;
}

/**
* Query the cache for a value with the given key and dependencies.
*/
get(key: K, deps: D, compute: () => V): V {
const entry = this.cache.get(key);

// If the key is present and its dependencies have not changed, return the
// existing value.
if (entry && this.comparator(entry.deps, deps)) {
return entry.value;
}

// Otherwise, recompute, cache, and return the new value.
const value = compute();
this.cache.set(key, {deps, value});
return value;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React, {Component, createRef} from 'react';
import React, {
Component, createContext, createRef, useState, useEffect,
} from 'react';
import {tsvParse} from 'd3-dsv';
import {applyMiddleware, createStore, Store} from 'redux';
import {Provider} from 'react-redux';
Expand All @@ -22,7 +24,9 @@ import {setDomain, setInterval} from '../series/store/state/bounds';
import {
setCoordinateSystem, setElectrodes,
} from '../series/store/state/montage';
import {EventMetadata, HEDSchemaElement} from '../series/store/types';
import {
ChannelInfo, ChannelInfos, ChannelMetadata, EventMetadata, HEDSchemaElement,
} from '../series/store/types';
import TriggerableModal from 'jsx/TriggerableModal';
import DatasetTagger from '../series/components/DatasetTagger';
import {InfoIcon} from '../series/components/components';
Expand All @@ -35,6 +39,7 @@ declare global {


type CProps = {
channelsURL: string,
chunksURL: string,
epochsURL: string,
electrodesURL: string,
Expand All @@ -59,16 +64,66 @@ const MenuOption = {
};

/**
* EEGLabSeriesProvider component
* The channel informaton context, which provides the BIDS information about\
* the channels present in the acquisition, if available.
*/
export const ChannelInfosContext = createContext<ChannelInfo[]>([]);

/**
* The channel metadata context, which provides the metadata about the channels
* present in the acquisition.
*/
export const ChannelMetasContext = createContext<ChannelMetadata[]>([]);

/**
* Function wrapper around the older `EEGLabSeriesProviderClass` class
* component.
*/
function EEGLabSeriesProvider(props: CProps) {
const [channelInfos, setChannelInfos] = useState<ChannelInfo[]>([]);
const [channelMetas, setChannelMetas] = useState<ChannelMetadata[]>([]);

// Fetch the channel BIDS information from the API.
useEffect(() => {
fetchJSON(props.channelsURL).then((json: ChannelInfos) => {
setChannelInfos(json.Channels);
});
}, [props.channelsURL]);

return (
<ChannelInfosContext.Provider value={channelInfos}>
<ChannelMetasContext.Provider value={channelMetas}>
<EEGLabSeriesProviderClass
{...props}
setChannelMetas={setChannelMetas}
/>
</ChannelMetasContext.Provider>
</ChannelInfosContext.Provider>
);
}

/**
* Props for the `EEGLabSeriesProviderClass` component, which extend the props
* of the functional component.
*/
type CClassProps = CProps & {
/**
* Setter for the channel metadata context lifted to the functional component.
*/
setChannelMetas: (_: ChannelMetadata[]) => void,
};

/**
* EEGLabSeriesProviderClass component
*/
class EEGLabSeriesProvider extends Component<CProps, any> {
class EEGLabSeriesProviderClass extends Component<CClassProps, any> {
private store: Store;

/**
* @class
* @param {object} props - React Component properties
*/
constructor(props: CProps) {
constructor(props: CClassProps) {
super(props);
const epicMiddleware = createEpicMiddleware();

Expand Down Expand Up @@ -99,6 +154,7 @@ class EEGLabSeriesProvider extends Component<CProps, any> {
eegMontageName,
recordingHasHED,
t,
setChannelMetas,
} = props;

if (!window.EEGLabSeriesProviderStore) {
Expand Down Expand Up @@ -184,6 +240,7 @@ class EEGLabSeriesProvider extends Component<CProps, any> {
const {
channelMetadata, shapes, timeInterval, seriesRange, validSamples,
} = json;
setChannelMetas(channelMetadata);
this.store.dispatch(
setDatasetMetadata({
chunksURL: url,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useEffect, useState} from 'react';
import React, {useContext, useEffect, useState} from 'react';
import {ChannelMetadata, Epoch as EpochType, HEDSchemaElement, HEDTag, RightPanel,} from '../store/types';
import {connect} from 'react-redux';
import {setTimeSelection} from '../store/state/timeSelection';
Expand All @@ -21,6 +21,7 @@ import swal from 'sweetalert2';
import {InfoIcon} from "./components";
import {colorOrder} from "../../color";
import {useTranslation} from "react-i18next";
import {ChannelMetasContext} from '../../eeglab/EEGLabSeriesProvider';


type CProps = {
Expand All @@ -40,7 +41,6 @@ type CProps = {
hedSchema: HEDSchemaElement[],
datasetTags: any,
channelDelimiter: string,
channelMetadata: ChannelMetadata[],
panelIsDirty: boolean,
setPanelIsDirty: (_: boolean) => void,
eventChannels: string[],
Expand All @@ -65,7 +65,6 @@ type CProps = {
* @param root0.hedSchema
* @param root0.datasetTags
* @param root0.channelDelimiter
* @param root0.channelMetadata
* @param root0.panelIsDirty
* @param root0.setPanelIsDirty
* @param root0.eventChannels
Expand All @@ -87,13 +86,13 @@ const AnnotationForm = ({
hedSchema,
datasetTags,
channelDelimiter,
channelMetadata,
panelIsDirty,
setPanelIsDirty,
eventChannels,
setEventChannels,
}: CProps) => {
const {t} = useTranslation();
const channelMetadata = useContext(ChannelMetasContext);
const [eventInterval, setEventInterval] = useState<(number | string)[]>(
timeSelection ?? ['', '']
);
Expand Down Expand Up @@ -1636,7 +1635,6 @@ export default connect(
hedSchema: state.dataset.hedSchema,
datasetTags: state.dataset.datasetTags,
channelDelimiter: state.dataset.channelDelimiter,
channelMetadata: state.dataset.channelMetadata,
channels: state.channels,
}),
(dispatch: (any) => void) => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {useCallback} from "react";
import {useTranslation} from "react-i18next";
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<string, ChannelTypeState>,
setChannelTypes: React.Dispatch<React.SetStateAction<Record<string, ChannelTypeState>>>,
}) => {
const {t} = useTranslation();

// Toggle the visibility of a channel type.
const toggleChannelType = useCallback((channelTypeName: string) => {
setChannelTypes((channelTypes) => {
const channelType = channelTypes[channelTypeName];
return ({
...channelTypes,
[channelTypeName]: {
...channelType,
visible: !channelType.visible,
},
});
});
}, [setChannelTypes]);

return (
<div style={{position: 'relative'}}>
<button className="btn btn-primary dropdown" data-toggle="dropdown">
{t('Channel Types')}
</button>
<ul className="dropdown-menu">
{Object.entries(channelTypes).map(([name, {visible, channelsCount}]) => (
<li
key={name}
style={{
display: 'flex',
justifyContent: 'space-between',
padding: '0.75rem 1.5rem',
}}
onClick={(e) => {
toggleChannelType(name);
e.stopPropagation();
}}
>
<span>{name} ({channelsCount})</span>
<input
type="checkbox"
checked={visible || false}
onClick={
// Do not collapse the dropdown on click.
(e) => e.stopPropagation()
}
onChange={() => toggleChannelType(name)}
/>
</li>
))}
</ul>
</div>
);
}

export default ChannelTypesSelector;
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from 'react';
import React, {useContext} from 'react';
import {vec2} from 'gl-matrix';
import {MIN_EPOCH_WIDTH} from '../../vector';
import {ScaleLinear} from 'd3-scale';
import {connect} from "react-redux";
import {RootState} from "../store";
import {Channel, ChannelMetadata} from "../store/types";
import {Channel} from "../store/types";
import {ChannelMetasContext} from '../../eeglab/EEGLabSeriesProvider';

type CProps = {
key: string,
Expand All @@ -20,7 +21,6 @@ type CProps = {
minWidth: number,
epochChannels?: string[],
channels: Channel[],
channelMetadata: ChannelMetadata[],
};

/**
Expand All @@ -35,7 +35,6 @@ type CProps = {
* @param root0.minWidth
* @param root0.epochChannels
* @param root0.channels
* @param root0.channelMetadata
*/
const Epoch = (
{
Expand All @@ -49,8 +48,9 @@ const Epoch = (
minWidth,
epochChannels,
channels,
channelMetadata,
}: CProps) => {
const channelMetadata = useContext(ChannelMetasContext);

onset = isNaN(onset) ? 0 : onset;
duration = isNaN(duration) ? 0 : duration;

Expand Down Expand Up @@ -121,5 +121,4 @@ Epoch.defaultProps = {
export default connect(
(state: RootState)=> ({
channels: state.channels,
channelMetadata: state.dataset.channelMetadata,
}))(Epoch);
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useState, useEffect} from 'react';
import React, {useState, useEffect, useContext} from 'react';
import {setCurrentAnnotation} from '../store/state/currentAnnotation';
import {MAX_RENDERED_EPOCHS} from '../../vector';
import {
Expand All @@ -15,7 +15,6 @@ import {
HEDTag,
HEDSchemaElement,
RightPanel,
ChannelMetadata,
Channel
} from '../store/types';
import {connect} from 'react-redux';
Expand All @@ -26,6 +25,7 @@ import {RootState} from '../store';
import {setFilteredEpochs} from '../store/state/dataset';
import {CheckboxElement} from './Form';
import {useTranslation, Trans} from "react-i18next";
import {ChannelMetasContext} from '../../eeglab/EEGLabSeriesProvider';

type CProps = {
timeSelection?: [number, number],
Expand All @@ -46,7 +46,6 @@ type CProps = {
datasetTags: any,
channelDelimiter: string,
channels: Channel[],
channelMetadata: ChannelMetadata[],
canEdit: boolean,
tagsHaveChanges: boolean,
};
Expand All @@ -70,7 +69,6 @@ type CProps = {
* @param root0.hedSchema
* @param root0.channelDelimiter
* @param root0.channels
* @param root0.channelMetadata
* @param root0.datasetTags
* @param root0.tagsHaveChanges
*/
Expand All @@ -92,11 +90,11 @@ const EventManager = ({
datasetTags,
channelDelimiter,
channels,
channelMetadata,
canEdit,
tagsHaveChanges,
}: CProps) => {
const {t} = useTranslation();
const channelMetadata = useContext(ChannelMetasContext);
const [epochsInRange, setEpochsInRange] = useState(getEpochsInRange(epochs, interval));
const [allEpochsVisible, setAllEpochsVisibility] = useState(() => {
if (epochsInRange.length < MAX_RENDERED_EPOCHS) {
Expand Down Expand Up @@ -769,7 +767,6 @@ export default connect(
datasetTags: state.dataset.datasetTags,
channelDelimiter: state.dataset.channelDelimiter,
channels: state.channels, // TODO: merge with below and pass?
channelMetadata: state.dataset.channelMetadata,
tagsHaveChanges: state.dataset.tagsHaveChanges,
}),
(dispatch: (_: any) => void) => ({
Expand Down
Loading
Loading