← Back to devlog

Snipping the ragged ends: PHRED quality trimming ships

Every Sanger trace has two flavours of signal. There is the clean core in the middle — tight peaks, confident base calls, reliable PHRED scores — and then there are the ends. The first twenty or thirty bases after the primer are often a noisy mess: overlapping peaks, miscalled bases, quality scores in the single digits. The last thirty bases fade into a similar haze as the polymerase runs out of steam. Professional viewers like Chromas and FinchTV have always let you trim those ends away before you copy the sequence or export a FASTA. Step 7 brings that to sanger-viewer.

What the Mott algorithm actually does

The algorithm dates to a 1995 paper by Rodger Mott (the same Mott behind the early QTL work) and was popularised by the Phred base-caller. It is clever and very simple. For each base at position i, compute a score:

score(i) = quality(i) − threshold

A base with quality 30 at threshold 20 scores +10: it is pushing the window in a good direction. A base with quality 5 scores −15: it is dragging the window down. You then run Kadane's maximum-sum subarray algorithm over that score sequence. Kadane is the classic O(n) trick for finding the contiguous subsequence whose values sum to the highest total — except here that window is the kept region, and everything outside it is trimmed away.

The result is two indices: trimStart (first kept base, inclusive) and trimEnd (one past the last kept base, exclusive). The length of the kept window is trimEnd − trimStart. We also report the mean PHRED quality over that window so you can see at a glance how confident the trimmed read is.

There are two degenerate cases the algorithm handles cleanly. If every base scores below threshold, the running sum never beats zero and nothing is kept — we return an all-trimmed status with a zero-length sequence rather than crashing. If there is no quality data at all (some old SCF files omit it), we return a no-quality status and the full sequence unchanged, with a visible label in the controls telling you why no trimming happened.

What you see on screen

The trim controls live in a second row directly below the main toolbar. There is a labelled range slider from Q=0 to Q=40 (default Q=20), a live numeric readout next to the thumb, and a pair of segmented "Full / Trimmed" buttons that switch the sequence panel and FASTA export between the two modes. Moving the slider triggers a rAF-throttled recompute exactly like the position readout already uses, so dragging stays smooth even on the large 3730-base fixture. No intermediate states, no jank.

On the canvas, trimmed end-regions are shaded with a semi-transparent amber overlay. Two dashed amber boundary lines mark the transition from trimmed to kept territory. The shading sits in the rendering stack between the background fill and the trace curves — visible and distinctive, but not obscuring the signal. The overlay is theme-aware and works in both light and dark mode.

In the sequence panel, switching to Trimmed mode shows only the kept window (centred on any selected base, still ±120 bases wide within the window). Full mode shows the ±120-base window as before, but now with a visual dimming on bases outside the quality boundary so you can see where the trim falls without losing context.

Export

The FASTA export reads the current mode. In Trimmed mode the exported sequence contains only the kept bases, and the FASTA header carries a compact annotation like [trimmed 23–761/795 bp Q38.2] so downstream tools or lab colleagues can see exactly what was removed. In Full mode the export is identical to the old behaviour. Reverse complement and quality trimming compose correctly: you can flip the strand and trim the revcomp read and the sequence and coordinates are always consistent.

The exported filename picks up a -trimmed suffix (and a -revcomp suffix if you are on the reverse strand) so the two modes never collide in your Downloads folder.

Tests

The trimming algorithm lives in src/quality/mottTrim.ts as a pure function with no DOM dependencies. That made it easy to write a thorough set of Vitest unit tests covering:

The Playwright E2E suite adds tests that act like a real user: load the 3100.ab1 fixture, confirm the trim controls render and the summary line appears, drag the slider and assert the summary changes, switch between Full and Trimmed modes and confirm the canvas ink sum differs, and export FASTAs in both modes to confirm the trimmed sequence is shorter, is contained in the full sequence, and that the headers carry the right annotations. These run on both desktop Chrome and the emulated iPad project.

What is still ahead

CI is green. Lint, typecheck, 49 Vitest unit tests, all Playwright E2E tests on desktop and tablet pass without regressions.

The north-star roadmap continues: subsequence find with IUPAC ambiguity codes and both-strand search, per-base peak amplitude display in the tooltip, multi-trace workspace, and high-resolution vector export. Each is its own small PR, its own devlog entry, its own green CI run.