1import { useEffect, useState } from "react";
2import type { RemoteH5FileX, RemoteH5Group } from "../remote-h5-file";
4// Number of samples we lazily pull from the multi-million-sample timeseries.
5export const WINDOW_SAMPLES = 30000;
7export type NwbData = {
8 rootGroup?: RemoteH5Group;
9 meta?: Record<string, string>;
10 timeseries?: {
11 t: Float64Array;
12 pupil: Float64Array;
13 running: Float64Array;
14 totalSamples: number;
15 };
16 trials?: { startTime: Float64Array; stimFrequency: Float64Array };
17 step: string;
18 error?: string;
19 done: boolean;
20};
22const PUPIL = "/processing/behavior/PupilTracking/pupil_diameter";
23const RUNNING = "/processing/behavior/running_speed";
24const TRIALS = "/intervals/trials";
26const toF64 = (x: unknown): Float64Array => {
27 if (x instanceof Float64Array) return x;
28 if (ArrayBuffer.isView(x)) return Float64Array.from(x as unknown as number[]);
29 if (Array.isArray(x)) return Float64Array.from(x as number[]);
30 return new Float64Array(0);
31};
33const readScalar = async (
34 file: RemoteH5FileX,
35 path: string,
36): Promise<string> => {
37 try {
38 const v = await file.getDatasetData(path, {});
39 if (v === undefined || v === null) return "";
40 if (typeof v === "string") return v;
41 if (ArrayBuffer.isView(v) || Array.isArray(v)) {
42 return Array.from(v as unknown as number[]).join(", ");
43 }
44 return String(v);
45 } catch {
46 return "";
47 }
48};
50export const useNwbData = (file: RemoteH5FileX | undefined): NwbData => {
51 const [data, setData] = useState<NwbData>({ step: "idle", done: false });
53 useEffect(() => {
54 if (!file) {
55 setData({ step: "idle", done: false });
56 return;
57 }
58 let canceled = false;
59 const update = (patch: Partial<NwbData>) =>
60 !canceled && setData((d) => ({ ...d, ...patch }));
62 const run = async () => {
63 try {
64 update({ step: "reading file structure (root group)", done: false });
65 const rootGroup = await file.getGroup("/");
66 update({ rootGroup });
68 update({ step: "reading metadata fields" });
69 const subjectPrefix = "/general/subject";
70 const meta: Record<string, string> = {
71 session_description: await readScalar(file, "/session_description"),
72 identifier: await readScalar(file, "/identifier"),
73 session_start_time: await readScalar(file, "/session_start_time"),
74 institution: await readScalar(file, "/general/institution"),
75 lab: await readScalar(file, "/general/lab"),
76 subject_id: await readScalar(file, `${subjectPrefix}/subject_id`),
77 species: await readScalar(file, `${subjectPrefix}/species`),
78 sex: await readScalar(file, `${subjectPrefix}/sex`),
79 age: await readScalar(file, `${subjectPrefix}/age`),
80 };
81 update({ meta });
83 update({
84 step: `lazily reading first ${WINDOW_SAMPLES.toLocaleString()} samples of pupil & running traces`,
85 });
86 const pupilDs = await file.getDataset(`${PUPIL}/data`);
87 const totalSamples = pupilDs?.shape?.[0] ?? 0;
88 const n = Math.min(WINDOW_SAMPLES, totalSamples || WINDOW_SAMPLES);
89 const slice: [number, number][] = [[0, n]];
90 const t = toF64(
91 await file.getDatasetData(`${PUPIL}/timestamps`, { slice }),
92 );
93 const pupil = toF64(
94 await file.getDatasetData(`${PUPIL}/data`, { slice }),
95 );
96 const running = toF64(
97 await file.getDatasetData(`${RUNNING}/data`, { slice }),
98 );
99 update({ timeseries: { t, pupil, running, totalSamples } });
101 update({ step: "reading trials table (stimulus frequency)" });
102 const startTime = toF64(
103 await file.getDatasetData(`${TRIALS}/start_time`, {}),
104 );
105 const stimFrequency = toF64(
106 await file.getDatasetData(`${TRIALS}/stim_frequency`, {}),
107 );
108 update({ trials: { startTime, stimFrequency } });
110 update({ step: "done", done: true });
111 } catch (err: unknown) {
112 update({
113 step: "error",
114 error: err instanceof Error ? err.message : String(err),
115 done: true,
116 });
117 }
118 };
119 run();
120 return () => {
121 canceled = true;
122 };
123 }, [file]);
125 return data;
126};