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
@@ -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,
@@ -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
 
@@ -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
@@ -227,6 +241,10 @@ export const withGraphViews = paneBase
227
241
  get contigThickness() {
228
242
  return nodeSizeOf(self.grammar.size).px
229
243
  },
244
+ // Link thickness by the walks taking it; see EDGE_WIDTHS
245
+ get linkWidth(): EdgeWidth {
246
+ return nodeSizeOf(self.grammar.size).links
247
+ },
230
248
  get layerSet() {
231
249
  return layersOf(self.grammar.layers)
232
250
  },
@@ -248,6 +266,9 @@ export const withGraphViews = paneBase
248
266
  get walkStrip() {
249
267
  return this.layerSet.has('walkStrip')
250
268
  },
269
+ get walkRibbons() {
270
+ return this.layerSet.has('walkRibbons')
271
+ },
251
272
  // One section per value of a field, as a grammar of graphics splits a
252
273
  // plot. A node layout draws a panel per `walk` (each lifted walk alone,
253
274
  // on the same layout) or per `sample` (a sample's haplotypes in a row);
@@ -265,6 +286,9 @@ export const withGraphViews = paneBase
265
286
  const kept = self.grammar.rows?.kept
266
287
  return Array.isArray(kept) ? kept : undefined
267
288
  },
289
+ get walkRowOrder() {
290
+ return self.grammar.rows?.order === 'shared' ? 'shared' : undefined
291
+ },
268
292
  get hoverLightsEverything() {
269
293
  return this.chosenHover === 'everything'
270
294
  },
@@ -740,12 +764,26 @@ export const withGraphViews = paneBase
740
764
  // Each walk sliced to the window, for walk rows and the strip; a repeat
741
765
  // pick slices again by its own array
742
766
  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
767
+ const cut = self.walkCut
768
+ const runs = cut?.walkRowRuns
769
+ const visits = this.rowVisitsNeeded
770
+ return cut && runs && visits && self.graphRegion
771
+ ? walkRowsFromCut(cut, self.graphRegion, undefined, true)
772
+ : runs
773
+ ? walkRowsOf(runs)
774
+ : self.graph
775
+ ? walkRows(self.graph, self.graphRegion, undefined, { visits })
776
+ : undefined
777
+ },
778
+ // Ribbons, shared blocks and the shared order compare the rows node by
779
+ // node, so the rows carry their visits
780
+ get rowVisitsNeeded() {
781
+ return (
782
+ self.chosenLayoutMode === 'walkrows' &&
783
+ (self.walkRibbons ||
784
+ self.layerSet.has('walkBlocks') ||
785
+ self.walkRowOrder === 'shared')
786
+ )
749
787
  },
750
788
  // An assembly by each name a backbone may spell it with: the session's
751
789
  // name and aliases, and the PanSN prefix the source track maps each of
@@ -852,10 +890,11 @@ export const withGraphViews = paneBase
852
890
  }
853
891
  const repeat = self.selectedRepeat
854
892
  const cut = self.walkCut
893
+ const visits = self.rowVisitsNeeded
855
894
  const bars = repeat
856
895
  ? cut
857
- ? walkRowsFromCut(cut, repeat, repeat.unit)
858
- : walkRows(self.graph, repeat, repeat.unit)
896
+ ? walkRowsFromCut(cut, repeat, repeat.unit, visits)
897
+ : walkRows(self.graph, repeat, repeat.unit, { visits })
859
898
  : self.cutWalkRows
860
899
  if (!bars) {
861
900
  return undefined
@@ -929,10 +968,18 @@ export const withGraphViews = paneBase
929
968
  ? { by: 'column', ...grouped.groupBy, table: grouped.table }
930
969
  : undefined
931
970
  },
932
- // the cohort's rows the sample filter keeps
971
+ // The cohort's rows the sample filter keeps, in the shared order where
972
+ // asked; a grouping then keeps that order within each section
933
973
  get keptCohortRows() {
934
974
  const bars = this.cohortWalkRows
935
- return bars && filterSamples(bars.rows, self.walkRowSamples)
975
+ const rows = bars && filterSamples(bars.rows, self.walkRowSamples)
976
+ if (!bars || !rows || self.walkRowOrder !== 'shared') {
977
+ return rows
978
+ }
979
+ const all = [bars.reference, ...rows]
980
+ return sharedRowOrder(all)
981
+ .filter(i => i > 0)
982
+ .map(i => all[i]!)
936
983
  },
937
984
  // The kept rows in a section per value of the grouped column, or
938
985
  // undefined while ungrouped or the table lacks that column
@@ -952,6 +999,27 @@ export const withGraphViews = paneBase
952
999
  ? { ...bars, rows: this.walkRowGroups?.rows ?? rows }
953
1000
  : undefined
954
1001
  },
1002
+ // Where each row walks the same nodes as the row below it, as bar
1003
+ // offsets on both, the reference row first
1004
+ get walkRowRibbons() {
1005
+ const bars = this.walkRowBars
1006
+ if (!self.walkRibbons || !self.rowVisitsNeeded || !bars) {
1007
+ return undefined
1008
+ }
1009
+ const rows = [bars.reference, ...bars.rows]
1010
+ return walkRowLinks(rows, ribbonJoin(rows))
1011
+ },
1012
+ // The stretches of each row through a block of nodes off the
1013
+ // reference's path that two or more rows walk, block 0 the one with the
1014
+ // most bp, which the bars letter A, B, C...
1015
+ get sharedBlocks() {
1016
+ const bars = this.walkRowBars
1017
+ if (!self.layerSet.has('walkBlocks') || !self.rowVisitsNeeded || !bars) {
1018
+ return undefined
1019
+ }
1020
+ const rows = [bars.reference, ...bars.rows]
1021
+ return walkRowBlocks(rows, ribbonJoin(rows))
1022
+ },
955
1023
  // Whether the canvas draws the graph's nodes. A tube map and walk rows
956
1024
  // draw a picture of their own over it, so what sits on nodes (genes,
957
1025
  // bubble halos, deletion arcs, the reference strip) has nowhere to go.
@@ -967,12 +1035,17 @@ export const withGraphViews = paneBase
967
1035
  get genePins() {
968
1036
  dependOn(self.positionsVersion)
969
1037
  const positions = self.layoutResult?.nodePositions
1038
+ // each haplotype's own genes ride its nodes off the reference
1039
+ const genes = [
1040
+ ...(self.backboneGenes ?? []),
1041
+ ...(self.ownGeneFeatures ?? []),
1042
+ ]
970
1043
  return self.showGenes &&
971
1044
  this.drawsNodes &&
972
1045
  self.graph &&
973
- self.backboneGenes &&
1046
+ genes.length > 0 &&
974
1047
  positions
975
- ? genePins(self.graph, self.backboneGenes, positions)
1048
+ ? genePins(self.graph, genes, positions)
976
1049
  : []
977
1050
  },
978
1051
  // The bubbles as halos along their nodes.
@@ -980,10 +1053,17 @@ export const withGraphViews = paneBase
980
1053
  get bubbleHalos() {
981
1054
  dependOn(self.positionsVersion)
982
1055
  const positions = self.layoutResult?.nodePositions
983
- if (!self.showBubbles || !this.drawsNodes || !self.graph || !positions) {
1056
+ // with the layer off, a bubble hovered from a linear view's bubble or
1057
+ // variant track still rings
1058
+ const shown = self.showBubbles
1059
+ ? self.bubbles
1060
+ : self.hoveredBubble
1061
+ ? [self.hoveredBubble]
1062
+ : []
1063
+ if (shown.length === 0 || !this.drawsNodes || !self.graph || !positions) {
984
1064
  return []
985
1065
  }
986
- return bubbleHalos(self.graph, self.bubbles, positions, self.repeatArrays)
1066
+ return bubbleHalos(self.graph, shown, positions, self.repeatArrays)
987
1067
  },
988
1068
  // What `reference-position` paints from, which is also what `auto`
989
1069
  // 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
+ })
@@ -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