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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jbrowse-plugin-graphgenomeviewer",
3
- "version": "7.4.4",
3
+ "version": "7.6.0",
4
4
  "description": "JBrowse 2 graph genome (pangenome) view: rGFA reference-anchored layouts, the Bandage force-directed engine, and haplotype lanes read from a gbz-base database",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -40,7 +40,7 @@
40
40
  "@emotion/react": "^11.14.0",
41
41
  "@emotion/styled": "^11.14.1",
42
42
  "@eslint/js": "^10.0.1",
43
- "@jbrowse/bandage-core": "^11.9.0",
43
+ "@jbrowse/bandage-core": "^11.14.0",
44
44
  "@jbrowse/cli": "^4.3.0",
45
45
  "@jbrowse/mobx-state-tree": "^6.9.0",
46
46
  "@jbrowse/plugin-linear-genome-view": "5.0.0-beta.11",
@@ -0,0 +1,24 @@
1
+ import { layoutEngine } from '@jbrowse/bandage-core/layoutEngines'
2
+ import PluginManager from '@jbrowse/core/PluginManager'
3
+
4
+ import GraphComputeLayout from './GraphComputeLayout'
5
+
6
+ vi.mock('@jbrowse/bandage-core/layoutEngines', () => ({
7
+ layoutEngine: vi.fn(() => Promise.resolve({ result: {}, duration: 0 })),
8
+ }))
9
+
10
+ // A layout the user moved on from while it waited behind another in the
11
+ // worker's queue
12
+ test('a layout aborted before it starts never reaches the engine', async () => {
13
+ const method = new GraphComputeLayout(new PluginManager([]))
14
+ const gone = new AbortController()
15
+ gone.abort()
16
+ await expect(
17
+ method.execute({
18
+ graph: { name: 'g', nodes: [], edges: [] },
19
+ options: {},
20
+ signal: gone.signal,
21
+ } as never),
22
+ ).rejects.toMatchObject({ name: 'AbortError' })
23
+ expect(layoutEngine).not.toHaveBeenCalled()
24
+ })
@@ -24,8 +24,10 @@ export default class GraphComputeLayout extends RpcMethodType<'GraphComputeLayou
24
24
  name = 'GraphComputeLayout' as const
25
25
 
26
26
  async execute(args: RpcExecuteArgs<'GraphComputeLayout'>) {
27
- const { graph, options, statusCallback } = args
28
-
27
+ const { graph, options, signal, statusCallback } = args
28
+ // The engine takes no signal, so a layout superseded while it queued
29
+ // behind another is dropped here rather than holding up the next cut
30
+ signal?.throwIfAborted()
29
31
  statusCallback?.('Computing layout')
30
32
  return layoutEngine({ graph, options })
31
33
  }
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from 'react'
2
2
 
3
3
  import { BUBBLE_SPREADS } from '@jbrowse/bandage-core/bubbleSpreads'
4
+ import { EDGE_WIDTHS } from '@jbrowse/bandage-core/edgeWidths'
4
5
  import { LAYOUT_ENGINES } from '@jbrowse/bandage-core/layoutEngines'
5
6
  import { LAYOUT_QUALITIES } from '@jbrowse/bandage-core/layoutQualities'
6
7
  import { NODE_WIDTHS } from '@jbrowse/bandage-core/nodeWidths'
@@ -227,6 +228,21 @@ const GraphSettingsDialog = observer(function GraphSettingsDialog(props: {
227
228
  </Caption>
228
229
  </SettingSelect>
229
230
 
231
+ <SettingSelect
232
+ label="Link width"
233
+ value={model.linkWidth}
234
+ options={EDGE_WIDTHS}
235
+ testId="graph-link-width-select"
236
+ onChange={links => {
237
+ model.setLinkWidth(links)
238
+ }}
239
+ >
240
+ <Caption>
241
+ Walks as width, so a common route reads heavy and a private one
242
+ thin.
243
+ </Caption>
244
+ </SettingSelect>
245
+
230
246
  <SettingSwitch
231
247
  label="Draw paths"
232
248
  checked={model.drawPaths}
@@ -5,6 +5,7 @@ import { observer } from 'mobx-react'
5
5
 
6
6
  import ElTree, { useSvgId } from './ElTree'
7
7
  import { PaneOverlayCanvas, PaneSvgLayer } from './PaneLayer'
8
+ import { paintWalkRowBlocks } from '../walkRowBlocks'
8
9
  import { SECTION_HEADER_PX, walkRowTrees } from '../walkRowGroups'
9
10
  import { paintWalkRowSpans, walkRowSpans } from '../walkRowSpans'
10
11
 
@@ -83,6 +84,82 @@ function WalkRowSpansCanvas({ model }: { model: GraphPaneModel }) {
83
84
  )
84
85
  }
85
86
 
87
+ const RIBBON_FORWARD = 'rgba(160, 196, 232, 0.55)'
88
+ const RIBBON_REVERSED = 'rgba(160, 140, 220, 0.55)'
89
+
90
+ // Under the bars, a ribbon from each stretch of a row to where the row below
91
+ // walks the same nodes, lavender where it walks them backwards, as ggbandage
92
+ // draws them: neighbours that share a copy, an inversion or a duplication
93
+ // read off the ribbons' shape
94
+ function WalkRowRibbonsCanvas({ model }: { model: GraphPaneModel }) {
95
+ const paint = useCallback(
96
+ (ctx: CanvasRenderingContext2D) => {
97
+ const links = model.walkRowRibbons
98
+ const bars = model.walkRowBars
99
+ const place = model.walkRowPlacement
100
+ const pitch = model.walkRowPitch
101
+ if (!links || !bars || !place || !pitch) {
102
+ return
103
+ }
104
+ const { scaleX, scaleY, translateX, translateY } = model.screenTransform
105
+ const x = (offset: number) => (bars.origin + offset) * scaleX + translateX
106
+ const half = pitch.barPx / 2
107
+ for (const { row, a0, a1, b0, b1 } of links) {
108
+ const top = place.rowY(row) * scaleY + translateY + half
109
+ const bottom = place.rowY(row + 1) * scaleY + translateY - half
110
+ ctx.fillStyle = b0 > b1 ? RIBBON_REVERSED : RIBBON_FORWARD
111
+ ctx.beginPath()
112
+ ctx.moveTo(x(a0), top)
113
+ ctx.lineTo(x(a1), top)
114
+ ctx.lineTo(x(b1), bottom)
115
+ ctx.lineTo(x(b0), bottom)
116
+ ctx.closePath()
117
+ ctx.fill()
118
+ }
119
+ },
120
+ [model],
121
+ )
122
+ return (
123
+ <PaneOverlayCanvas
124
+ model={model}
125
+ zIndex={2}
126
+ testId="graph-walk-rows-ribbons"
127
+ paint={paint}
128
+ />
129
+ )
130
+ }
131
+
132
+ // Over the bars, the lettered blocks: copy A on two rows, and C where another
133
+ // row has A, read at a glance
134
+ function WalkRowBlocksCanvas({ model }: { model: GraphPaneModel }) {
135
+ const paint = useCallback(
136
+ (ctx: CanvasRenderingContext2D) => {
137
+ const blocks = model.sharedBlocks
138
+ const bars = model.walkRowBars
139
+ const place = model.walkRowPlacement
140
+ const pitch = model.walkRowPitch
141
+ if (!blocks || !bars || !place || !pitch) {
142
+ return
143
+ }
144
+ const { scaleX, scaleY, translateX, translateY } = model.screenTransform
145
+ paintWalkRowBlocks(ctx, blocks, {
146
+ x: offset => (bars.origin + offset) * scaleX + translateX,
147
+ rowCentre: row => place.rowY(row) * scaleY + translateY,
148
+ barPx: pitch.barPx,
149
+ })
150
+ },
151
+ [model],
152
+ )
153
+ return (
154
+ <PaneOverlayCanvas
155
+ model={model}
156
+ zIndex={3}
157
+ testId="graph-walk-rows-blocks"
158
+ paint={paint}
159
+ />
160
+ )
161
+ }
162
+
86
163
  // Its own observer, so moving between rows redraws this rect and not every row
87
164
  const HoveredWalkRow = observer(function HoveredWalkRow({
88
165
  model,
@@ -142,7 +219,13 @@ const WalkRowsOverlay = observer(function WalkRowsOverlay({
142
219
  }
143
220
  return (
144
221
  <>
222
+ {vector || !model.walkRowRibbons ? null : (
223
+ <WalkRowRibbonsCanvas model={model} />
224
+ )}
145
225
  {vector ? null : <WalkRowSpansCanvas model={model} />}
226
+ {vector || !model.sharedBlocks ? null : (
227
+ <WalkRowBlocksCanvas model={model} />
228
+ )}
146
229
  <PaneSvgLayer
147
230
  model={model}
148
231
  zIndex={3}
@@ -136,7 +136,7 @@ test('a hosted pane that pans itself claims presses from the linear view; one th
136
136
  })
137
137
 
138
138
  // A pane mid-drag, whose transform moves as the model's would
139
- function panModel() {
139
+ function panModel(state: Record<string, unknown> = {}) {
140
140
  const model: Record<string, unknown> = fakeModel({
141
141
  isPanning: false,
142
142
  draggingNode: null,
@@ -155,6 +155,7 @@ function panModel() {
155
155
  model.isPanning = false
156
156
  },
157
157
  clearHover: vi.fn(),
158
+ ...state,
158
159
  })
159
160
  return model
160
161
  }
@@ -382,11 +383,12 @@ describe('a press on a bubble label', () => {
382
383
  window.dispatchEvent(e)
383
384
  }
384
385
 
385
- function pressLabel() {
386
- const model = panModel()
387
- const { result } = renderHook(() =>
386
+ function pressLabel(
387
+ model = panModel(),
388
+ { result } = renderHook(() =>
388
389
  usePaneGestures(model as unknown as GraphPaneModel),
389
- )
390
+ ),
391
+ ) {
390
392
  const layer = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
391
393
  layer.setAttribute(PANS_WITH_DRAWING, '')
392
394
  const chip = document.createElementNS('http://www.w3.org/2000/svg', 'g')
@@ -402,6 +404,7 @@ describe('a press on a bubble label', () => {
402
404
  const click = { stopPropagation: vi.fn() }
403
405
  return {
404
406
  model,
407
+ result,
405
408
  click: () => {
406
409
  onClickCapture(click as unknown as React.MouseEvent)
407
410
  return click.stopPropagation.mock.calls.length === 0
@@ -428,6 +431,75 @@ describe('a press on a bubble label', () => {
428
431
  expect(click()).toBe(false)
429
432
  })
430
433
 
434
+ // The pointer leaves a 3px tick at once, so the canvas under it sees the
435
+ // same moves the window listener pans by
436
+ test('pans once, not again through the canvas under the pointer', () => {
437
+ const frames: FrameRequestCallback[] = []
438
+ vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) =>
439
+ frames.push(cb),
440
+ )
441
+ const { model, result } = pressLabel()
442
+ act(() => {
443
+ pointer('pointermove', 110)
444
+ result.current.handlers.onPointerMove(at(110, 20))
445
+ pointer('pointermove', 140)
446
+ result.current.handlers.onPointerMove(at(140, 20))
447
+ frames.splice(0).forEach(f => {
448
+ f(0)
449
+ })
450
+ })
451
+ expect(model.translateX).toBe(40)
452
+ })
453
+
454
+ test('after a drag on the reference strip still pans the graph', () => {
455
+ const frames: FrameRequestCallback[] = []
456
+ vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) =>
457
+ frames.push(cb),
458
+ )
459
+ const horizontalScroll = vi.fn()
460
+ const model = panModel({
461
+ referenceStripZonePx: 30,
462
+ host: { horizontalScroll },
463
+ followsHostView: false,
464
+ })
465
+ const hook = renderHook(() =>
466
+ usePaneGestures(model as unknown as GraphPaneModel),
467
+ )
468
+ const canvas = Object.assign(document.createElement('canvas'), {
469
+ setPointerCapture: vi.fn(),
470
+ getBoundingClientRect: () => ({ left: 0, top: 0 }),
471
+ })
472
+ const press = (x: number) =>
473
+ ({
474
+ clientX: x,
475
+ clientY: 10,
476
+ button: 0,
477
+ pointerId: 1,
478
+ pointerType: 'mouse',
479
+ currentTarget: canvas,
480
+ }) as unknown as React.PointerEvent
481
+ const h = hook.result.current.handlers
482
+ act(() => {
483
+ h.onPointerDown(press(50))
484
+ h.onPointerMove(press(80))
485
+ frames.splice(0).forEach(f => {
486
+ f(0)
487
+ })
488
+ h.onPointerUp(press(80))
489
+ })
490
+ horizontalScroll.mockClear()
491
+ pressLabel(model, hook)
492
+ act(() => {
493
+ pointer('pointermove', 140)
494
+ frames.splice(0).forEach(f => {
495
+ f(0)
496
+ })
497
+ pointer('pointerup', 140)
498
+ })
499
+ expect(horizontalScroll).not.toHaveBeenCalled()
500
+ expect(model.translateX).toBe(40)
501
+ })
502
+
431
503
  test('that stays put is a click on the label', () => {
432
504
  const { model, click } = pressLabel()
433
505
  act(() => {
@@ -439,6 +511,11 @@ describe('a press on a bubble label', () => {
439
511
  })
440
512
  })
441
513
 
514
+ test('a frame that began before the wheel event never zooms backwards', () => {
515
+ expect(easeZoom(0.2, -5)).toBeGreaterThanOrEqual(0)
516
+ expect(easeZoom(-0.2, -5)).toBeLessThanOrEqual(0)
517
+ })
518
+
442
519
  test('a wheel notch eases in over a few frames and lands the whole zoom', () => {
443
520
  const owed = Math.log(1.21)
444
521
  let left = owed
@@ -27,9 +27,11 @@ const ZOOM_ACTIVE_MS = 100
27
27
  const ZOOM_EASE_MS = 24
28
28
  const MAX_ZOOM_BACKLOG = Math.log(16)
29
29
 
30
- // how much of the owed zoom, in log(scale), to apply this frame
30
+ // How much of the owed zoom, in log(scale), to apply this frame. A frame's
31
+ // timestamp is when it began, so a wheel handled late in one leaves elapsedMs
32
+ // negative, which would ease the zoom backwards.
31
33
  export function easeZoom(backlog: number, elapsedMs: number) {
32
- const rest = backlog * Math.exp(-elapsedMs / ZOOM_EASE_MS)
34
+ const rest = backlog * Math.exp(-Math.max(0, elapsedMs) / ZOOM_EASE_MS)
33
35
  return Math.abs(rest) < 1e-3 ? backlog : backlog - rest
34
36
  }
35
37
 
@@ -173,6 +175,9 @@ export function usePaneGestures(model: GraphPaneModel) {
173
175
  const dragCanvasRef = useRef<Element | null>(null)
174
176
  const pinchRef = useRef<ReturnType<typeof pinchOf> | null>(null)
175
177
  const stripPanRef = useRef(false)
178
+ // a press on a bubble label pans through window listeners, so the canvas
179
+ // under the pointer leaves those moves alone
180
+ const labelPanRef = useRef<{ end: () => void; moved: boolean } | null>(null)
176
181
  const verticalPanRef = useRef(false)
177
182
  // A pan and a hover are applied once per frame, not once per mousemove:
178
183
  // mousemove fires in bursts well above the frame rate, and each pan step
@@ -428,7 +433,7 @@ export function usePaneGestures(model: GraphPaneModel) {
428
433
  scheduleFrame()
429
434
  return
430
435
  }
431
- if (isOtherPointer(e)) {
436
+ if (isOtherPointer(e) || labelPanRef.current) {
432
437
  return
433
438
  }
434
439
  const dx = e.clientX - lastMouseRef.current.x
@@ -542,7 +547,6 @@ export function usePaneGestures(model: GraphPaneModel) {
542
547
  // the drawing does, rather than leaving a patch of the graph that will not
543
548
  // drag; one that stays put is a click on the label. The click that ends a
544
549
  // pan is swallowed before the label sees it.
545
- const labelPanRef = useRef<{ end: () => void; moved: boolean } | null>(null)
546
550
  useEffect(
547
551
  () => () => {
548
552
  labelPanRef.current?.end()
@@ -560,6 +564,8 @@ export function usePaneGestures(model: GraphPaneModel) {
560
564
  return
561
565
  }
562
566
  labelPanRef.current?.end()
567
+ stripPanRef.current = false
568
+ verticalPanRef.current = false
563
569
  const id = e.pointerId
564
570
  const start = pointOf(e)
565
571
  let last = start
@@ -9,8 +9,19 @@ import { isRecord } from '../isRecord'
9
9
  // - `referenceStrip`: reference segments at their bp, over a drawing in its
10
10
  // own coordinates inside a linear view
11
11
  // - `walkStrip`: walk rows under a node layout, linked to the drawing
12
+ // - `walkRibbons`: between neighbouring walk rows, a ribbon joining the
13
+ // stretches both walk through the same nodes
14
+ // - `walkBlocks`: on walk rows, a lettered box round each stretch of nodes off
15
+ // the reference's path that two or more rows walk
12
16
  export type GraphLayer =
13
- 'paths' | 'bubbles' | 'deletions' | 'genes' | 'referenceStrip' | 'walkStrip'
17
+ | 'paths'
18
+ | 'bubbles'
19
+ | 'deletions'
20
+ | 'genes'
21
+ | 'referenceStrip'
22
+ | 'walkStrip'
23
+ | 'walkRibbons'
24
+ | 'walkBlocks'
14
25
 
15
26
  const DEFAULTS: Record<GraphLayer, boolean> = {
16
27
  paths: false,
@@ -19,6 +30,8 @@ const DEFAULTS: Record<GraphLayer, boolean> = {
19
30
  genes: true,
20
31
  referenceStrip: true,
21
32
  walkStrip: false,
33
+ walkRibbons: false,
34
+ walkBlocks: false,
22
35
  }
23
36
 
24
37
  // A config or session states only what differs from the defaults, as
@@ -28,6 +28,7 @@ import { walkKey } from '@jbrowse/bandage-core/walkKey'
28
28
 
29
29
  import { CALL_TOLERANCE } from './repeats/walkCalls'
30
30
  import { PANEL_REFERENCE_TUBE } from './tubeMapPanels'
31
+ import { COLORED_BLOCKS, blockFill, blockLetter } from './walkRowBlocks'
31
32
 
32
33
  import type { GraphPaneModel } from './model'
33
34
  import type { KeySwatch } from '@jbrowse/bandage-core/layout/walkRowDraw'
@@ -399,6 +400,36 @@ export function walkRowSwatch(swatch: KeySwatch): LegendSwatch {
399
400
  }
400
401
  }
401
402
 
403
+ // The shared blocks the rows draw: each coloured one by its letter, then one
404
+ // row for the grey rest
405
+ function blockRows(model: GraphPaneModel): LegendRow[] {
406
+ const present = new Set(model.sharedBlocks?.map(b => b.block))
407
+ const swatch = (block: number) => ({
408
+ kind: 'fill' as const,
409
+ stops: flat(blockFill(block)),
410
+ height: WALK_ROW_SWATCH_PX,
411
+ })
412
+ return [
413
+ ...[...present]
414
+ .filter(block => block < COLORED_BLOCKS)
415
+ .sort((a, b) => a - b)
416
+ .map(block => ({
417
+ key: `block-${block}`,
418
+ label: `${blockLetter(block)}: off the reference, walked by several rows`,
419
+ swatch: swatch(block),
420
+ })),
421
+ ...([...present].some(block => block >= COLORED_BLOCKS)
422
+ ? [
423
+ {
424
+ key: 'block-more',
425
+ label: 'more shared blocks',
426
+ swatch: swatch(COLORED_BLOCKS),
427
+ },
428
+ ]
429
+ : []),
430
+ ]
431
+ }
432
+
402
433
  // The walk rows' bar colours, genes and ticks; each tick row appears once
403
434
  // the rows hold one, so a catalogue with no genotypes keeps the plain key
404
435
  function walkRowsRows(model: GraphPaneModel): LegendRow[] {
@@ -421,6 +452,7 @@ function walkRowsRows(model: GraphPaneModel): LegendRow[] {
421
452
  label: entry.label,
422
453
  swatch: entry.note ? undefined : walkRowSwatch(entry.swatch),
423
454
  })),
455
+ ...blockRows(model),
424
456
  ...(calls.some(call => call.spanningReads !== 0)
425
457
  ? [tick('call', CALL_TICK, 'allele length called for this walk')]
426
458
  : []),