1import { useBackend } from "../nwb/useBackend";
2import type { BackendMode } from "../nwb/useBackend";
3import { useNwbData, WINDOW_SAMPLES } from "../nwb/useNwbData";
4import { LinePlot } from "./LinePlot";
6type Props = {
7 title: string;
8 description: string;
9 mode: BackendMode;
10 downloadUrl: string;
11 dandisetId: string;
12};
14// Decimate a long array down to ~maxPoints for light SVG rendering.
15const decimate = <T extends { length: number; [i: number]: number }>(
16 arr: T,
17 maxPoints = 1500,
18): number[] => {
19 const step = Math.max(1, Math.floor(arr.length / maxPoints));
20 const out: number[] = [];
21 for (let i = 0; i < arr.length; i += step) out.push(arr[i]);
22 return out;
23};
25export const BackendPanel = ({
26 title,
27 description,
28 mode,
29 downloadUrl,
30 dandisetId,
31}: Props) => {
32 const backend = useBackend(downloadUrl, dandisetId, mode);
33 const data = useNwbData(backend.file);
35 return (
36 <div
37 style={{
38 flex: "1 1 420px",
39 minWidth: 380,
40 border: "1px solid #d0d0d0",
41 borderRadius: 8,
42 padding: 16,
43 background: "#fafafa",
44 }}
45 >
46 <h3 style={{ marginTop: 0 }}>{title}</h3>
47 <p style={{ color: "#555", fontSize: 13, marginTop: 0 }}>{description}</p>
49 <div style={{ fontSize: 13, marginBottom: 8 }}>
50 <strong>Status:</strong>{" "}
51 {backend.status === "opening" && "opening remote fileβ¦"}
52 {backend.status === "ready" && (
53 <span style={{ color: "#1a7f37" }}>
54 opened in {backend.openMs?.toFixed(0)} ms
55 </span>
56 )}
57 {backend.status === "error" && (
58 <span style={{ color: "#b00020" }}>error</span>
59 )}
60 {backend.status === "idle" && "idle"}
61 </div>
63 {backend.resolvedUrl && (
64 <div
65 style={{
66 fontSize: 11,
67 color: "#777",
68 wordBreak: "break-all",
69 marginBottom: 8,
70 }}
71 >
72 <strong>Resolved:</strong> {backend.resolvedUrl}
73 </div>
74 )}
76 {backend.error && (
77 <div style={{ color: "#b00020", fontSize: 13, marginBottom: 8 }}>
78 {backend.error}
79 </div>
80 )}
82 {backend.status === "ready" && (
83 <>
84 <div style={{ fontSize: 13, marginBottom: 8 }}>
85 <strong>Reading:</strong>{" "}
86 {data.done ? (
87 data.error ? (
88 <span style={{ color: "#b00020" }}>{data.error}</span>
89 ) : (
90 <span style={{ color: "#1a7f37" }}>complete</span>
91 )
92 ) : (
93 <span>{data.step}β¦</span>
94 )}
95 </div>
97 {data.rootGroup && (
98 <details style={{ marginBottom: 10 }}>
99 <summary style={{ cursor: "pointer", fontSize: 13 }}>
100 Root group: {data.rootGroup.subgroups.length} groups,{" "}
101 {data.rootGroup.datasets.length} datasets
102 </summary>
103 <div style={{ fontFamily: "monospace", fontSize: 12, marginTop: 6 }}>
104 {data.rootGroup.subgroups.map((g) => (
105 <div key={g.path}>π {g.name}/</div>
106 ))}
107 {data.rootGroup.datasets.map((d) => (
108 <div key={d.path}>π {d.name}</div>
109 ))}
110 </div>
111 </details>
112 )}
114 {data.meta && (
115 <table style={{ fontSize: 12, marginBottom: 12, lineHeight: 1.5 }}>
116 <tbody>
117 {Object.entries(data.meta).map(([k, v]) => (
118 <tr key={k}>
119 <td
120 style={{
121 color: "#555",
122 paddingRight: 10,
123 verticalAlign: "top",
124 whiteSpace: "nowrap",
125 }}
126 >
127 {k}
128 </td>
129 <td>{v || <span style={{ color: "#aaa" }}>β</span>}</td>
130 </tr>
131 ))}
132 </tbody>
133 </table>
134 )}
136 {data.timeseries && (
137 <div style={{ marginBottom: 12 }}>
138 <div style={{ fontSize: 12, color: "#555", marginBottom: 4 }}>
139 Loaded first {data.timeseries.t.length.toLocaleString()} of{" "}
140 {data.timeseries.totalSamples.toLocaleString()} samples (
141 {(
142 (data.timeseries.t.length / (data.timeseries.totalSamples || 1)) *
143 100
144 ).toFixed(2)}
145 % of the trace β only those bytes were fetched).
146 </div>
147 <LinePlot
148 xLabel="time (s)"
149 yLabel="value"
150 series={[
151 {
152 x: decimate(data.timeseries.t),
153 y: decimate(data.timeseries.pupil),
154 color: "#1f77b4",
155 label: "pupil diameter",
156 },
157 {
158 x: decimate(data.timeseries.t),
159 y: decimate(data.timeseries.running),
160 color: "#d62728",
161 label: "running speed",
162 },
163 ]}
164 />
165 </div>
166 )}
168 {data.trials && (
169 <div>
170 <div style={{ fontSize: 12, color: "#555", marginBottom: 4 }}>
171 Trials table: {data.trials.stimFrequency.length.toLocaleString()}{" "}
172 trials β stimulus frequency over the session.
173 </div>
174 <LinePlot
175 xLabel="trial start time (s)"
176 yLabel="stim frequency (Hz)"
177 series={[
178 {
179 x: decimate(data.trials.startTime),
180 y: decimate(data.trials.stimFrequency),
181 color: "#2ca02c",
182 label: "stim frequency",
183 },
184 ]}
185 />
186 </div>
187 )}
188 </>
189 )}
190 <div style={{ fontSize: 11, color: "#999", marginTop: 10 }}>
191 Window size: {WINDOW_SAMPLES.toLocaleString()} samples
192 </div>
193 </div>
194 );
195};