jbrowse-plugin-graphgenomeviewer 4.0.19 → 4.0.21
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/README.md +2 -1
- package/dist/chunks/{GbzBaseSyntenyAdapter-KKSNCZXY.js → GbzBaseSyntenyAdapter-7ZKFAEJG.js} +2 -2
- package/dist/chunks/{GbzBaseSyntenyAdapter-KKSNCZXY.js.map → GbzBaseSyntenyAdapter-7ZKFAEJG.js.map} +2 -2
- package/dist/chunks/{GraphGenomeView-37F7MLFQ.js → GraphGenomeView-CLUMXH4K.js} +2 -2
- package/dist/chunks/{GraphTrackSettingsDialog-2QV6TPX7.js → GraphTrackSettingsDialog-QF5OMTCH.js} +2 -2
- package/dist/chunks/{LinearGraphDisplay-CJHA56RB.js → LinearGraphDisplay-4CMVCEM5.js} +2 -2
- package/dist/chunks/chunk-3FGJLRG2.js +2 -0
- package/dist/chunks/chunk-3FGJLRG2.js.map +7 -0
- package/dist/chunks/chunk-3SZJ4AFI.js +3 -0
- package/dist/chunks/chunk-3SZJ4AFI.js.map +7 -0
- package/dist/chunks/{chunk-5E7TVPCI.js → chunk-CLRXQ6BM.js} +2 -2
- package/dist/chunks/chunk-VIFXYTIS.js +2 -0
- package/dist/chunks/chunk-VIFXYTIS.js.map +7 -0
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +2 -2
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +3 -3
- package/package.json +2 -2
- package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.ts +1 -1
- package/src/GetSubgraph.ts +3 -0
- package/src/GraphGenomeView/components/BubbleHalos.tsx +3 -4
- package/src/GraphGenomeView/components/GraphCanvas.tsx +51 -15
- package/src/GraphGenomeView/components/LabelLayer.tsx +5 -6
- package/src/GraphGenomeView/components/WalkSelect.tsx +26 -13
- package/src/GraphGenomeView/model.test.ts +48 -0
- package/src/GraphGenomeView/model.ts +75 -21
- package/src/GraphGenomeView/viewModel.ts +15 -0
- package/src/LinearGraphDisplay/LinearGraphDisplay.test.ts +23 -2
- package/src/LinearGraphDisplay/model.ts +7 -2
- package/src/version.ts +1 -1
- package/dist/chunks/chunk-BI3M6FZD.js +0 -2
- package/dist/chunks/chunk-BI3M6FZD.js.map +0 -7
- package/dist/chunks/chunk-IUIIYSGF.js +0 -2
- package/dist/chunks/chunk-IUIIYSGF.js.map +0 -7
- package/dist/chunks/chunk-MNOQXDMY.js +0 -3
- package/dist/chunks/chunk-MNOQXDMY.js.map +0 -7
- /package/dist/chunks/{GraphGenomeView-37F7MLFQ.js.map → GraphGenomeView-CLUMXH4K.js.map} +0 -0
- /package/dist/chunks/{GraphTrackSettingsDialog-2QV6TPX7.js.map → GraphTrackSettingsDialog-QF5OMTCH.js.map} +0 -0
- /package/dist/chunks/{LinearGraphDisplay-CJHA56RB.js.map → LinearGraphDisplay-4CMVCEM5.js.map} +0 -0
- /package/dist/chunks/{chunk-5E7TVPCI.js.map → chunk-CLRXQ6BM.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jbrowse-plugin-graphgenomeviewer",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.21",
|
|
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": {
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"typescript-eslint": "^8.70.1",
|
|
77
77
|
"typescript7": "npm:typescript@^7.0.2",
|
|
78
78
|
"vitest": "^5.0.2",
|
|
79
|
-
"@jbrowse/bandage-core": "0.1.
|
|
79
|
+
"@jbrowse/bandage-core": "0.1.16"
|
|
80
80
|
},
|
|
81
81
|
"peerDependencies": {
|
|
82
82
|
"react": ">=18.0.0",
|
|
@@ -381,7 +381,7 @@ export default class GbzBaseSyntenyAdapter extends ComparativeAdapterBase<GbzBas
|
|
|
381
381
|
const keep = this.keepPredicate(opts.haplotypes)
|
|
382
382
|
return cutWindowGFA(db, query, start, end, {
|
|
383
383
|
context: this.getConf('context'),
|
|
384
|
-
snarls: this.getConf('subgraphSnarls'),
|
|
384
|
+
snarls: opts.snarls ?? this.getConf('subgraphSnarls'),
|
|
385
385
|
limit: this.getConf('nodeLimit'),
|
|
386
386
|
signal: opts.signal,
|
|
387
387
|
...(keep === undefined ? {} : { keep }),
|
package/src/GetSubgraph.ts
CHANGED
|
@@ -20,6 +20,9 @@ export interface SubgraphCutOptions {
|
|
|
20
20
|
hops?: number
|
|
21
21
|
haplotypes?: string[]
|
|
22
22
|
tier?: SubgraphTier
|
|
23
|
+
// which snarls a GBZ cut follows past the window, over the track's
|
|
24
|
+
// `subgraphSnarls`; 'overlapping' when the layout measures whole walks
|
|
25
|
+
snarls?: 'none' | 'contained' | 'overlapping'
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
// What an adapter's cut is handed: the payload, plus the call's own signal so
|
|
@@ -28,7 +28,7 @@ const BubbleHalos = observer(function BubbleHalos({
|
|
|
28
28
|
}: {
|
|
29
29
|
model: GraphPaneModel
|
|
30
30
|
}) {
|
|
31
|
-
const { bubbleHalos,
|
|
31
|
+
const { bubbleHalos, walkLift, hoveredBubble } = model
|
|
32
32
|
if (bubbleHalos.length === 0) {
|
|
33
33
|
return null
|
|
34
34
|
}
|
|
@@ -41,10 +41,9 @@ const BubbleHalos = observer(function BubbleHalos({
|
|
|
41
41
|
canvasHeight,
|
|
42
42
|
} = model
|
|
43
43
|
const halo = model.contigThickness * HALO_FACTOR
|
|
44
|
-
//
|
|
44
|
+
// lifted walks dim the bubbles none of them enters
|
|
45
45
|
const dimmedBubble = (h: BubbleHalo) =>
|
|
46
|
-
|
|
47
|
-
!h.nodeIds.some(id => walkHighlight.nodeIds.has(id))
|
|
46
|
+
walkLift !== undefined && !h.nodeIds.some(id => walkLift.nodeIds.has(id))
|
|
48
47
|
|
|
49
48
|
return (
|
|
50
49
|
<svg
|
|
@@ -7,6 +7,11 @@ import {
|
|
|
7
7
|
findHoveredNode,
|
|
8
8
|
} from '@jbrowse/bandage-core/util/hitDetection'
|
|
9
9
|
import { wheelZoomFactor } from '@jbrowse/bandage-core/util/wheelZoom'
|
|
10
|
+
import {
|
|
11
|
+
WALK_FIELDS,
|
|
12
|
+
encodingSwatchCss,
|
|
13
|
+
fieldLegend,
|
|
14
|
+
} from '@jbrowse/bandage-core/walkEncoding'
|
|
10
15
|
import { ErrorBanner, LoadingOverlay, Menu } from '@jbrowse/core/ui'
|
|
11
16
|
import { isAlive } from '@jbrowse/mobx-state-tree'
|
|
12
17
|
import { useRenderingBackend } from '@jbrowse/render-core/useRenderingBackend'
|
|
@@ -176,30 +181,61 @@ const PathLegend = observer(function PathLegend({
|
|
|
176
181
|
) : null
|
|
177
182
|
})
|
|
178
183
|
|
|
179
|
-
|
|
180
|
-
|
|
184
|
+
const walkSwatchStyle = { width: 26, height: 8, borderRadius: 2, flex: 'none' }
|
|
185
|
+
|
|
186
|
+
// What each lifted walk carries through the window, against the reference
|
|
187
|
+
// walk where the graph has one, beside the scale its lane is coloured by, and
|
|
188
|
+
// what each field in use means
|
|
189
|
+
function fieldName(field: string) {
|
|
190
|
+
return WALK_FIELDS.find(f => f.value === field)!.label.toLowerCase()
|
|
191
|
+
}
|
|
192
|
+
|
|
181
193
|
const WalkReadout = observer(function WalkReadout({
|
|
182
194
|
model,
|
|
183
195
|
}: {
|
|
184
196
|
model: GraphPaneModel
|
|
185
197
|
}) {
|
|
186
|
-
const
|
|
187
|
-
if (!
|
|
198
|
+
const lift = model.walkLift
|
|
199
|
+
if (!lift) {
|
|
188
200
|
return null
|
|
189
201
|
}
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
h.referenceBp === undefined
|
|
193
|
-
? ''
|
|
194
|
-
: h.bp === h.referenceBp
|
|
195
|
-
? ', the reference length'
|
|
196
|
-
: `, ${h.bp > h.referenceBp ? '+' : '−'}${Math.abs(h.bp - h.referenceBp).toLocaleString()} bp against the reference`
|
|
202
|
+
const labelOf = (name: string) =>
|
|
203
|
+
model.walkChoices.find(c => c.name === name)?.label ?? name
|
|
197
204
|
return (
|
|
198
205
|
<div style={legendBoxStyle} data-testid="graph-walk-readout">
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
206
|
+
{lift.walks.map(w => {
|
|
207
|
+
const delta =
|
|
208
|
+
w.referenceBp === undefined
|
|
209
|
+
? ''
|
|
210
|
+
: w.bp === w.referenceBp
|
|
211
|
+
? ', the reference length'
|
|
212
|
+
: `, ${w.bp > w.referenceBp ? '+' : '−'}${Math.abs(w.bp - w.referenceBp).toLocaleString()} bp against the reference`
|
|
213
|
+
return (
|
|
214
|
+
<div key={w.name} style={pathLegendRowStyle}>
|
|
215
|
+
<div
|
|
216
|
+
style={{
|
|
217
|
+
...walkSwatchStyle,
|
|
218
|
+
background: encodingSwatchCss(w.encoding),
|
|
219
|
+
}}
|
|
220
|
+
/>
|
|
221
|
+
<span>
|
|
222
|
+
<strong>{labelOf(w.name)}</strong>: {w.bp.toLocaleString()} bp
|
|
223
|
+
{delta} · {fieldName(w.encoding.field)}
|
|
224
|
+
</span>
|
|
225
|
+
</div>
|
|
226
|
+
)
|
|
227
|
+
})}
|
|
228
|
+
{WALK_FIELDS.filter(f =>
|
|
229
|
+
lift.walks.some(w => w.encoding.field === f.value),
|
|
230
|
+
).map(f => (
|
|
231
|
+
<div key={f.value}>
|
|
232
|
+
{fieldName(f.value)}: {fieldLegend(f.value)}
|
|
233
|
+
</div>
|
|
234
|
+
))}
|
|
235
|
+
<div>
|
|
236
|
+
paler nodes: on none of{' '}
|
|
237
|
+
{lift.walks.length > 1 ? 'these walks' : 'this walk'}
|
|
238
|
+
</div>
|
|
203
239
|
</div>
|
|
204
240
|
)
|
|
205
241
|
})
|
|
@@ -34,14 +34,13 @@ const LabelLayer = observer(function LabelLayer({
|
|
|
34
34
|
if (bubbles.length + genes.length + routes.length === 0) {
|
|
35
35
|
return null
|
|
36
36
|
}
|
|
37
|
-
const {
|
|
38
|
-
//
|
|
39
|
-
//
|
|
37
|
+
const { walkLift, scaleY, translateY, contigThickness } = model
|
|
38
|
+
// lifted walks dim the bubbles none of them enters and the routes none of
|
|
39
|
+
// them takes
|
|
40
40
|
const dimmedBubble = (h: BubbleHalo) =>
|
|
41
|
-
|
|
42
|
-
!h.nodeIds.some(id => walkHighlight.nodeIds.has(id))
|
|
41
|
+
walkLift !== undefined && !h.nodeIds.some(id => walkLift.nodeIds.has(id))
|
|
43
42
|
const dimmedRoute = (r: RouteLabel) =>
|
|
44
|
-
|
|
43
|
+
walkLift !== undefined && !r.route.walks.some(w => walkLift.names.has(w))
|
|
45
44
|
|
|
46
45
|
return (
|
|
47
46
|
<svg
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Checkbox,
|
|
3
|
+
FormControl,
|
|
4
|
+
InputLabel,
|
|
5
|
+
ListItemText,
|
|
6
|
+
MenuItem,
|
|
7
|
+
Select,
|
|
8
|
+
} from '@mui/material'
|
|
2
9
|
import { observer } from 'mobx-react'
|
|
3
10
|
import { makeStyles } from 'tss-react/mui'
|
|
4
11
|
|
|
@@ -7,12 +14,13 @@ import type { GraphPaneModel } from '../model'
|
|
|
7
14
|
const useStyles = makeStyles()({
|
|
8
15
|
formControl: {
|
|
9
16
|
minWidth: 130,
|
|
10
|
-
maxWidth:
|
|
17
|
+
maxWidth: 260,
|
|
11
18
|
},
|
|
12
19
|
})
|
|
13
20
|
|
|
14
|
-
// Which
|
|
15
|
-
// haplotypes a GBZ cut carries. Absent from a graph
|
|
21
|
+
// Which walks to lift out of the drawing: Bandage's path highlight, for as many
|
|
22
|
+
// of the haplotypes a GBZ cut carries as the reader picks. Absent from a graph
|
|
23
|
+
// with no walks.
|
|
16
24
|
const WalkSelect = observer(function WalkSelect({
|
|
17
25
|
model,
|
|
18
26
|
}: {
|
|
@@ -24,23 +32,28 @@ const WalkSelect = observer(function WalkSelect({
|
|
|
24
32
|
return null
|
|
25
33
|
}
|
|
26
34
|
const names = new Set(walkChoices.map(c => c.name))
|
|
35
|
+
const labels = new Map(walkChoices.map(c => [c.name, c.label]))
|
|
36
|
+
const picked = model.walkLayers
|
|
37
|
+
.map(layer => layer.walk)
|
|
38
|
+
.filter(name => names.has(name))
|
|
27
39
|
return (
|
|
28
40
|
<FormControl size="small" className={classes.formControl}>
|
|
29
|
-
<InputLabel>
|
|
41
|
+
<InputLabel>Walks</InputLabel>
|
|
30
42
|
<Select
|
|
31
|
-
|
|
32
|
-
|
|
43
|
+
multiple
|
|
44
|
+
value={picked}
|
|
45
|
+
label="Walks"
|
|
33
46
|
data-testid="graph-walk-select"
|
|
47
|
+
renderValue={selected => selected.map(n => labels.get(n)).join(', ')}
|
|
34
48
|
onChange={e => {
|
|
35
|
-
|
|
49
|
+
const { value } = e.target
|
|
50
|
+
model.liftWalks(typeof value === 'string' ? value.split(',') : value)
|
|
36
51
|
}}
|
|
37
52
|
>
|
|
38
|
-
<MenuItem value="">
|
|
39
|
-
<em>None</em>
|
|
40
|
-
</MenuItem>
|
|
41
53
|
{walkChoices.map(({ name, label }) => (
|
|
42
|
-
<MenuItem key={name} value={name}>
|
|
43
|
-
{
|
|
54
|
+
<MenuItem key={name} value={name} dense>
|
|
55
|
+
<Checkbox size="small" checked={picked.includes(name)} />
|
|
56
|
+
<ListItemText primary={label} />
|
|
44
57
|
</MenuItem>
|
|
45
58
|
))}
|
|
46
59
|
</Select>
|
|
@@ -2484,6 +2484,54 @@ describe('walk rows', () => {
|
|
|
2484
2484
|
model.setWalkRowSamples(['B'])
|
|
2485
2485
|
expect(model.walkRowBars!.rows.map(r => r.label)).toEqual(['B#1'])
|
|
2486
2486
|
})
|
|
2487
|
+
|
|
2488
|
+
test('walks lift together, each keeping the colour it was given', async () => {
|
|
2489
|
+
rpcRespond()
|
|
2490
|
+
const model = stateModelFactory().create({
|
|
2491
|
+
type: 'GraphGenomeView',
|
|
2492
|
+
layoutMode: 'auto',
|
|
2493
|
+
colorScheme: 'grey',
|
|
2494
|
+
})
|
|
2495
|
+
await model.loadGFA(WALKS_GFA, 'walks')
|
|
2496
|
+
model.liftWalks(['B#1#ctg', 'GRCh38#0#chr1'])
|
|
2497
|
+
model.setWalkColor('B#1#ctg', { field: 'strand' })
|
|
2498
|
+
model.setWalkColor('B#1#ctg', { scheme: 'greens' })
|
|
2499
|
+
model.toggleWalk('A#1#ctg')
|
|
2500
|
+
expect(model.walkLift!.walks.map(w => [w.name, w.encoding])).toEqual([
|
|
2501
|
+
['GRCh38#0#chr1', { field: 'reference', scheme: 'rainbow' }],
|
|
2502
|
+
['B#1#ctg', { field: 'strand', scheme: 'greens' }],
|
|
2503
|
+
['A#1#ctg', { field: 'progress', scheme: 'reds' }],
|
|
2504
|
+
])
|
|
2505
|
+
// the reference is coloured by position whatever the node colour scheme
|
|
2506
|
+
const ref = model.walkLift!.walks[0]!
|
|
2507
|
+
expect(new Set(ref.colors.values()).size).toBe(2)
|
|
2508
|
+
interface Item {
|
|
2509
|
+
label?: string
|
|
2510
|
+
checked?: boolean
|
|
2511
|
+
subMenu?: Item[]
|
|
2512
|
+
}
|
|
2513
|
+
const walkMenu = (model.graphMenuItems() as Item[]).find(
|
|
2514
|
+
item => item.label === 'Walk',
|
|
2515
|
+
)!
|
|
2516
|
+
const colour = walkMenu.subMenu!.find(item => item.label === 'Colour B#1')!
|
|
2517
|
+
expect(
|
|
2518
|
+
colour.subMenu.filter(item => item.checked).map(item => item.label),
|
|
2519
|
+
).toEqual(['Strand against the reference', 'Greens'])
|
|
2520
|
+
model.liftWalks(['B#1#ctg'])
|
|
2521
|
+
expect(model.walkLayers).toEqual([
|
|
2522
|
+
{ walk: 'B#1#ctg', color: { field: 'strand', scheme: 'greens' } },
|
|
2523
|
+
])
|
|
2524
|
+
model.toggleWalk('B#1#ctg')
|
|
2525
|
+
expect(model.walkLift).toBeUndefined()
|
|
2526
|
+
})
|
|
2527
|
+
|
|
2528
|
+
test('a session naming the one walk the old field lifted opens with none', () => {
|
|
2529
|
+
const model = stateModelFactory().create({
|
|
2530
|
+
type: 'GraphGenomeView',
|
|
2531
|
+
highlightedPath: 'A#1#ctg',
|
|
2532
|
+
} as never)
|
|
2533
|
+
expect(model.walkLayers).toEqual([])
|
|
2534
|
+
})
|
|
2487
2535
|
})
|
|
2488
2536
|
|
|
2489
2537
|
describe('annotation reads beside a cut', () => {
|
|
@@ -97,7 +97,8 @@ import {
|
|
|
97
97
|
viewportOf,
|
|
98
98
|
zoomAbout,
|
|
99
99
|
} from '@jbrowse/bandage-core/viewport'
|
|
100
|
-
import {
|
|
100
|
+
import { WALK_FIELDS, WALK_SCHEMES } from '@jbrowse/bandage-core/walkEncoding'
|
|
101
|
+
import { walkLift } from '@jbrowse/bandage-core/walkHighlight'
|
|
101
102
|
import { readConfObject } from '@jbrowse/core/configuration'
|
|
102
103
|
import { pushLaunchViewMenuItem } from '@jbrowse/core/ui'
|
|
103
104
|
import {
|
|
@@ -179,6 +180,10 @@ import type {
|
|
|
179
180
|
} from '@jbrowse/bandage-core/types'
|
|
180
181
|
import type { AxisScale } from '@jbrowse/bandage-core/util/geometry'
|
|
181
182
|
import type { NodeInk } from '@jbrowse/bandage-core/util/hitDetection'
|
|
183
|
+
import type {
|
|
184
|
+
WalkEncoding,
|
|
185
|
+
WalkLayer,
|
|
186
|
+
} from '@jbrowse/bandage-core/walkEncoding'
|
|
182
187
|
import type { AnyConfigurationModel } from '@jbrowse/core/configuration'
|
|
183
188
|
import type { MenuItem } from '@jbrowse/core/ui'
|
|
184
189
|
import type { Feature } from '@jbrowse/core/util'
|
|
@@ -440,9 +445,10 @@ export function GraphPaneMixin() {
|
|
|
440
445
|
// Samples whose walks the walk rows show, by the name before the
|
|
441
446
|
// haplotype number; undefined shows every walk the cut holds.
|
|
442
447
|
walkRowSamples: types.maybe(types.frozen<string[]>()),
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
|
|
448
|
+
// Walks lifted out of the drawing, each a layer with a lane of its own
|
|
449
|
+
// and the rest fading, coloured by the encoding it states or by the
|
|
450
|
+
// default one. See walkEncoding.ts. Empty lifts none.
|
|
451
|
+
walkLayers: types.optional(types.frozen<WalkLayer[]>(), []),
|
|
446
452
|
// Which of a general GFA's paths the anchored layouts put on x. A path
|
|
447
453
|
// GFA's names are arbitrary and none of them is marked as the
|
|
448
454
|
// reference, so this is a choice; empty means "infer", which is the
|
|
@@ -723,13 +729,6 @@ export function GraphPaneMixin() {
|
|
|
723
729
|
const paths = self.graph?.paths
|
|
724
730
|
return paths?.length ? pathLegend(paths) : []
|
|
725
731
|
},
|
|
726
|
-
// The lifted walk, or undefined when none is named or the graph on
|
|
727
|
-
// screen does not carry the one that was.
|
|
728
|
-
get walkHighlight() {
|
|
729
|
-
return self.graph && self.highlightedPath
|
|
730
|
-
? walkHighlight(self.graph, self.highlightedPath)
|
|
731
|
-
: undefined
|
|
732
|
-
},
|
|
733
732
|
// Whether the drawing is actually painted per path, which is not the same
|
|
734
733
|
// question as whether the user asked for it: past MAX_PATH_COLORS the
|
|
735
734
|
// colours say nothing and cost a stroke per path per edge. A bare getter
|
|
@@ -754,6 +753,19 @@ export function GraphPaneMixin() {
|
|
|
754
753
|
get rampDomain() {
|
|
755
754
|
return self.colorDomain ?? self.graphRegion
|
|
756
755
|
},
|
|
756
|
+
// The lifted walks the graph on screen carries, or undefined when none
|
|
757
|
+
// is named or it carries none of those that were. A walk coloured by
|
|
758
|
+
// reference position reads the ramp whatever the node colour scheme.
|
|
759
|
+
get walkLift() {
|
|
760
|
+
const { graph } = self
|
|
761
|
+
return graph && self.walkLayers.length > 0
|
|
762
|
+
? walkLift(
|
|
763
|
+
graph,
|
|
764
|
+
self.walkLayers,
|
|
765
|
+
computeReferenceRamp(graph, this.rampDomain),
|
|
766
|
+
)
|
|
767
|
+
: undefined
|
|
768
|
+
},
|
|
757
769
|
// The scheme the renderer actually paints with, which is the raw prop
|
|
758
770
|
// unless it is 'auto'. A bare getter returns a resolved value (root
|
|
759
771
|
// CLAUDE.md): every consumer of a colour reads this one, and the two
|
|
@@ -1419,7 +1431,7 @@ export function GraphPaneMixin() {
|
|
|
1419
1431
|
? referenceStripBlocks(graph, {
|
|
1420
1432
|
colorScheme: self.effectiveColorScheme,
|
|
1421
1433
|
referenceRamp: self.referenceRamp,
|
|
1422
|
-
walkNodes: self.
|
|
1434
|
+
walkNodes: self.walkLift?.nodeIds,
|
|
1423
1435
|
})
|
|
1424
1436
|
: []
|
|
1425
1437
|
},
|
|
@@ -1765,8 +1777,25 @@ export function GraphPaneMixin() {
|
|
|
1765
1777
|
setRepeatKey(key: string) {
|
|
1766
1778
|
self.repeatKey = key
|
|
1767
1779
|
},
|
|
1768
|
-
|
|
1769
|
-
self.
|
|
1780
|
+
setWalkLayers(layers: WalkLayer[]) {
|
|
1781
|
+
self.walkLayers = layers
|
|
1782
|
+
},
|
|
1783
|
+
// Lift the walks named, in that order, keeping the colour any of them
|
|
1784
|
+
// already had
|
|
1785
|
+
liftWalks(names: string[]) {
|
|
1786
|
+
const had = new Map(self.walkLayers.map(l => [l.walk, l]))
|
|
1787
|
+
self.walkLayers = names.map(walk => had.get(walk) ?? { walk })
|
|
1788
|
+
},
|
|
1789
|
+
toggleWalk(walk: string) {
|
|
1790
|
+
const layers = self.walkLayers
|
|
1791
|
+
self.walkLayers = layers.some(l => l.walk === walk)
|
|
1792
|
+
? layers.filter(l => l.walk !== walk)
|
|
1793
|
+
: [...layers, { walk }]
|
|
1794
|
+
},
|
|
1795
|
+
setWalkColor(walk: string, color: Partial<WalkEncoding>) {
|
|
1796
|
+
self.walkLayers = self.walkLayers.map(l =>
|
|
1797
|
+
l.walk === walk ? { ...l, color: { ...l.color, ...color } } : l,
|
|
1798
|
+
)
|
|
1770
1799
|
},
|
|
1771
1800
|
// Undefined restores the built-in ceiling. Nothing recomputes: the pane
|
|
1772
1801
|
// reads canvasHeight and the drawing is placed by zoomToFit, which the
|
|
@@ -2816,7 +2845,7 @@ export function GraphPaneMixin() {
|
|
|
2816
2845
|
connectorThickness: self.connectorThickness,
|
|
2817
2846
|
drawPaths: self.effectiveDrawPaths,
|
|
2818
2847
|
nodeWidth: self.nodeWidth,
|
|
2819
|
-
highlight: self.
|
|
2848
|
+
highlight: self.walkLift,
|
|
2820
2849
|
// Untracked, so a zoom does not eagerly rebuild geometry — the
|
|
2821
2850
|
// debounced viewportDirty bump drives the scale-dependent
|
|
2822
2851
|
// rebuild (flatness, arrow visibility, viewport culling), same
|
|
@@ -3002,21 +3031,46 @@ export function GraphPaneMixin() {
|
|
|
3002
3031
|
label: 'Walk',
|
|
3003
3032
|
subMenu: [
|
|
3004
3033
|
{
|
|
3005
|
-
type: 'radio' as const,
|
|
3006
3034
|
label: 'None',
|
|
3007
|
-
checked: self.highlightedPath === '',
|
|
3008
3035
|
onClick: () => {
|
|
3009
|
-
self.
|
|
3036
|
+
self.setWalkLayers([])
|
|
3010
3037
|
},
|
|
3011
3038
|
},
|
|
3012
3039
|
...walks.map(walk => ({
|
|
3013
|
-
type: '
|
|
3040
|
+
type: 'checkbox' as const,
|
|
3014
3041
|
label: walk.label,
|
|
3015
|
-
checked: self.
|
|
3042
|
+
checked: self.walkLayers.some(l => l.walk === walk.name),
|
|
3016
3043
|
onClick: () => {
|
|
3017
|
-
self.
|
|
3044
|
+
self.toggleWalk(walk.name)
|
|
3018
3045
|
},
|
|
3019
3046
|
})),
|
|
3047
|
+
...(self.walkLift?.walks ?? []).map(lifted => ({
|
|
3048
|
+
label: `Colour ${walks.find(w => w.name === lifted.name)?.label ?? lifted.name}`,
|
|
3049
|
+
subMenu: [
|
|
3050
|
+
{ type: 'subHeader' as const, label: 'Colour by' },
|
|
3051
|
+
...WALK_FIELDS.map(field => ({
|
|
3052
|
+
type: 'radio' as const,
|
|
3053
|
+
label: field.label,
|
|
3054
|
+
checked: lifted.encoding.field === field.value,
|
|
3055
|
+
onClick: () => {
|
|
3056
|
+
self.setWalkColor(lifted.name, {
|
|
3057
|
+
field: field.value,
|
|
3058
|
+
})
|
|
3059
|
+
},
|
|
3060
|
+
})),
|
|
3061
|
+
{ type: 'subHeader' as const, label: 'Palette' },
|
|
3062
|
+
...WALK_SCHEMES.map(scheme => ({
|
|
3063
|
+
type: 'radio' as const,
|
|
3064
|
+
label: scheme.label,
|
|
3065
|
+
checked: lifted.encoding.scheme === scheme.value,
|
|
3066
|
+
onClick: () => {
|
|
3067
|
+
self.setWalkColor(lifted.name, {
|
|
3068
|
+
scheme: scheme.value,
|
|
3069
|
+
})
|
|
3070
|
+
},
|
|
3071
|
+
})),
|
|
3072
|
+
],
|
|
3073
|
+
})),
|
|
3020
3074
|
],
|
|
3021
3075
|
},
|
|
3022
3076
|
]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { layoutModeByValue } from '@jbrowse/bandage-core/layoutModes'
|
|
1
2
|
import { readConfObject } from '@jbrowse/core/configuration'
|
|
2
3
|
import { BaseViewModel } from '@jbrowse/core/pluggableElementTypes/models'
|
|
3
4
|
import { getSession } from '@jbrowse/core/util'
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
} from '../graphTrackConfig'
|
|
11
12
|
|
|
12
13
|
import type { SubgraphRegion } from '../GetSubgraph'
|
|
14
|
+
import type { LayoutModeValue } from '@jbrowse/bandage-core/layoutModes'
|
|
13
15
|
import type { AnyConfigurationModel } from '@jbrowse/core/configuration'
|
|
14
16
|
import type { FileLocation } from '@jbrowse/core/util/types'
|
|
15
17
|
import type { Instance } from '@jbrowse/mobx-state-tree'
|
|
@@ -109,6 +111,9 @@ export default function stateModelFactory() {
|
|
|
109
111
|
return self.cutSubgraph(readConfObject(track, 'adapter'), region, {
|
|
110
112
|
hops: self.subgraphContext,
|
|
111
113
|
haplotypes: self.subgraphHaplotypes,
|
|
114
|
+
...(layoutModeByValue(self.chosenLayoutMode).wholeWalks
|
|
115
|
+
? { snarls: 'overlapping' as const }
|
|
116
|
+
: {}),
|
|
112
117
|
})
|
|
113
118
|
}
|
|
114
119
|
return undefined
|
|
@@ -119,6 +124,16 @@ export default function stateModelFactory() {
|
|
|
119
124
|
retryLoad() {
|
|
120
125
|
void self.load()
|
|
121
126
|
},
|
|
127
|
+
// walk rows measures whole walks, which a GBZ cut only follows when
|
|
128
|
+
// asked, so entering or leaving it cuts the track again
|
|
129
|
+
switchLayout(mode: LayoutModeValue) {
|
|
130
|
+
const from = layoutModeByValue(self.chosenLayoutMode)
|
|
131
|
+
self.setLayoutMode(mode)
|
|
132
|
+
return self.loadedTrackId &&
|
|
133
|
+
layoutModeByValue(mode).wholeWalks !== from.wholeWalks
|
|
134
|
+
? self.load()
|
|
135
|
+
: self.recomputeLayout()
|
|
136
|
+
},
|
|
122
137
|
// Here rather than when the rendering backend starts: the canvas only
|
|
123
138
|
// mounts once there is a graph, so a view whose graph must be fetched
|
|
124
139
|
// would never fetch it.
|
|
@@ -81,6 +81,7 @@ function syntheticGraph(
|
|
|
81
81
|
interface Cut {
|
|
82
82
|
tier: SubgraphTier
|
|
83
83
|
region: SubgraphRegion
|
|
84
|
+
snarls?: string
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
const FORCE_LAYOUT = {
|
|
@@ -234,7 +235,7 @@ function createEnvironment({ tiered = true, paths = false } = {}) {
|
|
|
234
235
|
method: string,
|
|
235
236
|
args: {
|
|
236
237
|
region: SubgraphRegion
|
|
237
|
-
opts?: { tier?: SubgraphTier }
|
|
238
|
+
opts?: { tier?: SubgraphTier; snarls?: string }
|
|
238
239
|
signal?: AbortSignal
|
|
239
240
|
},
|
|
240
241
|
) => {
|
|
@@ -247,7 +248,11 @@ function createEnvironment({ tiered = true, paths = false } = {}) {
|
|
|
247
248
|
if (method !== 'GetSubgraph') {
|
|
248
249
|
return Promise.reject(new Error(`Unexpected RPC: ${method}`))
|
|
249
250
|
}
|
|
250
|
-
const cut = {
|
|
251
|
+
const cut = {
|
|
252
|
+
tier: args.opts?.tier ?? 'fine',
|
|
253
|
+
region: args.region,
|
|
254
|
+
snarls: args.opts?.snarls,
|
|
255
|
+
}
|
|
251
256
|
cuts.push(cut)
|
|
252
257
|
if (args.signal) {
|
|
253
258
|
signals.push(args.signal)
|
|
@@ -604,6 +609,22 @@ test('walk rows fit their own bars in the track, since a walk can be longer than
|
|
|
604
609
|
expect(pane.viewportOwner).toBe('fit')
|
|
605
610
|
})
|
|
606
611
|
|
|
612
|
+
test('walk rows cut every snarl a walk takes past the window, so a walk comes whole', async () => {
|
|
613
|
+
const { pane, cuts } = await shownGraph({ paths: true })
|
|
614
|
+
await pane.switchLayout('force')
|
|
615
|
+
await wait(SETTLE_MS)
|
|
616
|
+
const before = cuts.length
|
|
617
|
+
expect(cuts.at(-1)!.snarls).toBeUndefined()
|
|
618
|
+
await pane.switchLayout('walkrows')
|
|
619
|
+
await wait(SETTLE_MS)
|
|
620
|
+
expect(cuts).toHaveLength(before + 1)
|
|
621
|
+
expect(cuts.at(-1)!.snarls).toBe('overlapping')
|
|
622
|
+
await pane.switchLayout('force')
|
|
623
|
+
await wait(SETTLE_MS)
|
|
624
|
+
expect(cuts).toHaveLength(before + 2)
|
|
625
|
+
expect(cuts.at(-1)!.snarls).toBeUndefined()
|
|
626
|
+
})
|
|
627
|
+
|
|
607
628
|
test('a launch in the force layout cuts the window alone', async () => {
|
|
608
629
|
const { view, cuts } = createEnvironment()
|
|
609
630
|
view.zoomTo(60_000 / WIDTH_PX)
|
|
@@ -220,6 +220,9 @@ export function stateModelFactory(configSchema: LinearGraphDisplayConfigModel) {
|
|
|
220
220
|
hops: coarse ? 0 : self.subgraphContext,
|
|
221
221
|
haplotypes: self.chosenHaplotypes,
|
|
222
222
|
tier: coarse ? 'coarse' : undefined,
|
|
223
|
+
...(layoutModeByValue(self.chosenLayoutMode).wholeWalks
|
|
224
|
+
? { snarls: 'overlapping' as const }
|
|
225
|
+
: {}),
|
|
223
226
|
})
|
|
224
227
|
},
|
|
225
228
|
}))
|
|
@@ -257,13 +260,15 @@ export function stateModelFactory(configSchema: LinearGraphDisplayConfigModel) {
|
|
|
257
260
|
return true
|
|
258
261
|
},
|
|
259
262
|
switchLayout(mode: LayoutModeValue) {
|
|
260
|
-
const
|
|
263
|
+
const from = layoutModeByValue(self.chosenLayoutMode)
|
|
261
264
|
self.setLayoutMode(mode)
|
|
265
|
+
const to = layoutModeByValue(mode)
|
|
262
266
|
const seen = self.host ? hostWindow(self.host) : undefined
|
|
263
267
|
if (
|
|
264
268
|
seen &&
|
|
265
269
|
!self.regionTooLarge &&
|
|
266
|
-
|
|
270
|
+
(to.cutMargins !== from.cutMargins ||
|
|
271
|
+
to.wholeWalks !== from.wholeWalks)
|
|
267
272
|
) {
|
|
268
273
|
return self.recutAt(seen)
|
|
269
274
|
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '4.0.
|
|
1
|
+
export const version = '4.0.21'
|