Frontend (JavaScript)

Browser-side dashboard: ES modules, Plotly, panel system, 3D brain visualization

New to ES modules? The frontend uses modern JavaScript module syntax (import/export). See JS ES Modules for a primer.

File Overview

FileRole
index.htmlHTML shell: layout divs, topbar, sidebar, modal skeletons
style.cssDark theme styling for all UI components
app.jsMain controller: state, load, sidebar, montage, zoom/pan coordination
panels.jsPanel registry and layout system
timeseries.jsMulti-channel strip chart rendering (Plotly)
spectral.jsPSD overlay and spectrogram heatmap (Plotly)
metrics.jsMetric heatmaps: line length, Hjorth, band power (Plotly)
brain.js3D brain scatter plot with animated metric coloring (Plotly)
browser.jsBIDS 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

  1. User types path and presses Load
  2. apiFetch("POST /api/load", {path}) — stores result in state.info
  3. buildSidebar(state.info) — renders channel list grouped by electrode prefix
  4. applyLayout(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:

StatusColor
goodGreen
badRed
soz (seizure onset zone)Orange
resectedPurple
unknown / missingGray

Filter buttons

Three filter modes change which channels are in state.channels, then re-render time series and PSD:

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 IDDescription
timeseriesMulti-channel scrollable strip chart
psdOverlay PSD curves for selected channels
spectrogramSTFT spectrogram for a single channel
line_lengthHeatmap: channels × time windows
hjorthHeatmap: channels × time windows (one Hjorth param)
band_powerHeatmap: channels × frequency bands
brain3d3D scatter plot of electrodes colored by metric
emptyPlaceholder (gray box)

Layout presets

PresetGridBest for
clinicalTimeseries (large) + PSD + EventsStandard clinical review
seizure_onsetTimeseries + spectrogram + line length + brainSeizure onset zone analysis
spectralPSD + spectrogram + band powerFrequency analysis

Key functions

FunctionDescription
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

ExportDescription
CHANNEL_COLORSArray 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

ExportDescription
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

ExportDescription
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

Modewindow_sPurpose
Scrub1.0sFast load; allows timeline scrubbing with immediate feedback
Play0.25sHigh-resolution animation at 4 frames/second when play button pressed

Metric options (BRAIN_METRICS)

Key internal functions

FunctionDescription
_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: