@jbrowse/plugin-alignments 1.6.4 → 1.6.7

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 (35) hide show
  1. package/dist/AlignmentsFeatureDetail/index.d.ts +1 -1
  2. package/dist/BamAdapter/BamSlightlyLazyFeature.d.ts +3 -2
  3. package/dist/BamAdapter/configSchema.d.ts +1 -1
  4. package/dist/CramAdapter/configSchema.d.ts +1 -1
  5. package/dist/HtsgetBamAdapter/configSchema.d.ts +1 -1
  6. package/dist/LinearAlignmentsDisplay/models/configSchema.d.ts +1 -1
  7. package/dist/LinearAlignmentsDisplay/models/model.d.ts +1 -1
  8. package/dist/LinearPileupDisplay/configSchema.d.ts +1 -1
  9. package/dist/LinearSNPCoverageDisplay/components/Tooltip.d.ts +1 -1
  10. package/dist/LinearSNPCoverageDisplay/models/configSchema.d.ts +1 -1
  11. package/dist/PileupRenderer/configSchema.d.ts +1 -1
  12. package/dist/SNPCoverageAdapter/configSchema.d.ts +1 -1
  13. package/dist/SNPCoverageRenderer/SNPCoverageRenderer.d.ts +1 -1
  14. package/dist/SNPCoverageRenderer/configSchema.d.ts +1 -1
  15. package/dist/SNPCoverageRenderer/index.d.ts +1 -1
  16. package/dist/plugin-alignments.cjs.development.js +291 -223
  17. package/dist/plugin-alignments.cjs.development.js.map +1 -1
  18. package/dist/plugin-alignments.cjs.production.min.js +1 -1
  19. package/dist/plugin-alignments.cjs.production.min.js.map +1 -1
  20. package/dist/plugin-alignments.esm.js +291 -223
  21. package/dist/plugin-alignments.esm.js.map +1 -1
  22. package/package.json +6 -6
  23. package/src/AlignmentsFeatureDetail/AlignmentsFeatureDetail.tsx +23 -14
  24. package/src/BamAdapter/BamAdapter.ts +3 -4
  25. package/src/BamAdapter/BamSlightlyLazyFeature.ts +8 -4
  26. package/src/LinearAlignmentsDisplay/components/AlignmentsDisplay.tsx +38 -30
  27. package/src/LinearAlignmentsDisplay/models/model.tsx +10 -9
  28. package/src/LinearPileupDisplay/model.ts +6 -6
  29. package/src/LinearSNPCoverageDisplay/components/Tooltip.tsx +5 -3
  30. package/src/LinearSNPCoverageDisplay/models/configSchema.ts +4 -5
  31. package/src/PileupRenderer/PileupRenderer.tsx +39 -27
  32. package/src/PileupRenderer/components/PileupRendering.tsx +5 -3
  33. package/src/SNPCoverageAdapter/SNPCoverageAdapter.ts +188 -169
  34. package/src/SNPCoverageRenderer/SNPCoverageRenderer.ts +86 -56
  35. package/src/SNPCoverageRenderer/configSchema.js +1 -1
@@ -9,7 +9,7 @@ import SerializableFilterChain from '@jbrowse/core/pluggableElementTypes/rendere
9
9
  import { ObservableCreate } from '@jbrowse/core/util/rxjs'
10
10
  import { reduce, filter, toArray } from 'rxjs/operators'
11
11
  import { Observable } from 'rxjs'
12
- import { getTagAlt } from '../util'
12
+ import { getTag, getTagAlt } from '../util'
13
13
  import {
14
14
  parseCigar,
15
15
  getNextRefPos,
@@ -88,20 +88,18 @@ export default class SNPCoverageAdapter extends BaseFeatureDataAdapter {
88
88
  )
89
89
 
90
90
  bins.forEach((bin, index) => {
91
- if (bin.total) {
92
- observer.next(
93
- new SimpleFeature({
94
- id: `${this.id}-${region.start}-${index}`,
95
- data: {
96
- score: bin.total,
97
- snpinfo: bin,
98
- start: region.start + index,
99
- end: region.start + index + 1,
100
- refName: region.refName,
101
- },
102
- }),
103
- )
104
- }
91
+ observer.next(
92
+ new SimpleFeature({
93
+ id: `${this.id}-${region.start}-${index}`,
94
+ data: {
95
+ score: bin.total,
96
+ snpinfo: bin,
97
+ start: region.start + index,
98
+ end: region.start + index + 1,
99
+ refName: region.refName,
100
+ },
101
+ }),
102
+ )
105
103
  })
106
104
 
107
105
  // make fake features from the coverage
@@ -171,14 +169,6 @@ export default class SNPCoverageAdapter extends BaseFeatureDataAdapter {
171
169
  // delskips are elements that don't contribute to coverage, but should be
172
170
  // reported also (and are not interbase)
173
171
  type BinType = { total: number; strands: { [key: string]: number } }
174
- const initBins = Array.from({ length: binMax }, () => ({
175
- total: 0,
176
- lowqual: {} as BinType,
177
- cov: {} as BinType,
178
- delskips: {} as BinType,
179
- noncov: {} as BinType,
180
- ref: {} as BinType,
181
- }))
182
172
 
183
173
  // request an extra +1 on the end to get CpG crossing region boundary
184
174
  let regionSeq: string | undefined
@@ -189,7 +179,7 @@ export default class SNPCoverageAdapter extends BaseFeatureDataAdapter {
189
179
  refName: originalRefName || refName,
190
180
  start,
191
181
  end: end + 1,
192
- assemblyName: 'na',
182
+ assemblyName: region.assemblyName,
193
183
  })
194
184
  .pipe(toArray())
195
185
  .toPromise()
@@ -198,172 +188,201 @@ export default class SNPCoverageAdapter extends BaseFeatureDataAdapter {
198
188
 
199
189
  const bins = await features
200
190
  .pipe(
201
- reduce((bins, feature) => {
202
- const cigar = feature.get('CIGAR')
203
- const fstart = feature.get('start')
204
- const fend = feature.get('end')
205
- const fstrand = feature.get('strand')
206
- const cigarOps = parseCigar(cigar)
207
-
208
- for (let j = fstart; j < fend; j++) {
209
- const i = j - region.start
210
- if (i >= 0 && i < bins.length) {
211
- const bin = bins[i]
212
- bin.total++
213
- inc(bin, fstrand, 'ref', 'ref')
191
+ reduce(
192
+ (bins, feature) => {
193
+ const cigar = feature.get('CIGAR')
194
+ const fstart = feature.get('start')
195
+ const fend = feature.get('end')
196
+ const fstrand = feature.get('strand')
197
+ const cigarOps = parseCigar(cigar)
198
+
199
+ for (let j = fstart; j < fend + 1; j++) {
200
+ const i = j - region.start
201
+ if (i >= 0 && i < binMax) {
202
+ const bin = bins[i] || {
203
+ total: 0,
204
+ lowqual: {} as BinType,
205
+ cov: {} as BinType,
206
+ delskips: {} as BinType,
207
+ noncov: {} as BinType,
208
+ ref: {} as BinType,
209
+ }
210
+ if (j !== fend) {
211
+ bin.total++
212
+ inc(bin, fstrand, 'ref', 'ref')
213
+ }
214
+ bins[i] = bin
215
+ }
214
216
  }
215
- }
216
-
217
- if (colorBy?.type === 'modifications') {
218
- const seq = feature.get('seq')
219
- const mm = getTagAlt(feature, 'MM', 'Mm') || ''
220
-
221
- const ml =
222
- (getTagAlt(feature, 'ML', 'Ml') as number[] | string) || []
223
217
 
224
- const probabilities = ml
225
- ? (typeof ml === 'string' ? ml.split(',').map(e => +e) : ml).map(
226
- e => e / 255,
227
- )
228
- : (getTagAlt(feature, 'MP', 'Mp') as string)
229
- .split('')
230
- .map(s => s.charCodeAt(0) - 33)
231
- .map(elt => Math.min(1, elt / 50))
232
-
233
- let probIndex = 0
234
- getModificationPositions(mm, seq, fstrand).forEach(
235
- ({ type, positions }) => {
236
- const mod = `mod_${type}`
237
- for (const pos of getNextRefPos(cigarOps, positions)) {
238
- const epos = pos + fstart - region.start
239
- if (epos >= 0 && epos < bins.length && pos + fstart < fend) {
240
- const bin = bins[epos]
241
- if (probabilities[probIndex] > 0.5) {
242
- inc(bin, fstrand, 'cov', mod)
243
- } else {
244
- inc(bin, fstrand, 'lowqual', mod)
218
+ if (colorBy?.type === 'modifications') {
219
+ const seq = feature.get('seq')
220
+ const mm = getTagAlt(feature, 'MM', 'Mm') || ''
221
+
222
+ const ml =
223
+ (getTagAlt(feature, 'ML', 'Ml') as number[] | string) || []
224
+
225
+ const probabilities = ml
226
+ ? (typeof ml === 'string'
227
+ ? ml.split(',').map(e => +e)
228
+ : ml
229
+ ).map(e => e / 255)
230
+ : (getTagAlt(feature, 'MP', 'Mp') as string)
231
+ .split('')
232
+ .map(s => s.charCodeAt(0) - 33)
233
+ .map(elt => Math.min(1, elt / 50))
234
+
235
+ let probIndex = 0
236
+ getModificationPositions(mm, seq, fstrand).forEach(
237
+ ({ type, positions }) => {
238
+ const mod = `mod_${type}`
239
+ for (const pos of getNextRefPos(cigarOps, positions)) {
240
+ const epos = pos + fstart - region.start
241
+ if (
242
+ epos >= 0 &&
243
+ epos < bins.length &&
244
+ pos + fstart < fend
245
+ ) {
246
+ const bin = bins[epos] || {
247
+ total: 0,
248
+ lowqual: {} as BinType,
249
+ cov: {} as BinType,
250
+ delskips: {} as BinType,
251
+ noncov: {} as BinType,
252
+ ref: {} as BinType,
253
+ }
254
+
255
+ if (probabilities[probIndex] > 0.5) {
256
+ inc(bin, fstrand, 'cov', mod)
257
+ } else {
258
+ inc(bin, fstrand, 'lowqual', mod)
259
+ }
245
260
  }
261
+ probIndex++
246
262
  }
247
- probIndex++
248
- }
249
- },
250
- )
251
- }
252
-
253
- // methylation based coloring takes into account both reference
254
- // sequence CpG detection and reads
255
- else if (colorBy?.type === 'methylation') {
256
- if (!regionSeq) {
257
- throw new Error(
258
- 'no region sequence detected, need sequenceAdapter configuration',
263
+ },
259
264
  )
260
265
  }
261
- const seq = feature.get('seq')
262
- const mm = getTagAlt(feature, 'MM', 'Mm') || ''
263
- const methBins = new Array(region.end - region.start).fill(0)
264
-
265
- getModificationPositions(mm, seq, fstrand).forEach(
266
- ({ type, positions }) => {
267
- // we are processing methylation
268
- if (type === 'm') {
269
- for (const pos of getNextRefPos(cigarOps, positions)) {
270
- const epos = pos + fstart - region.start
271
- if (epos >= 0 && epos < methBins.length) {
272
- methBins[epos] = 1
266
+
267
+ // methylation based coloring takes into account both reference
268
+ // sequence CpG detection and reads
269
+ else if (colorBy?.type === 'methylation') {
270
+ if (!regionSeq) {
271
+ throw new Error(
272
+ 'no region sequence detected, need sequenceAdapter configuration',
273
+ )
274
+ }
275
+ const seq = feature.get('seq')
276
+ const mm = getTagAlt(feature, 'MM', 'Mm') || ''
277
+ const methBins = new Array(region.end - region.start).fill(0)
278
+
279
+ getModificationPositions(mm, seq, fstrand).forEach(
280
+ ({ type, positions }) => {
281
+ // we are processing methylation
282
+ if (type === 'm') {
283
+ for (const pos of getNextRefPos(cigarOps, positions)) {
284
+ const epos = pos + fstart - region.start
285
+ if (epos >= 0 && epos < methBins.length) {
286
+ methBins[epos] = 1
287
+ }
273
288
  }
274
289
  }
275
- }
276
- },
277
- )
290
+ },
291
+ )
278
292
 
279
- for (let j = fstart; j < fend; j++) {
280
- const i = j - region.start
281
- if (i >= 0 && i < bins.length - 1) {
282
- const l1 = regionSeq[i].toLowerCase()
283
- const l2 = regionSeq[i + 1].toLowerCase()
284
- const bin = bins[i]
285
- const bin1 = bins[i + 1]
286
-
287
- // color
288
- if (l1 === 'c' && l2 === 'g') {
289
- if (methBins[i] || methBins[i + 1]) {
290
- inc(bin, fstrand, 'cov', 'meth')
291
- inc(bin1, fstrand, 'cov', 'meth')
292
- dec(bin, fstrand, 'ref', 'ref')
293
- dec(bin1, fstrand, 'ref', 'ref')
294
- } else {
295
- inc(bin, fstrand, 'cov', 'unmeth')
296
- inc(bin1, fstrand, 'cov', 'unmeth')
297
- dec(bin, fstrand, 'ref', 'ref')
298
- dec(bin1, fstrand, 'ref', 'ref')
293
+ for (let j = fstart; j < fend; j++) {
294
+ const i = j - region.start
295
+ if (i >= 0 && i < bins.length - 1) {
296
+ const l1 = regionSeq[i].toLowerCase()
297
+ const l2 = regionSeq[i + 1].toLowerCase()
298
+ const bin = bins[i]
299
+ const bin1 = bins[i + 1]
300
+
301
+ // color
302
+ if (l1 === 'c' && l2 === 'g') {
303
+ if (methBins[i] || methBins[i + 1]) {
304
+ inc(bin, fstrand, 'cov', 'meth')
305
+ inc(bin1, fstrand, 'cov', 'meth')
306
+ dec(bin, fstrand, 'ref', 'ref')
307
+ dec(bin1, fstrand, 'ref', 'ref')
308
+ } else {
309
+ inc(bin, fstrand, 'cov', 'unmeth')
310
+ inc(bin1, fstrand, 'cov', 'unmeth')
311
+ dec(bin, fstrand, 'ref', 'ref')
312
+ dec(bin1, fstrand, 'ref', 'ref')
313
+ }
299
314
  }
300
315
  }
301
316
  }
302
317
  }
303
- }
304
-
305
- // normal SNP based coloring
306
- else {
307
- const mismatches = feature.get('mismatches') as
308
- | Mismatch[]
309
- | undefined
310
-
311
- if (mismatches) {
312
- for (let i = 0; i < mismatches.length; i++) {
313
- const mismatch = mismatches[i]
314
- const mstart = fstart + mismatch.start
315
- for (let j = mstart; j < mstart + mismatchLen(mismatch); j++) {
316
- const epos = j - region.start
317
- if (epos >= 0 && epos < bins.length) {
318
- const bin = bins[epos]
319
- const { base, type } = mismatch
320
- const interbase = isInterbase(type)
321
- if (!interbase) {
322
- dec(bin, fstrand, 'ref', 'ref')
323
- } else {
324
- inc(bin, fstrand, 'noncov', type)
325
- }
326
318
 
327
- if (type === 'deletion' || type === 'skip') {
328
- inc(bin, fstrand, 'delskips', type)
329
- bin.total--
330
- } else if (!interbase) {
331
- inc(bin, fstrand, 'cov', base)
319
+ // normal SNP based coloring
320
+ else {
321
+ const mismatches = feature.get('mismatches') as
322
+ | Mismatch[]
323
+ | undefined
324
+
325
+ if (mismatches) {
326
+ for (let i = 0; i < mismatches.length; i++) {
327
+ const mismatch = mismatches[i]
328
+ const ms = fstart + mismatch.start
329
+ for (let j = ms; j < ms + mismatchLen(mismatch); j++) {
330
+ const epos = j - region.start
331
+ if (epos >= 0 && epos < bins.length) {
332
+ const bin = bins[epos]
333
+ const { base, type } = mismatch
334
+ const interbase = isInterbase(type)
335
+ if (!interbase) {
336
+ dec(bin, fstrand, 'ref', 'ref')
337
+ } else {
338
+ inc(bin, fstrand, 'noncov', type)
339
+ }
340
+
341
+ if (type === 'deletion' || type === 'skip') {
342
+ inc(bin, fstrand, 'delskips', type)
343
+ bin.total--
344
+ } else if (!interbase) {
345
+ inc(bin, fstrand, 'cov', base)
346
+ }
332
347
  }
333
348
  }
334
349
  }
335
- }
336
350
 
337
- mismatches
338
- .filter(mismatch => mismatch.type === 'skip')
339
- .forEach(mismatch => {
340
- const mstart = feature.get('start') + mismatch.start
341
- const start = mstart
342
- const end = mstart + mismatch.length
343
- const strand = feature.get('strand')
344
- const hash = `${start}_${end}_${strand}`
345
- if (!skipmap[hash]) {
346
- skipmap[hash] = {
347
- feature: feature,
348
- start,
349
- end,
350
- strand,
351
- xs:
352
- feature.get('xs') ||
353
- feature.get('ts') ||
354
- feature.get('tags').XS ||
355
- feature.get('tags').TS,
356
- score: 1,
351
+ mismatches
352
+ .filter(mismatch => mismatch.type === 'skip')
353
+ .forEach(mismatch => {
354
+ const mstart = feature.get('start') + mismatch.start
355
+ const start = mstart
356
+ const end = mstart + mismatch.length
357
+ const strand = feature.get('strand')
358
+ const hash = `${start}_${end}_${strand}`
359
+ if (!skipmap[hash]) {
360
+ skipmap[hash] = {
361
+ feature: feature,
362
+ start,
363
+ end,
364
+ strand,
365
+ xs: getTag(feature, 'XS') || getTag(feature, 'TS'),
366
+ score: 1,
367
+ }
368
+ } else {
369
+ skipmap[hash].score++
357
370
  }
358
- } else {
359
- skipmap[hash].score++
360
- }
361
- })
371
+ })
372
+ }
362
373
  }
363
- }
364
374
 
365
- return bins
366
- }, initBins),
375
+ return bins
376
+ },
377
+ [] as {
378
+ total: number
379
+ lowqual: BinType
380
+ cov: BinType
381
+ delskips: BinType
382
+ noncov: BinType
383
+ ref: BinType
384
+ }[],
385
+ ),
367
386
  )
368
387
  .toPromise()
369
388
 
@@ -1,8 +1,7 @@
1
1
  import { createJBrowseTheme } from '@jbrowse/core/ui'
2
- import { featureSpanPx } from '@jbrowse/core/util'
2
+ import { featureSpanPx, bpSpanPx } from '@jbrowse/core/util'
3
3
  import { Feature } from '@jbrowse/core/util/simpleFeature'
4
4
  import { readConfObject } from '@jbrowse/core/configuration'
5
- import { bpSpanPx } from '@jbrowse/core/util'
6
5
  import { RenderArgsDeserialized as FeatureRenderArgsDeserialized } from '@jbrowse/core/pluggableElementTypes/renderers/FeatureRendererType'
7
6
  import {
8
7
  getOrigin,
@@ -24,7 +23,7 @@ export interface RenderArgsDeserializedWithFeatures
24
23
  features: Map<string, Feature>
25
24
  ticks: { values: number[] }
26
25
  displayCrossHatches: boolean
27
- modificationTagMap: Record<string, string>
26
+ modificationTagMap?: Record<string, string>
28
27
  }
29
28
 
30
29
  type Counts = {
@@ -49,12 +48,12 @@ export default class SNPCoverageRenderer extends WiggleBaseRenderer {
49
48
  regions,
50
49
  bpPerPx,
51
50
  displayCrossHatches,
52
- modificationTagMap,
51
+ modificationTagMap = {},
53
52
  scaleOpts,
54
53
  height: unadjustedHeight,
55
54
  theme: configTheme,
56
55
  config: cfg,
57
- ticks: { values },
56
+ ticks,
58
57
  } = props
59
58
  const theme = createJBrowseTheme(configTheme)
60
59
  const [region] = regions
@@ -66,9 +65,23 @@ export default class SNPCoverageRenderer extends WiggleBaseRenderer {
66
65
  const offset = YSCALEBAR_LABEL_OFFSET
67
66
  const height = unadjustedHeight - offset * 2
68
67
 
68
+ const { domain } = scaleOpts
69
+ if (!domain) {
70
+ return
71
+ }
69
72
  const opts = { ...scaleOpts, range: [0, height] }
70
73
  const viewScale = getScale(opts)
71
- const snpViewScale = getScale({ ...opts, scaleType: 'linear' })
74
+ const snpViewScale = getScale({
75
+ ...opts,
76
+ range: [0, height],
77
+ scaleType: 'linear',
78
+ })
79
+ // clipping and insertion indicators, uses a smaller height/2 scale
80
+ const indicatorViewScale = getScale({
81
+ ...opts,
82
+ range: [0, height / 2],
83
+ scaleType: 'linear',
84
+ })
72
85
  const originY = getOrigin(scaleOpts.scaleType)
73
86
  const snpOriginY = getOrigin('linear')
74
87
 
@@ -83,7 +96,11 @@ export default class SNPCoverageRenderer extends WiggleBaseRenderer {
83
96
 
84
97
  // this is always linear scale, even when plotted on top of log scale
85
98
  const snpToY = (n: number) => height - (snpViewScale(n) || 0) + offset
99
+ const indicatorToY = (n: number) =>
100
+ height - (indicatorViewScale(n) || 0) + offset
86
101
  const snpToHeight = (n: number) => snpToY(snpOriginY) - snpToY(n)
102
+ const indicatorToHeight = (n: number) =>
103
+ indicatorToY(snpOriginY) - indicatorToY(n)
87
104
 
88
105
  const colorForBase: { [key: string]: string } = {
89
106
  A: theme.palette.bases.A.main,
@@ -106,91 +123,104 @@ export default class SNPCoverageRenderer extends WiggleBaseRenderer {
106
123
  // Use two pass rendering, which helps in visualizing the SNPs at higher
107
124
  // bpPerPx First pass: draw the gray background
108
125
  ctx.fillStyle = colorForBase.total
109
- coverage.forEach(feature => {
126
+ for (let i = 0; i < coverage.length; i++) {
127
+ const feature = coverage[i]
110
128
  const [leftPx, rightPx] = featureSpanPx(feature, region, bpPerPx)
111
129
  const w = rightPx - leftPx + 0.3
112
130
  const score = feature.get('score') as number
113
131
  ctx.fillRect(leftPx, toY(score), w, toHeight(score))
114
- })
115
- ctx.fillStyle = 'grey'
116
- ctx.beginPath()
117
- ctx.lineTo(0, 0)
118
- ctx.moveTo(0, width)
119
- ctx.stroke()
132
+ }
133
+
134
+ // Keep track of previous total which we will use it to draw the interbase
135
+ // indicator (if there is a sudden clip, there will be no read coverage but
136
+ // there will be "clip" coverage) at that position beyond the read. if the
137
+ // clip is right at a block boundary then prevTotal will not be available,
138
+ // so this is a best attempt to plot interbase indicator at the "cliffs"
139
+ let prevTotal = 0
140
+
141
+ // extraHorizontallyFlippedOffset is used to draw interbase items, which
142
+ // are located to the left when forward and right when reversed
143
+ const extraHorizontallyFlippedOffset = region.reversed ? 1 / bpPerPx : 0
120
144
 
121
145
  // Second pass: draw the SNP data, and add a minimum feature width of 1px
122
146
  // which can be wider than the actual bpPerPx This reduces overdrawing of
123
147
  // the grey background over the SNPs
124
- coverage.forEach(feature => {
148
+
149
+ for (let i = 0; i < coverage.length; i++) {
150
+ const feature = coverage[i]
125
151
  const [leftPx, rightPx] = featureSpanPx(feature, region, bpPerPx)
126
152
 
127
153
  const snpinfo = feature.get('snpinfo') as SNPInfo
128
154
  const w = Math.max(rightPx - leftPx + 0.3, 1)
129
155
  const totalScore = snpinfo.total
156
+ const keys = Object.keys(snpinfo.cov).sort()
130
157
 
131
- Object.entries(snpinfo.cov)
132
- .sort(([a], [b]) => {
133
- if (a < b) {
134
- return -1
135
- }
136
- if (a > b) {
137
- return 1
138
- }
139
- return 0
140
- })
141
- .reduce((curr, [base, { total }]) => {
142
- ctx.fillStyle =
143
- colorForBase[base] ||
144
- modificationTagMap[base.replace('mod_', '')] ||
145
- 'red'
146
- ctx.fillRect(leftPx, snpToY(total + curr), w, snpToHeight(total))
147
- return curr + total
148
- }, 0)
149
-
150
- const interbaseEvents = Object.entries(snpinfo.noncov)
158
+ let curr = 0
159
+ for (let i = 0; i < keys.length; i++) {
160
+ const base = keys[i]
161
+ const { total } = snpinfo.cov[base]
162
+ ctx.fillStyle =
163
+ colorForBase[base] ||
164
+ modificationTagMap[base.replace('mod_', '')] ||
165
+ '#888'
166
+ ctx.fillRect(leftPx, snpToY(total + curr), w, snpToHeight(total))
167
+ curr += total
168
+ }
169
+
170
+ const interbaseEvents = Object.keys(snpinfo.noncov)
151
171
  const indicatorHeight = 4.5
152
172
  if (drawInterbaseCounts) {
153
- interbaseEvents.reduce((curr, [base, { total }]) => {
173
+ let curr = 0
174
+ for (let i = 0; i < interbaseEvents.length; i++) {
175
+ const base = interbaseEvents[i]
176
+ const { total } = snpinfo.noncov[base]
154
177
  ctx.fillStyle = colorForBase[base]
155
178
  ctx.fillRect(
156
- leftPx - 0.6,
157
- indicatorHeight + snpToHeight(curr),
179
+ leftPx - 0.6 + extraHorizontallyFlippedOffset,
180
+ indicatorHeight + indicatorToHeight(curr),
158
181
  1.2,
159
- snpToHeight(total),
182
+ indicatorToHeight(total),
160
183
  )
161
- return curr + total
162
- }, 0)
184
+ curr += total
185
+ }
163
186
  }
164
187
 
165
188
  if (drawIndicators) {
166
189
  let accum = 0
167
190
  let max = 0
168
191
  let maxBase = ''
169
- interbaseEvents.forEach(([base, { total }]) => {
192
+ for (let i = 0; i < interbaseEvents.length; i++) {
193
+ const base = interbaseEvents[i]
194
+ const { total } = snpinfo.noncov[base]
170
195
  accum += total
171
196
  if (total > max) {
172
197
  max = total
173
198
  maxBase = base
174
199
  }
175
- })
200
+ }
176
201
 
177
202
  // avoid drawing a bunch of indicators if coverage is very low e.g.
178
- // less than 7
179
- if (accum > totalScore * indicatorThreshold && totalScore > 7) {
203
+ // less than 7, uses the prev total in the case of the "cliff"
204
+ const indicatorComparatorScore = Math.max(totalScore, prevTotal)
205
+ if (
206
+ accum > indicatorComparatorScore * indicatorThreshold &&
207
+ indicatorComparatorScore > 7
208
+ ) {
180
209
  ctx.fillStyle = colorForBase[maxBase]
181
210
  ctx.beginPath()
182
- ctx.moveTo(leftPx - 3, 0)
183
- ctx.lineTo(leftPx + 3, 0)
184
- ctx.lineTo(leftPx, indicatorHeight)
211
+ const l = leftPx + extraHorizontallyFlippedOffset
212
+ ctx.moveTo(l - 3.5, 0)
213
+ ctx.lineTo(l + 3.5, 0)
214
+ ctx.lineTo(l, indicatorHeight)
185
215
  ctx.fill()
186
216
  }
187
217
  }
188
- })
189
-
190
- ctx.globalAlpha = 0.7
218
+ prevTotal = totalScore
219
+ }
191
220
 
192
221
  if (drawArcs) {
193
- skips.forEach(f => {
222
+ for (let i = 0; i < skips.length; i++) {
223
+ const f = skips[i]
194
224
  const [left, right] = bpSpanPx(
195
225
  f.get('start'),
196
226
  f.get('end'),
@@ -201,9 +231,9 @@ export default class SNPCoverageRenderer extends WiggleBaseRenderer {
201
231
  ctx.beginPath()
202
232
  const str = f.get('strand') as number
203
233
  const xs = f.get('xs') as string
204
- const pos = 'rgb(255,200,200)'
205
- const neg = 'rgb(200,200,255)'
206
- const neutral = 'rgb(200,200,200)'
234
+ const pos = 'rgba(255,200,200,0.7)'
235
+ const neg = 'rgba(200,200,255,0.7)'
236
+ const neutral = 'rgba(200,200,200,0.7)'
207
237
 
208
238
  if (xs === '+') {
209
239
  ctx.strokeStyle = pos
@@ -221,13 +251,13 @@ export default class SNPCoverageRenderer extends WiggleBaseRenderer {
221
251
  ctx.moveTo(left, height - offset * 2)
222
252
  ctx.bezierCurveTo(left, 0, right, 0, right, height - offset * 2)
223
253
  ctx.stroke()
224
- })
254
+ }
225
255
  }
226
256
 
227
257
  if (displayCrossHatches) {
228
258
  ctx.lineWidth = 1
229
259
  ctx.strokeStyle = 'rgba(140,140,140,0.8)'
230
- values.forEach(tick => {
260
+ ticks.values.forEach(tick => {
231
261
  ctx.beginPath()
232
262
  ctx.moveTo(0, Math.round(toY(tick)))
233
263
  ctx.lineTo(width, Math.round(toY(tick)))