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
@@ -16,7 +16,12 @@ import {
16
16
  } from '@jbrowse/core/pluggableElementTypes/models'
17
17
  import { LAUNCH_LABEL } from '@jbrowse/core/ui'
18
18
  import SimpleFeature from '@jbrowse/core/util/simpleFeature'
19
- import { applyPatch, getType, types } from '@jbrowse/mobx-state-tree'
19
+ import {
20
+ applyPatch,
21
+ getSnapshot,
22
+ getType,
23
+ types,
24
+ } from '@jbrowse/mobx-state-tree'
20
25
  import { linearGenomeViewStateModelFactory } from '@jbrowse/plugin-linear-genome-view'
21
26
  import RocketLaunchIcon from '@mui/icons-material/RocketLaunch'
22
27
  import { reaction } from 'mobx'
@@ -868,8 +873,14 @@ test('walk rows fit their own bars in the track, since a walk can be longer than
868
873
  expect(pane.viewportOwner).toBe('fit')
869
874
  })
870
875
 
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 })
876
+ test('walk rows cut every snarl a GBZ walk takes past the window, so a walk comes whole', async () => {
877
+ const { view, cuts } = createEnvironment({ paths: true })
878
+ view.zoomTo(60_000 / WIDTH_PX)
879
+ view.scrollTo(1_000_000 / view.bpPerPx)
880
+ view.showTrack('walks')
881
+ const pane = view.tracks[0]!.displays[0] as LinearGraphDisplayModel
882
+ pane.startRenderingBackend(fakeRenderer())
883
+ await wait(SETTLE_MS)
873
884
  await pane.switchLayout('force')
874
885
  await wait(SETTLE_MS)
875
886
  const before = cuts.length
@@ -884,6 +895,18 @@ test('walk rows cut every snarl a walk takes past the window, so a walk comes wh
884
895
  expect(cuts.at(-1)!.snarls).toBeUndefined()
885
896
  })
886
897
 
898
+ // A walk file cuts the same whatever the snarl option says
899
+ test('walk rows on an rGFA track cut for the step budget, not for snarls', async () => {
900
+ const { pane, cuts } = await shownGraph({ paths: true })
901
+ await pane.switchLayout('force')
902
+ await wait(SETTLE_MS)
903
+ const before = cuts.length
904
+ await pane.switchLayout('walkrows')
905
+ await wait(SETTLE_MS)
906
+ expect(cuts).toHaveLength(before + 1)
907
+ expect(cuts.at(-1)!.snarls).toBeUndefined()
908
+ })
909
+
887
910
  test('a launch in the force layout cuts the window alone', async () => {
888
911
  const { view, cuts } = createEnvironment()
889
912
  view.zoomTo(60_000 / WIDTH_PX)
@@ -967,7 +990,7 @@ test("a launch names the pane's props without its type, and opens in that layout
967
990
  {
968
991
  type: 'LinearGraphDisplay',
969
992
  layoutMode: 'force',
970
- color: 'uniform',
993
+ encoding: { color: { value: 'uniform' } },
971
994
  },
972
995
  )
973
996
  const display = view.tracks[0]!.displays[0] as LinearGraphDisplayModel
@@ -979,6 +1002,51 @@ test("a launch names the pane's props without its type, and opens in that layout
979
1002
  expect(display.hostPlacesX).toBe(false)
980
1003
  })
981
1004
 
1005
+ test.each([
1006
+ ['uniform', 'uniform'],
1007
+ [{ field: 'id', scale: 'categorical' }, 'random'],
1008
+ [{ field: 'id', scheme: 'rainbow' }, 'rainbow'],
1009
+ [{ field: 'depth' }, 'depth'],
1010
+ ])('a flat color %j in a saved config is read as %s', async (color, scheme) => {
1011
+ const { view } = createEnvironment()
1012
+ view.showTrack('graph', {}, { type: 'LinearGraphDisplay', color })
1013
+ const display = view.tracks[0]!.displays[0] as LinearGraphDisplayModel
1014
+ expect(display.chosenColorScheme).toBe(scheme)
1015
+ expect(getSnapshot(display.configuration)).not.toHaveProperty('color')
1016
+ })
1017
+
1018
+ test('a flat color lifts to encoding.color', async () => {
1019
+ const { view } = createEnvironment()
1020
+ view.showTrack(
1021
+ 'graph',
1022
+ {},
1023
+ {
1024
+ type: 'LinearGraphDisplay',
1025
+ color: { field: 'position', domainMin: 5, domainMax: 50 },
1026
+ },
1027
+ )
1028
+ const lifted = view.tracks[0]!.displays[0] as LinearGraphDisplayModel
1029
+ expect(readConfObject(lifted.configuration, 'encoding')).toEqual({
1030
+ color: { field: 'position', scale: { domainMin: 5, domainMax: 50 } },
1031
+ })
1032
+ expect(lifted.statedColorDomain).toEqual({ start: 5, end: 50 })
1033
+ })
1034
+
1035
+ test('an encoding beside a flat color wins', () => {
1036
+ const { view } = createEnvironment()
1037
+ view.showTrack(
1038
+ 'graph',
1039
+ {},
1040
+ {
1041
+ type: 'LinearGraphDisplay',
1042
+ color: { field: 'depth', domainMin: 1 },
1043
+ encoding: { color: { field: 'length' } },
1044
+ },
1045
+ )
1046
+ const display = view.tracks[0]!.displays[0] as LinearGraphDisplayModel
1047
+ expect(display.chosenColorScheme).toBe('node-length')
1048
+ })
1049
+
982
1050
  test.each([
983
1051
  ['force', false],
984
1052
  ['auto', true],
@@ -1000,7 +1068,14 @@ test('a launch that states one choice takes the rest from the config', async ()
1000
1068
  const { view, cuts } = createEnvironment()
1001
1069
  view.zoomTo(60_000 / WIDTH_PX)
1002
1070
  view.scrollTo(1_000_000 / view.bpPerPx)
1003
- view.showTrack('graph', {}, { type: 'LinearGraphDisplay', color: 'uniform' })
1071
+ view.showTrack(
1072
+ 'graph',
1073
+ {},
1074
+ {
1075
+ type: 'LinearGraphDisplay',
1076
+ encoding: { color: { value: 'uniform' } },
1077
+ },
1078
+ )
1004
1079
  const display = view.tracks[0]!.displays[0] as LinearGraphDisplayModel
1005
1080
  display.startRenderingBackend(fakeRenderer())
1006
1081
  await wait(SETTLE_MS)
@@ -1018,13 +1093,15 @@ test('each grammar setter writes the track config, and the pane reads it back',
1018
1093
  display.setLayoutMode('ordered')
1019
1094
  display.setColorScheme('depth')
1020
1095
  display.setNodeWidth('uniform')
1096
+ display.setLinkWidth('walks')
1021
1097
  display.setLayer('bubbles', true)
1022
1098
  display.setFacet('walk')
1023
1099
  display.setWalkRowSamples(['HG1'])
1100
+ display.setWalkRowOrder('shared')
1024
1101
  display.setHover('off')
1025
1102
  const slots = [
1026
1103
  'layoutMode',
1027
- 'color',
1104
+ 'encoding',
1028
1105
  'size',
1029
1106
  'layers',
1030
1107
  'facet',
@@ -1039,22 +1116,32 @@ test('each grammar setter writes the track config, and the pane reads it back',
1039
1116
  ),
1040
1117
  ).toEqual({
1041
1118
  layoutMode: 'ordered',
1042
- color: { field: 'depth' },
1043
- size: 6,
1119
+ encoding: { color: { field: 'depth' } },
1120
+ size: { value: 6, links: 'walks' },
1044
1121
  layers: { bubbles: true },
1045
1122
  facet: { field: 'walk', domain: [] },
1046
- rows: { kept: ['HG1'] },
1123
+ rows: { kept: ['HG1'], order: 'shared' },
1047
1124
  hover: 'off',
1048
1125
  })
1049
1126
  expect([
1050
1127
  display.chosenLayoutMode,
1051
1128
  display.chosenColorScheme,
1052
1129
  display.nodeWidth,
1130
+ display.linkWidth,
1053
1131
  display.showBubbles,
1054
1132
  display.facetSetting.field,
1055
1133
  display.walkRowSamples,
1056
1134
  display.chosenHover,
1057
- ]).toEqual(['ordered', 'depth', 'uniform', true, 'walk', ['HG1'], 'off'])
1135
+ ]).toEqual([
1136
+ 'ordered',
1137
+ 'depth',
1138
+ 'uniform',
1139
+ 'walks',
1140
+ true,
1141
+ 'walk',
1142
+ ['HG1'],
1143
+ 'off',
1144
+ ])
1058
1145
  })
1059
1146
 
1060
1147
  // 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
@@ -4,6 +4,7 @@ import { trackHeightConfigSchemaFields } from '@jbrowse/display-kit/trackHeightC
4
4
  import { types } from '@jbrowse/mobx-state-tree'
5
5
 
6
6
  import { HOVER_HIGHLIGHT_VALUES } from '../GraphGenomeView/hoverHighlight'
7
+ import { retiredGraphColor } from '../GraphGenomeView/retiredNodeColor'
7
8
 
8
9
  /**
9
10
  * #config LinearGraphDisplay
@@ -27,15 +28,17 @@ export function configSchemaFactory() {
27
28
  },
28
29
  /**
29
30
  * #slot
30
- * the node color: `grey` or `uniform`, or the field nodes are colored
31
- * by, `depth`, `length`, `rank`, `position` or `id` (`scale:
32
- * 'categorical'` or `scheme: 'rainbow'`). `domainMin`/`domainMax` span
33
- * the position ramp. `{}` colors an anchored graph by position.
31
+ * the channels the nodes are painted through, as core's `encoding`.
32
+ * `color` is `{ value: 'grey' }` or `{ value: 'uniform' }`, or the
33
+ * `field` nodes are colored by, `depth`, `length`, `rank`, `position` or
34
+ * `id` (`scale: { type: 'ordinal' }` or `scale: { scheme: 'rainbow' }`).
35
+ * `scale.domainMin` and `scale.domainMax` span the position ramp. `{}`
36
+ * colors an anchored graph by position.
34
37
  * ```js
35
- * { color: { field: 'depth' } }
38
+ * { encoding: { color: { field: 'depth' } } }
36
39
  * ```
37
40
  */
38
- color: {
41
+ encoding: {
39
42
  type: 'frozen',
40
43
  defaultValue: {},
41
44
  plot: true,
@@ -44,7 +47,8 @@ export function configSchemaFactory() {
44
47
  * #slot
45
48
  * the node thickness: a px number draws every node that thick;
46
49
  * `{ field: 'depth', value }` thickens a node the more paths carry it,
47
- * around `value` px
50
+ * around `value` px; `links: 'walks'` thickens a link the more walks
51
+ * take it
48
52
  */
49
53
  size: {
50
54
  type: 'frozen',
@@ -54,8 +58,8 @@ export function configSchemaFactory() {
54
58
  /**
55
59
  * #slot
56
60
  * 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
61
+ * the defaults: `paths`, `bubbles`, `walkStrip` and `walkRibbons` are
62
+ * off, `deletions`, `genes` and `referenceStrip` on
59
63
  * ```js
60
64
  * { layers: { bubbles: true, genes: false } }
61
65
  * ```
@@ -101,7 +105,11 @@ export function configSchemaFactory() {
101
105
  height: 'the height of the track the graph is drawn in',
102
106
  }),
103
107
  },
104
- { explicitlyTyped: true, explicitIdentifier: 'displayId' },
108
+ {
109
+ explicitlyTyped: true,
110
+ explicitIdentifier: 'displayId',
111
+ retired: retiredGraphColor,
112
+ },
105
113
  )
106
114
  }
107
115
 
@@ -129,7 +129,7 @@ export function stateModelFactory(configSchema: LinearGraphDisplayConfigModel) {
129
129
  get grammar(): GraphGrammar {
130
130
  return {
131
131
  layoutMode: getConf(self, 'layoutMode'),
132
- color: getConf(self, 'color'),
132
+ encoding: getConf(self, 'encoding'),
133
133
  size: getConf(self, 'size'),
134
134
  layers: getConf(self, 'layers') as GraphLayers,
135
135
  facet: getConf(self, 'facet'),
@@ -590,6 +590,7 @@ export function stateModelFactory(configSchema: LinearGraphDisplayConfigModel) {
590
590
  }
591
591
  const start = Math.floor(seen.start)
592
592
  const end = Math.ceil(seen.end)
593
+ const { encoding, ...grammar } = self.grammar
593
594
  return {
594
595
  ...paneSettings(self),
595
596
  loadedTrackId: trackId,
@@ -601,7 +602,8 @@ export function stateModelFactory(configSchema: LinearGraphDisplayConfigModel) {
601
602
  },
602
603
  subgraphHaplotypes: self.cutHaplotypes,
603
604
  maxRegionBp: Math.max(self.maxRegionBp, end - start),
604
- ...self.grammar,
605
+ ...grammar,
606
+ color: encoding?.color,
605
607
  layoutMode: self.chosenLayoutMode,
606
608
  connectedViewId: self.host?.id,
607
609
  } satisfies GraphViewSpec
@@ -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
  }
@@ -101,12 +101,9 @@ export default class WalkTabixSyntenyAdapter extends ComparativeAdapterBase<Walk
101
101
  * asks for, and the nodes and links they visit
102
102
  */
103
103
  async getSubgraph(region: Region, opts: SubgraphAdapterOptions = {}) {
104
- const refName = await this.refNames.resolve(region, { signal: opts.signal })
105
- if (refName === undefined) {
106
- throw new Error(
107
- `${region.assemblyName} ${region.refName} is not in this graph's index; the graph is cut on ${this.anchor}, mapped to its PanSN prefix in assemblyNameToPanSN`,
108
- )
109
- }
104
+ const refName = await this.refNames.resolveForCut(region, {
105
+ signal: opts.signal,
106
+ })
110
107
  return this.walks.subgraph(refName, region, opts)
111
108
  }
112
109
 
@@ -200,3 +200,56 @@ describe("the drawing view has to be on the highlight's assembly", () => {
200
200
  expect(graphViewHighlights(noAssembly, 'lgv1', ['Sakai'])).toHaveLength(1)
201
201
  })
202
202
  })
203
+
204
+ describe("a hovered node's span on a haplotype's own assembly", () => {
205
+ const onHap = {
206
+ refName: 'CM094351.1',
207
+ start: 1200,
208
+ end: 1500,
209
+ assemblyName: 'HG002.1',
210
+ }
211
+
212
+ // the stack's rows and a view opened beside are not the graph's pair
213
+ test('draws in a view of that assembly, paired or not', () => {
214
+ const views = [
215
+ graphView({
216
+ connectedViewId: 'lgv-ref',
217
+ assemblyHoverHighlights: [onHap],
218
+ }),
219
+ linearView('lgv-ref'),
220
+ linearView('lgv-hap'),
221
+ ]
222
+ expect(graphViewHighlights(views, 'lgv-hap', ['HG002.1'])).toEqual([
223
+ { key: 'graph1-0', region: onHap },
224
+ ])
225
+ })
226
+
227
+ test('draws nowhere else, nor where the view states no assemblies', () => {
228
+ const views = [graphView({ assemblyHoverHighlights: [onHap] })]
229
+ expect(graphViewHighlights(views, 'lgv1', ['hg38'])).toEqual([])
230
+ expect(graphViewHighlights(views, 'lgv1')).toEqual([])
231
+ })
232
+
233
+ test("comes from a graph track's display too", () => {
234
+ const views = [
235
+ {
236
+ ...linearView('lgv-ref'),
237
+ tracks: [
238
+ {
239
+ displays: [
240
+ {
241
+ id: 'display1',
242
+ type: 'LinearGraphDisplay',
243
+ assemblyHoverHighlights: [onHap],
244
+ },
245
+ ],
246
+ },
247
+ ],
248
+ },
249
+ linearView('lgv-hap'),
250
+ ]
251
+ expect(graphViewHighlights(views, 'lgv-hap', ['HG002.1'])).toEqual([
252
+ { key: 'display1-0', region: onHap },
253
+ ])
254
+ })
255
+ })
@@ -124,6 +124,31 @@ export function graphViewHighlights(
124
124
  }
125
125
  }
126
126
  }
127
+ // A hovered node's span on each haplotype's own assembly goes to any
128
+ // view of that assembly, paired or not: the assembly name says where it
129
+ // belongs, and those views are a synteny stack's rows or opened beside
130
+ for (const graph of isRecord(view)
131
+ ? [
132
+ ...trackGraphs(view),
133
+ ...(view.type === 'GraphGenomeView' ? [view] : []),
134
+ ]
135
+ : []) {
136
+ const spans = Array.isArray(graph.assemblyHoverHighlights)
137
+ ? graph.assemblyHoverHighlights
138
+ : []
139
+ spans.forEach((value: unknown, i) => {
140
+ const region = readRegion(value)
141
+ if (
142
+ region?.assemblyName !== undefined &&
143
+ linearAssemblyNames?.includes(region.assemblyName)
144
+ ) {
145
+ highlights.push({
146
+ key: `${typeof graph.id === 'string' ? graph.id : 'graph'}-${i}`,
147
+ region,
148
+ })
149
+ }
150
+ })
151
+ }
127
152
  if (
128
153
  isRecord(view) &&
129
154
  view.type === 'GraphGenomeView' &&