jbrowse-plugin-mafviewer 1.2.1 → 1.2.3

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 (26) hide show
  1. package/README.md +6 -1
  2. package/dist/BgzipTaffyAdapter/BgzipTaffyAdapter.d.ts +11 -2
  3. package/dist/BgzipTaffyAdapter/BgzipTaffyAdapter.js +123 -94
  4. package/dist/BgzipTaffyAdapter/BgzipTaffyAdapter.js.map +1 -1
  5. package/dist/LinearMafDisplay/components/ColorLegend.js +1 -1
  6. package/dist/LinearMafDisplay/components/ColorLegend.js.map +1 -1
  7. package/dist/LinearMafDisplay/components/RectBg.js +1 -1
  8. package/dist/LinearMafDisplay/stateModel.d.ts +10 -0
  9. package/dist/LinearMafDisplay/stateModel.js +30 -10
  10. package/dist/LinearMafDisplay/stateModel.js.map +1 -1
  11. package/dist/LinearMafRenderer/LinearMafRenderer.js +1 -1
  12. package/dist/LinearMafRenderer/LinearMafRenderer.js.map +1 -1
  13. package/dist/LinearMafRenderer/makeImageData.js +25 -9
  14. package/dist/LinearMafRenderer/makeImageData.js.map +1 -1
  15. package/dist/MafAddTrackWorkflow/AddTrackWorkflow.js +1 -1
  16. package/dist/MafAddTrackWorkflow/AddTrackWorkflow.js.map +1 -1
  17. package/dist/jbrowse-plugin-mafviewer.umd.production.min.js +8 -7
  18. package/dist/jbrowse-plugin-mafviewer.umd.production.min.js.map +4 -4
  19. package/package.json +2 -1
  20. package/src/BgzipTaffyAdapter/BgzipTaffyAdapter.ts +135 -99
  21. package/src/LinearMafDisplay/components/ColorLegend.tsx +2 -2
  22. package/src/LinearMafDisplay/components/RectBg.tsx +1 -1
  23. package/src/LinearMafDisplay/stateModel.ts +30 -10
  24. package/src/LinearMafRenderer/LinearMafRenderer.ts +1 -1
  25. package/src/LinearMafRenderer/makeImageData.ts +38 -9
  26. package/src/MafAddTrackWorkflow/AddTrackWorkflow.tsx +9 -11
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.2.1",
2
+ "version": "1.2.3",
3
3
  "license": "MIT",
4
4
  "name": "jbrowse-plugin-mafviewer",
5
5
  "keywords": [
@@ -63,6 +63,7 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@gmod/bgzf-filehandle": "^2.0.4",
66
+ "abortable-promise-cache": "^1.5.0",
66
67
  "buffer": "^6.0.3",
67
68
  "d3-array": "^3.2.4",
68
69
  "d3-hierarchy": "^3.1.2",
@@ -9,8 +9,10 @@ import {
9
9
  SimpleFeature,
10
10
  updateStatus,
11
11
  } from '@jbrowse/core/util'
12
+ import QuickLRU from '@jbrowse/core/util/QuickLRU'
12
13
  import { openLocation } from '@jbrowse/core/util/io'
13
14
  import { ObservableCreate } from '@jbrowse/core/util/rxjs'
15
+ import AbortablePromiseCache from 'abortable-promise-cache'
14
16
  import Long from 'long'
15
17
 
16
18
  import VirtualOffset from './virtualOffset'
@@ -20,6 +22,7 @@ import { parseRowInstructions } from './rowInstructions'
20
22
  import { parseLineByLine } from './util'
21
23
 
22
24
  import type { IndexData, OrganismRecord } from './types'
25
+
23
26
  interface Entry {
24
27
  type: string
25
28
  row: number
@@ -35,11 +38,125 @@ const toP = (s = 0) => +(+s).toFixed(1)
35
38
  export default class BgzipTaffyAdapter extends BaseFeatureDataAdapter {
36
39
  public setupP?: Promise<IndexData>
37
40
 
41
+ private cache = new AbortablePromiseCache({
42
+ cache: new QuickLRU({ maxSize: 50 }),
43
+ // @ts-expect-error
44
+ fill: async ({ nextEntry, firstEntry }, signal, statusCallback) => {
45
+ const file = openLocation(this.getConf('tafGzLocation'))
46
+ const response = await file.read(
47
+ nextEntry.virtualOffset.blockPosition -
48
+ firstEntry.virtualOffset.blockPosition,
49
+ firstEntry.virtualOffset.blockPosition,
50
+ )
51
+ const buffer = await unzip(response)
52
+ const slice = buffer.slice(firstEntry.virtualOffset.dataPosition)
53
+ return this.getChunk(slice, {
54
+ statusCallback: statusCallback as (arg: string) => void,
55
+ signal,
56
+ })
57
+ },
58
+ })
59
+
38
60
  async getRefNames() {
39
61
  const data = await this.setup()
40
62
  return Object.keys(data)
41
63
  }
42
64
 
65
+ async getChunk(buffer: Uint8Array, opts?: BaseOptions) {
66
+ const { statusCallback = () => {} } = opts || {}
67
+ const alignments = {} as Record<string, OrganismRecord>
68
+ const data = [] as Entry[]
69
+ let a0: any
70
+ let j = 0
71
+ let b = 0
72
+ parseLineByLine(buffer, line => {
73
+ if (j++ % 100 === 0) {
74
+ statusCallback(
75
+ `Processing ${toP(b / 1_000_000)}/${toP(buffer.length / 1_000_000)}Mb`,
76
+ )
77
+ }
78
+ b += line.length
79
+ if (line) {
80
+ const [lineData, rowInstructions] = line.split(' ; ')
81
+ if (rowInstructions) {
82
+ for (const ins of parseRowInstructions(rowInstructions)) {
83
+ if (ins.type === 'i') {
84
+ data.splice(ins.row, 0, ins)
85
+ if (!alignments[ins.asm]) {
86
+ alignments[ins.asm] = {
87
+ start: ins.start,
88
+ strand: ins.strand,
89
+ srcSize: ins.length,
90
+ chr: ins.ref,
91
+ data: '',
92
+ }
93
+ }
94
+ const e = alignments[ins.asm]!
95
+ e.data += ' '.repeat(Math.max(0, j - e.data.length - 1)) // catch it up
96
+ } else if (ins.type === 's') {
97
+ if (!alignments[ins.asm]) {
98
+ alignments[ins.asm] = {
99
+ start: ins.start,
100
+ strand: ins.strand,
101
+ srcSize: ins.length,
102
+ chr: ins.ref,
103
+ data: '',
104
+ }
105
+ }
106
+ const e = alignments[ins.asm]!
107
+ e.data += ' '.repeat(Math.max(0, j - e.data.length - 1)) // catch it up
108
+ data[ins.row] = ins
109
+ } else if (ins.type === 'd') {
110
+ data.splice(ins.row, 1)
111
+ }
112
+
113
+ // no gaps for now(?)
114
+ // else if (ins.type === 'g') {
115
+ // console.log('g??')
116
+ // } else if (ins.type === 'G') {
117
+ // console.log('G??')
118
+ // }
119
+ }
120
+ if (!a0) {
121
+ a0 = data[0]
122
+ }
123
+ }
124
+ const lineLen = lineData!.length
125
+
126
+ for (let i = 0; i < lineLen; i++) {
127
+ const letter = lineData![i]
128
+ const r = data[i]
129
+
130
+ if (r) {
131
+ alignments[r.asm]!.data += letter
132
+ } else {
133
+ // not sure why but chr22_KI270731v1_random.taf.gz ends up here
134
+ }
135
+ }
136
+ }
137
+ })
138
+ if (a0) {
139
+ const row0 = alignments[a0.asm]!
140
+
141
+ // see
142
+ // https://github.com/ComparativeGenomicsToolkit/taffy/blob/f5a5354/docs/taffy_utilities.md#referenced-based-maftaf-and-indexing
143
+ // for the significance of row[0]:
144
+ //
145
+ // "An anchor line in TAF is a column from which all sequence
146
+ // coordinates can be deduced without scanning backwards to previous
147
+ // lines "
148
+ return {
149
+ uniqueId: `${row0.start}-${row0.data.length}`,
150
+ start: row0.start,
151
+ end: row0.start + row0.data.length,
152
+ strand: row0.strand,
153
+ alignments,
154
+ seq: row0.data,
155
+ }
156
+ }
157
+ return undefined
158
+ }
159
+
43
160
  setupPre() {
44
161
  if (!this.setupP) {
45
162
  this.setupP = this.readTaiFile().catch((e: unknown) => {
@@ -104,103 +221,23 @@ export default class BgzipTaffyAdapter extends BaseFeatureDataAdapter {
104
221
  return ObservableCreate<Feature>(async observer => {
105
222
  try {
106
223
  const byteRanges = await this.setup()
107
- const buffer = await updateStatus(
224
+ const feat = await updateStatus(
108
225
  'Downloading alignments',
109
226
  statusCallback,
110
227
  () => this.getLines(query, byteRanges),
111
228
  )
112
- if (buffer) {
113
- const alignments = {} as Record<string, OrganismRecord>
114
- const data = [] as Entry[]
115
- let a0: any
116
- let j = 0
117
- let b = 0
118
- parseLineByLine(buffer, line => {
119
- if (j++ % 100 === 0) {
120
- statusCallback(
121
- `Processing ${toP(b / 1_000_000)}/${toP(buffer.length / 1_000_000)}Mb`,
122
- )
123
- }
124
- b += line.length
125
- if (line) {
126
- const [lineData, rowInstructions] = line.split(' ; ')
127
- if (rowInstructions) {
128
- for (const ins of parseRowInstructions(rowInstructions)) {
129
- if (ins.type === 'i') {
130
- data.splice(ins.row, 0, ins)
131
- if (!alignments[ins.asm]) {
132
- alignments[ins.asm] = {
133
- start: ins.start,
134
- strand: ins.strand,
135
- srcSize: ins.length,
136
- chr: ins.ref,
137
- data: '',
138
- }
139
- }
140
- const e = alignments[ins.asm]!
141
- e.data += ' '.repeat(Math.max(0, j - e.data.length)) // catch it up
142
- } else if (ins.type === 's') {
143
- if (!alignments[ins.asm]) {
144
- alignments[ins.asm] = {
145
- start: ins.start,
146
- strand: ins.strand,
147
- srcSize: ins.length,
148
- chr: ins.ref,
149
- data: '',
150
- }
151
- }
152
- const e = alignments[ins.asm]!
153
- e.data += ' '.repeat(Math.max(0, j - e.data.length)) // catch it up
154
- data[ins.row] = ins
155
- } else if (ins.type === 'd') {
156
- data.splice(ins.row, 1)
157
- }
158
-
159
- // no gaps for now(?)
160
- // else if (ins.type === 'g') {
161
- // }
162
- // else if (ins.type === 'G') {
163
- // }
164
- }
165
- if (!a0) {
166
- a0 = data[0]
167
- }
168
- }
169
- const lineLen = lineData!.length
170
- for (let i = 0; i < lineLen; i++) {
171
- const letter = lineData![i]
172
- const r = data[i]
173
- if (r) {
174
- alignments[r.asm]!.data += letter
175
- } else {
176
- // not sure why but chr22_KI270731v1_random.taf.gz ends up here
177
- }
178
- }
179
- }
180
- })
181
- if (a0) {
182
- const row0 = alignments[a0.asm]!
183
-
184
- // see
185
- // https://github.com/ComparativeGenomicsToolkit/taffy/blob/f5a5354/docs/taffy_utilities.md#referenced-based-maftaf-and-indexing
186
- // for the significance of row[0]:
187
- //
188
- // "An anchor line in TAF is a column from which all sequence
189
- // coordinates can be deduced without scanning backwards to previous
190
- // lines "
191
- observer.next(
192
- new SimpleFeature({
193
- uniqueId: `${row0.start}-${row0.data.length}`,
194
- refName: query.refName,
195
- start: row0.start,
196
- end: row0.start + row0.data.length,
197
- strand: row0.strand,
198
- alignments,
199
- seq: row0.data,
200
- }),
201
- )
202
- }
229
+ if (feat) {
230
+ observer.next(
231
+ // @ts-expect-error
232
+ new SimpleFeature({
233
+ ...feat,
234
+ refName: query.refName,
235
+ }),
236
+ )
237
+ } else {
238
+ console.error('no feature found')
203
239
  }
240
+
204
241
  statusCallback('')
205
242
  observer.complete()
206
243
  } catch (e) {
@@ -226,7 +263,6 @@ export default class BgzipTaffyAdapter extends BaseFeatureDataAdapter {
226
263
 
227
264
  // TODO: cache processed large chunks
228
265
  async getLines(query: Region, byteRanges: IndexData) {
229
- const file = openLocation(this.getConf('tafGzLocation'))
230
266
  const records = byteRanges[query.refName]
231
267
  if (records) {
232
268
  let firstEntry
@@ -255,13 +291,13 @@ export default class BgzipTaffyAdapter extends BaseFeatureDataAdapter {
255
291
  // file whn you request e.g. out of range region (e.g. taf in chr22:1-100
256
292
  // and you are at chr22:200-300)
257
293
  if (firstEntry && nextEntry) {
258
- const response = await file.read(
259
- nextEntry.virtualOffset.blockPosition -
260
- firstEntry.virtualOffset.blockPosition,
261
- firstEntry.virtualOffset.blockPosition,
294
+ return this.cache.get(
295
+ `${JSON.stringify(nextEntry)}_${JSON.stringify(firstEntry)}`,
296
+ {
297
+ nextEntry,
298
+ firstEntry,
299
+ },
262
300
  )
263
- const buffer = await unzip(response)
264
- return buffer.slice(firstEntry.virtualOffset.dataPosition)
265
301
  }
266
302
  }
267
303
  return undefined
@@ -43,10 +43,10 @@ const ColorLegend = observer(function ({
43
43
  ? samples.map((sample, idx) => (
44
44
  <text
45
45
  key={`${sample.id}-${idx}`}
46
- y={idx * rowHeight + rowHeight / 2}
47
46
  dominantBaseline="middle"
48
- x={2}
49
47
  fontSize={svgFontSize}
48
+ x={2}
49
+ y={idx * rowHeight + rowHeight / 2}
50
50
  >
51
51
  {sample.label}
52
52
  </text>
@@ -7,7 +7,7 @@ const RectBg = (props: {
7
7
  height: number
8
8
  color?: string
9
9
  }) => {
10
- const { color = 'rgb(255,255,255,0.8)' } = props
10
+ const { color = 'rgb(255,255,255,0.5)' } = props
11
11
  return <rect {...props} fill={color} />
12
12
  }
13
13
 
@@ -244,7 +244,7 @@ export default function stateModelFactory(
244
244
  return {
245
245
  ...s,
246
246
  notReady:
247
- !self.volatileSamples || !self.volatileTree || super.notReady,
247
+ (!self.volatileSamples && !self.volatileTree) || super.notReady,
248
248
  config: rendererConfig,
249
249
  samples,
250
250
  rowHeight,
@@ -260,16 +260,36 @@ export default function stateModelFactory(
260
260
  return [
261
261
  ...superTrackMenuItems(),
262
262
  {
263
- label: 'Set row height',
264
- onClick: () => {
265
- getSession(self).queueDialog(handleClose => [
266
- SetRowHeightDialog,
267
- {
268
- model: self,
269
- handleClose,
263
+ label: 'Set feature height',
264
+ type: 'subMenu',
265
+ subMenu: [
266
+ {
267
+ label: 'Normal',
268
+ onClick: () => {
269
+ self.setRowHeight(15)
270
+ self.setRowProportion(0.8)
270
271
  },
271
- ])
272
- },
272
+ },
273
+ {
274
+ label: 'Compact',
275
+ onClick: () => {
276
+ self.setRowHeight(8)
277
+ self.setRowProportion(0.9)
278
+ },
279
+ },
280
+ {
281
+ label: 'Manually set height',
282
+ onClick: () => {
283
+ getSession(self).queueDialog(handleClose => [
284
+ SetRowHeightDialog,
285
+ {
286
+ model: self,
287
+ handleClose,
288
+ },
289
+ ])
290
+ },
291
+ },
292
+ ],
273
293
  },
274
294
  {
275
295
  label: 'Show all letters',
@@ -42,7 +42,7 @@ export default class LinearMafRenderer extends FeatureRendererType {
42
42
  rowHeight,
43
43
  } = renderProps
44
44
  const region = regions[0]!
45
- const height = samples.length * (rowHeight + 1) + 100
45
+ const height = samples.length * rowHeight + 100
46
46
  const width = (region.end - region.start) / bpPerPx
47
47
  const features = await this.getFeatures(renderProps)
48
48
  const res = await renderToAbstractCanvas(
@@ -1,6 +1,6 @@
1
1
  import { RenderArgsDeserialized } from '@jbrowse/core/pluggableElementTypes/renderers/BoxRendererType'
2
2
  import { createJBrowseTheme } from '@jbrowse/core/ui'
3
- import { Feature, featureSpanPx } from '@jbrowse/core/util'
3
+ import { Feature, featureSpanPx, measureText } from '@jbrowse/core/util'
4
4
 
5
5
  import {
6
6
  fillRect,
@@ -97,7 +97,7 @@ export function makeImageData({
97
97
  if (seq[i] !== '-') {
98
98
  const c = alignment[i]
99
99
  const l = leftPx + scale * o
100
- if (seq[i] === c && c !== '-') {
100
+ if (seq[i] === c && c !== '-' && c !== ' ') {
101
101
  fillRect(ctx, l, offset + t, scale + f, h, canvasWidth)
102
102
  }
103
103
  o++
@@ -183,8 +183,6 @@ export function makeImageData({
183
183
 
184
184
  const t = rowHeight * row
185
185
 
186
- ctx.beginPath()
187
- ctx.fillStyle = 'purple'
188
186
  for (let i = 0, o = 0; i < alignment.length; i++) {
189
187
  let ins = ''
190
188
  while (seq[i] === '-') {
@@ -196,15 +194,46 @@ export function makeImageData({
196
194
  if (ins.length > 0) {
197
195
  const l = leftPx + scale * o - 1
198
196
 
199
- ctx.rect(l, offset + t, 1, h)
200
- if (bpPerPx < 1) {
201
- ctx.rect(l - 2, offset + t, 5, 1)
202
- ctx.rect(l - 2, offset + t + h - 1, 5, 1)
197
+ if (ins.length > 10) {
198
+ const txt = `${ins.length}`
199
+ if (bpPerPx > 10) {
200
+ fillRect(ctx, l - 1, t, 2, h, canvasWidth, 'purple')
201
+ } else if (h > charHeight) {
202
+ const rwidth = measureText(txt)
203
+ const padding = 5
204
+ fillRect(
205
+ ctx,
206
+ l - rwidth / 2 - padding,
207
+ t,
208
+ rwidth + 2 * padding,
209
+ h,
210
+ canvasWidth,
211
+ 'purple',
212
+ )
213
+ ctx.fillStyle = 'white'
214
+ ctx.fillText(txt, l - rwidth / 2, t + h)
215
+ } else {
216
+ const padding = 2
217
+ fillRect(
218
+ ctx,
219
+ l - padding,
220
+ t,
221
+ 2 * padding,
222
+ h,
223
+ canvasWidth,
224
+ 'purple',
225
+ )
226
+ }
227
+ } else {
228
+ fillRect(ctx, l, offset + t, 1, h, canvasWidth, 'purple')
229
+ if (bpPerPx < 0.2 && rowHeight > 5) {
230
+ fillRect(ctx, l - 2, offset + t, 5, 1, canvasWidth)
231
+ fillRect(ctx, l - 2, offset + t + h - 1, 5, 1, canvasWidth)
232
+ }
203
233
  }
204
234
  }
205
235
  o++
206
236
  }
207
- ctx.fill()
208
237
  }
209
238
  }
210
239
  }
@@ -69,17 +69,15 @@ export default function MultiMAFWidget({ model }: { model: AddTrackModel }) {
69
69
  setFileTypeChoice(event.target.value as AdapterTypeOptions)
70
70
  }}
71
71
  >
72
- {['BigMafAdapter', 'MafTabixAdapter', 'BgzipTaffyAdapter'].map(
73
- r => (
74
- <FormControlLabel
75
- key={r}
76
- value={r}
77
- control={<Radio />}
78
- checked={fileTypeChoice === r}
79
- label={r}
80
- />
81
- ),
82
- )}
72
+ {['BigMafAdapter', 'MafTabixAdapter'].map(r => (
73
+ <FormControlLabel
74
+ key={r}
75
+ value={r}
76
+ control={<Radio />}
77
+ checked={fileTypeChoice === r}
78
+ label={r}
79
+ />
80
+ ))}
83
81
  </RadioGroup>
84
82
  </FormControl>
85
83
  {fileTypeChoice === 'BigMafAdapter' ? (