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
@@ -520,6 +520,7 @@ export const withFitViews = withHostViews
520
520
  connectorThickness: self.connectorThickness,
521
521
  drawPaths,
522
522
  nodeWidth: self.nodeWidth,
523
+ edgeWidth: self.linkWidth,
523
524
  highlight,
524
525
  // Untracked, so a zoom does not eagerly rebuild geometry — the
525
526
  // debounced viewportDirty bump drives the scale-dependent rebuild
@@ -573,13 +574,16 @@ export const withFitViews = withHostViews
573
574
  facet: self.facetSpec,
574
575
  colorScheme: self.effectiveColorScheme,
575
576
  nodeWidth: self.nodeWidth,
577
+ edgeWidth: self.linkWidth,
576
578
  showDeletionEdges: self.showDeletionEdges,
577
579
  linearLayout: self.linearLayout,
578
580
  contigThickness: self.contigThickness,
579
581
  connectorThickness: self.connectorThickness,
580
582
  region: self.graphRegion,
581
583
  colorDomain: self.statedColorDomain,
582
- genes: self.showGenes ? self.backboneGenes : undefined,
584
+ genes: self.showGenes
585
+ ? [...(self.backboneGenes ?? []), ...(self.ownGeneFeatures ?? [])]
586
+ : undefined,
583
587
  walkRows: self.walkRowBars,
584
588
  walkRowPitch: self.walkRowPitch,
585
589
  rowGenes: self.walkRowGenes,
@@ -12,6 +12,12 @@ import {
12
12
  import { facetSettingOf } from '@jbrowse/bandage-core/facetGrid'
13
13
  import { genePins } from '@jbrowse/bandage-core/genes/genePins'
14
14
  import { formatBp } from '@jbrowse/bandage-core/graphLabels'
15
+ import {
16
+ ribbonJoin,
17
+ sharedRowOrder,
18
+ walkRowBlocks,
19
+ walkRowLinks,
20
+ } from '@jbrowse/bandage-core/layout/walkRowLinks'
15
21
  import {
16
22
  walkRowRuns,
17
23
  walkRowsOf,
@@ -89,7 +95,7 @@ import type { ScreenTransform } from './keepInView'
89
95
  import type { WalkCut } from '../../RgfaTabixAdapter/walkCut.ts'
90
96
  import type { GraphLayers } from '../graphLayers'
91
97
  import type { HoverHighlight } from '../hoverHighlight'
92
- import type { NodeColor } from '../nodeColor'
98
+ import type { GraphEncoding } from '../nodeColor'
93
99
  import type { NodeSize } from '../nodeSize'
94
100
  import type { TubeMapPanelSplit } from '../tubeMapPanels'
95
101
  import type { WalkRowGroupBy } from '../walkRowGroups'
@@ -98,6 +104,7 @@ import type {
98
104
  ColorScheme,
99
105
  ResolvedColorScheme,
100
106
  } from '@jbrowse/bandage-core/colorSchemes'
107
+ import type { EdgeWidth } from '@jbrowse/bandage-core/edgeWidths'
101
108
  import type { FacetInput, FacetSetting } from '@jbrowse/bandage-core/facetGrid'
102
109
  import type { LayoutModeValue } from '@jbrowse/bandage-core/layoutModes'
103
110
  import type { NodeWidth } from '@jbrowse/bandage-core/nodeWidths'
@@ -110,12 +117,16 @@ import type { AnyConfigurationModel } from '@jbrowse/core/configuration'
110
117
  import type { FileLocation } from '@jbrowse/core/util/types'
111
118
 
112
119
  // A repeat pick slices the cut's walks again, by the repeat's own array
120
+ // The rows again from the cut's tables: for a repeat pick, by the repeat's
121
+ // own array, or with each row's visits, which ribbons need and the worker's
122
+ // runs leave out
113
123
  function walkRowsFromCut(
114
124
  cut: WalkCut,
115
125
  region: { start: number; end: number },
116
126
  unit?: number,
127
+ visits?: boolean,
117
128
  ) {
118
- const runs = walkRowRuns(cut, region)
129
+ const runs = walkRowRuns(cut, region, { visits })
119
130
  return runs ? walkRowsOf(runs, unit) : undefined
120
131
  }
121
132
 
@@ -151,7 +162,7 @@ function spanIn(
151
162
  // as props, a track display in its config
152
163
  export interface GraphGrammar {
153
164
  layoutMode?: LayoutModeValue
154
- color?: NodeColor
165
+ encoding?: GraphEncoding
155
166
  size?: NodeSize
156
167
  layers?: GraphLayers
157
168
  facet?: FacetInput
@@ -162,9 +173,12 @@ export interface GraphGrammar {
162
173
  type TubeColorKind = 'value' | 'tinted' | 'role' | 'path'
163
174
 
164
175
  // The rows walk rows draw: `kept` names the samples whose walks show, by the
165
- // name before the haplotype number; unset shows every walk the cut holds
176
+ // name before the haplotype number; unset shows every walk the cut holds.
177
+ // `order: 'shared'` stacks each row under the one it shares the most sequence
178
+ // with, so ribbons stay short and shared blocks line up.
166
179
  export interface GraphRows {
167
180
  kept?: string[]
181
+ order?: 'shared'
168
182
  }
169
183
 
170
184
  export const withGraphViews = paneBase
@@ -199,7 +213,9 @@ export const withGraphViews = paneBase
199
213
  computed(derive, {
200
214
  equals: (a, b) => JSON.stringify(a) === JSON.stringify(b),
201
215
  })
202
- const colorDomain = byValue(() => domainOfColor(self.grammar.color))
216
+ const colorDomain = byValue(() =>
217
+ domainOfColor(self.grammar.encoding?.color),
218
+ )
203
219
  const facet = byValue(() => facetSettingOf(self.grammar.facet))
204
220
  const groupBy = byValue(() => {
205
221
  const g = groupByOf(self.grammar.facet)
@@ -210,7 +226,7 @@ export const withGraphViews = paneBase
210
226
  return self.grammar.layoutMode ?? 'force'
211
227
  },
212
228
  get chosenColorScheme(): ColorScheme {
213
- return schemeOfColor(self.grammar.color)
229
+ return schemeOfColor(self.grammar.encoding?.color)
214
230
  },
215
231
  // the reference span the reference-position ramp runs over, for a graph
216
232
  // with no region of its own to span it
@@ -227,6 +243,10 @@ export const withGraphViews = paneBase
227
243
  get contigThickness() {
228
244
  return nodeSizeOf(self.grammar.size).px
229
245
  },
246
+ // Link thickness by the walks taking it; see EDGE_WIDTHS
247
+ get linkWidth(): EdgeWidth {
248
+ return nodeSizeOf(self.grammar.size).links
249
+ },
230
250
  get layerSet() {
231
251
  return layersOf(self.grammar.layers)
232
252
  },
@@ -248,6 +268,9 @@ export const withGraphViews = paneBase
248
268
  get walkStrip() {
249
269
  return this.layerSet.has('walkStrip')
250
270
  },
271
+ get walkRibbons() {
272
+ return this.layerSet.has('walkRibbons')
273
+ },
251
274
  // One section per value of a field, as a grammar of graphics splits a
252
275
  // plot. A node layout draws a panel per `walk` (each lifted walk alone,
253
276
  // on the same layout) or per `sample` (a sample's haplotypes in a row);
@@ -265,6 +288,9 @@ export const withGraphViews = paneBase
265
288
  const kept = self.grammar.rows?.kept
266
289
  return Array.isArray(kept) ? kept : undefined
267
290
  },
291
+ get walkRowOrder() {
292
+ return self.grammar.rows?.order === 'shared' ? 'shared' : undefined
293
+ },
268
294
  get hoverLightsEverything() {
269
295
  return this.chosenHover === 'everything'
270
296
  },
@@ -740,12 +766,26 @@ export const withGraphViews = paneBase
740
766
  // Each walk sliced to the window, for walk rows and the strip; a repeat
741
767
  // pick slices again by its own array
742
768
  get cutWalkRows() {
743
- const runs = self.walkCut?.walkRowRuns
744
- return runs
745
- ? walkRowsOf(runs)
746
- : self.graph
747
- ? walkRows(self.graph, self.graphRegion)
748
- : undefined
769
+ const cut = self.walkCut
770
+ const runs = cut?.walkRowRuns
771
+ const visits = this.rowVisitsNeeded
772
+ return cut && runs && visits && self.graphRegion
773
+ ? walkRowsFromCut(cut, self.graphRegion, undefined, true)
774
+ : runs
775
+ ? walkRowsOf(runs)
776
+ : self.graph
777
+ ? walkRows(self.graph, self.graphRegion, undefined, { visits })
778
+ : undefined
779
+ },
780
+ // Ribbons, shared blocks and the shared order compare the rows node by
781
+ // node, so the rows carry their visits
782
+ get rowVisitsNeeded() {
783
+ return (
784
+ self.chosenLayoutMode === 'walkrows' &&
785
+ (self.walkRibbons ||
786
+ self.layerSet.has('walkBlocks') ||
787
+ self.walkRowOrder === 'shared')
788
+ )
749
789
  },
750
790
  // An assembly by each name a backbone may spell it with: the session's
751
791
  // name and aliases, and the PanSN prefix the source track maps each of
@@ -852,10 +892,11 @@ export const withGraphViews = paneBase
852
892
  }
853
893
  const repeat = self.selectedRepeat
854
894
  const cut = self.walkCut
895
+ const visits = self.rowVisitsNeeded
855
896
  const bars = repeat
856
897
  ? cut
857
- ? walkRowsFromCut(cut, repeat, repeat.unit)
858
- : walkRows(self.graph, repeat, repeat.unit)
898
+ ? walkRowsFromCut(cut, repeat, repeat.unit, visits)
899
+ : walkRows(self.graph, repeat, repeat.unit, { visits })
859
900
  : self.cutWalkRows
860
901
  if (!bars) {
861
902
  return undefined
@@ -929,10 +970,18 @@ export const withGraphViews = paneBase
929
970
  ? { by: 'column', ...grouped.groupBy, table: grouped.table }
930
971
  : undefined
931
972
  },
932
- // the cohort's rows the sample filter keeps
973
+ // The cohort's rows the sample filter keeps, in the shared order where
974
+ // asked; a grouping then keeps that order within each section
933
975
  get keptCohortRows() {
934
976
  const bars = this.cohortWalkRows
935
- return bars && filterSamples(bars.rows, self.walkRowSamples)
977
+ const rows = bars && filterSamples(bars.rows, self.walkRowSamples)
978
+ if (!bars || !rows || self.walkRowOrder !== 'shared') {
979
+ return rows
980
+ }
981
+ const all = [bars.reference, ...rows]
982
+ return sharedRowOrder(all)
983
+ .filter(i => i > 0)
984
+ .map(i => all[i]!)
936
985
  },
937
986
  // The kept rows in a section per value of the grouped column, or
938
987
  // undefined while ungrouped or the table lacks that column
@@ -952,6 +1001,27 @@ export const withGraphViews = paneBase
952
1001
  ? { ...bars, rows: this.walkRowGroups?.rows ?? rows }
953
1002
  : undefined
954
1003
  },
1004
+ // Where each row walks the same nodes as the row below it, as bar
1005
+ // offsets on both, the reference row first
1006
+ get walkRowRibbons() {
1007
+ const bars = this.walkRowBars
1008
+ if (!self.walkRibbons || !self.rowVisitsNeeded || !bars) {
1009
+ return undefined
1010
+ }
1011
+ const rows = [bars.reference, ...bars.rows]
1012
+ return walkRowLinks(rows, ribbonJoin(rows))
1013
+ },
1014
+ // The stretches of each row through a block of nodes off the
1015
+ // reference's path that two or more rows walk, block 0 the one with the
1016
+ // most bp, which the bars letter A, B, C...
1017
+ get sharedBlocks() {
1018
+ const bars = this.walkRowBars
1019
+ if (!self.layerSet.has('walkBlocks') || !self.rowVisitsNeeded || !bars) {
1020
+ return undefined
1021
+ }
1022
+ const rows = [bars.reference, ...bars.rows]
1023
+ return walkRowBlocks(rows, ribbonJoin(rows))
1024
+ },
955
1025
  // Whether the canvas draws the graph's nodes. A tube map and walk rows
956
1026
  // draw a picture of their own over it, so what sits on nodes (genes,
957
1027
  // bubble halos, deletion arcs, the reference strip) has nowhere to go.
@@ -967,12 +1037,17 @@ export const withGraphViews = paneBase
967
1037
  get genePins() {
968
1038
  dependOn(self.positionsVersion)
969
1039
  const positions = self.layoutResult?.nodePositions
1040
+ // each haplotype's own genes ride its nodes off the reference
1041
+ const genes = [
1042
+ ...(self.backboneGenes ?? []),
1043
+ ...(self.ownGeneFeatures ?? []),
1044
+ ]
970
1045
  return self.showGenes &&
971
1046
  this.drawsNodes &&
972
1047
  self.graph &&
973
- self.backboneGenes &&
1048
+ genes.length > 0 &&
974
1049
  positions
975
- ? genePins(self.graph, self.backboneGenes, positions)
1050
+ ? genePins(self.graph, genes, positions)
976
1051
  : []
977
1052
  },
978
1053
  // The bubbles as halos along their nodes.
@@ -980,10 +1055,17 @@ export const withGraphViews = paneBase
980
1055
  get bubbleHalos() {
981
1056
  dependOn(self.positionsVersion)
982
1057
  const positions = self.layoutResult?.nodePositions
983
- if (!self.showBubbles || !this.drawsNodes || !self.graph || !positions) {
1058
+ // with the layer off, a bubble hovered from a linear view's bubble or
1059
+ // variant track still rings
1060
+ const shown = self.showBubbles
1061
+ ? self.bubbles
1062
+ : self.hoveredBubble
1063
+ ? [self.hoveredBubble]
1064
+ : []
1065
+ if (shown.length === 0 || !this.drawsNodes || !self.graph || !positions) {
984
1066
  return []
985
1067
  }
986
- return bubbleHalos(self.graph, self.bubbles, positions, self.repeatArrays)
1068
+ return bubbleHalos(self.graph, shown, positions, self.repeatArrays)
987
1069
  },
988
1070
  // What `reference-position` paints from, which is also what `auto`
989
1071
  // resolves to on any anchored graph
@@ -22,14 +22,15 @@ import {
22
22
  import { withRenderingActions } from './renderingActions'
23
23
  import { graphLaunchMenuItems } from '../../launchFromGraph/graphMenuItems'
24
24
  import {
25
+ canonicalAssemblyName,
25
26
  highlightInLinearView,
26
27
  launchSyntenyView,
27
28
  showInLinearView,
28
29
  } from '../../launchFromGraph/launchFromGraph'
29
- import { launchTracks } from '../../launchFromGraph/launchTracks'
30
30
  import { pushLaunchItem } from '../../launchFromGraph/pushLaunchItem'
31
31
  import { launchableSyntenyTracks } from '../../launchFromGraph/syntenyTracks'
32
32
  import { downloadText } from '../download'
33
+ import { GENE_ADAPTER_TYPES } from '../genes/geneFeatures'
33
34
  import { HOVER_HIGHLIGHT_VALUES } from '../hoverHighlight'
34
35
  import { TUBE_MAP_PANELS, tubeMapPanelGroups } from '../tubeMapPanels'
35
36
 
@@ -67,10 +68,10 @@ function radioItems<T>(
67
68
  export const withLaunchActions = withRenderingActions
68
69
  .actions(self => ({
69
70
  // Move the linear view already on screen, or open one if there is none,
70
- // and pair with whichever it was. A view being created gets the session's
71
- // annotation for the assembly it opens on (see launchTracks), led by the
72
- // graph's own segments track when the launch is on the reference — the one
73
- // assembly that track is configured for.
71
+ // and pair with whichever it was. A view being created gets the graph's
72
+ // own track when the launch is on the reference, plus the one gene track
73
+ // the graph reads for that assembly: a hub config can hold hundreds of
74
+ // FeatureTracks per assembly, and a launch is a jump to a locus.
74
75
  //
75
76
  // Only a view on the reference is paired with. A node's hover band is on
76
77
  // that assembly, so paired with a view on another the band fails that
@@ -79,16 +80,31 @@ export const withLaunchActions = withRenderingActions
79
80
  showInLinearView(target: { location: GraphLocation; assembly: string }) {
80
81
  const session = getSession(self)
81
82
  const onReference = target.assembly === self.graphRegion?.assemblyName
83
+ const assembly = canonicalAssemblyName(session, target.assembly)
84
+ // an assembly whose annotation nothing names as genes, a strain's BED,
85
+ // still opens on its first feature track rather than on none
86
+ const geneTrack =
87
+ (onReference
88
+ ? self.geneTrack
89
+ : self.geneTracksByAssembly.get(assembly)) ??
90
+ self.sessionTracks.find(
91
+ t =>
92
+ t.assemblies.includes(assembly) &&
93
+ GENE_ADAPTER_TYPES.has(t.adapterType),
94
+ )
82
95
  const viewId = showInLinearView({
83
96
  session,
84
97
  location: target.location,
85
98
  assembly: target.assembly,
86
99
  connectedViewId: self.pairedViewId,
87
- tracks: launchTracks({
88
- session,
89
- assemblyName: target.assembly,
90
- first: onReference ? self.sourceTrackId : undefined,
91
- }),
100
+ tracks: [
101
+ ...new Set(
102
+ [
103
+ onReference ? self.sourceTrackId : undefined,
104
+ geneTrack?.trackId,
105
+ ].filter(id => id !== undefined),
106
+ ),
107
+ ],
92
108
  })
93
109
  if (onReference) {
94
110
  self.pairWithLinearView(viewId)
@@ -282,7 +298,35 @@ export const withLaunchActions = withRenderingActions
282
298
  ]
283
299
  : []),
284
300
  ...(self.chosenLayoutMode === 'walkrows' && self.walkRowBars
285
- ? [{ label: 'Samples', subMenu: this.walkRowSampleMenuItems() }]
301
+ ? [
302
+ { label: 'Samples', subMenu: this.walkRowSampleMenuItems() },
303
+ {
304
+ type: 'checkbox' as const,
305
+ label: 'Ribbons between rows',
306
+ checked: self.walkRibbons,
307
+ onClick: () => {
308
+ self.setLayer('walkRibbons', !self.walkRibbons)
309
+ },
310
+ },
311
+ {
312
+ type: 'checkbox' as const,
313
+ label: 'Letter shared blocks',
314
+ checked: self.layerSet.has('walkBlocks'),
315
+ onClick: () => {
316
+ self.setLayer('walkBlocks', !self.layerSet.has('walkBlocks'))
317
+ },
318
+ },
319
+ {
320
+ type: 'checkbox' as const,
321
+ label: 'Order rows by shared sequence',
322
+ checked: self.walkRowOrder === 'shared',
323
+ onClick: () => {
324
+ self.setWalkRowOrder(
325
+ self.walkRowOrder === 'shared' ? undefined : 'shared',
326
+ )
327
+ },
328
+ },
329
+ ]
286
330
  : []),
287
331
  ...(self.chosenLayoutMode === 'walkrows' &&
288
332
  self.walkRowBars &&
@@ -5,9 +5,12 @@ import {
5
5
  import { getSession } from '@jbrowse/core/util'
6
6
 
7
7
  import { withGraphViews } from './graphViews'
8
+ import { OWN_GENE_CONTIGS } from './paneBase'
8
9
  import {
9
10
  contributingAssemblies,
11
+ nodeAssemblySpans,
10
12
  nodeOwnLocation,
13
+ offReferenceContigs,
11
14
  resolveContributors,
12
15
  resolveLocationAssembly,
13
16
  } from '../../launchFromGraph/contributors'
@@ -170,4 +173,47 @@ export const withLaunchViews = withGraphViews
170
173
  : undefined,
171
174
  }
172
175
  },
176
+ // The gene reads that put each loaded haplotype's own genes on its nodes
177
+ // off the reference: its assembly's gene track over the span those nodes
178
+ // cover, the first OWN_GENE_CONTIGS contigs of them
179
+ get ownGeneReads() {
180
+ const { graph } = self
181
+ if (!graph || !self.showGenes || !self.modeDrawsNodes) {
182
+ return []
183
+ }
184
+ return offReferenceContigs(
185
+ graph,
186
+ self.assemblyResolver,
187
+ self.graphRegion?.assemblyName,
188
+ )
189
+ .flatMap(({ stableRefName, assemblyName, refName, start, end }) => {
190
+ const track = self.geneTracksByAssembly.get(assemblyName)
191
+ return track
192
+ ? [
193
+ {
194
+ stableRefName,
195
+ trackId: track.trackId,
196
+ region: { assemblyName, refName, start, end },
197
+ },
198
+ ]
199
+ : []
200
+ })
201
+ .slice(0, OWN_GENE_CONTIGS)
202
+ },
203
+ // The hovered node's own sequence on every loaded assembly but the
204
+ // reference, for linear views of those assemblies to band: an allele off
205
+ // the reference is a few hundred bp "instead of" on GRCh38, and here it
206
+ // is where it actually lives on each haplotype
207
+ get assemblyHoverHighlights() {
208
+ const id = self.hoveredNode
209
+ const node = id === null ? undefined : self.nodeById?.get(id)
210
+ return node && self.graph && self.chosenHover !== 'off'
211
+ ? nodeAssemblySpans(
212
+ self.graph,
213
+ node,
214
+ self.assemblyResolver,
215
+ self.graphRegion?.assemblyName,
216
+ )
217
+ : []
218
+ },
173
219
  }))
@@ -9,6 +9,7 @@ import {
9
9
  import { walkRowLayoutOf } from '@jbrowse/bandage-core/layout/walkRowLayout'
10
10
  import { walkRowsOf } from '@jbrowse/bandage-core/layout/walkRowRuns'
11
11
  import { layoutModeByValue } from '@jbrowse/bandage-core/layoutModes'
12
+ import { panSNHaplotype, panSNSample } from '@jbrowse/bandage-core/pansn'
12
13
  import {
13
14
  anchorFromPaths,
14
15
  chooseReferencePath,
@@ -93,6 +94,15 @@ export const withLoadActions = withEngineSettings.actions(self => {
93
94
  }) as Promise<{ result: LayoutResult; duration: number }>
94
95
  }
95
96
 
97
+ // the node cap is a drawing's cost
98
+ function assertDrawable(graph: Graph) {
99
+ if (graph.nodes.length > self.maxGraphNodes) {
100
+ throw new Error(
101
+ `Graph too large to draw: ${graph.nodes.length.toLocaleString()} nodes (limit ${self.maxGraphNodes.toLocaleString()}). Pick Layout → Walk rows, zoom in to a smaller region, or raise the node limit in Graph settings.`,
102
+ )
103
+ }
104
+ }
105
+
96
106
  // Single dispatch point for every layout mode. A mode that returns a
97
107
  // result computed it locally; one that returns undefined can't draw this
98
108
  // graph and hands off to the remote FMMM engine, which is also how
@@ -150,6 +160,8 @@ export const withLoadActions = withEngineSettings.actions(self => {
150
160
  mode: self.chosenLayoutMode,
151
161
  }
152
162
  }
163
+ // walk rows and tube maps fall back here for a graph with no walks
164
+ assertDrawable(graph)
153
165
  const settings = self.engineSettings
154
166
  const hit = forceLayouts.ready(graph, settings)
155
167
  if (hit) {
@@ -209,11 +221,9 @@ export const withLoadActions = withEngineSettings.actions(self => {
209
221
  const signal = loadController?.signal
210
222
  let computed
211
223
  try {
212
- // the node cap is a drawing's cost, and walk rows draw a bar per walk
213
- if (self.modeDrawsNodes && graph.nodes.length > self.maxGraphNodes) {
214
- throw new Error(
215
- `Graph too large to draw: ${graph.nodes.length.toLocaleString()} nodes (limit ${self.maxGraphNodes.toLocaleString()}). Pick Layout → Walk rows, zoom in to a smaller region, or raise the node limit in Graph settings.`,
216
- )
224
+ // walk rows draw a bar per walk, not a node each
225
+ if (self.modeDrawsNodes) {
226
+ assertDrawable(graph)
217
227
  }
218
228
  computed = yield* computeLayout(graph)
219
229
  } catch (e) {
@@ -314,6 +324,7 @@ export const withLoadActions = withEngineSettings.actions(self => {
314
324
  self.indexBubbles = undefined
315
325
  self.geneTrackFeatures = undefined
316
326
  self.walkGeneFeatures = undefined
327
+ self.ownGeneFeatures = undefined
317
328
  walkGeneCache.clear()
318
329
  self.repeatArrays = undefined
319
330
  self.clearInteractionState()
@@ -478,11 +489,18 @@ export const withLoadActions = withEngineSettings.actions(self => {
478
489
  { adapterConfig, regions: [region], signal },
479
490
  )) as Feature[]
480
491
  } catch (e) {
481
- console.warn(`[GraphGenomeView] no ${what} for this graph`, e)
492
+ // a cut the next one replaced aborts its reads, which is not a failure
493
+ if (!signal?.aborted) {
494
+ console.warn(`[GraphGenomeView] no ${what} for this graph`, e)
495
+ }
482
496
  return undefined
483
497
  }
484
498
  }
485
499
 
500
+ // Bubble indexes a read failed on, so a graph without one, the ordinary
501
+ // case, is not asked again on every re-cut of a pan
502
+ const missingBubbleIndexes = new Set<string>()
503
+
486
504
  // The bubble index that the hosted HPRC build keeps beside its segments,
487
505
  // `<prefix>.bubbles.bed.gz`, read through the bubble adapter over the same
488
506
  // window. A graph whose source has no such file, the ordinary case for a
@@ -499,13 +517,18 @@ export const withLoadActions = withEngineSettings.actions(self => {
499
517
  // The track config arrives as written, so the prefix is either the
500
518
  // `uri` shorthand or the segments location it expands to.
501
519
  const prefix = bubblePrefix(adapterConfig)
502
- if (adapterConfig.type !== 'RgfaTabixAdapter' || prefix === undefined) {
520
+ const uri = `${prefix}.bubbles.bed.gz`
521
+ if (
522
+ adapterConfig.type !== 'RgfaTabixAdapter' ||
523
+ prefix === undefined ||
524
+ missingBubbleIndexes.has(uri)
525
+ ) {
503
526
  return
504
527
  }
505
528
  const features = yield* readFeatures(
506
529
  {
507
530
  type: 'MinigraphBubbleAdapter',
508
- uri: `${prefix}.bubbles.bed.gz`,
531
+ uri,
509
532
  baseUri: adapterConfig.baseUri,
510
533
  assemblyNameToPanSN: adapterConfig.assemblyNameToPanSN,
511
534
  },
@@ -513,6 +536,9 @@ export const withLoadActions = withEngineSettings.actions(self => {
513
536
  'bubble index',
514
537
  signal,
515
538
  )
539
+ if (features === undefined && !signal?.aborted) {
540
+ missingBubbleIndexes.add(uri)
541
+ }
516
542
  if (features && isLive()) {
517
543
  self.indexBubbles = features.flatMap(f => {
518
544
  const start = f.get('start')
@@ -559,6 +585,25 @@ export const withLoadActions = withEngineSettings.actions(self => {
559
585
  )
560
586
  }
561
587
 
588
+ // A haplotype assembly's genes over a span of its own contig, named for
589
+ // that contig whatever the track calls it, read once per cut
590
+ function* haplotypeGenes(trackId: string, region: SubgraphRegion) {
591
+ const cacheKey = `${trackId} ${region.refName}:${region.start}-${region.end}`
592
+ let genes = walkGeneCache.get(cacheKey)
593
+ if (!genes) {
594
+ const features = yield* trackFeatures(trackId, region, 'haplotype genes')
595
+ if (!features) {
596
+ return undefined
597
+ }
598
+ genes = geneModelsFrom(features).map(g => ({
599
+ ...g,
600
+ refName: region.refName,
601
+ }))
602
+ walkGeneCache.set(cacheKey, genes)
603
+ }
604
+ return genes
605
+ }
606
+
562
607
  // The annotation tracks read over the cut: genes, which `geneFeatures`
563
608
  // makes genes of for the backbone to carry, and tandem repeat arrays, for
564
609
  // the walk rows to measure between and tile by
@@ -860,24 +905,10 @@ export const withLoadActions = withEngineSettings.actions(self => {
860
905
  yield Promise.all(
861
906
  reads.map(({ row, trackId, region }) =>
862
907
  flow(function* () {
863
- const cacheKey = `${trackId} ${region.refName}:${region.start}-${region.end}`
864
- let genes = walkGeneCache.get(cacheKey)
865
- if (!genes) {
866
- const features = yield* trackFeatures(
867
- trackId,
868
- region,
869
- 'haplotype genes',
870
- )
871
- if (!features) {
872
- return
873
- }
874
- genes = geneModelsFrom(features).map(g => ({
875
- ...g,
876
- refName: region.refName,
877
- }))
878
- walkGeneCache.set(cacheKey, genes)
908
+ const genes = yield* haplotypeGenes(trackId, region)
909
+ if (genes) {
910
+ fetched.set(row, genes)
879
911
  }
880
- fetched.set(row, genes)
881
912
  })(),
882
913
  ),
883
914
  )
@@ -889,6 +920,35 @@ export const withLoadActions = withEngineSettings.actions(self => {
889
920
  self.walkGeneFeatures = reads.length ? fetched : undefined
890
921
  }
891
922
  }),
923
+ // Each loaded haplotype's genes on its own off-reference nodes, renamed
924
+ // to those nodes' stable sequence so genePins lands them there
925
+ loadOwnGenes: flow(function* () {
926
+ const reads = self.ownGeneReads
927
+ const key = JSON.stringify(reads)
928
+ const graph = self.graph
929
+ const fetched = (yield Promise.all(
930
+ reads.map(({ stableRefName, trackId, region }) =>
931
+ flow(function* () {
932
+ const genes = yield* haplotypeGenes(trackId, region)
933
+ // a chip reading LPA twice would not say whose copy each is
934
+ const whose =
935
+ panSNHaplotype(stableRefName) ?? panSNSample(stableRefName)
936
+ return (genes ?? []).map(g => ({
937
+ ...g,
938
+ name: `${g.name} · ${whose}`,
939
+ refName: stableRefName,
940
+ }))
941
+ })(),
942
+ ),
943
+ )) as GeneModel[][]
944
+ if (
945
+ isAlive(self) &&
946
+ self.graph === graph &&
947
+ JSON.stringify(self.ownGeneReads) === key
948
+ ) {
949
+ self.ownGeneFeatures = reads.length ? fetched.flat() : undefined
950
+ }
951
+ }),
892
952
  // Open a bubble in place: it keeps its colour, the rest of the drawing
893
953
  // greys, and the bubbles inside it get labels of their own. Opening an
894
954
  // open one closes it and those inside it.
@@ -1,5 +1,5 @@
1
1
  import { abgrAlpha, packAbgr } from '@jbrowse/bandage-core/renderer/colorBits'
2
- import { expect, test } from 'vitest'
2
+ import { expect, test, vi } from 'vitest'
3
3
 
4
4
  import {
5
5
  blendInto,
@@ -7,6 +7,7 @@ import {
7
7
  fadeEntering,
8
8
  morphStarts,
9
9
  routeStarts,
10
+ runMorph,
10
11
  } from './morph'
11
12
 
12
13
  import type { RenderBatch } from '@jbrowse/bandage-core/renderer/types'
@@ -248,3 +249,26 @@ test('a graph past the frame budget snaps', () => {
248
249
  expect(canMorph(free, free, 3000, false)).toBe(true)
249
250
  expect(canMorph(free, free, 3001, false)).toBe(false)
250
251
  })
252
+
253
+ // The layout cache holds the positions a morph blends, so a morph abandoned
254
+ // half way would leave a cached layout half blended for the next cache hit
255
+ test('a morph cut short lands its last frame', () => {
256
+ const frames: FrameRequestCallback[] = []
257
+ vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) =>
258
+ frames.push(cb),
259
+ )
260
+ const blends: number[] = []
261
+ let live = true
262
+ const ended = vi.fn()
263
+ runMorph(
264
+ t => blends.push(t),
265
+ () => live,
266
+ ended,
267
+ )
268
+ frames.shift()!(0)
269
+ live = false
270
+ frames.shift()!(0)
271
+ expect(blends.at(-1)).toBe(1)
272
+ expect(ended).toHaveBeenCalledTimes(1)
273
+ vi.unstubAllGlobals()
274
+ })