jbrowse-plugin-graphgenomeviewer 7.4.4 → 7.6.0
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/dist/chunks/{GbzBaseSyntenyAdapter-A5MNPCKX.js → GbzBaseSyntenyAdapter-3PVPTKNW.js} +2 -2
- package/dist/chunks/{GbzBaseSyntenyAdapter-A5MNPCKX.js.map → GbzBaseSyntenyAdapter-3PVPTKNW.js.map} +1 -1
- package/dist/chunks/{GraphGenomeView-SEAPDRXS.js → GraphGenomeView-GQW4YC23.js} +2 -2
- package/dist/chunks/GraphTrackSettingsDialog-ARWCWJW4.js +4 -0
- package/dist/chunks/GraphTrackSettingsDialog-ARWCWJW4.js.map +7 -0
- package/dist/chunks/{HighlightColorDialog-QAYWLO5I.js → HighlightColorDialog-PD3KMF2U.js} +2 -2
- package/dist/chunks/LinearGraphDisplay-QNS4EAL3.js +2 -0
- package/dist/chunks/{LinearGraphDisplay-ZC4BQ27J.js.map → LinearGraphDisplay-QNS4EAL3.js.map} +2 -2
- package/dist/chunks/{MinigraphBubbleAdapter-3QM4XV47.js → MinigraphBubbleAdapter-L6JSQIOL.js} +2 -2
- package/dist/chunks/RgfaTabixAdapter-5HCAJ7QI.js +2 -0
- package/dist/chunks/RgfaTabixAdapter-5HCAJ7QI.js.map +7 -0
- package/dist/chunks/{WalkFormsAdapter-FT2SW4H6.js → WalkFormsAdapter-NX36GP35.js} +2 -2
- package/dist/chunks/WalkTabixSyntenyAdapter-JFNFX3JP.js +2 -0
- package/dist/chunks/{WalkTabixSyntenyAdapter-3WRVNDLB.js.map → WalkTabixSyntenyAdapter-JFNFX3JP.js.map} +2 -2
- package/dist/chunks/{chunk-KIYRMZWP.js → chunk-2CPD5NX2.js} +2 -2
- package/dist/chunks/{chunk-KIYRMZWP.js.map → chunk-2CPD5NX2.js.map} +1 -1
- package/dist/chunks/chunk-3AC33AXB.js +2 -0
- package/dist/chunks/{chunk-HF4H6LQI.js → chunk-4PHM6JNU.js} +2 -2
- package/dist/chunks/chunk-5LFPNQWH.js +2 -0
- package/dist/chunks/{chunk-6VNU7SD4.js.map → chunk-5LFPNQWH.js.map} +4 -4
- package/dist/chunks/{chunk-6WUWQX2P.js → chunk-CNEUOM7X.js} +2 -2
- package/dist/chunks/{chunk-6WUWQX2P.js.map → chunk-CNEUOM7X.js.map} +1 -1
- package/dist/chunks/{chunk-P3FBI5ML.js → chunk-DOC7QIE2.js} +1 -1
- package/dist/chunks/{chunk-P3FBI5ML.js.map → chunk-DOC7QIE2.js.map} +1 -1
- package/dist/chunks/{chunk-G53BI7VK.js → chunk-G4OPM6G7.js} +2 -2
- package/dist/chunks/{chunk-G53BI7VK.js.map → chunk-G4OPM6G7.js.map} +1 -1
- package/dist/chunks/chunk-GRLNDNX4.js +2 -0
- package/dist/chunks/{chunk-S333HOG5.js.map → chunk-GRLNDNX4.js.map} +4 -4
- package/dist/chunks/{chunk-7RJMG6I2.js → chunk-JYYF3U2H.js} +1 -1
- package/dist/chunks/{chunk-7RJMG6I2.js.map → chunk-JYYF3U2H.js.map} +1 -1
- package/dist/chunks/chunk-KDXVLEHS.js +3 -0
- package/dist/chunks/chunk-KDXVLEHS.js.map +7 -0
- package/dist/chunks/chunk-LGV6BTOY.js +2 -0
- package/dist/chunks/{chunk-HCDKP3IP.js.map → chunk-LGV6BTOY.js.map} +4 -4
- package/dist/chunks/{chunk-2KVE7YAS.js → chunk-LJ7O3736.js} +1 -1
- package/dist/chunks/{chunk-2KVE7YAS.js.map → chunk-LJ7O3736.js.map} +1 -1
- package/dist/chunks/{chunk-OJWBLPPH.js → chunk-OQHN2GWX.js} +2 -2
- package/dist/chunks/chunk-PTDOYJFH.js +4 -0
- package/dist/chunks/chunk-PTDOYJFH.js.map +7 -0
- package/dist/chunks/chunk-SGP63UMC.js +2 -0
- package/dist/chunks/chunk-SGP63UMC.js.map +7 -0
- package/dist/chunks/{chunk-XXBE4IFZ.js → chunk-SI5VLKVN.js} +2 -2
- package/dist/chunks/{chunk-XXBE4IFZ.js.map → chunk-SI5VLKVN.js.map} +1 -1
- package/dist/chunks/{chunk-6SWXMRAV.js → chunk-TWPUVVYH.js} +1 -1
- package/dist/chunks/{chunk-6SWXMRAV.js.map → chunk-TWPUVVYH.js.map} +1 -1
- package/dist/chunks/{chunk-FTXDTU27.js → chunk-XF6GU4GK.js} +3 -3
- package/dist/chunks/{chunk-FTXDTU27.js.map → chunk-XF6GU4GK.js.map} +2 -2
- package/dist/chunks/{chunk-MUONTMYZ.js → chunk-XSDGE4RF.js} +2 -2
- package/dist/chunks/{chunk-LE7FZM6Y.js → chunk-ZFJGNGIB.js} +2 -2
- package/dist/chunks/{chunk-LE7FZM6Y.js.map → chunk-ZFJGNGIB.js.map} +1 -1
- package/dist/chunks/chunk-ZI7RFIA2.js +4 -0
- package/dist/chunks/chunk-ZI7RFIA2.js.map +7 -0
- package/dist/chunks/{renderSvg-ZIUOPWJQ.js → renderSvg-NIWFCY3U.js} +2 -2
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +4 -3
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
- package/package.json +2 -2
- package/src/GraphComputeLayout.test.ts +24 -0
- package/src/GraphComputeLayout.ts +4 -2
- package/src/GraphGenomeView/components/GraphSettingsDialog.tsx +16 -0
- package/src/GraphGenomeView/components/WalkRowsOverlay.tsx +83 -0
- package/src/GraphGenomeView/components/usePaneGestures.test.tsx +82 -5
- package/src/GraphGenomeView/components/usePaneGestures.ts +10 -4
- package/src/GraphGenomeView/graphLayers.ts +14 -1
- package/src/GraphGenomeView/legendSections.ts +32 -0
- package/src/GraphGenomeView/model.test.ts +307 -7
- package/src/GraphGenomeView/model.ts +3 -10
- package/src/GraphGenomeView/nodeColor.test.ts +27 -1
- package/src/GraphGenomeView/nodeColor.ts +38 -29
- package/src/GraphGenomeView/nodeSize.test.ts +21 -5
- package/src/GraphGenomeView/nodeSize.ts +24 -8
- package/src/GraphGenomeView/pane/fitViews.ts +5 -1
- package/src/GraphGenomeView/pane/graphViews.ts +102 -20
- package/src/GraphGenomeView/pane/launchActions.ts +55 -11
- package/src/GraphGenomeView/pane/launchViews.ts +46 -0
- package/src/GraphGenomeView/pane/loadActions.ts +85 -25
- package/src/GraphGenomeView/pane/morph.test.ts +25 -1
- package/src/GraphGenomeView/pane/morph.ts +3 -1
- package/src/GraphGenomeView/pane/paneBase.ts +5 -0
- package/src/GraphGenomeView/pane/renderingActions.ts +61 -2
- package/src/GraphGenomeView/pane/settingActions.ts +65 -5
- package/src/GraphGenomeView/pane/walkRowViews.ts +17 -6
- package/src/GraphGenomeView/retiredNodeColor.test.ts +46 -0
- package/src/GraphGenomeView/retiredNodeColor.ts +76 -0
- package/src/GraphGenomeView/tubeMapPanels.ts +2 -1
- package/src/GraphGenomeView/viewModel.ts +16 -9
- package/src/GraphGenomeView/walkRowBlocks.ts +50 -0
- package/src/LinearGraphDisplay/LinearGraphDisplay.test.ts +97 -10
- package/src/LinearGraphDisplay/components/GraphStatusChrome.test.tsx +18 -0
- package/src/LinearGraphDisplay/components/GraphStatusChrome.tsx +4 -2
- package/src/LinearGraphDisplay/components/SubgraphHaplotypesField.test.tsx +41 -1
- package/src/LinearGraphDisplay/components/SubgraphHaplotypesField.tsx +33 -19
- package/src/LinearGraphDisplay/configSchema.ts +18 -10
- package/src/LinearGraphDisplay/model.ts +4 -2
- package/src/RgfaTabixAdapter/RgfaTabixAdapter.ts +2 -9
- package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.ts +3 -6
- package/src/hoverSync/graphViewHighlights.test.ts +53 -0
- package/src/hoverSync/graphViewHighlights.ts +25 -0
- package/src/hoverSync/lgvHover.test.ts +100 -1
- package/src/hoverSync/lgvHover.ts +77 -6
- package/src/launchFromGraph/contributors.test.ts +52 -0
- package/src/launchFromGraph/contributors.ts +105 -1
- package/src/launchFromGraph/launchFromGraph.ts +2 -2
- package/src/panSNTabix.test.ts +28 -0
- package/src/panSNTabix.ts +26 -8
- package/src/version.ts +1 -1
- package/dist/chunks/GraphTrackSettingsDialog-EVIIGC5G.js +0 -6
- package/dist/chunks/GraphTrackSettingsDialog-EVIIGC5G.js.map +0 -7
- package/dist/chunks/LinearGraphDisplay-ZC4BQ27J.js +0 -2
- package/dist/chunks/RgfaTabixAdapter-XSOIEWBK.js +0 -2
- package/dist/chunks/RgfaTabixAdapter-XSOIEWBK.js.map +0 -7
- package/dist/chunks/WalkTabixSyntenyAdapter-3WRVNDLB.js +0 -2
- package/dist/chunks/chunk-6VNU7SD4.js +0 -2
- package/dist/chunks/chunk-GRTN25B6.js +0 -3
- package/dist/chunks/chunk-GRTN25B6.js.map +0 -7
- package/dist/chunks/chunk-HCDKP3IP.js +0 -2
- package/dist/chunks/chunk-LE43I4PT.js +0 -4
- package/dist/chunks/chunk-LE43I4PT.js.map +0 -7
- package/dist/chunks/chunk-QV7SSOMJ.js +0 -2
- package/dist/chunks/chunk-S333HOG5.js +0 -2
- package/dist/chunks/chunk-VZQ6VOUP.js +0 -2
- package/dist/chunks/chunk-VZQ6VOUP.js.map +0 -7
- package/dist/chunks/chunk-WLRRI35A.js +0 -3
- package/dist/chunks/chunk-WLRRI35A.js.map +0 -7
- package/src/launchFromGraph/launchTracks.test.ts +0 -95
- package/src/launchFromGraph/launchTracks.ts +0 -49
- /package/dist/chunks/{GraphGenomeView-SEAPDRXS.js.map → GraphGenomeView-GQW4YC23.js.map} +0 -0
- /package/dist/chunks/{HighlightColorDialog-QAYWLO5I.js.map → HighlightColorDialog-PD3KMF2U.js.map} +0 -0
- /package/dist/chunks/{MinigraphBubbleAdapter-3QM4XV47.js.map → MinigraphBubbleAdapter-L6JSQIOL.js.map} +0 -0
- /package/dist/chunks/{WalkFormsAdapter-FT2SW4H6.js.map → WalkFormsAdapter-NX36GP35.js.map} +0 -0
- /package/dist/chunks/{chunk-QV7SSOMJ.js.map → chunk-3AC33AXB.js.map} +0 -0
- /package/dist/chunks/{chunk-HF4H6LQI.js.map → chunk-4PHM6JNU.js.map} +0 -0
- /package/dist/chunks/{chunk-OJWBLPPH.js.map → chunk-OQHN2GWX.js.map} +0 -0
- /package/dist/chunks/{chunk-MUONTMYZ.js.map → chunk-XSDGE4RF.js.map} +0 -0
- /package/dist/chunks/{renderSvg-ZIUOPWJQ.js.map → renderSvg-NIWFCY3U.js.map} +0 -0
|
@@ -156,6 +156,7 @@ vi.mock('@jbrowse/core/util/io', () => ({
|
|
|
156
156
|
}))
|
|
157
157
|
|
|
158
158
|
const SIMPLE_GFA = 'H\tVN:Z:1.0\nS\t1\tACGT\nS\t2\tGGCC\nL\t1\t+\t2\t+\t0M\n'
|
|
159
|
+
const WALKED_GFA = `${SIMPLE_GFA}W\tGRCh38\t0\tchr1\t0\t8\t>1>2\nW\tA\t1\tctg\t0\t8\t>1>2\n`
|
|
159
160
|
|
|
160
161
|
const MOCK_LAYOUT = {
|
|
161
162
|
nodePositions: {
|
|
@@ -218,7 +219,9 @@ function createAnchoredModel(props: { layers?: GraphLayers } = {}) {
|
|
|
218
219
|
|
|
219
220
|
// A view attached to a tree, so afterAttach installs its reactions. A root
|
|
220
221
|
// model never attaches.
|
|
221
|
-
function attachedModel(
|
|
222
|
+
function attachedModel(
|
|
223
|
+
props: { layoutMode?: LayoutModeValue; [key: string]: unknown } = {},
|
|
224
|
+
) {
|
|
222
225
|
return types
|
|
223
226
|
.model({ view: stateModelFactory() })
|
|
224
227
|
.create({ view: { type: 'GraphGenomeView', ...props } }).view
|
|
@@ -292,12 +295,24 @@ describe('rampDomain', () => {
|
|
|
292
295
|
expect(model.rampDomain).toBeUndefined()
|
|
293
296
|
})
|
|
294
297
|
|
|
298
|
+
test('a saved flat color is read as the channel object', () => {
|
|
299
|
+
const model = restoredView({
|
|
300
|
+
color: { field: 'position', domainMin: 5, domainMax: 50 },
|
|
301
|
+
})
|
|
302
|
+
expect(model.color).toEqual({
|
|
303
|
+
field: 'position',
|
|
304
|
+
scale: { domainMin: 5, domainMax: 50 },
|
|
305
|
+
})
|
|
306
|
+
expect(model.statedColorDomain).toEqual({ start: 5, end: 50 })
|
|
307
|
+
expect(restoredView({ color: 'uniform' }).chosenColorScheme).toBe('uniform')
|
|
308
|
+
})
|
|
309
|
+
|
|
295
310
|
test('a stated color domain survives loading a GFA file, and wins', async () => {
|
|
296
311
|
rpcRespond()
|
|
297
312
|
const model = createModel()
|
|
298
313
|
applySnapshot(model, {
|
|
299
314
|
...getSnapshot(model),
|
|
300
|
-
color: { domainMin: 1445000, domainMax: 1474500 },
|
|
315
|
+
color: { scale: { domainMin: 1445000, domainMax: 1474500 } },
|
|
301
316
|
loadedRegion: TEST_REGION,
|
|
302
317
|
})
|
|
303
318
|
await model.loadGFA(SIMPLE_GFA)
|
|
@@ -366,6 +381,30 @@ describe('restoring a stated cut', () => {
|
|
|
366
381
|
)
|
|
367
382
|
})
|
|
368
383
|
|
|
384
|
+
// Only a GBZ cut follows snarls past the window; a walk file cuts the same
|
|
385
|
+
// graph either way, so the strip's toggle has nothing to fetch.
|
|
386
|
+
test.each([
|
|
387
|
+
['RgfaTabixAdapter', 0],
|
|
388
|
+
['GbzBaseSyntenyAdapter', 1],
|
|
389
|
+
])('the walk strip on a %s track cuts again %i times', async (type, n) => {
|
|
390
|
+
rpcRespond()
|
|
391
|
+
mockSession.tracks = [{ trackId: 'rgfa-track', adapter: { type } }]
|
|
392
|
+
const model = attachedModel({
|
|
393
|
+
layoutMode: 'force',
|
|
394
|
+
loadedTrackId: 'rgfa-track',
|
|
395
|
+
loadedRegion: TEST_REGION,
|
|
396
|
+
})
|
|
397
|
+
await vi.waitFor(() => {
|
|
398
|
+
expect(model.graph).toBeDefined()
|
|
399
|
+
})
|
|
400
|
+
const cuts = () =>
|
|
401
|
+
mockRpcCall.mock.calls.filter(c => c[1] === 'GetSubgraph').length
|
|
402
|
+
const before = cuts()
|
|
403
|
+
model.setWalkStrip(!model.walkStrip)
|
|
404
|
+
await layoutsSettled(model)
|
|
405
|
+
expect(cuts()).toBe(before + n)
|
|
406
|
+
})
|
|
407
|
+
|
|
369
408
|
test('a stated track the session lacks is reported without a fetch', async () => {
|
|
370
409
|
mockSession.tracks = []
|
|
371
410
|
const model = restoredView()
|
|
@@ -1136,7 +1175,7 @@ describe('layoutMode', () => {
|
|
|
1136
1175
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
1137
1176
|
rpcRespond()
|
|
1138
1177
|
const model = attachedModel()
|
|
1139
|
-
await model.loadGFA(
|
|
1178
|
+
await model.loadGFA(WALKED_GFA, 'plain')
|
|
1140
1179
|
|
|
1141
1180
|
model.setMaxGraphNodes(1)
|
|
1142
1181
|
await layoutsSettled(model)
|
|
@@ -1434,7 +1473,7 @@ describe('node budget', () => {
|
|
|
1434
1473
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
1435
1474
|
rpcRespond()
|
|
1436
1475
|
const model = withLimit(1)
|
|
1437
|
-
await model.loadGFA(
|
|
1476
|
+
await model.loadGFA(WALKED_GFA, 'two nodes')
|
|
1438
1477
|
expect(String(model.error)).toMatch(/too large to draw/i)
|
|
1439
1478
|
|
|
1440
1479
|
await model.switchLayout('walkrows')
|
|
@@ -1443,6 +1482,21 @@ describe('node budget', () => {
|
|
|
1443
1482
|
consoleSpy.mockRestore()
|
|
1444
1483
|
})
|
|
1445
1484
|
|
|
1485
|
+
// walk rows hand a graph with no walks to the force engine, which draws
|
|
1486
|
+
// every node
|
|
1487
|
+
test('walk rows of a graph with no walks keep the budget', async () => {
|
|
1488
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
1489
|
+
rpcRespond()
|
|
1490
|
+
const model = withLimit(1)
|
|
1491
|
+
model.setLayoutMode('walkrows')
|
|
1492
|
+
await model.loadGFA(SIMPLE_GFA, 'two nodes')
|
|
1493
|
+
expect(String(model.error)).toMatch(/too large to draw/i)
|
|
1494
|
+
expect(
|
|
1495
|
+
mockRpcCall.mock.calls.filter(c => c[1] === 'GraphComputeLayout'),
|
|
1496
|
+
).toHaveLength(0)
|
|
1497
|
+
consoleSpy.mockRestore()
|
|
1498
|
+
})
|
|
1499
|
+
|
|
1446
1500
|
test('a declined graph does not keep the last graph drawn under it', async () => {
|
|
1447
1501
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
1448
1502
|
rpcRespond()
|
|
@@ -1466,7 +1520,7 @@ describe('node budget', () => {
|
|
|
1466
1520
|
rpcRespond()
|
|
1467
1521
|
const model = withLimit(1)
|
|
1468
1522
|
model.setLayoutMode('walkrows')
|
|
1469
|
-
await model.loadGFA(
|
|
1523
|
+
await model.loadGFA(WALKED_GFA, 'two nodes')
|
|
1470
1524
|
expect(model.error).toBeUndefined()
|
|
1471
1525
|
expect(model.layoutResult).toBeDefined()
|
|
1472
1526
|
})
|
|
@@ -2279,6 +2333,61 @@ describe('launching out of the graph', () => {
|
|
|
2279
2333
|
// Where every contributor is a loaded assembly the graph offers one panel per
|
|
2280
2334
|
// strain, and a synteny view of all of them — the item is disabled while no
|
|
2281
2335
|
// synteny track aligns them, rather than absent.
|
|
2336
|
+
// Sakai's allele is drawn bare unless Sakai's own annotation is read onto
|
|
2337
|
+
// it: the reference's genes only reach the backbone
|
|
2338
|
+
test("a loaded strain's own genes are read onto its nodes off the reference", async () => {
|
|
2339
|
+
mockSession.assemblyNames = ['K12', 'Sakai']
|
|
2340
|
+
mockSession.tracks = [
|
|
2341
|
+
{
|
|
2342
|
+
trackId: 'Sakai_genes',
|
|
2343
|
+
name: 'Sakai genes',
|
|
2344
|
+
assemblyNames: ['Sakai'],
|
|
2345
|
+
adapter: { type: 'Gff3TabixAdapter' },
|
|
2346
|
+
},
|
|
2347
|
+
]
|
|
2348
|
+
const model = await loadedGraph(ECOLI_RGFA, {
|
|
2349
|
+
refName: 'chr',
|
|
2350
|
+
assemblyName: 'K12',
|
|
2351
|
+
start: 1000,
|
|
2352
|
+
end: 1020,
|
|
2353
|
+
})
|
|
2354
|
+
expect(model.ownGeneReads).toEqual([
|
|
2355
|
+
{
|
|
2356
|
+
stableRefName: 'Sakai#1#chr',
|
|
2357
|
+
trackId: 'Sakai_genes',
|
|
2358
|
+
region: {
|
|
2359
|
+
assemblyName: 'Sakai',
|
|
2360
|
+
refName: 'chr',
|
|
2361
|
+
start: 90000,
|
|
2362
|
+
end: 90004,
|
|
2363
|
+
},
|
|
2364
|
+
},
|
|
2365
|
+
])
|
|
2366
|
+
mockRpcCall.mockImplementation((_sid: unknown, method: string) =>
|
|
2367
|
+
method === 'CoreGetFeatures'
|
|
2368
|
+
? Promise.resolve([
|
|
2369
|
+
{
|
|
2370
|
+
name: 'stx2A',
|
|
2371
|
+
refName: 'chr',
|
|
2372
|
+
start: 90001,
|
|
2373
|
+
end: 90003,
|
|
2374
|
+
strand: 1,
|
|
2375
|
+
subfeatures: [{ type: 'exon', start: 90001, end: 90003 }],
|
|
2376
|
+
},
|
|
2377
|
+
])
|
|
2378
|
+
: Promise.reject(new Error(`Unexpected RPC: ${method}`)),
|
|
2379
|
+
)
|
|
2380
|
+
await model.loadOwnGenes()
|
|
2381
|
+
expect(model.ownGeneFeatures).toEqual([
|
|
2382
|
+
expect.objectContaining({
|
|
2383
|
+
name: 'stx2A · Sakai#1',
|
|
2384
|
+
refName: 'Sakai#1#chr',
|
|
2385
|
+
start: 90001,
|
|
2386
|
+
end: 90003,
|
|
2387
|
+
}),
|
|
2388
|
+
])
|
|
2389
|
+
})
|
|
2390
|
+
|
|
2282
2391
|
test('loaded strains offer a per-strain linear view and a synteny item', async () => {
|
|
2283
2392
|
mockSession.assemblyNames = ['K12', 'Sakai']
|
|
2284
2393
|
const model = await loadedGraph(ECOLI_RGFA, {
|
|
@@ -2345,6 +2454,68 @@ describe('launching out of the graph', () => {
|
|
|
2345
2454
|
expect(model.connectedViewId).toBe('view-1')
|
|
2346
2455
|
})
|
|
2347
2456
|
|
|
2457
|
+
test('an opened view carries one gene track, not every feature track', async () => {
|
|
2458
|
+
mockSession.assemblyNames = ['hg38']
|
|
2459
|
+
const feature = (trackId: string, name: string, type: string) => ({
|
|
2460
|
+
trackId,
|
|
2461
|
+
name,
|
|
2462
|
+
type: 'FeatureTrack',
|
|
2463
|
+
assemblyNames: ['hg38'],
|
|
2464
|
+
adapter: { type },
|
|
2465
|
+
})
|
|
2466
|
+
mockSession.tracks = [
|
|
2467
|
+
feature('rgfa-track', 'graph', 'RgfaTabixAdapter'),
|
|
2468
|
+
feature('clinvar', 'ClinVar', 'BigBedAdapter'),
|
|
2469
|
+
feature('ncbi', 'NCBI RefSeq genes', 'Gff3TabixAdapter'),
|
|
2470
|
+
feature('gwas', 'GWAS catalog', 'BedTabixAdapter'),
|
|
2471
|
+
]
|
|
2472
|
+
const model = await loadedGraph(HPRC_RGFA)
|
|
2473
|
+
const { reference } = model.nodeLaunchTargets('1+')
|
|
2474
|
+
model.showInLinearView(reference!)
|
|
2475
|
+
|
|
2476
|
+
expect(mockSession.addedViews).toEqual([
|
|
2477
|
+
[
|
|
2478
|
+
'LinearGenomeView',
|
|
2479
|
+
expect.objectContaining({
|
|
2480
|
+
init: expect.objectContaining({ tracks: ['rgfa-track', 'ncbi'] }),
|
|
2481
|
+
}),
|
|
2482
|
+
],
|
|
2483
|
+
])
|
|
2484
|
+
})
|
|
2485
|
+
|
|
2486
|
+
// a BED named for no annotation is not picked as genes, but beats `No
|
|
2487
|
+
// tracks active`
|
|
2488
|
+
test('a strain with no gene-looking track opens on its first feature track', async () => {
|
|
2489
|
+
mockSession.assemblyNames = ['K12', 'Sakai']
|
|
2490
|
+
mockSession.tracks = [
|
|
2491
|
+
{
|
|
2492
|
+
trackId: 'Sakai_segments',
|
|
2493
|
+
name: 'Sakai segments',
|
|
2494
|
+
type: 'FeatureTrack',
|
|
2495
|
+
assemblyNames: ['Sakai'],
|
|
2496
|
+
adapter: { type: 'BedAdapter' },
|
|
2497
|
+
},
|
|
2498
|
+
]
|
|
2499
|
+
const model = await loadedGraph(ECOLI_RGFA, {
|
|
2500
|
+
refName: 'chr',
|
|
2501
|
+
assemblyName: 'K12',
|
|
2502
|
+
start: 1000,
|
|
2503
|
+
end: 1020,
|
|
2504
|
+
})
|
|
2505
|
+
model.showInLinearView(model.nodeLaunchTargets('3+').own!)
|
|
2506
|
+
expect(mockSession.addedViews).toEqual([
|
|
2507
|
+
[
|
|
2508
|
+
'LinearGenomeView',
|
|
2509
|
+
expect.objectContaining({
|
|
2510
|
+
init: expect.objectContaining({
|
|
2511
|
+
assembly: 'Sakai',
|
|
2512
|
+
tracks: ['Sakai_segments'],
|
|
2513
|
+
}),
|
|
2514
|
+
}),
|
|
2515
|
+
],
|
|
2516
|
+
])
|
|
2517
|
+
})
|
|
2518
|
+
|
|
2348
2519
|
// A pairing is not stolen from a view still on screen: a graph launched from
|
|
2349
2520
|
// a linear view came in on that sync. One whose view has been closed is held
|
|
2350
2521
|
// by nothing, and kept, the view the graph opens next never got its hover.
|
|
@@ -2978,7 +3149,7 @@ describe('the auto color scheme', () => {
|
|
|
2978
3149
|
const model = createModel()
|
|
2979
3150
|
applySnapshot(model, {
|
|
2980
3151
|
...getSnapshot(model),
|
|
2981
|
-
color: { domainMin: 1000, domainMax: 5000 },
|
|
3152
|
+
color: { scale: { domainMin: 1000, domainMax: 5000 } },
|
|
2982
3153
|
})
|
|
2983
3154
|
await model.loadGFA(RGFA, 'rgfa')
|
|
2984
3155
|
|
|
@@ -3244,6 +3415,71 @@ describe('walk rows', () => {
|
|
|
3244
3415
|
expect(model.walkStripRows).toBeUndefined()
|
|
3245
3416
|
})
|
|
3246
3417
|
|
|
3418
|
+
// node 2 is off GRCh38's path and both A#1 and B#1 walk it
|
|
3419
|
+
test('the block two rows walk off the reference is lettered on both', async () => {
|
|
3420
|
+
rpcRespond()
|
|
3421
|
+
const model = stateModelFactory().create({
|
|
3422
|
+
type: 'GraphGenomeView',
|
|
3423
|
+
layoutMode: 'walkrows',
|
|
3424
|
+
})
|
|
3425
|
+
await model.loadGFA(WALKS_GFA, 'walks')
|
|
3426
|
+
expect(model.sharedBlocks).toBeUndefined()
|
|
3427
|
+
model.setLayer('walkBlocks', true)
|
|
3428
|
+
const rows = [model.walkRowBars!.reference, ...model.walkRowBars!.rows]
|
|
3429
|
+
const lettered = model.sharedBlocks!.map(b => ({
|
|
3430
|
+
label: rows[b.row]!.label,
|
|
3431
|
+
start: b.start,
|
|
3432
|
+
bp: b.bp,
|
|
3433
|
+
block: b.block,
|
|
3434
|
+
}))
|
|
3435
|
+
expect(lettered.sort((a, b) => a.label.localeCompare(b.label))).toEqual([
|
|
3436
|
+
{ label: 'A#1', start: 4, bp: 8, block: 0 },
|
|
3437
|
+
{ label: 'B#1', start: 4, bp: 8, block: 0 },
|
|
3438
|
+
])
|
|
3439
|
+
})
|
|
3440
|
+
|
|
3441
|
+
test('the shared order stacks rows that walk the same nodes together', async () => {
|
|
3442
|
+
rpcRespond()
|
|
3443
|
+
const model = stateModelFactory().create({
|
|
3444
|
+
type: 'GraphGenomeView',
|
|
3445
|
+
layoutMode: 'walkrows',
|
|
3446
|
+
})
|
|
3447
|
+
await model.loadGFA(WALKS_GFA, 'walks')
|
|
3448
|
+
model.setWalkRowSamples(['A', 'B'])
|
|
3449
|
+
const before = model.walkRowBars!.rows.map(r => r.label)
|
|
3450
|
+
model.setWalkRowOrder('shared')
|
|
3451
|
+
const labels = model.walkRowBars!.rows.map(r => r.label)
|
|
3452
|
+
expect(labels).not.toEqual(before)
|
|
3453
|
+
// A#1 and B#1 share node 2, so B#1 stacks under A#1 rather than after
|
|
3454
|
+
// A#2, which walks GRCh38's own route
|
|
3455
|
+
expect(labels).toEqual(['A#1', 'B#1', 'A#2'])
|
|
3456
|
+
// the sample filter and the order sit side by side in the grammar
|
|
3457
|
+
expect(model.walkRowSamples).toEqual(['A', 'B'])
|
|
3458
|
+
model.setWalkRowSamples(undefined)
|
|
3459
|
+
expect(model.walkRowOrder).toBe('shared')
|
|
3460
|
+
})
|
|
3461
|
+
|
|
3462
|
+
test('ribbons join neighbouring walk rows where they walk the same nodes', async () => {
|
|
3463
|
+
rpcRespond()
|
|
3464
|
+
const model = stateModelFactory().create({
|
|
3465
|
+
type: 'GraphGenomeView',
|
|
3466
|
+
layoutMode: 'walkrows',
|
|
3467
|
+
})
|
|
3468
|
+
await model.loadGFA(WALKS_GFA, 'walks')
|
|
3469
|
+
expect(model.walkRowRibbons).toBeUndefined()
|
|
3470
|
+
model.setLayer('walkRibbons', true)
|
|
3471
|
+
const ribbons = model.walkRowRibbons!
|
|
3472
|
+
expect(ribbons.length).toBeGreaterThan(0)
|
|
3473
|
+
// every row passes nodes 1 and 3, so each pair of neighbours shares a
|
|
3474
|
+
// stretch that starts at both bars' left ends
|
|
3475
|
+
const rows = [model.walkRowBars!.reference, ...model.walkRowBars!.rows]
|
|
3476
|
+
for (let row = 0; row < rows.length - 1; row++) {
|
|
3477
|
+
expect(ribbons.some(r => r.row === row && r.a0 === 0 && r.b0 === 0)).toBe(
|
|
3478
|
+
true,
|
|
3479
|
+
)
|
|
3480
|
+
}
|
|
3481
|
+
})
|
|
3482
|
+
|
|
3247
3483
|
test('walk rows and tube maps take no strip', async () => {
|
|
3248
3484
|
rpcRespond()
|
|
3249
3485
|
const model = stateModelFactory().create({
|
|
@@ -3604,7 +3840,7 @@ describe('walk rows', () => {
|
|
|
3604
3840
|
model.liftWalks(['B#1#ctg'])
|
|
3605
3841
|
const svg = model.figure()!
|
|
3606
3842
|
expect(svg).toMatch(/^<svg xmlns="http:\/\/www\.w3\.org\/2000\/svg"/)
|
|
3607
|
-
expect(svg).toContain('
|
|
3843
|
+
expect(svg).toContain('only in other haplotypes')
|
|
3608
3844
|
})
|
|
3609
3845
|
|
|
3610
3846
|
test('a tube map lifts no walk: its tubes are the walks', async () => {
|
|
@@ -3801,6 +4037,33 @@ describe('walk rows', () => {
|
|
|
3801
4037
|
model.setHoveredRowWalks([])
|
|
3802
4038
|
expect(model.walkLift).toBeUndefined()
|
|
3803
4039
|
})
|
|
4040
|
+
|
|
4041
|
+
// each lift rebuilds the drawing, and a sweep down a lane crosses many rows
|
|
4042
|
+
test('moving between lane rows lifts the next once the pointer settles', async () => {
|
|
4043
|
+
rpcRespond()
|
|
4044
|
+
const model = stateModelFactory().create({
|
|
4045
|
+
type: 'GraphGenomeView',
|
|
4046
|
+
layoutMode: 'auto',
|
|
4047
|
+
})
|
|
4048
|
+
await model.loadGFA(WALKS_GFA, 'walks')
|
|
4049
|
+
vi.useFakeTimers()
|
|
4050
|
+
try {
|
|
4051
|
+
model.setHoveredRowWalks(['A#1#ctg'])
|
|
4052
|
+
expect(model.hoveredRowWalks).toEqual(['A#1#ctg'])
|
|
4053
|
+
model.setHoveredRowWalks(['A#2#ctg'])
|
|
4054
|
+
model.setHoveredRowWalks(['B#1#ctg'])
|
|
4055
|
+
vi.advanceTimersByTime(40)
|
|
4056
|
+
// more moves within the row do not put the lift off
|
|
4057
|
+
model.setHoveredRowWalks(['B#1#ctg'])
|
|
4058
|
+
expect(model.hoveredRowWalks).toEqual(['A#1#ctg'])
|
|
4059
|
+
vi.advanceTimersByTime(60)
|
|
4060
|
+
expect(model.hoveredRowWalks).toEqual(['B#1#ctg'])
|
|
4061
|
+
model.setHoveredRowWalks([])
|
|
4062
|
+
expect(model.hoveredRowWalks).toEqual([])
|
|
4063
|
+
} finally {
|
|
4064
|
+
vi.useRealTimers()
|
|
4065
|
+
}
|
|
4066
|
+
})
|
|
3804
4067
|
})
|
|
3805
4068
|
|
|
3806
4069
|
describe('annotation reads beside a cut', () => {
|
|
@@ -3900,6 +4163,43 @@ describe('annotation reads beside a cut', () => {
|
|
|
3900
4163
|
expect(model.error).toBeUndefined()
|
|
3901
4164
|
})
|
|
3902
4165
|
|
|
4166
|
+
// most graphs have no bubble index, and a pan re-cuts many times
|
|
4167
|
+
test('a bubble index that failed to read is not asked for again', async () => {
|
|
4168
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
4169
|
+
const bubbleReads = () =>
|
|
4170
|
+
mockRpcCall.mock.calls.filter(
|
|
4171
|
+
c =>
|
|
4172
|
+
(c[2] as { adapterConfig?: { type?: string } }).adapterConfig
|
|
4173
|
+
?.type === 'MinigraphBubbleAdapter',
|
|
4174
|
+
).length
|
|
4175
|
+
const respond = mockRpcCall.getMockImplementation()!
|
|
4176
|
+
mockRpcCall.mockImplementation(
|
|
4177
|
+
(sid: unknown, method: string, args: Record<string, unknown>) =>
|
|
4178
|
+
(args.adapterConfig as { type?: string } | undefined)?.type ===
|
|
4179
|
+
'MinigraphBubbleAdapter'
|
|
4180
|
+
? Promise.reject(new Error('404'))
|
|
4181
|
+
: respond(sid, method, args),
|
|
4182
|
+
)
|
|
4183
|
+
const model = attachedModel()
|
|
4184
|
+
const settleAll = async () => {
|
|
4185
|
+
await vi.waitFor(() => {
|
|
4186
|
+
expect(reads.length).toBeGreaterThanOrEqual(2)
|
|
4187
|
+
})
|
|
4188
|
+
for (const read of reads.splice(0)) {
|
|
4189
|
+
read.settle([])
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
let loaded = cut(model)
|
|
4193
|
+
await settleAll()
|
|
4194
|
+
await loaded
|
|
4195
|
+
expect(bubbleReads()).toBe(1)
|
|
4196
|
+
loaded = cut(model)
|
|
4197
|
+
await settleAll()
|
|
4198
|
+
await loaded
|
|
4199
|
+
expect(bubbleReads()).toBe(1)
|
|
4200
|
+
warn.mockRestore()
|
|
4201
|
+
})
|
|
4202
|
+
|
|
3903
4203
|
test('a cut too large to draw still reads its genes, for when the limit is raised', async () => {
|
|
3904
4204
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
3905
4205
|
const model = attachedModel()
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { cutsWholeWalks as wholeWalksRule } from '@jbrowse/bandage-core/layout/walkStrip'
|
|
2
|
-
import { layoutModeByValue } from '@jbrowse/bandage-core/layoutModes'
|
|
3
1
|
import { getSession } from '@jbrowse/core/util'
|
|
4
2
|
|
|
5
3
|
import {
|
|
@@ -46,16 +44,11 @@ export const GraphPane = withLaunchActions
|
|
|
46
44
|
canonicalAssemblyName(session, region.assemblyName),
|
|
47
45
|
)
|
|
48
46
|
},
|
|
49
|
-
//
|
|
50
|
-
// walks,
|
|
51
|
-
// that changes whether the drawing needs them cuts the track again.
|
|
47
|
+
// A layout change that changes the cut options cuts the track again:
|
|
48
|
+
// whole walks (cutsWholeWalksIn), or the walk-row step budget.
|
|
52
49
|
cutChangedFrom(from: LayoutModeValue) {
|
|
53
|
-
const before = wholeWalksRule(layoutModeByValue(from), {
|
|
54
|
-
walkStrip: self.walkStrip,
|
|
55
|
-
host: !!self.host,
|
|
56
|
-
})
|
|
57
50
|
return (
|
|
58
|
-
self.cutsWholeWalks !==
|
|
51
|
+
self.cutsWholeWalks !== self.cutsWholeWalksIn(from) ||
|
|
59
52
|
(from === 'walkrows') !== (self.chosenLayoutMode === 'walkrows')
|
|
60
53
|
)
|
|
61
54
|
},
|
|
@@ -8,12 +8,38 @@ test.each(COLOR_SCHEME_VALUES)('%s survives the trip through a color', s => {
|
|
|
8
8
|
|
|
9
9
|
test('a color carries its domain', () => {
|
|
10
10
|
const color = colorOfScheme('reference-position', { start: 5, end: 50 })
|
|
11
|
-
expect(color).toEqual({
|
|
11
|
+
expect(color).toEqual({
|
|
12
|
+
field: 'position',
|
|
13
|
+
scale: { domainMin: 5, domainMax: 50 },
|
|
14
|
+
})
|
|
12
15
|
expect(domainOfColor(color)).toEqual({ start: 5, end: 50 })
|
|
13
16
|
})
|
|
14
17
|
|
|
15
18
|
test('a color it cannot read is auto', () => {
|
|
16
19
|
expect(schemeOfColor({ field: 'gc' })).toBe('auto')
|
|
17
20
|
expect(schemeOfColor('red')).toBe('auto')
|
|
21
|
+
expect(schemeOfColor({ value: 'red' })).toBe('auto')
|
|
18
22
|
expect(schemeOfColor(undefined)).toBe('auto')
|
|
19
23
|
})
|
|
24
|
+
|
|
25
|
+
test('a constant is a value and a field is its own member', () => {
|
|
26
|
+
expect(colorOfScheme('grey')).toEqual({ value: 'grey' })
|
|
27
|
+
expect(colorOfScheme('depth')).toEqual({ field: 'depth' })
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('the id field reads its scale off the nested scale', () => {
|
|
31
|
+
expect(schemeOfColor({ field: 'id' })).toBe('random')
|
|
32
|
+
expect(schemeOfColor({ field: 'id', scale: { type: 'ordinal' } })).toBe(
|
|
33
|
+
'random',
|
|
34
|
+
)
|
|
35
|
+
expect(schemeOfColor({ field: 'id', scale: { scheme: 'rainbow' } })).toBe(
|
|
36
|
+
'rainbow',
|
|
37
|
+
)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('a flat member beside the field reads as nothing', () => {
|
|
41
|
+
expect(schemeOfColor({ field: 'id', scheme: 'rainbow' })).toBe('random')
|
|
42
|
+
expect(domainOfColor({ field: 'position', domainMin: 1, domainMax: 2 })).toBe(
|
|
43
|
+
undefined,
|
|
44
|
+
)
|
|
45
|
+
})
|
|
@@ -7,21 +7,28 @@ interface Span {
|
|
|
7
7
|
end: number
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
// The node color as a config or session writes it, in core's
|
|
11
|
-
//
|
|
12
|
-
// colored by. `{}` or unset picks by the
|
|
13
|
-
// anchored one.
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
10
|
+
// The node color as a config or session writes it, in core's channel object
|
|
11
|
+
// (ADR-240): a `value` naming a constant scheme, or the `field` the nodes are
|
|
12
|
+
// colored by with the `scale` it maps through. `{}` or unset picks by the
|
|
13
|
+
// graph, reference position on an anchored one. Not display-kit's
|
|
14
|
+
// `colorConfigSchema`: its `value` is a CSS color and its `field` a feature
|
|
15
|
+
// attribute, where Bandage's painter takes its own schemes, which BandageJS
|
|
16
|
+
// shares.
|
|
17
|
+
export interface NodeColor {
|
|
18
|
+
value?: 'grey' | 'uniform'
|
|
19
|
+
field?: 'depth' | 'length' | 'rank' | 'position' | 'id'
|
|
20
|
+
scale?: {
|
|
21
|
+
type?: 'ordinal'
|
|
22
|
+
scheme?: 'rainbow'
|
|
23
|
+
domainMin?: number
|
|
24
|
+
domainMax?: number
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// What the node color sits in on a display's config: core's `encoding`
|
|
29
|
+
export interface GraphEncoding {
|
|
30
|
+
color?: NodeColor
|
|
31
|
+
}
|
|
25
32
|
|
|
26
33
|
const SCHEME_OF_FIELD = {
|
|
27
34
|
depth: 'depth',
|
|
@@ -35,13 +42,15 @@ const FIELD_OF_SCHEME = Object.fromEntries(
|
|
|
35
42
|
) as Record<string, keyof typeof SCHEME_OF_FIELD>
|
|
36
43
|
|
|
37
44
|
export function schemeOfColor(color: unknown): ColorScheme {
|
|
38
|
-
if (color === 'grey' || color === 'uniform') {
|
|
39
|
-
return color
|
|
40
|
-
}
|
|
41
45
|
if (isRecord(color)) {
|
|
42
|
-
const { field,
|
|
46
|
+
const { field, value, scale } = color
|
|
47
|
+
if (value === 'grey' || value === 'uniform') {
|
|
48
|
+
return value
|
|
49
|
+
}
|
|
43
50
|
if (field === 'id') {
|
|
44
|
-
return scheme === 'rainbow'
|
|
51
|
+
return isRecord(scale) && scale.scheme === 'rainbow'
|
|
52
|
+
? 'rainbow'
|
|
53
|
+
: 'random'
|
|
45
54
|
}
|
|
46
55
|
if (typeof field === 'string' && field in SCHEME_OF_FIELD) {
|
|
47
56
|
return SCHEME_OF_FIELD[field as keyof typeof SCHEME_OF_FIELD]
|
|
@@ -51,26 +60,26 @@ export function schemeOfColor(color: unknown): ColorScheme {
|
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
export function domainOfColor(color: unknown): Span | undefined {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
typeof
|
|
57
|
-
|
|
63
|
+
const scale = isRecord(color) ? color.scale : undefined
|
|
64
|
+
return isRecord(scale) &&
|
|
65
|
+
typeof scale.domainMin === 'number' &&
|
|
66
|
+
typeof scale.domainMax === 'number'
|
|
67
|
+
? { start: scale.domainMin, end: scale.domainMax }
|
|
58
68
|
: undefined
|
|
59
69
|
}
|
|
60
70
|
|
|
61
71
|
export function colorOfScheme(scheme: ColorScheme, domain?: Span): NodeColor {
|
|
62
72
|
if (scheme === 'grey' || scheme === 'uniform') {
|
|
63
|
-
return scheme
|
|
73
|
+
return { value: scheme }
|
|
64
74
|
}
|
|
65
75
|
return scheme === 'random'
|
|
66
|
-
? { field: 'id', scale: '
|
|
76
|
+
? { field: 'id', scale: { type: 'ordinal' } }
|
|
67
77
|
: scheme === 'rainbow'
|
|
68
|
-
? { field: 'id', scheme: 'rainbow' }
|
|
78
|
+
? { field: 'id', scale: { scheme: 'rainbow' } }
|
|
69
79
|
: scheme === 'reference-position' && domain
|
|
70
80
|
? {
|
|
71
81
|
field: 'position',
|
|
72
|
-
domainMin: domain.start,
|
|
73
|
-
domainMax: domain.end,
|
|
82
|
+
scale: { domainMin: domain.start, domainMax: domain.end },
|
|
74
83
|
}
|
|
75
84
|
: FIELD_OF_SCHEME[scheme]
|
|
76
85
|
? { field: FIELD_OF_SCHEME[scheme] }
|
|
@@ -1,14 +1,30 @@
|
|
|
1
|
-
import { nodeSizeOf,
|
|
1
|
+
import { nodeSizeOf, sizeOf } from './nodeSize'
|
|
2
2
|
|
|
3
3
|
test('a number is a uniform thickness, an object scales by its field', () => {
|
|
4
|
-
expect(nodeSizeOf(9)).toEqual({ width: 'uniform', px: 9 })
|
|
4
|
+
expect(nodeSizeOf(9)).toEqual({ width: 'uniform', px: 9, links: 'uniform' })
|
|
5
5
|
expect(nodeSizeOf({ field: 'depth', value: 4 })).toEqual({
|
|
6
6
|
width: 'depth',
|
|
7
7
|
px: 4,
|
|
8
|
+
links: 'uniform',
|
|
9
|
+
})
|
|
10
|
+
expect(nodeSizeOf(undefined)).toEqual({
|
|
11
|
+
width: 'depth',
|
|
12
|
+
px: 6,
|
|
13
|
+
links: 'uniform',
|
|
14
|
+
})
|
|
15
|
+
expect(nodeSizeOf({ value: 5, links: 'walks' })).toEqual({
|
|
16
|
+
width: 'uniform',
|
|
17
|
+
px: 5,
|
|
18
|
+
links: 'walks',
|
|
8
19
|
})
|
|
9
|
-
expect(nodeSizeOf(undefined)).toEqual({ width: 'depth', px: 6 })
|
|
10
20
|
})
|
|
11
21
|
|
|
12
|
-
test.each([
|
|
13
|
-
|
|
22
|
+
test.each([
|
|
23
|
+
['depth', 'uniform'],
|
|
24
|
+
['uniform', 'uniform'],
|
|
25
|
+
['depth', 'walks'],
|
|
26
|
+
['uniform', 'walks'],
|
|
27
|
+
] as const)('nodes %s, links %s survive the trip', (width, links) => {
|
|
28
|
+
const size = { width, px: 5, links }
|
|
29
|
+
expect(nodeSizeOf(sizeOf(size))).toEqual(size)
|
|
14
30
|
})
|
|
@@ -1,25 +1,41 @@
|
|
|
1
1
|
import { isRecord } from '../isRecord'
|
|
2
2
|
|
|
3
|
+
import type { EdgeWidth } from '@jbrowse/bandage-core/edgeWidths'
|
|
3
4
|
import type { NodeWidth } from '@jbrowse/bandage-core/nodeWidths'
|
|
4
5
|
|
|
5
|
-
// The
|
|
6
|
+
// The mark thickness as a config or session writes it: a px number draws every
|
|
6
7
|
// node that thick; `{ field: 'depth', value }` thickens a node the more paths
|
|
7
|
-
// carry it, around `value` px
|
|
8
|
-
|
|
8
|
+
// carry it, around `value` px; `links: 'walks'` thickens a link the more walks
|
|
9
|
+
// take it. Unset is nodes by depth around 6, links uniform.
|
|
10
|
+
export type NodeSize =
|
|
11
|
+
number | { field?: 'depth'; value?: number; links?: 'walks' }
|
|
9
12
|
|
|
10
13
|
const DEFAULT_PX = 6
|
|
11
14
|
|
|
12
|
-
export
|
|
15
|
+
export interface MarkSize {
|
|
16
|
+
width: NodeWidth
|
|
17
|
+
px: number
|
|
18
|
+
links: EdgeWidth
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function nodeSizeOf(size: unknown): MarkSize {
|
|
13
22
|
return typeof size === 'number'
|
|
14
|
-
? { width: 'uniform', px: size }
|
|
23
|
+
? { width: 'uniform', px: size, links: 'uniform' }
|
|
15
24
|
: isRecord(size)
|
|
16
25
|
? {
|
|
17
26
|
width: size.field === 'depth' ? 'depth' : 'uniform',
|
|
18
27
|
px: typeof size.value === 'number' ? size.value : DEFAULT_PX,
|
|
28
|
+
links: size.links === 'walks' ? 'walks' : 'uniform',
|
|
19
29
|
}
|
|
20
|
-
: { width: 'depth', px: DEFAULT_PX }
|
|
30
|
+
: { width: 'depth', px: DEFAULT_PX, links: 'uniform' }
|
|
21
31
|
}
|
|
22
32
|
|
|
23
|
-
export function
|
|
24
|
-
return width === '
|
|
33
|
+
export function sizeOf({ width, px, links }: MarkSize): NodeSize {
|
|
34
|
+
return width === 'uniform' && links === 'uniform'
|
|
35
|
+
? px
|
|
36
|
+
: {
|
|
37
|
+
...(width === 'depth' ? { field: 'depth' as const } : {}),
|
|
38
|
+
value: px,
|
|
39
|
+
...(links === 'walks' ? { links } : {}),
|
|
40
|
+
}
|
|
25
41
|
}
|