← Back to devlog

Step 10: multi-trace workspace and high-resolution SVG export

Until today, sanger-viewer could hold exactly one trace at a time. Open a second file and the first was gone — no switching, no comparison, no "let me quickly check what the other reaction looked like." That changes with Step 10.

Multi-trace workspace

The core change is a new TraceWorkspace class in src/workspace/TraceWorkspace.ts. It is a simple ordered array of slots — one per loaded trace — with LRU eviction capped at five open traces. Each slot stores everything needed to fully restore a trace session: the parsed TraceData, the strand state (forward / reverse complement), the trim settings and last computed trim result, the search query and matches, and the exact canvas viewport (start sample, samples-per-pixel). When you switch away from a tab and come back, you pick up exactly where you left off.

The LRU cap is important. A large .ab1 file can carry several megabytes of raw signal in four Float32Array channels. Keeping an unbounded number of them open would silently inflate memory until the tab hit the browser limit. Instead, when a sixth trace is opened, the oldest slot that is not currently displayed has its rawTrace set to null — the shell (file name, tab, saved state) stays in the list, the signal arrays are released for GC. The active slot is never evicted. This is the same invariant that professional desktop apps use when they limit the "recently opened" list.

Workspace bar UI

A horizontal tab strip appears above the controls whenever two or more traces are open. Each tab shows the file name, highlights the active one, and carries a × close button. A small button at the right end of the bar opens a file chooser for a second trace without disturbing the current one. The bar hides itself when only a single trace is loaded so it adds zero visual noise to the common one-file workflow.

Internally the bar works via bubbling CustomEvents: workspace-switch, workspace-close, and workspace-open. TraceViewer listens for these on its root element, saves the departing slot's state, then restores the arriving slot's state in a single synchronous pass. No extra re-parses, no flash.

High-resolution SVG export

The existing "Export PNG" button grabs whatever the canvas currently shows at screen resolution. For a publication figure or a lab notebook printout you often want something much sharper. The new "Export SVG" button produces a fully-vector rendering of the current viewport: four <path> elements (one per channel, with the same ACGT colour palette as the canvas), plus a <text> element for each visible base call.

Crucially, exportSvg in src/export/svg.ts is a pure function — no DOM, no canvas, no side effects. It reuses the same decimation and viewport-clamping logic as the canvas renderer but accumulates SVG path data strings instead of issuing canvas draw calls. Being DOM-free means it is directly testable in Vitest's node environment, which is how we cover the output validity (namespace, dimensions, path count, XML escaping) without spinning up a browser.

The exported SVG is 2× the canvas CSS dimensions by default, so on a retina screen where the canvas might be 800 × 300 CSS pixels the SVG comes out at 1600 × 600 — already print-quality. The viewBox matches the pixel dimensions so it scales cleanly in any vector editor.

Testing

Thirteen new Vitest unit tests cover exportSvg: valid SVG structure, correct dimensions, all four TRACE_COLORS present, base-call <text> elements, XML escaping of special characters, custom start/end sample windowing, and edge cases (empty base calls, single-sample traces).

Thirteen more unit tests cover TraceWorkspace directly: add/activate/close lifecycle, updateSlot patching, the LRU invariant (cap never exceeded, active slot never evicted, shell not null after cap).

Five new Playwright tests exercise the full browser workflow: two fixtures load into two tabs, switching restores the correct file name and a non-blank canvas, closing a tab collapses the bar, Export SVG triggers a download whose content is a real SVG document with channel paths, and the "+" button wires up to the hidden extra file input.

What's next

The workspace is intentionally minimal: tabs are file-name only, there is no thumbnail preview, no side-by-side comparison pane, and evicted slots cannot be re-loaded without re-opening the file. Future passes can add alignment / overlay view, better eviction feedback, and thumbnail generation. The SVG export could grow to include the quality bar, trim overlays, and search highlights. Each of those is a small, focused PR on top of this foundation.