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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { REFERENCE_RAMP_MAX_HUE } from '@jbrowse/bandage-core/renderer/GeometryBuilder'
|
|
2
|
+
|
|
3
|
+
// The reference-position ramp in CSS, from the same three numbers
|
|
4
|
+
// `getNodeColor` paints with (hue 0 to REFERENCE_RAMP_MAX_HUE at 70%/50%), so
|
|
5
|
+
// a key or an overlay cannot come to describe a ramp the canvas stopped using.
|
|
6
|
+
|
|
7
|
+
export function rampHueCss(hue: number) {
|
|
8
|
+
return `hsl(${hue}, 70%, 50%)`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function rampHue(bp: number, domain: { start: number; end: number }) {
|
|
12
|
+
const frac = (bp - domain.start) / (domain.end - domain.start)
|
|
13
|
+
return Math.max(0, Math.min(1, frac)) * REFERENCE_RAMP_MAX_HUE
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// hue degrees one gradient stop spans, since SVG interpolates stops in RGB
|
|
17
|
+
const HUE_STOP_DEG = 30
|
|
18
|
+
|
|
19
|
+
// The hues a gradient across `bp` of reference paints, evenly spaced along it
|
|
20
|
+
// and walked from `to`'s end when reversed. One hue means the span reads flat.
|
|
21
|
+
export function rampStops(
|
|
22
|
+
{ start, bp, reversed }: { start: number; bp: number; reversed?: boolean },
|
|
23
|
+
domain: { start: number; end: number },
|
|
24
|
+
) {
|
|
25
|
+
const low = rampHue(start, domain)
|
|
26
|
+
const high = rampHue(start + bp, domain)
|
|
27
|
+
if (high - low < 1) {
|
|
28
|
+
return [(low + high) / 2]
|
|
29
|
+
}
|
|
30
|
+
const n = Math.ceil((high - low) / HUE_STOP_DEG)
|
|
31
|
+
return Array.from({ length: n + 1 }, (_, i) =>
|
|
32
|
+
rampHue(start + (bp * (reversed ? n - i : i)) / n, domain),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const RAMP_GRADIENT_CSS = `linear-gradient(to right, ${Array.from(
|
|
37
|
+
{ length: 7 },
|
|
38
|
+
(_, i) => `${rampHueCss((i / 6) * REFERENCE_RAMP_MAX_HUE)} ${(i / 6) * 100}%`,
|
|
39
|
+
).join(', ')})`
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { genePins } from '@jbrowse/bandage-core/genes/genePins'
|
|
2
2
|
import { convertGFAToGraph } from '@jbrowse/bandage-core/gfa/gfaConverter'
|
|
3
3
|
import { parseGFA } from '@jbrowse/bandage-core/gfa-core/index'
|
|
4
|
+
import {
|
|
5
|
+
featuresOnBackbone,
|
|
6
|
+
graphBackbone,
|
|
7
|
+
} from '@jbrowse/bandage-core/reference'
|
|
4
8
|
|
|
5
9
|
import { geneModelsFrom, pickGeneTrack } from './geneFeatures'
|
|
6
10
|
|
|
@@ -93,11 +97,14 @@ test('exons land on the backbone stretch they cover, and the name at the midpoin
|
|
|
93
97
|
expect(pin!.covered).toBe(1)
|
|
94
98
|
})
|
|
95
99
|
|
|
96
|
-
test('a PanSN-named backbone
|
|
100
|
+
test('a PanSN-named backbone carries a gene named by its contig once renamed onto it', () => {
|
|
97
101
|
const pansn = convertGFAToGraph(
|
|
98
102
|
parseGFA(GFA.replaceAll('SN:Z:chr1', 'SN:Z:GRCh38#0#chr1')),
|
|
99
103
|
)
|
|
100
|
-
|
|
104
|
+
const genes = geneModelsFrom([gene])
|
|
105
|
+
expect(genePins(pansn, genes, positions)).toEqual([])
|
|
106
|
+
const renamed = featuresOnBackbone(genes, graphBackbone(pansn)!)
|
|
107
|
+
expect(genePins(pansn, renamed, positions)).toHaveLength(1)
|
|
101
108
|
})
|
|
102
109
|
|
|
103
110
|
test('a gene on another sequence pins nothing', () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { cutHolds, hostCut } from './host'
|
|
1
|
+
import { cutHolds, hostCut, hostFrame } from './host'
|
|
2
2
|
|
|
3
|
-
import type { HostWindow } from './host'
|
|
3
|
+
import type { HostWindow, LinearHost } from './host'
|
|
4
4
|
|
|
5
5
|
const window: HostWindow = {
|
|
6
6
|
refName: 'chr1',
|
|
@@ -39,3 +39,26 @@ test('a cut trimmed to a cap the window fills still holds that window', () => {
|
|
|
39
39
|
expect(cut.end - cut.start).toBeLessThanOrEqual(60_000)
|
|
40
40
|
expect(cutHolds(cut, window)).toBe(true)
|
|
41
41
|
})
|
|
42
|
+
|
|
43
|
+
test('a reversed block runs bp right to left from the same screen edge', () => {
|
|
44
|
+
const block = {
|
|
45
|
+
refName: 'chr1',
|
|
46
|
+
assemblyName: 'hg38',
|
|
47
|
+
start: 1000,
|
|
48
|
+
end: 2000,
|
|
49
|
+
offsetPx: 500,
|
|
50
|
+
}
|
|
51
|
+
const view = (reversed: boolean) =>
|
|
52
|
+
({
|
|
53
|
+
bpPerPx: 2,
|
|
54
|
+
offsetPx: 400,
|
|
55
|
+
dynamicBlocks: { contentBlocks: [{ ...block, reversed }] },
|
|
56
|
+
}) as unknown as LinearHost
|
|
57
|
+
const region = { refName: 'chr1', assemblyName: 'hg38', start: 0, end: 1 }
|
|
58
|
+
const at = (reversed: boolean, bp: number) => {
|
|
59
|
+
const frame = hostFrame(view(reversed), region)!
|
|
60
|
+
return bp * frame.scale + frame.translateX
|
|
61
|
+
}
|
|
62
|
+
expect([at(false, 1000), at(false, 2000)]).toEqual([100, 600])
|
|
63
|
+
expect([at(true, 2000), at(true, 1000)]).toEqual([100, 600])
|
|
64
|
+
})
|
|
@@ -65,7 +65,8 @@ export interface HostWindow {
|
|
|
65
65
|
// x is reference bp on an anchored layout, so the graph shows what the linear
|
|
66
66
|
// view shows when one bp is as many px in both and the cut's refName sits at
|
|
67
67
|
// the same screen x. Undefined when the linear view shows no block of that
|
|
68
|
-
// refName, which is a re-cut's job rather than the frame's.
|
|
68
|
+
// refName, which is a re-cut's job rather than the frame's. A reversed block
|
|
69
|
+
// runs bp right to left, which only the tube map's connectors can draw.
|
|
69
70
|
export function hostFrame(view: LinearHost, region: SubgraphRegion) {
|
|
70
71
|
const block = widest(
|
|
71
72
|
view.dynamicBlocks.contentBlocks.filter(
|
|
@@ -74,12 +75,13 @@ export function hostFrame(view: LinearHost, region: SubgraphRegion) {
|
|
|
74
75
|
),
|
|
75
76
|
)
|
|
76
77
|
const { bpPerPx } = view
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
:
|
|
78
|
+
if (!block || !(bpPerPx > 0)) {
|
|
79
|
+
return undefined
|
|
80
|
+
}
|
|
81
|
+
const left = block.offsetPx - view.offsetPx
|
|
82
|
+
return block.reversed
|
|
83
|
+
? { scale: -1 / bpPerPx, translateX: left + block.end / bpPerPx }
|
|
84
|
+
: { scale: 1 / bpPerPx, translateX: left - block.start / bpPerPx }
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
// Read off the LIVE blocks even when the coarse ones woke the caller: those
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// "the submenu is called something else".
|
|
5
5
|
|
|
6
6
|
import { spreadFor } from '@jbrowse/bandage-core/bubbleSpreads'
|
|
7
|
+
import { rowLabelBox } from '@jbrowse/bandage-core/graphLabels'
|
|
7
8
|
import {
|
|
8
9
|
PROPORTIONAL_LENGTH,
|
|
9
10
|
bandageAutoScale,
|
|
@@ -21,6 +22,8 @@ import { applySnapshot, getSnapshot } from '@jbrowse/mobx-state-tree'
|
|
|
21
22
|
import { MAX_GRAPH_REGION_BP, formatSpanBp } from './model'
|
|
22
23
|
import stateModelFactory from './viewModel'
|
|
23
24
|
|
|
25
|
+
import type { LayoutModeValue } from '@jbrowse/bandage-core/layoutModes'
|
|
26
|
+
import type { Renderer } from '@jbrowse/bandage-core/renderer/types'
|
|
24
27
|
import type { Graph } from '@jbrowse/bandage-core/types'
|
|
25
28
|
|
|
26
29
|
const mockRpcCall = vi.fn()
|
|
@@ -46,7 +49,14 @@ const mockSession = {
|
|
|
46
49
|
has: (name: string) => canonicalAssembly(name) !== undefined,
|
|
47
50
|
get: (name: string) => {
|
|
48
51
|
const canonical = canonicalAssembly(name)
|
|
49
|
-
return canonical === undefined
|
|
52
|
+
return canonical === undefined
|
|
53
|
+
? undefined
|
|
54
|
+
: {
|
|
55
|
+
name: canonical,
|
|
56
|
+
aliases: Object.keys(mockSession.assemblyAliases).filter(
|
|
57
|
+
alias => mockSession.assemblyAliases[alias] === canonical,
|
|
58
|
+
),
|
|
59
|
+
}
|
|
50
60
|
},
|
|
51
61
|
},
|
|
52
62
|
views: [] as unknown[],
|
|
@@ -134,6 +144,18 @@ function rpcRespond() {
|
|
|
134
144
|
})
|
|
135
145
|
}
|
|
136
146
|
|
|
147
|
+
function fakeRenderer() {
|
|
148
|
+
return {
|
|
149
|
+
resize: () => {},
|
|
150
|
+
uploadGeometry: () => {},
|
|
151
|
+
updateTransform: () => {},
|
|
152
|
+
render: () => {},
|
|
153
|
+
setNodeHighlights: () => {},
|
|
154
|
+
setEdgeHighlight: () => {},
|
|
155
|
+
dispose: () => {},
|
|
156
|
+
} as unknown as Renderer
|
|
157
|
+
}
|
|
158
|
+
|
|
137
159
|
function createModel() {
|
|
138
160
|
return stateModelFactory().create({ type: 'GraphGenomeView' })
|
|
139
161
|
}
|
|
@@ -1181,6 +1203,79 @@ describe('canvas height follows the drawing', () => {
|
|
|
1181
1203
|
})
|
|
1182
1204
|
})
|
|
1183
1205
|
|
|
1206
|
+
// A 22 kb cut of 1,295 nodes fitted whole drew at 3.4%, a strip of 1 px
|
|
1207
|
+
// tubes. On its own axis a tube map fits no smaller than 5 px tubes and is
|
|
1208
|
+
// panned along, as in sequenceTubeMap.
|
|
1209
|
+
describe('zoomToFit on a tube map', () => {
|
|
1210
|
+
// A SNP between each pair of backbone nodes, so the boxes cannot merge
|
|
1211
|
+
function snpChainGfa(snps: number) {
|
|
1212
|
+
const lines = ['H\tVN:Z:1.0', 'S\tb0\t*\tLN:i:1000']
|
|
1213
|
+
const ref = ['b0+']
|
|
1214
|
+
const alt = ['b0+']
|
|
1215
|
+
for (let i = 1; i <= snps; i++) {
|
|
1216
|
+
lines.push(
|
|
1217
|
+
`S\tr${i}\tA`,
|
|
1218
|
+
`S\ta${i}\tC`,
|
|
1219
|
+
`S\tb${i}\t*\tLN:i:1000`,
|
|
1220
|
+
`L\tb${i - 1}\t+\tr${i}\t+\t0M`,
|
|
1221
|
+
`L\tb${i - 1}\t+\ta${i}\t+\t0M`,
|
|
1222
|
+
`L\tr${i}\t+\tb${i}\t+\t0M`,
|
|
1223
|
+
`L\ta${i}\t+\tb${i}\t+\t0M`,
|
|
1224
|
+
)
|
|
1225
|
+
ref.push(`r${i}+`, `b${i}+`)
|
|
1226
|
+
alt.push(`a${i}+`, `b${i}+`)
|
|
1227
|
+
}
|
|
1228
|
+
lines.push(`P\tref\t${ref.join(',')}\t*`, `P\talt\t${alt.join(',')}\t*`)
|
|
1229
|
+
return `${lines.join('\n')}\n`
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
async function tubeMap(snps: number) {
|
|
1233
|
+
const model = stateModelFactory().create({
|
|
1234
|
+
type: 'GraphGenomeView',
|
|
1235
|
+
layoutMode: 'tubemap',
|
|
1236
|
+
})
|
|
1237
|
+
await model.loadGFA(snpChainGfa(snps), `${snps} SNPs`)
|
|
1238
|
+
model.zoomToFit()
|
|
1239
|
+
return model
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function tubePx(model: Awaited<ReturnType<typeof tubeMap>>) {
|
|
1243
|
+
return model.layoutResult!.tubeMap!.layout.tracks[0]!.width * model.scale
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
test('a cut too long for the pane opens at 5 px tubes, its left end on screen', async () => {
|
|
1247
|
+
const model = await tubeMap(100)
|
|
1248
|
+
const bounds = model.layoutBounds!
|
|
1249
|
+
expect(model.width - 80).toBeLessThan(bounds.w * model.scale)
|
|
1250
|
+
|
|
1251
|
+
expect(tubePx(model)).toBeCloseTo(5, 10)
|
|
1252
|
+
expect(bounds.minX * model.scale + model.translateX).toBeCloseTo(40, 5)
|
|
1253
|
+
expect(model.canvasHeight).toBe(Math.max(160, bounds.h * model.scale + 80))
|
|
1254
|
+
})
|
|
1255
|
+
|
|
1256
|
+
// An action's reads are untracked, so an autorun that called zoomToFit
|
|
1257
|
+
// refitted on a new layout and on nothing else
|
|
1258
|
+
test('a view the user has not moved refits when its pane widens', async () => {
|
|
1259
|
+
const model = await tubeMap(2)
|
|
1260
|
+
model.startRenderingBackend(fakeRenderer())
|
|
1261
|
+
const before = model.scale
|
|
1262
|
+
|
|
1263
|
+
model.setWidth(model.width * 2)
|
|
1264
|
+
expect(model.scale).toBeGreaterThan(before)
|
|
1265
|
+
})
|
|
1266
|
+
|
|
1267
|
+
test('a cut that fits with wider tubes still fits whole', async () => {
|
|
1268
|
+
const model = await tubeMap(2)
|
|
1269
|
+
const bounds = model.layoutBounds!
|
|
1270
|
+
|
|
1271
|
+
expect(tubePx(model)).toBeGreaterThan(5)
|
|
1272
|
+
const left = bounds.minX * model.scale + model.translateX
|
|
1273
|
+
const right = left + bounds.w * model.scale
|
|
1274
|
+
expect(left).toBeGreaterThanOrEqual(40 - 1e-6)
|
|
1275
|
+
expect(right).toBeLessThanOrEqual(model.width - 40 + 1e-6)
|
|
1276
|
+
})
|
|
1277
|
+
})
|
|
1278
|
+
|
|
1184
1279
|
// hoveredEdge is an index into graph.edges, so it addresses the graph it was set
|
|
1185
1280
|
// against; carrying it across a load pointed the tooltip and the highlight at
|
|
1186
1281
|
// whatever ended up at that index in the new graph.
|
|
@@ -1949,13 +2044,16 @@ describe('a row layout draws y in pixels', () => {
|
|
|
1949
2044
|
expect(model.canvasHeight).toBe(600)
|
|
1950
2045
|
// the vertical fit is the tighter one, and is not the one taken
|
|
1951
2046
|
expect((model.canvasHeight - 80) / model.layoutBounds!.h).toBeLessThan(
|
|
1952
|
-
(model.width -
|
|
2047
|
+
(model.width - model.fitPadLeft - 40) / model.layoutBounds!.w,
|
|
1953
2048
|
)
|
|
1954
2049
|
|
|
1955
2050
|
const xs = Object.values(model.nodePositions!)
|
|
1956
2051
|
.flat()
|
|
1957
2052
|
.map(p => p.x * model.scaleX + model.translateX)
|
|
1958
|
-
expect(Math.max(...xs) - Math.min(...xs)).toBeCloseTo(
|
|
2053
|
+
expect(Math.max(...xs) - Math.min(...xs)).toBeCloseTo(
|
|
2054
|
+
model.width - model.fitPadLeft - 40,
|
|
2055
|
+
5,
|
|
2056
|
+
)
|
|
1959
2057
|
})
|
|
1960
2058
|
|
|
1961
2059
|
// Rows are a track's row height, so zooming the sequence axis leaves them
|
|
@@ -2112,7 +2210,19 @@ describe('what the row axis draws, in pixels', () => {
|
|
|
2112
2210
|
// in the path.
|
|
2113
2211
|
test('the backbone spans the pane', async () => {
|
|
2114
2212
|
const model = await fitted()
|
|
2115
|
-
expect(spread(render(model).map(p => p.x))).toBeCloseTo(
|
|
2213
|
+
expect(spread(render(model).map(p => p.x))).toBeCloseTo(
|
|
2214
|
+
model.width - model.fitPadLeft - 40,
|
|
2215
|
+
5,
|
|
2216
|
+
)
|
|
2217
|
+
})
|
|
2218
|
+
|
|
2219
|
+
test('the fit starts the drawing right of the row labels', async () => {
|
|
2220
|
+
const model = await fitted()
|
|
2221
|
+
const labelsEnd = Math.max(
|
|
2222
|
+
...model.drawnRowLabels.map(r => rowLabelBox(r.label, 0).x1),
|
|
2223
|
+
)
|
|
2224
|
+
expect(labelsEnd).toBeGreaterThan(40)
|
|
2225
|
+
expect(Math.min(...render(model).map(p => p.x))).toBeGreaterThan(labelsEnd)
|
|
2116
2226
|
})
|
|
2117
2227
|
|
|
2118
2228
|
// Zoom is an x-only gesture on this axis, so afterwards the rows are drawn in
|
|
@@ -2495,3 +2605,272 @@ describe('annotation reads beside a cut', () => {
|
|
|
2495
2605
|
expect(model.geneFeatures?.map(g => g.name)).toEqual(['XYZ2'])
|
|
2496
2606
|
})
|
|
2497
2607
|
})
|
|
2608
|
+
|
|
2609
|
+
// GRCh38 and CHM13 walk chr6 at overlapping coordinates, so hg38's HLA-A lies
|
|
2610
|
+
// over segment 1 whichever walk x is drawn along. Only GRCh38's is hg38's.
|
|
2611
|
+
describe('genes on a backbone of another assembly', () => {
|
|
2612
|
+
const SEGMENTS = [
|
|
2613
|
+
'S\t1\tAAAAAAAAAA',
|
|
2614
|
+
'S\t2\tCCCCCCCCCC',
|
|
2615
|
+
'S\t3\tGGGGGGGGGG',
|
|
2616
|
+
'S\t4\tTTTTTTTTTT',
|
|
2617
|
+
'L\t1\t+\t2\t+\t0M',
|
|
2618
|
+
'L\t1\t+\t3\t+\t0M',
|
|
2619
|
+
'L\t2\t+\t4\t+\t0M',
|
|
2620
|
+
'L\t3\t+\t4\t+\t0M',
|
|
2621
|
+
]
|
|
2622
|
+
// A gbz-base cut writes the track's reference walk first
|
|
2623
|
+
const gfaOf = (...walks: string[]) => [...SEGMENTS, ...walks, ''].join('\n')
|
|
2624
|
+
const GRCH38 = 'W\tGRCh38\t0\tchr6\t1000\t1030\t>1>2>4'
|
|
2625
|
+
const CHM13 = 'W\tCHM13\t0\tchr6\t1005\t1035\t>1>3>4'
|
|
2626
|
+
const CHR6_WALKS = gfaOf(GRCH38, CHM13)
|
|
2627
|
+
// The same alleles named the way an older graph does, with no PanSN prefix to
|
|
2628
|
+
// say whose chr6 each is
|
|
2629
|
+
const BARE_CHR6_PATHS = gfaOf(
|
|
2630
|
+
'P\tchr6:1000-1030\t1+,2+,4+\t*',
|
|
2631
|
+
'P\tCHM13_chr6:1005-1035\t1+,3+,4+\t*',
|
|
2632
|
+
)
|
|
2633
|
+
const BARE_CHR6_RGFA = [
|
|
2634
|
+
'S\t1\tAAAAAAAAAA\tSN:Z:chr6\tSO:i:1000\tSR:i:0',
|
|
2635
|
+
'S\t2\tCCCCCCCCCC\tSN:Z:chr6\tSO:i:1010\tSR:i:0',
|
|
2636
|
+
'S\t3\tGGGGGGGGGG\tSN:Z:CHM13_chr6\tSO:i:1015\tSR:i:1',
|
|
2637
|
+
'S\t4\tTTTTTTTTTT\tSN:Z:chr6\tSO:i:1020\tSR:i:0',
|
|
2638
|
+
...SEGMENTS.filter(line => line.startsWith('L')),
|
|
2639
|
+
'',
|
|
2640
|
+
].join('\n')
|
|
2641
|
+
// chr6 bp that only GRCh38's walk covers, only CHM13's, and both
|
|
2642
|
+
const ON_GRCH38 = { start: 1000, end: 1004 }
|
|
2643
|
+
const ON_CHM13 = { start: 1031, end: 1034 }
|
|
2644
|
+
const ON_BOTH = { start: 1012, end: 1018 }
|
|
2645
|
+
|
|
2646
|
+
let gfa = CHR6_WALKS
|
|
2647
|
+
let geneAt = { refName: 'chr6', start: 1002, end: 1008 }
|
|
2648
|
+
const bySlot = (obj: Record<string, unknown>, key: string) => obj[key]
|
|
2649
|
+
const adapterOnly = (obj: Record<string, unknown>, key: string) =>
|
|
2650
|
+
key === 'adapter' ? obj.adapter : undefined
|
|
2651
|
+
|
|
2652
|
+
function sessionWith(assemblyName: string, graphAdapter = {}) {
|
|
2653
|
+
mockSession.tracks = [
|
|
2654
|
+
{ trackId: 'graph', assemblyNames: [], adapter: graphAdapter },
|
|
2655
|
+
{
|
|
2656
|
+
trackId: 'refseq',
|
|
2657
|
+
name: 'RefSeq genes',
|
|
2658
|
+
assemblyNames: [assemblyName],
|
|
2659
|
+
adapter: { type: 'Gff3TabixAdapter' },
|
|
2660
|
+
},
|
|
2661
|
+
]
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
beforeEach(() => {
|
|
2665
|
+
vi.mocked(readConfObject).mockImplementation(bySlot)
|
|
2666
|
+
sessionWith('hg38')
|
|
2667
|
+
gfa = CHR6_WALKS
|
|
2668
|
+
geneAt = { refName: 'chr6', start: 1002, end: 1008 }
|
|
2669
|
+
mockRpcCall.mockReset()
|
|
2670
|
+
mockRpcCall.mockImplementation((_sid: unknown, method: string) =>
|
|
2671
|
+
method === 'GetSubgraph'
|
|
2672
|
+
? Promise.resolve(gfa)
|
|
2673
|
+
: method === 'CoreGetFeatures'
|
|
2674
|
+
? Promise.resolve([{ type: 'gene', name: 'HLA-A', ...geneAt }])
|
|
2675
|
+
: Promise.reject(new Error(`Unexpected RPC: ${method}`)),
|
|
2676
|
+
)
|
|
2677
|
+
})
|
|
2678
|
+
|
|
2679
|
+
afterEach(() => {
|
|
2680
|
+
vi.mocked(readConfObject).mockImplementation(adapterOnly)
|
|
2681
|
+
mockSession.tracks = []
|
|
2682
|
+
mockSession.assemblyNames = []
|
|
2683
|
+
mockSession.assemblyAliases = {}
|
|
2684
|
+
})
|
|
2685
|
+
|
|
2686
|
+
async function cutFor(
|
|
2687
|
+
assemblyName: string,
|
|
2688
|
+
layoutMode: LayoutModeValue = 'auto',
|
|
2689
|
+
) {
|
|
2690
|
+
const model = stateModelFactory().create({
|
|
2691
|
+
type: 'GraphGenomeView',
|
|
2692
|
+
layoutMode,
|
|
2693
|
+
loadedTrackId: 'graph',
|
|
2694
|
+
})
|
|
2695
|
+
await model.cutSubgraph(
|
|
2696
|
+
{},
|
|
2697
|
+
{ refName: geneAt.refName, assemblyName, start: 1000, end: 1035 },
|
|
2698
|
+
)
|
|
2699
|
+
return model
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
type Model = Awaited<ReturnType<typeof cutFor>>
|
|
2703
|
+
const pinned = (model: Model) => model.genePins.map(p => p.gene.name)
|
|
2704
|
+
const tubeMapped = (model: Model) => model.tubeMapGenes.map(g => g.gene.name)
|
|
2705
|
+
|
|
2706
|
+
async function drawAlong(model: Model, path: string) {
|
|
2707
|
+
model.setReferencePath(path)
|
|
2708
|
+
await model.recomputeLayout()
|
|
2709
|
+
expect(model.activeReferencePath).toBe(path)
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
test.each([
|
|
2713
|
+
['auto', pinned],
|
|
2714
|
+
['tubemapref', tubeMapped],
|
|
2715
|
+
] as const)(
|
|
2716
|
+
'%s draws hg38 genes only while x is drawn along the GRCh38 walk',
|
|
2717
|
+
async (layoutMode, drawn) => {
|
|
2718
|
+
const model = await cutFor('hg38', layoutMode)
|
|
2719
|
+
expect(model.activeReferencePath).toBe('GRCh38#0#chr6')
|
|
2720
|
+
expect(drawn(model)).toEqual(['HLA-A'])
|
|
2721
|
+
|
|
2722
|
+
await drawAlong(model, 'CHM13#0#chr6')
|
|
2723
|
+
expect(model.geneFeatures).toHaveLength(1)
|
|
2724
|
+
expect(drawn(model)).toEqual([])
|
|
2725
|
+
|
|
2726
|
+
await drawAlong(model, 'GRCh38#0#chr6')
|
|
2727
|
+
expect(drawn(model)).toEqual(['HLA-A'])
|
|
2728
|
+
},
|
|
2729
|
+
)
|
|
2730
|
+
|
|
2731
|
+
test('an assembly’s session aliases or PanSN prefix put x on its walk', async () => {
|
|
2732
|
+
gfa = gfaOf(CHM13, GRCH38)
|
|
2733
|
+
geneAt = { refName: 'chr6', ...ON_GRCH38 }
|
|
2734
|
+
sessionWith('mine')
|
|
2735
|
+
const unnamed = await cutFor('mine')
|
|
2736
|
+
expect(unnamed.activeReferencePath).toBe('CHM13#0#chr6')
|
|
2737
|
+
expect(pinned(unnamed)).toEqual([])
|
|
2738
|
+
|
|
2739
|
+
mockSession.assemblyNames = ['mine']
|
|
2740
|
+
mockSession.assemblyAliases = { GRCh38: 'mine' }
|
|
2741
|
+
expect(pinned(await cutFor('mine'))).toEqual(['HLA-A'])
|
|
2742
|
+
|
|
2743
|
+
mockSession.assemblyAliases = {}
|
|
2744
|
+
sessionWith('mine', { assemblyNameToPanSN: { mine: 'GRCh38#0' } })
|
|
2745
|
+
expect(pinned(await cutFor('mine'))).toEqual(['HLA-A'])
|
|
2746
|
+
})
|
|
2747
|
+
|
|
2748
|
+
test('an assemblyNameToPanSN keyed by a session alias puts x on its walk', async () => {
|
|
2749
|
+
gfa = gfaOf(CHM13, GRCH38)
|
|
2750
|
+
geneAt = { refName: 'chr6', ...ON_GRCH38 }
|
|
2751
|
+
mockSession.assemblyNames = ['mine']
|
|
2752
|
+
mockSession.assemblyAliases = { 'GRCh38.p14': 'mine' }
|
|
2753
|
+
sessionWith('mine', { assemblyNameToPanSN: { 'GRCh38.p14': 'GRCh38#0' } })
|
|
2754
|
+
const model = await cutFor('mine')
|
|
2755
|
+
expect(model.activeReferencePath).toBe('GRCh38#0#chr6')
|
|
2756
|
+
expect(pinned(model)).toEqual(['HLA-A'])
|
|
2757
|
+
})
|
|
2758
|
+
|
|
2759
|
+
// gbz-base writes a reference path that names no sample as `_gbwt_ref`
|
|
2760
|
+
test('a vg graph’s generic reference takes its track’s genes', async () => {
|
|
2761
|
+
gfa = gfaOf(
|
|
2762
|
+
'W\t_gbwt_ref\t0\tchr6\t1000\t1030\t>1>2>4',
|
|
2763
|
+
'W\tHG002\t1\tchr6\t1005\t1035\t>1>3>4',
|
|
2764
|
+
)
|
|
2765
|
+
const model = await cutFor('hg38')
|
|
2766
|
+
expect(model.activeReferencePath).toBe('_gbwt_ref#0#chr6')
|
|
2767
|
+
expect(pinned(model)).toEqual(['HLA-A'])
|
|
2768
|
+
|
|
2769
|
+
await drawAlong(model, 'HG002#1#chr6')
|
|
2770
|
+
expect(pinned(model)).toEqual([])
|
|
2771
|
+
|
|
2772
|
+
await drawAlong(model, '_gbwt_ref#0#chr6')
|
|
2773
|
+
expect(pinned(model)).toEqual(['HLA-A'])
|
|
2774
|
+
})
|
|
2775
|
+
|
|
2776
|
+
// The adapter cuts along its referenceSample, else along the database's only
|
|
2777
|
+
// reference sample, and names neither after the assembly
|
|
2778
|
+
test.each([
|
|
2779
|
+
[
|
|
2780
|
+
'the referenceSample slot',
|
|
2781
|
+
'T2T-CHM13v2.0',
|
|
2782
|
+
{ type: 'GbzBaseSyntenyAdapter', referenceSample: 'CHM13' },
|
|
2783
|
+
gfaOf(CHM13, GRCH38),
|
|
2784
|
+
{ refName: 'chr6', ...ON_CHM13 },
|
|
2785
|
+
],
|
|
2786
|
+
[
|
|
2787
|
+
'the only reference sample',
|
|
2788
|
+
'mm39',
|
|
2789
|
+
{ type: 'GbzBaseSyntenyAdapter' },
|
|
2790
|
+
gfaOf(
|
|
2791
|
+
'W\tC57BL_6J\t0\tchr17\t1000\t1030\t>1>2>4',
|
|
2792
|
+
'W\tDBA_2J\t1\tchr17\t1005\t1035\t>1>3>4',
|
|
2793
|
+
),
|
|
2794
|
+
{ refName: 'chr17', ...ON_GRCH38 },
|
|
2795
|
+
],
|
|
2796
|
+
])(
|
|
2797
|
+
'the walk a GBZ track cuts along with %s takes its genes',
|
|
2798
|
+
async (_how, assemblyName, adapter, walks, at) => {
|
|
2799
|
+
gfa = walks
|
|
2800
|
+
geneAt = at
|
|
2801
|
+
sessionWith(assemblyName, adapter)
|
|
2802
|
+
const model = await cutFor(assemblyName)
|
|
2803
|
+
expect(pinned(model)).toEqual(['HLA-A'])
|
|
2804
|
+
},
|
|
2805
|
+
)
|
|
2806
|
+
|
|
2807
|
+
test('a re-cut keeps the genes off a walk the user drew x along', async () => {
|
|
2808
|
+
gfa = gfaOf(CHM13, GRCH38)
|
|
2809
|
+
geneAt = { refName: 'chr6', ...ON_BOTH }
|
|
2810
|
+
sessionWith('T2T-CHM13v2.0', {
|
|
2811
|
+
type: 'GbzBaseSyntenyAdapter',
|
|
2812
|
+
referenceSample: 'CHM13',
|
|
2813
|
+
})
|
|
2814
|
+
const model = await cutFor('T2T-CHM13v2.0')
|
|
2815
|
+
expect(pinned(model)).toEqual(['HLA-A'])
|
|
2816
|
+
|
|
2817
|
+
await drawAlong(model, 'GRCh38#0#chr6')
|
|
2818
|
+
expect(pinned(model)).toEqual([])
|
|
2819
|
+
|
|
2820
|
+
const region = model.graphRegion
|
|
2821
|
+
await model.cutSubgraph(
|
|
2822
|
+
{},
|
|
2823
|
+
{
|
|
2824
|
+
refName: 'chr6',
|
|
2825
|
+
assemblyName: 'T2T-CHM13v2.0',
|
|
2826
|
+
start: 1005,
|
|
2827
|
+
end: 1035,
|
|
2828
|
+
},
|
|
2829
|
+
)
|
|
2830
|
+
expect(model.graphRegion).not.toBe(region)
|
|
2831
|
+
expect(model.activeReferencePath).toBe('GRCh38#0#chr6')
|
|
2832
|
+
expect(pinned(model)).toEqual([])
|
|
2833
|
+
|
|
2834
|
+
await drawAlong(model, 'CHM13#0#chr6')
|
|
2835
|
+
expect(pinned(model)).toEqual(['HLA-A'])
|
|
2836
|
+
})
|
|
2837
|
+
|
|
2838
|
+
test('a diploid’s sample-level name leaves the other haplotype bare', async () => {
|
|
2839
|
+
gfa = gfaOf(
|
|
2840
|
+
'W\tHG002\t1\tchr6\t1000\t1030\t>1>2>4',
|
|
2841
|
+
'W\tHG002\t2\tchr6\t1000\t1030\t>1>3>4',
|
|
2842
|
+
)
|
|
2843
|
+
sessionWith('HG002.1', { assemblyNameToPanSN: { 'HG002.1': 'HG002' } })
|
|
2844
|
+
const model = await cutFor('HG002.1')
|
|
2845
|
+
expect(model.activeReferencePath).toBe('HG002#1#chr6')
|
|
2846
|
+
expect(pinned(model)).toEqual(['HLA-A'])
|
|
2847
|
+
|
|
2848
|
+
await drawAlong(model, 'HG002#2#chr6')
|
|
2849
|
+
expect(pinned(model)).toEqual([])
|
|
2850
|
+
})
|
|
2851
|
+
|
|
2852
|
+
test('a bare rGFA takes the assembly its track puts it on', async () => {
|
|
2853
|
+
gfa = BARE_CHR6_RGFA
|
|
2854
|
+
const model = await cutFor('hg38')
|
|
2855
|
+
expect(pinned(model)).toEqual(['HLA-A'])
|
|
2856
|
+
|
|
2857
|
+
model.setReferencePath('CHM13_chr6')
|
|
2858
|
+
await model.recomputeLayout()
|
|
2859
|
+
expect(pinned(model)).toEqual(['HLA-A'])
|
|
2860
|
+
})
|
|
2861
|
+
|
|
2862
|
+
test('a bare path graph takes it only along the path it came with', async () => {
|
|
2863
|
+
gfa = BARE_CHR6_PATHS
|
|
2864
|
+
const model = await cutFor('hg38')
|
|
2865
|
+
expect(model.activeReferencePath).toBe('chr6')
|
|
2866
|
+
expect(pinned(model)).toEqual(['HLA-A'])
|
|
2867
|
+
|
|
2868
|
+
await drawAlong(model, 'CHM13_chr6')
|
|
2869
|
+
expect(model.geneFeatures).toHaveLength(1)
|
|
2870
|
+
expect(model.backboneGenes).toBeUndefined()
|
|
2871
|
+
expect(pinned(model)).toEqual([])
|
|
2872
|
+
|
|
2873
|
+
await drawAlong(model, 'chr6')
|
|
2874
|
+
expect(pinned(model)).toEqual(['HLA-A'])
|
|
2875
|
+
})
|
|
2876
|
+
})
|