jbrowse-plugin-graphgenomeviewer 7.4.4 → 7.6.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 (134) hide show
  1. package/dist/chunks/{GbzBaseSyntenyAdapter-A5MNPCKX.js → GbzBaseSyntenyAdapter-3PVPTKNW.js} +2 -2
  2. package/dist/chunks/{GbzBaseSyntenyAdapter-A5MNPCKX.js.map → GbzBaseSyntenyAdapter-3PVPTKNW.js.map} +1 -1
  3. package/dist/chunks/{GraphGenomeView-SEAPDRXS.js → GraphGenomeView-GQW4YC23.js} +2 -2
  4. package/dist/chunks/GraphTrackSettingsDialog-ARWCWJW4.js +4 -0
  5. package/dist/chunks/GraphTrackSettingsDialog-ARWCWJW4.js.map +7 -0
  6. package/dist/chunks/{HighlightColorDialog-QAYWLO5I.js → HighlightColorDialog-PD3KMF2U.js} +2 -2
  7. package/dist/chunks/LinearGraphDisplay-QNS4EAL3.js +2 -0
  8. package/dist/chunks/{LinearGraphDisplay-ZC4BQ27J.js.map → LinearGraphDisplay-QNS4EAL3.js.map} +2 -2
  9. package/dist/chunks/{MinigraphBubbleAdapter-3QM4XV47.js → MinigraphBubbleAdapter-L6JSQIOL.js} +2 -2
  10. package/dist/chunks/RgfaTabixAdapter-5HCAJ7QI.js +2 -0
  11. package/dist/chunks/RgfaTabixAdapter-5HCAJ7QI.js.map +7 -0
  12. package/dist/chunks/{WalkFormsAdapter-FT2SW4H6.js → WalkFormsAdapter-NX36GP35.js} +2 -2
  13. package/dist/chunks/WalkTabixSyntenyAdapter-JFNFX3JP.js +2 -0
  14. package/dist/chunks/{WalkTabixSyntenyAdapter-3WRVNDLB.js.map → WalkTabixSyntenyAdapter-JFNFX3JP.js.map} +2 -2
  15. package/dist/chunks/{chunk-KIYRMZWP.js → chunk-2CPD5NX2.js} +2 -2
  16. package/dist/chunks/{chunk-KIYRMZWP.js.map → chunk-2CPD5NX2.js.map} +1 -1
  17. package/dist/chunks/chunk-3AC33AXB.js +2 -0
  18. package/dist/chunks/{chunk-HF4H6LQI.js → chunk-4PHM6JNU.js} +2 -2
  19. package/dist/chunks/chunk-5LFPNQWH.js +2 -0
  20. package/dist/chunks/{chunk-6VNU7SD4.js.map → chunk-5LFPNQWH.js.map} +4 -4
  21. package/dist/chunks/{chunk-6WUWQX2P.js → chunk-CNEUOM7X.js} +2 -2
  22. package/dist/chunks/{chunk-6WUWQX2P.js.map → chunk-CNEUOM7X.js.map} +1 -1
  23. package/dist/chunks/{chunk-P3FBI5ML.js → chunk-DOC7QIE2.js} +1 -1
  24. package/dist/chunks/{chunk-P3FBI5ML.js.map → chunk-DOC7QIE2.js.map} +1 -1
  25. package/dist/chunks/{chunk-G53BI7VK.js → chunk-G4OPM6G7.js} +2 -2
  26. package/dist/chunks/{chunk-G53BI7VK.js.map → chunk-G4OPM6G7.js.map} +1 -1
  27. package/dist/chunks/chunk-GRLNDNX4.js +2 -0
  28. package/dist/chunks/{chunk-S333HOG5.js.map → chunk-GRLNDNX4.js.map} +4 -4
  29. package/dist/chunks/{chunk-7RJMG6I2.js → chunk-JYYF3U2H.js} +1 -1
  30. package/dist/chunks/{chunk-7RJMG6I2.js.map → chunk-JYYF3U2H.js.map} +1 -1
  31. package/dist/chunks/chunk-KDXVLEHS.js +3 -0
  32. package/dist/chunks/chunk-KDXVLEHS.js.map +7 -0
  33. package/dist/chunks/chunk-LGV6BTOY.js +2 -0
  34. package/dist/chunks/{chunk-HCDKP3IP.js.map → chunk-LGV6BTOY.js.map} +4 -4
  35. package/dist/chunks/{chunk-2KVE7YAS.js → chunk-LJ7O3736.js} +1 -1
  36. package/dist/chunks/{chunk-2KVE7YAS.js.map → chunk-LJ7O3736.js.map} +1 -1
  37. package/dist/chunks/{chunk-OJWBLPPH.js → chunk-OQHN2GWX.js} +2 -2
  38. package/dist/chunks/chunk-PTDOYJFH.js +4 -0
  39. package/dist/chunks/chunk-PTDOYJFH.js.map +7 -0
  40. package/dist/chunks/chunk-SGP63UMC.js +2 -0
  41. package/dist/chunks/chunk-SGP63UMC.js.map +7 -0
  42. package/dist/chunks/{chunk-XXBE4IFZ.js → chunk-SI5VLKVN.js} +2 -2
  43. package/dist/chunks/{chunk-XXBE4IFZ.js.map → chunk-SI5VLKVN.js.map} +1 -1
  44. package/dist/chunks/{chunk-6SWXMRAV.js → chunk-TWPUVVYH.js} +1 -1
  45. package/dist/chunks/{chunk-6SWXMRAV.js.map → chunk-TWPUVVYH.js.map} +1 -1
  46. package/dist/chunks/{chunk-FTXDTU27.js → chunk-XF6GU4GK.js} +3 -3
  47. package/dist/chunks/{chunk-FTXDTU27.js.map → chunk-XF6GU4GK.js.map} +2 -2
  48. package/dist/chunks/{chunk-MUONTMYZ.js → chunk-XSDGE4RF.js} +2 -2
  49. package/dist/chunks/{chunk-LE7FZM6Y.js → chunk-ZFJGNGIB.js} +2 -2
  50. package/dist/chunks/{chunk-LE7FZM6Y.js.map → chunk-ZFJGNGIB.js.map} +1 -1
  51. package/dist/chunks/chunk-ZI7RFIA2.js +4 -0
  52. package/dist/chunks/chunk-ZI7RFIA2.js.map +7 -0
  53. package/dist/chunks/{renderSvg-ZIUOPWJQ.js → renderSvg-NIWFCY3U.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 +307 -7
  66. package/src/GraphGenomeView/model.ts +3 -10
  67. package/src/GraphGenomeView/nodeColor.test.ts +27 -1
  68. package/src/GraphGenomeView/nodeColor.ts +38 -29
  69. package/src/GraphGenomeView/nodeSize.test.ts +21 -5
  70. package/src/GraphGenomeView/nodeSize.ts +24 -8
  71. package/src/GraphGenomeView/pane/fitViews.ts +5 -1
  72. package/src/GraphGenomeView/pane/graphViews.ts +102 -20
  73. package/src/GraphGenomeView/pane/launchActions.ts +55 -11
  74. package/src/GraphGenomeView/pane/launchViews.ts +46 -0
  75. package/src/GraphGenomeView/pane/loadActions.ts +85 -25
  76. package/src/GraphGenomeView/pane/morph.test.ts +25 -1
  77. package/src/GraphGenomeView/pane/morph.ts +3 -1
  78. package/src/GraphGenomeView/pane/paneBase.ts +5 -0
  79. package/src/GraphGenomeView/pane/renderingActions.ts +61 -2
  80. package/src/GraphGenomeView/pane/settingActions.ts +65 -5
  81. package/src/GraphGenomeView/pane/walkRowViews.ts +17 -6
  82. package/src/GraphGenomeView/retiredNodeColor.test.ts +46 -0
  83. package/src/GraphGenomeView/retiredNodeColor.ts +76 -0
  84. package/src/GraphGenomeView/tubeMapPanels.ts +2 -1
  85. package/src/GraphGenomeView/viewModel.ts +16 -9
  86. package/src/GraphGenomeView/walkRowBlocks.ts +50 -0
  87. package/src/LinearGraphDisplay/LinearGraphDisplay.test.ts +97 -10
  88. package/src/LinearGraphDisplay/components/GraphStatusChrome.test.tsx +18 -0
  89. package/src/LinearGraphDisplay/components/GraphStatusChrome.tsx +4 -2
  90. package/src/LinearGraphDisplay/components/SubgraphHaplotypesField.test.tsx +41 -1
  91. package/src/LinearGraphDisplay/components/SubgraphHaplotypesField.tsx +33 -19
  92. package/src/LinearGraphDisplay/configSchema.ts +18 -10
  93. package/src/LinearGraphDisplay/model.ts +4 -2
  94. package/src/RgfaTabixAdapter/RgfaTabixAdapter.ts +2 -9
  95. package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.ts +3 -6
  96. package/src/hoverSync/graphViewHighlights.test.ts +53 -0
  97. package/src/hoverSync/graphViewHighlights.ts +25 -0
  98. package/src/hoverSync/lgvHover.test.ts +100 -1
  99. package/src/hoverSync/lgvHover.ts +77 -6
  100. package/src/launchFromGraph/contributors.test.ts +52 -0
  101. package/src/launchFromGraph/contributors.ts +105 -1
  102. package/src/launchFromGraph/launchFromGraph.ts +2 -2
  103. package/src/panSNTabix.test.ts +28 -0
  104. package/src/panSNTabix.ts +26 -8
  105. package/src/version.ts +1 -1
  106. package/dist/chunks/GraphTrackSettingsDialog-EVIIGC5G.js +0 -6
  107. package/dist/chunks/GraphTrackSettingsDialog-EVIIGC5G.js.map +0 -7
  108. package/dist/chunks/LinearGraphDisplay-ZC4BQ27J.js +0 -2
  109. package/dist/chunks/RgfaTabixAdapter-XSOIEWBK.js +0 -2
  110. package/dist/chunks/RgfaTabixAdapter-XSOIEWBK.js.map +0 -7
  111. package/dist/chunks/WalkTabixSyntenyAdapter-3WRVNDLB.js +0 -2
  112. package/dist/chunks/chunk-6VNU7SD4.js +0 -2
  113. package/dist/chunks/chunk-GRTN25B6.js +0 -3
  114. package/dist/chunks/chunk-GRTN25B6.js.map +0 -7
  115. package/dist/chunks/chunk-HCDKP3IP.js +0 -2
  116. package/dist/chunks/chunk-LE43I4PT.js +0 -4
  117. package/dist/chunks/chunk-LE43I4PT.js.map +0 -7
  118. package/dist/chunks/chunk-QV7SSOMJ.js +0 -2
  119. package/dist/chunks/chunk-S333HOG5.js +0 -2
  120. package/dist/chunks/chunk-VZQ6VOUP.js +0 -2
  121. package/dist/chunks/chunk-VZQ6VOUP.js.map +0 -7
  122. package/dist/chunks/chunk-WLRRI35A.js +0 -3
  123. package/dist/chunks/chunk-WLRRI35A.js.map +0 -7
  124. package/src/launchFromGraph/launchTracks.test.ts +0 -95
  125. package/src/launchFromGraph/launchTracks.ts +0 -49
  126. /package/dist/chunks/{GraphGenomeView-SEAPDRXS.js.map → GraphGenomeView-GQW4YC23.js.map} +0 -0
  127. /package/dist/chunks/{HighlightColorDialog-QAYWLO5I.js.map → HighlightColorDialog-PD3KMF2U.js.map} +0 -0
  128. /package/dist/chunks/{MinigraphBubbleAdapter-3QM4XV47.js.map → MinigraphBubbleAdapter-L6JSQIOL.js.map} +0 -0
  129. /package/dist/chunks/{WalkFormsAdapter-FT2SW4H6.js.map → WalkFormsAdapter-NX36GP35.js.map} +0 -0
  130. /package/dist/chunks/{chunk-QV7SSOMJ.js.map → chunk-3AC33AXB.js.map} +0 -0
  131. /package/dist/chunks/{chunk-HF4H6LQI.js.map → chunk-4PHM6JNU.js.map} +0 -0
  132. /package/dist/chunks/{chunk-OJWBLPPH.js.map → chunk-OQHN2GWX.js.map} +0 -0
  133. /package/dist/chunks/{chunk-MUONTMYZ.js.map → chunk-XSDGE4RF.js.map} +0 -0
  134. /package/dist/chunks/{renderSvg-ZIUOPWJQ.js.map → renderSvg-NIWFCY3U.js.map} +0 -0
@@ -314,7 +314,8 @@ export function motionAllowed() {
314
314
  }
315
315
 
316
316
  // Calls `blend` with the eased progress once a frame for MORPH_MS while
317
- // `isLive` holds, then `onEnd`. `finish` jumps to the last frame.
317
+ // `isLive` holds, then `onEnd`. `finish` jumps to the last frame, as does a
318
+ // morph cut short, since the layout cache keeps the positions it blends.
318
319
  export function runMorph(
319
320
  blend: (t: number) => void,
320
321
  isLive: () => boolean,
@@ -330,6 +331,7 @@ export function runMorph(
330
331
  let last = performance.now()
331
332
  const step = () => {
332
333
  if (!isLive()) {
334
+ blend(1)
333
335
  onEnd()
334
336
  return
335
337
  }
@@ -95,6 +95,8 @@ export const VIEWPORT_DEBOUNCE_MS = 150
95
95
  export const VIEWPORT_PANES_BUILT = 1
96
96
  // How many walk rows read their haplotype's genes, the first rows down
97
97
  export const WALK_GENE_ROWS = 40
98
+ // How many off-reference contigs read their haplotype's genes
99
+ export const OWN_GENE_CONTIGS = 40
98
100
 
99
101
  // What the canvas draws under the tube map, whose ink is all TubeMapOverlay's
100
102
  export type TubeMapRoutes = 'each' | 'grouped' | 'bundled'
@@ -359,6 +361,9 @@ export const paneBase = types
359
361
  // each walk row's genes, read from its haplotype's own assembly, by walk
360
362
  // name
361
363
  walkGeneFeatures: undefined as Map<string, GeneModel[]> | undefined,
364
+ // each loaded haplotype's genes on its own nodes off the reference, under
365
+ // those nodes' stable names (ownGeneReads)
366
+ ownGeneFeatures: undefined as GeneModel[] | undefined,
362
367
  // the source track's samples TSV, which walk rows group by
363
368
  walkRowSampleTable: undefined as SampleRow[] | undefined,
364
369
  // the tandem repeat arrays over the cut window, from the repeat track
@@ -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
@@ -85,7 +88,9 @@ export const withSettingActions = withTubeMapViews
85
88
  ) {
86
89
  keptDomain = self.statedColorDomain
87
90
  }
88
- self.writeGrammar({ color: colorOfScheme(scheme, keptDomain) })
91
+ self.writeGrammar({
92
+ encoding: { color: colorOfScheme(scheme, keptDomain) },
93
+ })
89
94
  },
90
95
  }
91
96
  })
@@ -166,7 +171,20 @@ export const withSettingActions = withTubeMapViews
166
171
  },
167
172
  setNodeWidth(width: NodeWidth) {
168
173
  self.writeGrammar({
169
- size: sizeOfNodeWidth(width, self.contigThickness),
174
+ size: sizeOf({
175
+ width,
176
+ px: self.contigThickness,
177
+ links: self.linkWidth,
178
+ }),
179
+ })
180
+ },
181
+ setLinkWidth(links: EdgeWidth) {
182
+ self.writeGrammar({
183
+ size: sizeOf({
184
+ width: self.nodeWidth,
185
+ px: self.contigThickness,
186
+ links,
187
+ }),
170
188
  })
171
189
  },
172
190
  setTubeMapFold(bp: number) {
@@ -237,7 +255,19 @@ export const withSettingActions = withTubeMapViews
237
255
  )
238
256
  },
239
257
  setWalkRowSamples(samples: string[] | undefined) {
240
- self.writeGrammar({ rows: samples ? { kept: samples } : {} })
258
+ const order = self.walkRowOrder
259
+ self.writeGrammar({
260
+ rows: {
261
+ ...(samples ? { kept: samples } : {}),
262
+ ...(order ? { order } : {}),
263
+ },
264
+ })
265
+ },
266
+ setWalkRowOrder(order: 'shared' | undefined) {
267
+ const kept = self.walkRowSamples
268
+ self.writeGrammar({
269
+ rows: { ...(kept ? { kept } : {}), ...(order ? { order } : {}) },
270
+ })
241
271
  },
242
272
  setWalkRowGroupBy(groupBy: WalkRowGroupBy | undefined) {
243
273
  if (groupBy || self.walkRowGroupBy) {
@@ -276,7 +306,7 @@ export const withSettingActions = withTubeMapViews
276
306
  self.hoveredTube = tube
277
307
  }
278
308
  },
279
- setHoveredRowWalks(walks: string[]) {
309
+ applyHoveredRowWalks(walks: string[]) {
280
310
  if (walks.join('\n') !== self.hoveredRowWalks.join('\n')) {
281
311
  self.hoveredRowWalks = walks
282
312
  }
@@ -469,6 +499,36 @@ export const withSettingActions = withTubeMapViews
469
499
  self.builtViewport = undefined
470
500
  },
471
501
  }))
502
+ .actions(self => {
503
+ let settle: ReturnType<typeof setTimeout> | undefined
504
+ let pending: string | undefined
505
+ return {
506
+ // A lane row's lift rebuilds the drawing (43 ms at 10k nodes), and a
507
+ // sweep down a lane crosses a row every few px, so a move from one row
508
+ // to the next lifts once the pointer has stayed on a row a moment.
509
+ // Entering and leaving the rows apply at once.
510
+ setHoveredRowWalks(walks: string[]) {
511
+ const key = walks.join('\n')
512
+ if (key === pending) {
513
+ return
514
+ }
515
+ clearTimeout(settle)
516
+ pending = undefined
517
+ if (walks.length > 0 && self.hoveredRowWalks.length > 0) {
518
+ pending = key
519
+ settle = setTimeout(() => {
520
+ pending = undefined
521
+ // a hover cleared meanwhile, by a pan, stays cleared
522
+ if (isAlive(self) && self.hoveredRowWalks.length > 0) {
523
+ self.applyHoveredRowWalks(walks)
524
+ }
525
+ }, ROW_HOVER_SETTLE_MS)
526
+ } else {
527
+ self.applyHoveredRowWalks(walks)
528
+ }
529
+ },
530
+ }
531
+ })
472
532
  .actions(self => ({
473
533
  // The frame clock. Unclamped, since clampZoom's floor is there to keep a
474
534
  // 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() {
@@ -0,0 +1,46 @@
1
+ import { liftFlatNodeColor } from './retiredNodeColor'
2
+
3
+ test('a bare constant becomes a value', () => {
4
+ expect(liftFlatNodeColor('grey')).toEqual({ value: 'grey' })
5
+ expect(liftFlatNodeColor('uniform')).toEqual({ value: 'uniform' })
6
+ })
7
+
8
+ test('categorical becomes an ordinal scale', () => {
9
+ expect(liftFlatNodeColor({ field: 'id', scale: 'categorical' })).toEqual({
10
+ field: 'id',
11
+ scale: { type: 'ordinal' },
12
+ })
13
+ })
14
+
15
+ test('scheme and the domain move under scale', () => {
16
+ expect(liftFlatNodeColor({ field: 'id', scheme: 'rainbow' })).toEqual({
17
+ field: 'id',
18
+ scale: { scheme: 'rainbow' },
19
+ })
20
+ expect(
21
+ liftFlatNodeColor({ field: 'position', domainMin: 5, domainMax: 50 }),
22
+ ).toEqual({ field: 'position', scale: { domainMin: 5, domainMax: 50 } })
23
+ })
24
+
25
+ test('labels and title move to scale.names and legend.title', () => {
26
+ expect(
27
+ liftFlatNodeColor({ field: 'id', labels: ['a'], title: 'Node' }),
28
+ ).toEqual({ field: 'id', scale: { names: ['a'] }, legend: { title: 'Node' } })
29
+ })
30
+
31
+ test('a member the channel object already nests wins over its flat twin', () => {
32
+ expect(
33
+ liftFlatNodeColor({
34
+ field: 'position',
35
+ domainMin: 1,
36
+ scale: { domainMin: 7 },
37
+ }),
38
+ ).toEqual({ field: 'position', scale: { domainMin: 7 } })
39
+ })
40
+
41
+ test('a channel object is the object it was', () => {
42
+ const color = { field: 'position', scale: { domainMin: 1, domainMax: 2 } }
43
+ expect(liftFlatNodeColor(color)).toBe(color)
44
+ expect(liftFlatNodeColor({ field: 'depth' })).toEqual({ field: 'depth' })
45
+ expect(liftFlatNodeColor(undefined)).toBeUndefined()
46
+ })
@@ -0,0 +1,76 @@
1
+ import { isRecord } from '../isRecord'
2
+
3
+ // Where each member of the flat color object sits in the channel object, as
4
+ // display-kit's `liftFlatColor` places it
5
+ const FLAT_MEMBERS: Readonly<
6
+ Record<string, readonly ['scale' | 'legend', string]>
7
+ > = {
8
+ scale: ['scale', 'type'],
9
+ domain: ['scale', 'domain'],
10
+ range: ['scale', 'range'],
11
+ labels: ['scale', 'names'],
12
+ scheme: ['scale', 'scheme'],
13
+ reverse: ['scale', 'reverse'],
14
+ domainMin: ['scale', 'domainMin'],
15
+ domainMax: ['scale', 'domainMax'],
16
+ domainMid: ['scale', 'domainMid'],
17
+ domainQuantile: ['scale', 'domainQuantile'],
18
+ unknown: ['scale', 'unknown'],
19
+ title: ['legend', 'title'],
20
+ descending: ['legend', 'descending'],
21
+ }
22
+
23
+ function isFlat(color: Record<string, unknown>) {
24
+ return Object.entries(color).some(
25
+ ([key, value]) =>
26
+ key in FLAT_MEMBERS && !(key === 'scale' && isRecord(value)),
27
+ )
28
+ }
29
+
30
+ /**
31
+ * @deprecated The node color through 7.5.0, a bare `'grey'` or `'uniform'` or a
32
+ * flat object (`scale: 'categorical'`, `scheme`, `domainMin`, `domainMax`
33
+ * beside `field`), lifted to the channel object: the constants become
34
+ * `value`, scale members go under `scale` and `categorical` becomes
35
+ * `ordinal`. Covers a LinearGraphDisplay's `color` and a GraphGenomeView's
36
+ * saved `color`. Goes once no saved session or hosted config predates `encoding.color`.
37
+ */
38
+ export function liftFlatNodeColor(color: unknown): unknown {
39
+ if (typeof color === 'string') {
40
+ return { value: color }
41
+ }
42
+ if (!isRecord(color) || !isFlat(color)) {
43
+ return color
44
+ }
45
+ const out: Record<string, unknown> = {}
46
+ const parts: Record<'scale' | 'legend', Record<string, unknown>> = {
47
+ scale: {},
48
+ legend: {},
49
+ }
50
+ for (const [key, value] of Object.entries(color)) {
51
+ const at = FLAT_MEMBERS[key]
52
+ if (!at || (key === 'scale' && isRecord(value))) {
53
+ out[key] = value
54
+ } else if (value !== undefined) {
55
+ parts[at[0]][at[1]] =
56
+ key === 'scale' && value === 'categorical' ? 'ordinal' : value
57
+ }
58
+ }
59
+ for (const part of ['scale', 'legend'] as const) {
60
+ if (Object.keys(parts[part]).length > 0) {
61
+ const nested = out[part]
62
+ out[part] = { ...parts[part], ...(isRecord(nested) ? nested : {}) }
63
+ }
64
+ }
65
+ return out
66
+ }
67
+
68
+ /**
69
+ * @deprecated A LinearGraphDisplay's flat `color` as `encoding.color`, for
70
+ * the schema's `retired`. Goes with `liftFlatNodeColor`.
71
+ */
72
+ export const retiredGraphColor = {
73
+ color: (color: unknown) => ({
74
+ encoding: { color: liftFlatNodeColor(color) },
75
+ }),
76
+ }
@@ -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'
@@ -10,6 +7,7 @@ import { reaction } from 'mobx'
10
7
  import { HOVER_HIGHLIGHT_VALUES } from './hoverHighlight'
11
8
  import { lenientMaybeEnum } from './lenientEnum'
12
9
  import { GraphPane, fileName, formatSpanBp } from './model'
10
+ import { liftFlatNodeColor } from './retiredNodeColor'
13
11
  import { canonicalRegion } from '../launchFromGraph/launchFromGraph'
14
12
 
15
13
  import type { GraphLayers } from './graphLayers'
@@ -51,11 +49,16 @@ export default function stateModelFactory() {
51
49
  hover: lenientMaybeEnum(HOVER_HIGHLIGHT_VALUES),
52
50
  }),
53
51
  )
52
+ .preProcessSnapshot(snap =>
53
+ snap.color === undefined
54
+ ? snap
55
+ : { ...snap, color: liftFlatNodeColor(snap.color) as NodeColor },
56
+ )
54
57
  .views(self => ({
55
58
  get grammar(): GraphGrammar {
56
59
  return {
57
60
  layoutMode: self.layoutMode,
58
- color: self.color,
61
+ encoding: { color: self.color },
59
62
  size: self.size,
60
63
  layers: self.layers,
61
64
  facet: self.facet,
@@ -65,8 +68,11 @@ export default function stateModelFactory() {
65
68
  },
66
69
  }))
67
70
  .actions(self => ({
68
- writeGrammar(settings: GraphGrammar) {
69
- Object.assign(self, settings)
71
+ writeGrammar({ encoding, ...settings }: GraphGrammar) {
72
+ Object.assign(
73
+ self,
74
+ encoding ? { ...settings, color: encoding.color } : settings,
75
+ )
70
76
  },
71
77
  }))
72
78
  .volatile(() => ({
@@ -222,10 +228,11 @@ export default function stateModelFactory() {
222
228
  self,
223
229
  reaction(
224
230
  () => self.walkStrip,
225
- () => {
231
+ on => {
226
232
  if (
227
233
  self.loadedTrackId &&
228
- !layoutModeByValue(self.chosenLayoutMode).wholeWalks
234
+ self.cutsWholeWalks !==
235
+ self.cutsWholeWalksIn(self.chosenLayoutMode, !on)
229
236
  ) {
230
237
  void self.load()
231
238
  }
@@ -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
+ }