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:
- Channels mirrored: the new A channel is the T channel reversed element-by-element; new T = reversed A; new C = reversed G; new G = reversed C. This physically flips the chromatogram left-to-right and swaps the colour assignments so the trace reads naturally from left to right again.
-
Peak positions recomputed: each old position p maps to
sampleCount − 1 − p, and the order of bases is reversed, sonewPeakPos[i] = (sampleCount − 1) − oldPeakPos[len − 1 − i]. Hit testing, tooltips, and the sequence panel all stay in sync. - Base calls reversed and complemented: each base goes through the IUPAC complement table (A↔T, C↔G, R↔Y, K↔M, B↔V, D↔H, S/W/N stay). Case is preserved.
- Qualities reversed to match the new base order.
-
Scalar fields unchanged:
format,fileName,sampleCount,metadataare copied as-is.
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:
- Asserts the toggle button starts as 5′→3′ with
aria-pressed="false". - Clicks the toggle, waits for the canvas to re-paint, and asserts the pixel ink sum changed.
- Asserts the button label flips to 3′→5′ and back on a second click.
- Asserts the sequence panel text content differs after the toggle.
- Asserts that toggling twice returns to within 1% of the original ink sum.
- On desktop only: sweeps the mouse to trigger a tooltip and asserts it still contains peak: data.
- Triggers FASTA export after toggling and asserts the header line and filename both contain revcomp, and that the sequence lines are valid IUPAC characters.
- On desktop only: verifies the strand toggle button is reachable via keyboard
Tab navigation, and that activating it with Space / Enter
correctly flips
aria-pressedand re-paints the canvas — confirming full keyboard accessibility.
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.