All files / src/render sashimi.ts

55% Statements 33/60
91.66% Branches 11/12
50% Functions 1/2
55% Lines 33/60

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                                          2x 2x 2x         2x           2x 4x 3x 4x 2x     2x 2x 2x 2x 2x 3x 3x 3x 3x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 4x 4x 4x 4x                                 2x                                                        
/**
 * A coverage lane's own arcs: the sashimi plot, from the reads (§8.23).
 *
 * A BAM's depth lane is counted from its reads per window (§8.13), and the
 * reads that skip an intron say so in their CIGAR — so the introns they skip,
 * and how many reads skip each, come from the same pass for nothing
 * (`bam/pile.ts`'s `eachIntron`). Drawn over the depth they are Katz et al.'s
 * sashimi plot: the curve says how much of each exon was read, the arcs say how
 * the exons were joined, and the count on an arc is the reads behind it. No
 * junction file is needed; any spliced alignment file draws them.
 *
 * The arcs are the junction lane's (`render/junctions.ts`), stood on the
 * depth's baseline rather than split by strand, and in the plot's ink rather
 * than the lane's colour, which is the colour of the filled curve under them.
 * Their ceiling is the strongest junction **in view**: the depth under them is
 * autoscaled to the window, so an arc whose height meant something the depth
 * does not would be the one number in the lane read on another scale — and
 * every arc carries its count.
 */
 
import type { FeatureSet, TrackSampleSet } from '../annotations/types'
import { FeatureBuilder } from '../annotations/featureset'
import { layoutFeatures, type FeaturePaint } from './features'
import { drawJunctions } from './junctions'
import type { LaneFrame } from './lane'
import type { LocusAxis } from './locus'
 
/** One set per sample set, so a frame that did not refetch does not rebuild it. */
const built = new WeakMap<TrackSampleSet, FeatureSet | null>()
 
/**
 * The introns a window's reads skipped, as a set of junctions, or `null` for a
 * window with none — a DNA library's, or one not yet answered.
 */
export function junctionsOfSamples(samples: TrackSampleSet | null): FeatureSet | null {
  if (samples === null) return null
  const held = built.get(samples)
  if (held !== undefined) return held
  const builder = new FeatureBuilder()
  // A junction reaching into two padded windows of one view is one junction,
  // counted once in each: keep the larger count, which is the whole of it.
  const seen = new Map<string, number>()
  for (const region of samples.regions) {
    const junctions = region.junctions
    if (junctions === undefined) continue
    for (let i = 0; i < junctions.starts.length; i++) {
      const key = `${region.contig}:${junctions.starts[i]}:${junctions.ends[i]}:${junctions.strands[i]}`
      const count = junctions.counts[i]
      const before = seen.get(key)
      if (before !== undefined && before >= count) continue
      seen.set(key, count)
    }
  }
  for (const [key, count] of seen) {
    const parts = key.split(':')
    const strand = Number(parts.pop())
    const end = Number(parts.pop())
    const start = Number(parts.pop())
    builder.add(parts.join(':'), start, end, { strand, score: count })
  }
  const set = builder.length === 0 ? null : builder.finish()
  built.set(samples, set)
  return set
}
 
/** What the arcs need of the lane's own paint. */
export interface SashimiPaint {
  guideColour: string
  haloColour: string
  font: string
  opacity: number
  labels: boolean
  labelBand?: number
}
 
/**
 * Draw a coverage lane's arcs over its depth: every intron in view that at
 * least `floor` reads skipped, as tall as its count against the strongest one
 * in view, labelled with that count.
 */
export function drawSashimi(
  ctx: CanvasRenderingContext2D,
  frame: LaneFrame,
  axis: LocusAxis,
  set: FeatureSet,
  paint: SashimiPaint,
  options: { floor: number; flip: boolean },
): void {
  const layout = layoutFeatures(frame, axis, set, 1, 0, false)
  let top = 0
  for (const placement of layout.placements) {
    const count = set.score[placement.index]
    if (count > top) top = count
  }
  if (top < options.floor) return
  const arcs: FeaturePaint = {
    colour: paint.guideColour,
    guideColour: paint.guideColour,
    haloColour: paint.haloColour,
    font: paint.font,
    opacity: paint.opacity,
    outline: false,
    labels: paint.labels,
    ...(paint.labelBand === undefined ? {} : { labelBand: paint.labelBand }),
    axisLimits: { min: options.floor, max: Math.max(top, options.floor + 1) },
  }
  drawJunctions(ctx, frame, set, layout, arcs, { split: false, flip: options.flip })
}