jbrowse-plugin-graphgenomeviewer 4.0.21 → 4.0.22
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 +5 -2
- package/dist/chunks/AddTrackWorkflow-NKKGI6SM.js +2 -0
- package/dist/chunks/{AddTrackWorkflow-WYRVQYH2.js.map → AddTrackWorkflow-NKKGI6SM.js.map} +1 -1
- package/dist/chunks/{GbzBaseSyntenyAdapter-7ZKFAEJG.js → GbzBaseSyntenyAdapter-HNETLUW4.js} +2 -2
- package/dist/chunks/{GraphGenomeView-CLUMXH4K.js → GraphGenomeView-GMHRO7E4.js} +2 -2
- package/dist/chunks/{GraphGenomeView-CLUMXH4K.js.map → GraphGenomeView-GMHRO7E4.js.map} +1 -1
- package/dist/chunks/LinearGraphDisplay-I2OOPQKB.js +2 -0
- package/dist/chunks/{LinearGraphDisplay-4CMVCEM5.js.map → LinearGraphDisplay-I2OOPQKB.js.map} +1 -1
- package/dist/chunks/{MinigraphBubbleAdapter-NRDD66AK.js → MinigraphBubbleAdapter-FIFZYJ27.js} +2 -2
- package/dist/chunks/chunk-2PYVZZ3R.js +3 -0
- package/dist/chunks/chunk-2PYVZZ3R.js.map +7 -0
- package/dist/chunks/chunk-D7XFR2IV.js +2 -0
- package/dist/chunks/chunk-D7XFR2IV.js.map +7 -0
- package/dist/chunks/chunk-DMYUVGGI.js +3 -0
- package/dist/chunks/{chunk-QSH263Y6.js.map → chunk-DMYUVGGI.js.map} +4 -4
- package/dist/chunks/chunk-JXSDDOKD.js +3 -0
- package/dist/chunks/chunk-JXSDDOKD.js.map +7 -0
- package/dist/chunks/chunk-OSTJHAAK.js +2 -0
- package/dist/chunks/chunk-OSTJHAAK.js.map +7 -0
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +1 -2
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
- package/package.json +2 -2
- package/src/GraphGenomeView/components/FacetPanels.tsx +204 -0
- package/src/GraphGenomeView/components/GenePins.tsx +12 -5
- package/src/GraphGenomeView/components/GraphCanvas.tsx +86 -42
- package/src/GraphGenomeView/components/ReferenceStripOverlay.tsx +19 -7
- package/src/GraphGenomeView/model.test.ts +36 -10
- package/src/GraphGenomeView/model.ts +130 -9
- package/src/version.ts +1 -1
- package/dist/chunks/AddTrackWorkflow-WYRVQYH2.js +0 -2
- package/dist/chunks/LinearGraphDisplay-4CMVCEM5.js +0 -2
- package/dist/chunks/chunk-3FGJLRG2.js +0 -2
- package/dist/chunks/chunk-3FGJLRG2.js.map +0 -7
- package/dist/chunks/chunk-3SZJ4AFI.js +0 -3
- package/dist/chunks/chunk-3SZJ4AFI.js.map +0 -7
- package/dist/chunks/chunk-QSH263Y6.js +0 -2
- package/dist/chunks/chunk-SCMIJAAX.js +0 -3
- package/dist/chunks/chunk-SCMIJAAX.js.map +0 -7
- package/dist/chunks/chunk-XK7OPCRQ.js +0 -2
- package/dist/chunks/chunk-XK7OPCRQ.js.map +0 -7
- /package/dist/chunks/{GbzBaseSyntenyAdapter-7ZKFAEJG.js.map → GbzBaseSyntenyAdapter-HNETLUW4.js.map} +0 -0
- /package/dist/chunks/{MinigraphBubbleAdapter-NRDD66AK.js.map → MinigraphBubbleAdapter-FIFZYJ27.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.22",
|
|
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.17"
|
|
80
80
|
},
|
|
81
81
|
"peerDependencies": {
|
|
82
82
|
"react": ">=18.0.0",
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import { formatBp } from '@jbrowse/bandage-core/graphLabels'
|
|
4
|
+
import { fitTransform } from '@jbrowse/bandage-core/pipeline'
|
|
5
|
+
import { Canvas2DRenderer } from '@jbrowse/bandage-core/renderer/Canvas2DRenderer'
|
|
6
|
+
import { buildGeometry } from '@jbrowse/bandage-core/renderer/GeometryBuilder'
|
|
7
|
+
import { axisScaleOf } from '@jbrowse/bandage-core/viewport'
|
|
8
|
+
import { encodingSwatchCss } from '@jbrowse/bandage-core/walkEncoding'
|
|
9
|
+
import { getDpr } from '@jbrowse/render-core/canvas2dUtils'
|
|
10
|
+
import { observer } from 'mobx-react'
|
|
11
|
+
|
|
12
|
+
import { FACET_GAP_PX, FACET_PAD_PX, FACET_TITLE_PX } from '../model'
|
|
13
|
+
|
|
14
|
+
import type { GraphPaneModel } from '../model'
|
|
15
|
+
import type { WalkLift } from '@jbrowse/bandage-core/walkHighlight'
|
|
16
|
+
|
|
17
|
+
// The pane faceted by walk: the same layout drawn once per lifted walk, each
|
|
18
|
+
// panel with that walk alone shading along itself, so walks compare by where
|
|
19
|
+
// they go rather than by which lane is which colour. A panel's title is its
|
|
20
|
+
// scale, and clicking it lifts that walk alone in the whole pane.
|
|
21
|
+
|
|
22
|
+
const gridStyle = {
|
|
23
|
+
position: 'absolute' as const,
|
|
24
|
+
left: 0,
|
|
25
|
+
top: 0,
|
|
26
|
+
zIndex: 4,
|
|
27
|
+
display: 'grid',
|
|
28
|
+
gap: FACET_GAP_PX,
|
|
29
|
+
lineHeight: 'normal',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const titleStyle = {
|
|
33
|
+
height: FACET_TITLE_PX,
|
|
34
|
+
padding: '2px 6px',
|
|
35
|
+
boxSizing: 'border-box' as const,
|
|
36
|
+
fontSize: 11,
|
|
37
|
+
cursor: 'pointer',
|
|
38
|
+
border: 'none',
|
|
39
|
+
background: 'none',
|
|
40
|
+
textAlign: 'left' as const,
|
|
41
|
+
width: '100%',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const barRowStyle = { display: 'flex', alignItems: 'center', gap: 5 }
|
|
45
|
+
const barStyle = { flex: 1, height: 8, borderRadius: 2 }
|
|
46
|
+
|
|
47
|
+
const FacetPanel = observer(function FacetPanel({
|
|
48
|
+
model,
|
|
49
|
+
lift,
|
|
50
|
+
width,
|
|
51
|
+
height,
|
|
52
|
+
}: {
|
|
53
|
+
model: GraphPaneModel
|
|
54
|
+
lift: WalkLift
|
|
55
|
+
width: number
|
|
56
|
+
height: number
|
|
57
|
+
}) {
|
|
58
|
+
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
59
|
+
const { graph, nodePositions, nodeById, layoutBounds, pixelRows } = model
|
|
60
|
+
const walk = lift.walks[0]!
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const canvas = canvasRef.current
|
|
64
|
+
if (!canvas || !graph || !nodePositions || !nodeById || !layoutBounds) {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
const renderer = new Canvas2DRenderer(canvas)
|
|
68
|
+
renderer.resize(width, height)
|
|
69
|
+
const fit = fitTransform(layoutBounds, width, height, pixelRows, {
|
|
70
|
+
padLeft: FACET_PAD_PX,
|
|
71
|
+
padTop: FACET_PAD_PX,
|
|
72
|
+
padRight: FACET_PAD_PX,
|
|
73
|
+
padBottom: FACET_PAD_PX,
|
|
74
|
+
})
|
|
75
|
+
if (fit) {
|
|
76
|
+
const axis = axisScaleOf(fit.scale, pixelRows)
|
|
77
|
+
renderer.uploadGeometry(
|
|
78
|
+
buildGeometry({
|
|
79
|
+
nodePositions,
|
|
80
|
+
graph,
|
|
81
|
+
nodeById,
|
|
82
|
+
colorScheme: model.effectiveColorScheme,
|
|
83
|
+
contigThickness: model.contigThickness,
|
|
84
|
+
connectorThickness: model.connectorThickness,
|
|
85
|
+
drawPaths: false,
|
|
86
|
+
nodeWidth: model.nodeWidth,
|
|
87
|
+
highlight: lift,
|
|
88
|
+
axis,
|
|
89
|
+
linearLayout: model.linearLayout,
|
|
90
|
+
referenceRamp: model.referenceRamp,
|
|
91
|
+
deletions: model.deletionEdgeIndexes,
|
|
92
|
+
hiddenEdges: model.hiddenEdgeIndexes,
|
|
93
|
+
version: model.positionsVersion,
|
|
94
|
+
}),
|
|
95
|
+
)
|
|
96
|
+
const dpr = getDpr()
|
|
97
|
+
renderer.updateTransform({
|
|
98
|
+
scaleX: axis.scaleX * dpr,
|
|
99
|
+
scaleY: axis.scaleY * dpr,
|
|
100
|
+
translateX: fit.translateX * dpr,
|
|
101
|
+
translateY: fit.translateY * dpr,
|
|
102
|
+
dpr,
|
|
103
|
+
})
|
|
104
|
+
renderer.render(model.darkMode ? [0.12, 0.12, 0.12, 1] : [1, 1, 1, 1])
|
|
105
|
+
}
|
|
106
|
+
return () => {
|
|
107
|
+
renderer.dispose()
|
|
108
|
+
}
|
|
109
|
+
}, [
|
|
110
|
+
model,
|
|
111
|
+
lift,
|
|
112
|
+
width,
|
|
113
|
+
height,
|
|
114
|
+
graph,
|
|
115
|
+
nodePositions,
|
|
116
|
+
nodeById,
|
|
117
|
+
layoutBounds,
|
|
118
|
+
pixelRows,
|
|
119
|
+
])
|
|
120
|
+
|
|
121
|
+
const label =
|
|
122
|
+
model.walkChoices.find(c => c.name === walk.name)?.label ?? walk.name
|
|
123
|
+
const delta =
|
|
124
|
+
walk.referenceBp === undefined || walk.bp === walk.referenceBp
|
|
125
|
+
? ''
|
|
126
|
+
: ` ${walk.bp > walk.referenceBp ? '+' : '−'}${formatBp(Math.abs(walk.bp - walk.referenceBp))}`
|
|
127
|
+
const range = walk.range
|
|
128
|
+
return (
|
|
129
|
+
<div>
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
style={titleStyle}
|
|
133
|
+
data-testid="graph-facet-title"
|
|
134
|
+
title={
|
|
135
|
+
range
|
|
136
|
+
? `${range.contig}:${range.start.toLocaleString()}-${range.end.toLocaleString()} · click to lift ${label} alone`
|
|
137
|
+
: `click to lift ${label} alone`
|
|
138
|
+
}
|
|
139
|
+
onClick={() => {
|
|
140
|
+
model.liftWalks([walk.name])
|
|
141
|
+
model.setFacet('none')
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
<div>
|
|
145
|
+
<strong>{label}</strong>
|
|
146
|
+
{delta}
|
|
147
|
+
{walk.reversedBp > 0 ? `, ${formatBp(walk.reversedBp)} reversed` : ''}
|
|
148
|
+
</div>
|
|
149
|
+
<div style={barRowStyle}>
|
|
150
|
+
{range ? <span>{range.start.toLocaleString()}</span> : null}
|
|
151
|
+
<div
|
|
152
|
+
style={{
|
|
153
|
+
...barStyle,
|
|
154
|
+
background: encodingSwatchCss(walk.encoding),
|
|
155
|
+
}}
|
|
156
|
+
/>
|
|
157
|
+
{range ? <span>{range.end.toLocaleString()}</span> : null}
|
|
158
|
+
</div>
|
|
159
|
+
</button>
|
|
160
|
+
<canvas
|
|
161
|
+
ref={canvasRef}
|
|
162
|
+
data-testid="graph-facet-canvas"
|
|
163
|
+
style={{ width, height, display: 'block' }}
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const FacetPanels = observer(function FacetPanels({
|
|
170
|
+
model,
|
|
171
|
+
}: {
|
|
172
|
+
model: GraphPaneModel
|
|
173
|
+
}) {
|
|
174
|
+
const panels = model.facetPanels
|
|
175
|
+
const grid = model.facetGrid
|
|
176
|
+
if (!panels || !grid) {
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
const { columns, width, height } = grid
|
|
180
|
+
return (
|
|
181
|
+
<div
|
|
182
|
+
data-testid="graph-facet-panels"
|
|
183
|
+
style={{
|
|
184
|
+
...gridStyle,
|
|
185
|
+
width: model.paneWidth,
|
|
186
|
+
minHeight: model.canvasHeight,
|
|
187
|
+
gridTemplateColumns: `repeat(${columns}, ${width}px)`,
|
|
188
|
+
background: model.darkMode ? '#1f1f1f' : 'white',
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
{panels.map(lift => (
|
|
192
|
+
<FacetPanel
|
|
193
|
+
key={lift.walks[0]!.name}
|
|
194
|
+
model={model}
|
|
195
|
+
lift={lift}
|
|
196
|
+
width={width}
|
|
197
|
+
height={height}
|
|
198
|
+
/>
|
|
199
|
+
))}
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
export default FacetPanels
|
|
@@ -4,7 +4,8 @@ import type { GraphPaneModel } from '../model'
|
|
|
4
4
|
|
|
5
5
|
// The session's genes drawn onto the graph: exons as dark stretches along the
|
|
6
6
|
// backbone nodes that carry them, in layout units, moved by the same transform
|
|
7
|
-
// the halos use. Their names are LabelLayer's.
|
|
7
|
+
// the halos use. Their names are LabelLayer's. Over lifted walks an exon is a
|
|
8
|
+
// faint band across all the lanes, so no lane reads as broken by it.
|
|
8
9
|
|
|
9
10
|
const svgStyle = {
|
|
10
11
|
position: 'absolute' as const,
|
|
@@ -16,13 +17,15 @@ const svgStyle = {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export const EXON_COLOR = '#1c1c22'
|
|
20
|
+
// a lane's width at the least, for the band an exon draws across them
|
|
21
|
+
const EXON_BAND_LANE_PX = 4
|
|
19
22
|
|
|
20
23
|
const GenePins = observer(function GenePins({
|
|
21
24
|
model,
|
|
22
25
|
}: {
|
|
23
26
|
model: GraphPaneModel
|
|
24
27
|
}) {
|
|
25
|
-
const { genePins } = model
|
|
28
|
+
const { genePins, walkLift } = model
|
|
26
29
|
if (genePins.length === 0) {
|
|
27
30
|
return null
|
|
28
31
|
}
|
|
@@ -52,9 +55,13 @@ const GenePins = observer(function GenePins({
|
|
|
52
55
|
d={pin.exons}
|
|
53
56
|
fill="none"
|
|
54
57
|
stroke={EXON_COLOR}
|
|
55
|
-
strokeOpacity={0.9}
|
|
56
|
-
strokeWidth={
|
|
57
|
-
|
|
58
|
+
strokeOpacity={walkLift ? 0.2 : 0.9}
|
|
59
|
+
strokeWidth={
|
|
60
|
+
walkLift
|
|
61
|
+
? walkLift.walks.length * EXON_BAND_LANE_PX + 6
|
|
62
|
+
: contigThickness * 0.55
|
|
63
|
+
}
|
|
64
|
+
strokeLinecap={walkLift ? 'butt' : 'round'}
|
|
58
65
|
vectorEffect="non-scaling-stroke"
|
|
59
66
|
/>
|
|
60
67
|
) : null,
|
|
@@ -7,11 +7,7 @@ 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
|
+
import { encodingSwatchCss } from '@jbrowse/bandage-core/walkEncoding'
|
|
15
11
|
import { ErrorBanner, LoadingOverlay, Menu } from '@jbrowse/core/ui'
|
|
16
12
|
import { isAlive } from '@jbrowse/mobx-state-tree'
|
|
17
13
|
import { useRenderingBackend } from '@jbrowse/render-core/useRenderingBackend'
|
|
@@ -19,6 +15,7 @@ import InfoIcon from '@mui/icons-material/Info'
|
|
|
19
15
|
import { observer } from 'mobx-react'
|
|
20
16
|
|
|
21
17
|
import BubbleHalos, { HaloLegend } from './BubbleHalos'
|
|
18
|
+
import FacetPanels from './FacetPanels'
|
|
22
19
|
import GenePins from './GenePins'
|
|
23
20
|
import GraphToolbar from './GraphToolbar'
|
|
24
21
|
import LabelLayer from './LabelLayer'
|
|
@@ -34,6 +31,7 @@ import { nodeLaunchMenuItems } from '../../launchFromGraph/graphMenuItems'
|
|
|
34
31
|
import { createGraphRenderer } from '../renderer/GraphRenderer'
|
|
35
32
|
|
|
36
33
|
import type { GraphPaneModel } from '../model'
|
|
34
|
+
import type { LiftedWalk } from '@jbrowse/bandage-core/walkHighlight'
|
|
37
35
|
|
|
38
36
|
// Bottom RIGHT, not bottom left: the row labels of a row-structured layout are
|
|
39
37
|
// pinned to the left edge, so a bottom-left tooltip lands on top of them and
|
|
@@ -181,14 +179,16 @@ const PathLegend = observer(function PathLegend({
|
|
|
181
179
|
) : null
|
|
182
180
|
})
|
|
183
181
|
|
|
184
|
-
const
|
|
182
|
+
const walkBlockStyle = { marginBottom: 2 }
|
|
183
|
+
const walkSwatchStyle = { width: 18, height: 8, borderRadius: 2, flex: 'none' }
|
|
184
|
+
// a faded node: grey at the fade's alpha
|
|
185
|
+
const FADED_SWATCH = 'rgba(160, 160, 160, 0.18)'
|
|
186
|
+
const walkBarRowStyle = { display: 'flex', alignItems: 'center', gap: 5 }
|
|
187
|
+
const walkBarStyle = { flex: 1, minWidth: 60, height: 8, borderRadius: 2 }
|
|
185
188
|
|
|
186
189
|
// What each lifted walk carries through the window, against the reference
|
|
187
190
|
// walk where the graph has one, beside the scale its lane is coloured by, and
|
|
188
191
|
// what each field in use means
|
|
189
|
-
function fieldName(field: string) {
|
|
190
|
-
return WALK_FIELDS.find(f => f.value === field)!.label.toLowerCase()
|
|
191
|
-
}
|
|
192
192
|
|
|
193
193
|
const WalkReadout = observer(function WalkReadout({
|
|
194
194
|
model,
|
|
@@ -196,50 +196,92 @@ const WalkReadout = observer(function WalkReadout({
|
|
|
196
196
|
model: GraphPaneModel
|
|
197
197
|
}) {
|
|
198
198
|
const lift = model.walkLift
|
|
199
|
-
|
|
199
|
+
// faceted, each panel's title is its walk's key
|
|
200
|
+
if (!lift || model.facetPanels) {
|
|
200
201
|
return null
|
|
201
202
|
}
|
|
202
203
|
const labelOf = (name: string) =>
|
|
203
204
|
model.walkChoices.find(c => c.name === name)?.label ?? name
|
|
205
|
+
const alone = lift.walks.length === 1
|
|
204
206
|
return (
|
|
205
207
|
<div style={legendBoxStyle} data-testid="graph-walk-readout">
|
|
206
|
-
{lift.walks.map(w =>
|
|
207
|
-
|
|
208
|
-
w.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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>
|
|
208
|
+
{lift.walks.map(w => (
|
|
209
|
+
<WalkRow
|
|
210
|
+
key={w.name}
|
|
211
|
+
walk={w}
|
|
212
|
+
label={labelOf(w.name)}
|
|
213
|
+
referenceDomain={lift.referenceDomain}
|
|
214
|
+
referenceName={model.graphRegion?.refName}
|
|
215
|
+
/>
|
|
234
216
|
))}
|
|
235
|
-
<div>
|
|
236
|
-
|
|
237
|
-
|
|
217
|
+
<div style={pathLegendRowStyle}>
|
|
218
|
+
<div style={{ ...walkSwatchStyle, background: FADED_SWATCH }} />
|
|
219
|
+
<span>
|
|
220
|
+
not on {alone ? labelOf(lift.walks[0]!.name) : 'these walks'}
|
|
221
|
+
</span>
|
|
238
222
|
</div>
|
|
239
223
|
</div>
|
|
240
224
|
)
|
|
241
225
|
})
|
|
242
226
|
|
|
227
|
+
// One walk's key: its swatch, its name and its length against the reference.
|
|
228
|
+
// A lane shading along the walk shows its scale, the walk's first and last
|
|
229
|
+
// coordinate either side of the bar; where it sits hovers on the row.
|
|
230
|
+
function WalkRow({
|
|
231
|
+
walk: w,
|
|
232
|
+
label,
|
|
233
|
+
referenceDomain,
|
|
234
|
+
referenceName,
|
|
235
|
+
}: {
|
|
236
|
+
walk: LiftedWalk
|
|
237
|
+
label: string
|
|
238
|
+
referenceDomain?: { start: number; end: number }
|
|
239
|
+
referenceName?: string
|
|
240
|
+
}) {
|
|
241
|
+
const delta =
|
|
242
|
+
w.referenceBp === undefined || w.bp === w.referenceBp
|
|
243
|
+
? ''
|
|
244
|
+
: ` ${w.bp > w.referenceBp ? '+' : '−'}${formatBp(Math.abs(w.bp - w.referenceBp))}`
|
|
245
|
+
const ends =
|
|
246
|
+
w.encoding.field === 'progress'
|
|
247
|
+
? w.range
|
|
248
|
+
: w.encoding.field === 'reference'
|
|
249
|
+
? referenceDomain
|
|
250
|
+
: undefined
|
|
251
|
+
const where = w.range
|
|
252
|
+
? `${w.range.contig}:${w.range.start.toLocaleString()}-${w.range.end.toLocaleString()}`
|
|
253
|
+
: w.encoding.field === 'reference' && referenceName
|
|
254
|
+
? referenceName
|
|
255
|
+
: undefined
|
|
256
|
+
const bar = (
|
|
257
|
+
<div
|
|
258
|
+
style={{
|
|
259
|
+
...(ends ? walkBarStyle : walkSwatchStyle),
|
|
260
|
+
background: encodingSwatchCss(w.encoding),
|
|
261
|
+
}}
|
|
262
|
+
/>
|
|
263
|
+
)
|
|
264
|
+
return (
|
|
265
|
+
<div style={walkBlockStyle} title={where}>
|
|
266
|
+
<div style={pathLegendRowStyle}>
|
|
267
|
+
{ends ? null : bar}
|
|
268
|
+
<span>
|
|
269
|
+
<strong>{label}</strong>
|
|
270
|
+
{delta}
|
|
271
|
+
{w.reversedBp > 0 ? `, ${formatBp(w.reversedBp)} reversed` : ''}
|
|
272
|
+
</span>
|
|
273
|
+
</div>
|
|
274
|
+
{ends ? (
|
|
275
|
+
<div style={walkBarRowStyle}>
|
|
276
|
+
<span>{Math.round(ends.start).toLocaleString()}</span>
|
|
277
|
+
{bar}
|
|
278
|
+
<span>{Math.round(ends.end).toLocaleString()}</span>
|
|
279
|
+
</div>
|
|
280
|
+
) : null}
|
|
281
|
+
</div>
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
243
285
|
// The reference-position ramp, as a strip labelled with the interval it runs
|
|
244
286
|
// over. Nothing on screen used to say that red-to-magenta means left-to-right of
|
|
245
287
|
// the cut window, so two tutorials carried that sentence in prose and a reader
|
|
@@ -262,7 +304,8 @@ const ReferenceRampLegend = observer(function ReferenceRampLegend({
|
|
|
262
304
|
}: {
|
|
263
305
|
model: GraphPaneModel
|
|
264
306
|
}) {
|
|
265
|
-
|
|
307
|
+
// under lifted walks each lane states its own scale, and the rest is grey
|
|
308
|
+
const domain = model.walkLift ? undefined : model.referenceRampDomain
|
|
266
309
|
return domain ? (
|
|
267
310
|
<div style={legendBoxStyle} data-testid="graph-ramp-legend">
|
|
268
311
|
<div style={rampStripStyle} />
|
|
@@ -819,6 +862,7 @@ const GraphCanvas = observer(function GraphCanvas({
|
|
|
819
862
|
<LabelLayer model={model} />
|
|
820
863
|
<UnpopButton model={model} />
|
|
821
864
|
<WalkRowsOverlay model={model} />
|
|
865
|
+
<FacetPanels model={model} />
|
|
822
866
|
<Legends model={model} />
|
|
823
867
|
|
|
824
868
|
{ownChrome ? (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react'
|
|
2
2
|
|
|
3
3
|
import { drawReferenceStrip } from '@jbrowse/bandage-core/referenceStrip'
|
|
4
|
+
import { encodingSwatchCss } from '@jbrowse/bandage-core/walkEncoding'
|
|
4
5
|
import { getDpr } from '@jbrowse/render-core/canvas2dUtils'
|
|
5
6
|
import { autorun } from 'mobx'
|
|
6
7
|
import { observer } from 'mobx-react'
|
|
@@ -75,16 +76,27 @@ const legendBoxStyle = {
|
|
|
75
76
|
const legendRowStyle = { display: 'flex', alignItems: 'center', gap: 5 }
|
|
76
77
|
const SWATCH_PX = 18
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
// the strip's own colours: the reference lane's under lifted walks, grey
|
|
80
|
+
// when the reference is not one of them, else the node scheme's ramp
|
|
81
|
+
function StripSwatch({ background }: { background: string }) {
|
|
79
82
|
return (
|
|
80
|
-
<
|
|
81
|
-
<rect x={0} y={0} width={6} height={4} fill="hsl(0, 70%, 50%)" />
|
|
82
|
-
<rect x={6} y={0} width={5} height={4} fill="hsl(120, 70%, 50%)" />
|
|
83
|
-
<rect x={11} y={0} width={7} height={4} fill="hsl(240, 70%, 50%)" />
|
|
84
|
-
</svg>
|
|
83
|
+
<div style={{ width: SWATCH_PX, height: 4, flex: 'none', background }} />
|
|
85
84
|
)
|
|
86
85
|
}
|
|
87
86
|
|
|
87
|
+
const RAMP_SWATCH =
|
|
88
|
+
'linear-gradient(to right, hsl(0, 70%, 50%) 0 33%, hsl(120, 70%, 50%) 33% 61%, hsl(240, 70%, 50%) 61%)'
|
|
89
|
+
|
|
90
|
+
function stripBackground(model: GraphPaneModel) {
|
|
91
|
+
const lift = model.walkLift
|
|
92
|
+
const reference = lift?.walks.find(w => w.reference)
|
|
93
|
+
return reference
|
|
94
|
+
? encodingSwatchCss(reference.encoding)
|
|
95
|
+
: lift
|
|
96
|
+
? 'rgb(160, 160, 160)'
|
|
97
|
+
: RAMP_SWATCH
|
|
98
|
+
}
|
|
99
|
+
|
|
88
100
|
function OverhangSwatch() {
|
|
89
101
|
return (
|
|
90
102
|
<svg width={SWATCH_PX} height={8} style={{ flex: 'none' }}>
|
|
@@ -102,7 +114,7 @@ export const ReferenceStripLegend = observer(function ReferenceStripLegend({
|
|
|
102
114
|
return model.referenceStripShown ? (
|
|
103
115
|
<div style={legendBoxStyle} data-testid="graph-reference-strip-legend">
|
|
104
116
|
<div style={legendRowStyle}>
|
|
105
|
-
<StripSwatch />
|
|
117
|
+
<StripSwatch background={stripBackground(model)} />
|
|
106
118
|
<span>top strip: reference segments at their bp</span>
|
|
107
119
|
</div>
|
|
108
120
|
{model.referenceStripOverhangs ? (
|
|
@@ -2494,17 +2494,17 @@ describe('walk rows', () => {
|
|
|
2494
2494
|
})
|
|
2495
2495
|
await model.loadGFA(WALKS_GFA, 'walks')
|
|
2496
2496
|
model.liftWalks(['B#1#ctg', 'GRCh38#0#chr1'])
|
|
2497
|
-
model.setWalkColor('B#1#ctg', { field: '
|
|
2498
|
-
model.setWalkColor('B#1#ctg', { scheme: '
|
|
2497
|
+
model.setWalkColor('B#1#ctg', { field: 'walk' })
|
|
2498
|
+
model.setWalkColor('B#1#ctg', { scheme: 'green' })
|
|
2499
2499
|
model.toggleWalk('A#1#ctg')
|
|
2500
2500
|
expect(model.walkLift!.walks.map(w => [w.name, w.encoding])).toEqual([
|
|
2501
|
-
['GRCh38#0#chr1', { field: '
|
|
2502
|
-
['B#1#ctg', { field: '
|
|
2503
|
-
['A#1#ctg', { field: '
|
|
2501
|
+
['GRCh38#0#chr1', { field: 'walk', scheme: 'grey' }],
|
|
2502
|
+
['B#1#ctg', { field: 'walk', scheme: 'green' }],
|
|
2503
|
+
['A#1#ctg', { field: 'walk', scheme: 'red' }],
|
|
2504
2504
|
])
|
|
2505
|
-
//
|
|
2505
|
+
// lifted together, each walk is one flat colour
|
|
2506
2506
|
const ref = model.walkLift!.walks[0]!
|
|
2507
|
-
expect(new Set(ref.colors.values()).size).toBe(
|
|
2507
|
+
expect(new Set(ref.colors.values()).size).toBe(1)
|
|
2508
2508
|
interface Item {
|
|
2509
2509
|
label?: string
|
|
2510
2510
|
checked?: boolean
|
|
@@ -2515,16 +2515,42 @@ describe('walk rows', () => {
|
|
|
2515
2515
|
)!
|
|
2516
2516
|
const colour = walkMenu.subMenu!.find(item => item.label === 'Colour B#1')!
|
|
2517
2517
|
expect(
|
|
2518
|
-
colour.subMenu
|
|
2519
|
-
).toEqual(['
|
|
2518
|
+
colour.subMenu!.filter(item => item.checked).map(item => item.label),
|
|
2519
|
+
).toEqual(['One colour for the walk', 'Lime to forest'])
|
|
2520
|
+
// the rainbow is offered for reference position only
|
|
2521
|
+
expect(colour.subMenu!.some(item => item.label === 'Rainbow')).toBe(false)
|
|
2520
2522
|
model.liftWalks(['B#1#ctg'])
|
|
2521
2523
|
expect(model.walkLayers).toEqual([
|
|
2522
|
-
{ walk: 'B#1#ctg', color: { field: '
|
|
2524
|
+
{ walk: 'B#1#ctg', color: { field: 'walk', scheme: 'green' } },
|
|
2523
2525
|
])
|
|
2524
2526
|
model.toggleWalk('B#1#ctg')
|
|
2525
2527
|
expect(model.walkLift).toBeUndefined()
|
|
2526
2528
|
})
|
|
2527
2529
|
|
|
2530
|
+
test('faceted by walk, the pane draws one panel per lifted walk on one scale', async () => {
|
|
2531
|
+
rpcRespond()
|
|
2532
|
+
const model = stateModelFactory().create({
|
|
2533
|
+
type: 'GraphGenomeView',
|
|
2534
|
+
layoutMode: 'auto',
|
|
2535
|
+
})
|
|
2536
|
+
await model.loadGFA(WALKS_GFA, 'walks')
|
|
2537
|
+
model.liftWalks(['GRCh38#0#chr1'])
|
|
2538
|
+
model.setFacet('walk')
|
|
2539
|
+
// one walk needs no facet
|
|
2540
|
+
expect(model.facetPanels).toBeUndefined()
|
|
2541
|
+
model.liftWalks(['GRCh38#0#chr1', 'B#1#ctg', 'A#1#ctg'])
|
|
2542
|
+
expect(
|
|
2543
|
+
model.facetPanels!.map(p => [p.walks[0]!.name, p.walks[0]!.encoding]),
|
|
2544
|
+
).toEqual([
|
|
2545
|
+
['GRCh38#0#chr1', { field: 'progress', scheme: 'red' }],
|
|
2546
|
+
['B#1#ctg', { field: 'progress', scheme: 'red' }],
|
|
2547
|
+
['A#1#ctg', { field: 'progress', scheme: 'red' }],
|
|
2548
|
+
])
|
|
2549
|
+
expect(model.facetGrid!.columns).toBe(3)
|
|
2550
|
+
model.setFacet('none')
|
|
2551
|
+
expect(model.facetPanels).toBeUndefined()
|
|
2552
|
+
})
|
|
2553
|
+
|
|
2528
2554
|
test('a session naming the one walk the old field lifted opens with none', () => {
|
|
2529
2555
|
const model = stateModelFactory().create({
|
|
2530
2556
|
type: 'GraphGenomeView',
|