All files / src/render variants.ts

100% Statements 14/14
80% Branches 8/10
100% Functions 1/1
100% Lines 14/14

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110                                                                                          2x                                         2x     2x                             2x 20x       20x 17x   17x 20x 20x 9x 20x 20x                     2x  
/**
 * The ink a called variant is drawn in.
 *
 * One rule: **a substitution takes the colour of the base it substitutes to**.
 * That is the whole of it, and the reason it is worth a module is what it buys
 * vertically. Open a FASTA, a BAM and a VCF over one locus and a heterozygous
 * A>G reads as one column in inks that agree — the sequence row writes a green
 * `A`, the pile puts orange `G` marks through half its reads, and the variant
 * row draws an orange box saying somebody called it. The colours come from the one table
 * `render/sequence.ts` holds, which `render/alignments.ts` already borrows for
 * exactly this reason, so there is no second palette to keep in step.
 *
 * ## The record with no base to be coloured by
 *
 * An insertion's bases are not in the reference at all and a deletion's ALT is
 * the padding base that did not change, so neither has a single base the mark
 * could be *about*. Those take `BASE_COLOURS`' fifth slot — the grey the
 * sequence lane and the pile already draw an `N` in, which in this application
 * means exactly *no single-base answer*. Same claim, same ink, and it is the
 * one entry in that table that is a neutral rather than a hue, so it can never
 * be mistaken for one of the four.
 *
 * The lane's **own** colour was tried there first and is wrong, visibly: the
 * first colour `ANNOTATION_PALETTE` hands out is `#0072b2` and `C` is
 * `#2f6fb5` — ten degrees of hue apart — so on a stock lane an insertion and a
 * substitution to C were the same blue. A reader's colour is
 * not out of the picture — it is what the density rung is drawn in, which is
 * where it does the work of telling two variant files apart at a zoom where no
 * individual mark is legible anyway.
 *
 * ## What this deliberately does not distinguish
 *
 * An insertion from a deletion: both are grey. Inventing two more inks would
 * put two colours on the lane that mean something other than what every other
 * colour on it means, and this application's own rule (§8.12, and
 * `VARIANT_COLOUR` at `render/tracks.ts`) is that an ink says one thing.
 *
 * What tells them apart instead is **extent**, which is free and exact: a
 * forty-kilobase deletion is a forty-kilobase box and a SNV is one base, so the
 * two classes a reader most needs to separate are separated by the strongest
 * channel on the lane. The rest — insertion against one-base deletion, which do
 * look alike — is in the box's detail and in the card behind a click, where
 * `A>AGGT` and `AT>A` are two strings nobody can confuse.
 */
 
import { BASE_COLOURS } from './sequence'
 
/**
 * The narrowest a variant box is drawn, in pixels.
 *
 * A single base is a hairline at every zoom a reader browses a chromosome at —
 * `laneRect` floors it to one pixel, which is enough to *exist* and not enough
 * to see or to hit. Three is where a mark reads as a mark beside the
 * one-pixel-wide guides this application already draws, and it is small enough
 * that a lane of them still packs into rows rather than merging.
 *
 * Grown from the variant's *start*, never centred on it: a mark centred on a
 * single base sits half a mark before the position it is about, and at the zoom
 * where that matters — a reader checking a call against the residue under it —
 * being one base out is the whole question.
 *
 * Handed to `layoutFeatures`, which writes it into the placement — so the box
 * that is drawn, the box that is clicked and the box the packing keeps a row
 * clear for are the same box. Applied in the paint alone, as it first was, two
 * thirds of every mark was dead to the pointer.
 */
export const VARIANT_MIN_WIDTH = 3
 
/** `BASE_CODE`'s order, which is {@link BASE_COLOURS}' order. */
const BASES = 'ACGT'
 
/**
 * The colour for one variant, from the `REF>ALT` its detail string opens with.
 *
 * Reading it back out of the detail rather than carrying a colour column,
 * because a `FeatureSet` is a struct of arrays sized for millions of rows and
 * this is asked a few hundred times per frame — once per *placement*, which the
 * layout has already capped. The same pass that writes the labels.
 *
 * A record with several ALTs takes the grey even when they are all single
 * bases: `A>G,T` is two substitutions to two different bases and there is no
 * single honest colour for it, so it says *no single-base answer* rather than
 * picking one of them.
 */
export function variantInk(detail: string, fallback: string): string {
  const arrow = detail.indexOf('>')
  // Not a `REF>ALT` at all, which is a detail string from some other parser and
  // therefore a bug rather than a variant. A bug must not paint: the lane's own
  // colour is the answer that says nothing.
  if (arrow <= 0) return fallback
  const ref = detail.slice(0, arrow)
  // The detail's first part ends at the separator, wherever the joiner put it.
  const stop = detail.indexOf(' ', arrow)
  const alt = stop < 0 ? detail.slice(arrow + 1) : detail.slice(arrow + 1, stop).replace(/·$/, '')
  if (ref.length !== 1 || alt.length !== 1) return NO_SINGLE_BASE
  const at = BASES.indexOf(alt.toUpperCase())
  return at < 0 ? NO_SINGLE_BASE : BASE_COLOURS[at]
}
 
/**
 * The ink for a record with no single base behind it.
 *
 * `BASE_COLOURS`' fifth slot, which is what the sequence lane and the pile draw
 * an `N` in — so it already means *no single-base answer* everywhere else on
 * screen, and this is the same sentence rather than a new one. It is also the
 * one entry in that table that is a neutral rather than a hue, which is why it
 * cannot be confused with a letter the way the lane's own colour could.
 */
const NO_SINGLE_BASE = BASE_COLOURS[4]