Frontend (JavaScript)
Browser-side dashboard: ES modules, Plotly, panel system, 3D brain visualization
import/export).
See JS ES Modules for a primer.
File Overview
| File | Role |
|---|---|
| index.html | HTML shell: layout divs, topbar, sidebar, modal skeletons |
| style.css | Dark theme styling for all UI components |
| app.js | Main controller: state, load, sidebar, montage, zoom/pan coordination |
| panels.js | Panel registry and layout system |
| timeseries.js | Multi-channel strip chart rendering (Plotly) |
| spectral.js | PSD overlay and spectrogram heatmap (Plotly) |
| metrics.js | Metric heatmaps: line length, Hjorth, band power (Plotly) |
| brain.js | 3D brain scatter plot with animated metric coloring (Plotly) |
| browser.js | BIDS dataset navigator and folder picker dialog |
Module Dependency Graph
graph TD HTML["index.html"] APP["app.js\nMain entry point — imports everything"] PANELS["panels.js\nPanel registry, layout"] TS["timeseries.js\nPure render"] SP["spectral.js\nPure render"] MT["metrics.js\nPure render"] BR["brain.js\nSelf-contained:\nfetches + renders"] BW["browser.js\nBIDS navigator +\nfolder picker"] HTML --> APP APP --> PANELS & TS & SP & MT & BR & BW
The render modules (timeseries.js, spectral.js, metrics.js) are
pure: they take data and a DOM element, produce a Plotly chart, and have no side effects.
They do not hold any application state — that lives only in app.js.
app.js — Main Controller
Application state object
const state = {
info: null, // response from /api/load: channels, fs, duration, etc.
channels: [], // currently selected channels (subset of info.channels)
montage: "monopolar",
notchMode: "none",
viewRange: null, // [t_start, t_end] for current zoom window, or null = full
activeMetric: null, // for metric panels
};
apiFetch
async function apiFetch(url, options = {}) {
const res = await fetch(url, options);
if (!res.ok) throw new Error(await res.text());
return res.json();
}
All server calls go through apiFetch. If the server returns an error status, it throws with the error message so it can be shown to the user.
Load sequence
- User types path and presses Load
apiFetch("POST /api/load", {path})— stores result instate.infobuildSidebar(state.info)— renders channel list grouped by electrode prefixapplyLayout(currentGrid)— triggers all panel render functions
Sidebar
Channels are grouped by electrode prefix — the alphabetic part of names like "SEEG1" → prefix "SEEG". The regex /^(.+?)\d+$/ extracts the prefix.
Status indicators (colored dots) are derived from channel_metadata.status and channel_metadata.status_description:
| Status | Color |
|---|---|
| good | Green |
| bad | Red |
| soz (seizure onset zone) | Orange |
| resected | Purple |
| unknown / missing | Gray |
Filter buttons
Three filter modes change which channels are in state.channels, then re-render time series and PSD:
- All — all channels
- SOZ — only channels with status_description = "soz"
- Good — only channels with status = "good"
Montage switching
async function switchMontage(montage) {
const result = await apiFetch("/api/montage", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({ montage }),
});
state.info.channels = result.channels;
state.info.channel_metadata = result.channel_metadata;
state.montage = montage;
buildSidebar(state.info); // channel names change in bipolar
rerenderAll();
}
Zoom/pan coordination
The time series panel registers a Plotly zoom/pan listener via onZoomPan(div, callback)
(from timeseries.js). When the user zooms, the callback fires with the new [t_start, t_end],
which is stored in state.viewRange and a new /api/timeseries request is made for
just that window — providing dynamic level-of-detail.
panels.js — Layout System
The dashboard uses a configurable grid of panels. Each panel type has a registered render function.
The layout is stored in localStorage so it persists between sessions.
Panel types
| Panel ID | Description |
|---|---|
| timeseries | Multi-channel scrollable strip chart |
| psd | Overlay PSD curves for selected channels |
| spectrogram | STFT spectrogram for a single channel |
| line_length | Heatmap: channels × time windows |
| hjorth | Heatmap: channels × time windows (one Hjorth param) |
| band_power | Heatmap: channels × frequency bands |
| brain3d | 3D scatter plot of electrodes colored by metric |
| empty | Placeholder (gray box) |
Layout presets
| Preset | Grid | Best for |
|---|---|---|
| clinical | Timeseries (large) + PSD + Events | Standard clinical review |
| seizure_onset | Timeseries + spectrogram + line length + brain | Seizure onset zone analysis |
| spectral | PSD + spectrogram + band power | Frequency analysis |
Key functions
| Function | Description |
|---|---|
| registerPanel(id, fn) | Register a render function for a panel type. fn receives a div element and is called when the panel needs to render. |
| applyLayout(grid) | Rebuild the #main CSS grid with given panel arrangement, then call each registered render function. |
| rerenderAll() | Re-call all panel render functions without rebuilding DOM. Used after montage/notch changes. |
| saveLayout(grid) | Save grid to localStorage as JSON. |
| loadLayout() | Load grid from localStorage, or return default preset. |
timeseries.js
| Export | Description |
|---|---|
| CHANNEL_COLORS | Array of 10 hex colors for cycling through channels. |
| computeOffset(values) | Computes per-channel vertical offset for the strip chart. Based on median RMS so channels don't overlap. |
| buildTimeseriesTraces(data, offsetOverride) | Builds an array of Plotly Scatter traces, one per channel, with vertical offsets applied. |
| renderTimeSeries(div, data, shapes, offsetOverride, xRange) | Calls Plotly.react(div, traces, layout). Uses Plotly.react (not .newPlot) for efficient updates. |
| onZoomPan(div, callback) | Attaches a Plotly plotly_relayout event listener. Fires callback with new [xmin, xmax] when user zooms/pans. Debounced to avoid excessive API calls. |
spectral.js
| Export | Description |
|---|---|
| renderPSD(div, data) | Overlay PSD curves for all channels. Y-axis in log scale (dB). One colored line per channel. |
| renderSpectrogram(div, data, channelName) | Renders a 2D heatmap (time × frequency) using Plotly Heatmap with Viridis colorscale. Power in dB. |
metrics.js
| Export | Description |
|---|---|
| renderLineLength(div, data) | Heatmap: channels on y-axis, time windows on x-axis, line length value as color. |
| renderHjorth(div, data, paramIdx) | Heatmap for one Hjorth parameter (0=activity, 1=mobility, 2=complexity). |
| renderBandPower(div, data) | Heatmap: channels on y-axis, frequency bands on x-axis, power as color. |
brain.js — 3D Brain Visualization
Renders a 3D scatter plot of electrode contacts positioned in MNI space, with each contact colored by a metric value that animates over time.
Two-resolution strategy
| Mode | window_s | Purpose |
|---|---|---|
| Scrub | 1.0s | Fast load; allows timeline scrubbing with immediate feedback |
| Play | 0.25s | High-resolution animation at 4 frames/second when play button pressed |
Metric options (BRAIN_METRICS)
- line_length
- hjorth_activity (component 0)
- hjorth_mobility (component 1)
- band_power: gamma (index 4)
- band_power: high_gamma (index 5)
- band_power: beta (index 3)
Key internal functions
| Function | Description |
|---|---|
| _colorRange(metricData) | Computes global [cmin, cmax] across all channels and time windows. Used for consistent colorscale throughout animation. |
| _colorsAtTime(metricData, timeIdx, channels) | Extracts per-channel scalar at a specific time index for coloring the 3D scatter markers. |
| _render(div, positions, colors, cmin, cmax) | Calls Plotly.react() with a scatter3d trace. Marker size encodes SOZ status (SOZ contacts rendered larger). |
Styling (style.css)
Dark theme. Key design choices:
- Paper color:
#1a1d27— dark blue-gray - Plot background:
#0f1117— near-black - Sidebar: collapsible, groups channels by electrode with status dots
- Grid: CSS Grid layout, configurable columns/rows per preset
- Modals: file browser and layout editor use the same dark overlay style