jbrowse-plugin-graphgenomeviewer 4.0.13 → 4.0.15
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 -1
- package/dist/chunks/{GbzBaseSyntenyAdapter-Y5NBXK7P.js → GbzBaseSyntenyAdapter-3ZTHTTHA.js} +2 -2
- package/dist/chunks/{GraphGenomeView-TTQEJJQD.js → GraphGenomeView-5SMMNFD4.js} +2 -2
- package/dist/chunks/{GraphTrackSettingsDialog-3A2PKZRI.js → GraphTrackSettingsDialog-2QV6TPX7.js} +2 -2
- package/dist/chunks/{LinearGraphDisplay-MJ5XZFE5.js → LinearGraphDisplay-BEOEIUGA.js} +2 -2
- package/dist/chunks/{MinigraphBubbleAdapter-APC7JYOM.js → MinigraphBubbleAdapter-NRDD66AK.js} +2 -2
- package/dist/chunks/{RgfaTabixAdapter-4E365PXO.js → RgfaTabixAdapter-NQEERXXI.js} +2 -2
- package/dist/chunks/{chunk-5DP7IXD3.js → chunk-33VHHB43.js} +2 -2
- package/dist/chunks/{chunk-LA7MQS33.js → chunk-5E7TVPCI.js} +2 -2
- package/dist/chunks/chunk-IUIIYSGF.js +2 -0
- package/dist/chunks/{chunk-M4YEAXSR.js.map → chunk-IUIIYSGF.js.map} +3 -3
- package/dist/chunks/chunk-JXAJ7YLT.js +2 -0
- package/dist/chunks/chunk-JXAJ7YLT.js.map +7 -0
- package/dist/chunks/chunk-KULNKXPT.js +2 -0
- package/dist/chunks/chunk-KULNKXPT.js.map +7 -0
- package/dist/chunks/chunk-QSH263Y6.js +2 -0
- package/dist/chunks/chunk-QSH263Y6.js.map +7 -0
- package/dist/chunks/chunk-UQEPPJEO.js +2 -0
- package/dist/chunks/{chunk-VAGPKHHS.js.map → chunk-UQEPPJEO.js.map} +3 -3
- package/dist/chunks/chunk-UQN2NWT5.js +4 -0
- package/dist/chunks/chunk-UQN2NWT5.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/GraphGenomeView/components/BubbleHalos.tsx +67 -3
- package/src/GraphGenomeView/components/BubbleOverlay.tsx +10 -0
- package/src/GraphGenomeView/components/GraphCanvas.tsx +46 -9
- package/src/GraphGenomeView/components/LabelChip.tsx +9 -1
- package/src/GraphGenomeView/components/LabelLayer.tsx +7 -0
- package/src/GraphGenomeView/components/ReferenceStripOverlay.tsx +116 -0
- package/src/GraphGenomeView/components/TubeMapOverlay.tsx +109 -41
- package/src/GraphGenomeView/model.test.ts +3 -0
- package/src/GraphGenomeView/model.ts +204 -43
- package/src/LinearGraphDisplay/model.ts +2 -0
- package/src/version.ts +1 -1
- package/dist/chunks/chunk-CJ56O2VG.js +0 -2
- package/dist/chunks/chunk-CJ56O2VG.js.map +0 -7
- package/dist/chunks/chunk-IFRDWMQ2.js +0 -2
- package/dist/chunks/chunk-IFRDWMQ2.js.map +0 -7
- package/dist/chunks/chunk-M4YEAXSR.js +0 -2
- package/dist/chunks/chunk-MUK3KYB4.js +0 -2
- package/dist/chunks/chunk-MUK3KYB4.js.map +0 -7
- package/dist/chunks/chunk-VAGPKHHS.js +0 -2
- package/dist/chunks/chunk-XDOIZTY6.js +0 -4
- package/dist/chunks/chunk-XDOIZTY6.js.map +0 -7
- /package/dist/chunks/{GbzBaseSyntenyAdapter-Y5NBXK7P.js.map → GbzBaseSyntenyAdapter-3ZTHTTHA.js.map} +0 -0
- /package/dist/chunks/{GraphGenomeView-TTQEJJQD.js.map → GraphGenomeView-5SMMNFD4.js.map} +0 -0
- /package/dist/chunks/{GraphTrackSettingsDialog-3A2PKZRI.js.map → GraphTrackSettingsDialog-2QV6TPX7.js.map} +0 -0
- /package/dist/chunks/{LinearGraphDisplay-MJ5XZFE5.js.map → LinearGraphDisplay-BEOEIUGA.js.map} +0 -0
- /package/dist/chunks/{MinigraphBubbleAdapter-APC7JYOM.js.map → MinigraphBubbleAdapter-NRDD66AK.js.map} +0 -0
- /package/dist/chunks/{RgfaTabixAdapter-4E365PXO.js.map → RgfaTabixAdapter-NQEERXXI.js.map} +0 -0
- /package/dist/chunks/{chunk-5DP7IXD3.js.map → chunk-33VHHB43.js.map} +0 -0
- /package/dist/chunks/{chunk-LA7MQS33.js.map → chunk-5E7TVPCI.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.15",
|
|
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.10"
|
|
80
80
|
},
|
|
81
81
|
"peerDependencies": {
|
|
82
82
|
"react": ">=18.0.0",
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sameBubble } from '@jbrowse/bandage-core/bubbles/bubbleLine'
|
|
2
|
+
import {
|
|
3
|
+
BUBBLE_KIND_COLORS,
|
|
4
|
+
BUBBLE_KIND_NAMES,
|
|
5
|
+
} from '@jbrowse/bandage-core/bubbles/classifyBubble'
|
|
2
6
|
import { HALO_FACTOR } from '@jbrowse/bandage-core/labelLayout'
|
|
3
7
|
import { observer } from 'mobx-react'
|
|
4
8
|
|
|
@@ -24,7 +28,7 @@ const BubbleHalos = observer(function BubbleHalos({
|
|
|
24
28
|
}: {
|
|
25
29
|
model: GraphPaneModel
|
|
26
30
|
}) {
|
|
27
|
-
const { bubbleHalos, walkHighlight } = model
|
|
31
|
+
const { bubbleHalos, walkHighlight, hoveredBubble } = model
|
|
28
32
|
if (bubbleHalos.length === 0) {
|
|
29
33
|
return null
|
|
30
34
|
}
|
|
@@ -60,7 +64,13 @@ const BubbleHalos = observer(function BubbleHalos({
|
|
|
60
64
|
d={h.path}
|
|
61
65
|
fill="none"
|
|
62
66
|
stroke={BUBBLE_KIND_COLORS[h.kind]}
|
|
63
|
-
strokeOpacity={
|
|
67
|
+
strokeOpacity={
|
|
68
|
+
hoveredBubble && sameBubble(h.bubble, hoveredBubble)
|
|
69
|
+
? 0.45
|
|
70
|
+
: dimmedBubble(h)
|
|
71
|
+
? 0.06
|
|
72
|
+
: 0.22
|
|
73
|
+
}
|
|
64
74
|
strokeWidth={halo}
|
|
65
75
|
strokeLinecap="round"
|
|
66
76
|
strokeLinejoin="round"
|
|
@@ -73,3 +83,57 @@ const BubbleHalos = observer(function BubbleHalos({
|
|
|
73
83
|
})
|
|
74
84
|
|
|
75
85
|
export default BubbleHalos
|
|
86
|
+
|
|
87
|
+
const legendBoxStyle = {
|
|
88
|
+
background: 'rgba(255,255,255,0.82)',
|
|
89
|
+
padding: '4px 6px',
|
|
90
|
+
borderRadius: 3,
|
|
91
|
+
fontSize: 11,
|
|
92
|
+
lineHeight: '15px',
|
|
93
|
+
whiteSpace: 'nowrap' as const,
|
|
94
|
+
}
|
|
95
|
+
const legendRowStyle = { display: 'flex', alignItems: 'center', gap: 5 }
|
|
96
|
+
|
|
97
|
+
function HaloSwatch({ color }: { color: string }) {
|
|
98
|
+
return (
|
|
99
|
+
<svg width={18} height={10} style={{ flex: 'none' }}>
|
|
100
|
+
<line
|
|
101
|
+
x1={5}
|
|
102
|
+
y1={5}
|
|
103
|
+
x2={13}
|
|
104
|
+
y2={5}
|
|
105
|
+
stroke={color}
|
|
106
|
+
strokeOpacity={0.35}
|
|
107
|
+
strokeWidth={8}
|
|
108
|
+
strokeLinecap="round"
|
|
109
|
+
/>
|
|
110
|
+
</svg>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// One row per halo colour on screen, naming the kinds it stands for
|
|
115
|
+
export const HaloLegend = observer(function HaloLegend({
|
|
116
|
+
model,
|
|
117
|
+
}: {
|
|
118
|
+
model: GraphPaneModel
|
|
119
|
+
}) {
|
|
120
|
+
const kindsByColor = new Map<string, Set<string>>()
|
|
121
|
+
for (const h of model.bubbleHalos) {
|
|
122
|
+
if (!h.whole) {
|
|
123
|
+
const color = BUBBLE_KIND_COLORS[h.kind]
|
|
124
|
+
const kinds = kindsByColor.get(color) ?? new Set<string>()
|
|
125
|
+
kinds.add(BUBBLE_KIND_NAMES[h.kind])
|
|
126
|
+
kindsByColor.set(color, kinds)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return kindsByColor.size > 0 ? (
|
|
130
|
+
<div style={legendBoxStyle} data-testid="graph-halo-legend">
|
|
131
|
+
{[...kindsByColor].map(([color, kinds]) => (
|
|
132
|
+
<div key={color} style={legendRowStyle}>
|
|
133
|
+
<HaloSwatch color={color} />
|
|
134
|
+
<span>bubble: {[...kinds].join(', ')}</span>
|
|
135
|
+
</div>
|
|
136
|
+
))}
|
|
137
|
+
</div>
|
|
138
|
+
) : null
|
|
139
|
+
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BUBBLE_KIND_COLORS } from '@jbrowse/bandage-core/bubbles/classifyBubble'
|
|
2
2
|
import { LABEL_PAD } from '@jbrowse/bandage-core/overlayLabels'
|
|
3
|
+
import { isAlive } from '@jbrowse/mobx-state-tree'
|
|
3
4
|
import { Button } from '@mui/material'
|
|
4
5
|
import { observer } from 'mobx-react'
|
|
5
6
|
|
|
@@ -111,6 +112,15 @@ const BubbleOverlay = observer(function BubbleOverlay({
|
|
|
111
112
|
onClick={() => {
|
|
112
113
|
void model.popBubble(bubble)
|
|
113
114
|
}}
|
|
115
|
+
onMouseEnter={() => {
|
|
116
|
+
model.setHoveredBubble(bubble)
|
|
117
|
+
}}
|
|
118
|
+
// also fired as the view closing it unmounts the glyph
|
|
119
|
+
onMouseLeave={() => {
|
|
120
|
+
if (isAlive(model)) {
|
|
121
|
+
model.setHoveredBubble(null)
|
|
122
|
+
}
|
|
123
|
+
}}
|
|
114
124
|
>
|
|
115
125
|
<title>{`${label}\n${bubble.segmentCount} segments · click to open`}</title>
|
|
116
126
|
</path>
|
|
@@ -13,11 +13,14 @@ import { useRenderingBackend } from '@jbrowse/render-core/useRenderingBackend'
|
|
|
13
13
|
import InfoIcon from '@mui/icons-material/Info'
|
|
14
14
|
import { observer } from 'mobx-react'
|
|
15
15
|
|
|
16
|
-
import BubbleHalos from './BubbleHalos'
|
|
16
|
+
import BubbleHalos, { HaloLegend } from './BubbleHalos'
|
|
17
17
|
import BubbleOverlay from './BubbleOverlay'
|
|
18
18
|
import GenePins from './GenePins'
|
|
19
19
|
import GraphToolbar from './GraphToolbar'
|
|
20
20
|
import LabelLayer from './LabelLayer'
|
|
21
|
+
import ReferenceStripOverlay, {
|
|
22
|
+
ReferenceStripLegend,
|
|
23
|
+
} from './ReferenceStripOverlay'
|
|
21
24
|
import TubeMapOverlay, { TubeMapLegend } from './TubeMapOverlay'
|
|
22
25
|
import WalkRowsOverlay, { WalkRowsLegend } from './WalkRowsOverlay'
|
|
23
26
|
import { RAMP_GRADIENT_CSS } from './referenceRampCss'
|
|
@@ -195,6 +198,8 @@ const WalkReadout = observer(function WalkReadout({
|
|
|
195
198
|
<div style={legendBoxStyle} data-testid="graph-walk-readout">
|
|
196
199
|
<strong>{label}</strong>: {h.steps.toLocaleString()} steps,{' '}
|
|
197
200
|
{h.bp.toLocaleString()} bp{delta}
|
|
201
|
+
<br />
|
|
202
|
+
paler: what {label} does not carry
|
|
198
203
|
</div>
|
|
199
204
|
)
|
|
200
205
|
})
|
|
@@ -291,8 +296,16 @@ const Legends = observer(function Legends({
|
|
|
291
296
|
}
|
|
292
297
|
}, [model])
|
|
293
298
|
return (
|
|
294
|
-
<div
|
|
299
|
+
<div
|
|
300
|
+
ref={ref}
|
|
301
|
+
style={{
|
|
302
|
+
...legendStackStyle,
|
|
303
|
+
top: LEGEND_INSET_PX + model.referenceStripZonePx,
|
|
304
|
+
}}
|
|
305
|
+
>
|
|
295
306
|
<ReferenceRampLegend model={model} />
|
|
307
|
+
<ReferenceStripLegend model={model} />
|
|
308
|
+
<HaloLegend model={model} />
|
|
296
309
|
<PathLegend model={model} />
|
|
297
310
|
<TubeMapLegend model={model} />
|
|
298
311
|
<WalkRowsLegend model={model} />
|
|
@@ -564,11 +577,14 @@ const GraphCanvas = observer(function GraphCanvas({
|
|
|
564
577
|
// has one does not pay for a second getBoundingClientRect.
|
|
565
578
|
function nodeAt(x: number, y: number) {
|
|
566
579
|
const { nodePositions } = model
|
|
580
|
+
const sx = x * model.scaleX + model.translateX
|
|
581
|
+
const sy = y * model.scaleY + model.translateY
|
|
567
582
|
if (model.layoutResult?.tubeMap) {
|
|
568
|
-
return model.tubeMapNodeAt(
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
583
|
+
return model.tubeMapNodeAt(sx, sy)
|
|
584
|
+
}
|
|
585
|
+
const onStrip = model.referenceStripNodeAt(sx, sy)
|
|
586
|
+
if (onStrip) {
|
|
587
|
+
return onStrip
|
|
572
588
|
}
|
|
573
589
|
return nodePositions
|
|
574
590
|
? findHoveredNode(
|
|
@@ -592,8 +608,13 @@ const GraphCanvas = observer(function GraphCanvas({
|
|
|
592
608
|
e.stopPropagation()
|
|
593
609
|
}
|
|
594
610
|
const { x, y } = getMouseCoord(e)
|
|
595
|
-
// a tube map's boxes are the layout's,
|
|
596
|
-
|
|
611
|
+
// a tube map's boxes are the layout's, and the strip's are bp, not
|
|
612
|
+
// positions to drag
|
|
613
|
+
const node =
|
|
614
|
+
model.layoutResult?.tubeMap ||
|
|
615
|
+
e.nativeEvent.offsetY < model.referenceStripZonePx
|
|
616
|
+
? null
|
|
617
|
+
: nodeAt(x, y)
|
|
597
618
|
if (node) {
|
|
598
619
|
model.setDraggingNode(node)
|
|
599
620
|
} else {
|
|
@@ -684,8 +705,13 @@ const GraphCanvas = observer(function GraphCanvas({
|
|
|
684
705
|
model.stopDragging()
|
|
685
706
|
}
|
|
686
707
|
|
|
708
|
+
// also fired by the canvas unmounting under a resting pointer, after the
|
|
709
|
+
// view closing it has destroyed the model
|
|
687
710
|
function handleMouseLeave() {
|
|
688
711
|
dropPending()
|
|
712
|
+
if (!isAlive(model)) {
|
|
713
|
+
return
|
|
714
|
+
}
|
|
689
715
|
model.stopDragging()
|
|
690
716
|
model.setHoveredNode(null)
|
|
691
717
|
model.setHoveredEdge(null)
|
|
@@ -720,7 +746,17 @@ const GraphCanvas = observer(function GraphCanvas({
|
|
|
720
746
|
<div style={wrapperStyle}>
|
|
721
747
|
{ownChrome ? <GraphToolbar model={model} /> : null}
|
|
722
748
|
|
|
723
|
-
<div
|
|
749
|
+
<div
|
|
750
|
+
style={canvasAreaStyle}
|
|
751
|
+
onMouseEnter={() => {
|
|
752
|
+
model.setPointerInPane(true)
|
|
753
|
+
}}
|
|
754
|
+
onMouseLeave={() => {
|
|
755
|
+
if (isAlive(model)) {
|
|
756
|
+
model.setPointerInPane(false)
|
|
757
|
+
}
|
|
758
|
+
}}
|
|
759
|
+
>
|
|
724
760
|
<canvas
|
|
725
761
|
ref={canvasRef}
|
|
726
762
|
data-testid="graph-genome-canvas"
|
|
@@ -739,6 +775,7 @@ const GraphCanvas = observer(function GraphCanvas({
|
|
|
739
775
|
/>
|
|
740
776
|
|
|
741
777
|
<TubeMapOverlay model={model} />
|
|
778
|
+
<ReferenceStripOverlay model={model} />
|
|
742
779
|
<RowLabels model={model} />
|
|
743
780
|
<GraphSizeLabels model={model} />
|
|
744
781
|
<BubbleHalos model={model} />
|
|
@@ -14,6 +14,7 @@ export default function LabelChip({
|
|
|
14
14
|
title,
|
|
15
15
|
testId,
|
|
16
16
|
onClick,
|
|
17
|
+
onHover,
|
|
17
18
|
}: {
|
|
18
19
|
x: number
|
|
19
20
|
y: number
|
|
@@ -26,16 +27,23 @@ export default function LabelChip({
|
|
|
26
27
|
title?: string
|
|
27
28
|
testId?: string
|
|
28
29
|
onClick?: () => void
|
|
30
|
+
onHover?: (hovered: boolean) => void
|
|
29
31
|
}) {
|
|
30
32
|
return (
|
|
31
33
|
<g
|
|
32
34
|
style={{
|
|
33
|
-
pointerEvents: onClick ? 'auto' : 'none',
|
|
35
|
+
pointerEvents: onClick || onHover ? 'auto' : 'none',
|
|
34
36
|
cursor: onClick ? 'pointer' : undefined,
|
|
35
37
|
opacity: dimmed ? 0.35 : 1,
|
|
36
38
|
}}
|
|
37
39
|
data-testid={testId}
|
|
38
40
|
onClick={onClick}
|
|
41
|
+
onMouseEnter={() => {
|
|
42
|
+
onHover?.(true)
|
|
43
|
+
}}
|
|
44
|
+
onMouseLeave={() => {
|
|
45
|
+
onHover?.(false)
|
|
46
|
+
}}
|
|
39
47
|
>
|
|
40
48
|
{title ? <title>{title}</title> : null}
|
|
41
49
|
<rect
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BUBBLE_KIND_COLORS } from '@jbrowse/bandage-core/bubbles/classifyBubble'
|
|
2
2
|
import { LABEL_PX } from '@jbrowse/bandage-core/overlayLabels'
|
|
3
|
+
import { isAlive } from '@jbrowse/mobx-state-tree'
|
|
3
4
|
import { observer } from 'mobx-react'
|
|
4
5
|
|
|
5
6
|
import { EXON_COLOR } from './GenePins'
|
|
@@ -100,6 +101,12 @@ const LabelLayer = observer(function LabelLayer({
|
|
|
100
101
|
onClick={() => {
|
|
101
102
|
void model.popBubble(h.bubble)
|
|
102
103
|
}}
|
|
104
|
+
// a leave also fires as the view closing it unmounts the chip
|
|
105
|
+
onHover={hovered => {
|
|
106
|
+
if (isAlive(model)) {
|
|
107
|
+
model.setHoveredBubble(hovered ? h.bubble : null)
|
|
108
|
+
}
|
|
109
|
+
}}
|
|
103
110
|
/>
|
|
104
111
|
))}
|
|
105
112
|
</svg>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import { drawReferenceStrip } from '@jbrowse/bandage-core/referenceStrip'
|
|
4
|
+
import { getDpr } from '@jbrowse/render-core/canvas2dUtils'
|
|
5
|
+
import { autorun } from 'mobx'
|
|
6
|
+
import { observer } from 'mobx-react'
|
|
7
|
+
|
|
8
|
+
import type { GraphPaneModel } from '../model'
|
|
9
|
+
|
|
10
|
+
const canvasStyle = {
|
|
11
|
+
position: 'absolute' as const,
|
|
12
|
+
left: 0,
|
|
13
|
+
top: 0,
|
|
14
|
+
pointerEvents: 'none' as const,
|
|
15
|
+
zIndex: 5,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Each reference segment at its bp in the linear view, over a drawing that
|
|
19
|
+
// has no bp axis of its own. Repainted by an autorun, so a pan in the linear
|
|
20
|
+
// view moves the strip in the same frame as its other tracks.
|
|
21
|
+
const ReferenceStripOverlay = observer(function ReferenceStripOverlay({
|
|
22
|
+
model,
|
|
23
|
+
}: {
|
|
24
|
+
model: GraphPaneModel
|
|
25
|
+
}) {
|
|
26
|
+
const ref = useRef<HTMLCanvasElement>(null)
|
|
27
|
+
const { referenceStripShown, paneWidth, canvasHeight } = model
|
|
28
|
+
useEffect(
|
|
29
|
+
() =>
|
|
30
|
+
autorun(() => {
|
|
31
|
+
const canvas = ref.current
|
|
32
|
+
const frame = model.referenceStripFrame
|
|
33
|
+
const ctx = canvas?.getContext('2d')
|
|
34
|
+
if (canvas && ctx && frame && model.referenceStripShown) {
|
|
35
|
+
const dpr = getDpr()
|
|
36
|
+
const width = model.paneWidth
|
|
37
|
+
const height = model.canvasHeight
|
|
38
|
+
canvas.width = Math.round(width * dpr)
|
|
39
|
+
canvas.height = Math.round(height * dpr)
|
|
40
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
41
|
+
ctx.clearRect(0, 0, width, height)
|
|
42
|
+
drawReferenceStrip(ctx, model.referenceStripBlocks, frame, {
|
|
43
|
+
width,
|
|
44
|
+
lit: model.referenceStripLit,
|
|
45
|
+
darkMode: model.darkMode,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}),
|
|
49
|
+
[model],
|
|
50
|
+
)
|
|
51
|
+
return (
|
|
52
|
+
<canvas
|
|
53
|
+
ref={ref}
|
|
54
|
+
data-testid={referenceStripShown ? 'graph-reference-strip' : undefined}
|
|
55
|
+
style={{
|
|
56
|
+
...canvasStyle,
|
|
57
|
+
width: paneWidth,
|
|
58
|
+
height: canvasHeight,
|
|
59
|
+
display: referenceStripShown ? 'block' : 'none',
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
export default ReferenceStripOverlay
|
|
66
|
+
|
|
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
|
+
const SWATCH_PX = 18
|
|
77
|
+
|
|
78
|
+
function StripSwatch() {
|
|
79
|
+
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>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function OverhangSwatch() {
|
|
89
|
+
return (
|
|
90
|
+
<svg width={SWATCH_PX} height={8} style={{ flex: 'none' }}>
|
|
91
|
+
<path d="M0,4 L5,0.5 L5,7.5 Z M18,4 L13,0.5 L13,7.5 Z" fill="#18181c" />
|
|
92
|
+
</svg>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Hovering a node boxes its span on the strip and draws a leader to it
|
|
97
|
+
export const ReferenceStripLegend = observer(function ReferenceStripLegend({
|
|
98
|
+
model,
|
|
99
|
+
}: {
|
|
100
|
+
model: GraphPaneModel
|
|
101
|
+
}) {
|
|
102
|
+
return model.referenceStripShown ? (
|
|
103
|
+
<div style={legendBoxStyle} data-testid="graph-reference-strip-legend">
|
|
104
|
+
<div style={legendRowStyle}>
|
|
105
|
+
<StripSwatch />
|
|
106
|
+
<span>top strip: reference segments at their bp</span>
|
|
107
|
+
</div>
|
|
108
|
+
{model.referenceStripOverhangs ? (
|
|
109
|
+
<div style={legendRowStyle}>
|
|
110
|
+
<OverhangSwatch />
|
|
111
|
+
<span>the graph runs past this edge</span>
|
|
112
|
+
</div>
|
|
113
|
+
) : null}
|
|
114
|
+
</div>
|
|
115
|
+
) : null
|
|
116
|
+
})
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
2
3
|
|
|
3
4
|
import { formatBp } from '@jbrowse/bandage-core/graphLabels'
|
|
4
|
-
import {
|
|
5
|
-
ZIGZAG_AMPLITUDE_PX,
|
|
6
|
-
ZIGZAG_PX,
|
|
7
|
-
drawTubeMapRuler,
|
|
8
|
-
rulerInk,
|
|
9
|
-
} from '@jbrowse/bandage-core/tubeMap/axis'
|
|
5
|
+
import { drawTubeMapRuler, rulerInk } from '@jbrowse/bandage-core/tubeMap/axis'
|
|
10
6
|
import { drawTubeMapConnectors } from '@jbrowse/bandage-core/tubeMap/connectors'
|
|
11
7
|
import {
|
|
12
8
|
deviationInk,
|
|
@@ -14,6 +10,10 @@ import {
|
|
|
14
10
|
} from '@jbrowse/bandage-core/tubeMap/deviations'
|
|
15
11
|
import { drawTubeMap } from '@jbrowse/bandage-core/tubeMap/draw'
|
|
16
12
|
import { drawTubeMapGenes } from '@jbrowse/bandage-core/tubeMap/genes'
|
|
13
|
+
import {
|
|
14
|
+
FORWARD_READ_COLORS,
|
|
15
|
+
REVERSE_READ_COLORS,
|
|
16
|
+
} from '@jbrowse/bandage-core/tubeMap/reads'
|
|
17
17
|
import { getDpr } from '@jbrowse/render-core/canvas2dUtils'
|
|
18
18
|
import { autorun } from 'mobx'
|
|
19
19
|
import { observer } from 'mobx-react'
|
|
@@ -79,7 +79,13 @@ const TubeMapOverlay = observer(function TubeMapOverlay({
|
|
|
79
79
|
)
|
|
80
80
|
const boxes = model.tubeMapRulerBoxes
|
|
81
81
|
if (boxes) {
|
|
82
|
-
drawTubeMapRuler(
|
|
82
|
+
drawTubeMapRuler(
|
|
83
|
+
ctx,
|
|
84
|
+
boxes,
|
|
85
|
+
tubeFrame,
|
|
86
|
+
frame.y(bounds.maxY) + 4,
|
|
87
|
+
model.layoutResult?.referenceAxis,
|
|
88
|
+
)
|
|
83
89
|
}
|
|
84
90
|
}
|
|
85
91
|
}),
|
|
@@ -114,22 +120,16 @@ const legendBoxStyle = {
|
|
|
114
120
|
const legendRowStyle = { display: 'flex', alignItems: 'center', gap: 5 }
|
|
115
121
|
const SWATCH_PX = 18
|
|
116
122
|
|
|
117
|
-
//
|
|
118
|
-
function
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
for (let i = 0; i < teeth; i++) {
|
|
124
|
-
points.push(
|
|
125
|
-
`${(i + 0.5) * dx},${mid + (i % 2 ? 1 : -1) * ZIGZAG_AMPLITUDE_PX}`,
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
|
-
points.push(`${SWATCH_PX},${mid}`)
|
|
123
|
+
// Two boxes' brackets, as the ruler draws them under every box
|
|
124
|
+
function BracketSwatch() {
|
|
125
|
+
const brackets = [
|
|
126
|
+
[0.5, 5.5],
|
|
127
|
+
[8.5, SWATCH_PX - 0.5],
|
|
128
|
+
]
|
|
129
129
|
return (
|
|
130
|
-
<svg width={SWATCH_PX} height={
|
|
131
|
-
<
|
|
132
|
-
|
|
130
|
+
<svg width={SWATCH_PX} height={7} style={{ flex: 'none' }}>
|
|
131
|
+
<path
|
|
132
|
+
d={brackets.map(([x0, x1]) => `M${x0},6 V1.5 H${x1} V6`).join(' ')}
|
|
133
133
|
fill="none"
|
|
134
134
|
stroke={rulerInk()}
|
|
135
135
|
strokeWidth={1}
|
|
@@ -138,10 +138,10 @@ function ZigzagSwatch() {
|
|
|
138
138
|
)
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
const barSwatchStyle = { width: SWATCH_PX, height: 7, flex: 'none' }
|
|
142
|
+
|
|
141
143
|
const tubeSwatchStyle = {
|
|
142
|
-
|
|
143
|
-
height: 7,
|
|
144
|
-
flex: 'none',
|
|
144
|
+
...barSwatchStyle,
|
|
145
145
|
display: 'flex',
|
|
146
146
|
justifyContent: 'center',
|
|
147
147
|
backgroundColor: '#b8b8c0',
|
|
@@ -155,28 +155,96 @@ function TickSwatch() {
|
|
|
155
155
|
)
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
//
|
|
159
|
-
|
|
158
|
+
// A read takes one shade of its strand's palette, varied so neighbours differ
|
|
159
|
+
function ReadSwatch({ colors }: { colors: readonly string[] }) {
|
|
160
|
+
const step = 100 / colors.length
|
|
161
|
+
const stops = colors.map((c, i) => `${c} ${i * step}% ${(i + 1) * step}%`)
|
|
162
|
+
return (
|
|
163
|
+
<div
|
|
164
|
+
style={{
|
|
165
|
+
...barSwatchStyle,
|
|
166
|
+
background: `linear-gradient(to right, ${stops.join(', ')})`,
|
|
167
|
+
}}
|
|
168
|
+
/>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const glyphSwatchStyle = {
|
|
173
|
+
width: SWATCH_PX,
|
|
174
|
+
flex: 'none',
|
|
175
|
+
textAlign: 'center' as const,
|
|
176
|
+
fontFamily: 'monospace',
|
|
177
|
+
fontSize: 12,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function DeletionSwatch() {
|
|
181
|
+
return <div style={{ ...barSwatchStyle, backgroundColor: 'grey' }} />
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function LegendRow({
|
|
185
|
+
swatch,
|
|
186
|
+
children,
|
|
187
|
+
}: {
|
|
188
|
+
swatch: ReactNode
|
|
189
|
+
children: ReactNode
|
|
190
|
+
}) {
|
|
191
|
+
return (
|
|
192
|
+
<div style={legendRowStyle}>
|
|
193
|
+
{swatch}
|
|
194
|
+
<span>{children}</span>
|
|
195
|
+
</div>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// What a tube map draws that nothing else on screen explains, each listed
|
|
200
|
+
// only while it applies
|
|
160
201
|
export const TubeMapLegend = observer(function TubeMapLegend({
|
|
161
202
|
model,
|
|
162
203
|
}: {
|
|
163
204
|
model: GraphPaneModel
|
|
164
205
|
}) {
|
|
165
|
-
const {
|
|
166
|
-
|
|
206
|
+
const { logWidths, foldBp, forwardReads, reverseReads, mismatches } =
|
|
207
|
+
model.tubeMapKeys
|
|
208
|
+
const rows = [
|
|
209
|
+
forwardReads && (
|
|
210
|
+
<LegendRow key="fwd" swatch={<ReadSwatch colors={FORWARD_READ_COLORS} />}>
|
|
211
|
+
read on the forward strand
|
|
212
|
+
</LegendRow>
|
|
213
|
+
),
|
|
214
|
+
reverseReads && (
|
|
215
|
+
<LegendRow key="rev" swatch={<ReadSwatch colors={REVERSE_READ_COLORS} />}>
|
|
216
|
+
read on the reverse strand
|
|
217
|
+
</LegendRow>
|
|
218
|
+
),
|
|
219
|
+
mismatches.has('substitution') && (
|
|
220
|
+
<LegendRow key="sub" swatch={<span style={glyphSwatchStyle}>A</span>}>
|
|
221
|
+
a read's base unlike the node's
|
|
222
|
+
</LegendRow>
|
|
223
|
+
),
|
|
224
|
+
mismatches.has('insertion') && (
|
|
225
|
+
<LegendRow key="ins" swatch={<span style={glyphSwatchStyle}>*</span>}>
|
|
226
|
+
bases a read inserts
|
|
227
|
+
</LegendRow>
|
|
228
|
+
),
|
|
229
|
+
mismatches.has('deletion') && (
|
|
230
|
+
<LegendRow key="del" swatch={<DeletionSwatch />}>
|
|
231
|
+
bases a read skips
|
|
232
|
+
</LegendRow>
|
|
233
|
+
),
|
|
234
|
+
foldBp !== undefined && (
|
|
235
|
+
<LegendRow key="fold" swatch={<TickSwatch />}>
|
|
236
|
+
a haplotype's variant under {formatBp(foldBp)}
|
|
237
|
+
</LegendRow>
|
|
238
|
+
),
|
|
239
|
+
logWidths && (
|
|
240
|
+
<LegendRow key="log" swatch={<BracketSwatch />}>
|
|
241
|
+
width grows with log of length
|
|
242
|
+
</LegendRow>
|
|
243
|
+
),
|
|
244
|
+
].filter(Boolean)
|
|
245
|
+
return rows.length > 0 ? (
|
|
167
246
|
<div style={legendBoxStyle} data-testid="graph-tube-map-legend">
|
|
168
|
-
{
|
|
169
|
-
<div style={legendRowStyle}>
|
|
170
|
-
<TickSwatch />
|
|
171
|
-
<span>a haplotype's variant under {formatBp(foldBp)}</span>
|
|
172
|
-
</div>
|
|
173
|
-
) : null}
|
|
174
|
-
{squeezed ? (
|
|
175
|
-
<div style={legendRowStyle}>
|
|
176
|
-
<ZigzagSwatch />
|
|
177
|
-
<span>ruler not to scale, box length below</span>
|
|
178
|
-
</div>
|
|
179
|
-
) : null}
|
|
247
|
+
{rows}
|
|
180
248
|
</div>
|
|
181
249
|
) : null
|
|
182
250
|
})
|
|
@@ -2398,6 +2398,9 @@ describe('popping a bubble', () => {
|
|
|
2398
2398
|
model.setLayoutMode('ordered')
|
|
2399
2399
|
await model.loadGFA(RGFA_BUBBLE, 'rgfa')
|
|
2400
2400
|
expect(model.bubbleGlyphs).toEqual([])
|
|
2401
|
+
// off by default
|
|
2402
|
+
expect(model.bubbleHalos).toEqual([])
|
|
2403
|
+
model.setShowBubbles(true)
|
|
2401
2404
|
expect(
|
|
2402
2405
|
model.bubbleHalos.map(h => [h.label, h.members, h.path.startsWith('M')]),
|
|
2403
2406
|
).toEqual([['≤4 bp ins', 1, true]])
|