jbrowse-plugin-graphgenomeviewer 7.4.3 → 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 (170) hide show
  1. package/README.md +10 -7
  2. package/dist/chunks/{AddTrackWorkflow-LK25EC3H.js → AddTrackWorkflow-NLSZYK56.js} +2 -2
  3. package/dist/chunks/{GbzBaseSyntenyAdapter-EKLUEECW.js → GbzBaseSyntenyAdapter-4KVWVUN6.js} +2 -2
  4. package/dist/chunks/{GbzBaseSyntenyAdapter-EKLUEECW.js.map → GbzBaseSyntenyAdapter-4KVWVUN6.js.map} +1 -1
  5. package/dist/chunks/{GraphGenomeView-P3XMSKUQ.js → GraphGenomeView-KI4FQ5A7.js} +2 -2
  6. package/dist/chunks/GraphTrackSettingsDialog-UQNMDXTE.js +4 -0
  7. package/dist/chunks/GraphTrackSettingsDialog-UQNMDXTE.js.map +7 -0
  8. package/dist/chunks/{HighlightColorDialog-G6AFRYUD.js → HighlightColorDialog-D4RCUCUF.js} +2 -2
  9. package/dist/chunks/LinearGraphDisplay-RPZUZSM2.js +2 -0
  10. package/dist/chunks/{LinearGraphDisplay-O4SQSXJJ.js.map → LinearGraphDisplay-RPZUZSM2.js.map} +2 -2
  11. package/dist/chunks/MinigraphBubbleAdapter-TJYI7KEN.js +2 -0
  12. package/dist/chunks/{MinigraphBubbleAdapter-3NJ2VLN2.js.map → MinigraphBubbleAdapter-TJYI7KEN.js.map} +1 -1
  13. package/dist/chunks/RgfaTabixAdapter-5ZMJLFZP.js +2 -0
  14. package/dist/chunks/RgfaTabixAdapter-5ZMJLFZP.js.map +7 -0
  15. package/dist/chunks/WalkFormsAdapter-CAJOWOEX.js +2 -0
  16. package/dist/chunks/WalkFormsAdapter-CAJOWOEX.js.map +7 -0
  17. package/dist/chunks/WalkTabixSyntenyAdapter-VI3KA2GX.js +2 -0
  18. package/dist/chunks/{WalkTabixSyntenyAdapter-NHNSZZBX.js.map → WalkTabixSyntenyAdapter-VI3KA2GX.js.map} +2 -2
  19. package/dist/chunks/{chunk-MCE6STZR.js → chunk-22ANI2KL.js} +2 -2
  20. package/dist/chunks/{chunk-MCE6STZR.js.map → chunk-22ANI2KL.js.map} +1 -1
  21. package/dist/chunks/chunk-42KD4AVY.js +2 -0
  22. package/dist/chunks/{chunk-OKRGO7XA.js.map → chunk-42KD4AVY.js.map} +4 -4
  23. package/dist/chunks/chunk-4NIWMRKH.js +2 -0
  24. package/dist/chunks/chunk-4NIWMRKH.js.map +7 -0
  25. package/dist/chunks/chunk-BCOXRUNQ.js +3 -0
  26. package/dist/chunks/chunk-BCOXRUNQ.js.map +7 -0
  27. package/dist/chunks/chunk-DAPI7M4M.js +4 -0
  28. package/dist/chunks/chunk-DAPI7M4M.js.map +7 -0
  29. package/dist/chunks/chunk-DEWZOJSC.js +2 -0
  30. package/dist/chunks/{chunk-6OXXT2YP.js.map → chunk-DEWZOJSC.js.map} +4 -4
  31. package/dist/chunks/{chunk-K4TFAHGC.js → chunk-FCMXEFJA.js} +1 -1
  32. package/dist/chunks/{chunk-K4TFAHGC.js.map → chunk-FCMXEFJA.js.map} +1 -1
  33. package/dist/chunks/{chunk-UOFWYS6T.js → chunk-GYY6LF5L.js} +1 -1
  34. package/dist/chunks/{chunk-UOFWYS6T.js.map → chunk-GYY6LF5L.js.map} +1 -1
  35. package/dist/chunks/{chunk-F2DMEJEK.js → chunk-HHMJNAA3.js} +2 -2
  36. package/dist/chunks/{chunk-NPV7SRJD.js → chunk-HTTONSO4.js} +2 -2
  37. package/dist/chunks/{chunk-NPV7SRJD.js.map → chunk-HTTONSO4.js.map} +1 -1
  38. package/dist/chunks/{chunk-MVVXRZXV.js → chunk-JRJHXLGP.js} +2 -2
  39. package/dist/chunks/{chunk-MVVXRZXV.js.map → chunk-JRJHXLGP.js.map} +2 -2
  40. package/dist/chunks/chunk-K4JL57NR.js +2 -0
  41. package/dist/chunks/chunk-K4JL57NR.js.map +7 -0
  42. package/dist/chunks/chunk-KOC23YNN.js +4 -0
  43. package/dist/chunks/chunk-KOC23YNN.js.map +7 -0
  44. package/dist/chunks/{chunk-MNCPMOHJ.js → chunk-LTNFVL2F.js} +2 -2
  45. package/dist/chunks/chunk-OZEKEYT3.js +2 -0
  46. package/dist/chunks/{chunk-ZAYC5OMQ.js.map → chunk-OZEKEYT3.js.map} +4 -4
  47. package/dist/chunks/{chunk-DUPBXBYE.js → chunk-POUV7OPY.js} +2 -2
  48. package/dist/chunks/{chunk-DUPBXBYE.js.map → chunk-POUV7OPY.js.map} +1 -1
  49. package/dist/chunks/{chunk-CNNZDE6I.js → chunk-SPL3UO2D.js} +2 -2
  50. package/dist/chunks/{chunk-CNNZDE6I.js.map → chunk-SPL3UO2D.js.map} +1 -1
  51. package/dist/chunks/{chunk-QJ43NDYS.js → chunk-TYV6UXJA.js} +2 -2
  52. package/dist/chunks/chunk-UR2MJJUP.js +2 -0
  53. package/dist/chunks/chunk-VTMJQFN5.js +2 -0
  54. package/dist/chunks/chunk-VTMJQFN5.js.map +7 -0
  55. package/dist/chunks/{chunk-CXOMX2I6.js → chunk-XZGYJ3JC.js} +1 -1
  56. package/dist/chunks/{chunk-CXOMX2I6.js.map → chunk-XZGYJ3JC.js.map} +1 -1
  57. package/dist/chunks/{chunk-TGSDXZXI.js → chunk-Z3IKZAYC.js} +2 -2
  58. package/dist/chunks/{chunk-TGSDXZXI.js.map → chunk-Z3IKZAYC.js.map} +1 -1
  59. package/dist/chunks/chunk-ZV5FNOHS.js +3 -0
  60. package/dist/chunks/chunk-ZV5FNOHS.js.map +7 -0
  61. package/dist/chunks/{renderSvg-7G6NWBXF.js → renderSvg-3PPAEMBT.js} +2 -2
  62. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +4 -4
  63. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
  64. package/package.json +3 -3
  65. package/src/GraphComputeLayout.test.ts +24 -0
  66. package/src/GraphComputeLayout.ts +4 -2
  67. package/src/GraphGenomeView/components/GraphSettingsDialog.tsx +16 -0
  68. package/src/GraphGenomeView/components/WalkRowsOverlay.tsx +83 -0
  69. package/src/GraphGenomeView/components/usePaneGestures.test.tsx +82 -5
  70. package/src/GraphGenomeView/components/usePaneGestures.ts +10 -4
  71. package/src/GraphGenomeView/graphLayers.ts +14 -1
  72. package/src/GraphGenomeView/legendSections.ts +32 -0
  73. package/src/GraphGenomeView/model.test.ts +294 -113
  74. package/src/GraphGenomeView/model.ts +3 -10
  75. package/src/GraphGenomeView/nodeSize.test.ts +21 -5
  76. package/src/GraphGenomeView/nodeSize.ts +24 -8
  77. package/src/GraphGenomeView/pane/fitViews.ts +5 -110
  78. package/src/GraphGenomeView/pane/graphViews.ts +96 -20
  79. package/src/GraphGenomeView/pane/launchActions.ts +55 -33
  80. package/src/GraphGenomeView/pane/launchViews.ts +46 -0
  81. package/src/GraphGenomeView/pane/loadActions.ts +85 -25
  82. package/src/GraphGenomeView/pane/morph.test.ts +25 -1
  83. package/src/GraphGenomeView/pane/morph.ts +3 -1
  84. package/src/GraphGenomeView/pane/paneBase.ts +5 -7
  85. package/src/GraphGenomeView/pane/renderingActions.ts +61 -2
  86. package/src/GraphGenomeView/pane/settingActions.ts +62 -4
  87. package/src/GraphGenomeView/pane/walkRowViews.ts +17 -6
  88. package/src/GraphGenomeView/tubeMapPanels.ts +2 -1
  89. package/src/GraphGenomeView/viewModel.ts +4 -9
  90. package/src/GraphGenomeView/walkRowBlocks.ts +50 -0
  91. package/src/LinearGraphDisplay/LinearGraphDisplay.test.ts +35 -5
  92. package/src/LinearGraphDisplay/components/GraphStatusChrome.test.tsx +18 -0
  93. package/src/LinearGraphDisplay/components/GraphStatusChrome.tsx +4 -2
  94. package/src/LinearGraphDisplay/components/SubgraphHaplotypesField.test.tsx +41 -1
  95. package/src/LinearGraphDisplay/components/SubgraphHaplotypesField.tsx +33 -19
  96. package/src/LinearGraphDisplay/configSchema.ts +4 -3
  97. package/src/RgfaTabixAdapter/RgfaTabixAdapter.ts +2 -9
  98. package/src/RgfaTabixAdapter/configSchema.ts +3 -3
  99. package/src/RgfaTabixAdapter/walkFiles.test.ts +6 -6
  100. package/src/WalkFormsAdapter/WalkFormsAdapter.test.ts +204 -0
  101. package/src/WalkFormsAdapter/WalkFormsAdapter.ts +232 -0
  102. package/src/WalkFormsAdapter/configSchema.ts +127 -0
  103. package/src/WalkFormsAdapter/formRows.test.ts +258 -0
  104. package/src/WalkFormsAdapter/formRows.ts +231 -0
  105. package/src/WalkFormsAdapter/formsMenu.test.ts +237 -0
  106. package/src/WalkFormsAdapter/formsMenu.ts +202 -0
  107. package/src/WalkFormsAdapter/index.ts +21 -0
  108. package/src/WalkFormsAdapter/pinned.ts +33 -0
  109. package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.test.ts +5 -5
  110. package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.ts +3 -6
  111. package/src/WalkTabixSyntenyAdapter/configSchema.ts +2 -2
  112. package/src/hoverSync/graphViewHighlights.test.ts +53 -0
  113. package/src/hoverSync/graphViewHighlights.ts +25 -0
  114. package/src/hoverSync/lgvHover.test.ts +100 -1
  115. package/src/hoverSync/lgvHover.ts +77 -6
  116. package/src/index.ts +4 -0
  117. package/src/launchFromGraph/contributors.test.ts +52 -0
  118. package/src/launchFromGraph/contributors.ts +105 -1
  119. package/src/launchFromGraph/launchFromGraph.ts +2 -2
  120. package/src/panSNAliases/panSNAliases.test.ts +2 -2
  121. package/src/panSNTabix.test.ts +28 -0
  122. package/src/panSNTabix.ts +26 -8
  123. package/src/version.ts +1 -1
  124. package/dist/chunks/GraphTrackSettingsDialog-UTYH6HEC.js +0 -6
  125. package/dist/chunks/GraphTrackSettingsDialog-UTYH6HEC.js.map +0 -7
  126. package/dist/chunks/LinearGraphDisplay-O4SQSXJJ.js +0 -2
  127. package/dist/chunks/MinigraphBubbleAdapter-3NJ2VLN2.js +0 -2
  128. package/dist/chunks/RgfaTabixAdapter-55DGDID2.js +0 -2
  129. package/dist/chunks/RgfaTabixAdapter-55DGDID2.js.map +0 -7
  130. package/dist/chunks/WalkTabixSyntenyAdapter-NHNSZZBX.js +0 -2
  131. package/dist/chunks/chunk-4I3K4RHE.js +0 -2
  132. package/dist/chunks/chunk-4I3K4RHE.js.map +0 -7
  133. package/dist/chunks/chunk-6OXXT2YP.js +0 -2
  134. package/dist/chunks/chunk-NFH2U74B.js +0 -2
  135. package/dist/chunks/chunk-OKRGO7XA.js +0 -2
  136. package/dist/chunks/chunk-P7VMOTUM.js +0 -3
  137. package/dist/chunks/chunk-P7VMOTUM.js.map +0 -7
  138. package/dist/chunks/chunk-RU4WSKBO.js +0 -3
  139. package/dist/chunks/chunk-RU4WSKBO.js.map +0 -7
  140. package/dist/chunks/chunk-WGTLNVST.js +0 -2
  141. package/dist/chunks/chunk-WGTLNVST.js.map +0 -7
  142. package/dist/chunks/chunk-XQFBLJU5.js +0 -4
  143. package/dist/chunks/chunk-XQFBLJU5.js.map +0 -7
  144. package/dist/chunks/chunk-Y5DNBFDY.js +0 -3
  145. package/dist/chunks/chunk-Y5DNBFDY.js.map +0 -7
  146. package/dist/chunks/chunk-ZAYC5OMQ.js +0 -2
  147. package/src/launchFromGraph/launchTracks.test.ts +0 -95
  148. package/src/launchFromGraph/launchTracks.ts +0 -49
  149. /package/dist/chunks/{AddTrackWorkflow-LK25EC3H.js.map → AddTrackWorkflow-NLSZYK56.js.map} +0 -0
  150. /package/dist/chunks/{GraphGenomeView-P3XMSKUQ.js.map → GraphGenomeView-KI4FQ5A7.js.map} +0 -0
  151. /package/dist/chunks/{HighlightColorDialog-G6AFRYUD.js.map → HighlightColorDialog-D4RCUCUF.js.map} +0 -0
  152. /package/dist/chunks/{chunk-F2DMEJEK.js.map → chunk-HHMJNAA3.js.map} +0 -0
  153. /package/dist/chunks/{chunk-MNCPMOHJ.js.map → chunk-LTNFVL2F.js.map} +0 -0
  154. /package/dist/chunks/{chunk-QJ43NDYS.js.map → chunk-TYV6UXJA.js.map} +0 -0
  155. /package/dist/chunks/{chunk-NFH2U74B.js.map → chunk-UR2MJJUP.js.map} +0 -0
  156. /package/dist/chunks/{renderSvg-7G6NWBXF.js.map → renderSvg-3PPAEMBT.js.map} +0 -0
  157. /package/src/RgfaTabixAdapter/test_data/{walks4_follow.GRCh38.walks.bed.gz → walks5_follow.GRCh38.walks.bed.gz} +0 -0
  158. /package/src/RgfaTabixAdapter/test_data/{walks4_follow.GRCh38.walks.bed.gz.tbi → walks5_follow.GRCh38.walks.bed.gz.tbi} +0 -0
  159. /package/src/RgfaTabixAdapter/test_data/{walks4_header.GRCh38.walks.bed.gz → walks5_header.GRCh38.walks.bed.gz} +0 -0
  160. /package/src/RgfaTabixAdapter/test_data/{walks4_header.GRCh38.walks.bed.gz.tbi → walks5_header.GRCh38.walks.bed.gz.tbi} +0 -0
  161. /package/src/RgfaTabixAdapter/test_data/{walks4_inversion.GRCh38.walks.bed.gz → walks5_inversion.GRCh38.walks.bed.gz} +0 -0
  162. /package/src/RgfaTabixAdapter/test_data/{walks4_inversion.GRCh38.walks.bed.gz.tbi → walks5_inversion.GRCh38.walks.bed.gz.tbi} +0 -0
  163. /package/src/RgfaTabixAdapter/test_data/{walks4_lanepairs.GRCh38.walks.bed.gz → walks5_lanepairs.GRCh38.walks.bed.gz} +0 -0
  164. /package/src/RgfaTabixAdapter/test_data/{walks4_lanepairs.GRCh38.walks.bed.gz.tbi → walks5_lanepairs.GRCh38.walks.bed.gz.tbi} +0 -0
  165. /package/src/RgfaTabixAdapter/test_data/{walks4_maxnode.GRCh38.walks.bed.gz → walks5_maxnode.GRCh38.walks.bed.gz} +0 -0
  166. /package/src/RgfaTabixAdapter/test_data/{walks4_maxnode.GRCh38.walks.bed.gz.tbi → walks5_maxnode.GRCh38.walks.bed.gz.tbi} +0 -0
  167. /package/src/RgfaTabixAdapter/test_data/{walks4_rejoin.GRCh38.walks.bed.gz → walks5_rejoin.GRCh38.walks.bed.gz} +0 -0
  168. /package/src/RgfaTabixAdapter/test_data/{walks4_rejoin.GRCh38.walks.bed.gz.tbi → walks5_rejoin.GRCh38.walks.bed.gz.tbi} +0 -0
  169. /package/src/RgfaTabixAdapter/test_data/{walks4_twopart.GRCh38.walks.bed.gz → walks5_twopart.GRCh38.walks.bed.gz} +0 -0
  170. /package/src/RgfaTabixAdapter/test_data/{walks4_twopart.GRCh38.walks.bed.gz.tbi → walks5_twopart.GRCh38.walks.bed.gz.tbi} +0 -0
@@ -868,8 +868,14 @@ test('walk rows fit their own bars in the track, since a walk can be longer than
868
868
  expect(pane.viewportOwner).toBe('fit')
869
869
  })
870
870
 
871
- test('walk rows cut every snarl a walk takes past the window, so a walk comes whole', async () => {
872
- const { pane, cuts } = await shownGraph({ paths: true })
871
+ test('walk rows cut every snarl a GBZ walk takes past the window, so a walk comes whole', async () => {
872
+ const { view, cuts } = createEnvironment({ paths: true })
873
+ view.zoomTo(60_000 / WIDTH_PX)
874
+ view.scrollTo(1_000_000 / view.bpPerPx)
875
+ view.showTrack('walks')
876
+ const pane = view.tracks[0]!.displays[0] as LinearGraphDisplayModel
877
+ pane.startRenderingBackend(fakeRenderer())
878
+ await wait(SETTLE_MS)
873
879
  await pane.switchLayout('force')
874
880
  await wait(SETTLE_MS)
875
881
  const before = cuts.length
@@ -884,6 +890,18 @@ test('walk rows cut every snarl a walk takes past the window, so a walk comes wh
884
890
  expect(cuts.at(-1)!.snarls).toBeUndefined()
885
891
  })
886
892
 
893
+ // A walk file cuts the same whatever the snarl option says
894
+ test('walk rows on an rGFA track cut for the step budget, not for snarls', async () => {
895
+ const { pane, cuts } = await shownGraph({ paths: true })
896
+ await pane.switchLayout('force')
897
+ await wait(SETTLE_MS)
898
+ const before = cuts.length
899
+ await pane.switchLayout('walkrows')
900
+ await wait(SETTLE_MS)
901
+ expect(cuts).toHaveLength(before + 1)
902
+ expect(cuts.at(-1)!.snarls).toBeUndefined()
903
+ })
904
+
887
905
  test('a launch in the force layout cuts the window alone', async () => {
888
906
  const { view, cuts } = createEnvironment()
889
907
  view.zoomTo(60_000 / WIDTH_PX)
@@ -1018,9 +1036,11 @@ test('each grammar setter writes the track config, and the pane reads it back',
1018
1036
  display.setLayoutMode('ordered')
1019
1037
  display.setColorScheme('depth')
1020
1038
  display.setNodeWidth('uniform')
1039
+ display.setLinkWidth('walks')
1021
1040
  display.setLayer('bubbles', true)
1022
1041
  display.setFacet('walk')
1023
1042
  display.setWalkRowSamples(['HG1'])
1043
+ display.setWalkRowOrder('shared')
1024
1044
  display.setHover('off')
1025
1045
  const slots = [
1026
1046
  'layoutMode',
@@ -1040,21 +1060,31 @@ test('each grammar setter writes the track config, and the pane reads it back',
1040
1060
  ).toEqual({
1041
1061
  layoutMode: 'ordered',
1042
1062
  color: { field: 'depth' },
1043
- size: 6,
1063
+ size: { value: 6, links: 'walks' },
1044
1064
  layers: { bubbles: true },
1045
1065
  facet: { field: 'walk', domain: [] },
1046
- rows: { kept: ['HG1'] },
1066
+ rows: { kept: ['HG1'], order: 'shared' },
1047
1067
  hover: 'off',
1048
1068
  })
1049
1069
  expect([
1050
1070
  display.chosenLayoutMode,
1051
1071
  display.chosenColorScheme,
1052
1072
  display.nodeWidth,
1073
+ display.linkWidth,
1053
1074
  display.showBubbles,
1054
1075
  display.facetSetting.field,
1055
1076
  display.walkRowSamples,
1056
1077
  display.chosenHover,
1057
- ]).toEqual(['ordered', 'depth', 'uniform', true, 'walk', ['HG1'], 'off'])
1078
+ ]).toEqual([
1079
+ 'ordered',
1080
+ 'depth',
1081
+ 'uniform',
1082
+ 'walks',
1083
+ true,
1084
+ 'walk',
1085
+ ['HG1'],
1086
+ 'off',
1087
+ ])
1058
1088
  })
1059
1089
 
1060
1090
  // Reset track settings, undo and the config editor write the slot without
@@ -39,6 +39,24 @@ test('a drawn graph sits in the chrome with its readiness attributes', () => {
39
39
  expect(screen.getByTestId('graph-canvas')).toBeTruthy()
40
40
  })
41
41
 
42
+ // GraphCanvas shows it, with the retry that rebuilds the backend
43
+ test('a render error alone is left to the canvas', () => {
44
+ render(
45
+ <GraphStatusChrome
46
+ model={model({ renderError: new Error('context lost') })}
47
+ phase="error"
48
+ drawn={false}
49
+ testid="linear-graph-display"
50
+ >
51
+ <canvas />
52
+ </GraphStatusChrome>,
53
+ )
54
+ expect(screen.queryByText(/context lost/)).toBeNull()
55
+ expect(screen.getByTestId('linear-graph-display').dataset.displayPhase).toBe(
56
+ 'error',
57
+ )
58
+ })
59
+
42
60
  test('a refused cut shows its notice over the graph', () => {
43
61
  const error = Object.assign(
44
62
  new Error('Zoom in to about 54Kbp to see the graph'),
@@ -31,7 +31,7 @@ const FORCE_LOAD_ADVICE =
31
31
  // read `model` and `visible`; later hosts read values and callbacks. Each
32
32
  // overlay gets both, so one build runs on either.
33
33
  function overlayProps(model: LinearGraphDisplayModel) {
34
- const error = model.error ?? model.renderError
34
+ const { error } = model
35
35
  const regionTooLarge =
36
36
  error instanceof Error &&
37
37
  (error as { regionTooLarge?: unknown }).regionTooLarge === true
@@ -128,7 +128,9 @@ export default observer(function GraphStatusChrome({
128
128
  {children}
129
129
  </BottomRightCornerContext>
130
130
  <TrackOverlayPortal>
131
- {phase === 'error' ? (
131
+ {/* a backend's render error is the canvas's banner, whose retry
132
+ rebuilds the backend */}
133
+ {phase === 'error' && model.error !== undefined ? (
132
134
  <ErrorBar model={model} visible {...props.error} />
133
135
  ) : null}
134
136
  <Loading
@@ -1,4 +1,5 @@
1
- import { fireEvent, render, screen } from '@testing-library/react'
1
+ import { act, fireEvent, render, screen } from '@testing-library/react'
2
+ import { observable, runInAction } from 'mobx'
2
3
 
3
4
  import SubgraphHaplotypesField from './SubgraphHaplotypesField'
4
5
 
@@ -58,3 +59,42 @@ test('without names the field takes typed ones', () => {
58
59
  fireEvent.keyDown(field, { key: 'Enter' })
59
60
  expect(m.setSubgraphHaplotypes).toHaveBeenCalledWith(['HG002', 'NA19240#1'])
60
61
  })
62
+
63
+ // A model whose set re-renders the field, as the display's does
64
+ function liveModel(haplotypeNames?: string[]) {
65
+ const m = observable({
66
+ sourceAdapter: { type: 'RgfaTabixAdapter', walksUri: 'chr22' },
67
+ cutHaplotypes: ['HG002'],
68
+ haplotypeNames,
69
+ setSubgraphHaplotypes(haplotypes: string[]) {
70
+ m.cutHaplotypes = haplotypes
71
+ },
72
+ recut: vi.fn(),
73
+ })
74
+ return m as unknown as LinearGraphCutModel & { cutHaplotypes: string[] }
75
+ }
76
+
77
+ test('applying from the field keeps its focus', () => {
78
+ const m = liveModel()
79
+ render(<SubgraphHaplotypesField model={m} />)
80
+ const field = screen.getByTestId<HTMLInputElement>('graph-haplotypes-field')
81
+ field.focus()
82
+ fireEvent.change(field, { target: { value: 'HG002, NA19240#1' } })
83
+ fireEvent.keyDown(field, { key: 'Enter' })
84
+ expect(m.cutHaplotypes).toEqual(['HG002', 'NA19240#1'])
85
+ expect(screen.getByTestId('graph-haplotypes-field')).toBe(field)
86
+ expect(document.activeElement).toBe(field)
87
+ })
88
+
89
+ test('a set changed elsewhere resets the draft', () => {
90
+ const m = liveModel()
91
+ render(<SubgraphHaplotypesField model={m} />)
92
+ act(() => {
93
+ runInAction(() => {
94
+ m.cutHaplotypes = ['HG00097#1']
95
+ })
96
+ })
97
+ expect(
98
+ screen.getByTestId<HTMLInputElement>('graph-haplotypes-field').value,
99
+ ).toBe('HG00097#1')
100
+ })
@@ -18,20 +18,39 @@ function parseHaplotypeList(text: string) {
18
18
  return text.split(/[\s,]+/).filter(name => name !== '')
19
19
  }
20
20
 
21
+ const sameList = (a: readonly string[], b: readonly string[]) =>
22
+ a.join('\n') === b.join('\n')
23
+
24
+ // The cut's set, and the last one this field saw: an outside change (a
25
+ // restored session, a launch) resets the draft, while the field's own apply
26
+ // records what it set first, so it keeps its focus and draft
27
+ function useCutHaplotypes(model: LinearGraphCutModel, reset: () => void) {
28
+ const current = model.cutHaplotypes ?? []
29
+ const [seen, setSeen] = useState(current)
30
+ if (!sameList(current, seen)) {
31
+ setSeen(current)
32
+ reset()
33
+ }
34
+ return (haplotypes: string[]) => {
35
+ if (!sameList(haplotypes, current)) {
36
+ setSeen(haplotypes)
37
+ model.setSubgraphHaplotypes(haplotypes)
38
+ model.recut()
39
+ }
40
+ }
41
+ }
42
+
21
43
  const HaplotypeListField = observer(function HaplotypeListField({
22
44
  model,
23
45
  }: {
24
46
  model: LinearGraphCutModel
25
47
  }) {
26
48
  const [draft, setDraft] = useState(model.cutHaplotypes?.join(', ') ?? '')
49
+ const setCut = useCutHaplotypes(model, () => {
50
+ setDraft(model.cutHaplotypes?.join(', ') ?? '')
51
+ })
27
52
  const apply = () => {
28
- const parsed = parseHaplotypeList(draft)
29
- const unchanged =
30
- parsed.join('\n') === (model.cutHaplotypes ?? []).join('\n')
31
- if (!unchanged) {
32
- model.setSubgraphHaplotypes(parsed)
33
- model.recut()
34
- }
53
+ setCut(parseHaplotypeList(draft))
35
54
  }
36
55
  return (
37
56
  <TextField
@@ -72,12 +91,10 @@ const HaplotypePickField = observer(function HaplotypePickField({
72
91
  const draftRef = useRef(draft)
73
92
  const typed = useRef('')
74
93
  const [open, setOpen] = useState(false)
75
- const apply = (haplotypes: string[]) => {
76
- if (haplotypes.join('\n') !== (model.cutHaplotypes ?? []).join('\n')) {
77
- model.setSubgraphHaplotypes(haplotypes)
78
- model.recut()
79
- }
80
- }
94
+ const apply = useCutHaplotypes(model, () => {
95
+ draftRef.current = model.cutHaplotypes ?? []
96
+ setDraft(draftRef.current)
97
+ })
81
98
  return (
82
99
  <Autocomplete
83
100
  multiple
@@ -126,9 +143,7 @@ const HaplotypePickField = observer(function HaplotypePickField({
126
143
  )
127
144
  })
128
145
 
129
- // Only a GBZ cut and a walk-indexed rGFA cut read the set. The inner field is
130
- // keyed on it so an outside change (a restored session, a launch) resets the
131
- // draft rather than fighting it.
146
+ // Only a GBZ cut and a walk-indexed rGFA cut read the set
132
147
  const SubgraphHaplotypesField = observer(function SubgraphHaplotypesField({
133
148
  model,
134
149
  }: {
@@ -136,14 +151,13 @@ const SubgraphHaplotypesField = observer(function SubgraphHaplotypesField({
136
151
  }) {
137
152
  const { classes } = useStyles()
138
153
  const names = model.haplotypeNames
139
- const key = model.cutHaplotypes?.join(',') ?? ''
140
154
  const adapter = model.sourceAdapter
141
155
  return adapter && cutsByHaplotype(adapter) ? (
142
156
  <div className={classes.section}>
143
157
  {names ? (
144
- <HaplotypePickField key={key} model={model} names={names} />
158
+ <HaplotypePickField model={model} names={names} />
145
159
  ) : (
146
- <HaplotypeListField key={key} model={model} />
160
+ <HaplotypeListField model={model} />
147
161
  )}
148
162
  <Typography variant="caption" component="div" color="text.secondary">
149
163
  {names
@@ -44,7 +44,8 @@ export function configSchemaFactory() {
44
44
  * #slot
45
45
  * the node thickness: a px number draws every node that thick;
46
46
  * `{ field: 'depth', value }` thickens a node the more paths carry it,
47
- * around `value` px
47
+ * around `value` px; `links: 'walks'` thickens a link the more walks
48
+ * take it
48
49
  */
49
50
  size: {
50
51
  type: 'frozen',
@@ -54,8 +55,8 @@ export function configSchemaFactory() {
54
55
  /**
55
56
  * #slot
56
57
  * what the drawing adds over its nodes and edges, stated as changes to
57
- * the defaults: `paths`, `bubbles` and `walkStrip` are off, `deletions`,
58
- * `genes` and `referenceStrip` on
58
+ * the defaults: `paths`, `bubbles`, `walkStrip` and `walkRibbons` are
59
+ * off, `deletions`, `genes` and `referenceStrip` on
59
60
  * ```js
60
61
  * { layers: { bubbles: true, genes: false } }
61
62
  * ```
@@ -4,11 +4,7 @@ import { ObservableCreate } from '@jbrowse/core/util/rxjs'
4
4
  import { parseSegmentRow, rgfaCut, segmentSamples } from '@jbrowse/graph-walks'
5
5
 
6
6
  import { locationIsSet } from '../locationName.ts'
7
- import {
8
- PanSNRefNames,
9
- openTabixSlot,
10
- unmappedRegionMessage,
11
- } from '../panSNTabix.ts'
7
+ import { PanSNRefNames, openTabixSlot } from '../panSNTabix.ts'
12
8
  import { getLines } from '../tabixRange.ts'
13
9
  import { WalkIndex } from './walkReader.ts'
14
10
 
@@ -194,10 +190,7 @@ export default class RgfaTabixAdapter extends BaseFeatureDataAdapter<RgfaTabixAd
194
190
  ): Promise<string | WalkCut> {
195
191
  const { hops, signal, tier } = opts
196
192
  const index = this.index(tier)
197
- const tabixRefName = await index.refNames.resolve(region, { signal })
198
- if (tabixRefName === undefined) {
199
- throw new Error(unmappedRegionMessage(region))
200
- }
193
+ const tabixRefName = await index.refNames.resolveForCut(region, { signal })
201
194
  if (index.walks) {
202
195
  return index.walks.subgraph(tabixRefName, region, opts)
203
196
  }
@@ -30,8 +30,8 @@ import type { Instance } from '@jbrowse/mobx-state-tree'
30
30
  * `walksUri` takes the prefix of a walk file in place of `uri`, and resolves
31
31
  * `<walksUri>.walks.bed.gz` and its `.tbi`. `gfa-to-tabix --walks -o <prefix>`
32
32
  * writes one per reference sample, so a track names one as
33
- * `<prefix>.<sample>`. The adapter reads walk format 4, which gfa-to-tabix
34
- * writes from 1.2, through @jbrowse/graph-walks. Such a graph's cut carries
33
+ * `<prefix>.<sample>`. The adapter reads walk format 5, which gfa-to-tabix
34
+ * writes from 1.4, through @jbrowse/graph-walks. Such a graph's cut carries
35
35
  * the haplotypes' walks, and `defaultHaplotypes` names the ones it is cut for
36
36
  * until the user picks others.
37
37
  *
@@ -152,7 +152,7 @@ const RgfaTabixAdapter = ConfigurationSchema(
152
152
  ),
153
153
  /**
154
154
  * #slot
155
- * a `gfa-to-tabix --walks` file in walk format 4: per reference chunk,
155
+ * a `gfa-to-tabix --walks` file in walk format 5: per reference chunk,
156
156
  * the reference's path, the ways haplotypes leave it and each haplotype's
157
157
  * piece. Unset means the graph has no walks.
158
158
  */
@@ -35,27 +35,27 @@ const fixture = (name: string) =>
35
35
  // W HG002 1 chr1 >1>2>7>4>5>6 7 replaces 3
36
36
  // W HG002 2 chr1 >1>2>3>4>8>6 8 replaces 5
37
37
  // W HG00097 1 chr1 >1>2>3>4>5>6
38
- const headerPrefix = fixture('walks4_header')
38
+ const headerPrefix = fixture('walks5_header')
39
39
 
40
- // walks4_header's walks with the reference named as pggb names a P-line,
40
+ // walks5_header's walks with the reference named as pggb names a P-line,
41
41
  // GRCh38#chr1
42
- const twoPartPrefix = fixture('walks4_twopart')
42
+ const twoPartPrefix = fixture('walks5_twopart')
43
43
 
44
44
  // An unchopped graph, node 1 spanning 0-2600 and filed under chunk 0 alone:
45
45
  // W GRCh38 0 chr1 >1>2>4
46
46
  // W HG002 1 chr1 >1>3>4 3 replaces 2
47
- const maxNodePrefix = fixture('walks4_maxnode')
47
+ const maxNodePrefix = fixture('walks5_maxnode')
48
48
 
49
49
  // Twelve 500 bp reference nodes over chr1:0-6000 in one 10 kb chunk:
50
50
  // W GRCh38 0 chr1 >1>2>3>4>5>6>7>8>9>10>11>12
51
51
  // W HG002 1 chr1 >1>2>3>20>21>9>10>11>12 20 and 21, 2 kb each, replace 4-8
52
52
  // W HG002 2 chr1 >1>2>3>4>5>6>7>8>9>10>11>12
53
53
  // W HG003 1 chr1 >1>2>3>22 the contig ends on 22, 300 bp
54
- const rejoinPrefix = fixture('walks4_rejoin')
54
+ const rejoinPrefix = fixture('walks5_rejoin')
55
55
 
56
56
  // The same graph in 1 kb chunks, whose rows name the chunks their path goes on
57
57
  // in: HG002#1 leaves chunk 1000 for 20 and 21 and rejoins at 9, in chunk 4000
58
- const followPrefix = fixture('walks4_follow')
58
+ const followPrefix = fixture('walks5_follow')
59
59
 
60
60
  function makeAdapter(prefix: string, slots = {}) {
61
61
  const local = (path: string) => ({
@@ -0,0 +1,204 @@
1
+ import PluginManager from '@jbrowse/core/PluginManager'
2
+ import { ExpansionTooLargeError, formsCut } from '@jbrowse/graph-walks'
3
+ import { firstValueFrom } from 'rxjs'
4
+ import { toArray } from 'rxjs/operators'
5
+
6
+ import Adapter from './WalkFormsAdapter.ts'
7
+ import configSchema from './configSchema.ts'
8
+ import WalkFormsAdapterF from './index.ts'
9
+
10
+ import type * as GraphWalks from '@jbrowse/graph-walks'
11
+
12
+ vi.mock('@jbrowse/graph-walks', async importOriginal => {
13
+ const actual = await importOriginal<typeof GraphWalks>()
14
+ return { ...actual, formsCut: vi.fn(actual.formsCut) }
15
+ })
16
+
17
+ // Four walks over a 3.6 kb reference of six 600 bp nodes, in 1 kb chunks
18
+ // (RgfaTabixAdapter/walkFiles.test.ts builds on the same file):
19
+ // W GRCh38 0 chr1 >1>2>3>4>5>6
20
+ // W HG002 1 chr1 >1>2>7>4>5>6 7 (550 bp) replaces 3
21
+ // W HG002 2 chr1 >1>2>3>4>8>6 8 (20 bp) replaces 5
22
+ // W HG00097 1 chr1 >1>2>3>4>5>6
23
+ const walks =
24
+ require.resolve('../RgfaTabixAdapter/test_data/walks5_header.GRCh38.walks.bed.gz')
25
+
26
+ const local = (path: string) => ({
27
+ localPath: path,
28
+ locationType: 'LocalPathLocation',
29
+ })
30
+
31
+ // Each test pins its own window, since the cut cache outlives an adapter
32
+ function makeAdapter(slots: Record<string, unknown>) {
33
+ return new Adapter(
34
+ configSchema.create({
35
+ walksLocation: local(walks),
36
+ walksIndex: { location: local(`${walks}.tbi`) },
37
+ ...slots,
38
+ }),
39
+ )
40
+ }
41
+
42
+ const pinned = (start: number) => ({ refName: 'chr1', start, end: 3600 })
43
+
44
+ const region = (start: number, end: number, refName = 'chr1') => ({
45
+ refName,
46
+ assemblyName: 'hg38',
47
+ start,
48
+ end,
49
+ })
50
+
51
+ const featuresOf = (adapter: Adapter, query = region(0, 3600)) =>
52
+ firstValueFrom(adapter.getFeatures(query).pipe(toArray()))
53
+
54
+ const placeholders = (features: Awaited<ReturnType<typeof featuresOf>>) =>
55
+ features
56
+ .filter(f => f.id().startsWith('forms-reference-'))
57
+ .map(f => ({
58
+ start: f.get('start'),
59
+ end: f.get('end'),
60
+ genotypes: f.get('genotypes'),
61
+ }))
62
+
63
+ test('registers the adapter type', () => {
64
+ const pm = new PluginManager()
65
+ WalkFormsAdapterF(pm)
66
+ pm.createPluggableElements()
67
+ expect(pm.getAdapterType('WalkFormsAdapter').name).toBe('WalkFormsAdapter')
68
+ })
69
+
70
+ test('names the reference sample contigs as the assembly does', async () => {
71
+ expect(await makeAdapter({ pinned: pinned(0) }).getRefNames()).toEqual([
72
+ 'chr1',
73
+ ])
74
+ })
75
+
76
+ test('one source per form, labelled by share and change', async () => {
77
+ const adapter = makeAdapter({ pinned: pinned(1) })
78
+ expect(await adapter.getSourcesAndWarnings()).toEqual({
79
+ sources: [
80
+ { name: 'F1', label: '33% · ref', haplotypes: 1, forms: 1 },
81
+ {
82
+ name: 'F2',
83
+ label: '33% · repl 600bp->550bp',
84
+ haplotypes: 1,
85
+ forms: 1,
86
+ },
87
+ { name: 'F3', label: '33% · del 580bp', haplotypes: 1, forms: 1 },
88
+ ],
89
+ warnings: [],
90
+ })
91
+ })
92
+
93
+ test('features are the pinned window changes, with a genotype per form', async () => {
94
+ const features = await featuresOf(makeAdapter({ pinned: pinned(2) }))
95
+ expect(
96
+ features.map(f => ({
97
+ start: f.get('start'),
98
+ end: f.get('end'),
99
+ consequence: f.get('consequence'),
100
+ svType: f.get('svType'),
101
+ type: f.get('type'),
102
+ genotypes: f.get('genotypes'),
103
+ })),
104
+ ).toEqual([
105
+ {
106
+ start: 1200,
107
+ end: 1800,
108
+ consequence: 'loss',
109
+ svType: 'CPX',
110
+ type: 'complex_substitution',
111
+ genotypes: { F1: '0', F2: '1', F3: '0' },
112
+ },
113
+ {
114
+ start: 2400,
115
+ end: 3000,
116
+ consequence: 'loss',
117
+ svType: 'DEL',
118
+ type: 'deletion',
119
+ genotypes: { F1: '0', F2: '0', F3: '1' },
120
+ },
121
+ ])
122
+ })
123
+
124
+ test('a query with no change gives one reference placeholder', async () => {
125
+ const adapter = makeAdapter({ pinned: pinned(3) })
126
+ expect(placeholders(await featuresOf(adapter, region(1900, 2300)))).toEqual([
127
+ { start: 1900, end: 1901, genotypes: { F1: '0', F2: '0', F3: '0' } },
128
+ ])
129
+ expect(
130
+ placeholders(await featuresOf(adapter, region(0, 3600, 'chr2'))),
131
+ ).toHaveLength(1)
132
+ expect(
133
+ placeholders(await featuresOf(adapter, region(2900, 3600))),
134
+ ).toHaveLength(0)
135
+ })
136
+
137
+ test('a floor change regroups the cut without cutting again', async () => {
138
+ vi.mocked(formsCut).mockClear()
139
+ const fine = makeAdapter({ pinned: pinned(4), floorBp: 50 })
140
+ const coarse = makeAdapter({ pinned: pinned(4), floorBp: 1000 })
141
+ expect((await fine.getSourcesAndWarnings()).sources).toHaveLength(3)
142
+ expect((await coarse.getSourcesAndWarnings()).sources).toEqual([
143
+ { name: 'F1', label: '100% · ref', haplotypes: 3, forms: 1 },
144
+ ])
145
+ expect(placeholders(await featuresOf(coarse))).toEqual([
146
+ { start: 0, end: 1, genotypes: { F1: '0' } },
147
+ ])
148
+ expect(formsCut).toHaveBeenCalledTimes(1)
149
+ })
150
+
151
+ test('exclude leaves samples out of every form', async () => {
152
+ const adapter = makeAdapter({ pinned: pinned(5), exclude: ['HG002'] })
153
+ expect((await adapter.getSourcesAndWarnings()).sources).toEqual([
154
+ { name: 'F1', label: '100% · ref', haplotypes: 1, forms: 1 },
155
+ ])
156
+ })
157
+
158
+ test('maxForms gives the smaller forms one empty row', async () => {
159
+ const adapter = makeAdapter({ pinned: pinned(6), maxForms: 1 })
160
+ const { sources, warnings } = await adapter.getSourcesAndWarnings()
161
+ expect(sources.map(s => s.label)).toEqual(['33% · ref', 'other forms · 67%'])
162
+ expect(warnings).toEqual([
163
+ expect.stringMatching(/^Forms of chr1:7-3,600: 2 smaller forms/),
164
+ ])
165
+ expect(placeholders(await featuresOf(adapter))).toEqual([
166
+ { start: 0, end: 1, genotypes: { F1: '0' } },
167
+ ])
168
+ })
169
+
170
+ test('a pinned window past walkByteBudget says so, without a zoom-in notice', async () => {
171
+ const error = await makeAdapter({ pinned: pinned(7), walkByteBudget: 1 })
172
+ .getSourcesAndWarnings()
173
+ .catch((e: unknown) => e)
174
+ expect(error).toBeInstanceOf(Error)
175
+ expect(error).not.toHaveProperty('regionTooLarge')
176
+ expect(`${error}`).toMatch(
177
+ /^Error: The pinned window chr1:8-3,600 is too large to cut for structural forms: .*walkByteBudget, 0\.0 MB\. .*Raise walkByteBudget$/,
178
+ )
179
+ })
180
+
181
+ test('a window whose walks are too many to read in full says so', async () => {
182
+ vi.mocked(formsCut).mockClear()
183
+ vi.mocked(formsCut).mockRejectedValueOnce(
184
+ new ExpansionTooLargeError(24_000_000, 16_000_000, 3592),
185
+ )
186
+ const error = await makeAdapter({ pinned: pinned(9) })
187
+ .getSourcesAndWarnings()
188
+ .catch((e: unknown) => e)
189
+ expect(formsCut).toHaveBeenCalledWith(
190
+ expect.anything(),
191
+ expect.anything(),
192
+ expect.objectContaining({ partial: false }),
193
+ )
194
+ expect(error).not.toHaveProperty('regionTooLarge')
195
+ expect(`${error}`).toBe(
196
+ 'Error: The pinned window chr1:10-3,600 is too large to read every walk in full for structural forms: its walks take 24.0 M steps, over the 16.0 M step budget, and about 2.39Kbp of it fits. Pin a narrower window',
197
+ )
198
+ })
199
+
200
+ test('an adapter with no pinned window says so', async () => {
201
+ await expect(makeAdapter({}).getSourcesAndWarnings()).rejects.toThrow(
202
+ /needs `pinned`/,
203
+ )
204
+ })