@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.
- package/dist/AlignmentsFeatureDetail/index.d.ts +1 -1
- package/dist/BamAdapter/BamSlightlyLazyFeature.d.ts +3 -2
- package/dist/BamAdapter/configSchema.d.ts +1 -1
- package/dist/CramAdapter/configSchema.d.ts +1 -1
- package/dist/HtsgetBamAdapter/configSchema.d.ts +1 -1
- package/dist/LinearAlignmentsDisplay/models/configSchema.d.ts +1 -1
- package/dist/LinearAlignmentsDisplay/models/model.d.ts +1 -1
- package/dist/LinearPileupDisplay/configSchema.d.ts +1 -1
- package/dist/LinearSNPCoverageDisplay/components/Tooltip.d.ts +1 -1
- package/dist/LinearSNPCoverageDisplay/models/configSchema.d.ts +1 -1
- package/dist/PileupRenderer/configSchema.d.ts +1 -1
- package/dist/SNPCoverageAdapter/configSchema.d.ts +1 -1
- package/dist/SNPCoverageRenderer/SNPCoverageRenderer.d.ts +1 -1
- package/dist/SNPCoverageRenderer/configSchema.d.ts +1 -1
- package/dist/SNPCoverageRenderer/index.d.ts +1 -1
- package/dist/plugin-alignments.cjs.development.js +291 -223
- package/dist/plugin-alignments.cjs.development.js.map +1 -1
- package/dist/plugin-alignments.cjs.production.min.js +1 -1
- package/dist/plugin-alignments.cjs.production.min.js.map +1 -1
- package/dist/plugin-alignments.esm.js +291 -223
- package/dist/plugin-alignments.esm.js.map +1 -1
- package/package.json +6 -6
- package/src/AlignmentsFeatureDetail/AlignmentsFeatureDetail.tsx +23 -14
- package/src/BamAdapter/BamAdapter.ts +3 -4
- package/src/BamAdapter/BamSlightlyLazyFeature.ts +8 -4
- package/src/LinearAlignmentsDisplay/components/AlignmentsDisplay.tsx +38 -30
- package/src/LinearAlignmentsDisplay/models/model.tsx +10 -9
- package/src/LinearPileupDisplay/model.ts +6 -6
- package/src/LinearSNPCoverageDisplay/components/Tooltip.tsx +5 -3
- package/src/LinearSNPCoverageDisplay/models/configSchema.ts +4 -5
- package/src/PileupRenderer/PileupRenderer.tsx +39 -27
- package/src/PileupRenderer/components/PileupRendering.tsx +5 -3
- package/src/SNPCoverageAdapter/SNPCoverageAdapter.ts +188 -169
- package/src/SNPCoverageRenderer/SNPCoverageRenderer.ts +86 -56
- 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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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:
|
|
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(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
}
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
})
|
|
371
|
+
})
|
|
372
|
+
}
|
|
362
373
|
}
|
|
363
|
-
}
|
|
364
374
|
|
|
365
|
-
|
|
366
|
-
|
|
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
|
|
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
|
|
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({
|
|
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.
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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 +
|
|
179
|
+
leftPx - 0.6 + extraHorizontallyFlippedOffset,
|
|
180
|
+
indicatorHeight + indicatorToHeight(curr),
|
|
158
181
|
1.2,
|
|
159
|
-
|
|
182
|
+
indicatorToHeight(total),
|
|
160
183
|
)
|
|
161
|
-
|
|
162
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
ctx.
|
|
184
|
-
ctx.lineTo(
|
|
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.
|
|
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 = '
|
|
205
|
-
const neg = '
|
|
206
|
-
const neutral = '
|
|
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)))
|