JavaScript ES Modules
Background reading: how the frontend organizes code into separate files with import/export
The Old Way: Script Tags and Globals
Historically, JavaScript had no module system. Every script loaded with a <script>
tag shared the same global scope. Any variable defined in one file was visible in all others —
which caused name collisions and made it impossible to tell where something came from.
<!-- Old way: multiple scripts, all sharing globals -->
<script src="utils.js"></script> <!-- defines renderChart(), computeOffset() -->
<script src="app.js"></script> <!-- can accidentally overwrite renderChart -->
ES Modules: Explicit Import and Export
ES Modules (ESM) are the modern standard, introduced in ECMAScript 2015 (ES6). Each file is its own module with its own private scope. Variables and functions are only shared if explicitly exported, and only imported where explicitly imported.
// timeseries.js — explicitly exports two things:
export function renderTimeSeries(div, data) {
// ... Plotly rendering code ...
}
export function onZoomPan(div, callback) {
// ... event listener ...
}
// This stays private — not exported, not visible outside:
function _buildTraces(data) { ... }
// app.js — explicitly imports what it needs:
import { renderTimeSeries, onZoomPan } from "./timeseries.js";
// Now these are available in this file only
renderTimeSeries(div, data);
How Modules Are Loaded in the Browser
In HTML, you mark the entry-point script as a module with type="module":
<!-- index.html -->
<script type="module" src="app.js"></script>
The browser loads app.js. When it sees import ... from "./timeseries.js",
it automatically fetches timeseries.js from the server.
It follows this chain recursively until all dependencies are loaded.
No bundler required — browsers do this natively.
Named vs Default Exports
// Named exports — import by exact name:
export function renderPSD(div, data) { ... }
export function renderSpectrogram(div, data) { ... }
export const CHANNEL_COLORS = ["#5b8dee", "#4ade80", ...];
// In another file:
import { renderPSD, CHANNEL_COLORS } from "./spectral.js";
// Default export — import with any name you choose:
export default function lttb(times, values, n) { ... }
// In another file:
import lttb from "./downsample.js"; // you choose the name
import myDownsample from "./downsample.js"; // also valid
Montage Concord uses named exports throughout — each file exports its public functions by name, making it obvious what's available.
The Module Graph in Montage Concord
The frontend's module graph is deliberately structured to avoid circular dependencies.
app.js is the only file that imports from all others; the render modules don't import from each other or from app.js.
// app.js imports from all render modules:
import { registerPanel, applyLayout, rerenderAll } from "./panels.js";
import { renderTimeSeries, onZoomPan } from "./timeseries.js";
import { renderPSD, renderSpectrogram } from "./spectral.js";
import { renderLineLength, renderHjorth, renderBandPower } from "./metrics.js";
import { renderBrain3DPanel } from "./brain.js";
// timeseries.js imports from nothing (pure):
// spectral.js imports from nothing (pure):
// metrics.js imports from nothing (pure):
Async/Await: Non-Blocking HTTP Calls
The browser is single-threaded — if it waited for a network response, the whole page would freeze.
JavaScript solves this with async functions and await:
// Sync version (would freeze the browser):
const data = fetchData("/api/timeseries"); // blocks until done — BAD
render(data);
// Async version (browser stays responsive):
async function loadAndRender() {
const data = await fetchData("/api/timeseries"); // pauses THIS function, not the browser
render(data);
}
await pauses execution of the current async function until the network request completes,
but lets the rest of the browser (other event handlers, animations, etc.) keep running.
async function apiFetch(url, options = {}) {
const response = await fetch(url, options); // built-in browser HTTP function
if (!response.ok) {
const text = await response.text();
throw new Error(text);
}
return response.json(); // parse JSON body → JavaScript object
}
How Plotly.react Works
Plotly is a JavaScript charting library. Instead of creating a new chart every time data changes,
Montage Concord uses Plotly.react(), which updates an existing chart efficiently:
// First call: creates the chart
Plotly.react(divElement, traces, layout);
// Subsequent calls with new data: updates in place (much faster than newPlot)
Plotly.react(divElement, newTraces, layout);
Plotly.react (introduced in Plotly 1.34) diffs the new traces against the existing ones
and only re-renders what changed. This is why the dashboard stays responsive when zooming/panning —
it doesn't rebuild the entire chart.
localStorage: Persisting Layout
localStorage is a browser API for storing small amounts of data that persists
between page loads (unlike regular JavaScript variables which disappear on refresh).
// Save layout:
localStorage.setItem("concord_layout", JSON.stringify(grid));
// Load layout:
const saved = localStorage.getItem("concord_layout");
const grid = saved ? JSON.parse(saved) : DEFAULT_GRID;
This is how the panel layout (which panels are shown, in what arrangement) is remembered between sessions. It's stored in the browser's local storage — not on the server — so it's specific to this browser on this machine.