jbrowse-plugin-graphgenomeviewer 7.4.4 → 7.5.0

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 (129) hide show
  1. package/dist/chunks/{GbzBaseSyntenyAdapter-A5MNPCKX.js → GbzBaseSyntenyAdapter-4KVWVUN6.js} +2 -2
  2. package/dist/chunks/{GbzBaseSyntenyAdapter-A5MNPCKX.js.map → GbzBaseSyntenyAdapter-4KVWVUN6.js.map} +1 -1
  3. package/dist/chunks/{GraphGenomeView-SEAPDRXS.js → GraphGenomeView-KI4FQ5A7.js} +2 -2
  4. package/dist/chunks/GraphTrackSettingsDialog-UQNMDXTE.js +4 -0
  5. package/dist/chunks/GraphTrackSettingsDialog-UQNMDXTE.js.map +7 -0
  6. package/dist/chunks/{HighlightColorDialog-QAYWLO5I.js → HighlightColorDialog-D4RCUCUF.js} +2 -2
  7. package/dist/chunks/LinearGraphDisplay-RPZUZSM2.js +2 -0
  8. package/dist/chunks/{LinearGraphDisplay-ZC4BQ27J.js.map → LinearGraphDisplay-RPZUZSM2.js.map} +2 -2
  9. package/dist/chunks/{MinigraphBubbleAdapter-3QM4XV47.js → MinigraphBubbleAdapter-TJYI7KEN.js} +2 -2
  10. package/dist/chunks/RgfaTabixAdapter-5ZMJLFZP.js +2 -0
  11. package/dist/chunks/RgfaTabixAdapter-5ZMJLFZP.js.map +7 -0
  12. package/dist/chunks/{WalkFormsAdapter-FT2SW4H6.js → WalkFormsAdapter-CAJOWOEX.js} +2 -2
  13. package/dist/chunks/WalkTabixSyntenyAdapter-VI3KA2GX.js +2 -0
  14. package/dist/chunks/{WalkTabixSyntenyAdapter-3WRVNDLB.js.map → WalkTabixSyntenyAdapter-VI3KA2GX.js.map} +2 -2
  15. package/dist/chunks/{chunk-6WUWQX2P.js → chunk-22ANI2KL.js} +2 -2
  16. package/dist/chunks/{chunk-6WUWQX2P.js.map → chunk-22ANI2KL.js.map} +1 -1
  17. package/dist/chunks/chunk-42KD4AVY.js +2 -0
  18. package/dist/chunks/{chunk-HCDKP3IP.js.map → chunk-42KD4AVY.js.map} +4 -4
  19. package/dist/chunks/chunk-4NIWMRKH.js +2 -0
  20. package/dist/chunks/{chunk-S333HOG5.js.map → chunk-4NIWMRKH.js.map} +4 -4
  21. package/dist/chunks/chunk-BCOXRUNQ.js +3 -0
  22. package/dist/chunks/chunk-BCOXRUNQ.js.map +7 -0
  23. package/dist/chunks/chunk-DAPI7M4M.js +4 -0
  24. package/dist/chunks/chunk-DAPI7M4M.js.map +7 -0
  25. package/dist/chunks/{chunk-G53BI7VK.js → chunk-DEWZOJSC.js} +2 -2
  26. package/dist/chunks/{chunk-G53BI7VK.js.map → chunk-DEWZOJSC.js.map} +1 -1
  27. package/dist/chunks/{chunk-6SWXMRAV.js → chunk-FCMXEFJA.js} +1 -1
  28. package/dist/chunks/{chunk-6SWXMRAV.js.map → chunk-FCMXEFJA.js.map} +1 -1
  29. package/dist/chunks/{chunk-P3FBI5ML.js → chunk-GYY6LF5L.js} +1 -1
  30. package/dist/chunks/{chunk-P3FBI5ML.js.map → chunk-GYY6LF5L.js.map} +1 -1
  31. package/dist/chunks/{chunk-HF4H6LQI.js → chunk-HHMJNAA3.js} +2 -2
  32. package/dist/chunks/{chunk-XXBE4IFZ.js → chunk-HTTONSO4.js} +2 -2
  33. package/dist/chunks/{chunk-XXBE4IFZ.js.map → chunk-HTTONSO4.js.map} +1 -1
  34. package/dist/chunks/{chunk-MUONTMYZ.js → chunk-K4JL57NR.js} +2 -2
  35. package/dist/chunks/chunk-KOC23YNN.js +4 -0
  36. package/dist/chunks/chunk-KOC23YNN.js.map +7 -0
  37. package/dist/chunks/{chunk-OJWBLPPH.js → chunk-LTNFVL2F.js} +2 -2
  38. package/dist/chunks/chunk-OZEKEYT3.js +2 -0
  39. package/dist/chunks/{chunk-6VNU7SD4.js.map → chunk-OZEKEYT3.js.map} +4 -4
  40. package/dist/chunks/{chunk-KIYRMZWP.js → chunk-POUV7OPY.js} +2 -2
  41. package/dist/chunks/{chunk-KIYRMZWP.js.map → chunk-POUV7OPY.js.map} +1 -1
  42. package/dist/chunks/{chunk-LE7FZM6Y.js → chunk-SPL3UO2D.js} +2 -2
  43. package/dist/chunks/{chunk-LE7FZM6Y.js.map → chunk-SPL3UO2D.js.map} +1 -1
  44. package/dist/chunks/chunk-UR2MJJUP.js +2 -0
  45. package/dist/chunks/chunk-VTMJQFN5.js +2 -0
  46. package/dist/chunks/chunk-VTMJQFN5.js.map +7 -0
  47. package/dist/chunks/{chunk-2KVE7YAS.js → chunk-XZGYJ3JC.js} +1 -1
  48. package/dist/chunks/{chunk-2KVE7YAS.js.map → chunk-XZGYJ3JC.js.map} +1 -1
  49. package/dist/chunks/{chunk-7RJMG6I2.js → chunk-Z3IKZAYC.js} +1 -1
  50. package/dist/chunks/{chunk-7RJMG6I2.js.map → chunk-Z3IKZAYC.js.map} +1 -1
  51. package/dist/chunks/{chunk-FTXDTU27.js → chunk-ZV5FNOHS.js} +3 -3
  52. package/dist/chunks/{chunk-FTXDTU27.js.map → chunk-ZV5FNOHS.js.map} +2 -2
  53. package/dist/chunks/{renderSvg-ZIUOPWJQ.js → renderSvg-3PPAEMBT.js} +2 -2
  54. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +4 -3
  55. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
  56. package/package.json +2 -2
  57. package/src/GraphComputeLayout.test.ts +24 -0
  58. package/src/GraphComputeLayout.ts +4 -2
  59. package/src/GraphGenomeView/components/GraphSettingsDialog.tsx +16 -0
  60. package/src/GraphGenomeView/components/WalkRowsOverlay.tsx +83 -0
  61. package/src/GraphGenomeView/components/usePaneGestures.test.tsx +82 -5
  62. package/src/GraphGenomeView/components/usePaneGestures.ts +10 -4
  63. package/src/GraphGenomeView/graphLayers.ts +14 -1
  64. package/src/GraphGenomeView/legendSections.ts +32 -0
  65. package/src/GraphGenomeView/model.test.ts +292 -4
  66. package/src/GraphGenomeView/model.ts +3 -10
  67. package/src/GraphGenomeView/nodeSize.test.ts +21 -5
  68. package/src/GraphGenomeView/nodeSize.ts +24 -8
  69. package/src/GraphGenomeView/pane/fitViews.ts +5 -1
  70. package/src/GraphGenomeView/pane/graphViews.ts +96 -16
  71. package/src/GraphGenomeView/pane/launchActions.ts +55 -11
  72. package/src/GraphGenomeView/pane/launchViews.ts +46 -0
  73. package/src/GraphGenomeView/pane/loadActions.ts +85 -25
  74. package/src/GraphGenomeView/pane/morph.test.ts +25 -1
  75. package/src/GraphGenomeView/pane/morph.ts +3 -1
  76. package/src/GraphGenomeView/pane/paneBase.ts +5 -0
  77. package/src/GraphGenomeView/pane/renderingActions.ts +61 -2
  78. package/src/GraphGenomeView/pane/settingActions.ts +62 -4
  79. package/src/GraphGenomeView/pane/walkRowViews.ts +17 -6
  80. package/src/GraphGenomeView/tubeMapPanels.ts +2 -1
  81. package/src/GraphGenomeView/viewModel.ts +4 -6
  82. package/src/GraphGenomeView/walkRowBlocks.ts +50 -0
  83. package/src/LinearGraphDisplay/LinearGraphDisplay.test.ts +35 -5
  84. package/src/LinearGraphDisplay/components/GraphStatusChrome.test.tsx +18 -0
  85. package/src/LinearGraphDisplay/components/GraphStatusChrome.tsx +4 -2
  86. package/src/LinearGraphDisplay/components/SubgraphHaplotypesField.test.tsx +41 -1
  87. package/src/LinearGraphDisplay/components/SubgraphHaplotypesField.tsx +33 -19
  88. package/src/LinearGraphDisplay/configSchema.ts +4 -3
  89. package/src/RgfaTabixAdapter/RgfaTabixAdapter.ts +2 -9
  90. package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.ts +3 -6
  91. package/src/hoverSync/graphViewHighlights.test.ts +53 -0
  92. package/src/hoverSync/graphViewHighlights.ts +25 -0
  93. package/src/hoverSync/lgvHover.test.ts +100 -1
  94. package/src/hoverSync/lgvHover.ts +77 -6
  95. package/src/launchFromGraph/contributors.test.ts +52 -0
  96. package/src/launchFromGraph/contributors.ts +105 -1
  97. package/src/launchFromGraph/launchFromGraph.ts +2 -2
  98. package/src/panSNTabix.test.ts +28 -0
  99. package/src/panSNTabix.ts +26 -8
  100. package/src/version.ts +1 -1
  101. package/dist/chunks/GraphTrackSettingsDialog-EVIIGC5G.js +0 -6
  102. package/dist/chunks/GraphTrackSettingsDialog-EVIIGC5G.js.map +0 -7
  103. package/dist/chunks/LinearGraphDisplay-ZC4BQ27J.js +0 -2
  104. package/dist/chunks/RgfaTabixAdapter-XSOIEWBK.js +0 -2
  105. package/dist/chunks/RgfaTabixAdapter-XSOIEWBK.js.map +0 -7
  106. package/dist/chunks/WalkTabixSyntenyAdapter-3WRVNDLB.js +0 -2
  107. package/dist/chunks/chunk-6VNU7SD4.js +0 -2
  108. package/dist/chunks/chunk-GRTN25B6.js +0 -3
  109. package/dist/chunks/chunk-GRTN25B6.js.map +0 -7
  110. package/dist/chunks/chunk-HCDKP3IP.js +0 -2
  111. package/dist/chunks/chunk-LE43I4PT.js +0 -4
  112. package/dist/chunks/chunk-LE43I4PT.js.map +0 -7
  113. package/dist/chunks/chunk-QV7SSOMJ.js +0 -2
  114. package/dist/chunks/chunk-S333HOG5.js +0 -2
  115. package/dist/chunks/chunk-VZQ6VOUP.js +0 -2
  116. package/dist/chunks/chunk-VZQ6VOUP.js.map +0 -7
  117. package/dist/chunks/chunk-WLRRI35A.js +0 -3
  118. package/dist/chunks/chunk-WLRRI35A.js.map +0 -7
  119. package/src/launchFromGraph/launchTracks.test.ts +0 -95
  120. package/src/launchFromGraph/launchTracks.ts +0 -49
  121. /package/dist/chunks/{GraphGenomeView-SEAPDRXS.js.map → GraphGenomeView-KI4FQ5A7.js.map} +0 -0
  122. /package/dist/chunks/{HighlightColorDialog-QAYWLO5I.js.map → HighlightColorDialog-D4RCUCUF.js.map} +0 -0
  123. /package/dist/chunks/{MinigraphBubbleAdapter-3QM4XV47.js.map → MinigraphBubbleAdapter-TJYI7KEN.js.map} +0 -0
  124. /package/dist/chunks/{WalkFormsAdapter-FT2SW4H6.js.map → WalkFormsAdapter-CAJOWOEX.js.map} +0 -0
  125. /package/dist/chunks/{chunk-HF4H6LQI.js.map → chunk-HHMJNAA3.js.map} +0 -0
  126. /package/dist/chunks/{chunk-MUONTMYZ.js.map → chunk-K4JL57NR.js.map} +0 -0
  127. /package/dist/chunks/{chunk-OJWBLPPH.js.map → chunk-LTNFVL2F.js.map} +0 -0
  128. /package/dist/chunks/{chunk-QV7SSOMJ.js.map → chunk-UR2MJJUP.js.map} +0 -0
  129. /package/dist/chunks/{renderSvg-ZIUOPWJQ.js.map → renderSvg-3PPAEMBT.js.map} +0 -0
@@ -1,3 +1,4 @@
1
+ import { sameBubble } from '@jbrowse/bandage-core/bubbles/bubbleLine'
1
2
  import { contains } from '@jbrowse/bandage-core/viewport'
2
3
  import { getSession } from '@jbrowse/core/util'
3
4
  import { addDisposer } from '@jbrowse/mobx-state-tree'
@@ -11,11 +12,16 @@ import {
11
12
  paneViewportOf,
12
13
  } from './paneBase'
13
14
  import {
15
+ bubbleForLgvHover,
16
+ hoverFromPair,
14
17
  hoverInRegion,
15
18
  nodeForLgvHover,
16
19
  readLgvHover,
20
+ readSelectedFeature,
17
21
  } from '../../hoverSync/lgvHover'
18
22
  import { walksForRow } from '../../hoverSync/rowWalks'
23
+ import { isRecord } from '../../isRecord'
24
+ import { withRows } from '../../launchFromGraph/linearViewTarget'
19
25
  import { trackedDpr } from '../trackedDpr'
20
26
 
21
27
  import type { LayoutEngineKind } from '@jbrowse/bandage-core/layoutEngines'
@@ -88,6 +94,23 @@ export const withRenderingActions = withLoadActions
88
94
  },
89
95
  ),
90
96
  )
97
+ // and the haplotypes' own genes on their nodes off the reference
98
+ addDisposer(
99
+ self,
100
+ reaction(
101
+ () => ({
102
+ graph: self.graph,
103
+ reads: JSON.stringify(self.ownGeneReads),
104
+ }),
105
+ () => {
106
+ void self.loadOwnGenes()
107
+ },
108
+ {
109
+ fireImmediately: true,
110
+ equals: (a, b) => a.graph === b.graph && a.reads === b.reads,
111
+ },
112
+ ),
113
+ )
91
114
  // Walk rows and tube map panels read the source track's samples
92
115
  // TSV, to group by its columns, again for a graph after a dropped one
93
116
  addDisposer(
@@ -145,8 +168,16 @@ export const withRenderingActions = withLoadActions
145
168
  untracked(() => {
146
169
  const region = self.graphRegion
147
170
  const graph = self.graph
171
+ const liveViewIds = new Set(
172
+ withRows([...getSession(self).views]).flatMap(v =>
173
+ isRecord(v) && typeof v.id === 'string' ? [v.id] : [],
174
+ ),
175
+ )
148
176
  const inRegion =
149
- hover && region && hoverInRegion(hover, region)
177
+ hover &&
178
+ region &&
179
+ hoverInRegion(hover, region) &&
180
+ hoverFromPair(hover, self.pairedViewId, liveViewIds)
150
181
  ? hover
151
182
  : undefined
152
183
  // a row naming a walk is the hover, not the node at its bp,
@@ -155,9 +186,14 @@ export const withRenderingActions = withLoadActions
155
186
  inRegion?.featureName && graph?.paths
156
187
  ? walksForRow(inRegion.featureName, graph.paths)
157
188
  : []
189
+ // a bubble track's feature, or a graph-derived VCF record,
190
+ // names a whole bubble, which lights whatever the setting
191
+ const bubble =
192
+ inRegion && bubbleForLgvHover(inRegion, self.bubbles)
158
193
  if (region && graph && !self.pointerInPane) {
194
+ self.setHoveredBubble(bubble ?? null)
159
195
  self.setHoveredNode(
160
- inRegion && lightsNode && rowWalks.length === 0
196
+ inRegion && lightsNode && rowWalks.length === 0 && !bubble
161
197
  ? nodeForLgvHover({ hover: inRegion, nodes: graph.nodes })
162
198
  : null,
163
199
  )
@@ -167,6 +203,29 @@ export const withRenderingActions = withLoadActions
167
203
  }),
168
204
  )
169
205
 
206
+ // A click on a bubble feature or graph-derived variant in a linear
207
+ // view opens its bubble here, as clicking its label would
208
+ addDisposer(
209
+ self,
210
+ reaction(
211
+ () => readSelectedFeature(getSession(self).selection),
212
+ selected => {
213
+ const region = self.graphRegion
214
+ const bubble =
215
+ selected &&
216
+ region &&
217
+ hoverInRegion(selected, region) &&
218
+ bubbleForLgvHover(selected, self.bubbles)
219
+ if (
220
+ bubble &&
221
+ !self.openBubbles.some(open => sameBubble(open, bubble))
222
+ ) {
223
+ self.toggleBubble(bubble)
224
+ }
225
+ },
226
+ ),
227
+ )
228
+
170
229
  // Reaction: drop a hover the pointer cannot still be over.
171
230
  //
172
231
  // A hover is set by a mousemove on the canvas, and here the drawing
@@ -20,7 +20,7 @@ import { nodeOwnLocation } from '../../launchFromGraph/contributors'
20
20
  import { withRows } from '../../launchFromGraph/linearViewTarget'
21
21
  import { withLayer } from '../graphLayers'
22
22
  import { colorOfScheme } from '../nodeColor'
23
- import { sizeOfNodeWidth } from '../nodeSize'
23
+ import { sizeOf } from '../nodeSize'
24
24
 
25
25
  import type { GraphGrammar } from './graphViews'
26
26
  import type { ScreenTransform } from './keepInView'
@@ -32,6 +32,7 @@ import type { WalkRowGroupBy } from '../walkRowGroups'
32
32
  import type { BubbleSpread } from '@jbrowse/bandage-core/bubbleSpreads'
33
33
  import type { MinigraphBubble } from '@jbrowse/bandage-core/bubbles/bubbleLine'
34
34
  import type { ColorScheme } from '@jbrowse/bandage-core/colorSchemes'
35
+ import type { EdgeWidth } from '@jbrowse/bandage-core/edgeWidths'
35
36
  import type { FacetSetting } from '@jbrowse/bandage-core/facetGrid'
36
37
  import type { LayoutEngineKind } from '@jbrowse/bandage-core/layoutEngines'
37
38
  import type { LayoutModeValue } from '@jbrowse/bandage-core/layoutModes'
@@ -44,6 +45,8 @@ import type {
44
45
  WalkLayer,
45
46
  } from '@jbrowse/bandage-core/walkEncoding'
46
47
 
48
+ const ROW_HOVER_SETTLE_MS = 80
49
+
47
50
  export const withSettingActions = withTubeMapViews
48
51
  .actions(() => ({
49
52
  // each host overrides: a view assigns its props, a track display writes
@@ -166,7 +169,20 @@ export const withSettingActions = withTubeMapViews
166
169
  },
167
170
  setNodeWidth(width: NodeWidth) {
168
171
  self.writeGrammar({
169
- size: sizeOfNodeWidth(width, self.contigThickness),
172
+ size: sizeOf({
173
+ width,
174
+ px: self.contigThickness,
175
+ links: self.linkWidth,
176
+ }),
177
+ })
178
+ },
179
+ setLinkWidth(links: EdgeWidth) {
180
+ self.writeGrammar({
181
+ size: sizeOf({
182
+ width: self.nodeWidth,
183
+ px: self.contigThickness,
184
+ links,
185
+ }),
170
186
  })
171
187
  },
172
188
  setTubeMapFold(bp: number) {
@@ -237,7 +253,19 @@ export const withSettingActions = withTubeMapViews
237
253
  )
238
254
  },
239
255
  setWalkRowSamples(samples: string[] | undefined) {
240
- self.writeGrammar({ rows: samples ? { kept: samples } : {} })
256
+ const order = self.walkRowOrder
257
+ self.writeGrammar({
258
+ rows: {
259
+ ...(samples ? { kept: samples } : {}),
260
+ ...(order ? { order } : {}),
261
+ },
262
+ })
263
+ },
264
+ setWalkRowOrder(order: 'shared' | undefined) {
265
+ const kept = self.walkRowSamples
266
+ self.writeGrammar({
267
+ rows: { ...(kept ? { kept } : {}), ...(order ? { order } : {}) },
268
+ })
241
269
  },
242
270
  setWalkRowGroupBy(groupBy: WalkRowGroupBy | undefined) {
243
271
  if (groupBy || self.walkRowGroupBy) {
@@ -276,7 +304,7 @@ export const withSettingActions = withTubeMapViews
276
304
  self.hoveredTube = tube
277
305
  }
278
306
  },
279
- setHoveredRowWalks(walks: string[]) {
307
+ applyHoveredRowWalks(walks: string[]) {
280
308
  if (walks.join('\n') !== self.hoveredRowWalks.join('\n')) {
281
309
  self.hoveredRowWalks = walks
282
310
  }
@@ -469,6 +497,36 @@ export const withSettingActions = withTubeMapViews
469
497
  self.builtViewport = undefined
470
498
  },
471
499
  }))
500
+ .actions(self => {
501
+ let settle: ReturnType<typeof setTimeout> | undefined
502
+ let pending: string | undefined
503
+ return {
504
+ // A lane row's lift rebuilds the drawing (43 ms at 10k nodes), and a
505
+ // sweep down a lane crosses a row every few px, so a move from one row
506
+ // to the next lifts once the pointer has stayed on a row a moment.
507
+ // Entering and leaving the rows apply at once.
508
+ setHoveredRowWalks(walks: string[]) {
509
+ const key = walks.join('\n')
510
+ if (key === pending) {
511
+ return
512
+ }
513
+ clearTimeout(settle)
514
+ pending = undefined
515
+ if (walks.length > 0 && self.hoveredRowWalks.length > 0) {
516
+ pending = key
517
+ settle = setTimeout(() => {
518
+ pending = undefined
519
+ // a hover cleared meanwhile, by a pan, stays cleared
520
+ if (isAlive(self) && self.hoveredRowWalks.length > 0) {
521
+ self.applyHoveredRowWalks(walks)
522
+ }
523
+ }, ROW_HOVER_SETTLE_MS)
524
+ } else {
525
+ self.applyHoveredRowWalks(walks)
526
+ }
527
+ },
528
+ }
529
+ })
472
530
  .actions(self => ({
473
531
  // The frame clock. Unclamped, since clampZoom's floor is there to keep a
474
532
  // scale finite and 1 / bpPerPx already is; clamped, a whole-chromosome
@@ -18,6 +18,7 @@ import { resolveLocationAssembly } from '../../launchFromGraph/contributors'
18
18
 
19
19
  import type { SubgraphRegion } from '../../GetSubgraph'
20
20
  import type { WalkAxis, WalkRow } from '@jbrowse/bandage-core/layout/walkRows'
21
+ import type { LayoutModeValue } from '@jbrowse/bandage-core/layoutModes'
21
22
 
22
23
  export const withWalkRowViews = withLaunchViews
23
24
  .views(self => ({
@@ -47,13 +48,23 @@ export const withWalkRowViews = withLaunchViews
47
48
  }
48
49
  return walkStripFrame(bars, { width: self.paneWidth })
49
50
  },
50
- // Whether a cut must follow every snarl a walk leaves the window by, so
51
- // walks come back whole: walk rows measure them, and so does the strip
51
+ // Whether a cut under `mode` must follow every snarl a walk leaves the
52
+ // window by, so walks come back whole: walk rows measure them, and so does
53
+ // the strip under a layout that draws nodes. Only a GBZ cut follows
54
+ // snarls; a walk file cuts the same either way, so asking would re-fetch
55
+ // an identical graph.
56
+ cutsWholeWalksIn(mode: LayoutModeValue, walkStrip = self.walkStrip) {
57
+ const m = layoutModeByValue(mode)
58
+ return (
59
+ self.sourceAdapter?.type === 'GbzBaseSyntenyAdapter' &&
60
+ wholeWalksRule(m, {
61
+ walkStrip: walkStrip && m.drawsNodes,
62
+ host: !!self.host,
63
+ })
64
+ )
65
+ },
52
66
  get cutsWholeWalks() {
53
- return wholeWalksRule(layoutModeByValue(self.chosenLayoutMode), {
54
- walkStrip: self.walkStrip,
55
- host: !!self.host,
56
- })
67
+ return this.cutsWholeWalksIn(self.chosenLayoutMode)
57
68
  },
58
69
  // Where each walk passes the hovered node, or the selected one
59
70
  get walkStripMarks() {
@@ -215,9 +215,10 @@ export function walksThrough(graph: Graph, box: string) {
215
215
  for (const p of graph.paths ?? []) {
216
216
  const walk = walkOf(p.name)
217
217
  if (walk !== graph.referencePath) {
218
+ const through = p.nodeIds.includes(box)
218
219
  for (const member of p.members ?? [walk]) {
219
220
  all.add(member)
220
- if (p.nodeIds.includes(box)) {
221
+ if (through) {
221
222
  here.add(member)
222
223
  }
223
224
  }
@@ -1,7 +1,4 @@
1
- import {
2
- LAYOUT_MODE_VALUES,
3
- layoutModeByValue,
4
- } from '@jbrowse/bandage-core/layoutModes'
1
+ import { LAYOUT_MODE_VALUES } from '@jbrowse/bandage-core/layoutModes'
5
2
  import { BaseViewModel } from '@jbrowse/core/pluggableElementTypes/models'
6
3
  import { getSession } from '@jbrowse/core/util'
7
4
  import { addDisposer, isAlive, types } from '@jbrowse/mobx-state-tree'
@@ -222,10 +219,11 @@ export default function stateModelFactory() {
222
219
  self,
223
220
  reaction(
224
221
  () => self.walkStrip,
225
- () => {
222
+ on => {
226
223
  if (
227
224
  self.loadedTrackId &&
228
- !layoutModeByValue(self.chosenLayoutMode).wholeWalks
225
+ self.cutsWholeWalks !==
226
+ self.cutsWholeWalksIn(self.chosenLayoutMode, !on)
229
227
  ) {
230
228
  void self.load()
231
229
  }
@@ -0,0 +1,50 @@
1
+ import type { WalkRowBlock } from '@jbrowse/bandage-core/layout/walkRowLinks'
2
+
3
+ // ggbandage's: the three biggest blocks in colour, the rest a pale grey that
4
+ // still carries its letter
5
+ const BLOCK_COLORS = ['#d4735e', '#d8a94a', '#c98aa4']
6
+ const OTHER_BLOCK = '#e6e6e6'
7
+ const BLOCK_EDGE = '#555'
8
+ const LETTER_FONT = 'bold 11px sans-serif'
9
+ const MIN_LETTER_PX = 12
10
+ const MIN_LETTER_BAR_PX = 9
11
+
12
+ export const blockFill = (block: number) => BLOCK_COLORS[block] ?? OTHER_BLOCK
13
+
14
+ // A, B, ... Z, then AA, AB, ...
15
+ export function blockLetter(block: number): string {
16
+ const letter = String.fromCharCode(65 + (block % 26))
17
+ return block < 26 ? letter : blockLetter(Math.floor(block / 26) - 1) + letter
18
+ }
19
+
20
+ // How many blocks get a colour of their own, for the key
21
+ export const COLORED_BLOCKS = BLOCK_COLORS.length
22
+
23
+ // Each stretch as a box over its row's bar, lettered where it is wide enough
24
+ export function paintWalkRowBlocks(
25
+ ctx: CanvasRenderingContext2D,
26
+ blocks: WalkRowBlock[],
27
+ f: {
28
+ x: (offset: number) => number
29
+ rowCentre: (row: number) => number
30
+ barPx: number
31
+ },
32
+ ) {
33
+ ctx.font = LETTER_FONT
34
+ ctx.textAlign = 'center'
35
+ ctx.textBaseline = 'middle'
36
+ for (const { row, start, bp, block } of blocks) {
37
+ const left = f.x(start)
38
+ const width = f.x(start + bp) - left
39
+ const top = f.rowCentre(row) - f.barPx / 2
40
+ ctx.fillStyle = blockFill(block)
41
+ ctx.fillRect(left, top, width, f.barPx)
42
+ ctx.strokeStyle = BLOCK_EDGE
43
+ ctx.lineWidth = 1
44
+ ctx.strokeRect(left + 0.5, top + 0.5, width - 1, f.barPx - 1)
45
+ if (width >= MIN_LETTER_PX && f.barPx >= MIN_LETTER_BAR_PX) {
46
+ ctx.fillStyle = '#111'
47
+ ctx.fillText(blockLetter(block), left + width / 2, f.rowCentre(row))
48
+ }
49
+ }
50
+ }
@@ -868,8 +868,14 @@ test('walk rows fit their own bars in the track, since a walk can be longer than
868
868
  expect(pane.viewportOwner).toBe('fit')
869
869
  })
870
870
 
871
- test('walk rows cut every snarl a walk takes past the window, so a walk comes whole', async () => {
872
- const { pane, cuts } = await shownGraph({ paths: true })
871
+ test('walk rows cut every snarl a GBZ walk takes past the window, so a walk comes whole', async () => {
872
+ const { view, cuts } = createEnvironment({ paths: true })
873
+ view.zoomTo(60_000 / WIDTH_PX)
874
+ view.scrollTo(1_000_000 / view.bpPerPx)
875
+ view.showTrack('walks')
876
+ const pane = view.tracks[0]!.displays[0] as LinearGraphDisplayModel
877
+ pane.startRenderingBackend(fakeRenderer())
878
+ await wait(SETTLE_MS)
873
879
  await pane.switchLayout('force')
874
880
  await wait(SETTLE_MS)
875
881
  const before = cuts.length
@@ -884,6 +890,18 @@ test('walk rows cut every snarl a walk takes past the window, so a walk comes wh
884
890
  expect(cuts.at(-1)!.snarls).toBeUndefined()
885
891
  })
886
892
 
893
+ // A walk file cuts the same whatever the snarl option says
894
+ test('walk rows on an rGFA track cut for the step budget, not for snarls', async () => {
895
+ const { pane, cuts } = await shownGraph({ paths: true })
896
+ await pane.switchLayout('force')
897
+ await wait(SETTLE_MS)
898
+ const before = cuts.length
899
+ await pane.switchLayout('walkrows')
900
+ await wait(SETTLE_MS)
901
+ expect(cuts).toHaveLength(before + 1)
902
+ expect(cuts.at(-1)!.snarls).toBeUndefined()
903
+ })
904
+
887
905
  test('a launch in the force layout cuts the window alone', async () => {
888
906
  const { view, cuts } = createEnvironment()
889
907
  view.zoomTo(60_000 / WIDTH_PX)
@@ -1018,9 +1036,11 @@ test('each grammar setter writes the track config, and the pane reads it back',
1018
1036
  display.setLayoutMode('ordered')
1019
1037
  display.setColorScheme('depth')
1020
1038
  display.setNodeWidth('uniform')
1039
+ display.setLinkWidth('walks')
1021
1040
  display.setLayer('bubbles', true)
1022
1041
  display.setFacet('walk')
1023
1042
  display.setWalkRowSamples(['HG1'])
1043
+ display.setWalkRowOrder('shared')
1024
1044
  display.setHover('off')
1025
1045
  const slots = [
1026
1046
  'layoutMode',
@@ -1040,21 +1060,31 @@ test('each grammar setter writes the track config, and the pane reads it back',
1040
1060
  ).toEqual({
1041
1061
  layoutMode: 'ordered',
1042
1062
  color: { field: 'depth' },
1043
- size: 6,
1063
+ size: { value: 6, links: 'walks' },
1044
1064
  layers: { bubbles: true },
1045
1065
  facet: { field: 'walk', domain: [] },
1046
- rows: { kept: ['HG1'] },
1066
+ rows: { kept: ['HG1'], order: 'shared' },
1047
1067
  hover: 'off',
1048
1068
  })
1049
1069
  expect([
1050
1070
  display.chosenLayoutMode,
1051
1071
  display.chosenColorScheme,
1052
1072
  display.nodeWidth,
1073
+ display.linkWidth,
1053
1074
  display.showBubbles,
1054
1075
  display.facetSetting.field,
1055
1076
  display.walkRowSamples,
1056
1077
  display.chosenHover,
1057
- ]).toEqual(['ordered', 'depth', 'uniform', true, 'walk', ['HG1'], 'off'])
1078
+ ]).toEqual([
1079
+ 'ordered',
1080
+ 'depth',
1081
+ 'uniform',
1082
+ 'walks',
1083
+ true,
1084
+ 'walk',
1085
+ ['HG1'],
1086
+ 'off',
1087
+ ])
1058
1088
  })
1059
1089
 
1060
1090
  // Reset track settings, undo and the config editor write the slot without
@@ -39,6 +39,24 @@ test('a drawn graph sits in the chrome with its readiness attributes', () => {
39
39
  expect(screen.getByTestId('graph-canvas')).toBeTruthy()
40
40
  })
41
41
 
42
+ // GraphCanvas shows it, with the retry that rebuilds the backend
43
+ test('a render error alone is left to the canvas', () => {
44
+ render(
45
+ <GraphStatusChrome
46
+ model={model({ renderError: new Error('context lost') })}
47
+ phase="error"
48
+ drawn={false}
49
+ testid="linear-graph-display"
50
+ >
51
+ <canvas />
52
+ </GraphStatusChrome>,
53
+ )
54
+ expect(screen.queryByText(/context lost/)).toBeNull()
55
+ expect(screen.getByTestId('linear-graph-display').dataset.displayPhase).toBe(
56
+ 'error',
57
+ )
58
+ })
59
+
42
60
  test('a refused cut shows its notice over the graph', () => {
43
61
  const error = Object.assign(
44
62
  new Error('Zoom in to about 54Kbp to see the graph'),
@@ -31,7 +31,7 @@ const FORCE_LOAD_ADVICE =
31
31
  // read `model` and `visible`; later hosts read values and callbacks. Each
32
32
  // overlay gets both, so one build runs on either.
33
33
  function overlayProps(model: LinearGraphDisplayModel) {
34
- const error = model.error ?? model.renderError
34
+ const { error } = model
35
35
  const regionTooLarge =
36
36
  error instanceof Error &&
37
37
  (error as { regionTooLarge?: unknown }).regionTooLarge === true
@@ -128,7 +128,9 @@ export default observer(function GraphStatusChrome({
128
128
  {children}
129
129
  </BottomRightCornerContext>
130
130
  <TrackOverlayPortal>
131
- {phase === 'error' ? (
131
+ {/* a backend's render error is the canvas's banner, whose retry
132
+ rebuilds the backend */}
133
+ {phase === 'error' && model.error !== undefined ? (
132
134
  <ErrorBar model={model} visible {...props.error} />
133
135
  ) : null}
134
136
  <Loading
@@ -1,4 +1,5 @@
1
- import { fireEvent, render, screen } from '@testing-library/react'
1
+ import { act, fireEvent, render, screen } from '@testing-library/react'
2
+ import { observable, runInAction } from 'mobx'
2
3
 
3
4
  import SubgraphHaplotypesField from './SubgraphHaplotypesField'
4
5
 
@@ -58,3 +59,42 @@ test('without names the field takes typed ones', () => {
58
59
  fireEvent.keyDown(field, { key: 'Enter' })
59
60
  expect(m.setSubgraphHaplotypes).toHaveBeenCalledWith(['HG002', 'NA19240#1'])
60
61
  })
62
+
63
+ // A model whose set re-renders the field, as the display's does
64
+ function liveModel(haplotypeNames?: string[]) {
65
+ const m = observable({
66
+ sourceAdapter: { type: 'RgfaTabixAdapter', walksUri: 'chr22' },
67
+ cutHaplotypes: ['HG002'],
68
+ haplotypeNames,
69
+ setSubgraphHaplotypes(haplotypes: string[]) {
70
+ m.cutHaplotypes = haplotypes
71
+ },
72
+ recut: vi.fn(),
73
+ })
74
+ return m as unknown as LinearGraphCutModel & { cutHaplotypes: string[] }
75
+ }
76
+
77
+ test('applying from the field keeps its focus', () => {
78
+ const m = liveModel()
79
+ render(<SubgraphHaplotypesField model={m} />)
80
+ const field = screen.getByTestId<HTMLInputElement>('graph-haplotypes-field')
81
+ field.focus()
82
+ fireEvent.change(field, { target: { value: 'HG002, NA19240#1' } })
83
+ fireEvent.keyDown(field, { key: 'Enter' })
84
+ expect(m.cutHaplotypes).toEqual(['HG002', 'NA19240#1'])
85
+ expect(screen.getByTestId('graph-haplotypes-field')).toBe(field)
86
+ expect(document.activeElement).toBe(field)
87
+ })
88
+
89
+ test('a set changed elsewhere resets the draft', () => {
90
+ const m = liveModel()
91
+ render(<SubgraphHaplotypesField model={m} />)
92
+ act(() => {
93
+ runInAction(() => {
94
+ m.cutHaplotypes = ['HG00097#1']
95
+ })
96
+ })
97
+ expect(
98
+ screen.getByTestId<HTMLInputElement>('graph-haplotypes-field').value,
99
+ ).toBe('HG00097#1')
100
+ })
@@ -18,20 +18,39 @@ function parseHaplotypeList(text: string) {
18
18
  return text.split(/[\s,]+/).filter(name => name !== '')
19
19
  }
20
20
 
21
+ const sameList = (a: readonly string[], b: readonly string[]) =>
22
+ a.join('\n') === b.join('\n')
23
+
24
+ // The cut's set, and the last one this field saw: an outside change (a
25
+ // restored session, a launch) resets the draft, while the field's own apply
26
+ // records what it set first, so it keeps its focus and draft
27
+ function useCutHaplotypes(model: LinearGraphCutModel, reset: () => void) {
28
+ const current = model.cutHaplotypes ?? []
29
+ const [seen, setSeen] = useState(current)
30
+ if (!sameList(current, seen)) {
31
+ setSeen(current)
32
+ reset()
33
+ }
34
+ return (haplotypes: string[]) => {
35
+ if (!sameList(haplotypes, current)) {
36
+ setSeen(haplotypes)
37
+ model.setSubgraphHaplotypes(haplotypes)
38
+ model.recut()
39
+ }
40
+ }
41
+ }
42
+
21
43
  const HaplotypeListField = observer(function HaplotypeListField({
22
44
  model,
23
45
  }: {
24
46
  model: LinearGraphCutModel
25
47
  }) {
26
48
  const [draft, setDraft] = useState(model.cutHaplotypes?.join(', ') ?? '')
49
+ const setCut = useCutHaplotypes(model, () => {
50
+ setDraft(model.cutHaplotypes?.join(', ') ?? '')
51
+ })
27
52
  const apply = () => {
28
- const parsed = parseHaplotypeList(draft)
29
- const unchanged =
30
- parsed.join('\n') === (model.cutHaplotypes ?? []).join('\n')
31
- if (!unchanged) {
32
- model.setSubgraphHaplotypes(parsed)
33
- model.recut()
34
- }
53
+ setCut(parseHaplotypeList(draft))
35
54
  }
36
55
  return (
37
56
  <TextField
@@ -72,12 +91,10 @@ const HaplotypePickField = observer(function HaplotypePickField({
72
91
  const draftRef = useRef(draft)
73
92
  const typed = useRef('')
74
93
  const [open, setOpen] = useState(false)
75
- const apply = (haplotypes: string[]) => {
76
- if (haplotypes.join('\n') !== (model.cutHaplotypes ?? []).join('\n')) {
77
- model.setSubgraphHaplotypes(haplotypes)
78
- model.recut()
79
- }
80
- }
94
+ const apply = useCutHaplotypes(model, () => {
95
+ draftRef.current = model.cutHaplotypes ?? []
96
+ setDraft(draftRef.current)
97
+ })
81
98
  return (
82
99
  <Autocomplete
83
100
  multiple
@@ -126,9 +143,7 @@ const HaplotypePickField = observer(function HaplotypePickField({
126
143
  )
127
144
  })
128
145
 
129
- // Only a GBZ cut and a walk-indexed rGFA cut read the set. The inner field is
130
- // keyed on it so an outside change (a restored session, a launch) resets the
131
- // draft rather than fighting it.
146
+ // Only a GBZ cut and a walk-indexed rGFA cut read the set
132
147
  const SubgraphHaplotypesField = observer(function SubgraphHaplotypesField({
133
148
  model,
134
149
  }: {
@@ -136,14 +151,13 @@ const SubgraphHaplotypesField = observer(function SubgraphHaplotypesField({
136
151
  }) {
137
152
  const { classes } = useStyles()
138
153
  const names = model.haplotypeNames
139
- const key = model.cutHaplotypes?.join(',') ?? ''
140
154
  const adapter = model.sourceAdapter
141
155
  return adapter && cutsByHaplotype(adapter) ? (
142
156
  <div className={classes.section}>
143
157
  {names ? (
144
- <HaplotypePickField key={key} model={model} names={names} />
158
+ <HaplotypePickField model={model} names={names} />
145
159
  ) : (
146
- <HaplotypeListField key={key} model={model} />
160
+ <HaplotypeListField model={model} />
147
161
  )}
148
162
  <Typography variant="caption" component="div" color="text.secondary">
149
163
  {names
@@ -44,7 +44,8 @@ export function configSchemaFactory() {
44
44
  * #slot
45
45
  * the node thickness: a px number draws every node that thick;
46
46
  * `{ field: 'depth', value }` thickens a node the more paths carry it,
47
- * around `value` px
47
+ * around `value` px; `links: 'walks'` thickens a link the more walks
48
+ * take it
48
49
  */
49
50
  size: {
50
51
  type: 'frozen',
@@ -54,8 +55,8 @@ export function configSchemaFactory() {
54
55
  /**
55
56
  * #slot
56
57
  * what the drawing adds over its nodes and edges, stated as changes to
57
- * the defaults: `paths`, `bubbles` and `walkStrip` are off, `deletions`,
58
- * `genes` and `referenceStrip` on
58
+ * the defaults: `paths`, `bubbles`, `walkStrip` and `walkRibbons` are
59
+ * off, `deletions`, `genes` and `referenceStrip` on
59
60
  * ```js
60
61
  * { layers: { bubbles: true, genes: false } }
61
62
  * ```
@@ -4,11 +4,7 @@ import { ObservableCreate } from '@jbrowse/core/util/rxjs'
4
4
  import { parseSegmentRow, rgfaCut, segmentSamples } from '@jbrowse/graph-walks'
5
5
 
6
6
  import { locationIsSet } from '../locationName.ts'
7
- import {
8
- PanSNRefNames,
9
- openTabixSlot,
10
- unmappedRegionMessage,
11
- } from '../panSNTabix.ts'
7
+ import { PanSNRefNames, openTabixSlot } from '../panSNTabix.ts'
12
8
  import { getLines } from '../tabixRange.ts'
13
9
  import { WalkIndex } from './walkReader.ts'
14
10
 
@@ -194,10 +190,7 @@ export default class RgfaTabixAdapter extends BaseFeatureDataAdapter<RgfaTabixAd
194
190
  ): Promise<string | WalkCut> {
195
191
  const { hops, signal, tier } = opts
196
192
  const index = this.index(tier)
197
- const tabixRefName = await index.refNames.resolve(region, { signal })
198
- if (tabixRefName === undefined) {
199
- throw new Error(unmappedRegionMessage(region))
200
- }
193
+ const tabixRefName = await index.refNames.resolveForCut(region, { signal })
201
194
  if (index.walks) {
202
195
  return index.walks.subgraph(tabixRefName, region, opts)
203
196
  }