jbrowse-plugin-graphgenomeviewer 4.0.11 → 4.0.13
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 +22 -4
- package/dist/chunks/GbzBaseSyntenyAdapter-Y5NBXK7P.js +8 -0
- package/dist/chunks/{GbzBaseSyntenyAdapter-URQ4Z2JB.js.map → GbzBaseSyntenyAdapter-Y5NBXK7P.js.map} +4 -4
- package/dist/chunks/{GraphGenomeView-44GJL4RA.js → GraphGenomeView-TTQEJJQD.js} +2 -2
- package/dist/chunks/{GraphGenomeView-44GJL4RA.js.map → GraphGenomeView-TTQEJJQD.js.map} +1 -1
- package/dist/chunks/GraphTrackSettingsDialog-3A2PKZRI.js +4 -0
- package/dist/chunks/{GraphTrackSettingsDialog-LZITZ3VD.js.map → GraphTrackSettingsDialog-3A2PKZRI.js.map} +1 -1
- package/dist/chunks/LinearGraphDisplay-MJ5XZFE5.js +2 -0
- package/dist/chunks/{LinearGraphDisplay-BWBXVSEQ.js.map → LinearGraphDisplay-MJ5XZFE5.js.map} +1 -1
- package/dist/chunks/{MinigraphBubbleAdapter-QTXGK7DW.js → MinigraphBubbleAdapter-APC7JYOM.js} +2 -2
- package/dist/chunks/{RgfaTabixAdapter-O3ERR2GY.js → RgfaTabixAdapter-4E365PXO.js} +2 -2
- package/dist/chunks/{chunk-LGGAXAS6.js → chunk-5DP7IXD3.js} +2 -2
- package/dist/chunks/chunk-CJ56O2VG.js +2 -0
- package/dist/chunks/chunk-CJ56O2VG.js.map +7 -0
- package/dist/chunks/chunk-EFVAQ2KM.js +2 -0
- package/dist/chunks/chunk-EFVAQ2KM.js.map +7 -0
- package/dist/chunks/chunk-IFRDWMQ2.js +2 -0
- package/dist/chunks/chunk-IFRDWMQ2.js.map +7 -0
- package/dist/chunks/chunk-JAQ2RQAE.js +2 -0
- package/dist/chunks/{chunk-QRVREF7H.js.map → chunk-JAQ2RQAE.js.map} +4 -4
- package/dist/chunks/{chunk-WETM2CD7.js → chunk-LA7MQS33.js} +2 -2
- package/dist/chunks/chunk-M4YEAXSR.js +2 -0
- package/dist/chunks/chunk-M4YEAXSR.js.map +7 -0
- package/dist/chunks/chunk-XDOIZTY6.js +4 -0
- package/dist/chunks/chunk-XDOIZTY6.js.map +7 -0
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +2 -2
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
- package/package.json +4 -4
- package/src/GbzBaseSyntenyAdapter/configSchema.ts +3 -4
- package/src/GraphGenomeView/components/GraphCanvas.tsx +4 -11
- package/src/GraphGenomeView/components/TubeMapOverlay.tsx +117 -4
- package/src/GraphGenomeView/components/WalkRowsOverlay.tsx +261 -44
- package/src/GraphGenomeView/components/referenceRampCss.test.ts +27 -0
- package/src/GraphGenomeView/components/referenceRampCss.ts +39 -0
- package/src/GraphGenomeView/genes/genes.test.ts +9 -2
- package/src/GraphGenomeView/host.test.ts +25 -2
- package/src/GraphGenomeView/host.ts +9 -7
- package/src/GraphGenomeView/model.test.ts +383 -4
- package/src/GraphGenomeView/model.ts +413 -88
- package/src/GraphGenomeView/repeats/repeatFeatures.test.ts +75 -0
- package/src/GraphGenomeView/repeats/repeatFeatures.ts +156 -8
- package/src/GraphGenomeView/repeats/walkCalls.test.ts +18 -0
- package/src/GraphGenomeView/repeats/walkCalls.ts +35 -18
- package/src/GraphGenomeView/subgraphLoad.test.ts +1 -0
- package/src/LinearGraphDisplay/LinearGraphDisplay.test.ts +165 -6
- package/src/LinearGraphDisplay/model.ts +61 -1
- package/src/gaf/gafFile.test.ts +18 -0
- package/src/version.ts +1 -1
- package/dist/chunks/GbzBaseSyntenyAdapter-URQ4Z2JB.js +0 -8
- package/dist/chunks/GraphTrackSettingsDialog-LZITZ3VD.js +0 -4
- package/dist/chunks/LinearGraphDisplay-BWBXVSEQ.js +0 -2
- package/dist/chunks/chunk-ADEBM7AA.js +0 -2
- package/dist/chunks/chunk-ADEBM7AA.js.map +0 -7
- package/dist/chunks/chunk-MWSRAF5B.js +0 -4
- package/dist/chunks/chunk-MWSRAF5B.js.map +0 -7
- package/dist/chunks/chunk-QRVREF7H.js +0 -2
- package/dist/chunks/chunk-ZYQJBNS5.js +0 -2
- package/dist/chunks/chunk-ZYQJBNS5.js.map +0 -7
- /package/dist/chunks/{MinigraphBubbleAdapter-QTXGK7DW.js.map → MinigraphBubbleAdapter-APC7JYOM.js.map} +0 -0
- /package/dist/chunks/{RgfaTabixAdapter-O3ERR2GY.js.map → RgfaTabixAdapter-4E365PXO.js.map} +0 -0
- /package/dist/chunks/{chunk-LGGAXAS6.js.map → chunk-5DP7IXD3.js.map} +0 -0
- /package/dist/chunks/{chunk-WETM2CD7.js.map → chunk-LA7MQS33.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.13",
|
|
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": {
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"private": false,
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@gmod/bgzf-filehandle": "^6.7.0",
|
|
29
|
-
"@gmod/gbz-base": "^
|
|
30
|
-
"@gmod/tabix": "^3.
|
|
29
|
+
"@gmod/gbz-base": "^3.0.0",
|
|
30
|
+
"@gmod/tabix": "^3.9.0",
|
|
31
31
|
"@gmod/tubemap-core": "^0.1.0",
|
|
32
32
|
"@jbrowse/cigar-utils": "5.0.0-beta.9",
|
|
33
33
|
"@jbrowse/core": "5.0.0-beta.9",
|
|
@@ -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.8"
|
|
80
80
|
},
|
|
81
81
|
"peerDependencies": {
|
|
82
82
|
"react": ">=18.0.0",
|
|
@@ -13,10 +13,9 @@ import type { Instance } from '@jbrowse/mobx-state-tree'
|
|
|
13
13
|
* graph's reference path, every haplotype's walk through the nodes it covers
|
|
14
14
|
* is recovered and named, and each haplotype becomes one alignment record with
|
|
15
15
|
* a CIGAR, so the graph is read through HTTP range requests with no offline
|
|
16
|
-
* conversion.
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
19
|
-
* graph cannot say which haplotype a walk belongs to.
|
|
16
|
+
* conversion. Naming the walks takes the companion haplotype index that
|
|
17
|
+
* `gbz-haplotype-index` writes; without it the graph cannot say which
|
|
18
|
+
* haplotype a walk belongs to.
|
|
20
19
|
*
|
|
21
20
|
* Only the fine tier exists: a whole-chromosome view stays on a PIF's coarse
|
|
22
21
|
* tier. A query on a haplotype lane answers nothing, so the multi-way display
|
|
@@ -2,7 +2,6 @@ import { useEffect, useRef, useState } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { formatBp } from '@jbrowse/bandage-core/graphLabels'
|
|
4
4
|
import { LEGEND_INSET_PX } from '@jbrowse/bandage-core/labelLayout'
|
|
5
|
-
import { REFERENCE_RAMP_MAX_HUE } from '@jbrowse/bandage-core/renderer/GeometryBuilder'
|
|
6
5
|
import {
|
|
7
6
|
findHoveredEdge,
|
|
8
7
|
findHoveredNode,
|
|
@@ -19,8 +18,9 @@ import BubbleOverlay from './BubbleOverlay'
|
|
|
19
18
|
import GenePins from './GenePins'
|
|
20
19
|
import GraphToolbar from './GraphToolbar'
|
|
21
20
|
import LabelLayer from './LabelLayer'
|
|
22
|
-
import TubeMapOverlay from './TubeMapOverlay'
|
|
21
|
+
import TubeMapOverlay, { TubeMapLegend } from './TubeMapOverlay'
|
|
23
22
|
import WalkRowsOverlay, { WalkRowsLegend } from './WalkRowsOverlay'
|
|
23
|
+
import { RAMP_GRADIENT_CSS } from './referenceRampCss'
|
|
24
24
|
import { locLabel, nodeOwnLocation } from '../../launchFromGraph/contributors'
|
|
25
25
|
import { nodeLaunchMenuItems } from '../../launchFromGraph/graphMenuItems'
|
|
26
26
|
import { createGraphRenderer } from '../renderer/GraphRenderer'
|
|
@@ -203,19 +203,11 @@ const WalkReadout = observer(function WalkReadout({
|
|
|
203
203
|
// over. Nothing on screen used to say that red-to-magenta means left-to-right of
|
|
204
204
|
// the cut window, so two tutorials carried that sentence in prose and a reader
|
|
205
205
|
// arriving at a figure had no way to know it at all.
|
|
206
|
-
//
|
|
207
|
-
// The gradient is built from the same three numbers `getNodeColor` paints with
|
|
208
|
-
// (hue 0 to REFERENCE_RAMP_MAX_HUE at 70%/50%), rather than from a picked pair
|
|
209
|
-
// of hex stops, so it cannot come to describe a ramp the drawing stopped using.
|
|
210
206
|
const rampStripStyle = {
|
|
211
207
|
minWidth: 90,
|
|
212
208
|
height: 8,
|
|
213
209
|
borderRadius: 2,
|
|
214
|
-
background:
|
|
215
|
-
{ length: 7 },
|
|
216
|
-
(_, i) =>
|
|
217
|
-
`hsl(${(i / 6) * REFERENCE_RAMP_MAX_HUE}, 70%, 50%) ${(i / 6) * 100}%`,
|
|
218
|
-
).join(', ')})`,
|
|
210
|
+
background: RAMP_GRADIENT_CSS,
|
|
219
211
|
}
|
|
220
212
|
|
|
221
213
|
const rampEndsStyle = {
|
|
@@ -302,6 +294,7 @@ const Legends = observer(function Legends({
|
|
|
302
294
|
<div ref={ref} style={legendStackStyle}>
|
|
303
295
|
<ReferenceRampLegend model={model} />
|
|
304
296
|
<PathLegend model={model} />
|
|
297
|
+
<TubeMapLegend model={model} />
|
|
305
298
|
<WalkRowsLegend model={model} />
|
|
306
299
|
<WalkReadout model={model} />
|
|
307
300
|
</div>
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react'
|
|
2
2
|
|
|
3
|
+
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'
|
|
10
|
+
import { drawTubeMapConnectors } from '@jbrowse/bandage-core/tubeMap/connectors'
|
|
11
|
+
import {
|
|
12
|
+
deviationInk,
|
|
13
|
+
drawDeviationMarks,
|
|
14
|
+
} from '@jbrowse/bandage-core/tubeMap/deviations'
|
|
3
15
|
import { drawTubeMap } from '@jbrowse/bandage-core/tubeMap/draw'
|
|
16
|
+
import { drawTubeMapGenes } from '@jbrowse/bandage-core/tubeMap/genes'
|
|
4
17
|
import { getDpr } from '@jbrowse/render-core/canvas2dUtils'
|
|
5
18
|
import { autorun } from 'mobx'
|
|
6
19
|
import { observer } from 'mobx-react'
|
|
@@ -16,8 +29,11 @@ const canvasStyle = {
|
|
|
16
29
|
}
|
|
17
30
|
|
|
18
31
|
// The tube map's ink, over the canvas, which draws nothing under a tube map
|
|
19
|
-
// layout.
|
|
20
|
-
//
|
|
32
|
+
// layout. Above the tubes go the session's genes in a view of its own, or in
|
|
33
|
+
// a linear view the bands tying each reference box to its bp there; under
|
|
34
|
+
// them a reference ruler, unless the linear view's is already the axis.
|
|
35
|
+
// Repainted by an autorun on every change of transform, so a pan in the linear
|
|
36
|
+
// view above moves the tubes in the same frame as its other tracks.
|
|
21
37
|
const TubeMapOverlay = observer(function TubeMapOverlay({
|
|
22
38
|
model,
|
|
23
39
|
}: {
|
|
@@ -40,12 +56,31 @@ const TubeMapOverlay = observer(function TubeMapOverlay({
|
|
|
40
56
|
canvas.height = Math.round(height * dpr)
|
|
41
57
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
42
58
|
ctx.clearRect(0, 0, width, height)
|
|
43
|
-
|
|
59
|
+
const tubeFrame = {
|
|
44
60
|
...frame,
|
|
45
61
|
width,
|
|
46
62
|
highlightNode: model.hoveredNode ?? model.selectedNode,
|
|
47
63
|
darkMode: model.darkMode,
|
|
48
|
-
}
|
|
64
|
+
}
|
|
65
|
+
drawTubeMap(ctx, picture, tubeFrame)
|
|
66
|
+
drawDeviationMarks(ctx, model.tubeMapDeviations, tubeFrame)
|
|
67
|
+
drawTubeMapConnectors(
|
|
68
|
+
ctx,
|
|
69
|
+
model.tubeMapConnectors,
|
|
70
|
+
model.connectorZoneBottom,
|
|
71
|
+
tubeFrame,
|
|
72
|
+
)
|
|
73
|
+
const { bounds } = picture
|
|
74
|
+
drawTubeMapGenes(
|
|
75
|
+
ctx,
|
|
76
|
+
model.tubeMapGenes,
|
|
77
|
+
tubeFrame,
|
|
78
|
+
frame.y(bounds.minY) - 2,
|
|
79
|
+
)
|
|
80
|
+
const boxes = model.tubeMapRulerBoxes
|
|
81
|
+
if (boxes) {
|
|
82
|
+
drawTubeMapRuler(ctx, boxes, tubeFrame, frame.y(bounds.maxY) + 4)
|
|
83
|
+
}
|
|
49
84
|
}
|
|
50
85
|
}),
|
|
51
86
|
[model],
|
|
@@ -67,3 +102,81 @@ const TubeMapOverlay = observer(function TubeMapOverlay({
|
|
|
67
102
|
})
|
|
68
103
|
|
|
69
104
|
export default TubeMapOverlay
|
|
105
|
+
|
|
106
|
+
const legendBoxStyle = {
|
|
107
|
+
background: 'rgba(255,255,255,0.82)',
|
|
108
|
+
padding: '4px 6px',
|
|
109
|
+
borderRadius: 3,
|
|
110
|
+
fontSize: 11,
|
|
111
|
+
lineHeight: '15px',
|
|
112
|
+
whiteSpace: 'nowrap' as const,
|
|
113
|
+
}
|
|
114
|
+
const legendRowStyle = { display: 'flex', alignItems: 'center', gap: 5 }
|
|
115
|
+
const SWATCH_PX = 18
|
|
116
|
+
|
|
117
|
+
// The ruler's own zigzag, tooth for tooth
|
|
118
|
+
function ZigzagSwatch() {
|
|
119
|
+
const teeth = Math.round(SWATCH_PX / ZIGZAG_PX)
|
|
120
|
+
const dx = SWATCH_PX / teeth
|
|
121
|
+
const mid = ZIGZAG_AMPLITUDE_PX + 1
|
|
122
|
+
const points = [`0,${mid}`]
|
|
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}`)
|
|
129
|
+
return (
|
|
130
|
+
<svg width={SWATCH_PX} height={mid * 2} style={{ flex: 'none' }}>
|
|
131
|
+
<polyline
|
|
132
|
+
points={points.join(' ')}
|
|
133
|
+
fill="none"
|
|
134
|
+
stroke={rulerInk()}
|
|
135
|
+
strokeWidth={1}
|
|
136
|
+
/>
|
|
137
|
+
</svg>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const tubeSwatchStyle = {
|
|
142
|
+
width: SWATCH_PX,
|
|
143
|
+
height: 7,
|
|
144
|
+
flex: 'none',
|
|
145
|
+
display: 'flex',
|
|
146
|
+
justifyContent: 'center',
|
|
147
|
+
backgroundColor: '#b8b8c0',
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function TickSwatch() {
|
|
151
|
+
return (
|
|
152
|
+
<div style={tubeSwatchStyle}>
|
|
153
|
+
<div style={{ width: 2, backgroundColor: deviationInk() }} />
|
|
154
|
+
</div>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// The two marks a tube map draws that nothing else on screen explains, each
|
|
159
|
+
// listed only while it is drawn
|
|
160
|
+
export const TubeMapLegend = observer(function TubeMapLegend({
|
|
161
|
+
model,
|
|
162
|
+
}: {
|
|
163
|
+
model: GraphPaneModel
|
|
164
|
+
}) {
|
|
165
|
+
const { squeezed, foldBp } = model.tubeMapKeys
|
|
166
|
+
return squeezed || foldBp !== undefined ? (
|
|
167
|
+
<div style={legendBoxStyle} data-testid="graph-tube-map-legend">
|
|
168
|
+
{foldBp !== undefined ? (
|
|
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}
|
|
180
|
+
</div>
|
|
181
|
+
) : null
|
|
182
|
+
})
|
|
@@ -1,20 +1,34 @@
|
|
|
1
|
+
import { useId } from 'react'
|
|
2
|
+
|
|
1
3
|
import { ROW_HEIGHT_PX } from '@jbrowse/bandage-core/layout/rowSpacing'
|
|
2
4
|
import { LABEL_CHAR_PX } from '@jbrowse/bandage-core/overlayLabels'
|
|
5
|
+
import { REFERENCE_RAMP_ALT_CSS } from '@jbrowse/bandage-core/renderer/GeometryBuilder'
|
|
3
6
|
import { observer } from 'mobx-react'
|
|
4
7
|
|
|
8
|
+
import { RAMP_GRADIENT_CSS, rampHueCss, rampStops } from './referenceRampCss'
|
|
5
9
|
import { CALL_TOLERANCE } from '../repeats/walkCalls'
|
|
6
10
|
|
|
7
11
|
import type { GraphPaneModel } from '../model'
|
|
12
|
+
import type { RepeatSequence, RepeatUnit } from '../repeats/repeatFeatures'
|
|
13
|
+
import type { WalkCall } from '../repeats/walkCalls'
|
|
14
|
+
import type { WalkRun } from '@jbrowse/bandage-core/layout/walkRows'
|
|
8
15
|
|
|
9
16
|
// The walk-rows layout's bars: one per haplotype walk under the reference line
|
|
10
17
|
// the canvas draws, each on its own bp axis from the window's left edge. Blue
|
|
11
|
-
//
|
|
12
|
-
//
|
|
18
|
+
// marks sequence shared with the reference walk and purple sequence only the
|
|
19
|
+
// haplotypes carry. Under the reference-position ramp, shared sequence takes
|
|
20
|
+
// the hue of the reference it is threaded through and haplotype-only sequence
|
|
21
|
+
// the charcoal the ramp reserves for rGFA's off-reference ranks. In a tandem
|
|
22
|
+
// array the threading is the aligner's pick among near-identical copies, so
|
|
23
|
+
// the hue there says which reference copy the graph used, not which one a copy
|
|
24
|
+
// resembles. The readout at the end of a bar is what it carries against the
|
|
13
25
|
// reference, which for a repeat array is the expansion.
|
|
14
26
|
//
|
|
15
|
-
// A
|
|
16
|
-
//
|
|
17
|
-
//
|
|
27
|
+
// A repeat record that states each allele's runs (VCF 4.5 <CNV:TR> RN, RUS,
|
|
28
|
+
// RUC, RB) paints every bar by unit instead: the allele paired with THAT walk,
|
|
29
|
+
// one colour per unit the record names. A record stating only a length marks
|
|
30
|
+
// it with a tick. The walk rows pair them — see repeats/walkCalls.ts, which
|
|
31
|
+
// also holds the threshold this reads a red readout off.
|
|
18
32
|
|
|
19
33
|
const svgStyle = {
|
|
20
34
|
position: 'absolute' as const,
|
|
@@ -32,6 +46,29 @@ const DISAGREES = '#c62828'
|
|
|
32
46
|
const OFF_REFERENCE = '#8e3fbf'
|
|
33
47
|
const BAR_PX = 12
|
|
34
48
|
|
|
49
|
+
// Tableau 10, whose leading hues stay apart in lightness as well as hue
|
|
50
|
+
const UNIT_COLORS = [
|
|
51
|
+
'#4e79a7',
|
|
52
|
+
'#f28e2b',
|
|
53
|
+
'#59a14f',
|
|
54
|
+
'#e15759',
|
|
55
|
+
'#76b7b2',
|
|
56
|
+
'#edc948',
|
|
57
|
+
'#b07aa1',
|
|
58
|
+
'#ff9da7',
|
|
59
|
+
'#9c755f',
|
|
60
|
+
'#bab0ac',
|
|
61
|
+
]
|
|
62
|
+
const NO_ALLELE = '#d0d0d0'
|
|
63
|
+
// the canvas's grey scheme, for the backbone past the array when no ramp is
|
|
64
|
+
// drawn anywhere else
|
|
65
|
+
const BACKBONE = 'rgb(160, 160, 160)'
|
|
66
|
+
|
|
67
|
+
function unitColor(units: RepeatUnit[], unit: string) {
|
|
68
|
+
const i = units.findIndex(u => u.unit === unit)
|
|
69
|
+
return i < 0 ? NO_ALLELE : UNIT_COLORS[i % UNIT_COLORS.length]!
|
|
70
|
+
}
|
|
71
|
+
|
|
35
72
|
const legendBoxStyle = {
|
|
36
73
|
background: 'rgba(255,255,255,0.82)',
|
|
37
74
|
padding: '4px 6px',
|
|
@@ -69,17 +106,58 @@ export const WalkRowsLegend = observer(function WalkRowsLegend({
|
|
|
69
106
|
if (!bars) {
|
|
70
107
|
return null
|
|
71
108
|
}
|
|
72
|
-
const
|
|
109
|
+
const ramp = model.referenceRampDomain
|
|
110
|
+
const units = model.walkRowUnits
|
|
111
|
+
const drawn = [bars.reference, ...bars.rows]
|
|
112
|
+
const calls = drawn.flatMap(row =>
|
|
113
|
+
row.call && !row.call.sequences ? row.call : [],
|
|
114
|
+
)
|
|
73
115
|
return (
|
|
74
116
|
<div style={legendBoxStyle} data-testid="graph-walk-rows-legend">
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
117
|
+
{units ? (
|
|
118
|
+
<>
|
|
119
|
+
{units.map((unit, i) => (
|
|
120
|
+
<div key={unit.unit} style={legendRowStyle}>
|
|
121
|
+
<div
|
|
122
|
+
style={{
|
|
123
|
+
...swatchStyle,
|
|
124
|
+
backgroundColor: unitColor(units, unit.unit),
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
<span>
|
|
128
|
+
unit {i + 1} · {unit.unitLength.toLocaleString()} bp
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
))}
|
|
132
|
+
{drawn.some(row => !row.call?.sequences) ? (
|
|
133
|
+
<div style={legendRowStyle}>
|
|
134
|
+
<div style={{ ...swatchStyle, backgroundColor: NO_ALLELE }} />
|
|
135
|
+
<span>no allele in the repeat track</span>
|
|
136
|
+
</div>
|
|
137
|
+
) : null}
|
|
138
|
+
</>
|
|
139
|
+
) : (
|
|
140
|
+
<>
|
|
141
|
+
<div style={legendRowStyle}>
|
|
142
|
+
<div
|
|
143
|
+
style={{
|
|
144
|
+
...swatchStyle,
|
|
145
|
+
background: ramp ? RAMP_GRADIENT_CSS : ON_REFERENCE,
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
<span>shared with {bars.reference.label}</span>
|
|
149
|
+
</div>
|
|
150
|
+
<div style={legendRowStyle}>
|
|
151
|
+
<div
|
|
152
|
+
style={{
|
|
153
|
+
...swatchStyle,
|
|
154
|
+
backgroundColor: ramp ? REFERENCE_RAMP_ALT_CSS : OFF_REFERENCE,
|
|
155
|
+
}}
|
|
156
|
+
/>
|
|
157
|
+
<span>carried by haplotypes only</span>
|
|
158
|
+
</div>
|
|
159
|
+
</>
|
|
160
|
+
)}
|
|
83
161
|
{calls.some(call => call.spanningReads !== 0) ? (
|
|
84
162
|
<div style={legendRowStyle}>
|
|
85
163
|
<TickSwatch color={CALL_TICK} />
|
|
@@ -112,16 +190,27 @@ function units(bp: number, unit: number | undefined) {
|
|
|
112
190
|
return unit ? ` ≈ ${Math.round(bp / unit)} units` : ''
|
|
113
191
|
}
|
|
114
192
|
|
|
193
|
+
// A call stating its runs counts its copies outright; otherwise the bar's
|
|
194
|
+
// length in units is the estimate.
|
|
195
|
+
function copies(bp: number, unit: number | undefined, call?: WalkCall) {
|
|
196
|
+
if (!call?.sequences) {
|
|
197
|
+
return units(bp, unit)
|
|
198
|
+
}
|
|
199
|
+
const n = call.sequences.reduce((sum, run) => sum + run.count, 0)
|
|
200
|
+
return ` · ${Number.isInteger(n) ? n : n.toFixed(1)} copies`
|
|
201
|
+
}
|
|
202
|
+
|
|
115
203
|
function readout(
|
|
116
|
-
bp: number,
|
|
204
|
+
row: { bp: number; complete: boolean; call?: WalkCall },
|
|
117
205
|
referenceBp: number,
|
|
118
|
-
complete: boolean,
|
|
119
206
|
unit: number | undefined,
|
|
120
207
|
) {
|
|
208
|
+
const { bp, complete, call } = row
|
|
121
209
|
const delta = bp - referenceBp
|
|
122
210
|
const against =
|
|
123
211
|
delta === 0 ? '' : ` (${delta > 0 ? '+' : '−'}${kb(Math.abs(delta))})`
|
|
124
|
-
|
|
212
|
+
const called = call && !call.sequences ? calledReadout(call) : ''
|
|
213
|
+
return `${kb(bp)}${copies(bp, unit, call)}${against}${complete ? '' : ' · partial walk'}${called}`
|
|
125
214
|
}
|
|
126
215
|
|
|
127
216
|
function calledReadout(call: { bp: number; spanningReads?: number }) {
|
|
@@ -129,6 +218,26 @@ function calledReadout(call: { bp: number; spanningReads?: number }) {
|
|
|
129
218
|
return ` · called ${kb(call.bp)}${unbacked}`
|
|
130
219
|
}
|
|
131
220
|
|
|
221
|
+
// Each run's extent from the bar's left end, and where each of its copies
|
|
222
|
+
// starts: RUB's lengths where the record gives them, else the unit's.
|
|
223
|
+
function runsOf(sequences: RepeatSequence[]) {
|
|
224
|
+
let at = 0
|
|
225
|
+
return sequences.map(run => {
|
|
226
|
+
const start = at
|
|
227
|
+
const lengths =
|
|
228
|
+
run.copyBp ??
|
|
229
|
+
Array.from({ length: Math.ceil(run.count) }, () => run.unitLength)
|
|
230
|
+
const copyStarts: number[] = []
|
|
231
|
+
let copy = start
|
|
232
|
+
for (const bp of lengths) {
|
|
233
|
+
copyStarts.push(copy)
|
|
234
|
+
copy += bp
|
|
235
|
+
}
|
|
236
|
+
at += run.bp
|
|
237
|
+
return { start, bp: run.bp, unit: run.unit, copyStarts }
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
132
241
|
// One separator per unit along a bar, so copies are countable, dropped when a
|
|
133
242
|
// unit is under a few px. The reference bar is the canvas's, so it gets none.
|
|
134
243
|
const MIN_TILE_PX = 3
|
|
@@ -144,12 +253,46 @@ function tileSeparators(bp: number, unit: number, X: (bp: number) => number) {
|
|
|
144
253
|
return xs
|
|
145
254
|
}
|
|
146
255
|
|
|
256
|
+
// A shared run covers its reference contiguously, so a gradient along it
|
|
257
|
+
// paints the hue of every base.
|
|
258
|
+
function runFill(
|
|
259
|
+
run: WalkRun,
|
|
260
|
+
ramp: { start: number; end: number } | undefined,
|
|
261
|
+
gradientId: string,
|
|
262
|
+
) {
|
|
263
|
+
if (!ramp) {
|
|
264
|
+
return { fill: run.onReference ? ON_REFERENCE : OFF_REFERENCE }
|
|
265
|
+
}
|
|
266
|
+
if (run.referenceStart === undefined) {
|
|
267
|
+
return { fill: REFERENCE_RAMP_ALT_CSS }
|
|
268
|
+
}
|
|
269
|
+
const stops = rampStops({ ...run, start: run.referenceStart }, ramp)
|
|
270
|
+
if (stops.length === 1) {
|
|
271
|
+
return { fill: rampHueCss(stops[0]!) }
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
fill: `url(#${gradientId})`,
|
|
275
|
+
gradient: (
|
|
276
|
+
<linearGradient id={gradientId}>
|
|
277
|
+
{stops.map((hue, i) => (
|
|
278
|
+
<stop
|
|
279
|
+
key={i}
|
|
280
|
+
offset={i / (stops.length - 1)}
|
|
281
|
+
stopColor={rampHueCss(hue)}
|
|
282
|
+
/>
|
|
283
|
+
))}
|
|
284
|
+
</linearGradient>
|
|
285
|
+
),
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
147
289
|
const WalkRowsOverlay = observer(function WalkRowsOverlay({
|
|
148
290
|
model,
|
|
149
291
|
}: {
|
|
150
292
|
model: GraphPaneModel
|
|
151
293
|
}) {
|
|
152
|
-
const
|
|
294
|
+
const idPrefix = useId().replace(/[^\w-]/g, '')
|
|
295
|
+
const { walkRowBars, referenceRampDomain: ramp } = model
|
|
153
296
|
if (!walkRowBars) {
|
|
154
297
|
return null
|
|
155
298
|
}
|
|
@@ -164,8 +307,10 @@ const WalkRowsOverlay = observer(function WalkRowsOverlay({
|
|
|
164
307
|
const X = (bp: number) => bp * scaleX + translateX
|
|
165
308
|
const Y = (row: number) => row * ROW_HEIGHT_PX * scaleY + translateY
|
|
166
309
|
const { origin, unit, reference, rows } = walkRowBars
|
|
310
|
+
const units = model.walkRowUnits
|
|
167
311
|
// The backbone the canvas draws spans the cut window, which reaches past a
|
|
168
312
|
// selected array on both sides, so the reference readout sits after it.
|
|
313
|
+
const backboneStart = Math.min(origin, model.graphRegion?.start ?? origin)
|
|
169
314
|
const backboneEnd = Math.max(
|
|
170
315
|
origin + reference.bp,
|
|
171
316
|
model.graphRegion?.end ?? 0,
|
|
@@ -190,6 +335,62 @@ const WalkRowsOverlay = observer(function WalkRowsOverlay({
|
|
|
190
335
|
</text>
|
|
191
336
|
)
|
|
192
337
|
}
|
|
338
|
+
const separators = (starts: number[], bp: number, y: number) =>
|
|
339
|
+
starts
|
|
340
|
+
.filter(k => k > 0 && k < bp)
|
|
341
|
+
.map(k => (
|
|
342
|
+
<line
|
|
343
|
+
key={k}
|
|
344
|
+
x1={X(origin + k)}
|
|
345
|
+
x2={X(origin + k)}
|
|
346
|
+
y1={y - BAR_PX / 2}
|
|
347
|
+
y2={y + BAR_PX / 2}
|
|
348
|
+
stroke="white"
|
|
349
|
+
strokeWidth={1}
|
|
350
|
+
/>
|
|
351
|
+
))
|
|
352
|
+
const rect = (start: number, bp: number, y: number, fill: string) => (
|
|
353
|
+
<rect
|
|
354
|
+
key={start}
|
|
355
|
+
x={X(origin + start)}
|
|
356
|
+
y={y - BAR_PX / 2}
|
|
357
|
+
width={Math.max(1, bp * scaleX)}
|
|
358
|
+
height={BAR_PX}
|
|
359
|
+
fill={fill}
|
|
360
|
+
/>
|
|
361
|
+
)
|
|
362
|
+
// The paired allele's runs, cut at the end of the walk, over grey where it
|
|
363
|
+
// states none.
|
|
364
|
+
const unitBar = (
|
|
365
|
+
row: { bp: number; call?: WalkCall },
|
|
366
|
+
y: number,
|
|
367
|
+
painted: RepeatUnit[],
|
|
368
|
+
) => {
|
|
369
|
+
const runs = row.call?.sequences ? runsOf(row.call.sequences) : []
|
|
370
|
+
const tiled = unit !== undefined && unit * scaleX >= MIN_TILE_PX
|
|
371
|
+
return (
|
|
372
|
+
<>
|
|
373
|
+
{rect(0, row.bp, y, NO_ALLELE)}
|
|
374
|
+
{runs
|
|
375
|
+
.filter(run => run.start < row.bp)
|
|
376
|
+
.map(run =>
|
|
377
|
+
rect(
|
|
378
|
+
run.start,
|
|
379
|
+
Math.min(run.bp, row.bp - run.start),
|
|
380
|
+
y,
|
|
381
|
+
unitColor(painted, run.unit),
|
|
382
|
+
),
|
|
383
|
+
)}
|
|
384
|
+
{tiled
|
|
385
|
+
? separators(
|
|
386
|
+
runs.flatMap(run => run.copyStarts),
|
|
387
|
+
row.bp,
|
|
388
|
+
y,
|
|
389
|
+
)
|
|
390
|
+
: null}
|
|
391
|
+
</>
|
|
392
|
+
)
|
|
393
|
+
}
|
|
193
394
|
return (
|
|
194
395
|
<svg
|
|
195
396
|
style={svgStyle}
|
|
@@ -197,8 +398,21 @@ const WalkRowsOverlay = observer(function WalkRowsOverlay({
|
|
|
197
398
|
height={canvasHeight}
|
|
198
399
|
data-testid="graph-walk-rows"
|
|
199
400
|
>
|
|
401
|
+
{units ? (
|
|
402
|
+
<>
|
|
403
|
+
<rect
|
|
404
|
+
x={X(backboneStart) - BAR_PX / 2}
|
|
405
|
+
y={Y(0) - BAR_PX / 2}
|
|
406
|
+
width={(backboneEnd - backboneStart) * scaleX + BAR_PX}
|
|
407
|
+
height={BAR_PX}
|
|
408
|
+
rx={BAR_PX / 2}
|
|
409
|
+
fill={BACKBONE}
|
|
410
|
+
/>
|
|
411
|
+
{unitBar(reference, Y(0), units)}
|
|
412
|
+
</>
|
|
413
|
+
) : null}
|
|
200
414
|
{label(
|
|
201
|
-
`${kb(reference.bp)}${
|
|
415
|
+
`${kb(reference.bp)}${copies(reference.bp, unit, reference.call)}`,
|
|
202
416
|
backboneEnd,
|
|
203
417
|
Y(0),
|
|
204
418
|
)}
|
|
@@ -210,30 +424,33 @@ const WalkRowsOverlay = observer(function WalkRowsOverlay({
|
|
|
210
424
|
const { call } = row
|
|
211
425
|
return (
|
|
212
426
|
<g key={row.name} data-testid="graph-walk-row">
|
|
213
|
-
{
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
427
|
+
{units ? (
|
|
428
|
+
unitBar(row, y, units)
|
|
429
|
+
) : (
|
|
430
|
+
<>
|
|
431
|
+
{row.runs.map(run => {
|
|
432
|
+
const { fill, gradient } = runFill(
|
|
433
|
+
run,
|
|
434
|
+
ramp,
|
|
435
|
+
`${idPrefix}-${i}-${run.start}`,
|
|
436
|
+
)
|
|
437
|
+
return (
|
|
438
|
+
<g key={run.start}>
|
|
439
|
+
{gradient}
|
|
440
|
+
{rect(run.start, run.bp, y, fill)}
|
|
441
|
+
</g>
|
|
442
|
+
)
|
|
443
|
+
})}
|
|
444
|
+
{unit
|
|
445
|
+
? separators(
|
|
446
|
+
tileSeparators(row.bp, unit, bp => bp * scaleX),
|
|
447
|
+
row.bp,
|
|
448
|
+
y,
|
|
449
|
+
)
|
|
450
|
+
: null}
|
|
451
|
+
</>
|
|
452
|
+
)}
|
|
453
|
+
{call && !call.sequences ? (
|
|
237
454
|
<rect
|
|
238
455
|
data-testid="graph-walk-call"
|
|
239
456
|
x={X(origin + call.bp) - 1}
|
|
@@ -244,7 +461,7 @@ const WalkRowsOverlay = observer(function WalkRowsOverlay({
|
|
|
244
461
|
/>
|
|
245
462
|
) : null}
|
|
246
463
|
{label(
|
|
247
|
-
|
|
464
|
+
readout(row, reference.bp, unit),
|
|
248
465
|
origin + row.bp,
|
|
249
466
|
y,
|
|
250
467
|
call?.agrees === false ? DISAGREES : undefined,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { rampHue, rampStops } from './referenceRampCss'
|
|
2
|
+
|
|
3
|
+
const domain = { start: 1000, end: 2000 }
|
|
4
|
+
|
|
5
|
+
test('the hue runs 0 to 300 across the domain and holds past its ends', () => {
|
|
6
|
+
expect(rampHue(1000, domain)).toBe(0)
|
|
7
|
+
expect(rampHue(1500, domain)).toBe(150)
|
|
8
|
+
expect(rampHue(2000, domain)).toBe(300)
|
|
9
|
+
expect(rampHue(0, domain)).toBe(0)
|
|
10
|
+
expect(rampHue(9000, domain)).toBe(300)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('a span under a degree of hue reads as one flat hue', () => {
|
|
14
|
+
expect(rampStops({ start: 1500, bp: 2 }, domain)).toEqual([150.3])
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('stops step at most 30 degrees, from the far end when reversed', () => {
|
|
18
|
+
const forward = rampStops({ start: 1000, bp: 500 }, domain)
|
|
19
|
+
expect(forward).toEqual([0, 30, 60, 90, 120, 150])
|
|
20
|
+
expect(rampStops({ start: 1000, bp: 500, reversed: true }, domain)).toEqual(
|
|
21
|
+
[...forward].reverse(),
|
|
22
|
+
)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('a span reaching past the domain holds its last hue there', () => {
|
|
26
|
+
expect(rampStops({ start: 1800, bp: 400 }, domain)).toEqual([240, 300, 300])
|
|
27
|
+
})
|