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.
Files changed (42) hide show
  1. package/README.md +5 -2
  2. package/dist/chunks/AddTrackWorkflow-NKKGI6SM.js +2 -0
  3. package/dist/chunks/{AddTrackWorkflow-WYRVQYH2.js.map → AddTrackWorkflow-NKKGI6SM.js.map} +1 -1
  4. package/dist/chunks/{GbzBaseSyntenyAdapter-7ZKFAEJG.js → GbzBaseSyntenyAdapter-HNETLUW4.js} +2 -2
  5. package/dist/chunks/{GraphGenomeView-CLUMXH4K.js → GraphGenomeView-GMHRO7E4.js} +2 -2
  6. package/dist/chunks/{GraphGenomeView-CLUMXH4K.js.map → GraphGenomeView-GMHRO7E4.js.map} +1 -1
  7. package/dist/chunks/LinearGraphDisplay-I2OOPQKB.js +2 -0
  8. package/dist/chunks/{LinearGraphDisplay-4CMVCEM5.js.map → LinearGraphDisplay-I2OOPQKB.js.map} +1 -1
  9. package/dist/chunks/{MinigraphBubbleAdapter-NRDD66AK.js → MinigraphBubbleAdapter-FIFZYJ27.js} +2 -2
  10. package/dist/chunks/chunk-2PYVZZ3R.js +3 -0
  11. package/dist/chunks/chunk-2PYVZZ3R.js.map +7 -0
  12. package/dist/chunks/chunk-D7XFR2IV.js +2 -0
  13. package/dist/chunks/chunk-D7XFR2IV.js.map +7 -0
  14. package/dist/chunks/chunk-DMYUVGGI.js +3 -0
  15. package/dist/chunks/{chunk-QSH263Y6.js.map → chunk-DMYUVGGI.js.map} +4 -4
  16. package/dist/chunks/chunk-JXSDDOKD.js +3 -0
  17. package/dist/chunks/chunk-JXSDDOKD.js.map +7 -0
  18. package/dist/chunks/chunk-OSTJHAAK.js +2 -0
  19. package/dist/chunks/chunk-OSTJHAAK.js.map +7 -0
  20. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +1 -2
  21. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
  22. package/package.json +2 -2
  23. package/src/GraphGenomeView/components/FacetPanels.tsx +204 -0
  24. package/src/GraphGenomeView/components/GenePins.tsx +12 -5
  25. package/src/GraphGenomeView/components/GraphCanvas.tsx +86 -42
  26. package/src/GraphGenomeView/components/ReferenceStripOverlay.tsx +19 -7
  27. package/src/GraphGenomeView/model.test.ts +36 -10
  28. package/src/GraphGenomeView/model.ts +130 -9
  29. package/src/version.ts +1 -1
  30. package/dist/chunks/AddTrackWorkflow-WYRVQYH2.js +0 -2
  31. package/dist/chunks/LinearGraphDisplay-4CMVCEM5.js +0 -2
  32. package/dist/chunks/chunk-3FGJLRG2.js +0 -2
  33. package/dist/chunks/chunk-3FGJLRG2.js.map +0 -7
  34. package/dist/chunks/chunk-3SZJ4AFI.js +0 -3
  35. package/dist/chunks/chunk-3SZJ4AFI.js.map +0 -7
  36. package/dist/chunks/chunk-QSH263Y6.js +0 -2
  37. package/dist/chunks/chunk-SCMIJAAX.js +0 -3
  38. package/dist/chunks/chunk-SCMIJAAX.js.map +0 -7
  39. package/dist/chunks/chunk-XK7OPCRQ.js +0 -2
  40. package/dist/chunks/chunk-XK7OPCRQ.js.map +0 -7
  41. /package/dist/chunks/{GbzBaseSyntenyAdapter-7ZKFAEJG.js.map → GbzBaseSyntenyAdapter-HNETLUW4.js.map} +0 -0
  42. /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.21",
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.16"
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={contigThickness * 0.55}
57
- strokeLinecap="round"
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 walkSwatchStyle = { width: 26, height: 8, borderRadius: 2, flex: 'none' }
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
- if (!lift) {
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
- 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>
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
- paler nodes: on none of{' '}
237
- {lift.walks.length > 1 ? 'these walks' : 'this walk'}
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
- const domain = model.referenceRampDomain
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
- function StripSwatch() {
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
- <svg width={SWATCH_PX} height={4} style={{ flex: 'none' }}>
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: 'strand' })
2498
- model.setWalkColor('B#1#ctg', { scheme: 'greens' })
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: 'reference', scheme: 'rainbow' }],
2502
- ['B#1#ctg', { field: 'strand', scheme: 'greens' }],
2503
- ['A#1#ctg', { field: 'progress', scheme: 'reds' }],
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
- // the reference is coloured by position whatever the node colour scheme
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(2)
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.filter(item => item.checked).map(item => item.label),
2519
- ).toEqual(['Strand against the reference', 'Greens'])
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: 'strand', scheme: 'greens' } },
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',