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.
- package/dist/chunks/{GraphGenomeView-NJ5XCREB.js → GraphGenomeView-PSDUS64B.js} +2 -2
- package/dist/chunks/{HighlightColorDialog-UK5VDDV6.js → HighlightColorDialog-2MP3C5AK.js} +2 -2
- package/dist/chunks/{LinearGraphDisplay-TXTKAACH.js → LinearGraphDisplay-FTFJFNKT.js} +2 -2
- package/dist/chunks/{WalkTabixSyntenyAdapter-U53JLYVU.js → WalkTabixSyntenyAdapter-GH6NYQTF.js} +2 -2
- package/dist/chunks/{WalkTabixSyntenyAdapter-U53JLYVU.js.map → WalkTabixSyntenyAdapter-GH6NYQTF.js.map} +1 -1
- package/dist/chunks/chunk-HM57SCYO.js +4 -0
- package/dist/chunks/chunk-HM57SCYO.js.map +7 -0
- package/dist/chunks/{chunk-WEGWTC52.js → chunk-LELA7BG7.js} +2 -2
- package/dist/chunks/{chunk-WEGWTC52.js.map → chunk-LELA7BG7.js.map} +4 -4
- package/dist/chunks/chunk-NP42Y2MV.js +2 -0
- package/dist/chunks/chunk-NP42Y2MV.js.map +7 -0
- package/dist/chunks/chunk-PJ43KEV6.js +2 -0
- package/dist/chunks/chunk-PJ43KEV6.js.map +7 -0
- package/dist/chunks/chunk-RP3L3Z5W.js +3 -0
- package/dist/chunks/chunk-RP3L3Z5W.js.map +7 -0
- package/dist/chunks/{chunk-FITPB5X6.js → chunk-VQUMZEGN.js} +2 -2
- package/dist/chunks/renderSvg-V4OYQTQZ.js +2 -0
- package/dist/chunks/renderSvg-V4OYQTQZ.js.map +7 -0
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +4 -4
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
- package/package.json +2 -2
- package/src/GbzBaseSyntenyAdapter/configSchema.ts +2 -2
- package/src/GetSubgraph.ts +1 -1
- package/src/GraphAddTrackWorkflow/buildTrackConfig.test.ts +2 -2
- package/src/GraphGenomeView/components/GraphCanvas.test.tsx +108 -0
- package/src/GraphGenomeView/components/GraphCanvas.tsx +11 -8
- package/src/GraphGenomeView/components/Legend.tsx +3 -121
- package/src/GraphGenomeView/components/LegendSwatch.tsx +237 -0
- package/src/GraphGenomeView/components/PaneSvg.tsx +3 -190
- package/src/GraphGenomeView/components/WalkStrip.tsx +1 -1
- package/src/GraphGenomeView/model.test.ts +13 -5
- package/src/GraphGenomeView/pane/graphViews.ts +10 -0
- package/src/GraphGenomeView/pane/loadActions.ts +5 -1
- package/src/GraphGenomeView/pane/morph.test.ts +34 -1
- package/src/GraphGenomeView/pane/morph.ts +74 -5
- package/src/GraphGenomeView/pane/renderingActions.ts +1 -1
- package/src/GraphGenomeView/pane/settingActions.ts +10 -43
- package/src/GraphGenomeView/subgraphLoad.test.ts +56 -1
- package/src/GraphGenomeView/viewModel.ts +55 -33
- package/src/GraphGenomeView/walkRowSpans.ts +4 -10
- package/src/GraphTrack/index.ts +2 -2
- package/src/LinearGraphDisplay/LinearGraphDisplay.test.ts +12 -2
- package/src/LinearGraphDisplay/denseWindow.ts +0 -14
- package/src/LinearGraphDisplay/model.ts +2 -1
- package/src/RgfaTabixAdapter/walkFiles.test.ts +1 -0
- package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.ts +1 -1
- package/src/graphTrackDefaults/graphTrackDefaults.test.ts +15 -3
- package/src/graphTrackDefaults/index.ts +10 -5
- package/src/launchFromGraph/launchFromGraph.test.ts +7 -10
- package/src/launchFromGraph/launchFromGraph.ts +26 -2
- package/src/nodeLimitError.ts +12 -0
- package/src/panSNAliases/panSNAliases.test.ts +129 -3
- package/src/panSNAliases/trackAdapterConfig.ts +55 -11
- package/src/version.ts +1 -1
- package/dist/chunks/chunk-2MCOY3YG.js +0 -3
- package/dist/chunks/chunk-2MCOY3YG.js.map +0 -7
- package/dist/chunks/chunk-MURN3JPK.js +0 -2
- package/dist/chunks/chunk-MURN3JPK.js.map +0 -7
- package/dist/chunks/chunk-NWJYLP6Z.js +0 -4
- package/dist/chunks/chunk-NWJYLP6Z.js.map +0 -7
- package/dist/chunks/chunk-WCGVUZWP.js +0 -2
- package/dist/chunks/chunk-WCGVUZWP.js.map +0 -7
- package/dist/chunks/renderSvg-KSQI5DH6.js +0 -2
- package/dist/chunks/renderSvg-KSQI5DH6.js.map +0 -7
- /package/dist/chunks/{GraphGenomeView-NJ5XCREB.js.map → GraphGenomeView-PSDUS64B.js.map} +0 -0
- /package/dist/chunks/{HighlightColorDialog-UK5VDDV6.js.map → HighlightColorDialog-2MP3C5AK.js.map} +0 -0
- /package/dist/chunks/{LinearGraphDisplay-TXTKAACH.js.map → LinearGraphDisplay-FTFJFNKT.js.map} +0 -0
- /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
|
|
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.
|
|
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 './
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
605
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
|
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/)
|