jbrowse-plugin-graphgenomeviewer 3.0.4 → 3.1.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/README.md +29 -2
- package/dist/chunks/GbzBaseSyntenyAdapter-MIOUYHOF.js +8 -0
- package/dist/chunks/GbzBaseSyntenyAdapter-MIOUYHOF.js.map +7 -0
- package/dist/chunks/GraphGenomeView-TM7WYDLH.js +14 -0
- package/dist/chunks/GraphGenomeView-TM7WYDLH.js.map +7 -0
- package/dist/chunks/MinigraphBubbleAdapter-A7SBF2XY.js +2 -0
- package/dist/chunks/MinigraphBubbleAdapter-A7SBF2XY.js.map +7 -0
- package/dist/chunks/RgfaTabixAdapter-7ZGKJJYW.js +3 -0
- package/dist/chunks/RgfaTabixAdapter-7ZGKJJYW.js.map +7 -0
- package/dist/chunks/{chunk-X3X74OGB.js → chunk-7T2HTRP3.js} +2 -2
- package/dist/chunks/{chunk-X3X74OGB.js.map → chunk-7T2HTRP3.js.map} +3 -3
- package/dist/chunks/{chunk-JCG7RLMB.js → chunk-HOM2Y6XS.js} +2 -2
- package/dist/chunks/chunk-HOM2Y6XS.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 +21 -22
- package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.test.ts +69 -0
- package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.ts +13 -10
- package/src/GetSubgraph.ts +5 -1
- package/src/GraphGenomeView/components/FollowControl.test.tsx +69 -0
- package/src/GraphGenomeView/components/FollowControl.tsx +42 -0
- package/src/GraphGenomeView/components/GraphStats.tsx +1 -0
- package/src/GraphGenomeView/components/GraphToolbar.tsx +2 -0
- package/src/GraphGenomeView/follow.test.ts +624 -0
- package/src/GraphGenomeView/follow.ts +129 -0
- package/src/GraphGenomeView/layout/sampleRowLayout.test.ts +16 -0
- package/src/GraphGenomeView/layout/sampleRowLayout.ts +13 -4
- package/src/GraphGenomeView/layoutModes.ts +3 -1
- package/src/GraphGenomeView/model.ts +361 -55
- package/src/GraphGenomeView/types.ts +4 -0
- package/src/MinigraphBubbleAdapter/MinigraphBubbleAdapter.ts +1 -1
- package/src/RgfaTabixAdapter/RgfaTabixAdapter.test.ts +73 -0
- package/src/RgfaTabixAdapter/RgfaTabixAdapter.ts +48 -16
- package/src/RgfaTabixAdapter/configSchema.ts +70 -3
- package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.tier1000.links.bed.gz +0 -0
- package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.tier1000.links.bed.gz.tbi +0 -0
- package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.tier1000.segs.bed.gz +0 -0
- package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.tier1000.segs.bed.gz.tbi +0 -0
- package/src/launchSubgraph/index.ts +12 -6
- package/src/launchSubgraph/launchSubgraph.test.ts +89 -3
- package/src/launchSubgraph/launchSubgraphView.ts +14 -6
- package/src/launchSubgraph/linearViewMenuItems.ts +3 -0
- package/src/launchSubgraph/subgraphMenuItems.ts +7 -0
- package/src/launchSubgraph/testEnv.ts +2 -0
- package/src/panSNTabix.ts +6 -5
- package/src/synteny/ComparativeAdapterBase.ts +13 -10
- package/src/synteny/clipFeatureToRegion.test.ts +41 -1
- package/src/synteny/clipFeatureToRegion.ts +22 -5
- package/src/version.ts +1 -1
- package/dist/chunks/GbzBaseSyntenyAdapter-6UTRY4CV.js +0 -8
- package/dist/chunks/GbzBaseSyntenyAdapter-6UTRY4CV.js.map +0 -7
- package/dist/chunks/GraphGenomeView-UD5WCV3Q.js +0 -14
- package/dist/chunks/GraphGenomeView-UD5WCV3Q.js.map +0 -7
- package/dist/chunks/MinigraphBubbleAdapter-B75VQK7T.js +0 -2
- package/dist/chunks/MinigraphBubbleAdapter-B75VQK7T.js.map +0 -7
- package/dist/chunks/RgfaTabixAdapter-IT7I5RR3.js +0 -3
- package/dist/chunks/RgfaTabixAdapter-IT7I5RR3.js.map +0 -7
- package/dist/chunks/chunk-JCG7RLMB.js.map +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jbrowse-plugin-graphgenomeviewer",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
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": {
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
],
|
|
26
26
|
"private": false,
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@gmod/gbz-base": "^2.
|
|
29
|
-
"@gmod/tabix": "^3.8.
|
|
30
|
-
"@jbrowse/cigar-utils": "
|
|
31
|
-
"@jbrowse/core": "
|
|
32
|
-
"@jbrowse/render-core": "
|
|
33
|
-
"@jbrowse/synteny-core": "
|
|
28
|
+
"@gmod/gbz-base": "^2.8.0",
|
|
29
|
+
"@gmod/tabix": "^3.8.3",
|
|
30
|
+
"@jbrowse/cigar-utils": "5.0.0-beta.9",
|
|
31
|
+
"@jbrowse/core": "5.0.0-beta.9",
|
|
32
|
+
"@jbrowse/render-core": "5.0.0-beta.9",
|
|
33
|
+
"@jbrowse/synteny-core": "5.0.0-beta.9"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@emotion/react": "^11.14.0",
|
|
@@ -38,30 +38,30 @@
|
|
|
38
38
|
"@eslint/js": "^10.0.1",
|
|
39
39
|
"@fal-works/esbuild-plugin-global-externals": "^2.1.2",
|
|
40
40
|
"@jbrowse/cli": "^4.3.0",
|
|
41
|
-
"@jbrowse/mobx-state-tree": "^6.
|
|
42
|
-
"@jbrowse/plugin-canvas": "
|
|
43
|
-
"@jbrowse/plugin-linear-genome-view": "
|
|
41
|
+
"@jbrowse/mobx-state-tree": "^6.6.1",
|
|
42
|
+
"@jbrowse/plugin-canvas": "5.0.0-beta.9",
|
|
43
|
+
"@jbrowse/plugin-linear-genome-view": "5.0.0-beta.9",
|
|
44
44
|
"@mui/icons-material": "^9.4.0",
|
|
45
45
|
"@mui/material": "^9.4.0",
|
|
46
46
|
"@mui/system": "^9.4.0",
|
|
47
47
|
"@testing-library/react": "^16.3.3",
|
|
48
|
-
"@types/node": "^26.
|
|
48
|
+
"@types/node": "^26.6.2",
|
|
49
49
|
"@types/react": "^19.3.0",
|
|
50
50
|
"@types/react-dom": "^19.3.0",
|
|
51
|
-
"@typescript-eslint/eslint-plugin": "^8.70.
|
|
52
|
-
"@typescript-eslint/parser": "^8.70.
|
|
51
|
+
"@typescript-eslint/eslint-plugin": "^8.70.1",
|
|
52
|
+
"@typescript-eslint/parser": "^8.70.1",
|
|
53
53
|
"esbuild": "^0.28.2",
|
|
54
|
-
"eslint": "^10.
|
|
54
|
+
"eslint": "^10.11.0",
|
|
55
55
|
"eslint-plugin-import-x": "^4.17.1",
|
|
56
56
|
"eslint-plugin-react": "^7.37.5",
|
|
57
57
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
58
|
-
"git-cliff": "^2.
|
|
59
|
-
"jsdom": "^30.
|
|
60
|
-
"mobx": "^7.0.
|
|
58
|
+
"git-cliff": "^2.14.2",
|
|
59
|
+
"jsdom": "^30.1.1",
|
|
60
|
+
"mobx": "^7.0.4",
|
|
61
61
|
"mobx-react": "^10.0.2",
|
|
62
|
-
"prettier": "^3.9.
|
|
62
|
+
"prettier": "^3.9.9",
|
|
63
63
|
"pretty-bytes": "^7.1.3",
|
|
64
|
-
"puppeteer": "^25.
|
|
64
|
+
"puppeteer": "^25.12.0",
|
|
65
65
|
"react": "19.3.0",
|
|
66
66
|
"react-dom": "19.3.0",
|
|
67
67
|
"rimraf": "^6.1.3",
|
|
@@ -69,9 +69,9 @@
|
|
|
69
69
|
"serve": "^14.2.6",
|
|
70
70
|
"tss-react": "^4.9.21",
|
|
71
71
|
"typescript": "^6.0.3",
|
|
72
|
-
"typescript-eslint": "^8.70.
|
|
72
|
+
"typescript-eslint": "^8.70.1",
|
|
73
73
|
"typescript7": "npm:typescript@^7.0.2",
|
|
74
|
-
"vitest": "^5.0.
|
|
74
|
+
"vitest": "^5.0.2"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
77
|
"react": ">=18.0.0",
|
|
@@ -93,7 +93,6 @@
|
|
|
93
93
|
"bench:layout": "node scripts/bench-layout.mjs",
|
|
94
94
|
"layout-digest": "node scripts/layout-digest.mjs",
|
|
95
95
|
"build:bundle": "NODE_ENV=production node esbuild.mjs",
|
|
96
|
-
"betabuild": "bash scripts/betabuild.sh",
|
|
97
96
|
"typecheck": "node node_modules/typescript7/bin/tsc --noEmit",
|
|
98
97
|
"host-compat": "node scripts/host-compat-probe.mjs",
|
|
99
98
|
"lint": "eslint --report-unused-disable-directives --max-warnings 0",
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { Subgraph } from '@gmod/gbz-base'
|
|
2
|
+
import { numericCigarToString } from '@jbrowse/cigar-utils'
|
|
3
|
+
import PluginManager from '@jbrowse/core/PluginManager'
|
|
1
4
|
import { firstValueFrom } from 'rxjs'
|
|
2
5
|
import { toArray } from 'rxjs/operators'
|
|
3
6
|
|
|
@@ -7,6 +10,7 @@ import Adapter, {
|
|
|
7
10
|
laneAssemblyName,
|
|
8
11
|
} from './GbzBaseSyntenyAdapter.ts'
|
|
9
12
|
import configSchema from './configSchema.ts'
|
|
13
|
+
import GbzBaseSyntenyAdapterF from './index.ts'
|
|
10
14
|
|
|
11
15
|
import type { SyntenyMate } from '@jbrowse/synteny-core'
|
|
12
16
|
|
|
@@ -459,6 +463,15 @@ const spanOf = (intervals: { start: number; end: number }[]) => ({
|
|
|
459
463
|
end: Math.max(...intervals.map(i => i.end)),
|
|
460
464
|
})
|
|
461
465
|
|
|
466
|
+
test('the adapter type declares that a window of its anchor answers any lane pair', () => {
|
|
467
|
+
const pluginManager = new PluginManager()
|
|
468
|
+
GbzBaseSyntenyAdapterF(pluginManager)
|
|
469
|
+
pluginManager.createPluggableElements()
|
|
470
|
+
expect(
|
|
471
|
+
pluginManager.getAdapterType('GbzBaseSyntenyAdapter').adapterCapabilities,
|
|
472
|
+
).toContain('lanePairsOnAnchor')
|
|
473
|
+
})
|
|
474
|
+
|
|
462
475
|
// HG00673#1's insertion is 16 bp longer than HG01361#2's, so that pair holds
|
|
463
476
|
// an indel, which pins which side of the record its CIGAR walks. HG00438#1 is
|
|
464
477
|
// assembled reverse to GRCh38. Each pair covers the stretch of each haplotype
|
|
@@ -476,6 +489,7 @@ test.each(['HG02145#2', 'HG00673#1', 'HG00438#1'])(
|
|
|
476
489
|
expect(f.get('assemblyName')).toBe('HG01361#2')
|
|
477
490
|
const mate = mateOf(f)
|
|
478
491
|
expect(mate.assemblyName).toBe(lower)
|
|
492
|
+
expect(f.get('CIGAR')).toMatch(/^(\d+[=ID])+$/)
|
|
479
493
|
expect(cigarSpans(f.get('CIGAR'))).toEqual({
|
|
480
494
|
feature: f.get('end') - f.get('start'),
|
|
481
495
|
mate: mate.end - mate.start,
|
|
@@ -520,6 +534,61 @@ test('a reverse-strand pair writes its CIGAR along the query lane', async () =>
|
|
|
520
534
|
expect(reverse.get('CIGAR')).toBe(forward.get('CIGAR'))
|
|
521
535
|
})
|
|
522
536
|
|
|
537
|
+
// HG01361#2 and HG00673#1 differ by one SNP in this window, which the graph
|
|
538
|
+
// holds as a bubble: an insertion and a deletion, where comparing the bases
|
|
539
|
+
// would write 1X
|
|
540
|
+
const snpPair = {
|
|
541
|
+
queryAssemblyName: 'HG01361#2',
|
|
542
|
+
targetAssemblyName: 'HG00673#1',
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
test('a SNP between two lanes is the bubble the graph holds, an insertion and a deletion', async () => {
|
|
546
|
+
const records = await feats(anchoredAdapter(), window, snpPair)
|
|
547
|
+
expect(records.map(f => [f.get('CIGAR'), f.get('numMatches')])).toEqual([
|
|
548
|
+
['800=1I1D413=', 1213],
|
|
549
|
+
])
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
test('a lane pair fetched the way MultiWaySyntenyDisplay fetches it hands over those ops', async () => {
|
|
553
|
+
const records = await firstValueFrom(
|
|
554
|
+
anchoredAdapter()
|
|
555
|
+
.getFeaturesInMultipleRegions([window] as never, {
|
|
556
|
+
...snpPair,
|
|
557
|
+
clipToRegion: true,
|
|
558
|
+
splitAtGapBp: 10_000,
|
|
559
|
+
keepAlignment: true,
|
|
560
|
+
})
|
|
561
|
+
.pipe(toArray()),
|
|
562
|
+
)
|
|
563
|
+
expect(
|
|
564
|
+
records.map(f =>
|
|
565
|
+
numericCigarToString(f.get('alignmentOps') as Uint32Array),
|
|
566
|
+
),
|
|
567
|
+
).toEqual(['800=1I1D413='])
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
// HG00673#2 differs from HG01361#2 in a 4 bp bubble whose middle two bases
|
|
571
|
+
// agree, which comparing bases would count as matches. Nowhere in the slice
|
|
572
|
+
// would it find an inversion inside a gap, so the option alone keeps one out
|
|
573
|
+
test('a lane pair asks gbz-base to compare no base, and every match it answers is a shared node', async () => {
|
|
574
|
+
const read = vi.spyOn(Subgraph.prototype, 'pairAlignments')
|
|
575
|
+
await feats(anchoredAdapter(), window, {
|
|
576
|
+
queryAssemblyName: 'HG01361#2',
|
|
577
|
+
targetAssemblyName: 'HG00673#2',
|
|
578
|
+
})
|
|
579
|
+
const answered = read.mock.results.flatMap(result =>
|
|
580
|
+
result.type === 'return' ? result.value : [],
|
|
581
|
+
)
|
|
582
|
+
expect(answered.length).toBeGreaterThan(0)
|
|
583
|
+
for (const [opts] of read.mock.calls) {
|
|
584
|
+
expect(opts.bases).toBe(false)
|
|
585
|
+
}
|
|
586
|
+
for (const pair of answered) {
|
|
587
|
+
expect(pair.matches).toBe(pair.sharedBases)
|
|
588
|
+
}
|
|
589
|
+
read.mockRestore()
|
|
590
|
+
})
|
|
591
|
+
|
|
523
592
|
test('the pair with an indel between its lanes writes it', async () => {
|
|
524
593
|
const records = await feats(anchoredAdapter(), insertionWindow, {
|
|
525
594
|
queryAssemblyName: 'HG01361#2',
|
|
@@ -546,8 +546,9 @@ export default class GbzBaseSyntenyAdapter extends ComparativeAdapterBase<GbzBas
|
|
|
546
546
|
|
|
547
547
|
/**
|
|
548
548
|
* The two lanes' walks cut out of the anchor window alone, and each walk
|
|
549
|
-
* of the query lane aligned to each walk of the target lane
|
|
550
|
-
*
|
|
549
|
+
* of the query lane aligned to each walk of the target lane on the nodes
|
|
550
|
+
* both visit, with no base compared. The records sit on the query lane's
|
|
551
|
+
* contigs, the lane the display draws on top.
|
|
551
552
|
*
|
|
552
553
|
* Only a cut walked from the haplotype index's anchor rows holds each walk
|
|
553
554
|
* whole. The sampled cut leaves a walk in pieces wherever it strays past the
|
|
@@ -593,7 +594,7 @@ export default class GbzBaseSyntenyAdapter extends ComparativeAdapterBase<GbzBas
|
|
|
593
594
|
query && anchored && featureSide.length > 0 && mateSide.length > 0
|
|
594
595
|
? (
|
|
595
596
|
await updateStatus(
|
|
596
|
-
`
|
|
597
|
+
`Reading ${queryAssemblyName} against ${targetAssemblyName}`,
|
|
597
598
|
opts.statusCallback,
|
|
598
599
|
async () =>
|
|
599
600
|
Promise.all(
|
|
@@ -612,13 +613,15 @@ export default class GbzBaseSyntenyAdapter extends ComparativeAdapterBase<GbzBas
|
|
|
612
613
|
? subgraphs.flatMap(subgraph =>
|
|
613
614
|
featureSide.flatMap(target =>
|
|
614
615
|
mateSide.flatMap(mate =>
|
|
615
|
-
subgraph
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
616
|
+
subgraph
|
|
617
|
+
.pairAlignments({ target, query: mate, bases: false })
|
|
618
|
+
.map(pair =>
|
|
619
|
+
pairFeature({
|
|
620
|
+
pair,
|
|
621
|
+
lane: queryAssemblyName,
|
|
622
|
+
mateLane: targetAssemblyName,
|
|
623
|
+
}),
|
|
624
|
+
),
|
|
622
625
|
),
|
|
623
626
|
),
|
|
624
627
|
)
|
package/src/GetSubgraph.ts
CHANGED
|
@@ -12,10 +12,14 @@ import type { Region } from '@jbrowse/core/util'
|
|
|
12
12
|
// cut reads; the GBZ cut has its own bp `context` slot. `haplotypes` is the
|
|
13
13
|
// set the cut is for, lane assembly names or PanSN prefixes, which only the GBZ
|
|
14
14
|
// cut reads: it keeps those walks and the nodes they visit. Undefined or empty
|
|
15
|
-
// is every haplotype.
|
|
15
|
+
// is every haplotype. `tier` picks which of an rGFA track's two index pairs the
|
|
16
|
+
// cut reads, the adapter's own or its `coarse` one; fine when absent.
|
|
17
|
+
export type SubgraphTier = 'fine' | 'coarse'
|
|
18
|
+
|
|
16
19
|
export interface SubgraphCutOptions {
|
|
17
20
|
hops?: number
|
|
18
21
|
haplotypes?: string[]
|
|
22
|
+
tier?: SubgraphTier
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
// What an adapter's cut is handed: the payload, plus the call's own signal so
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
2
|
+
|
|
3
|
+
import FollowControl from './FollowControl'
|
|
4
|
+
|
|
5
|
+
import type { GraphGenomeViewModel } from '../model'
|
|
6
|
+
|
|
7
|
+
function control(state: Partial<GraphGenomeViewModel>) {
|
|
8
|
+
const model = {
|
|
9
|
+
loadedTrackId: 'graph',
|
|
10
|
+
followLinearView: true,
|
|
11
|
+
followState: { active: true },
|
|
12
|
+
followNote: undefined,
|
|
13
|
+
cutTier: 'fine',
|
|
14
|
+
setFollowLinearView: vi.fn(),
|
|
15
|
+
...state,
|
|
16
|
+
}
|
|
17
|
+
render(<FollowControl model={model as unknown as GraphGenomeViewModel} />)
|
|
18
|
+
return model
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function status() {
|
|
22
|
+
return screen.queryByTestId('graph-follow-status')?.textContent
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('a following graph offers Pin, and says it is following', () => {
|
|
26
|
+
const model = control({})
|
|
27
|
+
expect(status()).toBe('Following the linear view')
|
|
28
|
+
fireEvent.click(screen.getByText('Pin'))
|
|
29
|
+
expect(model.setFollowLinearView).toHaveBeenCalledWith(false)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('a pinned graph offers Follow, and says it is pinned', () => {
|
|
33
|
+
const model = control({
|
|
34
|
+
followLinearView: false,
|
|
35
|
+
followState: { active: false, reason: undefined },
|
|
36
|
+
})
|
|
37
|
+
expect(status()).toBe('Pinned')
|
|
38
|
+
fireEvent.click(screen.getByText('Follow'))
|
|
39
|
+
expect(model.setFollowLinearView).toHaveBeenCalledWith(true)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('a follow that cannot run says why', () => {
|
|
43
|
+
control({
|
|
44
|
+
followState: {
|
|
45
|
+
active: false,
|
|
46
|
+
reason: 'Not following: x is not reference bp (Force-directed layout)',
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
expect(status()).toMatch(/Force-directed layout/)
|
|
50
|
+
expect(screen.getByText('Pin')).toBeTruthy()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('a coarse cut says so', () => {
|
|
54
|
+
control({ cutTier: 'coarse' })
|
|
55
|
+
expect(status()).toBe('Following the linear view, coarse tier')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('a held cut says what it is holding', () => {
|
|
59
|
+
control({
|
|
60
|
+
followNote: 'Holding the last cut: 6 Mb is past the 5 Mb a cut may span',
|
|
61
|
+
})
|
|
62
|
+
expect(status()).toMatch(/^Holding the last cut/)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('a graph from a whole file has no follow control', () => {
|
|
66
|
+
control({ loadedTrackId: '' })
|
|
67
|
+
expect(screen.queryByText('Pin')).toBeNull()
|
|
68
|
+
expect(status()).toBeUndefined()
|
|
69
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Button, Typography } from '@mui/material'
|
|
2
|
+
import { observer } from 'mobx-react'
|
|
3
|
+
|
|
4
|
+
import type { GraphGenomeViewModel } from '../model'
|
|
5
|
+
|
|
6
|
+
const FollowControl = observer(function FollowControl({
|
|
7
|
+
model,
|
|
8
|
+
}: {
|
|
9
|
+
model: GraphGenomeViewModel
|
|
10
|
+
}) {
|
|
11
|
+
const { followLinearView, followState } = model
|
|
12
|
+
const note = followState.active ? model.followNote : undefined
|
|
13
|
+
const state = !followLinearView
|
|
14
|
+
? 'Pinned'
|
|
15
|
+
: followState.active
|
|
16
|
+
? (note ??
|
|
17
|
+
`Following the linear view${model.cutTier === 'coarse' ? ', coarse tier' : ''}`)
|
|
18
|
+
: followState.reason
|
|
19
|
+
return model.loadedTrackId ? (
|
|
20
|
+
<>
|
|
21
|
+
<Button
|
|
22
|
+
size="small"
|
|
23
|
+
onClick={() => {
|
|
24
|
+
model.setFollowLinearView(!followLinearView)
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
{followLinearView ? 'Pin' : 'Follow'}
|
|
28
|
+
</Button>
|
|
29
|
+
{state ? (
|
|
30
|
+
<Typography
|
|
31
|
+
variant="caption"
|
|
32
|
+
color={note ? 'warning.main' : 'text.secondary'}
|
|
33
|
+
data-testid="graph-follow-status"
|
|
34
|
+
>
|
|
35
|
+
{state}
|
|
36
|
+
</Typography>
|
|
37
|
+
) : null}
|
|
38
|
+
</>
|
|
39
|
+
) : null
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
export default FollowControl
|
|
@@ -69,6 +69,7 @@ const GraphStats = observer(function GraphStats({
|
|
|
69
69
|
data-layout-ms={model.lastLayoutMs ?? ''}
|
|
70
70
|
data-geometry-ms={model.lastGeometryMs ?? ''}
|
|
71
71
|
data-geometry-strokes={model.lastGeometryStrokeCount ?? ''}
|
|
72
|
+
data-follow-recuts={model.followRecuts}
|
|
72
73
|
>
|
|
73
74
|
<Typography variant="body2" component="span">
|
|
74
75
|
{model.nodeCount} nodes, {model.edgeCount} edges
|
|
@@ -6,6 +6,7 @@ import { observer } from 'mobx-react'
|
|
|
6
6
|
import { makeStyles } from 'tss-react/mui'
|
|
7
7
|
|
|
8
8
|
import ColorSchemeSelect from './ColorSchemeSelect'
|
|
9
|
+
import FollowControl from './FollowControl'
|
|
9
10
|
import GraphStats from './GraphStats'
|
|
10
11
|
import LayoutSelect from './LayoutSelect'
|
|
11
12
|
import RepeatSelect from './RepeatSelect'
|
|
@@ -75,6 +76,7 @@ const GraphToolbar = observer(function GraphToolbar({
|
|
|
75
76
|
</IconButton>
|
|
76
77
|
</Tooltip>
|
|
77
78
|
<ZoomDisplay model={model} />
|
|
79
|
+
<FollowControl model={model} />
|
|
78
80
|
<GraphStats model={model} />
|
|
79
81
|
<SettingsMenu model={model} />
|
|
80
82
|
</div>
|