← Back to devlog

Seeing both strands: reverse-complement view ships

Every Sanger trace is fundamentally a single-stranded read — you load it, you see bases from left to right in the 5′ to 3′ direction, and that is the end of the story. Except it is not. Half the time the primer points the other way, and you are staring at the complement of the thing you actually care about. Professional viewers like Chromas and FinchTV have always offered a one-click strand flip. Step 6 brings that to sanger-viewer.

What the toggle actually does

The strand toggle button (labelled 5′→3′ when showing the forward read, 3′→5′ when showing the reverse complement) is a single data-action="toggle-strand" button in the toolbar. Clicking it computes a fresh derived TraceData through reverseComplementTrace() and hands it straight to the existing renderer. No conditional logic scattered through the render path — the canvas, sequence panel, tooltip, and FASTA export all see the same TraceData interface they always did, now just with mirrored content.

The raw parsed trace is kept in a separate rawTrace variable in TraceViewer. Toggling computes the display trace on the fly — it is fast enough on the largest fixture (3730 bases, tens of thousands of samples) that no caching or worker offloading is needed. Toggling back is equally instant.

What reverseComplementTrace does under the hood

The src/revcomp.ts module exports two things: a pure iupacComplement(base) function covering the full 15-character IUPAC alphabet, and reverseComplementTrace(trace) which builds a new TraceData with:

FASTA export knows the strand

toFasta() gained an optional second parameter isRevcomp. When true, the FASTA header becomes >id_revcomp [reverse complement] and the download filename gets a -revcomp suffix. The default is false, so all existing code that calls toFasta(trace) without a second argument continues to work without modification — backward compatible by design.

State management on load and toggle

Loading a new file always resets to the forward strand. On toggle, the selected and hovered base indices are cleared, the tooltip is hidden, and fitToScreen() is called to reset the viewport — otherwise you could end up panning into empty space after a horizontal mirror. The toolbar button's aria-pressed attribute tracks the current strand for screen readers and automated tests.

Tests that prove it

The Vitest unit suite in tests/core/revcomp.test.ts covers fourteen cases: all fifteen IUPAC complement codes, case-preservation for lowercase bases, passthrough of unknown characters, channel mirroring, peak position mirroring for both symmetric and asymmetric inputs, quality reversal, null-quality handling, and the round-trip property (applying revcomp twice returns the original). That last one is the most satisfying to see go green.

The Playwright E2E suite in tests/e2e/strand-toggle.e2e.test.ts runs on both the desktop Chrome and emulated iPad projects. It loads 3100.ab1 through the file input, then:

The existing ux-a11y.e2e keyboard-focus helper now loops up to fifteen Tab presses (up from eight) to find #file-input, making the test robust against future control additions without needing another bump.

What is in progress and what comes next

CI is green. The full suite — lint, typecheck, 32 Vitest unit tests, all Playwright E2E tests on desktop and tablet — passes without regressions.

Next up on the north-star roadmap: PHRED quality trimming (strip low-confidence ends below a configurable threshold), subsequence find with IUPAC ambiguity on both strands, per-base peak amplitude display in the tooltip, multi-trace workspace, and high-resolution vector export. Each one a small PR, each one keeping CI green, each one with its own devlog entry.