@jbrowse/plugin-alignments 1.7.8 → 1.7.11

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.
@@ -1,30 +1,20 @@
1
1
  import { lazy } from 'react'
2
2
  import PluginManager from '@jbrowse/core/PluginManager'
3
3
  import { ConfigurationSchema } from '@jbrowse/core/configuration'
4
- import { ElementId } from '@jbrowse/core/util/types/mst'
5
4
  import { types } from 'mobx-state-tree'
6
5
  import WidgetType from '@jbrowse/core/pluggableElementTypes/WidgetType'
6
+ import { stateModelFactory as baseModelFactory } from '@jbrowse/core/BaseFeatureWidget'
7
7
 
8
8
  const configSchema = ConfigurationSchema('AlignmentsFeatureWidget', {})
9
9
 
10
10
  export function stateModelFactory(pluginManager: PluginManager) {
11
- return types
12
- .model('AlignmentsFeatureWidget', {
13
- id: ElementId,
11
+ const baseModel = baseModelFactory(pluginManager)
12
+ return types.compose(
13
+ baseModel,
14
+ types.model('AlignmentsFeatureWidget', {
14
15
  type: types.literal('AlignmentsFeatureWidget'),
15
- featureData: types.frozen(),
16
- view: types.safeReference(
17
- pluginManager.pluggableMstType('view', 'stateModel'),
18
- ),
19
- })
20
- .actions(self => ({
21
- setFeatureData(data: unknown) {
22
- self.featureData = data
23
- },
24
- clearFeatureData() {
25
- self.featureData = undefined
26
- },
27
- }))
16
+ }),
17
+ )
28
18
  }
29
19
 
30
20
  export default function register(pluginManager: PluginManager) {
@@ -10,6 +10,7 @@ export interface Mismatch {
10
10
  cliplen?: number
11
11
  }
12
12
  const mdRegex = new RegExp(/(\d+|\^[a-z]+|[a-z])/gi)
13
+ const modificationRegex = new RegExp(/([A-Z])([-+])([^,.?]+)([.?])?/)
13
14
  export function parseCigar(cigar: string) {
14
15
  return (cigar || '').split(/([MIDNSHPX=])/)
15
16
  }
@@ -38,6 +39,7 @@ export function cigarToMismatches(
38
39
  start: roffset + j,
39
40
  type: 'mismatch',
40
41
  base: seq[soffset + j],
42
+ altbase: ref[roffset + j],
41
43
  length: 1,
42
44
  })
43
45
  }
@@ -273,7 +275,7 @@ export function getModificationPositions(
273
275
  const [basemod, ...skips] = mod.split(',')
274
276
 
275
277
  // regexes based on parse_mm.pl from hts-specs
276
- const matches = basemod.match(/([A-Z])([-+])([^,.?]+)([.?])?/)
278
+ const matches = basemod.match(modificationRegex)
277
279
  if (!matches) {
278
280
  throw new Error('bad format for MM tag')
279
281
  }
@@ -328,7 +330,7 @@ export function getModificationTypes(mm: string) {
328
330
  .map(mod => {
329
331
  const [basemod] = mod.split(',')
330
332
 
331
- const matches = basemod.match(/([A-Z])([-+])([^,]+)/)
333
+ const matches = basemod.match(modificationRegex)
332
334
  if (!matches) {
333
335
  throw new Error('bad format for MM tag')
334
336
  }
@@ -1,5 +1,9 @@
1
1
  import { lazy } from 'react'
2
+ import { autorun, observable } from 'mobx'
3
+ import { cast, types, addDisposer, getEnv, Instance } from 'mobx-state-tree'
4
+ import copy from 'copy-to-clipboard'
2
5
  import {
6
+ AnyConfigurationModel,
3
7
  ConfigurationReference,
4
8
  readConfObject,
5
9
  getConf,
@@ -13,26 +17,25 @@ import {
13
17
  Feature,
14
18
  } from '@jbrowse/core/util'
15
19
 
16
- import VisibilityIcon from '@material-ui/icons/Visibility'
17
- import { ContentCopy as ContentCopyIcon } from '@jbrowse/core/ui/Icons'
18
20
  import {
19
21
  LinearGenomeViewModel,
20
22
  BaseLinearDisplay,
21
23
  } from '@jbrowse/plugin-linear-genome-view'
22
- import { cast, types, addDisposer, getEnv, Instance } from 'mobx-state-tree'
23
- import copy from 'copy-to-clipboard'
24
+
25
+ // icons
26
+ import VisibilityIcon from '@material-ui/icons/Visibility'
27
+ import { ContentCopy as ContentCopyIcon } from '@jbrowse/core/ui/Icons'
24
28
  import MenuOpenIcon from '@material-ui/icons/MenuOpen'
25
29
  import SortIcon from '@material-ui/icons/Sort'
26
30
  import PaletteIcon from '@material-ui/icons/Palette'
27
31
  import FilterListIcon from '@material-ui/icons/ClearAll'
28
32
 
29
- import { autorun, observable } from 'mobx'
30
- import { AnyConfigurationModel } from '@jbrowse/core/configuration/configurationSchema'
33
+ // locals
31
34
  import { LinearPileupDisplayConfigModel } from './configSchema'
32
35
  import LinearPileupDisplayBlurb from './components/LinearPileupDisplayBlurb'
33
-
34
36
  import { getUniqueTagValues, getUniqueModificationValues } from '../shared'
35
37
 
38
+ // async
36
39
  const ColorByTagDlg = lazy(() => import('./components/ColorByTag'))
37
40
  const FilterByTagDlg = lazy(() => import('./components/FilterByTag'))
38
41
  const SortByTagDlg = lazy(() => import('./components/SortByTag'))
@@ -94,6 +97,7 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) =>
94
97
  .volatile(() => ({
95
98
  colorTagMap: observable.map<string, string>({}),
96
99
  modificationTagMap: observable.map<string, string>({}),
100
+ featureUnderMouseVolatile: undefined as undefined | Feature,
97
101
  ready: false,
98
102
  }))
99
103
  .actions(self => ({
@@ -128,7 +132,9 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) =>
128
132
  },
129
133
 
130
134
  updateColorTagMap(uniqueTag: string[]) {
131
- // pale color scheme https://cran.r-project.org/web/packages/khroma/vignettes/tol.html e.g. "tol_light"
135
+ // pale color scheme
136
+ // https://cran.r-project.org/web/packages/khroma/vignettes/tol.html
137
+ // e.g. "tol_light"
132
138
  const colorPalette = [
133
139
  '#BBCCEE',
134
140
  'pink',
@@ -150,6 +156,9 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) =>
150
156
  }
151
157
  })
152
158
  },
159
+ setFeatureUnderMouse(feat?: Feature) {
160
+ self.featureUnderMouseVolatile = feat
161
+ },
153
162
  }))
154
163
  .actions(self => ({
155
164
  afterAttach() {
@@ -226,6 +235,48 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) =>
226
235
  { delay: 1000 },
227
236
  ),
228
237
  )
238
+
239
+ // autorun synchronizes featureUnderMouse with featureIdUnderMouse
240
+ addDisposer(
241
+ self,
242
+ autorun(async () => {
243
+ const session = getSession(self)
244
+ try {
245
+ const featureId = self.featureIdUnderMouse
246
+ if (self.featureUnderMouse?.id() !== featureId) {
247
+ if (!featureId) {
248
+ self.setFeatureUnderMouse(undefined)
249
+ } else {
250
+ const sessionId = getRpcSessionId(self)
251
+ const view = getContainingView(self)
252
+ const { feature } = (await session.rpcManager.call(
253
+ sessionId,
254
+ 'CoreGetFeatureDetails',
255
+ {
256
+ featureId,
257
+ sessionId,
258
+ layoutId: view.id,
259
+ rendererType: 'PileupRenderer',
260
+ },
261
+ )) as { feature: unknown }
262
+
263
+ // check featureIdUnderMouse is still the same as the
264
+ // feature.id that was returned e.g. that the user hasn't
265
+ // moused over to a new position during the async operation
266
+ // above
267
+ // @ts-ignore
268
+ if (self.featureIdUnderMouse === feature.uniqueId) {
269
+ // @ts-ignore
270
+ self.setFeatureUnderMouse(new SimpleFeature(feature))
271
+ }
272
+ }
273
+ }
274
+ } catch (e) {
275
+ console.error(e)
276
+ session.notify(`${e}`, 'error')
277
+ }
278
+ }),
279
+ )
229
280
  },
230
281
  selectFeature(feature: Feature) {
231
282
  const session = getSession(self)
@@ -337,6 +388,9 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) =>
337
388
  ? self.mismatchAlpha
338
389
  : readConfObject(this.rendererConfig, 'mismatchAlpha')
339
390
  },
391
+ get featureUnderMouse() {
392
+ return self.featureUnderMouseVolatile
393
+ },
340
394
  }))
341
395
  .views(self => {
342
396
  const {
@@ -395,6 +449,8 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) =>
395
449
  colorBy,
396
450
  filterBy,
397
451
  rpcDriverName,
452
+ currBpPerPx,
453
+ ready,
398
454
  } = self
399
455
 
400
456
  const superProps = superRenderProps()
@@ -403,8 +459,8 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) =>
403
459
  ...superProps,
404
460
  notReady:
405
461
  superProps.notReady ||
406
- !self.ready ||
407
- (sortedBy && self.currBpPerPx !== view.bpPerPx),
462
+ !ready ||
463
+ (sortedBy && currBpPerPx !== view.bpPerPx),
408
464
  rpcDriverName,
409
465
  displayModel: self,
410
466
  sortedBy,
@@ -414,7 +470,7 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) =>
414
470
  modificationTagMap: JSON.parse(JSON.stringify(modificationTagMap)),
415
471
  showSoftClip: self.showSoftClipping,
416
472
  config: self.rendererConfig,
417
- async onFeatureClick(_: unknown, featureId: string | undefined) {
473
+ async onFeatureClick(_: unknown, featureId?: string) {
418
474
  const session = getSession(self)
419
475
  const { rpcManager } = session
420
476
  try {
@@ -444,14 +500,12 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) =>
444
500
  session.notify(`${e}`)
445
501
  }
446
502
  },
503
+
447
504
  onClick() {
448
505
  self.clearFeatureSelection()
449
506
  },
450
507
  // similar to click but opens a menu with further options
451
- async onFeatureContextMenu(
452
- _: unknown,
453
- featureId: string | undefined,
454
- ) {
508
+ async onFeatureContextMenu(_: unknown, featureId?: string) {
455
509
  const session = getSession(self)
456
510
  const { rpcManager } = session
457
511
  try {
@@ -507,19 +561,17 @@ const stateModelFactory = (configSchema: LinearPileupDisplayConfigModel) =>
507
561
  disabled: self.showSoftClipping,
508
562
  subMenu: [
509
563
  ...['Start location', 'Read strand', 'Base pair'].map(
510
- option => {
511
- return {
512
- label: option,
513
- onClick: () => self.setSortedBy(option),
514
- }
515
- },
564
+ option => ({
565
+ label: option,
566
+ onClick: () => self.setSortedBy(option),
567
+ }),
516
568
  ),
517
569
  {
518
570
  label: 'Sort by tag...',
519
571
  onClick: () => {
520
- getSession(self).queueDialog(doneCallback => [
572
+ getSession(self).queueDialog(handleClose => [
521
573
  SortByTagDlg,
522
- { model: self, handleClose: doneCallback },
574
+ { model: self, handleClose },
523
575
  ])
524
576
  },
525
577
  },
@@ -7,37 +7,51 @@ import { Tooltip } from '@jbrowse/plugin-wiggle'
7
7
  type Count = {
8
8
  [key: string]: {
9
9
  total: number
10
+ '-1': number
11
+ '0': number
12
+ '1': number
10
13
  }
11
14
  }
12
15
 
13
16
  type SNPInfo = {
14
- ref: Count
15
17
  cov: Count
16
18
  lowqual: Count
17
19
  noncov: Count
18
20
  delskips: Count
21
+ refbase: string
19
22
  total: number
23
+ ref: number
24
+ all: number
20
25
  '-1': number
21
26
  '0': number
22
27
  '1': number
23
28
  }
24
29
 
25
30
  const en = (n: number) => n.toLocaleString('en-US')
31
+ const toP = (s = 0) => +(+s).toFixed(1)
32
+ const pct = (n: number, total: number) => `${toP((n / (total || 1)) * 100)}%`
26
33
 
27
34
  const TooltipContents = React.forwardRef(
28
- ({ feature }: { feature: Feature }, ref: any) => {
35
+ ({ feature }: { feature: Feature }, reactRef: any) => {
29
36
  const start = feature.get('start')
30
37
  const end = feature.get('end')
31
38
  const name = feature.get('refName')
32
- const info = feature.get('snpinfo') as SNPInfo
39
+ const {
40
+ refbase,
41
+ all,
42
+ total,
43
+ ref,
44
+ '-1': rn1,
45
+ '1': r1,
46
+ '0': r0,
47
+ ...info
48
+ } = feature.get('snpinfo') as SNPInfo
33
49
  const loc = [name, start === end ? en(start) : `${en(start)}..${en(end)}`]
34
50
  .filter(f => !!f)
35
51
  .join(':')
36
52
 
37
- const total = info?.total
38
-
39
53
  return (
40
- <div ref={ref}>
54
+ <div ref={reactRef}>
41
55
  <table>
42
56
  <caption>{loc}</caption>
43
57
  <thead>
@@ -52,37 +66,37 @@ const TooltipContents = React.forwardRef(
52
66
  <tbody>
53
67
  <tr>
54
68
  <td>Total</td>
55
- <td>{info.total}</td>
69
+ <td>{all}</td>
56
70
  </tr>
57
71
  <tr>
58
- <td>REF</td>
59
- <td>{info.ref}</td>
72
+ <td>REF {refbase ? `(${refbase.toUpperCase()})` : ''}</td>
73
+ <td>{ref}</td>
74
+ <td>{pct(ref, all)}</td>
60
75
  <td>
61
- {info['-1'] ? `${info['-1']}(-)` : ''}
62
- {info['1'] ? `${info['1']}(+)` : ''}
76
+ {rn1 ? `${rn1}(-)` : ''}
77
+ {r1 ? `${r1}(+)` : ''}
63
78
  </td>
64
79
  <td />
65
80
  </tr>
66
81
 
67
- {Object.entries(info).map(([key, entry]) =>
68
- Object.entries(entry).map(([base, score]) => (
69
- <tr key={base}>
70
- <td>{base.toUpperCase()}</td>
71
- <td>{score.total}</td>
72
- <td>
73
- {base === 'total' || base === 'skip'
74
- ? '---'
75
- : `${Math.floor(
76
- (score.total / (total || score.total || 1)) * 100,
77
- )}%`}
78
- </td>
79
- <td>
80
- {score['-1'] ? `${score['-1']}(-)` : ''}
81
- {score['1'] ? `${score['1']}(+)` : ''}
82
- </td>
83
- <td>{key}</td>
84
- </tr>
85
- )),
82
+ {Object.entries(info as unknown as Record<string, Count>).map(
83
+ ([key, entry]) =>
84
+ Object.entries(entry).map(([base, score]) => (
85
+ <tr key={base}>
86
+ <td>{base.toUpperCase()}</td>
87
+ <td>{score.total}</td>
88
+ <td>
89
+ {base === 'total' || base === 'skip'
90
+ ? '---'
91
+ : pct(score.total, all)}
92
+ </td>
93
+ <td>
94
+ {score['-1'] ? `${score['-1']}(-)` : ''}
95
+ {score['1'] ? `${score['1']}(+)` : ''}
96
+ </td>
97
+ <td>{key}</td>
98
+ </tr>
99
+ )),
86
100
  )}
87
101
  </tbody>
88
102
  </table>