Skip to content

Commit 15b85a9

Browse files
committed
dynamic visible channel type range
1 parent 8a735c2 commit 15b85a9

3 files changed

Lines changed: 185 additions & 7 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/series/components/SeriesRenderer.tsx

Lines changed: 104 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ import Pagination from './Pagination';
7878
import {SET_CHANNELS} from '../store/state/channels';
7979
import {UPDATE_VIEWED_CHUNKS} from '../store/logic/fetchChunks';
8080
import {ChannelInfosContext, ChannelMetasContext} from '../../eeglab/EEGLabSeriesProvider';
81-
import {getMinMaxRange} from '../../utils';
82-
import { number } from 'node_modules/@types/prop-types';
81+
import {computePercentileRange, computeMean} from '../../utils';
82+
import MutableKeyDepCache from '../../MutableDepCache';
8383

8484
/**
8585
* The state of a channel type.
@@ -89,6 +89,47 @@ export type ChannelTypeState = {
8989
channelsCount: number,
9090
};
9191

92+
/**
93+
* The specific cache type used to cache channel ranges.
94+
*/
95+
type ChannelRangeCache = MutableKeyDepCache<
96+
// The channel index.
97+
number,
98+
// The dependencies upon which the range depends.
99+
ChannelRangeCacheDeps,
100+
// The computed range.
101+
[number, number]
102+
>
103+
104+
/**
105+
* The dependencies upon which a channel range depends.
106+
*/
107+
type ChannelRangeCacheDeps = {
108+
interval: [number, number],
109+
chunkIds: number[],
110+
}
111+
112+
/**
113+
* Check whethere two channel range cache dependencies are equal.
114+
*/
115+
function compareChannelRangeDeps(a: ChannelRangeCacheDeps, b: ChannelRangeCacheDeps): boolean {
116+
if (a.interval[0] !== b.interval[0] || a.interval[1] !== b.interval[1]) {
117+
return false;
118+
}
119+
120+
if (a.chunkIds.length !== b.chunkIds.length) {
121+
return false;
122+
}
123+
124+
for (let i = 0; i < a.chunkIds.length; i += 1) {
125+
if (a.chunkIds[i] !== b.chunkIds[i]) {
126+
return false;
127+
}
128+
}
129+
130+
return true;
131+
}
132+
92133
type CProps = {
93134
ref: MutableRefObject<any>,
94135
viewerWidth: number,
@@ -580,6 +621,11 @@ const SeriesRenderer: FunctionComponent<CProps> = ({
580621
vec2.add(center, topLeft, bottomRight);
581622
vec2.scale(center, center, 1 / 2);
582623

624+
// A mutable cache for the visible range of the values each channel.
625+
const channelRangeCache: MutableRefObject<ChannelRangeCache> = useRef(
626+
new MutableKeyDepCache(compareChannelRangeDeps)
627+
);
628+
583629
const scales: [
584630
ScaleLinear<number, number, never>,
585631
ScaleLinear<number, number, never>
@@ -729,6 +775,31 @@ const SeriesRenderer: FunctionComponent<CProps> = ({
729775
)
730776
: filteredChannels;
731777

778+
// Get the maximum range for each channel type based on the ranges of
779+
// the visible values of each visible channel of that type.
780+
const channelTypeRanges: Record<string, number> = {};
781+
channelList.forEach((channel) => {
782+
const bidsChannel = findBidsChannel(channelMetadata[channel.index], bidsChannels);
783+
const type = bidsChannel?.ChannelType ?? 'Unknown';
784+
785+
// Get the visible values range of that channel, from the cache if possible.
786+
const [min, max] = channelRangeCache.current.get(
787+
channel.index,
788+
{
789+
interval,
790+
chunkIds: channel.traces.flatMap((trace) => trace.chunks.map((chunk) => chunk.index)).sort(),
791+
},
792+
() => {console.log("RECOMPUTE"); return getChannelVisibleRange(channel, interval) }
793+
);
794+
795+
const range = max - min;
796+
if (!channelTypeRanges[type]) {
797+
channelTypeRanges[type] = range;
798+
} else {
799+
channelTypeRanges[type] = Math.max(channelTypeRanges[type], range);
800+
}
801+
});
802+
732803
return (
733804
<>
734805
<clipPath
@@ -786,14 +857,18 @@ const SeriesRenderer: FunctionComponent<CProps> = ({
786857
(chunk) => chunk.values.length > 0
787858
).length;
788859

789-
const valuesInView = getChannelVisibleValues(trace, interval);
860+
const valuesInView = getTraceVisibleValues(trace, interval);
790861

791862
if (valuesInView.length === 0) {
792863
return;
793864
}
794865

795-
// Get the range of all the displayed values for this channel.
796-
const seriesRange = getMinMaxRange(valuesInView);
866+
// Get the range centered on the average for channels of the same type
867+
const average = computeMean(valuesInView);
868+
const bidsChannel = findBidsChannel(channelMetadata[channel.index], bidsChannels);
869+
const type = bidsChannel?.ChannelType ?? 'Unknown';
870+
const overallRange = channelTypeRanges[type] || 0;
871+
const seriesRange: [number, number] = [average - overallRange / 2, average + overallRange / 2];
797872

798873
const scales: [
799874
ScaleLinear<number, number, never>,
@@ -1607,9 +1682,31 @@ SeriesRenderer.defaultProps = {
16071682
};
16081683

16091684
/**
1610-
* Get the values of a channel that fall within the visible interval.
1685+
* Get the range of the visible values of a channel across all its traces.
1686+
*/
1687+
function getChannelVisibleRange(channel: Channel, interval: [number, number]): [number, number] {
1688+
let channelMin = Infinity;
1689+
let channelMax = -Infinity;
1690+
1691+
channel.traces.forEach((trace) => {
1692+
const values = getTraceVisibleValues(trace, interval);
1693+
if (values.length === 0) {
1694+
return;
1695+
}
1696+
1697+
const [traceMin, traceMax] = computePercentileRange(values);
1698+
console.log(traceMin, traceMax);
1699+
channelMin = Math.min(channelMin, traceMin);
1700+
channelMax = Math.max(channelMax, traceMax);
1701+
});
1702+
1703+
return [channelMin, channelMax];
1704+
}
1705+
1706+
/**
1707+
* Get the values of a trace that fall within the visible interval.
16111708
*/
1612-
function getChannelVisibleValues(trace: Trace, interval: [number, number]): number[] {
1709+
function getTraceVisibleValues(trace: Trace, interval: [number, number]): number[] {
16131710
const [start, end] = interval;
16141711
const values = [];
16151712

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,46 @@ export function getMinMaxRange(values: number[]): [number, number] {
1616
return [min, max];
1717
}
1818

19+
/**
20+
* Get the value at the n-th percentile index in a list.
21+
*/
22+
export function getIndexPercentile(values: number[], percentile: number): number {
23+
const index = Math.floor((percentile / 100) * values.length);
24+
const clampedIndex = Math.min(index, values.length - 1);
25+
return values[clampedIndex];
26+
};
27+
28+
/**
29+
* Compute the percentile range in a list of numbers.
30+
*/
31+
export function computePercentileRange(
32+
values: number[],
33+
lowerPercentile: number = 5,
34+
upperPercentile: number = 95,
35+
): [number, number] {
36+
if (values.length === 0) {
37+
return [0, 0];
38+
}
39+
40+
const sorted = [...values].sort((a, b) => a - b);
41+
42+
return [
43+
getIndexPercentile(sorted, lowerPercentile),
44+
getIndexPercentile(sorted, upperPercentile),
45+
];
46+
}
47+
48+
/**
49+
* Compute the mean in a list of numbers.
50+
*/
51+
export function computeMean(values: number[]): number {
52+
if (values.length === 0) {
53+
return 0;
54+
}
55+
56+
return values.reduce((a, b) => a + b, 0) / values.length;
57+
}
58+
1959
/**
2060
* Normalize a channel unit for visualization.
2161
*/

0 commit comments

Comments
 (0)