@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
@@ -9,63 +9,81 @@ export interface Mismatch {
9
9
  seq?: string
10
10
  cliplen?: number
11
11
  }
12
-
12
+ const mdRegex = new RegExp(/(\d+|\^[a-z]+|[a-z])/gi)
13
13
  export function parseCigar(cigar: string) {
14
14
  return (cigar || '').split(/([MIDNSHPX=])/)
15
15
  }
16
16
  export function cigarToMismatches(
17
17
  ops: string[],
18
18
  seq: string,
19
+ ref?: string,
19
20
  qual?: Buffer,
20
21
  ): Mismatch[] {
21
- let currOffset = 0
22
- let seqOffset = 0
22
+ let roffset = 0 // reference offset
23
+ let soffset = 0 // seq offset
23
24
  const mismatches: Mismatch[] = []
24
- for (let i = 0; i < ops.length - 1; i += 2) {
25
+ const hasRefAndSeq = ref && seq
26
+ for (let i = 0; i < ops.length; i += 2) {
25
27
  const len = +ops[i]
26
28
  const op = ops[i + 1]
29
+
27
30
  if (op === 'M' || op === '=' || op === 'E') {
28
- seqOffset += len
31
+ if (hasRefAndSeq) {
32
+ for (let j = 0; j < len; j++) {
33
+ if (
34
+ // @ts-ignore in the full yarn build of the repo, this says that object is possibly undefined for some reason, ignored
35
+ seq[soffset + j].toUpperCase() !== ref[roffset + j].toUpperCase()
36
+ ) {
37
+ mismatches.push({
38
+ start: roffset + j,
39
+ type: 'mismatch',
40
+ base: seq[soffset + j],
41
+ length: 1,
42
+ })
43
+ }
44
+ }
45
+ }
46
+ soffset += len
29
47
  }
30
48
  if (op === 'I') {
31
49
  mismatches.push({
32
- start: currOffset,
50
+ start: roffset,
33
51
  type: 'insertion',
34
52
  base: `${len}`,
35
53
  length: 0,
36
54
  })
37
- seqOffset += len
55
+ soffset += len
38
56
  } else if (op === 'D') {
39
57
  mismatches.push({
40
- start: currOffset,
58
+ start: roffset,
41
59
  type: 'deletion',
42
60
  base: '*',
43
61
  length: len,
44
62
  })
45
63
  } else if (op === 'N') {
46
64
  mismatches.push({
47
- start: currOffset,
65
+ start: roffset,
48
66
  type: 'skip',
49
67
  base: 'N',
50
68
  length: len,
51
69
  })
52
70
  } else if (op === 'X') {
53
- const r = seq.slice(seqOffset, seqOffset + len)
54
- const q = qual?.slice(seqOffset, seqOffset + len) || []
71
+ const r = seq.slice(soffset, soffset + len)
72
+ const q = qual?.slice(soffset, soffset + len) || []
55
73
 
56
74
  for (let j = 0; j < len; j++) {
57
75
  mismatches.push({
58
- start: currOffset + j,
76
+ start: roffset + j,
59
77
  type: 'mismatch',
60
78
  base: r[j],
61
79
  qual: q[j],
62
80
  length: 1,
63
81
  })
64
82
  }
65
- seqOffset += len
83
+ soffset += len
66
84
  } else if (op === 'H') {
67
85
  mismatches.push({
68
- start: currOffset,
86
+ start: roffset,
69
87
  type: 'hardclip',
70
88
  base: `H${len}`,
71
89
  cliplen: len,
@@ -73,17 +91,17 @@ export function cigarToMismatches(
73
91
  })
74
92
  } else if (op === 'S') {
75
93
  mismatches.push({
76
- start: currOffset,
94
+ start: roffset,
77
95
  type: 'softclip',
78
96
  base: `S${len}`,
79
97
  cliplen: len,
80
98
  length: 1,
81
99
  })
82
- seqOffset += len
100
+ soffset += len
83
101
  }
84
102
 
85
103
  if (op !== 'I' && op !== 'S' && op !== 'H') {
86
- currOffset += len
104
+ roffset += len
87
105
  }
88
106
  }
89
107
  return mismatches
@@ -95,7 +113,7 @@ export function cigarToMismatches(
95
113
  */
96
114
  export function mdToMismatches(
97
115
  mdstring: string,
98
- cigarOps: string[],
116
+ ops: string[],
99
117
  cigarMismatches: Mismatch[],
100
118
  seq: string,
101
119
  qual?: Buffer,
@@ -129,11 +147,12 @@ export function mdToMismatches(
129
147
  let refOffset = lastRefOffset
130
148
  for (
131
149
  let i = lastCigar;
132
- i < cigarOps.length && refOffset <= refCoord;
150
+ i < ops.length && refOffset <= refCoord;
133
151
  i += 2, lastCigar = i
134
152
  ) {
135
- const len = +cigarOps[i]
136
- const op = cigarOps[i + 1]
153
+ const len = +ops[i]
154
+ const op = ops[i + 1]
155
+
137
156
  if (op === 'S' || op === 'I') {
138
157
  templateOffset += len
139
158
  } else if (op === 'D' || op === 'P' || op === 'N') {
@@ -150,18 +169,14 @@ export function mdToMismatches(
150
169
  }
151
170
 
152
171
  // now actually parse the MD string
153
- const md = mdstring.match(/(\d+|\^[a-z]+|[a-z])/gi) || []
172
+ const md = mdstring.match(mdRegex) || []
154
173
  for (let i = 0; i < md.length; i++) {
155
174
  const token = md[i]
156
175
  const num = +token
157
176
  if (!Number.isNaN(num)) {
158
177
  curr.start += num
159
178
  } else if (token.startsWith('^')) {
160
- curr.length = token.length - 1
161
- curr.base = '*'
162
- curr.type = 'deletion'
163
- curr.seq = token.substring(1)
164
- nextRecord()
179
+ curr.start += token.length - 1
165
180
  } else {
166
181
  // mismatch
167
182
  for (let j = 0; j < token.length; j += 1) {
@@ -176,9 +191,9 @@ export function mdToMismatches(
176
191
  break
177
192
  }
178
193
  }
179
- const s = cigarOps ? getTemplateCoordLocal(curr.start) : curr.start
180
- curr.base = seq ? seq.substr(s, 1) : 'X'
181
- const qualScore = qual?.slice(s, s + 1)[0]
194
+ const s = getTemplateCoordLocal(curr.start)
195
+ curr.base = seq[s] || 'X'
196
+ const qualScore = qual?.[s]
182
197
  if (qualScore) {
183
198
  curr.qual = qualScore
184
199
  }
@@ -190,106 +205,30 @@ export function mdToMismatches(
190
205
  return mismatchRecords
191
206
  }
192
207
 
193
- export function getTemplateCoord(refCoord: number, cigarOps: string[]): number {
194
- let templateOffset = 0
195
- let refOffset = 0
196
- for (let i = 0; i < cigarOps.length && refOffset <= refCoord; i += 2) {
197
- const len = +cigarOps[i]
198
- const op = cigarOps[i + 1]
199
- if (op === 'S' || op === 'I') {
200
- templateOffset += len
201
- } else if (op === 'D' || op === 'P') {
202
- refOffset += len
203
- } else if (op !== 'H') {
204
- templateOffset += len
205
- refOffset += len
206
- }
207
- }
208
- return templateOffset - (refOffset - refCoord)
209
- }
210
-
211
208
  export function getMismatches(
212
- cigarString: string,
213
- mdString: string,
209
+ cigar: string,
210
+ md: string,
214
211
  seq: string,
212
+ ref?: string,
215
213
  qual?: Buffer,
216
214
  ): Mismatch[] {
217
215
  let mismatches: Mismatch[] = []
218
- let cigarOps: string[] = []
216
+ const ops = parseCigar(cigar)
219
217
 
220
218
  // parse the CIGAR tag if it has one
221
- if (cigarString) {
222
- cigarOps = parseCigar(cigarString)
223
- mismatches = mismatches.concat(cigarToMismatches(cigarOps, seq, qual))
219
+ if (cigar) {
220
+ mismatches = mismatches.concat(cigarToMismatches(ops, seq, ref, qual))
224
221
  }
225
222
 
226
223
  // now let's look for CRAM or MD mismatches
227
- if (mdString) {
224
+ if (md) {
228
225
  mismatches = mismatches.concat(
229
- mdToMismatches(mdString, cigarOps, mismatches, seq, qual),
226
+ mdToMismatches(md, ops, mismatches, seq, qual),
230
227
  )
231
228
  }
232
229
 
233
- // uniqify the mismatches
234
- const seen: { [index: string]: boolean } = {}
235
- return mismatches.filter(m => {
236
- const key = `${m.type},${m.start},${m.length}`
237
- const s = seen[key]
238
- seen[key] = true
239
- return !s
240
- })
241
- }
242
-
243
- // adapted from minimap2 code static void write_MD_core function
244
- export function generateMD(target: string, query: string, cigar: string) {
245
- let queryOffset = 0
246
- let targetOffset = 0
247
- let lengthMD = 0
248
- if (!target) {
249
- console.warn('no ref supplied to generateMD')
250
- return ''
251
- }
252
- const cigarOps = parseCigar(cigar)
253
- let str = ''
254
- for (let i = 0; i < cigarOps.length; i += 2) {
255
- const len = +cigarOps[i]
256
- const op = cigarOps[i + 1]
257
- if (op === 'M' || op === 'X' || op === '=') {
258
- for (let j = 0; j < len; j++) {
259
- if (
260
- query[queryOffset + j].toLowerCase() !==
261
- target[targetOffset + j].toLowerCase()
262
- ) {
263
- str += `${lengthMD}${target[targetOffset + j].toUpperCase()}`
264
- lengthMD = 0
265
- } else {
266
- lengthMD++
267
- }
268
- }
269
- queryOffset += len
270
- targetOffset += len
271
- } else if (op === 'I') {
272
- queryOffset += len
273
- } else if (op === 'D') {
274
- let tmp = ''
275
- for (let j = 0; j < len; j++) {
276
- tmp += target[targetOffset + j].toUpperCase()
277
- }
278
- str += `${lengthMD}^${tmp}`
279
- lengthMD = 0
280
- targetOffset += len
281
- } else if (op === 'N') {
282
- targetOffset += len
283
- } else if (op === 'S') {
284
- queryOffset += len
285
- }
286
- }
287
- if (lengthMD > 0) {
288
- str += lengthMD
289
- }
290
- return str
230
+ return mismatches
291
231
  }
292
-
293
232
  // get relative reference sequence positions for positions given relative to
294
233
  // the read sequence
295
234
  export function* getNextRefPos(cigarOps: string[], positions: number[]) {
@@ -315,7 +254,6 @@ export function* getNextRefPos(cigarOps: string[], positions: number[]) {
315
254
  yield positions[i] - readPos + refPos
316
255
  }
317
256
  }
318
-
319
257
  export function getModificationPositions(
320
258
  mm: string,
321
259
  fseq: string,
@@ -23,10 +23,6 @@ export default types.late(() =>
23
23
  },
24
24
  },
25
25
  }),
26
- chunkSizeLimit: {
27
- type: 'number',
28
- defaultValue: 100_000_000,
29
- },
30
26
  fetchSizeLimit: {
31
27
  type: 'number',
32
28
  defaultValue: 5_000_000,
@@ -18,12 +18,10 @@ export interface Mismatch {
18
18
  }
19
19
 
20
20
  export default class CramSlightlyLazyFeature implements Feature {
21
- private _store: CramAdapter
22
-
21
+ // uses parameter properties to automatically create fields on the class
22
+ // https://www.typescriptlang.org/docs/handbook/classes.html#parameter-properties
23
23
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
- constructor(private record: any, store: CramAdapter) {
25
- this._store = store
26
- }
24
+ constructor(private record: any, private _store: CramAdapter) {}
27
25
 
28
26
  _get_name() {
29
27
  return this.record.readName
@@ -232,7 +230,6 @@ export default class CramSlightlyLazyFeature implements Feature {
232
230
  prop =>
233
231
  prop.startsWith('_get_') &&
234
232
  prop !== '_get_mismatches' &&
235
- prop !== '_get_skips_and_dels' &&
236
233
  prop !== '_get_cram_read_features',
237
234
  )
238
235
  .map(methodName => methodName.replace('_get_', ''))
@@ -397,8 +394,4 @@ export default class CramSlightlyLazyFeature implements Feature {
397
394
  )
398
395
  return mismatches
399
396
  }
400
-
401
- _get_skips_and_dels(): Mismatch[] {
402
- return this._get_mismatches()
403
- }
404
397
  }
@@ -225,12 +225,10 @@ const stateModelFactory = (
225
225
  <>
226
226
  <g>{await self.SNPCoverageDisplay.renderSvg(opts)}</g>
227
227
  <g transform={`translate(0 ${self.SNPCoverageDisplay.height})`}>
228
- {
229
- await self.PileupDisplay.renderSvg({
230
- ...opts,
231
- overrideHeight: pileupHeight,
232
- })
233
- }
228
+ {await self.PileupDisplay.renderSvg({
229
+ ...opts,
230
+ overrideHeight: pileupHeight,
231
+ })}
234
232
  </g>
235
233
  </>
236
234
  )
@@ -4,6 +4,7 @@ import { ObservableMap } from 'mobx'
4
4
  import {
5
5
  Button,
6
6
  Dialog,
7
+ DialogActions,
7
8
  DialogContent,
8
9
  DialogTitle,
9
10
  IconButton,
@@ -14,14 +15,12 @@ import {
14
15
  import CloseIcon from '@material-ui/icons/Close'
15
16
 
16
17
  const useStyles = makeStyles(theme => ({
17
- root: {},
18
18
  closeButton: {
19
19
  position: 'absolute',
20
20
  right: theme.spacing(1),
21
21
  top: theme.spacing(1),
22
22
  color: theme.palette.grey[500],
23
23
  },
24
-
25
24
  table: {
26
25
  border: '1px solid #888',
27
26
  margin: theme.spacing(4),
@@ -84,85 +83,82 @@ function ColorByTagDlg(props: {
84
83
  </IconButton>
85
84
  </DialogTitle>
86
85
  <DialogContent>
87
- <div className={classes.root}>
88
- <Typography>
89
- You can choose to color the modifications in the BAM/CRAM MM/ML
90
- specification using this dialog. Choosing modifications colors the
91
- modified positions and can color multiple modification types.
92
- Choosing the methylation setting colors methylated and unmethylated
93
- CpG.
94
- </Typography>
95
- <Typography>
96
- Note: you can revisit this dialog to see the current mapping of
97
- colors to modification type for the modification coloring mode
98
- </Typography>
99
- <div style={{ margin: 20 }}>
100
- {colorBy?.type === 'modifications' ? (
101
- <div>
102
- {modifications.length ? (
103
- <>
104
- Current modification-type-to-color mapping
105
- <ModificationTable
106
- modifications={[...modificationTagMap.entries()]}
107
- />
108
- </>
109
- ) : (
110
- <div>
111
- <Typography>
112
- Note: color by modifications is already enabled. Loading
113
- current modifications...
114
- </Typography>
115
- <CircularProgress size={15} />
116
- </div>
117
- )}
118
- </div>
119
- ) : null}
120
- {colorBy?.type === 'methylation' ? (
121
- <ModificationTable
122
- modifications={[
123
- ['methylated', 'red'],
124
- ['unmethylated', 'blue'],
125
- ]}
126
- />
127
- ) : null}
128
- </div>
129
- <div style={{ display: 'flex' }}>
130
- <Button
131
- variant="contained"
132
- color="primary"
133
- style={{ margin: 5 }}
134
- onClick={() => {
135
- model.setColorScheme({
136
- type: 'modifications',
137
- })
138
- handleClose()
139
- }}
140
- >
141
- Modifications
142
- </Button>
143
- <Button
144
- variant="contained"
145
- color="primary"
146
- style={{ margin: 5 }}
147
- onClick={() => {
148
- model.setColorScheme({
149
- type: 'methylation',
150
- })
151
- handleClose()
152
- }}
153
- >
154
- Methylation
155
- </Button>
156
- <Button
157
- variant="contained"
158
- color="secondary"
159
- style={{ margin: 5 }}
160
- onClick={() => handleClose()}
161
- >
162
- Cancel
163
- </Button>
164
- </div>
86
+ <Typography>
87
+ You can choose to color the modifications in the BAM/CRAM MM/ML
88
+ specification using this dialog. Choosing modifications colors the
89
+ modified positions and can color multiple modification types. Choosing
90
+ the methylation setting colors methylated and unmethylated CpG.
91
+ </Typography>
92
+ <Typography>
93
+ Note: you can revisit this dialog to see the current mapping of colors
94
+ to modification type for the modification coloring mode
95
+ </Typography>
96
+ <div style={{ margin: 20 }}>
97
+ {colorBy?.type === 'modifications' ? (
98
+ <div>
99
+ {modifications.length ? (
100
+ <>
101
+ Current modification-type-to-color mapping
102
+ <ModificationTable
103
+ modifications={[...modificationTagMap.entries()]}
104
+ />
105
+ </>
106
+ ) : (
107
+ <div>
108
+ <Typography>
109
+ Note: color by modifications is already enabled. Loading
110
+ current modifications...
111
+ </Typography>
112
+ <CircularProgress size={15} />
113
+ </div>
114
+ )}
115
+ </div>
116
+ ) : null}
117
+ {colorBy?.type === 'methylation' ? (
118
+ <ModificationTable
119
+ modifications={[
120
+ ['methylated', 'red'],
121
+ ['unmethylated', 'blue'],
122
+ ]}
123
+ />
124
+ ) : null}
165
125
  </div>
126
+ <DialogActions>
127
+ <Button
128
+ variant="contained"
129
+ color="primary"
130
+ style={{ margin: 5 }}
131
+ onClick={() => {
132
+ model.setColorScheme({
133
+ type: 'modifications',
134
+ })
135
+ handleClose()
136
+ }}
137
+ >
138
+ Modifications
139
+ </Button>
140
+ <Button
141
+ variant="contained"
142
+ color="primary"
143
+ style={{ margin: 5 }}
144
+ onClick={() => {
145
+ model.setColorScheme({
146
+ type: 'methylation',
147
+ })
148
+ handleClose()
149
+ }}
150
+ >
151
+ Methylation
152
+ </Button>
153
+ <Button
154
+ variant="contained"
155
+ color="secondary"
156
+ style={{ margin: 5 }}
157
+ onClick={() => handleClose()}
158
+ >
159
+ Cancel
160
+ </Button>
161
+ </DialogActions>
166
162
  </DialogContent>
167
163
  </Dialog>
168
164
  )
@@ -4,6 +4,7 @@ import {
4
4
  Button,
5
5
  Dialog,
6
6
  DialogContent,
7
+ DialogActions,
7
8
  DialogTitle,
8
9
  IconButton,
9
10
  TextField,
@@ -46,32 +47,29 @@ function ColorByTagDlg(props: {
46
47
  </IconButton>
47
48
  </DialogTitle>
48
49
  <DialogContent style={{ overflowX: 'hidden' }}>
49
- <div className={classes.root}>
50
- <Typography>Enter tag to color by: </Typography>
51
- <Typography color="textSecondary">
52
- Examples: XS or TS for RNA-seq inferred read strand, ts (lower-case)
53
- for minimap2 read strand, HP for haplotype, RG for read group, etc.
54
- </Typography>
50
+ <Typography>Enter tag to color by: </Typography>
51
+ <Typography color="textSecondary">
52
+ Examples: XS or TS for RNA-seq inferred read strand, ts (lower-case)
53
+ for minimap2 read strand, HP for haplotype, RG for read group, etc.
54
+ </Typography>
55
55
 
56
- <TextField
57
- value={tag}
58
- onChange={event => {
59
- setTag(event.target.value)
60
- }}
61
- placeholder="Enter tag name"
62
- inputProps={{
63
- maxLength: 2,
64
- 'data-testid': 'color-tag-name-input',
65
- }}
66
- error={tag.length === 2 && !validTag}
67
- helperText={tag.length === 2 && !validTag ? 'Not a valid tag' : ''}
68
- autoComplete="off"
69
- data-testid="color-tag-name"
70
- />
56
+ <TextField
57
+ value={tag}
58
+ onChange={event => setTag(event.target.value)}
59
+ placeholder="Enter tag name"
60
+ inputProps={{
61
+ maxLength: 2,
62
+ 'data-testid': 'color-tag-name-input',
63
+ }}
64
+ error={tag.length === 2 && !validTag}
65
+ helperText={tag.length === 2 && !validTag ? 'Not a valid tag' : ''}
66
+ autoComplete="off"
67
+ data-testid="color-tag-name"
68
+ />
69
+ <DialogActions>
71
70
  <Button
72
71
  variant="contained"
73
72
  color="primary"
74
- style={{ marginLeft: 20 }}
75
73
  onClick={() => {
76
74
  model.setColorScheme({
77
75
  type: 'tag',
@@ -83,7 +81,10 @@ function ColorByTagDlg(props: {
83
81
  >
84
82
  Submit
85
83
  </Button>
86
- </div>
84
+ <Button variant="contained" color="secondary" onClick={handleClose}>
85
+ Cancel
86
+ </Button>
87
+ </DialogActions>
87
88
  </DialogContent>
88
89
  </Dialog>
89
90
  )