@jbrowse/plugin-alignments 1.6.6 → 1.6.9

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 (32) hide show
  1. package/dist/BamAdapter/BamSlightlyLazyFeature.d.ts +1 -10
  2. package/dist/BamAdapter/MismatchParser.d.ts +3 -5
  3. package/dist/CramAdapter/CramSlightlyLazyFeature.d.ts +1 -2
  4. package/dist/LinearSNPCoverageDisplay/models/model.d.ts +2 -2
  5. package/dist/PileupRenderer/PileupRenderer.d.ts +20 -6
  6. package/dist/SNPCoverageAdapter/SNPCoverageAdapter.d.ts +3 -11
  7. package/dist/plugin-alignments.cjs.development.js +591 -552
  8. package/dist/plugin-alignments.cjs.development.js.map +1 -1
  9. package/dist/plugin-alignments.cjs.production.min.js +1 -1
  10. package/dist/plugin-alignments.cjs.production.min.js.map +1 -1
  11. package/dist/plugin-alignments.esm.js +594 -555
  12. package/dist/plugin-alignments.esm.js.map +1 -1
  13. package/dist/util.d.ts +4 -0
  14. package/package.json +3 -3
  15. package/src/BamAdapter/BamAdapter.ts +10 -7
  16. package/src/BamAdapter/BamSlightlyLazyFeature.ts +11 -79
  17. package/src/BamAdapter/MismatchParser.test.ts +53 -297
  18. package/src/BamAdapter/MismatchParser.ts +54 -116
  19. package/src/BamAdapter/configSchema.ts +0 -4
  20. package/src/CramAdapter/CramSlightlyLazyFeature.ts +3 -10
  21. package/src/LinearAlignmentsDisplay/models/model.tsx +4 -6
  22. package/src/LinearPileupDisplay/components/ColorByModifications.tsx +76 -80
  23. package/src/LinearPileupDisplay/components/ColorByTag.tsx +24 -23
  24. package/src/LinearPileupDisplay/components/FilterByTag.tsx +73 -68
  25. package/src/LinearPileupDisplay/components/SetFeatureHeight.tsx +28 -26
  26. package/src/LinearPileupDisplay/components/SetMaxHeight.tsx +24 -13
  27. package/src/LinearPileupDisplay/components/SortByTag.tsx +29 -21
  28. package/src/LinearPileupDisplay/model.ts +6 -0
  29. package/src/PileupRenderer/PileupRenderer.tsx +178 -57
  30. package/src/SNPCoverageAdapter/SNPCoverageAdapter.ts +180 -229
  31. package/src/SNPCoverageRenderer/SNPCoverageRenderer.ts +12 -11
  32. package/src/util.ts +25 -0
@@ -1,5 +1,4 @@
1
- import { AnyConfigurationModel } from '@jbrowse/core/configuration/configurationSchema'
2
- import { toArray } from 'rxjs/operators'
1
+ import Color from 'color'
3
2
  import BoxRendererType, {
4
3
  RenderArgs,
5
4
  RenderArgsSerialized,
@@ -10,14 +9,20 @@ import BoxRendererType, {
10
9
  } from '@jbrowse/core/pluggableElementTypes/renderers/BoxRendererType'
11
10
  import { Theme } from '@material-ui/core'
12
11
  import { createJBrowseTheme } from '@jbrowse/core/ui'
13
- import { Feature } from '@jbrowse/core/util/simpleFeature'
14
- import { bpSpanPx, iterMap } from '@jbrowse/core/util'
15
- import Color from 'color'
16
- import { Region } from '@jbrowse/core/util/types'
12
+ import {
13
+ bpSpanPx,
14
+ iterMap,
15
+ measureText,
16
+ Region,
17
+ Feature,
18
+ } from '@jbrowse/core/util'
17
19
  import { renderToAbstractCanvas } from '@jbrowse/core/util/offscreenCanvasUtils'
18
20
  import { BaseLayout } from '@jbrowse/core/util/layouts/BaseLayout'
19
21
  import { getAdapter } from '@jbrowse/core/data_adapters/dataAdapterCache'
20
- import { readConfObject } from '@jbrowse/core/configuration'
22
+ import {
23
+ readConfObject,
24
+ AnyConfigurationModel,
25
+ } from '@jbrowse/core/configuration'
21
26
 
22
27
  // locals
23
28
  import {
@@ -27,7 +32,12 @@ import {
27
32
  getNextRefPos,
28
33
  } from '../BamAdapter/MismatchParser'
29
34
  import { sortFeature } from './sortUtil'
30
- import { getTagAlt, orientationTypes } from '../util'
35
+ import {
36
+ getTagAlt,
37
+ orientationTypes,
38
+ fetchSequence,
39
+ shouldFetchReferenceSequence,
40
+ } from '../util'
31
41
  import {
32
42
  PileupLayoutSession,
33
43
  PileupLayoutSessionProps,
@@ -44,6 +54,15 @@ function getColorBaseMap(theme: Theme) {
44
54
  }
45
55
  }
46
56
 
57
+ function getContrastBaseMap(theme: Theme) {
58
+ return Object.fromEntries(
59
+ Object.entries(getColorBaseMap(theme)).map(([key, value]) => [
60
+ key,
61
+ theme.palette.getContrastText(value),
62
+ ]),
63
+ )
64
+ }
65
+
47
66
  export interface RenderArgsDeserialized extends BoxRenderArgsDeserialized {
48
67
  colorBy?: { type: string; tag?: string }
49
68
  colorTagMap?: Record<string, string>
@@ -263,6 +282,64 @@ export default class PileupRenderer extends BoxRendererType {
263
282
  return strand === 1 ? 'color_fwd_strand' : 'color_rev_strand'
264
283
  }
265
284
 
285
+ colorByPerBaseLettering(
286
+ ctx: CanvasRenderingContext2D,
287
+ feat: LayoutFeature,
288
+ _config: AnyConfigurationModel,
289
+ region: Region,
290
+ bpPerPx: number,
291
+ props: {
292
+ colorForBase: Record<string, string>
293
+ contrastForBase: Record<string, string>
294
+ charWidth: number
295
+ charHeight: number
296
+ },
297
+ ) {
298
+ const { colorForBase, contrastForBase, charWidth, charHeight } = props
299
+ const heightLim = charHeight - 2
300
+ const { feature, topPx, heightPx } = feat
301
+ const seq = feature.get('seq') as string
302
+ const cigarOps = parseCigar(feature.get('CIGAR'))
303
+ const widthPx = 1 / bpPerPx
304
+ const start = feature.get('start')
305
+ let soffset = 0 // sequence offset
306
+ let roffset = 0 // reference offset
307
+
308
+ for (let i = 0; i < cigarOps.length; i += 2) {
309
+ const len = +cigarOps[i]
310
+ const op = cigarOps[i + 1]
311
+ if (op === 'S' || op === 'I') {
312
+ soffset += len
313
+ } else if (op === 'D' || op === 'N') {
314
+ roffset += len
315
+ } else if (op === 'M' || op === 'X' || op === '=') {
316
+ for (let m = 0; m < len; m++) {
317
+ const letter = seq[soffset + m]
318
+ ctx.fillStyle = colorForBase[letter]
319
+ const [leftPx] = bpSpanPx(
320
+ start + roffset + m,
321
+ start + roffset + m + 1,
322
+ region,
323
+ bpPerPx,
324
+ )
325
+ ctx.fillRect(leftPx, topPx, widthPx + 0.5, heightPx)
326
+
327
+ if (widthPx >= charWidth && heightPx >= heightLim) {
328
+ // normal SNP coloring
329
+ ctx.fillStyle = contrastForBase[letter]
330
+
331
+ ctx.fillText(
332
+ letter,
333
+ leftPx + (widthPx - charWidth) / 2 + 1,
334
+ topPx + heightPx,
335
+ )
336
+ }
337
+ }
338
+ soffset += len
339
+ roffset += len
340
+ }
341
+ }
342
+ }
266
343
  colorByPerBaseQuality(
267
344
  ctx: CanvasRenderingContext2D,
268
345
  feat: LayoutFeature,
@@ -276,27 +353,30 @@ export default class PileupRenderer extends BoxRendererType {
276
353
  const cigarOps = parseCigar(feature.get('CIGAR'))
277
354
  const width = 1 / bpPerPx
278
355
  const start = feature.get('start')
356
+ let soffset = 0 // sequence offset
357
+ let roffset = 0 // reference offset
279
358
 
280
- for (let i = 0, j = 0, k = 0; k < scores.length; i += 2, k++) {
359
+ for (let i = 0; i < cigarOps.length; i += 2) {
281
360
  const len = +cigarOps[i]
282
361
  const op = cigarOps[i + 1]
283
362
  if (op === 'S' || op === 'I') {
284
- k += len
363
+ soffset += len
285
364
  } else if (op === 'D' || op === 'N') {
286
- j += len
365
+ roffset += len
287
366
  } else if (op === 'M' || op === 'X' || op === '=') {
288
367
  for (let m = 0; m < len; m++) {
289
- const score = scores[k + m]
368
+ const score = scores[soffset + m]
290
369
  ctx.fillStyle = `hsl(${score === 255 ? 150 : score * 1.5},55%,50%)`
291
370
  const [leftPx] = bpSpanPx(
292
- start + j + m,
293
- start + j + m + 1,
371
+ start + roffset + m,
372
+ start + roffset + m + 1,
294
373
  region,
295
374
  bpPerPx,
296
375
  )
297
376
  ctx.fillRect(leftPx, topPx, width + 0.5, heightPx)
298
377
  }
299
- j += len
378
+ soffset += len
379
+ roffset += len
300
380
  }
301
381
  }
302
382
  }
@@ -372,8 +452,8 @@ export default class PileupRenderer extends BoxRendererType {
372
452
  }
373
453
 
374
454
  // Color by methylation is slightly modified version of color by
375
- // modifications
376
- //
455
+ // modifications that focuses on CpG sites, with non-methylated CpG colored
456
+ // blue
377
457
  colorByMethylation(
378
458
  ctx: CanvasRenderingContext2D,
379
459
  layoutFeature: LayoutFeature,
@@ -503,6 +583,10 @@ export default class PileupRenderer extends BoxRendererType {
503
583
  ctx: CanvasRenderingContext2D,
504
584
  feat: LayoutFeature,
505
585
  props: RenderArgsDeserializedWithFeaturesAndLayout & {
586
+ colorForBase: Record<string, string>
587
+ contrastForBase: Record<string, string>
588
+ charWidth: number
589
+ charHeight: number
506
590
  defaultColor: boolean
507
591
  },
508
592
  ) {
@@ -513,6 +597,10 @@ export default class PileupRenderer extends BoxRendererType {
513
597
  regions,
514
598
  colorBy,
515
599
  colorTagMap = {},
600
+ colorForBase,
601
+ contrastForBase,
602
+ charWidth,
603
+ charHeight,
516
604
  } = props
517
605
  const { tag = '', type: colorType = '' } = colorBy || {}
518
606
  const { feature } = feat
@@ -577,6 +665,19 @@ export default class PileupRenderer extends BoxRendererType {
577
665
  case 'insertSizeAndPairOrientation':
578
666
  break
579
667
 
668
+ case 'modifications':
669
+ case 'methylation':
670
+ // this coloring is similar to igv.js, and is helpful to color negative
671
+ // strand reads differently because their c-g will be flipped (e.g. g-c
672
+ // read right to left)
673
+ const flags = feature.get('flags')
674
+ if (flags & 16) {
675
+ ctx.fillStyle = '#c8dcc8'
676
+ } else {
677
+ ctx.fillStyle = '#c8c8c8'
678
+ }
679
+ break
680
+
580
681
  case 'normal':
581
682
  default:
582
683
  if (defaultColor) {
@@ -597,6 +698,15 @@ export default class PileupRenderer extends BoxRendererType {
597
698
  this.colorByPerBaseQuality(ctx, feat, config, region, bpPerPx)
598
699
  break
599
700
 
701
+ case 'perBaseLettering':
702
+ this.colorByPerBaseLettering(ctx, feat, config, region, bpPerPx, {
703
+ colorForBase,
704
+ contrastForBase,
705
+ charWidth,
706
+ charHeight,
707
+ })
708
+ break
709
+
600
710
  case 'modifications':
601
711
  this.colorByModifications(ctx, feat, config, region, bpPerPx, props)
602
712
  break
@@ -611,9 +721,9 @@ export default class PileupRenderer extends BoxRendererType {
611
721
  ctx: CanvasRenderingContext2D,
612
722
  feat: LayoutFeature,
613
723
  props: RenderArgsDeserializedWithFeaturesAndLayout,
614
- theme: Theme,
615
- colorForBase: { [key: string]: string },
616
724
  opts: {
725
+ colorForBase: { [key: string]: string }
726
+ contrastForBase: { [key: string]: string }
617
727
  mismatchAlpha?: boolean
618
728
  drawSNPs?: boolean
619
729
  drawIndels?: boolean
@@ -624,13 +734,15 @@ export default class PileupRenderer extends BoxRendererType {
624
734
  },
625
735
  ) {
626
736
  const {
627
- minSubfeatureWidth: minWidth,
737
+ minSubfeatureWidth,
628
738
  largeInsertionIndicatorScale,
629
739
  mismatchAlpha,
630
740
  drawSNPs = true,
631
741
  drawIndels = true,
632
742
  charWidth,
633
743
  charHeight,
744
+ colorForBase,
745
+ contrastForBase,
634
746
  } = opts
635
747
  const { bpPerPx, regions } = props
636
748
  const { heightPx, topPx, feature } = feat
@@ -638,7 +750,7 @@ export default class PileupRenderer extends BoxRendererType {
638
750
  const start = feature.get('start')
639
751
 
640
752
  const pxPerBp = Math.min(1 / bpPerPx, 2)
641
- const w = Math.max(minWidth, pxPerBp)
753
+ const w = Math.max(minSubfeatureWidth, pxPerBp)
642
754
  const mismatches: Mismatch[] = feature.get('mismatches')
643
755
  const heightLim = charHeight - 2
644
756
 
@@ -667,7 +779,7 @@ export default class PileupRenderer extends BoxRendererType {
667
779
  const mlen = mismatch.length
668
780
  const mbase = mismatch.base
669
781
  const [leftPx, rightPx] = bpSpanPx(mstart, mstart + mlen, region, bpPerPx)
670
- const widthPx = Math.max(minWidth, Math.abs(leftPx - rightPx))
782
+ const widthPx = Math.max(minSubfeatureWidth, Math.abs(leftPx - rightPx))
671
783
  if (mismatch.type === 'mismatch' && drawSNPs) {
672
784
  const baseColor = colorForBase[mismatch.base] || '#888'
673
785
 
@@ -678,7 +790,7 @@ export default class PileupRenderer extends BoxRendererType {
678
790
  if (widthPx >= charWidth && heightPx >= heightLim) {
679
791
  // normal SNP coloring
680
792
  ctx.fillStyle = getAlphaColor(
681
- theme.palette.getContrastText(baseColor),
793
+ contrastForBase[mismatch.base],
682
794
  mismatch,
683
795
  )
684
796
  ctx.fillText(
@@ -692,12 +804,12 @@ export default class PileupRenderer extends BoxRendererType {
692
804
  ctx.fillStyle = baseColor
693
805
  ctx.fillRect(leftPx, topPx, widthPx, heightPx)
694
806
  const txt = `${mismatch.length}`
695
- const rect = ctx.measureText(txt)
696
- if (widthPx >= rect.width && heightPx >= heightLim) {
697
- ctx.fillStyle = theme.palette.getContrastText(baseColor)
807
+ const rwidth = measureText(txt, 10)
808
+ if (widthPx >= rwidth && heightPx >= heightLim) {
809
+ ctx.fillStyle = contrastForBase.deletion
698
810
  ctx.fillText(
699
811
  txt,
700
- leftPx + (rightPx - leftPx) / 2 - rect.width / 2,
812
+ (leftPx + rightPx) / 2 - rwidth / 2,
701
813
  topPx + heightPx,
702
814
  )
703
815
  }
@@ -705,14 +817,12 @@ export default class PileupRenderer extends BoxRendererType {
705
817
  ctx.fillStyle = 'purple'
706
818
  const pos = leftPx + extraHorizontallyFlippedOffset
707
819
  const len = +mismatch.base || mismatch.length
708
- const insW = Math.max(minWidth, Math.min(1.2, 1 / bpPerPx))
820
+ const insW = Math.max(minSubfeatureWidth, Math.min(1.2, 1 / bpPerPx))
709
821
  if (len < 10) {
710
822
  ctx.fillRect(pos, topPx, insW, heightPx)
711
- if (1 / bpPerPx >= charWidth) {
823
+ if (1 / bpPerPx >= charWidth && heightPx >= heightLim) {
712
824
  ctx.fillRect(pos - insW, topPx, insW * 3, 1)
713
825
  ctx.fillRect(pos - insW, topPx + heightPx - 1, insW * 3, 1)
714
- }
715
- if (1 / bpPerPx >= charWidth && heightPx >= heightLim) {
716
826
  ctx.fillText(`(${mismatch.base})`, pos + 3, topPx + heightPx)
717
827
  }
718
828
  }
@@ -720,11 +830,9 @@ export default class PileupRenderer extends BoxRendererType {
720
830
  ctx.fillStyle = mismatch.type === 'hardclip' ? 'red' : 'blue'
721
831
  const pos = leftPx + extraHorizontallyFlippedOffset
722
832
  ctx.fillRect(pos, topPx, w, heightPx)
723
- if (1 / bpPerPx >= charWidth) {
833
+ if (1 / bpPerPx >= charWidth && heightPx >= heightLim) {
724
834
  ctx.fillRect(pos - w, topPx, w * 3, 1)
725
835
  ctx.fillRect(pos - w, topPx + heightPx - 1, w * 3, 1)
726
- }
727
- if (widthPx >= charWidth && heightPx >= heightLim) {
728
836
  ctx.fillText(`(${mismatch.base})`, pos + 3, topPx + heightPx)
729
837
  }
730
838
  } else if (mismatch.type === 'skip') {
@@ -865,6 +973,7 @@ export default class PileupRenderer extends BoxRendererType {
865
973
 
866
974
  const theme = createJBrowseTheme(configTheme)
867
975
  const colorForBase = getColorBaseMap(theme)
976
+ const contrastForBase = getContrastBaseMap(theme)
868
977
  if (!layout) {
869
978
  throw new Error(`layout required`)
870
979
  }
@@ -882,8 +991,12 @@ export default class PileupRenderer extends BoxRendererType {
882
991
  this.drawAlignmentRect(ctx, feat, {
883
992
  ...props,
884
993
  defaultColor,
994
+ colorForBase,
995
+ contrastForBase,
996
+ charWidth,
997
+ charHeight,
885
998
  })
886
- this.drawMismatches(ctx, feat, props, theme, colorForBase, {
999
+ this.drawMismatches(ctx, feat, props, {
887
1000
  mismatchAlpha,
888
1001
  drawSNPs: shouldDrawMismatches(colorBy?.type),
889
1002
  drawIndels: shouldDrawMismatches(colorBy?.type),
@@ -891,6 +1004,8 @@ export default class PileupRenderer extends BoxRendererType {
891
1004
  minSubfeatureWidth,
892
1005
  charWidth,
893
1006
  charHeight,
1007
+ colorForBase,
1008
+ contrastForBase,
894
1009
  })
895
1010
  if (showSoftClip) {
896
1011
  this.drawSoftClipping(ctx, feat, props, config, theme)
@@ -942,34 +1057,40 @@ export default class PileupRenderer extends BoxRendererType {
942
1057
  return layoutRecords
943
1058
  }
944
1059
 
945
- async render(renderProps: RenderArgsDeserialized) {
946
- const { sessionId, bpPerPx, regions, adapterConfig } = renderProps
1060
+ async fetchSequence(renderProps: RenderArgsDeserialized) {
1061
+ const { sessionId, regions, adapterConfig } = renderProps
947
1062
  const { sequenceAdapter } = adapterConfig
1063
+ if (!sequenceAdapter) {
1064
+ return undefined
1065
+ }
1066
+ const { dataAdapter } = await getAdapter(
1067
+ this.pluginManager,
1068
+ sessionId,
1069
+ sequenceAdapter,
1070
+ )
1071
+ const [region] = regions
1072
+ return fetchSequence(region, dataAdapter as BaseFeatureDataAdapter)
1073
+ }
1074
+
1075
+ async render(renderProps: RenderArgsDeserialized) {
948
1076
  const features = await this.getFeatures(renderProps)
949
1077
  const layout = this.createLayoutInWorker(renderProps)
1078
+ const { regions, bpPerPx } = renderProps
950
1079
 
951
- const layoutRecords = this.layoutFeats({ ...renderProps, features, layout })
1080
+ const layoutRecords = this.layoutFeats({
1081
+ ...renderProps,
1082
+ features,
1083
+ layout,
1084
+ })
952
1085
  const [region] = regions
953
- let regionSequence: string | undefined
954
- const { end, start, originalRefName, refName } = region
955
-
956
- if (sequenceAdapter) {
957
- const { dataAdapter } = await getAdapter(
958
- this.pluginManager,
959
- sessionId,
960
- sequenceAdapter,
961
- )
962
1086
 
963
- const feats = await (dataAdapter as BaseFeatureDataAdapter)
964
- .getFeatures({
965
- ...region,
966
- refName: originalRefName || refName,
967
- end: region.end + 1,
968
- })
969
- .pipe(toArray())
970
- .toPromise()
971
- regionSequence = feats[0]?.get('seq')
972
- }
1087
+ // only need reference sequence if there are features and only for some
1088
+ // cases
1089
+ const regionSequence =
1090
+ features.size && shouldFetchReferenceSequence(renderProps.colorBy?.type)
1091
+ ? await this.fetchSequence(renderProps)
1092
+ : undefined
1093
+ const { end, start } = region
973
1094
 
974
1095
  const width = (end - start) / bpPerPx
975
1096
  const height = Math.max(layout.getTotalHeight(), 1)