@jbrowse/plugin-linear-genome-view 1.7.7 → 1.7.10

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 (42) hide show
  1. package/dist/BaseLinearDisplay/components/BaseLinearDisplay.d.ts +1 -5
  2. package/dist/BaseLinearDisplay/components/BaseLinearDisplay.js +32 -120
  3. package/dist/BaseLinearDisplay/components/Tooltip.d.ts +8 -0
  4. package/dist/BaseLinearDisplay/components/Tooltip.js +125 -0
  5. package/dist/BaseLinearDisplay/models/BaseLinearDisplayModel.d.ts +3 -3
  6. package/dist/BaseLinearDisplay/models/BaseLinearDisplayModel.js +3 -4
  7. package/dist/LinearGenomeView/components/ExportSvgDialog.js +35 -25
  8. package/dist/LinearGenomeView/components/Header.js +5 -2
  9. package/dist/LinearGenomeView/components/HelpDialog.js +2 -3
  10. package/dist/LinearGenomeView/components/ImportForm.js +47 -47
  11. package/dist/LinearGenomeView/components/LinearGenomeView.js +6 -2
  12. package/dist/LinearGenomeView/components/LinearGenomeView.test.js +2 -2
  13. package/dist/LinearGenomeView/components/OverviewScaleBar.js +2 -2
  14. package/dist/LinearGenomeView/components/RefNameAutocomplete.d.ts +3 -2
  15. package/dist/LinearGenomeView/components/RefNameAutocomplete.js +7 -5
  16. package/dist/LinearGenomeView/components/ScaleBar.d.ts +8 -4
  17. package/dist/LinearGenomeView/components/ScaleBar.js +8 -3
  18. package/dist/LinearGenomeView/components/SearchBox.js +31 -22
  19. package/dist/LinearGenomeView/components/TrackLabel.js +25 -41
  20. package/dist/LinearGenomeView/index.d.ts +7 -11
  21. package/dist/LinearGenomeView/index.js +60 -33
  22. package/dist/LinearGenomeView/index.test.js +22 -5
  23. package/dist/index.js +22 -11
  24. package/package.json +3 -2
  25. package/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx +4 -89
  26. package/src/BaseLinearDisplay/components/Tooltip.tsx +97 -0
  27. package/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx +11 -4
  28. package/src/LinearGenomeView/components/ExportSvgDialog.tsx +24 -11
  29. package/src/LinearGenomeView/components/Header.tsx +3 -2
  30. package/src/LinearGenomeView/components/HelpDialog.tsx +5 -4
  31. package/src/LinearGenomeView/components/ImportForm.tsx +37 -32
  32. package/src/LinearGenomeView/components/LinearGenomeView.test.js +2 -2
  33. package/src/LinearGenomeView/components/LinearGenomeView.tsx +16 -10
  34. package/src/LinearGenomeView/components/OverviewScaleBar.tsx +3 -4
  35. package/src/LinearGenomeView/components/RefNameAutocomplete.tsx +10 -5
  36. package/src/LinearGenomeView/components/ScaleBar.tsx +6 -9
  37. package/src/LinearGenomeView/components/SearchBox.tsx +20 -4
  38. package/src/LinearGenomeView/components/TrackLabel.tsx +25 -28
  39. package/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.js.snap +4 -21
  40. package/src/LinearGenomeView/index.test.ts +20 -5
  41. package/src/LinearGenomeView/index.tsx +56 -27
  42. package/src/index.ts +35 -30
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
  import { getConf, readConfObject } from '@jbrowse/core/configuration'
3
- import { Menu } from '@jbrowse/core/ui'
3
+ import CascadingMenu from '@jbrowse/core/ui/CascadingMenu'
4
4
  import { getSession, getContainingView } from '@jbrowse/core/util'
5
5
  import { BaseTrackModel } from '@jbrowse/core/pluggableElementTypes/models'
6
6
  import {
@@ -11,14 +11,21 @@ import {
11
11
  makeStyles,
12
12
  } from '@material-ui/core'
13
13
 
14
+ import {
15
+ bindTrigger,
16
+ bindPopover,
17
+ usePopupState,
18
+ } from 'material-ui-popup-state/hooks'
19
+
20
+ import clsx from 'clsx'
21
+ import { observer } from 'mobx-react'
22
+ import { Instance } from 'mobx-state-tree'
23
+
14
24
  // icons
15
25
  import MoreVertIcon from '@material-ui/icons/MoreVert'
16
26
  import DragIcon from '@material-ui/icons/DragIndicator'
17
27
  import CloseIcon from '@material-ui/icons/Close'
18
28
 
19
- import clsx from 'clsx'
20
- import { observer } from 'mobx-react'
21
- import { Instance } from 'mobx-state-tree'
22
29
  import { LinearGenomeViewStateModel } from '..'
23
30
 
24
31
  const useStyles = makeStyles(theme => ({
@@ -54,22 +61,20 @@ const useStyles = makeStyles(theme => ({
54
61
  type LGV = Instance<LinearGenomeViewStateModel>
55
62
 
56
63
  const TrackLabel = React.forwardRef(
57
- (props: { track: BaseTrackModel; className?: string }, ref) => {
64
+ (
65
+ { track, className }: { track: BaseTrackModel; className?: string },
66
+ ref,
67
+ ) => {
58
68
  const classes = useStyles()
59
- const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null)
60
- const { track, className } = props
61
69
  const view = getContainingView(track) as LGV
62
70
  const session = getSession(track)
63
71
  const trackConf = track.configuration
64
72
  const trackId = getConf(track, 'trackId')
65
73
 
66
- const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
67
- setAnchorEl(event.currentTarget)
68
- }
69
-
70
- const handleClose = () => {
71
- setAnchorEl(null)
72
- }
74
+ const popupState = usePopupState({
75
+ popupId: 'trackLabelMenu',
76
+ variant: 'popover',
77
+ })
73
78
 
74
79
  const onDragStart = (event: React.DragEvent<HTMLSpanElement>) => {
75
80
  const target = event.target as HTMLElement
@@ -91,17 +96,12 @@ const TrackLabel = React.forwardRef(
91
96
  if (getConf(track, 'type') === 'ReferenceSequenceTrack') {
92
97
  const r = session.assemblies.find(a => a.sequence === trackConf)
93
98
  trackName =
94
- readConfObject(trackConf, 'name') ||
99
+ trackName ||
95
100
  (r
96
101
  ? `Reference Sequence (${readConfObject(r, 'name')})`
97
102
  : 'Reference Sequence')
98
103
  }
99
104
 
100
- function handleMenuItemClick(_: unknown, callback: Function) {
101
- callback()
102
- handleClose()
103
- }
104
-
105
105
  const items = [
106
106
  ...(session.getTrackActionMenuItems?.(trackConf) || []),
107
107
  ...track.trackMenuItems(),
@@ -135,9 +135,7 @@ const TrackLabel = React.forwardRef(
135
135
  {trackName}
136
136
  </Typography>
137
137
  <IconButton
138
- aria-controls="simple-menu"
139
- aria-haspopup="true"
140
- onClick={handleClick}
138
+ {...bindTrigger(popupState)}
141
139
  className={classes.iconButton}
142
140
  color="secondary"
143
141
  data-testid="track_menu_icon"
@@ -146,12 +144,11 @@ const TrackLabel = React.forwardRef(
146
144
  <MoreVertIcon />
147
145
  </IconButton>
148
146
  </Paper>
149
- <Menu
150
- anchorEl={anchorEl}
151
- onMenuItemClick={handleMenuItemClick}
152
- open={Boolean(anchorEl)}
153
- onClose={handleClose}
147
+ <CascadingMenu
148
+ {...bindPopover(popupState)}
149
+ onMenuItemClick={(_: unknown, callback: Function) => callback()}
154
150
  menuItems={items}
151
+ popupState={popupState}
155
152
  />
156
153
  </>
157
154
  )
@@ -255,8 +255,7 @@ exports[`<LinearGenomeView /> renders one track, one region 1`] = `
255
255
  <p
256
256
  class="MuiTypography-root makeStyles-bp MuiTypography-body2 MuiTypography-colorTextSecondary"
257
257
  >
258
- 100
259
- bp
258
+ 100bp
260
259
  </p>
261
260
  <div
262
261
  class="makeStyles-container"
@@ -425,13 +424,7 @@ exports[`<LinearGenomeView /> renders one track, one region 1`] = `
425
424
  <div
426
425
  class="makeStyles-tick"
427
426
  style="left: -1px;"
428
- >
429
- <p
430
- class="MuiTypography-root makeStyles-majorTickLabel MuiTypography-body1"
431
- >
432
- 0
433
- </p>
434
- </div>
427
+ />
435
428
  </div>
436
429
  <div
437
430
  class="makeStyles-boundaryPaddingBlock"
@@ -500,7 +493,6 @@ exports[`<LinearGenomeView /> renders one track, one region 1`] = `
500
493
  Foo Track
501
494
  </span>
502
495
  <button
503
- aria-controls="simple-menu"
504
496
  aria-haspopup="true"
505
497
  class="MuiButtonBase-root MuiIconButton-root makeStyles-iconButton MuiIconButton-colorSecondary"
506
498
  data-testid="track_menu_icon"
@@ -850,8 +842,7 @@ exports[`<LinearGenomeView /> renders two tracks, two regions 1`] = `
850
842
  <p
851
843
  class="MuiTypography-root makeStyles-bp MuiTypography-body2 MuiTypography-colorTextSecondary"
852
844
  >
853
- 798
854
- bp
845
+ 798bp
855
846
  </p>
856
847
  <div
857
848
  class="makeStyles-container"
@@ -1239,13 +1230,7 @@ exports[`<LinearGenomeView /> renders two tracks, two regions 1`] = `
1239
1230
  <div
1240
1231
  class="makeStyles-tick"
1241
1232
  style="left: -1px;"
1242
- >
1243
- <p
1244
- class="MuiTypography-root makeStyles-majorTickLabel MuiTypography-body1"
1245
- >
1246
- 0
1247
- </p>
1248
- </div>
1233
+ />
1249
1234
  </div>
1250
1235
  <div
1251
1236
  class="makeStyles-interRegionPaddingBlock"
@@ -1390,7 +1375,6 @@ exports[`<LinearGenomeView /> renders two tracks, two regions 1`] = `
1390
1375
  Foo Track
1391
1376
  </span>
1392
1377
  <button
1393
- aria-controls="simple-menu"
1394
1378
  aria-haspopup="true"
1395
1379
  class="MuiButtonBase-root MuiIconButton-root makeStyles-iconButton MuiIconButton-colorSecondary"
1396
1380
  data-testid="track_menu_icon"
@@ -1533,7 +1517,6 @@ exports[`<LinearGenomeView /> renders two tracks, two regions 1`] = `
1533
1517
  Bar Track
1534
1518
  </span>
1535
1519
  <button
1536
- aria-controls="simple-menu"
1537
1520
  aria-haspopup="true"
1538
1521
  class="MuiButtonBase-root MuiIconButton-root makeStyles-iconButton MuiIconButton-colorSecondary"
1539
1522
  data-testid="track_menu_icon"
@@ -72,7 +72,7 @@ function initialize() {
72
72
  })
73
73
  .actions(self => ({
74
74
  isValidRefName(str: string) {
75
- return !str.includes(':')
75
+ return str === 'ctgA' || str === 'ctgB'
76
76
  },
77
77
  get(str: string) {
78
78
  return self.assemblies.get(str)
@@ -989,8 +989,23 @@ test('multi region', () => {
989
989
  model.navToLocString('ctgA ctgB')
990
990
  expect(model.displayedRegions[0].refName).toBe('ctgA')
991
991
  expect(model.displayedRegions[1].refName).toBe('ctgB')
992
- // [
993
- // { refName: 'ctgA', start: 0, end: 50001 },
994
- // { refName: 'ctgB', start: 0, end: 6079 },
995
- // ])
992
+ })
993
+
994
+ test('space separated locstring', () => {
995
+ const { Session, LinearGenomeModel } = initialize()
996
+ const model = Session.create({
997
+ configuration: {},
998
+ }).setView(
999
+ LinearGenomeModel.create({
1000
+ type: 'LinearGenomeView',
1001
+ tracks: [{ name: 'foo track', type: 'BasicTrack' }],
1002
+ }),
1003
+ )
1004
+ model.setWidth(800)
1005
+ model.setDisplayedRegions(volvoxDisplayedRegions.slice(0, 1))
1006
+
1007
+ model.navToLocString('ctgA 0 100')
1008
+
1009
+ expect(model.offsetPx).toBe(0)
1010
+ expect(model.bpPerPx).toBe(0.125)
996
1011
  })
@@ -67,6 +67,7 @@ export interface BpOffset {
67
67
 
68
68
  export interface ExportSvgOptions {
69
69
  rasterizeLayers?: boolean
70
+ filename?: string
70
71
  }
71
72
 
72
73
  function calculateVisibleLocStrings(contentBlocks: BaseBlock[]) {
@@ -126,6 +127,7 @@ export function stateModelFactory(pluginManager: PluginManager) {
126
127
  ),
127
128
  hideHeader: false,
128
129
  hideHeaderOverview: false,
130
+ hideNoTracksActive: false,
129
131
  trackSelectorType: types.optional(
130
132
  types.enumeration(['hierarchical']),
131
133
  'hierarchical',
@@ -176,11 +178,18 @@ export function stateModelFactory(pluginManager: PluginManager) {
176
178
  get interRegionPaddingWidth() {
177
179
  return INTER_REGION_PADDING_WIDTH
178
180
  },
181
+
182
+ get assemblyNames() {
183
+ return [
184
+ ...new Set(self.displayedRegions.map(region => region.assemblyName)),
185
+ ]
186
+ },
179
187
  }))
180
188
  .views(self => ({
181
189
  get assemblyErrors() {
182
190
  const { assemblyManager } = getSession(self)
183
- return this.assemblyNames
191
+ const { assemblyNames } = self
192
+ return assemblyNames
184
193
  .map(a => assemblyManager.get(a)?.error)
185
194
  .filter(f => !!f)
186
195
  .join(', ')
@@ -188,9 +197,8 @@ export function stateModelFactory(pluginManager: PluginManager) {
188
197
 
189
198
  get assembliesInitialized() {
190
199
  const { assemblyManager } = getSession(self)
191
- return this.assemblyNames.every(
192
- a => assemblyManager.get(a)?.initialized,
193
- )
200
+ const { assemblyNames } = self
201
+ return assemblyNames.every(a => assemblyManager.get(a)?.initialized)
194
202
  },
195
203
  get initialized() {
196
204
  return self.volatileWidth !== undefined && this.assembliesInitialized
@@ -272,11 +280,6 @@ export function stateModelFactory(pluginManager: PluginManager) {
272
280
  }
273
281
  },
274
282
 
275
- get assemblyNames() {
276
- return [
277
- ...new Set(self.displayedRegions.map(region => region.assemblyName)),
278
- ]
279
- },
280
283
  searchScope(assemblyName: string) {
281
284
  return {
282
285
  assemblyName,
@@ -285,13 +288,6 @@ export function stateModelFactory(pluginManager: PluginManager) {
285
288
  }
286
289
  },
287
290
 
288
- /**
289
- * @param refName - refName of the displayedRegion
290
- * @param coord - coordinate at the displayed Region
291
- * @param regionNumber - optional param used as identifier when
292
- * there are multiple displayedRegions with the same refName
293
- * @returns offsetPx of the displayed region that it lands in
294
- */
295
291
  bpToPx({
296
292
  refName,
297
293
  coord,
@@ -395,10 +391,8 @@ export function stateModelFactory(pluginManager: PluginManager) {
395
391
  track => track.configuration.trackId,
396
392
  )
397
393
  results.forEach(result => {
398
- if (openTrackIds !== []) {
399
- if (openTrackIds.includes(result.trackId)) {
400
- result.updateScore(result.getScore() + 1)
401
- }
394
+ if (openTrackIds.includes(result.trackId)) {
395
+ result.updateScore(result.getScore() + 1)
402
396
  }
403
397
  })
404
398
  return results
@@ -406,7 +400,7 @@ export function stateModelFactory(pluginManager: PluginManager) {
406
400
 
407
401
  // modifies view menu action onClick to apply to all tracks of same type
408
402
  rewriteOnClicks(trackType: string, viewMenuActions: MenuItem[]) {
409
- viewMenuActions.forEach((action: MenuItem) => {
403
+ viewMenuActions.forEach(action => {
410
404
  // go to lowest level menu
411
405
  if ('subMenu' in action) {
412
406
  this.rewriteOnClicks(trackType, action.subMenu)
@@ -463,6 +457,9 @@ export function stateModelFactory(pluginManager: PluginManager) {
463
457
  toggleHeaderOverview() {
464
458
  self.hideHeaderOverview = !self.hideHeaderOverview
465
459
  },
460
+ toggleNoTracksActive() {
461
+ self.hideNoTracksActive = !self.hideNoTracksActive
462
+ },
466
463
 
467
464
  scrollTo(offsetPx: number) {
468
465
  const newOffsetPx = clamp(offsetPx, self.minOffset, self.maxOffset)
@@ -674,13 +671,38 @@ export function stateModelFactory(pluginManager: PluginManager) {
674
671
  const { assemblyManager } = getSession(self)
675
672
  const { isValidRefName } = assemblyManager
676
673
  const assemblyName = optAssemblyName || assemblyNames[0]
674
+ let parsedLocStrings
675
+ const inputs = locString
676
+ .split(/(\s+)/)
677
+ .map(f => f.trim())
678
+ .filter(f => !!f)
677
679
 
678
- const parsedLocStrings = locString
679
- .split(' ')
680
- .filter(f => !!f.trim())
681
- .map(l => parseLocString(l, ref => isValidRefName(ref, assemblyName)))
680
+ // first try interpreting as a whitespace-separated sequence of
681
+ // multiple locstrings
682
+ try {
683
+ parsedLocStrings = inputs.map(l =>
684
+ parseLocString(l, ref => isValidRefName(ref, assemblyName)),
685
+ )
686
+ } catch (e) {
687
+ // if this fails, try interpreting as a whitespace-separated refname,
688
+ // start, end if start and end are integer inputs
689
+ const [refName, start, end] = inputs
690
+ if (
691
+ `${e}`.match(/Unknown reference sequence/) &&
692
+ Number.isInteger(+start) &&
693
+ Number.isInteger(+end)
694
+ ) {
695
+ parsedLocStrings = [
696
+ parseLocString(refName + ':' + start + '..' + end, ref =>
697
+ isValidRefName(ref, assemblyName),
698
+ ),
699
+ ]
700
+ } else {
701
+ throw e
702
+ }
703
+ }
682
704
 
683
- const locations = parsedLocStrings.map(region => {
705
+ const locations = parsedLocStrings?.map(region => {
684
706
  const asmName = region.assemblyName || assemblyName
685
707
  const asm = assemblyManager.get(asmName)
686
708
  const { refName } = region
@@ -1206,6 +1228,13 @@ export function stateModelFactory(pluginManager: PluginManager) {
1206
1228
  onClick: self.toggleHeaderOverview,
1207
1229
  disabled: self.hideHeader,
1208
1230
  },
1231
+ {
1232
+ label: 'Show no tracks active button',
1233
+ icon: VisibilityIcon,
1234
+ type: 'checkbox',
1235
+ checked: !self.hideNoTracksActive,
1236
+ onClick: self.toggleNoTracksActive,
1237
+ },
1209
1238
  {
1210
1239
  label: 'Track labels',
1211
1240
  icon: LabelIcon,
@@ -1330,7 +1359,7 @@ export function stateModelFactory(pluginManager: PluginManager) {
1330
1359
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1331
1360
  const html = await renderToSvg(self as any, opts)
1332
1361
  const blob = new Blob([html], { type: 'image/svg+xml' })
1333
- saveAs(blob, 'image.svg')
1362
+ saveAs(blob, opts.filename || 'image.svg')
1334
1363
  },
1335
1364
  }))
1336
1365
  .views(self => ({
package/src/index.ts CHANGED
@@ -137,42 +137,47 @@ export default class LinearGenomeViewPlugin extends Plugin {
137
137
  loc: string
138
138
  tracks?: string[]
139
139
  }) => {
140
- const { assemblyManager } = session
141
- const view = session.addView('LinearGenomeView', {}) as LGV
140
+ try {
141
+ const { assemblyManager } = session
142
+ const view = session.addView('LinearGenomeView', {}) as LGV
142
143
 
143
- await when(() => !!view.volatileWidth)
144
+ await when(() => !!view.volatileWidth)
144
145
 
145
- if (!assembly) {
146
- throw new Error(
147
- 'No assembly provided when launching linear genome view',
148
- )
149
- }
146
+ if (!assembly) {
147
+ throw new Error(
148
+ 'No assembly provided when launching linear genome view',
149
+ )
150
+ }
150
151
 
151
- const asm = await assemblyManager.waitForAssembly(assembly)
152
- if (!asm) {
153
- throw new Error(
154
- `Assembly "${assembly}" not found when launching linear genome view`,
155
- )
156
- }
152
+ const asm = await assemblyManager.waitForAssembly(assembly)
153
+ if (!asm) {
154
+ throw new Error(
155
+ `Assembly "${assembly}" not found when launching linear genome view`,
156
+ )
157
+ }
157
158
 
158
- view.navToLocString(loc, assembly)
159
-
160
- const idsNotFound = [] as string[]
161
- tracks.forEach(track => {
162
- try {
163
- view.showTrack(track)
164
- } catch (e) {
165
- if (`${e}`.match('Could not resolve identifier')) {
166
- idsNotFound.push(track)
167
- } else {
168
- throw e
159
+ view.navToLocString(loc, assembly)
160
+
161
+ const idsNotFound = [] as string[]
162
+ tracks.forEach(track => {
163
+ try {
164
+ view.showTrack(track)
165
+ } catch (e) {
166
+ if (`${e}`.match('Could not resolve identifier')) {
167
+ idsNotFound.push(track)
168
+ } else {
169
+ throw e
170
+ }
169
171
  }
172
+ })
173
+ if (idsNotFound.length) {
174
+ throw new Error(
175
+ `Could not resolve identifiers: ${idsNotFound.join(',')}`,
176
+ )
170
177
  }
171
- })
172
- if (idsNotFound.length) {
173
- throw new Error(
174
- `Could not resolve identifiers: ${idsNotFound.join(',')}`,
175
- )
178
+ } catch (e) {
179
+ session.notify(`${e}`, 'error')
180
+ throw e
176
181
  }
177
182
  },
178
183
  )