@jbrowse/plugin-alignments 1.6.5 → 1.6.8

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 (52) hide show
  1. package/dist/AlignmentsFeatureDetail/index.d.ts +1 -1
  2. package/dist/BamAdapter/BamSlightlyLazyFeature.d.ts +2 -10
  3. package/dist/BamAdapter/MismatchParser.d.ts +3 -5
  4. package/dist/BamAdapter/configSchema.d.ts +1 -1
  5. package/dist/CramAdapter/CramSlightlyLazyFeature.d.ts +1 -2
  6. package/dist/CramAdapter/configSchema.d.ts +1 -1
  7. package/dist/HtsgetBamAdapter/configSchema.d.ts +1 -1
  8. package/dist/LinearAlignmentsDisplay/models/configSchema.d.ts +1 -1
  9. package/dist/LinearAlignmentsDisplay/models/model.d.ts +1 -1
  10. package/dist/LinearPileupDisplay/configSchema.d.ts +1 -1
  11. package/dist/LinearSNPCoverageDisplay/components/Tooltip.d.ts +1 -1
  12. package/dist/LinearSNPCoverageDisplay/models/configSchema.d.ts +1 -1
  13. package/dist/LinearSNPCoverageDisplay/models/model.d.ts +2 -2
  14. package/dist/PileupRenderer/PileupRenderer.d.ts +20 -6
  15. package/dist/PileupRenderer/configSchema.d.ts +1 -1
  16. package/dist/SNPCoverageAdapter/SNPCoverageAdapter.d.ts +3 -11
  17. package/dist/SNPCoverageAdapter/configSchema.d.ts +1 -1
  18. package/dist/SNPCoverageRenderer/SNPCoverageRenderer.d.ts +1 -1
  19. package/dist/SNPCoverageRenderer/configSchema.d.ts +1 -1
  20. package/dist/SNPCoverageRenderer/index.d.ts +1 -1
  21. package/dist/plugin-alignments.cjs.development.js +795 -682
  22. package/dist/plugin-alignments.cjs.development.js.map +1 -1
  23. package/dist/plugin-alignments.cjs.production.min.js +1 -1
  24. package/dist/plugin-alignments.cjs.production.min.js.map +1 -1
  25. package/dist/plugin-alignments.esm.js +797 -684
  26. package/dist/plugin-alignments.esm.js.map +1 -1
  27. package/dist/util.d.ts +4 -0
  28. package/package.json +4 -4
  29. package/src/AlignmentsFeatureDetail/AlignmentsFeatureDetail.tsx +23 -14
  30. package/src/BamAdapter/BamAdapter.ts +10 -7
  31. package/src/BamAdapter/BamSlightlyLazyFeature.ts +11 -75
  32. package/src/BamAdapter/MismatchParser.test.ts +53 -297
  33. package/src/BamAdapter/MismatchParser.ts +54 -116
  34. package/src/BamAdapter/configSchema.ts +0 -4
  35. package/src/CramAdapter/CramSlightlyLazyFeature.ts +3 -10
  36. package/src/LinearAlignmentsDisplay/components/AlignmentsDisplay.tsx +38 -30
  37. package/src/LinearAlignmentsDisplay/models/model.tsx +10 -9
  38. package/src/LinearPileupDisplay/components/ColorByModifications.tsx +76 -80
  39. package/src/LinearPileupDisplay/components/ColorByTag.tsx +24 -23
  40. package/src/LinearPileupDisplay/components/FilterByTag.tsx +73 -68
  41. package/src/LinearPileupDisplay/components/SetFeatureHeight.tsx +28 -26
  42. package/src/LinearPileupDisplay/components/SetMaxHeight.tsx +24 -13
  43. package/src/LinearPileupDisplay/components/SortByTag.tsx +29 -21
  44. package/src/LinearPileupDisplay/model.ts +12 -6
  45. package/src/LinearSNPCoverageDisplay/components/Tooltip.tsx +5 -3
  46. package/src/LinearSNPCoverageDisplay/models/configSchema.ts +4 -5
  47. package/src/PileupRenderer/PileupRenderer.tsx +202 -70
  48. package/src/PileupRenderer/components/PileupRendering.tsx +5 -3
  49. package/src/SNPCoverageAdapter/SNPCoverageAdapter.ts +192 -237
  50. package/src/SNPCoverageRenderer/SNPCoverageRenderer.ts +91 -60
  51. package/src/SNPCoverageRenderer/configSchema.js +1 -1
  52. package/src/util.ts +25 -0
@@ -4,12 +4,15 @@ import {
4
4
  } from '@jbrowse/core/data_adapters/BaseAdapter'
5
5
  import { AugmentedRegion as Region } from '@jbrowse/core/util/types'
6
6
  import SimpleFeature, { Feature } from '@jbrowse/core/util/simpleFeature'
7
- import { readConfObject } from '@jbrowse/core/configuration'
8
7
  import SerializableFilterChain from '@jbrowse/core/pluggableElementTypes/renderers/util/serializableFilterChain'
9
8
  import { ObservableCreate } from '@jbrowse/core/util/rxjs'
10
- import { reduce, filter, toArray } from 'rxjs/operators'
11
- import { Observable } from 'rxjs'
12
- import { getTag, getTagAlt } from '../util'
9
+ import { toArray } from 'rxjs/operators'
10
+ import {
11
+ getTag,
12
+ getTagAlt,
13
+ fetchSequence,
14
+ shouldFetchReferenceSequence,
15
+ } from '../util'
13
16
  import {
14
17
  parseCigar,
15
18
  getNextRefPos,
@@ -48,11 +51,8 @@ function dec(bin: any, strand: number, type: string, field: string) {
48
51
 
49
52
  export default class SNPCoverageAdapter extends BaseFeatureDataAdapter {
50
53
  protected async configure() {
51
- const subadapterConfig = readConfObject(this.config, 'subadapter')
52
- const sequenceConf = readConfObject(this.config, [
53
- 'subadapter',
54
- 'sequenceAdapter',
55
- ])
54
+ const subadapterConfig = this.getConf('subadapter')
55
+ const sequenceConf = this.getConf(['subadapter', 'sequenceAdapter'])
56
56
  const dataAdapter = await this.getSubAdapter?.(subadapterConfig)
57
57
 
58
58
  const sequenceAdapter = sequenceConf
@@ -71,37 +71,47 @@ export default class SNPCoverageAdapter extends BaseFeatureDataAdapter {
71
71
  }
72
72
  }
73
73
 
74
+ async fetchSequence(region: Region) {
75
+ const { sequenceAdapter } = await this.configure()
76
+ if (!sequenceAdapter) {
77
+ return undefined
78
+ }
79
+
80
+ return fetchSequence(region, sequenceAdapter)
81
+ }
82
+
74
83
  getFeatures(region: Region, opts: SNPCoverageOptions = {}) {
75
84
  return ObservableCreate<Feature>(async observer => {
76
85
  const { subadapter } = await this.configure()
77
- let stream = subadapter.getFeatures(region, opts)
86
+ let feats = await subadapter
87
+ .getFeatures(region, opts)
88
+ .pipe(toArray())
89
+ .toPromise()
78
90
 
79
91
  if (opts.filters) {
80
92
  const { filters } = opts
81
- stream = stream.pipe(filter(f => filters.passes(f, opts)))
93
+ feats = feats.filter(f => filters.passes(f, opts))
82
94
  }
83
95
 
84
96
  const { bins, skipmap } = await this.generateCoverageBins(
85
- stream,
97
+ feats,
86
98
  region,
87
99
  opts,
88
100
  )
89
101
 
90
102
  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
- }
103
+ observer.next(
104
+ new SimpleFeature({
105
+ id: `${this.id}-${region.start}-${index}`,
106
+ data: {
107
+ score: bin.total,
108
+ snpinfo: bin,
109
+ start: region.start + index,
110
+ end: region.start + index + 1,
111
+ refName: region.refName,
112
+ },
113
+ }),
114
+ )
105
115
  })
106
116
 
107
117
  // make fake features from the coverage
@@ -137,22 +147,12 @@ export default class SNPCoverageAdapter extends BaseFeatureDataAdapter {
137
147
 
138
148
  freeResources(/* { region } */): void {}
139
149
 
140
- /**
141
- * Generates coverage bins from features which details
142
- * the reference, mismatches, strands, and coverage info
143
- * @param features - Features of region to be passed in
144
- * @param region - Region
145
- * @param bpPerPx - base pairs per pixel
146
- * @returns Array of nested frequency tables
147
- */
148
150
  async generateCoverageBins(
149
- features: Observable<Feature>,
151
+ features: Feature[],
150
152
  region: Region,
151
153
  opts: { bpPerPx?: number; colorBy?: { type: string; tag?: string } },
152
154
  ) {
153
155
  const { colorBy } = opts
154
- const { sequenceAdapter } = await this.configure()
155
- const { originalRefName, refName, start, end } = region
156
156
  const binMax = Math.ceil(region.end - region.start)
157
157
 
158
158
  const skipmap = {} as {
@@ -166,221 +166,176 @@ export default class SNPCoverageAdapter extends BaseFeatureDataAdapter {
166
166
  }
167
167
  }
168
168
 
169
- // bins contain cov feature if they contribute to coverage, or noncov which
170
- // are interbase or other features that don't contribute to coverage.
171
- // delskips are elements that don't contribute to coverage, but should be
172
- // reported also (and are not interbase)
169
+ // bins contain:
170
+ // - cov feature if they contribute to coverage
171
+ // - noncov are insertions/clip features that don't contribute to coverage
172
+ // - delskips deletions or introns that don't contribute to coverage
173
173
  type BinType = { total: number; strands: { [key: string]: number } }
174
174
 
175
- // request an extra +1 on the end to get CpG crossing region boundary
176
- let regionSeq: string | undefined
177
-
178
- if (sequenceAdapter) {
179
- const [feat] = await sequenceAdapter
180
- .getFeatures({
181
- refName: originalRefName || refName,
182
- start,
183
- end: end + 1,
184
- assemblyName: region.assemblyName,
185
- })
186
- .pipe(toArray())
187
- .toPromise()
188
- regionSeq = feat?.get('seq')
189
- }
175
+ const regionSeq =
176
+ features.length && shouldFetchReferenceSequence(opts.colorBy?.type)
177
+ ? await this.fetchSequence(region)
178
+ : undefined
179
+
180
+ const bins = [] as {
181
+ total: number
182
+ lowqual: BinType
183
+ cov: BinType
184
+ delskips: BinType
185
+ noncov: BinType
186
+ ref: BinType
187
+ }[]
188
+
189
+ for (let i = 0; i < features.length; i++) {
190
+ const feature = features[i]
191
+ const ops = parseCigar(feature.get('CIGAR'))
192
+ const fstart = feature.get('start')
193
+ const fend = feature.get('end')
194
+ const fstrand = feature.get('strand')
195
+
196
+ for (let j = fstart; j < fend; j++) {
197
+ const i = j - region.start
198
+ if (i >= 0 && i < binMax) {
199
+ const bin = bins[i] || {
200
+ total: 0,
201
+ lowqual: {} as BinType,
202
+ cov: {} as BinType,
203
+ delskips: {} as BinType,
204
+ noncov: {} as BinType,
205
+ ref: {} as BinType,
206
+ }
207
+ if (j !== fend) {
208
+ bin.total++
209
+ inc(bin, fstrand, 'ref', 'ref')
210
+ }
211
+ bins[i] = bin
212
+ }
213
+ }
190
214
 
191
- const bins = await features
192
- .pipe(
193
- reduce(
194
- (bins, feature) => {
195
- const cigar = feature.get('CIGAR')
196
- const fstart = feature.get('start')
197
- const fend = feature.get('end')
198
- const fstrand = feature.get('strand')
199
- const cigarOps = parseCigar(cigar)
200
-
201
- for (let j = fstart; j < fend; j++) {
202
- const i = j - region.start
203
- if (i >= 0 && i < binMax) {
204
- const bin = bins[i] || {
205
- total: 0,
206
- lowqual: {} as BinType,
207
- cov: {} as BinType,
208
- delskips: {} as BinType,
209
- noncov: {} as BinType,
210
- ref: {} as BinType,
211
- }
212
- bin.total++
213
- inc(bin, fstrand, 'ref', 'ref')
214
- bins[i] = bin
215
+ if (colorBy?.type === 'modifications') {
216
+ const seq = feature.get('seq') as string
217
+ const mm = (getTagAlt(feature, 'MM', 'Mm') as string) || ''
218
+
219
+ getModificationPositions(mm, seq, fstrand).forEach(
220
+ ({ type, positions }) => {
221
+ const mod = `mod_${type}`
222
+ for (const pos of getNextRefPos(ops, positions)) {
223
+ const epos = pos + fstart - region.start
224
+ if (epos >= 0 && epos < bins.length && pos + fstart < fend) {
225
+ const bin = bins[epos]
226
+ inc(bin, fstrand, 'cov', mod)
215
227
  }
216
228
  }
229
+ },
230
+ )
231
+ }
217
232
 
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
- if (probabilities[probIndex] > 0.5) {
248
- inc(bin, fstrand, 'cov', mod)
249
- } else {
250
- inc(bin, fstrand, 'lowqual', mod)
251
- }
252
- }
253
- probIndex++
254
- }
255
- },
256
- )
233
+ // methylation based coloring takes into account both reference
234
+ // sequence CpG detection and reads
235
+ else if (colorBy?.type === 'methylation') {
236
+ if (!regionSeq) {
237
+ throw new Error(
238
+ 'no region sequence detected, need sequenceAdapter configuration',
239
+ )
240
+ }
241
+ const seq = feature.get('seq')
242
+ const mm = getTagAlt(feature, 'MM', 'Mm') || ''
243
+ const methBins = new Array(region.end - region.start).fill(0)
244
+
245
+ getModificationPositions(mm, seq, fstrand).forEach(
246
+ ({ type, positions }) => {
247
+ // we are processing methylation
248
+ if (type === 'm') {
249
+ for (const pos of getNextRefPos(ops, positions)) {
250
+ const epos = pos + fstart - region.start
251
+ if (epos >= 0 && epos < methBins.length) {
252
+ methBins[epos] = 1
253
+ }
254
+ }
257
255
  }
256
+ },
257
+ )
258
258
 
259
- // methylation based coloring takes into account both reference
260
- // sequence CpG detection and reads
261
- else if (colorBy?.type === 'methylation') {
262
- if (!regionSeq) {
263
- throw new Error(
264
- 'no region sequence detected, need sequenceAdapter configuration',
265
- )
266
- }
267
- const seq = feature.get('seq')
268
- const mm = getTagAlt(feature, 'MM', 'Mm') || ''
269
- const methBins = new Array(region.end - region.start).fill(0)
270
-
271
- getModificationPositions(mm, seq, fstrand).forEach(
272
- ({ type, positions }) => {
273
- // we are processing methylation
274
- if (type === 'm') {
275
- for (const pos of getNextRefPos(cigarOps, positions)) {
276
- const epos = pos + fstart - region.start
277
- if (epos >= 0 && epos < methBins.length) {
278
- methBins[epos] = 1
279
- }
280
- }
281
- }
282
- },
283
- )
284
-
285
- for (let j = fstart; j < fend; j++) {
286
- const i = j - region.start
287
- if (i >= 0 && i < bins.length - 1) {
288
- const l1 = regionSeq[i].toLowerCase()
289
- const l2 = regionSeq[i + 1].toLowerCase()
290
- const bin = bins[i]
291
- const bin1 = bins[i + 1]
292
-
293
- // color
294
- if (l1 === 'c' && l2 === 'g') {
295
- if (methBins[i] || methBins[i + 1]) {
296
- inc(bin, fstrand, 'cov', 'meth')
297
- inc(bin1, fstrand, 'cov', 'meth')
298
- dec(bin, fstrand, 'ref', 'ref')
299
- dec(bin1, fstrand, 'ref', 'ref')
300
- } else {
301
- inc(bin, fstrand, 'cov', 'unmeth')
302
- inc(bin1, fstrand, 'cov', 'unmeth')
303
- dec(bin, fstrand, 'ref', 'ref')
304
- dec(bin1, fstrand, 'ref', 'ref')
305
- }
306
- }
307
- }
259
+ for (let j = fstart; j < fend; j++) {
260
+ const i = j - region.start
261
+ if (i >= 0 && i < bins.length - 1) {
262
+ const l1 = regionSeq[i].toLowerCase()
263
+ const l2 = regionSeq[i + 1].toLowerCase()
264
+ const bin = bins[i]
265
+ const bin1 = bins[i + 1]
266
+
267
+ // color
268
+ if (l1 === 'c' && l2 === 'g') {
269
+ if (methBins[i] || methBins[i + 1]) {
270
+ inc(bin, fstrand, 'cov', 'meth')
271
+ inc(bin1, fstrand, 'cov', 'meth')
272
+ dec(bin, fstrand, 'ref', 'ref')
273
+ dec(bin1, fstrand, 'ref', 'ref')
274
+ } else {
275
+ inc(bin, fstrand, 'cov', 'unmeth')
276
+ inc(bin1, fstrand, 'cov', 'unmeth')
277
+ dec(bin, fstrand, 'ref', 'ref')
278
+ dec(bin1, fstrand, 'ref', 'ref')
308
279
  }
309
280
  }
281
+ }
282
+ }
283
+ }
310
284
 
311
- // normal SNP based coloring
312
- else {
313
- const mismatches = feature.get('mismatches') as
314
- | Mismatch[]
315
- | undefined
316
-
317
- if (mismatches) {
318
- for (let i = 0; i < mismatches.length; i++) {
319
- const mismatch = mismatches[i]
320
- const mstart = fstart + mismatch.start
321
- for (
322
- let j = mstart;
323
- j < mstart + mismatchLen(mismatch);
324
- j++
325
- ) {
326
- const epos = j - region.start
327
- if (epos >= 0 && epos < bins.length) {
328
- const bin = bins[epos]
329
- const { base, type } = mismatch
330
- const interbase = isInterbase(type)
331
- if (!interbase) {
332
- dec(bin, fstrand, 'ref', 'ref')
333
- } else {
334
- inc(bin, fstrand, 'noncov', type)
335
- }
336
-
337
- if (type === 'deletion' || type === 'skip') {
338
- inc(bin, fstrand, 'delskips', type)
339
- bin.total--
340
- } else if (!interbase) {
341
- inc(bin, fstrand, 'cov', base)
342
- }
343
- }
344
- }
285
+ // normal SNP based coloring
286
+ else {
287
+ const mismatches = feature.get('mismatches') as Mismatch[] | undefined
288
+
289
+ if (mismatches) {
290
+ for (let i = 0; i < mismatches.length; i++) {
291
+ const mismatch = mismatches[i]
292
+ const mstart = fstart + mismatch.start
293
+ for (let j = mstart; j < mstart + mismatchLen(mismatch); j++) {
294
+ const epos = j - region.start
295
+ if (epos >= 0 && epos < bins.length) {
296
+ const bin = bins[epos]
297
+ const { base, type } = mismatch
298
+ const interbase = isInterbase(type)
299
+ if (!interbase) {
300
+ dec(bin, fstrand, 'ref', 'ref')
301
+ } else {
302
+ inc(bin, fstrand, 'noncov', type)
345
303
  }
346
304
 
347
- mismatches
348
- .filter(mismatch => mismatch.type === 'skip')
349
- .forEach(mismatch => {
350
- const mstart = feature.get('start') + mismatch.start
351
- const start = mstart
352
- const end = mstart + mismatch.length
353
- const strand = feature.get('strand')
354
- const hash = `${start}_${end}_${strand}`
355
- if (!skipmap[hash]) {
356
- skipmap[hash] = {
357
- feature: feature,
358
- start,
359
- end,
360
- strand,
361
- xs: getTag(feature, 'XS') || getTag(feature, 'TS'),
362
- score: 1,
363
- }
364
- } else {
365
- skipmap[hash].score++
366
- }
367
- })
305
+ if (type === 'deletion' || type === 'skip') {
306
+ inc(bin, fstrand, 'delskips', type)
307
+ bin.total--
308
+ } else if (!interbase) {
309
+ inc(bin, fstrand, 'cov', base)
310
+ }
368
311
  }
369
312
  }
370
-
371
- return bins
372
- },
373
- [] as {
374
- total: number
375
- lowqual: BinType
376
- cov: BinType
377
- delskips: BinType
378
- noncov: BinType
379
- ref: BinType
380
- }[],
381
- ),
382
- )
383
- .toPromise()
313
+ }
314
+
315
+ mismatches
316
+ .filter(mismatch => mismatch.type === 'skip')
317
+ .forEach(mismatch => {
318
+ const mstart = feature.get('start') + mismatch.start
319
+ const start = mstart
320
+ const end = mstart + mismatch.length
321
+ const strand = feature.get('strand')
322
+ const hash = `${start}_${end}_${strand}`
323
+ if (!skipmap[hash]) {
324
+ skipmap[hash] = {
325
+ feature: feature,
326
+ start,
327
+ end,
328
+ strand,
329
+ xs: getTag(feature, 'XS') || getTag(feature, 'TS'),
330
+ score: 1,
331
+ }
332
+ } else {
333
+ skipmap[hash].score++
334
+ }
335
+ })
336
+ }
337
+ }
338
+ }
384
339
 
385
340
  return { bins, skipmap }
386
341
  }