jbrowse-plugin-graphgenomeviewer 4.0.21 → 4.0.23

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 (33) hide show
  1. package/README.md +5 -2
  2. package/dist/chunks/{GraphGenomeView-CLUMXH4K.js → GraphGenomeView-QXKHSZQJ.js} +2 -2
  3. package/dist/chunks/{LinearGraphDisplay-4CMVCEM5.js → LinearGraphDisplay-GMMLJWQ6.js} +2 -2
  4. package/dist/chunks/chunk-75CO6QJZ.js +2 -0
  5. package/dist/chunks/chunk-75CO6QJZ.js.map +7 -0
  6. package/dist/chunks/chunk-NSXARD3R.js +3 -0
  7. package/dist/chunks/chunk-NSXARD3R.js.map +7 -0
  8. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +2 -2
  9. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +3 -3
  10. package/package.json +2 -2
  11. package/src/GraphGenomeView/components/BubbleHalos.tsx +2 -10
  12. package/src/GraphGenomeView/components/FacetPanels.tsx +159 -0
  13. package/src/GraphGenomeView/components/GenePins.tsx +12 -5
  14. package/src/GraphGenomeView/components/GraphCanvas.tsx +45 -318
  15. package/src/GraphGenomeView/components/GraphToolbar.tsx +6 -2
  16. package/src/GraphGenomeView/components/ReferenceStripOverlay.tsx +21 -16
  17. package/src/GraphGenomeView/components/TubeMapOverlay.tsx +2 -9
  18. package/src/GraphGenomeView/components/UnpopButton.tsx +1 -2
  19. package/src/GraphGenomeView/components/WalkKey.tsx +52 -0
  20. package/src/GraphGenomeView/components/WalkRowsOverlay.tsx +1 -9
  21. package/src/GraphGenomeView/components/WalkSelect.tsx +2 -2
  22. package/src/GraphGenomeView/components/legendStyles.ts +11 -0
  23. package/src/GraphGenomeView/components/usePaneGestures.ts +274 -0
  24. package/src/GraphGenomeView/model.test.ts +102 -10
  25. package/src/GraphGenomeView/model.ts +318 -119
  26. package/src/LinearGraphDisplay/model.ts +2 -0
  27. package/src/version.ts +1 -1
  28. package/dist/chunks/chunk-3FGJLRG2.js +0 -2
  29. package/dist/chunks/chunk-3FGJLRG2.js.map +0 -7
  30. package/dist/chunks/chunk-3SZJ4AFI.js +0 -3
  31. package/dist/chunks/chunk-3SZJ4AFI.js.map +0 -7
  32. /package/dist/chunks/{GraphGenomeView-CLUMXH4K.js.map → GraphGenomeView-QXKHSZQJ.js.map} +0 -0
  33. /package/dist/chunks/{LinearGraphDisplay-4CMVCEM5.js.map → LinearGraphDisplay-GMMLJWQ6.js.map} +0 -0
@@ -48,7 +48,7 @@ const GraphToolbar = observer(function GraphToolbar({
48
48
  <IconButton
49
49
  size="small"
50
50
  onClick={() => {
51
- model.zoom(1.5, model.paneWidth / 2, model.canvasHeight / 2)
51
+ model.zoom(1.5, model.viewBox.width / 2, model.viewBox.height / 2)
52
52
  }}
53
53
  >
54
54
  <ZoomInIcon />
@@ -58,7 +58,11 @@ const GraphToolbar = observer(function GraphToolbar({
58
58
  <IconButton
59
59
  size="small"
60
60
  onClick={() => {
61
- model.zoom(1 / 1.5, model.paneWidth / 2, model.canvasHeight / 2)
61
+ model.zoom(
62
+ 1 / 1.5,
63
+ model.viewBox.width / 2,
64
+ model.viewBox.height / 2,
65
+ )
62
66
  }}
63
67
  >
64
68
  <ZoomOutIcon />
@@ -1,10 +1,13 @@
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'
7
8
 
9
+ import { legendBoxStyle, legendRowStyle } from './legendStyles'
10
+
8
11
  import type { GraphPaneModel } from '../model'
9
12
 
10
13
  const canvasStyle = {
@@ -64,27 +67,29 @@ const ReferenceStripOverlay = observer(function ReferenceStripOverlay({
64
67
 
65
68
  export default ReferenceStripOverlay
66
69
 
67
- const legendBoxStyle = {
68
- background: 'rgba(255,255,255,0.82)',
69
- padding: '4px 6px',
70
- borderRadius: 3,
71
- fontSize: 11,
72
- lineHeight: '15px',
73
- whiteSpace: 'nowrap' as const,
74
- }
75
- const legendRowStyle = { display: 'flex', alignItems: 'center', gap: 5 }
76
70
  const SWATCH_PX = 18
77
71
 
78
- function StripSwatch() {
72
+ // the strip's own colours: the reference lane's under lifted walks, grey
73
+ // when the reference is not one of them, else the node scheme's ramp
74
+ function StripSwatch({ background }: { background: string }) {
79
75
  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>
76
+ <div style={{ width: SWATCH_PX, height: 4, flex: 'none', background }} />
85
77
  )
86
78
  }
87
79
 
80
+ const RAMP_SWATCH =
81
+ 'linear-gradient(to right, hsl(0, 70%, 50%) 0 33%, hsl(120, 70%, 50%) 33% 61%, hsl(240, 70%, 50%) 61%)'
82
+
83
+ function stripBackground(model: GraphPaneModel) {
84
+ const lift = model.walkLift
85
+ const reference = lift?.walks.find(w => w.reference)
86
+ return reference
87
+ ? encodingSwatchCss(reference.encoding)
88
+ : lift
89
+ ? 'rgb(160, 160, 160)'
90
+ : RAMP_SWATCH
91
+ }
92
+
88
93
  function OverhangSwatch() {
89
94
  return (
90
95
  <svg width={SWATCH_PX} height={8} style={{ flex: 'none' }}>
@@ -102,7 +107,7 @@ export const ReferenceStripLegend = observer(function ReferenceStripLegend({
102
107
  return model.referenceStripShown ? (
103
108
  <div style={legendBoxStyle} data-testid="graph-reference-strip-legend">
104
109
  <div style={legendRowStyle}>
105
- <StripSwatch />
110
+ <StripSwatch background={stripBackground(model)} />
106
111
  <span>top strip: reference segments at their bp</span>
107
112
  </div>
108
113
  {model.referenceStripOverhangs ? (
@@ -18,6 +18,8 @@ import { getDpr } from '@jbrowse/render-core/canvas2dUtils'
18
18
  import { autorun } from 'mobx'
19
19
  import { observer } from 'mobx-react'
20
20
 
21
+ import { legendBoxStyle, legendRowStyle } from './legendStyles'
22
+
21
23
  import type { GraphPaneModel } from '../model'
22
24
 
23
25
  const canvasStyle = {
@@ -109,15 +111,6 @@ const TubeMapOverlay = observer(function TubeMapOverlay({
109
111
 
110
112
  export default TubeMapOverlay
111
113
 
112
- const legendBoxStyle = {
113
- background: 'rgba(255,255,255,0.82)',
114
- padding: '4px 6px',
115
- borderRadius: 3,
116
- fontSize: 11,
117
- lineHeight: '15px',
118
- whiteSpace: 'nowrap' as const,
119
- }
120
- const legendRowStyle = { display: 'flex', alignItems: 'center', gap: 5 }
121
114
  const SWATCH_PX = 18
122
115
 
123
116
  // Two boxes' brackets, as the ruler draws them under every box
@@ -6,7 +6,6 @@ import type { GraphPaneModel } from '../model'
6
6
  const style = {
7
7
  position: 'absolute' as const,
8
8
  left: 8,
9
- top: 8,
10
9
  zIndex: 5,
11
10
  background: 'rgba(255,255,255,0.9)',
12
11
  textTransform: 'none' as const,
@@ -22,7 +21,7 @@ const UnpopButton = observer(function UnpopButton({
22
21
  <Button
23
22
  size="small"
24
23
  variant="outlined"
25
- style={style}
24
+ style={{ ...style, ...(model.facetPanels ? { bottom: 8 } : { top: 8 }) }}
26
25
  data-testid="graph-unpop-bubble"
27
26
  onClick={() => {
28
27
  void model.unpopBubble()
@@ -0,0 +1,52 @@
1
+ import { encodingSwatchCss } from '@jbrowse/bandage-core/walkEncoding'
2
+ import { walkKey } from '@jbrowse/bandage-core/walkKey'
3
+
4
+ import { legendRowStyle } from './legendStyles'
5
+
6
+ import type { LiftedWalk } from '@jbrowse/bandage-core/walkHighlight'
7
+
8
+ export const walkSwatchStyle = {
9
+ width: 18,
10
+ height: 8,
11
+ borderRadius: 2,
12
+ flex: 'none',
13
+ }
14
+ const walkBlockStyle = { marginBottom: 2 }
15
+ const walkBarStyle = { ...walkSwatchStyle, width: 48 }
16
+
17
+ // One walk's key: a swatch, or a short bar of the scale its lane shades by,
18
+ // then its name; see walkKey for the words
19
+ export default function WalkKey({
20
+ walk,
21
+ label,
22
+ reference,
23
+ hint,
24
+ }: {
25
+ walk: LiftedWalk
26
+ label: string
27
+ reference?: { name?: string; start: number; end: number }
28
+ hint?: string
29
+ }) {
30
+ const key = walkKey(walk, reference)
31
+ return (
32
+ <div
33
+ style={walkBlockStyle}
34
+ title={[key.hover, hint].filter(Boolean).join(' · ') || undefined}
35
+ >
36
+ <div style={legendRowStyle}>
37
+ <div
38
+ style={{
39
+ ...(key.shades ? walkBarStyle : walkSwatchStyle),
40
+ background: encodingSwatchCss(walk.encoding),
41
+ }}
42
+ />
43
+ <span>
44
+ <strong>{label}</strong>
45
+ {key.delta}
46
+ {key.reversed}
47
+ </span>
48
+ </div>
49
+ {key.scale ? <div>{key.scale}</div> : null}
50
+ </div>
51
+ )
52
+ }
@@ -5,6 +5,7 @@ import { LABEL_CHAR_PX } from '@jbrowse/bandage-core/overlayLabels'
5
5
  import { REFERENCE_RAMP_ALT_CSS } from '@jbrowse/bandage-core/renderer/GeometryBuilder'
6
6
  import { observer } from 'mobx-react'
7
7
 
8
+ import { legendBoxStyle, legendRowStyle } from './legendStyles'
8
9
  import { RAMP_GRADIENT_CSS, rampHueCss, rampStops } from './referenceRampCss'
9
10
  import { CALL_TOLERANCE } from '../repeats/walkCalls'
10
11
 
@@ -44,15 +45,6 @@ const DISAGREES = '#c62828'
44
45
  const OFF_REFERENCE = '#8e3fbf'
45
46
  const BAR_PX = 12
46
47
 
47
- const legendBoxStyle = {
48
- background: 'rgba(255,255,255,0.82)',
49
- padding: '4px 6px',
50
- borderRadius: 3,
51
- fontSize: 11,
52
- lineHeight: '15px',
53
- whiteSpace: 'nowrap' as const,
54
- }
55
- const legendRowStyle = { display: 'flex', alignItems: 'center', gap: 5 }
56
48
  const swatchStyle = { width: 18, height: BAR_PX - 4, borderRadius: 2 }
57
49
  const tickSwatchStyle = {
58
50
  ...swatchStyle,
@@ -20,7 +20,7 @@ const useStyles = makeStyles()({
20
20
 
21
21
  // Which walks to lift out of the drawing: Bandage's path highlight, for as many
22
22
  // of the haplotypes a GBZ cut carries as the reader picks. Absent from a graph
23
- // with no walks.
23
+ // with no walks, and from a tube map, whose tubes are the walks.
24
24
  const WalkSelect = observer(function WalkSelect({
25
25
  model,
26
26
  }: {
@@ -28,7 +28,7 @@ const WalkSelect = observer(function WalkSelect({
28
28
  }) {
29
29
  const { classes } = useStyles()
30
30
  const { walkChoices } = model
31
- if (walkChoices.length === 0) {
31
+ if (walkChoices.length === 0 || !model.liftsWalks) {
32
32
  return null
33
33
  }
34
34
  const names = new Set(walkChoices.map(c => c.name))
@@ -0,0 +1,11 @@
1
+ // A key's box over the drawing, and one row in it: a swatch beside its words
2
+ export const legendBoxStyle = {
3
+ background: 'rgba(255,255,255,0.82)',
4
+ padding: '4px 6px',
5
+ borderRadius: 3,
6
+ fontSize: 11,
7
+ lineHeight: '15px',
8
+ whiteSpace: 'nowrap' as const,
9
+ }
10
+
11
+ export const legendRowStyle = { display: 'flex', alignItems: 'center', gap: 5 }
@@ -0,0 +1,274 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+
3
+ import {
4
+ findHoveredEdge,
5
+ findHoveredNode,
6
+ } from '@jbrowse/bandage-core/util/hitDetection'
7
+ import { wheelZoomFactor } from '@jbrowse/bandage-core/util/wheelZoom'
8
+ import { isAlive } from '@jbrowse/mobx-state-tree'
9
+
10
+ import type { GraphPaneModel } from '../model'
11
+
12
+ // wheel events need passive:false to call preventDefault — React registers
13
+ // wheel listeners as passive, so we must add this imperatively
14
+ export function useWheelZoom(
15
+ canvas: HTMLCanvasElement | null | undefined,
16
+ model: GraphPaneModel,
17
+ ) {
18
+ useEffect(() => {
19
+ if (!canvas) {
20
+ return undefined
21
+ }
22
+ const c = canvas
23
+ function handleWheel(e: WheelEvent) {
24
+ if (model.hostPlacesX) {
25
+ return
26
+ }
27
+ if (model.host) {
28
+ e.stopPropagation()
29
+ }
30
+ e.preventDefault()
31
+ const rect = c.getBoundingClientRect()
32
+ model.zoom(
33
+ wheelZoomFactor(e),
34
+ e.clientX - rect.left,
35
+ e.clientY - rect.top,
36
+ )
37
+ }
38
+ c.addEventListener('wheel', handleWheel, { passive: false })
39
+ return () => {
40
+ c.removeEventListener('wheel', handleWheel)
41
+ }
42
+ }, [canvas, model])
43
+ }
44
+
45
+ // The pointer on a canvas drawing the pane's transform: the pane's own, or any
46
+ // facet panel, since the panels share it. Coordinates are read against the
47
+ // canvas the event is on.
48
+ export function usePaneGestures(model: GraphPaneModel) {
49
+ // Where the pointer was last, and whether it has travelled since mousedown —
50
+ // per-gesture scratch that nothing renders from, which is what a ref is for.
51
+ // Whether a drag is in progress is model state (`isPanning`/`draggingNode`),
52
+ // because the cursor renders from it.
53
+ const lastMouseRef = useRef({ x: 0, y: 0 })
54
+ const hasMovedRef = useRef(false)
55
+ // A pan and a hover are applied once per frame, not once per mousemove:
56
+ // mousemove fires in bursts well above the frame rate, and each pan step
57
+ // repainted the canvas and re-placed every overlay label, while each hover
58
+ // step ran both hit indexes. The pending pan is the summed delta; the pending
59
+ // hover is the last pointer position, since only the last one can be right.
60
+ const pendingRef = useRef<{
61
+ frame: number
62
+ pan: { dx: number; dy: number } | null
63
+ hover: { x: number; y: number } | null
64
+ }>({ frame: 0, pan: null, hover: null })
65
+ useEffect(
66
+ () => () => {
67
+ cancelAnimationFrame(pendingRef.current.frame)
68
+ },
69
+ [],
70
+ )
71
+ const [contextNode, setContextNode] = useState<
72
+ { nodeId: string; top: number; left: number } | undefined
73
+ >(undefined)
74
+
75
+ function screenToGraph(screenX: number, screenY: number) {
76
+ return {
77
+ x: (screenX - model.translateX) / model.scaleX,
78
+ y: (screenY - model.translateY) / model.scaleY,
79
+ }
80
+ }
81
+
82
+ function getMouseCoord(e: React.MouseEvent) {
83
+ const rect = (e.currentTarget as HTMLCanvasElement).getBoundingClientRect()
84
+ return screenToGraph(e.clientX - rect.left, e.clientY - rect.top)
85
+ }
86
+
87
+ // The node at a graph coordinate, which mousedown, mousemove and click all
88
+ // need. Takes the coordinate rather than the event so a caller that already
89
+ // has one does not pay for a second getBoundingClientRect.
90
+ function nodeAt(x: number, y: number) {
91
+ const { nodePositions } = model
92
+ const sx = x * model.scaleX + model.translateX
93
+ const sy = y * model.scaleY + model.translateY
94
+ if (model.layoutResult?.tubeMap) {
95
+ return model.tubeMapNodeAt(sx, sy)
96
+ }
97
+ const onStrip = model.referenceStripNodeAt(sx, sy)
98
+ if (onStrip) {
99
+ return onStrip
100
+ }
101
+ return nodePositions
102
+ ? findHoveredNode(
103
+ nodePositions,
104
+ x,
105
+ y,
106
+ model.axisScale,
107
+ model.positionsVersion,
108
+ model.nodeInk,
109
+ )
110
+ : null
111
+ }
112
+
113
+ function onMouseDown(e: React.MouseEvent) {
114
+ if (e.button === 0) {
115
+ hasMovedRef.current = false
116
+ if (model.hostPlacesX) {
117
+ return
118
+ }
119
+ if (model.host) {
120
+ e.stopPropagation()
121
+ }
122
+ const { x, y } = getMouseCoord(e)
123
+ // a tube map's boxes are the layout's, and the strip's are bp, not
124
+ // positions to drag
125
+ const node =
126
+ model.layoutResult?.tubeMap ||
127
+ e.nativeEvent.offsetY < model.referenceStripZonePx
128
+ ? null
129
+ : nodeAt(x, y)
130
+ if (node) {
131
+ model.setDraggingNode(node)
132
+ } else {
133
+ model.setPanning(true)
134
+ }
135
+ lastMouseRef.current = { x: e.clientX, y: e.clientY }
136
+ }
137
+ }
138
+
139
+ function applyPending() {
140
+ const pending = pendingRef.current
141
+ pending.frame = 0
142
+ if (pending.pan) {
143
+ model.setTransform(
144
+ model.scale,
145
+ model.translateX + pending.pan.dx,
146
+ model.translateY + pending.pan.dy,
147
+ )
148
+ pending.pan = null
149
+ }
150
+ if (pending.hover && model.nodePositions && model.graph) {
151
+ const { x, y } = screenToGraph(pending.hover.x, pending.hover.y)
152
+ pending.hover = null
153
+ const node = nodeAt(x, y)
154
+ model.setHoveredNode(node)
155
+ model.setHoveredEdge(
156
+ node || model.layoutResult?.tubeMap
157
+ ? null
158
+ : findHoveredEdge(
159
+ model.nodePositions,
160
+ model.graph,
161
+ x,
162
+ y,
163
+ model.axisScale,
164
+ // resolved, so the hit index bounds the ribbons that are
165
+ // actually drawn — see effectiveDrawPaths. Facet panels draw
166
+ // none.
167
+ model.effectiveDrawPaths && !model.facetPanels,
168
+ model.positionsVersion,
169
+ model.deletionEdgeIndexes,
170
+ model.hiddenEdgeIndexes,
171
+ ),
172
+ )
173
+ }
174
+ }
175
+
176
+ function scheduleFrame() {
177
+ if (!pendingRef.current.frame) {
178
+ pendingRef.current.frame = requestAnimationFrame(applyPending)
179
+ }
180
+ }
181
+
182
+ function dropPending() {
183
+ const pending = pendingRef.current
184
+ cancelAnimationFrame(pending.frame)
185
+ pending.frame = 0
186
+ pending.pan = null
187
+ pending.hover = null
188
+ }
189
+
190
+ function onMouseMove(e: React.MouseEvent) {
191
+ const dx = e.clientX - lastMouseRef.current.x
192
+ const dy = e.clientY - lastMouseRef.current.y
193
+ lastMouseRef.current = { x: e.clientX, y: e.clientY }
194
+
195
+ if (Math.abs(dx) > 1 || Math.abs(dy) > 1) {
196
+ hasMovedRef.current = true
197
+ }
198
+
199
+ const pending = pendingRef.current
200
+ if (model.draggingNode) {
201
+ model.moveNode(model.draggingNode, dx / model.scaleX, dy / model.scaleY)
202
+ } else if (model.isPanning) {
203
+ pending.pan = {
204
+ dx: (pending.pan?.dx ?? 0) + dx,
205
+ dy: (pending.pan?.dy ?? 0) + dy,
206
+ }
207
+ scheduleFrame()
208
+ } else {
209
+ const rect = (
210
+ e.currentTarget as HTMLCanvasElement
211
+ ).getBoundingClientRect()
212
+ pending.hover = { x: e.clientX - rect.left, y: e.clientY - rect.top }
213
+ scheduleFrame()
214
+ }
215
+ }
216
+
217
+ function onMouseUp() {
218
+ model.stopDragging()
219
+ }
220
+
221
+ // also fired by the canvas unmounting under a resting pointer, after the
222
+ // view closing it has destroyed the model
223
+ function onMouseLeave() {
224
+ dropPending()
225
+ if (!isAlive(model)) {
226
+ return
227
+ }
228
+ model.stopDragging()
229
+ model.setHoveredNode(null)
230
+ model.setHoveredEdge(null)
231
+ }
232
+
233
+ // Right-clicking a node is the gesture that asks "where is this?", and until
234
+ // now the graph had no answer: a node named an assembly and an offset in its
235
+ // tags that nothing surfaced. The items come from the model's launch targets,
236
+ // so what is offered is what can actually be opened.
237
+ function onContextMenu(e: React.MouseEvent) {
238
+ const { x, y } = getMouseCoord(e)
239
+ const node = nodeAt(x, y)
240
+ if (node) {
241
+ e.preventDefault()
242
+ setContextNode({ nodeId: node, top: e.clientY, left: e.clientX })
243
+ }
244
+ }
245
+
246
+ function onClick(e: React.MouseEvent) {
247
+ // a click that ended a drag selects nothing
248
+ if (!hasMovedRef.current) {
249
+ const { x, y } = getMouseCoord(e)
250
+ const node = nodeAt(x, y)
251
+ model.setSelectedNode(node)
252
+ if (node) {
253
+ model.showNodeDetails(node)
254
+ }
255
+ }
256
+ }
257
+
258
+ return {
259
+ handlers: {
260
+ onMouseDown,
261
+ onMouseMove,
262
+ onMouseUp,
263
+ onMouseLeave,
264
+ onClick,
265
+ onContextMenu,
266
+ },
267
+ contextNode,
268
+ closeContextMenu: () => {
269
+ setContextNode(undefined)
270
+ },
271
+ }
272
+ }
273
+
274
+ export type PaneHandlers = ReturnType<typeof usePaneGestures>['handlers']
@@ -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,108 @@ 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
+ // a row layout's x is bp, so its panels stack full width
2550
+ expect(model.facetGrid!.columns).toBe(1)
2551
+ model.setFacetColumns(2)
2552
+ expect(model.facetGrid!.columns).toBe(2)
2553
+ model.setFacet('none')
2554
+ expect(model.facetPanels).toBeUndefined()
2555
+ })
2556
+
2557
+ test("faceted, the transform is each panel's and the menu keys what the panels draw", async () => {
2558
+ rpcRespond()
2559
+ const model = stateModelFactory().create({
2560
+ type: 'GraphGenomeView',
2561
+ layoutMode: 'auto',
2562
+ })
2563
+ await model.loadGFA(WALKS_GFA, 'walks')
2564
+ model.startRenderingBackend(fakeRenderer())
2565
+ model.liftWalks(['GRCh38#0#chr1', 'B#1#ctg'])
2566
+ const whole = model.scale
2567
+ model.setFacet('walk')
2568
+ const grid = model.facetGrid!
2569
+ expect(model.viewBox).toEqual({ width: grid.width, height: grid.height })
2570
+ expect(model.canvasHeight).toBe(Math.max(160, grid.total))
2571
+ expect(model.scale).toBeCloseTo(grid.scale, 10)
2572
+ expect(model.scale).not.toBeCloseTo(whole, 10)
2573
+ interface Item {
2574
+ label?: string
2575
+ checked?: boolean
2576
+ subMenu?: Item[]
2577
+ }
2578
+ const walkMenu = (model.graphMenuItems() as Item[]).find(
2579
+ item => item.label === 'Walk',
2580
+ )!
2581
+ const colour = walkMenu.subMenu!.find(item => item.label === 'Colour B#1')!
2582
+ expect(
2583
+ colour.subMenu!.filter(item => item.checked).map(item => item.label),
2584
+ ).toEqual(['Progress along the walk', 'Yellow to red'])
2585
+ const columns = walkMenu.subMenu!.find(item => item.label === 'Columns')!
2586
+ expect(columns.subMenu!.map(item => item.label)).toEqual(['Auto', '1', '2'])
2587
+ })
2588
+
2589
+ test('a tube map lifts no walk: its tubes are the walks', async () => {
2590
+ rpcRespond()
2591
+ const model = stateModelFactory().create({
2592
+ type: 'GraphGenomeView',
2593
+ layoutMode: 'tubemap',
2594
+ walkLayers: [{ walk: 'GRCh38#0#chr1' }, { walk: 'B#1#ctg' }],
2595
+ facet: 'walk',
2596
+ })
2597
+ await model.loadGFA(WALKS_GFA, 'walks')
2598
+ expect(model.walkLift).toBeUndefined()
2599
+ expect(model.facetPanels).toBeUndefined()
2600
+ expect(
2601
+ (model.graphMenuItems() as { label?: string }[]).some(
2602
+ item => item.label === 'Walk',
2603
+ ),
2604
+ ).toBe(false)
2605
+ })
2606
+
2607
+ test('a lane reads the reference ramp only when it is coloured by it', async () => {
2608
+ rpcRespond()
2609
+ const model = stateModelFactory().create({
2610
+ type: 'GraphGenomeView',
2611
+ layoutMode: 'auto',
2612
+ })
2613
+ await model.loadGFA(WALKS_GFA, 'walks')
2614
+ model.liftWalks(['B#1#ctg'])
2615
+ expect(model.walkRamp).toBeUndefined()
2616
+ model.setWalkColor('B#1#ctg', { field: 'reference' })
2617
+ expect(model.walkLift!.referenceDomain).toBeDefined()
2618
+ })
2619
+
2528
2620
  test('a session naming the one walk the old field lifted opens with none', () => {
2529
2621
  const model = stateModelFactory().create({
2530
2622
  type: 'GraphGenomeView',