@jbrowse/plugin-alignments 1.7.7 → 1.7.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/AlignmentsFeatureDetail/AlignmentsFeatureDetail.js +13 -3
  2. package/dist/AlignmentsFeatureDetail/index.d.ts +28 -3
  3. package/dist/AlignmentsFeatureDetail/index.js +6 -17
  4. package/dist/BamAdapter/BamAdapter.d.ts +1 -1
  5. package/dist/BamAdapter/BamAdapter.js +3 -3
  6. package/dist/BamAdapter/MismatchParser.d.ts +2 -5
  7. package/dist/BamAdapter/MismatchParser.js +108 -46
  8. package/dist/BamAdapter/MismatchParser.test.js +6 -14
  9. package/dist/CramAdapter/CramAdapter.d.ts +10 -9
  10. package/dist/CramAdapter/CramAdapter.js +6 -6
  11. package/dist/CramAdapter/CramSlightlyLazyFeature.js +35 -30
  12. package/dist/LinearPileupDisplay/model.d.ts +6 -3
  13. package/dist/LinearPileupDisplay/model.js +132 -51
  14. package/dist/LinearSNPCoverageDisplay/components/Tooltip.js +37 -17
  15. package/dist/LinearSNPCoverageDisplay/models/model.d.ts +2 -2
  16. package/dist/LinearSNPCoverageDisplay/models/model.js +1 -1
  17. package/dist/PileupRenderer/PileupLayoutSession.d.ts +3 -0
  18. package/dist/PileupRenderer/PileupLayoutSession.js +3 -1
  19. package/dist/PileupRenderer/PileupRenderer.d.ts +66 -9
  20. package/dist/PileupRenderer/PileupRenderer.js +296 -258
  21. package/dist/PileupRenderer/configSchema.js +2 -2
  22. package/dist/SNPCoverageAdapter/SNPCoverageAdapter.d.ts +6 -6
  23. package/dist/SNPCoverageAdapter/SNPCoverageAdapter.js +95 -96
  24. package/dist/SNPCoverageRenderer/configSchema.d.ts +1 -1
  25. package/package.json +3 -3
  26. package/src/AlignmentsFeatureDetail/AlignmentsFeatureDetail.tsx +14 -3
  27. package/src/AlignmentsFeatureDetail/index.ts +7 -17
  28. package/src/BamAdapter/BamAdapter.ts +3 -3
  29. package/src/BamAdapter/MismatchParser.test.ts +5 -7
  30. package/src/BamAdapter/MismatchParser.ts +72 -59
  31. package/src/CramAdapter/CramAdapter.ts +14 -10
  32. package/src/CramAdapter/CramSlightlyLazyFeature.ts +84 -91
  33. package/src/LinearPileupDisplay/model.ts +76 -24
  34. package/src/LinearSNPCoverageDisplay/components/Tooltip.tsx +41 -20
  35. package/src/LinearSNPCoverageDisplay/models/model.ts +1 -1
  36. package/src/PileupRenderer/PileupLayoutSession.ts +6 -1
  37. package/src/PileupRenderer/PileupRenderer.tsx +413 -225
  38. package/src/PileupRenderer/configSchema.ts +2 -2
  39. package/src/SNPCoverageAdapter/SNPCoverageAdapter.ts +89 -76
@@ -44,6 +44,25 @@ import {
44
44
  } from './PileupLayoutSession'
45
45
  import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter'
46
46
 
47
+ function fillRect(
48
+ ctx: CanvasRenderingContext2D,
49
+ l: number,
50
+ t: number,
51
+ w: number,
52
+ h: number,
53
+ cw: number,
54
+ color?: string,
55
+ ) {
56
+ if (l + w < 0 || l > cw) {
57
+ return
58
+ } else {
59
+ if (color) {
60
+ ctx.fillStyle = color
61
+ }
62
+ ctx.fillRect(l, t, w, h)
63
+ }
64
+ }
65
+
47
66
  function getColorBaseMap(theme: Theme) {
48
67
  return {
49
68
  A: theme.palette.bases.A.main,
@@ -118,18 +137,22 @@ interface LayoutFeature {
118
137
  feature: Feature
119
138
  }
120
139
 
121
- function shouldDrawMismatches(type?: string) {
140
+ function shouldDrawSNPs(type?: string) {
122
141
  return !['methylation', 'modifications'].includes(type || '')
123
142
  }
124
143
 
144
+ function shouldDrawIndels(type?: string) {
145
+ return true
146
+ }
147
+
125
148
  export default class PileupRenderer extends BoxRendererType {
126
149
  supportsSVG = true
127
150
 
128
151
  // get width and height of chars the height is an approximation: width
129
152
  // letter M is approximately the height
130
153
  getCharWidthHeight(ctx: CanvasRenderingContext2D) {
131
- const charWidth = ctx.measureText('A').width
132
- const charHeight = ctx.measureText('M').width
154
+ const charWidth = measureText('A')
155
+ const charHeight = measureText('M')
133
156
  return { charWidth, charHeight }
134
157
  }
135
158
 
@@ -283,25 +306,32 @@ export default class PileupRenderer extends BoxRendererType {
283
306
  return strand === 1 ? 'color_fwd_strand' : 'color_rev_strand'
284
307
  }
285
308
 
286
- colorByPerBaseLettering(
287
- ctx: CanvasRenderingContext2D,
288
- feat: LayoutFeature,
289
- _config: AnyConfigurationModel,
290
- region: Region,
291
- bpPerPx: number,
292
- props: {
293
- colorForBase: Record<string, string>
294
- contrastForBase: Record<string, string>
295
- charWidth: number
296
- charHeight: number
297
- },
298
- ) {
299
- const { colorForBase, contrastForBase, charWidth, charHeight } = props
309
+ colorByPerBaseLettering({
310
+ ctx,
311
+ feat,
312
+ region,
313
+ bpPerPx,
314
+ colorForBase,
315
+ contrastForBase,
316
+ charWidth,
317
+ charHeight,
318
+ canvasWidth,
319
+ }: {
320
+ ctx: CanvasRenderingContext2D
321
+ feat: LayoutFeature
322
+ region: Region
323
+ bpPerPx: number
324
+ colorForBase: Record<string, string>
325
+ contrastForBase: Record<string, string>
326
+ charWidth: number
327
+ charHeight: number
328
+ canvasWidth: number
329
+ }) {
300
330
  const heightLim = charHeight - 2
301
331
  const { feature, topPx, heightPx } = feat
302
332
  const seq = feature.get('seq') as string
303
333
  const cigarOps = parseCigar(feature.get('CIGAR'))
304
- const widthPx = 1 / bpPerPx
334
+ const w = 1 / bpPerPx
305
335
  const start = feature.get('start')
306
336
  let soffset = 0 // sequence offset
307
337
  let roffset = 0 // reference offset
@@ -316,22 +346,25 @@ export default class PileupRenderer extends BoxRendererType {
316
346
  } else if (op === 'M' || op === 'X' || op === '=') {
317
347
  for (let m = 0; m < len; m++) {
318
348
  const letter = seq[soffset + m]
319
- ctx.fillStyle = colorForBase[letter]
320
- const [leftPx] = bpSpanPx(
321
- start + roffset + m,
322
- start + roffset + m + 1,
323
- region,
324
- bpPerPx,
349
+ const r = start + roffset + m
350
+ const [leftPx] = bpSpanPx(r, r + 1, region, bpPerPx)
351
+ fillRect(
352
+ ctx,
353
+ leftPx,
354
+ topPx,
355
+ w + 0.5,
356
+ heightPx,
357
+ canvasWidth,
358
+ colorForBase[letter],
325
359
  )
326
- ctx.fillRect(leftPx, topPx, widthPx + 0.5, heightPx)
327
360
 
328
- if (widthPx >= charWidth && heightPx >= heightLim) {
361
+ if (w >= charWidth && heightPx >= heightLim) {
329
362
  // normal SNP coloring
330
363
  ctx.fillStyle = contrastForBase[letter]
331
364
 
332
365
  ctx.fillText(
333
366
  letter,
334
- leftPx + (widthPx - charWidth) / 2 + 1,
367
+ leftPx + (w - charWidth) / 2 + 1,
335
368
  topPx + heightPx,
336
369
  )
337
370
  }
@@ -341,13 +374,19 @@ export default class PileupRenderer extends BoxRendererType {
341
374
  }
342
375
  }
343
376
  }
344
- colorByPerBaseQuality(
345
- ctx: CanvasRenderingContext2D,
346
- feat: LayoutFeature,
347
- _config: AnyConfigurationModel,
348
- region: Region,
349
- bpPerPx: number,
350
- ) {
377
+ colorByPerBaseQuality({
378
+ ctx,
379
+ feat,
380
+ region,
381
+ bpPerPx,
382
+ canvasWidth,
383
+ }: {
384
+ ctx: CanvasRenderingContext2D
385
+ feat: LayoutFeature
386
+ region: Region
387
+ bpPerPx: number
388
+ canvasWidth: number
389
+ }) {
351
390
  const { feature, topPx, heightPx } = feat
352
391
  const qual: string = feature.get('qual') || ''
353
392
  const scores = qual.split(' ').map(val => +val)
@@ -367,14 +406,21 @@ export default class PileupRenderer extends BoxRendererType {
367
406
  } else if (op === 'M' || op === 'X' || op === '=') {
368
407
  for (let m = 0; m < len; m++) {
369
408
  const score = scores[soffset + m]
370
- ctx.fillStyle = `hsl(${score === 255 ? 150 : score * 1.5},55%,50%)`
371
409
  const [leftPx] = bpSpanPx(
372
410
  start + roffset + m,
373
411
  start + roffset + m + 1,
374
412
  region,
375
413
  bpPerPx,
376
414
  )
377
- ctx.fillRect(leftPx, topPx, width + 0.5, heightPx)
415
+ fillRect(
416
+ ctx,
417
+ leftPx,
418
+ topPx,
419
+ width + 0.5,
420
+ heightPx,
421
+ canvasWidth,
422
+ `hsl(${score === 255 ? 150 : score * 1.5},55%,50%)`,
423
+ )
378
424
  }
379
425
  soffset += len
380
426
  roffset += len
@@ -391,16 +437,23 @@ export default class PileupRenderer extends BoxRendererType {
391
437
  // has very high likelihood basecalls at that point, we really only care
392
438
  // about low qual calls <20 approx
393
439
  //
394
- colorByModifications(
395
- ctx: CanvasRenderingContext2D,
396
- layoutFeature: LayoutFeature,
397
- _config: AnyConfigurationModel,
398
- region: Region,
399
- bpPerPx: number,
400
- props: RenderArgsDeserializedWithFeaturesAndLayout,
401
- ) {
402
- const { feature, topPx, heightPx } = layoutFeature
403
- const { modificationTagMap = {} } = props
440
+ colorByModifications({
441
+ ctx,
442
+ feat,
443
+ region,
444
+ bpPerPx,
445
+ renderArgs,
446
+ canvasWidth,
447
+ }: {
448
+ ctx: CanvasRenderingContext2D
449
+ feat: LayoutFeature
450
+ region: Region
451
+ bpPerPx: number
452
+ renderArgs: RenderArgsDeserializedWithFeaturesAndLayout
453
+ canvasWidth: number
454
+ }) {
455
+ const { feature, topPx, heightPx } = feat
456
+ const { modificationTagMap = {} } = renderArgs
404
457
 
405
458
  const mm = (getTagAlt(feature, 'MM', 'Mm') as string) || ''
406
459
 
@@ -417,7 +470,6 @@ export default class PileupRenderer extends BoxRendererType {
417
470
 
418
471
  const cigar = feature.get('CIGAR')
419
472
  const start = feature.get('start')
420
- const end = feature.get('end')
421
473
  const seq = feature.get('seq')
422
474
  const strand = feature.get('strand')
423
475
  const cigarOps = parseCigar(cigar)
@@ -431,22 +483,27 @@ export default class PileupRenderer extends BoxRendererType {
431
483
  const col = modificationTagMap[type] || 'black'
432
484
  const base = Color(col)
433
485
  for (const readPos of getNextRefPos(cigarOps, positions)) {
434
- if (readPos >= 0 && start + readPos < end) {
435
- const [leftPx, rightPx] = bpSpanPx(
436
- start + readPos,
437
- start + readPos + 1,
438
- region,
439
- bpPerPx,
440
- )
441
-
442
- // give it a little boost of 0.1 to not make them fully
443
- // invisible to avoid confusion
444
- ctx.fillStyle = base
445
- .alpha(probabilities[probIndex] + 0.1)
446
- .hsl()
447
- .string()
448
- ctx.fillRect(leftPx, topPx, rightPx - leftPx + 0.5, heightPx)
449
- }
486
+ const r = start + readPos
487
+ const [leftPx, rightPx] = bpSpanPx(r, r + 1, region, bpPerPx)
488
+
489
+ // give it a little boost of 0.1 to not make them fully
490
+ // invisible to avoid confusion
491
+ const prob = probabilities[probIndex]
492
+
493
+ fillRect(
494
+ ctx,
495
+ leftPx,
496
+ topPx,
497
+ rightPx - leftPx + 0.5,
498
+ heightPx,
499
+ canvasWidth,
500
+ prob && prob !== 1
501
+ ? base
502
+ .alpha(prob + 0.1)
503
+ .hsl()
504
+ .string()
505
+ : col,
506
+ )
450
507
  probIndex++
451
508
  }
452
509
  }
@@ -455,16 +512,23 @@ export default class PileupRenderer extends BoxRendererType {
455
512
  // Color by methylation is slightly modified version of color by
456
513
  // modifications that focuses on CpG sites, with non-methylated CpG colored
457
514
  // blue
458
- colorByMethylation(
459
- ctx: CanvasRenderingContext2D,
460
- layoutFeature: LayoutFeature,
461
- _config: AnyConfigurationModel,
462
- region: Region,
463
- bpPerPx: number,
464
- props: RenderArgsDeserializedWithFeaturesAndLayout,
465
- ) {
466
- const { regionSequence } = props
467
- const { feature, topPx, heightPx } = layoutFeature
515
+ colorByMethylation({
516
+ ctx,
517
+ feat,
518
+ region,
519
+ bpPerPx,
520
+ renderArgs,
521
+ canvasWidth,
522
+ }: {
523
+ ctx: CanvasRenderingContext2D
524
+ feat: LayoutFeature
525
+ region: Region
526
+ bpPerPx: number
527
+ renderArgs: RenderArgsDeserializedWithFeaturesAndLayout
528
+ canvasWidth: number
529
+ }) {
530
+ const { regionSequence } = renderArgs
531
+ const { feature, topPx, heightPx } = feat
468
532
 
469
533
  const mm: string = getTagAlt(feature, 'MM', 'Mm') || ''
470
534
 
@@ -478,15 +542,14 @@ export default class PileupRenderer extends BoxRendererType {
478
542
  const seq = feature.get('seq')
479
543
  const strand = feature.get('strand')
480
544
  const cigarOps = parseCigar(cigar)
481
- const { start: rstart, end: rend } = region
482
545
 
483
- const methBins = new Array(rend - rstart).fill(0)
546
+ const methBins = new Array(region.end - region.start).fill(0)
484
547
  const modifications = getModificationPositions(mm, seq, strand)
485
548
  for (let i = 0; i < modifications.length; i++) {
486
549
  const { type, positions } = modifications[i]
487
550
  if (type === 'm' && positions) {
488
551
  for (const pos of getNextRefPos(cigarOps, positions)) {
489
- const epos = pos + fstart - rstart
552
+ const epos = pos + fstart - region.start
490
553
  if (epos >= 0 && epos < methBins.length) {
491
554
  methBins[epos] = 1
492
555
  }
@@ -495,7 +558,7 @@ export default class PileupRenderer extends BoxRendererType {
495
558
  }
496
559
 
497
560
  for (let j = fstart; j < fend; j++) {
498
- const i = j - rstart
561
+ const i = j - region.start
499
562
  if (i >= 0 && i < methBins.length) {
500
563
  const l1 = regionSequence[i].toLowerCase()
501
564
  const l2 = regionSequence[i + 1].toLowerCase()
@@ -503,36 +566,45 @@ export default class PileupRenderer extends BoxRendererType {
503
566
  // if we are zoomed out, display just a block over the cpg
504
567
  if (bpPerPx > 2) {
505
568
  if (l1 === 'c' && l2 === 'g') {
506
- const s = rstart + i
569
+ const s = region.start + i
507
570
  const [leftPx, rightPx] = bpSpanPx(s, s + 2, region, bpPerPx)
508
- if (methBins[i] || methBins[i + 1]) {
509
- ctx.fillStyle = 'red'
510
- } else {
511
- ctx.fillStyle = 'blue'
512
- }
513
- ctx.fillRect(leftPx, topPx, rightPx - leftPx + 0.5, heightPx)
571
+ fillRect(
572
+ ctx,
573
+ leftPx,
574
+ topPx,
575
+ rightPx - leftPx + 0.5,
576
+ heightPx,
577
+ canvasWidth,
578
+ methBins[i] || methBins[i + 1] ? 'red' : 'blue',
579
+ )
514
580
  }
515
581
  }
516
582
  // if we are zoomed in, color the c inside the cpg
517
583
  else {
518
584
  // color
519
585
  if (l1 === 'c' && l2 === 'g') {
520
- const s = rstart + i
586
+ const s = region.start + i
521
587
  const [leftPx, rightPx] = bpSpanPx(s, s + 1, region, bpPerPx)
522
- if (methBins[i]) {
523
- ctx.fillStyle = 'red'
524
- } else {
525
- ctx.fillStyle = 'blue'
526
- }
527
- ctx.fillRect(leftPx, topPx, rightPx - leftPx + 0.5, heightPx)
588
+ fillRect(
589
+ ctx,
590
+ leftPx,
591
+ topPx,
592
+ rightPx - leftPx + 0.5,
593
+ heightPx,
594
+ canvasWidth,
595
+ methBins[i] ? 'red' : 'blue',
596
+ )
528
597
 
529
598
  const [leftPx2, rightPx2] = bpSpanPx(s + 1, s + 2, region, bpPerPx)
530
- if (methBins[i + 1]) {
531
- ctx.fillStyle = 'red'
532
- } else {
533
- ctx.fillStyle = 'blue'
534
- }
535
- ctx.fillRect(leftPx2, topPx, rightPx2 - leftPx2 + 0.5, heightPx)
599
+ fillRect(
600
+ ctx,
601
+ leftPx2,
602
+ topPx,
603
+ rightPx2 - leftPx2 + 0.5,
604
+ heightPx,
605
+ canvasWidth,
606
+ methBins[i + 1] ? 'red' : 'blue',
607
+ )
536
608
  }
537
609
  }
538
610
  }
@@ -580,29 +652,29 @@ export default class PileupRenderer extends BoxRendererType {
580
652
  }
581
653
  }
582
654
 
583
- drawAlignmentRect(
584
- ctx: CanvasRenderingContext2D,
585
- feat: LayoutFeature,
586
- props: RenderArgsDeserializedWithFeaturesAndLayout & {
587
- colorForBase: Record<string, string>
588
- contrastForBase: Record<string, string>
589
- charWidth: number
590
- charHeight: number
591
- defaultColor: boolean
592
- },
593
- ) {
594
- const {
595
- defaultColor,
596
- config,
597
- bpPerPx,
598
- regions,
599
- colorBy,
600
- colorTagMap = {},
601
- colorForBase,
602
- contrastForBase,
603
- charWidth,
604
- charHeight,
605
- } = props
655
+ drawAlignmentRect({
656
+ ctx,
657
+ feat,
658
+ renderArgs,
659
+ colorForBase,
660
+ contrastForBase,
661
+ charWidth,
662
+ charHeight,
663
+ defaultColor,
664
+ canvasWidth,
665
+ }: {
666
+ ctx: CanvasRenderingContext2D
667
+ feat: LayoutFeature
668
+ renderArgs: RenderArgsDeserializedWithFeaturesAndLayout
669
+ colorForBase: Record<string, string>
670
+ contrastForBase: Record<string, string>
671
+ charWidth: number
672
+ charHeight: number
673
+ defaultColor: boolean
674
+ canvasWidth: number
675
+ }) {
676
+ const { config, bpPerPx, regions, colorBy, colorTagMap = {} } = renderArgs
677
+
606
678
  const { tag = '', type: colorType = '' } = colorBy || {}
607
679
  const { feature } = feat
608
680
  const region = regions[0]
@@ -689,62 +761,89 @@ export default class PileupRenderer extends BoxRendererType {
689
761
  break
690
762
  }
691
763
 
692
- this.drawRect(ctx, feat, props)
764
+ this.drawRect(ctx, feat, renderArgs)
693
765
 
694
766
  // second pass for color types that render per-base things that go over the
695
767
  // existing drawing
696
768
  switch (colorType) {
697
769
  case 'perBaseQuality':
698
- this.colorByPerBaseQuality(ctx, feat, config, region, bpPerPx)
770
+ this.colorByPerBaseQuality({
771
+ ctx,
772
+ feat,
773
+ region,
774
+ bpPerPx,
775
+ canvasWidth,
776
+ })
699
777
  break
700
778
 
701
779
  case 'perBaseLettering':
702
- this.colorByPerBaseLettering(ctx, feat, config, region, bpPerPx, {
780
+ this.colorByPerBaseLettering({
781
+ ctx,
782
+ feat,
783
+ region,
784
+ bpPerPx,
703
785
  colorForBase,
704
786
  contrastForBase,
705
787
  charWidth,
706
788
  charHeight,
789
+ canvasWidth,
707
790
  })
708
791
  break
709
792
 
710
793
  case 'modifications':
711
- this.colorByModifications(ctx, feat, config, region, bpPerPx, props)
794
+ this.colorByModifications({
795
+ ctx,
796
+ feat,
797
+ region,
798
+ bpPerPx,
799
+ renderArgs,
800
+ canvasWidth,
801
+ })
712
802
  break
713
803
 
714
804
  case 'methylation':
715
- this.colorByMethylation(ctx, feat, config, region, bpPerPx, props)
805
+ this.colorByMethylation({
806
+ ctx,
807
+ feat,
808
+ region,
809
+ bpPerPx,
810
+ renderArgs,
811
+ canvasWidth,
812
+ })
716
813
  break
717
814
  }
718
815
  }
719
816
 
720
- drawMismatches(
721
- ctx: CanvasRenderingContext2D,
722
- feat: LayoutFeature,
723
- props: RenderArgsDeserializedWithFeaturesAndLayout,
724
- opts: {
725
- colorForBase: { [key: string]: string }
726
- contrastForBase: { [key: string]: string }
727
- mismatchAlpha?: boolean
728
- drawSNPs?: boolean
729
- drawIndels?: boolean
730
- minSubfeatureWidth: number
731
- largeInsertionIndicatorScale: number
732
- charWidth: number
733
- charHeight: number
734
- },
735
- ) {
736
- const {
737
- minSubfeatureWidth,
738
- largeInsertionIndicatorScale,
739
- mismatchAlpha,
740
- drawSNPs = true,
741
- drawIndels = true,
742
- charWidth,
743
- charHeight,
744
- colorForBase,
745
- contrastForBase,
746
- } = opts
747
- const { bpPerPx, regions } = props
817
+ drawMismatches({
818
+ ctx,
819
+ feat,
820
+ renderArgs,
821
+ minSubfeatureWidth,
822
+ largeInsertionIndicatorScale,
823
+ mismatchAlpha,
824
+ charWidth,
825
+ charHeight,
826
+ colorForBase,
827
+ contrastForBase,
828
+ canvasWidth,
829
+ drawSNPs = true,
830
+ drawIndels = true,
831
+ }: {
832
+ ctx: CanvasRenderingContext2D
833
+ feat: LayoutFeature
834
+ renderArgs: RenderArgsDeserializedWithFeaturesAndLayout
835
+ colorForBase: { [key: string]: string }
836
+ contrastForBase: { [key: string]: string }
837
+ mismatchAlpha?: boolean
838
+ drawSNPs?: boolean
839
+ drawIndels?: boolean
840
+ minSubfeatureWidth: number
841
+ largeInsertionIndicatorScale: number
842
+ charWidth: number
843
+ charHeight: number
844
+ canvasWidth: number
845
+ }) {
846
+ const { bpPerPx, regions } = renderArgs
748
847
  const { heightPx, topPx, feature } = feat
749
848
  const [region] = regions
750
849
  const start = feature.get('start')
@@ -754,17 +853,6 @@ export default class PileupRenderer extends BoxRendererType {
754
853
  const mismatches: Mismatch[] = feature.get('mismatches')
755
854
  const heightLim = charHeight - 2
756
855
 
757
- function getAlphaColor(baseColor: string, mismatch: { qual?: number }) {
758
- let color = baseColor
759
- if (mismatchAlpha && mismatch.qual !== undefined) {
760
- color = Color(baseColor)
761
- .alpha(Math.min(1, mismatch.qual / 50))
762
- .hsl()
763
- .string()
764
- }
765
- return color
766
- }
767
-
768
856
  // extraHorizontallyFlippedOffset is used to draw interbase items, which
769
857
  // are located to the left when forward and right when reversed
770
858
  const extraHorizontallyFlippedOffset = region.reversed
@@ -783,14 +871,34 @@ export default class PileupRenderer extends BoxRendererType {
783
871
  if (mismatch.type === 'mismatch' && drawSNPs) {
784
872
  const baseColor = colorForBase[mismatch.base] || '#888'
785
873
 
786
- ctx.fillStyle = getAlphaColor(baseColor, mismatch)
787
-
788
- ctx.fillRect(leftPx, topPx, widthPx, heightPx)
874
+ fillRect(
875
+ ctx,
876
+ leftPx,
877
+ topPx,
878
+ widthPx,
879
+ heightPx,
880
+ canvasWidth,
881
+ !mismatchAlpha
882
+ ? baseColor
883
+ : mismatch.qual !== undefined
884
+ ? Color(baseColor)
885
+ .alpha(Math.min(1, mismatch.qual / 50))
886
+ .hsl()
887
+ .string()
888
+ : baseColor,
889
+ )
789
890
 
790
891
  if (widthPx >= charWidth && heightPx >= heightLim) {
791
892
  // normal SNP coloring
792
- const contrast = contrastForBase[mismatch.base] || 'black'
793
- ctx.fillStyle = getAlphaColor(contrast, mismatch)
893
+ const contrastColor = contrastForBase[mismatch.base] || 'black'
894
+ ctx.fillStyle = !mismatchAlpha
895
+ ? contrastColor
896
+ : mismatch.qual !== undefined
897
+ ? Color(contrastColor)
898
+ .alpha(Math.min(1, mismatch.qual / 50))
899
+ .hsl()
900
+ .string()
901
+ : contrastColor
794
902
  ctx.fillText(
795
903
  mbase,
796
904
  leftPx + (widthPx - charWidth) / 2 + 1,
@@ -798,9 +906,15 @@ export default class PileupRenderer extends BoxRendererType {
798
906
  )
799
907
  }
800
908
  } else if (mismatch.type === 'deletion' && drawIndels) {
801
- const baseColor = colorForBase.deletion
802
- ctx.fillStyle = baseColor
803
- ctx.fillRect(leftPx, topPx, widthPx, heightPx)
909
+ fillRect(
910
+ ctx,
911
+ leftPx,
912
+ topPx,
913
+ widthPx,
914
+ heightPx,
915
+ canvasWidth,
916
+ colorForBase.deletion,
917
+ )
804
918
  const txt = `${mismatch.length}`
805
919
  const rwidth = measureText(txt, 10)
806
920
  if (widthPx >= rwidth && heightPx >= heightLim) {
@@ -820,20 +934,34 @@ export default class PileupRenderer extends BoxRendererType {
820
934
  Math.min(1.2, 1 / bpPerPx),
821
935
  )
822
936
  if (len < 10) {
823
- ctx.fillRect(pos, topPx, insW, heightPx)
937
+ fillRect(ctx, pos, topPx, insW, heightPx, canvasWidth, 'purple')
824
938
  if (1 / bpPerPx >= charWidth && heightPx >= heightLim) {
825
- ctx.fillRect(pos - insW, topPx, insW * 3, 1)
826
- ctx.fillRect(pos - insW, topPx + heightPx - 1, insW * 3, 1)
939
+ fillRect(ctx, pos - insW, topPx, insW * 3, 1, canvasWidth)
940
+ fillRect(
941
+ ctx,
942
+ pos - insW,
943
+ topPx + heightPx - 1,
944
+ insW * 3,
945
+ 1,
946
+ canvasWidth,
947
+ )
827
948
  ctx.fillText(`(${mismatch.base})`, pos + 3, topPx + heightPx)
828
949
  }
829
950
  }
830
951
  } else if (mismatch.type === 'hardclip' || mismatch.type === 'softclip') {
831
- ctx.fillStyle = mismatch.type === 'hardclip' ? 'red' : 'blue'
832
952
  const pos = leftPx + extraHorizontallyFlippedOffset
833
- ctx.fillRect(pos, topPx, w, heightPx)
953
+ fillRect(
954
+ ctx,
955
+ pos,
956
+ topPx,
957
+ w,
958
+ heightPx,
959
+ canvasWidth,
960
+ mismatch.type === 'hardclip' ? 'red' : 'blue',
961
+ )
834
962
  if (1 / bpPerPx >= charWidth && heightPx >= heightLim) {
835
- ctx.fillRect(pos - w, topPx, w * 3, 1)
836
- ctx.fillRect(pos - w, topPx + heightPx - 1, w * 3, 1)
963
+ fillRect(ctx, pos - w, topPx, w * 3, 1, canvasWidth)
964
+ fillRect(ctx, pos - w, topPx + heightPx - 1, w * 3, 1, canvasWidth)
837
965
  ctx.fillText(`(${mismatch.base})`, pos + 3, topPx + heightPx)
838
966
  }
839
967
  } else if (mismatch.type === 'skip') {
@@ -844,12 +972,14 @@ export default class PileupRenderer extends BoxRendererType {
844
972
  // make small exons more visible when zoomed far out
845
973
  const adjustPx = widthPx - (bpPerPx > 10 ? 1.5 : 0)
846
974
  ctx.clearRect(leftPx, topPx, adjustPx, heightPx)
847
- ctx.fillStyle = '#333'
848
- ctx.fillRect(
975
+ fillRect(
976
+ ctx,
849
977
  Math.max(0, leftPx),
850
978
  topPx + heightPx / 2 - 1,
851
979
  adjustPx + (leftPx < 0 ? leftPx : 0),
852
980
  2,
981
+ canvasWidth,
982
+ '#333',
853
983
  )
854
984
  }
855
985
  }
@@ -866,39 +996,55 @@ export default class PileupRenderer extends BoxRendererType {
866
996
  const txt = `${len}`
867
997
  if (mismatch.type === 'insertion' && len >= 10) {
868
998
  if (bpPerPx > largeInsertionIndicatorScale) {
869
- ctx.fillStyle = 'purple'
870
- ctx.fillRect(leftPx - 1, topPx, 2, heightPx)
999
+ fillRect(ctx, leftPx - 1, topPx, 2, heightPx, canvasWidth, 'purple')
871
1000
  } else if (heightPx > charHeight) {
872
- const rect = ctx.measureText(txt)
1001
+ const rwidth = measureText(txt)
873
1002
  const padding = 5
874
- ctx.fillStyle = 'purple'
875
- ctx.fillRect(
876
- leftPx - rect.width / 2 - padding,
1003
+ fillRect(
1004
+ ctx,
1005
+ leftPx - rwidth / 2 - padding,
877
1006
  topPx,
878
- rect.width + 2 * padding,
1007
+ rwidth + 2 * padding,
879
1008
  heightPx,
1009
+ canvasWidth,
1010
+ 'purple',
880
1011
  )
881
1012
  ctx.fillStyle = 'white'
882
- ctx.fillText(txt, leftPx - rect.width / 2, topPx + heightPx)
1013
+ ctx.fillText(txt, leftPx - rwidth / 2, topPx + heightPx)
883
1014
  } else {
884
1015
  const padding = 2
885
- ctx.fillStyle = 'purple'
886
- ctx.fillRect(leftPx - padding, topPx, 2 * padding, heightPx)
1016
+ fillRect(
1017
+ ctx,
1018
+ leftPx - padding,
1019
+ topPx,
1020
+ 2 * padding,
1021
+ heightPx,
1022
+ canvasWidth,
1023
+ 'purple',
1024
+ )
887
1025
  }
888
1026
  }
889
1027
  }
890
1028
  }
891
1029
  }
892
1030
 
893
- drawSoftClipping(
894
- ctx: CanvasRenderingContext2D,
895
- feat: LayoutFeature,
896
- props: RenderArgsDeserializedWithFeaturesAndLayout,
897
- config: AnyConfigurationModel,
898
- theme: Theme,
899
- ) {
1031
+ drawSoftClipping({
1032
+ ctx,
1033
+ feat,
1034
+ renderArgs,
1035
+ config,
1036
+ theme,
1037
+ canvasWidth,
1038
+ }: {
1039
+ ctx: CanvasRenderingContext2D
1040
+ feat: LayoutFeature
1041
+ renderArgs: RenderArgsDeserializedWithFeaturesAndLayout
1042
+ config: AnyConfigurationModel
1043
+ theme: Theme
1044
+ canvasWidth: number
1045
+ }) {
900
1046
  const { feature, topPx, heightPx } = feat
901
- const { regions, bpPerPx } = props
1047
+ const { regions, bpPerPx } = renderArgs
902
1048
  const [region] = regions
903
1049
  const minFeatWidth = readConfObject(config, 'minSubfeatureWidth')
904
1050
  const mismatches: Mismatch[] = feature.get('mismatches')
@@ -946,7 +1092,14 @@ export default class PileupRenderer extends BoxRendererType {
946
1092
  // show in soft clipping
947
1093
  const baseColor = colorForBase[base] || '#000000'
948
1094
  ctx.fillStyle = baseColor
949
- ctx.fillRect(softClipLeftPx, topPx, softClipWidthPx, heightPx)
1095
+ fillRect(
1096
+ ctx,
1097
+ softClipLeftPx,
1098
+ topPx,
1099
+ softClipWidthPx,
1100
+ heightPx,
1101
+ canvasWidth,
1102
+ )
950
1103
 
951
1104
  if (softClipWidthPx >= charWidth && heightPx >= charHeight - 5) {
952
1105
  ctx.fillStyle = theme.palette.getContrastText(baseColor)
@@ -961,15 +1114,30 @@ export default class PileupRenderer extends BoxRendererType {
961
1114
  }
962
1115
  }
963
1116
 
964
- async makeImageData(
965
- ctx: CanvasRenderingContext2D,
966
- layoutRecords: (LayoutFeature | null)[],
967
- props: RenderArgsDeserializedWithFeaturesAndLayout,
968
- ) {
969
- const { layout, config, showSoftClip, colorBy, theme: configTheme } = props
1117
+ makeImageData({
1118
+ ctx,
1119
+ layoutRecords,
1120
+ canvasWidth,
1121
+ renderArgs,
1122
+ }: {
1123
+ ctx: CanvasRenderingContext2D
1124
+ canvasWidth: number
1125
+ layoutRecords: (LayoutFeature | null)[]
1126
+ renderArgs: RenderArgsDeserializedWithFeaturesAndLayout
1127
+ }) {
1128
+ const {
1129
+ layout,
1130
+ config,
1131
+ showSoftClip,
1132
+ colorBy,
1133
+ theme: configTheme,
1134
+ } = renderArgs
970
1135
  const mismatchAlpha = readConfObject(config, 'mismatchAlpha')
971
1136
  const minSubfeatureWidth = readConfObject(config, 'minSubfeatureWidth')
972
- const insertScale = readConfObject(config, 'largeInsertionIndicatorScale')
1137
+ const largeInsertionIndicatorScale = readConfObject(
1138
+ config,
1139
+ 'largeInsertionIndicatorScale',
1140
+ )
973
1141
  const defaultColor = readConfObject(config, 'color') === '#f0f'
974
1142
 
975
1143
  const theme = createJBrowseTheme(configTheme)
@@ -984,34 +1152,51 @@ export default class PileupRenderer extends BoxRendererType {
984
1152
  ctx.font = 'bold 10px Courier New,monospace'
985
1153
 
986
1154
  const { charWidth, charHeight } = this.getCharWidthHeight(ctx)
987
- layoutRecords.forEach(feat => {
1155
+ const drawSNPs = shouldDrawSNPs(colorBy?.type)
1156
+ const drawIndels = shouldDrawIndels(colorBy?.type)
1157
+ for (let i = 0; i < layoutRecords.length; i++) {
1158
+ const feat = layoutRecords[i]
988
1159
  if (feat === null) {
989
- return
1160
+ continue
990
1161
  }
991
1162
 
992
- this.drawAlignmentRect(ctx, feat, {
993
- ...props,
1163
+ this.drawAlignmentRect({
1164
+ ctx,
1165
+ feat,
1166
+ renderArgs,
994
1167
  defaultColor,
995
1168
  colorForBase,
996
1169
  contrastForBase,
997
1170
  charWidth,
998
1171
  charHeight,
1172
+ canvasWidth,
999
1173
  })
1000
- this.drawMismatches(ctx, feat, props, {
1174
+ this.drawMismatches({
1175
+ ctx,
1176
+ feat,
1177
+ renderArgs,
1001
1178
  mismatchAlpha,
1002
- drawSNPs: shouldDrawMismatches(colorBy?.type),
1003
- drawIndels: shouldDrawMismatches(colorBy?.type),
1004
- largeInsertionIndicatorScale: insertScale,
1179
+ drawSNPs,
1180
+ drawIndels,
1181
+ largeInsertionIndicatorScale,
1005
1182
  minSubfeatureWidth,
1006
1183
  charWidth,
1007
1184
  charHeight,
1008
1185
  colorForBase,
1009
1186
  contrastForBase,
1187
+ canvasWidth,
1010
1188
  })
1011
1189
  if (showSoftClip) {
1012
- this.drawSoftClipping(ctx, feat, props, config, theme)
1190
+ this.drawSoftClipping({
1191
+ ctx,
1192
+ feat,
1193
+ renderArgs,
1194
+ config,
1195
+ theme,
1196
+ canvasWidth,
1197
+ })
1013
1198
  }
1014
- })
1199
+ }
1015
1200
  }
1016
1201
 
1017
1202
  // we perform a full layout before render as a separate method because the
@@ -1041,7 +1226,7 @@ export default class PileupRenderer extends BoxRendererType {
1041
1226
 
1042
1227
  const heightPx = readConfObject(config, 'height')
1043
1228
  const displayMode = readConfObject(config, 'displayMode')
1044
- const layoutRecords = iterMap(
1229
+ return iterMap(
1045
1230
  featureMap.values(),
1046
1231
  feature =>
1047
1232
  this.layoutFeature({
@@ -1055,7 +1240,6 @@ export default class PileupRenderer extends BoxRendererType {
1055
1240
  }),
1056
1241
  featureMap.size,
1057
1242
  )
1058
- return layoutRecords
1059
1243
  }
1060
1244
 
1061
1245
  async fetchSequence(renderProps: RenderArgsDeserialized) {
@@ -1095,17 +1279,21 @@ export default class PileupRenderer extends BoxRendererType {
1095
1279
 
1096
1280
  const width = (end - start) / bpPerPx
1097
1281
  const height = Math.max(layout.getTotalHeight(), 1)
1098
-
1099
1282
  const res = await renderToAbstractCanvas(
1100
1283
  width,
1101
1284
  height,
1102
1285
  renderProps,
1103
1286
  (ctx: CanvasRenderingContext2D) =>
1104
- this.makeImageData(ctx, layoutRecords, {
1105
- ...renderProps,
1106
- layout,
1107
- features,
1108
- regionSequence,
1287
+ this.makeImageData({
1288
+ ctx,
1289
+ layoutRecords,
1290
+ canvasWidth: width,
1291
+ renderArgs: {
1292
+ ...renderProps,
1293
+ layout,
1294
+ features,
1295
+ regionSequence,
1296
+ },
1109
1297
  }),
1110
1298
  )
1111
1299