jbrowse-plugin-graphgenomeviewer 7.1.2 → 7.1.4

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 (68) hide show
  1. package/dist/chunks/{GraphGenomeView-NJ5XCREB.js → GraphGenomeView-PSDUS64B.js} +2 -2
  2. package/dist/chunks/{HighlightColorDialog-UK5VDDV6.js → HighlightColorDialog-2MP3C5AK.js} +2 -2
  3. package/dist/chunks/{LinearGraphDisplay-TXTKAACH.js → LinearGraphDisplay-FTFJFNKT.js} +2 -2
  4. package/dist/chunks/{WalkTabixSyntenyAdapter-U53JLYVU.js → WalkTabixSyntenyAdapter-GH6NYQTF.js} +2 -2
  5. package/dist/chunks/{WalkTabixSyntenyAdapter-U53JLYVU.js.map → WalkTabixSyntenyAdapter-GH6NYQTF.js.map} +1 -1
  6. package/dist/chunks/chunk-HM57SCYO.js +4 -0
  7. package/dist/chunks/chunk-HM57SCYO.js.map +7 -0
  8. package/dist/chunks/{chunk-WEGWTC52.js → chunk-LELA7BG7.js} +2 -2
  9. package/dist/chunks/{chunk-WEGWTC52.js.map → chunk-LELA7BG7.js.map} +4 -4
  10. package/dist/chunks/chunk-NP42Y2MV.js +2 -0
  11. package/dist/chunks/chunk-NP42Y2MV.js.map +7 -0
  12. package/dist/chunks/chunk-PJ43KEV6.js +2 -0
  13. package/dist/chunks/chunk-PJ43KEV6.js.map +7 -0
  14. package/dist/chunks/chunk-RP3L3Z5W.js +3 -0
  15. package/dist/chunks/chunk-RP3L3Z5W.js.map +7 -0
  16. package/dist/chunks/{chunk-FITPB5X6.js → chunk-VQUMZEGN.js} +2 -2
  17. package/dist/chunks/renderSvg-V4OYQTQZ.js +2 -0
  18. package/dist/chunks/renderSvg-V4OYQTQZ.js.map +7 -0
  19. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +4 -4
  20. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
  21. package/package.json +2 -2
  22. package/src/GbzBaseSyntenyAdapter/configSchema.ts +2 -2
  23. package/src/GetSubgraph.ts +1 -1
  24. package/src/GraphAddTrackWorkflow/buildTrackConfig.test.ts +2 -2
  25. package/src/GraphGenomeView/components/GraphCanvas.test.tsx +108 -0
  26. package/src/GraphGenomeView/components/GraphCanvas.tsx +11 -8
  27. package/src/GraphGenomeView/components/Legend.tsx +3 -121
  28. package/src/GraphGenomeView/components/LegendSwatch.tsx +237 -0
  29. package/src/GraphGenomeView/components/PaneSvg.tsx +3 -190
  30. package/src/GraphGenomeView/components/WalkStrip.tsx +1 -1
  31. package/src/GraphGenomeView/model.test.ts +13 -5
  32. package/src/GraphGenomeView/pane/graphViews.ts +10 -0
  33. package/src/GraphGenomeView/pane/loadActions.ts +5 -1
  34. package/src/GraphGenomeView/pane/morph.test.ts +34 -1
  35. package/src/GraphGenomeView/pane/morph.ts +74 -5
  36. package/src/GraphGenomeView/pane/renderingActions.ts +1 -1
  37. package/src/GraphGenomeView/pane/settingActions.ts +10 -43
  38. package/src/GraphGenomeView/subgraphLoad.test.ts +56 -1
  39. package/src/GraphGenomeView/viewModel.ts +55 -33
  40. package/src/GraphGenomeView/walkRowSpans.ts +4 -10
  41. package/src/GraphTrack/index.ts +2 -2
  42. package/src/LinearGraphDisplay/LinearGraphDisplay.test.ts +12 -2
  43. package/src/LinearGraphDisplay/denseWindow.ts +0 -14
  44. package/src/LinearGraphDisplay/model.ts +2 -1
  45. package/src/RgfaTabixAdapter/walkFiles.test.ts +1 -0
  46. package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.ts +1 -1
  47. package/src/graphTrackDefaults/graphTrackDefaults.test.ts +15 -3
  48. package/src/graphTrackDefaults/index.ts +10 -5
  49. package/src/launchFromGraph/launchFromGraph.test.ts +7 -10
  50. package/src/launchFromGraph/launchFromGraph.ts +26 -2
  51. package/src/nodeLimitError.ts +12 -0
  52. package/src/panSNAliases/panSNAliases.test.ts +129 -3
  53. package/src/panSNAliases/trackAdapterConfig.ts +55 -11
  54. package/src/version.ts +1 -1
  55. package/dist/chunks/chunk-2MCOY3YG.js +0 -3
  56. package/dist/chunks/chunk-2MCOY3YG.js.map +0 -7
  57. package/dist/chunks/chunk-MURN3JPK.js +0 -2
  58. package/dist/chunks/chunk-MURN3JPK.js.map +0 -7
  59. package/dist/chunks/chunk-NWJYLP6Z.js +0 -4
  60. package/dist/chunks/chunk-NWJYLP6Z.js.map +0 -7
  61. package/dist/chunks/chunk-WCGVUZWP.js +0 -2
  62. package/dist/chunks/chunk-WCGVUZWP.js.map +0 -7
  63. package/dist/chunks/renderSvg-KSQI5DH6.js +0 -2
  64. package/dist/chunks/renderSvg-KSQI5DH6.js.map +0 -7
  65. /package/dist/chunks/{GraphGenomeView-NJ5XCREB.js.map → GraphGenomeView-PSDUS64B.js.map} +0 -0
  66. /package/dist/chunks/{HighlightColorDialog-UK5VDDV6.js.map → HighlightColorDialog-2MP3C5AK.js.map} +0 -0
  67. /package/dist/chunks/{LinearGraphDisplay-TXTKAACH.js.map → LinearGraphDisplay-FTFJFNKT.js.map} +0 -0
  68. /package/dist/chunks/{chunk-FITPB5X6.js.map → chunk-VQUMZEGN.js.map} +0 -0
@@ -3,24 +3,23 @@ import { LEGEND_INSET_PX } from '@jbrowse/bandage-core/labelLayout'
3
3
  import { LABEL_CHAR_PX } from '@jbrowse/bandage-core/overlayLabels'
4
4
  import { Canvas2DRenderer } from '@jbrowse/bandage-core/renderer/Canvas2DRenderer'
5
5
  import { svgCanvas } from '@jbrowse/bandage-core/renderer/svgCanvas'
6
- import { rulerInk } from '@jbrowse/bandage-core/tubeMap/axis'
7
6
  import { PaintLayer } from '@jbrowse/core/util/paintLayer'
8
7
 
9
8
  import BubbleHalos from './BubbleHalos'
10
9
  import GenePins from './GenePins'
11
10
  import LabelLayer from './LabelLayer'
11
+ import { SvgSwatch } from './LegendSwatch'
12
12
  import { paintReferenceStrip } from './ReferenceStripOverlay'
13
13
  import { paintTubeMap } from './TubeMapOverlay'
14
14
  import WalkRowsOverlay from './WalkRowsOverlay'
15
15
  import {
16
- SWATCH_PX,
17
16
  facetKeyRows,
18
17
  legendSections,
19
18
  swatchWidth,
20
19
  walkKeyRow,
21
20
  } from '../legendSections'
22
21
 
23
- import type { LegendRow, LegendSwatch, Stop } from '../legendSections'
22
+ import type { LegendRow } from '../legendSections'
24
23
  import type { GraphPaneModel } from '../model'
25
24
  import type { WalkLift } from '@jbrowse/bandage-core/walkHighlight'
26
25
  import type { Ctx2D } from '@jbrowse/core/util/paintLayer'
@@ -79,192 +78,6 @@ function Body({
79
78
  )
80
79
  }
81
80
 
82
- function Gradient({
83
- id,
84
- x,
85
- y,
86
- stops,
87
- width,
88
- height,
89
- square,
90
- }: {
91
- id: string
92
- x: number
93
- y: number
94
- stops: readonly Stop[]
95
- width: number
96
- height: number
97
- square?: boolean
98
- }) {
99
- const rect = (fill: string) => (
100
- <rect
101
- x={x}
102
- y={y}
103
- width={width}
104
- height={height}
105
- rx={square ? 0 : 2}
106
- fill={fill}
107
- />
108
- )
109
- return stops.length > 1 ? (
110
- <>
111
- <defs>
112
- <linearGradient id={id}>
113
- {stops.map((s, i) => (
114
- <stop key={i} offset={s.offset} stopColor={s.color} />
115
- ))}
116
- </linearGradient>
117
- </defs>
118
- {rect(`url(#${id})`)}
119
- </>
120
- ) : (
121
- rect(stops[0]!.color)
122
- )
123
- }
124
-
125
- // A row's swatch with its left edge at x, centred on the row's text 5 px
126
- // above the baseline, as the screen centres it
127
- function SvgSwatch({
128
- swatch,
129
- x,
130
- id,
131
- }: {
132
- swatch: LegendSwatch
133
- x: number
134
- id: string
135
- }) {
136
- const mid = -5
137
- switch (swatch.kind) {
138
- case 'fill': {
139
- const height = swatch.height ?? 8
140
- return (
141
- <Gradient
142
- id={id}
143
- x={x}
144
- y={mid - height / 2}
145
- stops={swatch.stops}
146
- width={swatchWidth(swatch)}
147
- height={height}
148
- square={swatch.square}
149
- />
150
- )
151
- }
152
- case 'bands': {
153
- const band = swatch.bands.length > 1 ? 3 : 4
154
- const top = mid - (band * swatch.bands.length) / 2
155
- return (
156
- <>
157
- {swatch.bands.map((stops, i) => (
158
- <Gradient
159
- key={i}
160
- id={`${id}-${i}`}
161
- x={x}
162
- y={top + i * band}
163
- stops={stops}
164
- width={SWATCH_PX}
165
- height={band}
166
- square
167
- />
168
- ))}
169
- </>
170
- )
171
- }
172
- case 'line':
173
- return (
174
- <rect
175
- x={x}
176
- y={mid - 1.5}
177
- width={SWATCH_PX}
178
- height={3}
179
- rx={2}
180
- fill={swatch.color}
181
- />
182
- )
183
- case 'halo':
184
- return (
185
- <line
186
- x1={x + 5}
187
- x2={x + 13}
188
- y1={mid}
189
- y2={mid}
190
- stroke={swatch.color}
191
- strokeOpacity={0.35}
192
- strokeWidth={8}
193
- strokeLinecap="round"
194
- />
195
- )
196
- case 'outline': {
197
- const inset = swatch.stroke / 2
198
- return (
199
- <rect
200
- x={x + inset}
201
- y={mid - swatch.height / 2 + inset}
202
- width={SWATCH_PX - swatch.stroke}
203
- height={swatch.height - swatch.stroke}
204
- rx={Math.max(0, swatch.radius - inset)}
205
- fill="none"
206
- stroke={swatch.color}
207
- strokeWidth={swatch.stroke}
208
- />
209
- )
210
- }
211
- case 'overhang':
212
- return (
213
- <path
214
- transform={`translate(${x} ${mid - 5})`}
215
- d="M0,5 L8,0 L8,10 Z M18,5 L10,0 L10,10 Z"
216
- fill="#18181c"
217
- />
218
- )
219
- case 'glyph':
220
- return (
221
- <text
222
- x={x + SWATCH_PX / 2}
223
- y={0}
224
- textAnchor="middle"
225
- fontFamily="monospace"
226
- fontSize={12}
227
- >
228
- {swatch.char}
229
- </text>
230
- )
231
- case 'tick': {
232
- const y = mid - swatch.height / 2
233
- return (
234
- <>
235
- {swatch.backing ? (
236
- <rect
237
- x={x}
238
- y={y}
239
- width={SWATCH_PX}
240
- height={swatch.height}
241
- fill={swatch.backing}
242
- />
243
- ) : null}
244
- <rect
245
- x={x + SWATCH_PX / 2 - 1}
246
- y={y}
247
- width={2}
248
- height={swatch.height}
249
- fill={swatch.color}
250
- />
251
- </>
252
- )
253
- }
254
- case 'bracket':
255
- return (
256
- <path
257
- transform={`translate(${x} ${mid - 4})`}
258
- d={`M0.5,6 V1.5 H5.5 V6 M8.5,6 V1.5 H${SWATCH_PX - 0.5} V6`}
259
- fill="none"
260
- stroke={rulerInk()}
261
- />
262
- )
263
- case 'blank':
264
- return null
265
- }
266
- }
267
-
268
81
  interface KeyRow {
269
82
  key: string
270
83
  width: number
@@ -542,7 +355,7 @@ export default function PaneSvg({
542
355
  ) : (
543
356
  <Body
544
357
  model={model}
545
- lift={model.walkLift}
358
+ lift={model.drawnHighlight}
546
359
  drawPaths={model.effectiveDrawPaths}
547
360
  width={width}
548
361
  height={height}
@@ -16,7 +16,7 @@ import { isAlive } from '@jbrowse/mobx-state-tree'
16
16
  import { observer } from 'mobx-react'
17
17
 
18
18
  import ElTree, { useSvgId } from './ElTree'
19
- import { LegendSwatchView } from './Legend'
19
+ import { LegendSwatchView } from './LegendSwatch'
20
20
  import PaneContextMenu from './PaneContextMenu'
21
21
  import { legendRowStyle } from './legendStyles'
22
22
  import { walkRowSwatch } from '../legendSections'
@@ -25,7 +25,12 @@ import { recordingCanvas } from '@jbrowse/bandage-core/renderer/recordingCanvas'
25
25
  import { walkHighlight } from '@jbrowse/bandage-core/walkHighlight'
26
26
  import { readConfObject } from '@jbrowse/core/configuration'
27
27
  import { LAUNCH_LABEL, resolveSubMenu } from '@jbrowse/core/ui'
28
- import { applySnapshot, getSnapshot, types } from '@jbrowse/mobx-state-tree'
28
+ import {
29
+ applyPatch,
30
+ applySnapshot,
31
+ getSnapshot,
32
+ types,
33
+ } from '@jbrowse/mobx-state-tree'
29
34
  import { autorun } from 'mobx'
30
35
 
31
36
  import { facetKeyRows } from './legendSections'
@@ -626,7 +631,7 @@ describe('performance instrumentation', () => {
626
631
  consoleSpy.mockRestore()
627
632
  })
628
633
 
629
- test('a stop before a new graph is laid out draws nothing of the last one', async () => {
634
+ test('a stop before a new graph is laid out draws nothing of the last one, and stops loading', async () => {
630
635
  const { answers, layouts, holdLayouts } = heldCuts()
631
636
  const model = createModel()
632
637
  const first = model.cutSubgraph({ type: 'RgfaTabixAdapter' }, TEST_REGION)
@@ -642,10 +647,13 @@ describe('performance instrumentation', () => {
642
647
  })
643
648
  model.stopLoad()
644
649
  expect(model.layoutResult).toBeUndefined()
650
+ expect(model.graph).toBeUndefined()
645
651
  expect(model.loadCanceled).toBe(true)
652
+ expect(model.showLoading).toBe(false)
646
653
  layouts[0]!()
647
654
  await second
648
655
  expect(model.layoutResult).toBeUndefined()
656
+ expect(model.showLoading).toBe(false)
649
657
  })
650
658
  })
651
659
 
@@ -1616,7 +1624,7 @@ describe('canvas height follows the drawing', () => {
1616
1624
  await model.loadGFA(SIMPLE_GFA, 'square')
1617
1625
  expect(model.canvasHeight).toBe(600)
1618
1626
 
1619
- model.setPaneHeight(420)
1627
+ applyPatch(model, { op: 'replace', path: '/paneHeight', value: 420 })
1620
1628
  expect(model.canvasHeight).toBe(420)
1621
1629
  model.zoomToFit()
1622
1630
  expect(extent(model).h * model.scale).toBeLessThanOrEqual(420 - 80 + 1e-6)
@@ -1682,7 +1690,7 @@ describe('canvas height follows the drawing', () => {
1682
1690
  const model = createModel()
1683
1691
  await model.loadGFA(RGFA_FOUR_RANKS, 'four ranks')
1684
1692
 
1685
- model.setPaneHeight(40)
1693
+ applyPatch(model, { op: 'replace', path: '/paneHeight', value: 40 })
1686
1694
  expect(model.canvasHeight).toBe(160)
1687
1695
  })
1688
1696
 
@@ -1695,7 +1703,7 @@ describe('canvas height follows the drawing', () => {
1695
1703
  await model.loadGFA(RGFA_FOUR_RANKS, 'four ranks')
1696
1704
  const fitted = model.canvasHeight
1697
1705
 
1698
- model.setPaneHeight(900)
1706
+ applyPatch(model, { op: 'replace', path: '/paneHeight', value: 900 })
1699
1707
  expect(model.canvasHeight).toBe(fitted)
1700
1708
  })
1701
1709
  })
@@ -305,6 +305,11 @@ export const withGraphViews = paneBase
305
305
  get pathCount() {
306
306
  return self.graph?.paths?.length ?? 0
307
307
  },
308
+ // No hover is changing the legend, so its size is the one to fit the
309
+ // drawing around
310
+ get legendAtRest() {
311
+ return self.hoveredNode === null && self.hoveredRowWalks.length === 0
312
+ },
308
313
  get hasGraph() {
309
314
  return self.graph !== undefined
310
315
  },
@@ -602,6 +607,11 @@ export const withGraphViews = paneBase
602
607
  })
603
608
  return { walks: [], nodeIds, edgeIndexes, names: new Set() }
604
609
  },
610
+ // What the drawing keeps in colour while the rest greys, on screen and
611
+ // in the SVG export alike
612
+ get drawnHighlight(): WalkLift | undefined {
613
+ return self.walkLift ?? this.openBubbleFocus
614
+ },
605
615
  bubbleIsOpen(bubble: MinigraphBubble) {
606
616
  return self.openBubbles.some(o => sameBubble(o, bubble))
607
617
  },
@@ -687,10 +687,14 @@ export const withLoadActions = withEngineSettings.actions(self => {
687
687
  self.loadCanceled = true
688
688
  }
689
689
  },
690
- // The user's stop, leaving whatever is drawn under it
690
+ // The user's stop, leaving whatever is drawn under it, and dropping a
691
+ // swapped-in graph whose layout never landed
691
692
  stopLoad() {
692
693
  abortLoad()
693
694
  dropLayoutOfReplacedGraph()
695
+ if (!self.layoutResult) {
696
+ resetCut()
697
+ }
694
698
  self.isLoading = false
695
699
  self.statusMessage = ''
696
700
  self.loadCanceled = true
@@ -1,9 +1,16 @@
1
1
  import { abgrAlpha, packAbgr } from '@jbrowse/bandage-core/renderer/colorBits'
2
2
  import { expect, test } from 'vitest'
3
3
 
4
- import { blendInto, fadeEntering, morphStarts, routeStarts } from './morph'
4
+ import {
5
+ blendInto,
6
+ canMorph,
7
+ fadeEntering,
8
+ morphStarts,
9
+ routeStarts,
10
+ } from './morph'
5
11
 
6
12
  import type { RenderBatch } from '@jbrowse/bandage-core/renderer/types'
13
+ import type { LayoutResult } from '@jbrowse/bandage-core/types'
7
14
 
8
15
  const identity = { scaleX: 1, scaleY: 1, translateX: 0, translateY: 0 }
9
16
 
@@ -215,3 +222,29 @@ test('a node a morph brings in fades with every link touching it', () => {
215
222
  expect(alphas(batch.edgeCurves)).toEqual([128, 255])
216
223
  expect(alphas(batch.arrows)).toEqual([128])
217
224
  })
225
+
226
+ const free: LayoutResult = { nodePositions: {} }
227
+
228
+ test('a change between two free-placed layouts morphs', () => {
229
+ expect(canMorph(free, free, 100, false)).toBe(true)
230
+ })
231
+
232
+ test('a layout placed by rule on either side snaps', () => {
233
+ const ruled: LayoutResult[] = [
234
+ { ...free, referenceAxis: true },
235
+ { ...free, pixelRows: true },
236
+ ]
237
+ for (const r of ruled) {
238
+ expect(canMorph(r, free, 100, false)).toBe(false)
239
+ expect(canMorph(free, r, 100, false)).toBe(false)
240
+ }
241
+ expect(canMorph(free, { ...free, tubeMap: {} as never }, 100, false)).toBe(
242
+ false,
243
+ )
244
+ expect(canMorph(free, free, 100, true)).toBe(false)
245
+ })
246
+
247
+ test('a graph past the frame budget snaps', () => {
248
+ expect(canMorph(free, free, 3000, false)).toBe(true)
249
+ expect(canMorph(free, free, 3001, false)).toBe(false)
250
+ })
@@ -3,7 +3,11 @@ import { strandSides } from '@jbrowse/bandage-core/util/geometry'
3
3
 
4
4
  import type { ScreenTransform } from './keepInView'
5
5
  import type { RenderBatch, Run } from '@jbrowse/bandage-core/renderer/types'
6
- import type { Graph, NodeSegment } from '@jbrowse/bandage-core/types'
6
+ import type {
7
+ Graph,
8
+ LayoutResult,
9
+ NodeSegment,
10
+ } from '@jbrowse/bandage-core/types'
7
11
  import type { EdgeSides } from '@jbrowse/bandage-core/util/geometry'
8
12
 
9
13
  type Positions = Record<string, NodeSegment[]>
@@ -17,11 +21,11 @@ interface Attachment {
17
21
 
18
22
  // Morphs above this many nodes snap instead: each frame rebuilds the
19
23
  // geometry, which past it no longer fits a frame (GRAPH_SCALE_AND_LOD.md)
20
- export const MORPH_MAX_NODES = 3000
21
- export const MORPH_MS = 350
24
+ const MORPH_MAX_NODES = 3000
25
+ const MORPH_MS = 350
22
26
  // the most of the morph one frame may take, so a busy main thread delays
23
27
  // the motion rather than skipping it
24
- export const MORPH_MAX_STEP_MS = 34
28
+ const MORPH_MAX_STEP_MS = 34
25
29
 
26
30
  function arcPrefix(line: NodeSegment[]) {
27
31
  const arc = [0]
@@ -275,10 +279,75 @@ export function fadeEntering(
275
279
  })
276
280
  }
277
281
 
278
- export function easeInOut(t: number) {
282
+ function easeInOut(t: number) {
279
283
  return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2
280
284
  }
281
285
 
286
+ // A layout change slides only between two free-placed drawings: a reference
287
+ // axis, walk rows, a tube map or a facet grid places nodes by rule, and past
288
+ // MORPH_MAX_NODES a frame's geometry rebuild no longer fits in a frame
289
+ export function canMorph(
290
+ previous: LayoutResult,
291
+ next: LayoutResult,
292
+ nodeCount: number,
293
+ faceted: boolean,
294
+ ) {
295
+ return !(
296
+ previous.referenceAxis ||
297
+ next.referenceAxis ||
298
+ previous.pixelRows ||
299
+ next.pixelRows ||
300
+ next.tubeMap ||
301
+ faceted ||
302
+ nodeCount > MORPH_MAX_NODES
303
+ )
304
+ }
305
+
306
+ export function motionAllowed() {
307
+ return (
308
+ typeof requestAnimationFrame !== 'undefined' &&
309
+ !(
310
+ 'matchMedia' in window &&
311
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches
312
+ )
313
+ )
314
+ }
315
+
316
+ // Calls `blend` with the eased progress once a frame for MORPH_MS while
317
+ // `isLive` holds, then `onEnd`. `finish` jumps to the last frame.
318
+ export function runMorph(
319
+ blend: (t: number) => void,
320
+ isLive: () => boolean,
321
+ onEnd: () => void,
322
+ ) {
323
+ const run = {
324
+ frame: 0,
325
+ finish: () => {
326
+ blend(1)
327
+ },
328
+ }
329
+ let elapsed = 0
330
+ let last = performance.now()
331
+ const step = () => {
332
+ if (!isLive()) {
333
+ onEnd()
334
+ return
335
+ }
336
+ const now = performance.now()
337
+ elapsed += Math.min(now - last, MORPH_MAX_STEP_MS)
338
+ last = now
339
+ const t = Math.min(1, elapsed / MORPH_MS)
340
+ blend(easeInOut(t))
341
+ if (t < 1) {
342
+ run.frame = requestAnimationFrame(step)
343
+ } else {
344
+ onEnd()
345
+ }
346
+ }
347
+ run.frame = requestAnimationFrame(step)
348
+ return run
349
+ }
350
+
282
351
  // Writes the blend of `starts` and `ends` at `t` into `into` IN PLACE, the
283
352
  // way a node drag moves positions (settingActions' moveNode)
284
353
  export function blendInto(
@@ -252,7 +252,7 @@ export const withRenderingActions = withLoadActions
252
252
  }
253
253
  const geometryStart = performance.now()
254
254
  const built = self.buildDrawing(
255
- self.walkLift ?? self.openBubbleFocus,
255
+ self.drawnHighlight,
256
256
  self.effectiveDrawPaths,
257
257
  )
258
258
  if (built) {
@@ -7,13 +7,12 @@ import { isAlive } from '@jbrowse/mobx-state-tree'
7
7
 
8
8
  import { keepInView, onScreen } from './keepInView'
9
9
  import {
10
- MORPH_MAX_NODES,
11
- MORPH_MAX_STEP_MS,
12
- MORPH_MS,
13
10
  blendInto,
14
- easeInOut,
11
+ canMorph,
15
12
  morphStarts,
13
+ motionAllowed,
16
14
  routeStarts,
15
+ runMorph,
17
16
  } from './morph'
18
17
  import { VIEWPORT_DEBOUNCE_MS, forceLayouts } from './paneBase'
19
18
  import { withTubeMapViews } from './tubeMapViews'
@@ -228,13 +227,6 @@ export const withSettingActions = withTubeMapViews
228
227
  l.walk === walk ? { ...l, color: { ...l.color, ...color } } : l,
229
228
  )
230
229
  },
231
-
232
- // Undefined restores the built-in ceiling. Nothing recomputes: the pane
233
- // reads canvasHeight and the drawing is placed by zoomToFit, which the
234
- // caller runs if it wants the drawing refitted into the new pane.
235
- setPaneHeight(px: number | undefined) {
236
- self.paneHeight = px
237
- },
238
230
  setWalkRowSamples(samples: string[] | undefined) {
239
231
  self.writeGrammar({ rows: samples ? { kept: samples } : {} })
240
232
  },
@@ -601,16 +593,8 @@ export const withSettingActions = withTubeMapViews
601
593
  !shown ||
602
594
  !next ||
603
595
  !graph ||
604
- previous.referenceAxis ||
605
- next.referenceAxis ||
606
- previous.pixelRows ||
607
- next.pixelRows ||
608
- next.tubeMap ||
609
- self.facetGrid ||
610
- graph.nodes.length > MORPH_MAX_NODES ||
611
- typeof requestAnimationFrame === 'undefined' ||
612
- ('matchMedia' in window &&
613
- window.matchMedia('(prefers-reduced-motion: reduce)').matches)
596
+ !canMorph(previous, next, graph.nodes.length, !!self.facetGrid) ||
597
+ !motionAllowed()
614
598
  ) {
615
599
  return
616
600
  }
@@ -646,30 +630,13 @@ export const withSettingActions = withTubeMapViews
646
630
  }
647
631
  }
648
632
  blend(0)
649
- let elapsed = 0
650
- let last = performance.now()
651
- const step = () => {
652
- if (!isAlive(self) || self.layoutResult !== next) {
653
- morph = undefined
654
- return
655
- }
656
- const now = performance.now()
657
- elapsed += Math.min(now - last, MORPH_MAX_STEP_MS)
658
- last = now
659
- const t = Math.min(1, elapsed / MORPH_MS)
660
- blend(easeInOut(t))
661
- if (t < 1) {
662
- morph!.frame = requestAnimationFrame(step)
663
- } else {
633
+ morph = runMorph(
634
+ blend,
635
+ () => isAlive(self) && self.layoutResult === next,
636
+ () => {
664
637
  morph = undefined
665
- }
666
- }
667
- morph = {
668
- frame: requestAnimationFrame(step),
669
- finish: () => {
670
- blend(1)
671
638
  },
672
- }
639
+ )
673
640
  },
674
641
  finishMorph,
675
642
  // Moves the position objects IN PLACE, which is the one thing
@@ -1,14 +1,24 @@
1
1
  import stateModelFactory from './viewModel'
2
+ import { hoverInRegion } from '../hoverSync/lgvHover'
2
3
 
3
4
  import type { readConfObject } from '@jbrowse/core/configuration'
4
5
 
6
+ interface MockAssembly {
7
+ name: string
8
+ aliases?: string[]
9
+ initialized?: boolean
10
+ getCanonicalRefName2?: (refName: string) => string
11
+ }
12
+
5
13
  const mockRpcCall = vi.fn()
6
14
  const mockSession = {
7
15
  tracks: [] as Record<string, unknown>[],
8
16
  rpcManager: { call: mockRpcCall },
9
17
  assemblyManager: {
10
18
  has: (_name: string) => false,
11
- get: (_name: string) => undefined as { name: string } | undefined,
19
+ get: (_name: string) => undefined as MockAssembly | undefined,
20
+ waitForAssembly: undefined as
21
+ ((name: string) => Promise<MockAssembly | undefined>) | undefined,
12
22
  assemblyList: [] as { name: string; aliases: string[] }[],
13
23
  },
14
24
  }
@@ -106,6 +116,7 @@ beforeEach(() => {
106
116
  mockSession.tracks = [TRACK]
107
117
  mockSession.assemblyManager.has = () => false
108
118
  mockSession.assemblyManager.get = () => undefined
119
+ mockSession.assemblyManager.waitForAssembly = undefined
109
120
  mockSession.assemblyManager.assemblyList = []
110
121
  })
111
122
 
@@ -135,6 +146,50 @@ test("a track naming its reference by an alias of the region's assembly cuts it"
135
146
  expect(model.nodeCount).toBe(3)
136
147
  })
137
148
 
149
+ describe('a region named by aliases', () => {
150
+ const ALIASED = { ...ON_HG38, assemblyName: 'GRCh38', refName: '6' }
151
+
152
+ function sessionKnowsHg38(initialized = true) {
153
+ const hg38: MockAssembly = {
154
+ name: 'hg38',
155
+ aliases: ['GRCh38'],
156
+ initialized,
157
+ getCanonicalRefName2: refName =>
158
+ hg38.initialized && refName === '6' ? 'chr6' : refName,
159
+ }
160
+ const known = (name: string) => name === 'hg38' || name === 'GRCh38'
161
+ mockSession.assemblyManager.has = known
162
+ mockSession.assemblyManager.get = name => (known(name) ? hg38 : undefined)
163
+ return hg38
164
+ }
165
+
166
+ function cutRegion() {
167
+ return (mockRpcCall.mock.calls[0]![2] as { region: unknown }).region
168
+ }
169
+
170
+ test('is cut and kept by canonical names, so hover sync and pairing match', async () => {
171
+ sessionKnowsHg38()
172
+ const model = await cut(ALIASED)
173
+ expect(model.error).toBeUndefined()
174
+ expect(cutRegion()).toEqual(ON_HG38)
175
+ expect(model.graphRegion).toEqual(ON_HG38)
176
+ const hover = { refName: 'chr6', assemblyName: 'hg38', coord: 32_000_500 }
177
+ expect(hoverInRegion(hover, model.graphRegion!)).toBe(true)
178
+ expect(model.nodeLaunchTargets('s1+').reference?.assembly).toBe('hg38')
179
+ })
180
+
181
+ test('waits for its assembly to load before resolving the refName', async () => {
182
+ const hg38 = sessionKnowsHg38(false)
183
+ mockSession.assemblyManager.waitForAssembly = async () => {
184
+ hg38.initialized = true
185
+ return hg38
186
+ }
187
+ const model = await cut(ALIASED)
188
+ expect(cutRegion()).toEqual(ON_HG38)
189
+ expect(model.graphRegion).toEqual(ON_HG38)
190
+ })
191
+ })
192
+
138
193
  test('a cut that finds no segments is an error, not an empty graph', async () => {
139
194
  const model = await cut(ON_HG38, 'H\tVN:Z:1.0')
140
195
  expect(String(model.error)).toMatch(/No graph segments/)