jbrowse-plugin-graphgenomeviewer 6.2.0 → 6.3.1
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 +4 -3
- package/dist/chunks/ChooseWalksDialog-L6QKOT7M.js +2 -0
- package/dist/chunks/ChooseWalksDialog-L6QKOT7M.js.map +7 -0
- package/dist/chunks/{GbzBaseSyntenyAdapter-TLI5IUU7.js → GbzBaseSyntenyAdapter-KVJKGGQ4.js} +2 -2
- package/dist/chunks/{GbzBaseSyntenyAdapter-TLI5IUU7.js.map → GbzBaseSyntenyAdapter-KVJKGGQ4.js.map} +1 -1
- package/dist/chunks/{GraphGenomeView-5OPCROMQ.js → GraphGenomeView-VIKLXNQD.js} +2 -2
- package/dist/chunks/{GraphGenomeView-5OPCROMQ.js.map → GraphGenomeView-VIKLXNQD.js.map} +1 -1
- package/dist/chunks/GraphTrackSettingsDialog-MRMSH2FO.js +4 -0
- package/dist/chunks/{GraphTrackSettingsDialog-FGLLUIEE.js.map → GraphTrackSettingsDialog-MRMSH2FO.js.map} +1 -1
- package/dist/chunks/HighlightColorDialog-SABW5OMB.js +2 -0
- package/dist/chunks/HighlightColorDialog-SABW5OMB.js.map +7 -0
- package/dist/chunks/LinearGraphDisplay-AHCKWNFJ.js +2 -0
- package/dist/chunks/{LinearGraphDisplay-NBJHGOYX.js.map → LinearGraphDisplay-AHCKWNFJ.js.map} +1 -1
- package/dist/chunks/{MinigraphBubbleAdapter-LQUIL3MD.js → MinigraphBubbleAdapter-CAXB5FGR.js} +2 -2
- package/dist/chunks/{RgfaTabixAdapter-3BFNY4SB.js → RgfaTabixAdapter-GYRNCGT2.js} +2 -2
- package/dist/chunks/{bandage-layout-3AR6BCUD-I6CCWYSR.js → bandage-layout-3AR6BCUD-PYYNJEGI.js} +1 -1
- package/dist/chunks/{bandage-layout-3AR6BCUD-I6CCWYSR.js.map → bandage-layout-3AR6BCUD-PYYNJEGI.js.map} +1 -1
- package/dist/chunks/{chunk-O5EHYU3Y.js → chunk-2SIR336R.js} +2 -2
- package/dist/chunks/chunk-2SL675KP.js +3 -0
- package/dist/chunks/{chunk-NGQRKSHY.js.map → chunk-2SL675KP.js.map} +3 -3
- package/dist/chunks/chunk-A6GKF63C.js +2 -0
- package/dist/chunks/{chunk-ZKLOV6MW.js.map → chunk-A6GKF63C.js.map} +2 -2
- package/dist/chunks/chunk-B7GF3TC3.js +2 -0
- package/dist/chunks/chunk-B7GF3TC3.js.map +7 -0
- package/dist/chunks/chunk-B7KMMGFB.js +2 -0
- package/dist/chunks/chunk-B7KMMGFB.js.map +7 -0
- package/dist/chunks/{chunk-X24J6VF4.js → chunk-DILFGQIL.js} +1 -1
- package/dist/chunks/{chunk-X24J6VF4.js.map → chunk-DILFGQIL.js.map} +1 -1
- package/dist/chunks/{chunk-7MWOPEIC.js → chunk-EHV7EUMI.js} +1 -1
- package/dist/chunks/{chunk-7MWOPEIC.js.map → chunk-EHV7EUMI.js.map} +1 -1
- package/dist/chunks/chunk-GMSOU6VO.js +2 -0
- package/dist/chunks/chunk-GMSOU6VO.js.map +7 -0
- package/dist/chunks/{chunk-EKMGHBWM.js → chunk-KRXALLQG.js} +1 -1
- package/dist/chunks/{chunk-EKMGHBWM.js.map → chunk-KRXALLQG.js.map} +1 -1
- package/dist/chunks/{chunk-F7V6ZPKD.js → chunk-MYJYGSOH.js} +1 -1
- package/dist/chunks/{chunk-F7V6ZPKD.js.map → chunk-MYJYGSOH.js.map} +1 -1
- package/dist/chunks/chunk-WTOQRUK2.js +2 -0
- package/dist/chunks/chunk-WTOQRUK2.js.map +7 -0
- package/dist/chunks/{chunk-JYZAMHIP.js → chunk-WUGRVNUJ.js} +2 -2
- package/dist/chunks/{chunk-JYZAMHIP.js.map → chunk-WUGRVNUJ.js.map} +1 -1
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +6 -6
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
- package/package.json +2 -3
- package/src/GbzBaseSyntenyAdapter/configSchema.ts +1 -1
- package/src/GraphGenomeView/components/ChooseWalksDialog.tsx +6 -7
- package/src/GraphGenomeView/components/HighlightColorDialog.tsx +117 -0
- package/src/GraphGenomeView/components/TubeMapOverlay.tsx +1 -1
- package/src/GraphGenomeView/model.test.ts +107 -31
- package/src/GraphGenomeView/pane/fitViews.ts +8 -5
- package/src/GraphGenomeView/pane/graphViews.ts +11 -1
- package/src/GraphGenomeView/pane/hostViews.ts +1 -1
- package/src/GraphGenomeView/pane/keepInView.ts +62 -0
- package/src/GraphGenomeView/pane/launchActions.ts +159 -184
- package/src/GraphGenomeView/pane/loadActions.ts +3 -0
- package/src/GraphGenomeView/pane/paneBase.ts +9 -3
- package/src/GraphGenomeView/pane/renderingActions.ts +11 -1
- package/src/GraphGenomeView/pane/settingActions.ts +38 -0
- package/src/GraphGenomeView/renderPipeline.test.ts +4 -1
- package/src/GraphGenomeView/renderer/GraphRenderer.ts +2 -2
- package/src/GraphGenomeView/viewModel.ts +1 -1
- package/src/LinearGraphDisplay/LinearGraphDisplay.test.ts +12 -5
- package/src/LinearGraphDisplay/configSchema.ts +8 -0
- package/src/LinearGraphDisplay/model.ts +39 -33
- package/src/hoverSync/GraphNodeHighlight.tsx +3 -3
- package/src/version.ts +1 -1
- package/dist/chunks/ChooseWalksDialog-GVQLSGHX.js +0 -2
- package/dist/chunks/ChooseWalksDialog-GVQLSGHX.js.map +0 -7
- package/dist/chunks/GraphTrackSettingsDialog-FGLLUIEE.js +0 -4
- package/dist/chunks/LinearGraphDisplay-NBJHGOYX.js +0 -2
- package/dist/chunks/chunk-LZUNK4MQ.js +0 -2
- package/dist/chunks/chunk-LZUNK4MQ.js.map +0 -7
- package/dist/chunks/chunk-NGQRKSHY.js +0 -3
- package/dist/chunks/chunk-XWGKHQYO.js +0 -2
- package/dist/chunks/chunk-XWGKHQYO.js.map +0 -7
- package/dist/chunks/chunk-ZKLOV6MW.js +0 -2
- /package/dist/chunks/{MinigraphBubbleAdapter-LQUIL3MD.js.map → MinigraphBubbleAdapter-CAXB5FGR.js.map} +0 -0
- /package/dist/chunks/{RgfaTabixAdapter-3BFNY4SB.js.map → RgfaTabixAdapter-GYRNCGT2.js.map} +0 -0
- /package/dist/chunks/{chunk-O5EHYU3Y.js.map → chunk-2SIR336R.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jbrowse-plugin-graphgenomeviewer",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.1",
|
|
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": {
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
"@gmod/bgzf-filehandle": "^6.7.0",
|
|
29
29
|
"@gmod/gbz-base": "^7.1.0",
|
|
30
30
|
"@gmod/tabix": "^3.9.1",
|
|
31
|
-
"@gmod/tubemap-core": "^0.1.0",
|
|
32
31
|
"@jbrowse/cigar-utils": "5.0.0-beta.11",
|
|
33
32
|
"@jbrowse/core": "5.0.0-beta.11",
|
|
34
33
|
"@jbrowse/display-kit": "5.0.0-beta.11",
|
|
@@ -40,7 +39,7 @@
|
|
|
40
39
|
"@emotion/styled": "^11.14.1",
|
|
41
40
|
"@eslint/js": "^10.0.1",
|
|
42
41
|
"@fal-works/esbuild-plugin-global-externals": "^2.1.2",
|
|
43
|
-
"@jbrowse/bandage-core": "^
|
|
42
|
+
"@jbrowse/bandage-core": "^8.0.1",
|
|
44
43
|
"@jbrowse/cli": "^4.3.0",
|
|
45
44
|
"@jbrowse/mobx-state-tree": "^6.7.0",
|
|
46
45
|
"@jbrowse/plugin-canvas": "5.0.0-beta.11",
|
|
@@ -115,7 +115,7 @@ const GbzBaseSyntenyAdapter = ConfigurationSchema(
|
|
|
115
115
|
/**
|
|
116
116
|
* #slot
|
|
117
117
|
* A tab-separated table of per-sample metadata, which the graph view's
|
|
118
|
-
* walk rows group by ("Group by
|
|
118
|
+
* walk rows group by ("Group by..."). Its header row names the columns
|
|
119
119
|
* (`population`, `superpopulation`, ...), and its first column is the
|
|
120
120
|
* walk's sample (`HG00097`) or haplotype (`HG00097#1`). Empty means no
|
|
121
121
|
* grouping.
|
|
@@ -19,7 +19,7 @@ type WalkPicker = Pick<
|
|
|
19
19
|
'walkChoices' | 'walkLayers' | 'walkLabel' | 'liftWalks'
|
|
20
20
|
>
|
|
21
21
|
|
|
22
|
-
// Which
|
|
22
|
+
// Which haplotypes to highlight, searched by name, for a graph with more walks than a
|
|
23
23
|
// menu can list
|
|
24
24
|
const ChooseWalksDialog = observer(function ChooseWalksDialog({
|
|
25
25
|
model,
|
|
@@ -34,7 +34,7 @@ const ChooseWalksDialog = observer(function ChooseWalksDialog({
|
|
|
34
34
|
model.walkLayers.map(l => l.walk).filter(name => names.has(name)),
|
|
35
35
|
)
|
|
36
36
|
return (
|
|
37
|
-
<Dialog open onClose={onClose} title="Choose
|
|
37
|
+
<Dialog open onClose={onClose} title="Choose haplotypes">
|
|
38
38
|
<DialogContent>
|
|
39
39
|
<Autocomplete
|
|
40
40
|
multiple
|
|
@@ -56,15 +56,14 @@ const ChooseWalksDialog = observer(function ChooseWalksDialog({
|
|
|
56
56
|
<TextField
|
|
57
57
|
{...params}
|
|
58
58
|
autoFocus
|
|
59
|
-
label="
|
|
59
|
+
label="Haplotypes"
|
|
60
60
|
placeholder="type to search"
|
|
61
61
|
/>
|
|
62
62
|
)}
|
|
63
63
|
/>
|
|
64
64
|
<Typography variant="caption" color="text.secondary">
|
|
65
|
-
{walkChoices.length.toLocaleString()}
|
|
66
|
-
picked are
|
|
67
|
-
fades.
|
|
65
|
+
{walkChoices.length.toLocaleString()} haplotypes in this cut. The ones
|
|
66
|
+
picked are highlighted in this order, and the rest fades.
|
|
68
67
|
</Typography>
|
|
69
68
|
</DialogContent>
|
|
70
69
|
<DialogActions>
|
|
@@ -76,7 +75,7 @@ const ChooseWalksDialog = observer(function ChooseWalksDialog({
|
|
|
76
75
|
onClose()
|
|
77
76
|
}}
|
|
78
77
|
>
|
|
79
|
-
|
|
78
|
+
Highlight these
|
|
80
79
|
</Button>
|
|
81
80
|
</DialogActions>
|
|
82
81
|
</Dialog>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { WALK_FIELDS, WALK_SCHEMES } from '@jbrowse/bandage-core/walkEncoding'
|
|
2
|
+
import { Dialog } from '@jbrowse/core/ui'
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
DialogActions,
|
|
6
|
+
DialogContent,
|
|
7
|
+
MenuItem,
|
|
8
|
+
TextField,
|
|
9
|
+
Typography,
|
|
10
|
+
} from '@mui/material'
|
|
11
|
+
import { observer } from 'mobx-react'
|
|
12
|
+
|
|
13
|
+
import type { GraphPaneModel } from '../model'
|
|
14
|
+
|
|
15
|
+
type HighlightColors = Pick<
|
|
16
|
+
GraphPaneModel,
|
|
17
|
+
'drawnWalks' | 'walkLabel' | 'setWalkColor'
|
|
18
|
+
>
|
|
19
|
+
|
|
20
|
+
const HighlightColorDialog = observer(function HighlightColorDialog({
|
|
21
|
+
model,
|
|
22
|
+
onClose,
|
|
23
|
+
}: {
|
|
24
|
+
model: HighlightColors
|
|
25
|
+
onClose: () => void
|
|
26
|
+
}) {
|
|
27
|
+
return (
|
|
28
|
+
<Dialog open onClose={onClose} title="Color highlighted haplotypes">
|
|
29
|
+
<DialogContent
|
|
30
|
+
sx={{
|
|
31
|
+
display: 'grid',
|
|
32
|
+
gridTemplateColumns: 'auto 1fr 1fr',
|
|
33
|
+
alignItems: 'center',
|
|
34
|
+
gap: 1.5,
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
<Typography variant="subtitle2">Haplotype</Typography>
|
|
38
|
+
<Typography variant="subtitle2">Color by</Typography>
|
|
39
|
+
<Typography variant="subtitle2">Palette</Typography>
|
|
40
|
+
{model.drawnWalks.map(({ name, encoding }) => (
|
|
41
|
+
<Row
|
|
42
|
+
key={name}
|
|
43
|
+
label={model.walkLabel(name)}
|
|
44
|
+
field={encoding.field}
|
|
45
|
+
scheme={encoding.scheme}
|
|
46
|
+
onField={field => {
|
|
47
|
+
model.setWalkColor(name, { field })
|
|
48
|
+
}}
|
|
49
|
+
onScheme={scheme => {
|
|
50
|
+
model.setWalkColor(name, { scheme })
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
))}
|
|
54
|
+
</DialogContent>
|
|
55
|
+
<DialogActions>
|
|
56
|
+
<Button variant="contained" onClick={onClose}>
|
|
57
|
+
Done
|
|
58
|
+
</Button>
|
|
59
|
+
</DialogActions>
|
|
60
|
+
</Dialog>
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
type Field = (typeof WALK_FIELDS)[number]['value']
|
|
65
|
+
type Scheme = (typeof WALK_SCHEMES)[number]['value']
|
|
66
|
+
|
|
67
|
+
function Row({
|
|
68
|
+
label,
|
|
69
|
+
field,
|
|
70
|
+
scheme,
|
|
71
|
+
onField,
|
|
72
|
+
onScheme,
|
|
73
|
+
}: {
|
|
74
|
+
label: string
|
|
75
|
+
field: Field
|
|
76
|
+
scheme: Scheme
|
|
77
|
+
onField: (field: Field) => void
|
|
78
|
+
onScheme: (scheme: Scheme) => void
|
|
79
|
+
}) {
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
<Typography variant="body2">{label}</Typography>
|
|
83
|
+
<TextField
|
|
84
|
+
select
|
|
85
|
+
size="small"
|
|
86
|
+
value={field}
|
|
87
|
+
onChange={e => {
|
|
88
|
+
onField(e.target.value as Field)
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
{WALK_FIELDS.map(f => (
|
|
92
|
+
<MenuItem key={f.value} value={f.value}>
|
|
93
|
+
{f.label}
|
|
94
|
+
</MenuItem>
|
|
95
|
+
))}
|
|
96
|
+
</TextField>
|
|
97
|
+
<TextField
|
|
98
|
+
select
|
|
99
|
+
size="small"
|
|
100
|
+
value={scheme}
|
|
101
|
+
onChange={e => {
|
|
102
|
+
onScheme(e.target.value as Scheme)
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
{WALK_SCHEMES.filter(
|
|
106
|
+
s => s.value !== 'rainbow' || field === 'reference',
|
|
107
|
+
).map(s => (
|
|
108
|
+
<MenuItem key={s.value} value={s.value}>
|
|
109
|
+
{s.label}
|
|
110
|
+
</MenuItem>
|
|
111
|
+
))}
|
|
112
|
+
</TextField>
|
|
113
|
+
</>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export default HighlightColorDialog
|
|
@@ -61,7 +61,7 @@ const TubeMapOverlay = observer(function TubeMapOverlay({
|
|
|
61
61
|
const tubeFrame = {
|
|
62
62
|
...frame,
|
|
63
63
|
width,
|
|
64
|
-
highlightNode: model.
|
|
64
|
+
highlightNode: model.litNode ?? model.selectedNode,
|
|
65
65
|
darkMode: model.darkMode,
|
|
66
66
|
tubeColors: model.tubeMapTubeColors,
|
|
67
67
|
nodeColors: model.tubeMapNodeColors,
|
|
@@ -1522,6 +1522,20 @@ describe('hoverHighlight', () => {
|
|
|
1522
1522
|
expect(model.hoverHighlight).toMatchObject({ start: 0, end: 4 })
|
|
1523
1523
|
})
|
|
1524
1524
|
|
|
1525
|
+
test('turning hover highlighting off lights nothing', async () => {
|
|
1526
|
+
const model = await loadedFromTrack()
|
|
1527
|
+
model.setHighlightOnHover(false)
|
|
1528
|
+
model.setHoveredNode('2+')
|
|
1529
|
+
expect(model.hoverHighlight).toBeUndefined()
|
|
1530
|
+
expect(model.litNode).toBeNull()
|
|
1531
|
+
const lit: ReadonlyMap<string, number>[] = []
|
|
1532
|
+
model.applyHighlights({
|
|
1533
|
+
...fakeRenderer(),
|
|
1534
|
+
setNodeHighlights: (m: ReadonlyMap<string, number>) => lit.push(m),
|
|
1535
|
+
})
|
|
1536
|
+
expect(lit).toEqual([new Map()])
|
|
1537
|
+
})
|
|
1538
|
+
|
|
1525
1539
|
// A whole-file import has no region, so its stable names need not name anything
|
|
1526
1540
|
// in a loaded assembly and there is nothing to project onto.
|
|
1527
1541
|
test('a whole-file import publishes no highlight', async () => {
|
|
@@ -2682,10 +2696,8 @@ interface MenuItem {
|
|
|
2682
2696
|
subMenu?: MenuItem[]
|
|
2683
2697
|
}
|
|
2684
2698
|
|
|
2685
|
-
function
|
|
2686
|
-
return
|
|
2687
|
-
item => item.label === 'Walk',
|
|
2688
|
-
)!
|
|
2699
|
+
function highlightMenu(model: { highlightMenuItems: () => unknown }) {
|
|
2700
|
+
return model.highlightMenuItems() as MenuItem[]
|
|
2689
2701
|
}
|
|
2690
2702
|
|
|
2691
2703
|
describe('walk rows', () => {
|
|
@@ -2793,7 +2805,7 @@ describe('walk rows', () => {
|
|
|
2793
2805
|
await model.loadGFA(WALKS_GFA, 'walks')
|
|
2794
2806
|
const samples = () =>
|
|
2795
2807
|
(
|
|
2796
|
-
(model.
|
|
2808
|
+
(model.layoutOptionMenuItems() as MenuItem[]).find(
|
|
2797
2809
|
item => item.label === 'Samples',
|
|
2798
2810
|
) as { subMenu: (MenuItem & { onClick: () => void })[] }
|
|
2799
2811
|
).subMenu
|
|
@@ -2934,13 +2946,9 @@ describe('walk rows', () => {
|
|
|
2934
2946
|
// lifted together, each walk is one flat colour
|
|
2935
2947
|
const ref = model.walkLift!.walks[0]!
|
|
2936
2948
|
expect(new Set(ref.colors.values()).size).toBe(1)
|
|
2937
|
-
const menu = walkMenu(model)
|
|
2938
|
-
const colour = menu.subMenu!.find(item => item.label === 'Colour B#1')!
|
|
2939
2949
|
expect(
|
|
2940
|
-
|
|
2941
|
-
).
|
|
2942
|
-
// the rainbow is offered for reference position only
|
|
2943
|
-
expect(colour.subMenu!.some(item => item.label === 'Rainbow')).toBe(false)
|
|
2950
|
+
model.drawnWalks.find(w => w.name === 'B#1#ctg')!.encoding,
|
|
2951
|
+
).toMatchObject({ field: 'walk', scheme: 'green' })
|
|
2944
2952
|
model.liftWalks(['B#1#ctg'])
|
|
2945
2953
|
expect(model.walkLayers).toEqual([
|
|
2946
2954
|
{ walk: 'B#1#ctg', color: { field: 'walk', scheme: 'green' } },
|
|
@@ -2978,8 +2986,7 @@ describe('walk rows', () => {
|
|
|
2978
2986
|
})
|
|
2979
2987
|
await model.loadGFA(WALKS_GFA, 'walks')
|
|
2980
2988
|
model.liftWalks(['B#1#ctg', 'A#1#ctg'])
|
|
2981
|
-
|
|
2982
|
-
expect(menu.subMenu!.some(item => item.label === 'Side by side')).toBe(
|
|
2989
|
+
expect(highlightMenu(model).some(item => item.label === 'Arrange')).toBe(
|
|
2983
2990
|
false,
|
|
2984
2991
|
)
|
|
2985
2992
|
model.setFacet('walk')
|
|
@@ -3048,13 +3055,9 @@ describe('walk rows', () => {
|
|
|
3048
3055
|
expect(model.canvasHeight).toBe(Math.max(160, grid.total))
|
|
3049
3056
|
expect(model.scale).toBeCloseTo(grid.scale, 10)
|
|
3050
3057
|
expect(model.scale).not.toBeCloseTo(whole, 10)
|
|
3051
|
-
const menu = walkMenu(model)
|
|
3052
|
-
const colour = menu.subMenu!.find(item => item.label === 'Colour B#1')!
|
|
3053
3058
|
expect(
|
|
3054
|
-
|
|
3055
|
-
).
|
|
3056
|
-
const columns = menu.subMenu!.find(item => item.label === 'Columns')!
|
|
3057
|
-
expect(columns.subMenu!.map(item => item.label)).toEqual(['Auto', '1', '2'])
|
|
3059
|
+
model.drawnWalks.find(w => w.name === 'B#1#ctg')!.encoding,
|
|
3060
|
+
).toMatchObject({ field: 'progress', scheme: 'red' })
|
|
3058
3061
|
})
|
|
3059
3062
|
|
|
3060
3063
|
test('by sample, a sample takes a row and its haplotypes the columns', async () => {
|
|
@@ -3126,11 +3129,7 @@ describe('walk rows', () => {
|
|
|
3126
3129
|
await model.loadGFA(WALKS_GFA, 'walks')
|
|
3127
3130
|
expect(model.walkLift).toBeUndefined()
|
|
3128
3131
|
expect(model.facetPanels).toBeUndefined()
|
|
3129
|
-
expect(
|
|
3130
|
-
(model.graphMenuItems() as { label?: string }[]).some(
|
|
3131
|
-
item => item.label === 'Walk',
|
|
3132
|
-
),
|
|
3133
|
-
).toBe(false)
|
|
3132
|
+
expect(model.highlightMenuItems()).toEqual([])
|
|
3134
3133
|
})
|
|
3135
3134
|
|
|
3136
3135
|
test('a lane reads the reference ramp only when it is coloured by it', async () => {
|
|
@@ -3841,12 +3840,12 @@ describe('walk rows grouped by a sample column', () => {
|
|
|
3841
3840
|
expect(stale.walkRowPlacement!.headers).toEqual([])
|
|
3842
3841
|
})
|
|
3843
3842
|
|
|
3844
|
-
test('Group by
|
|
3843
|
+
test('Group by... lists None and each column, and the SVG export waits for None', async () => {
|
|
3845
3844
|
const model = await groupedModel()
|
|
3846
3845
|
const groupBy = () =>
|
|
3847
3846
|
(
|
|
3848
|
-
(model.
|
|
3849
|
-
item => item.label === 'Group by
|
|
3847
|
+
(model.layoutOptionMenuItems() as MenuItem[]).find(
|
|
3848
|
+
item => item.label === 'Group by...',
|
|
3850
3849
|
) as { subMenu: (MenuItem & { onClick: () => void })[] }
|
|
3851
3850
|
).subMenu
|
|
3852
3851
|
expect(groupBy().map(item => [item.label, item.checked])).toEqual([
|
|
@@ -3857,19 +3856,96 @@ describe('walk rows grouped by a sample column', () => {
|
|
|
3857
3856
|
groupBy()[2]!.onClick()
|
|
3858
3857
|
expect(model.walkRowGroupBy).toEqual({ field: 'superpopulation' })
|
|
3859
3858
|
expect(groupBy()[2]!.checked).toBe(true)
|
|
3860
|
-
expect(model.figureUnavailable).toContain('Group by
|
|
3859
|
+
expect(model.figureUnavailable).toContain('Group by...')
|
|
3861
3860
|
groupBy()[0]!.onClick()
|
|
3862
3861
|
expect(model.walkRowGroupBy).toBeUndefined()
|
|
3863
3862
|
expect(model.figureUnavailable).toBeUndefined()
|
|
3864
3863
|
})
|
|
3865
3864
|
|
|
3866
|
-
test('no samples table, no Group by
|
|
3865
|
+
test('no samples table, no Group by...', async () => {
|
|
3867
3866
|
mockSession.tracks = []
|
|
3868
3867
|
const model = await groupedModel()
|
|
3869
3868
|
expect(
|
|
3870
|
-
(model.
|
|
3871
|
-
item => item.label === 'Group by
|
|
3869
|
+
(model.layoutOptionMenuItems() as MenuItem[]).some(
|
|
3870
|
+
item => item.label === 'Group by...',
|
|
3872
3871
|
),
|
|
3873
3872
|
).toBe(false)
|
|
3874
3873
|
})
|
|
3875
3874
|
})
|
|
3875
|
+
|
|
3876
|
+
describe('keeping the drawing on screen', () => {
|
|
3877
|
+
function screenBox(model: ReturnType<typeof createModel>) {
|
|
3878
|
+
const b = model.layoutBounds!
|
|
3879
|
+
const left = b.minX * model.scaleX + model.translateX
|
|
3880
|
+
const top = b.minY * model.scaleY + model.translateY
|
|
3881
|
+
return {
|
|
3882
|
+
left,
|
|
3883
|
+
top,
|
|
3884
|
+
right: left + b.w * model.scaleX,
|
|
3885
|
+
bottom: top + b.h * model.scaleY,
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
async function forceModel() {
|
|
3890
|
+
rpcRespond()
|
|
3891
|
+
const model = createModel()
|
|
3892
|
+
await model.loadGFA(SIMPLE_GFA, 'simple')
|
|
3893
|
+
model.startRenderingBackend(fakeRenderer())
|
|
3894
|
+
model.setWidth(800)
|
|
3895
|
+
return model
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
test('a pan stops with some of the drawing still in the pane', async () => {
|
|
3899
|
+
const model = await forceModel()
|
|
3900
|
+
model.setTransform(model.scale, model.translateX + 10_000, -10_000)
|
|
3901
|
+
const box = screenBox(model)
|
|
3902
|
+
expect(box.left).toBeLessThanOrEqual(model.viewBox.width - 48 + 1e-6)
|
|
3903
|
+
expect(box.bottom).toBeGreaterThanOrEqual(48 - 1e-6)
|
|
3904
|
+
})
|
|
3905
|
+
|
|
3906
|
+
test('a zoom about a point far from the drawing keeps it in the pane', async () => {
|
|
3907
|
+
const model = await forceModel()
|
|
3908
|
+
model.zoom(50, model.viewBox.width, model.viewBox.height)
|
|
3909
|
+
const box = screenBox(model)
|
|
3910
|
+
expect(box.right).toBeGreaterThan(0)
|
|
3911
|
+
expect(box.left).toBeLessThan(model.viewBox.width)
|
|
3912
|
+
expect(box.bottom).toBeGreaterThan(0)
|
|
3913
|
+
expect(box.top).toBeLessThan(model.viewBox.height)
|
|
3914
|
+
})
|
|
3915
|
+
|
|
3916
|
+
// A fresh force run, or a new cut as a linear view moves, lands in
|
|
3917
|
+
// coordinates of its own; the old zoom showed empty paper
|
|
3918
|
+
test('a new force layout refits a drawing the user had moved', async () => {
|
|
3919
|
+
const model = await forceModel()
|
|
3920
|
+
model.zoom(4, 0, 0)
|
|
3921
|
+
expect(model.viewportOwner).toBe('user')
|
|
3922
|
+
const shifted = {
|
|
3923
|
+
nodePositions: Object.fromEntries(
|
|
3924
|
+
Object.entries(MOCK_LAYOUT.nodePositions).map(([k, segs]) => [
|
|
3925
|
+
k,
|
|
3926
|
+
segs.map(p => ({ x: p.x + 5000, y: p.y - 3000 })),
|
|
3927
|
+
]),
|
|
3928
|
+
),
|
|
3929
|
+
}
|
|
3930
|
+
mockRpcCall.mockImplementation((_sid: unknown, method: string) =>
|
|
3931
|
+
method === 'GraphComputeLayout'
|
|
3932
|
+
? Promise.resolve({ result: shifted, duration: 5 })
|
|
3933
|
+
: Promise.reject(new Error(`Unexpected RPC: ${method}`)),
|
|
3934
|
+
)
|
|
3935
|
+
await model.toggleDeletionEdges()
|
|
3936
|
+
|
|
3937
|
+
expect(model.viewportOwner).toBe('fit')
|
|
3938
|
+
expect(model.scale).toBeCloseTo(model.fittedTransform!.scale, 10)
|
|
3939
|
+
expect(model.translateX).toBeCloseTo(model.fittedTransform!.translateX, 10)
|
|
3940
|
+
})
|
|
3941
|
+
|
|
3942
|
+
test('a layout handed back unchanged keeps the user’s zoom', async () => {
|
|
3943
|
+
const model = await forceModel()
|
|
3944
|
+
model.zoom(4, 100, 50)
|
|
3945
|
+
const { scale, translateX } = model
|
|
3946
|
+
await model.recomputeLayout()
|
|
3947
|
+
expect(model.viewportOwner).toBe('user')
|
|
3948
|
+
expect(model.scale).toBe(scale)
|
|
3949
|
+
expect(model.translateX).toBe(translateX)
|
|
3950
|
+
})
|
|
3951
|
+
})
|
|
@@ -438,16 +438,19 @@ export const withFitViews = withHostViews
|
|
|
438
438
|
// there is nothing to restore and nothing to go stale when a rebuild
|
|
439
439
|
// renumbers the batch
|
|
440
440
|
applyHighlights(b: Renderer) {
|
|
441
|
-
const {
|
|
441
|
+
const { litNode, hoveredEdge, selectedNode } = self
|
|
442
442
|
const nodes = new Map<string, number>()
|
|
443
443
|
if (selectedNode !== null) {
|
|
444
444
|
nodes.set(selectedNode, SELECT_BRIGHTEN)
|
|
445
445
|
}
|
|
446
|
-
if (
|
|
447
|
-
nodes.set(
|
|
446
|
+
if (litNode !== null && litNode !== selectedNode) {
|
|
447
|
+
nodes.set(litNode, HOVER_BRIGHTEN)
|
|
448
448
|
}
|
|
449
449
|
b.setNodeHighlights(nodes)
|
|
450
|
-
b.setEdgeHighlight(
|
|
450
|
+
b.setEdgeHighlight(
|
|
451
|
+
self.chosenHighlightOnHover ? hoveredEdge : null,
|
|
452
|
+
HOVER_BRIGHTEN,
|
|
453
|
+
)
|
|
451
454
|
},
|
|
452
455
|
// Draws the uploaded batch through the pane's transform
|
|
453
456
|
paint(b: Renderer) {
|
|
@@ -627,7 +630,7 @@ export const withFitViews = withHostViews
|
|
|
627
630
|
return !self.layoutResult
|
|
628
631
|
? 'Nothing is drawn yet'
|
|
629
632
|
: self.walkRowGroups
|
|
630
|
-
? 'The SVG export cannot group walk rows yet: set Group by
|
|
633
|
+
? 'The SVG export cannot group walk rows yet: set Group by... to None first'
|
|
631
634
|
: self.drawsNodes || self.walkRowBars
|
|
632
635
|
? undefined
|
|
633
636
|
: `${layoutModeByValue(self.chosenLayoutMode).label} draws a picture of its own, which the SVG export does not`
|
|
@@ -79,6 +79,9 @@ export const withGraphViews = paneBase
|
|
|
79
79
|
get defaultColorScheme(): ColorScheme {
|
|
80
80
|
return 'auto'
|
|
81
81
|
},
|
|
82
|
+
get defaultHighlightOnHover(): boolean {
|
|
83
|
+
return true
|
|
84
|
+
},
|
|
82
85
|
// a source declared but not yet loaded
|
|
83
86
|
get hasPendingSource() {
|
|
84
87
|
return false
|
|
@@ -102,6 +105,13 @@ export const withGraphViews = paneBase
|
|
|
102
105
|
get chosenColorScheme() {
|
|
103
106
|
return self.colorScheme ?? self.defaultColorScheme
|
|
104
107
|
},
|
|
108
|
+
get chosenHighlightOnHover() {
|
|
109
|
+
return self.highlightOnHover ?? self.defaultHighlightOnHover
|
|
110
|
+
},
|
|
111
|
+
// the hovered node, where hovering is to light it
|
|
112
|
+
get litNode() {
|
|
113
|
+
return this.chosenHighlightOnHover ? self.hoveredNode : null
|
|
114
|
+
},
|
|
105
115
|
}))
|
|
106
116
|
.views(self => ({
|
|
107
117
|
get paneWidth() {
|
|
@@ -877,7 +887,7 @@ export const withGraphViews = paneBase
|
|
|
877
887
|
get hoverHighlight() {
|
|
878
888
|
const region = self.graphRegion
|
|
879
889
|
const span = self.hoveredSpan
|
|
880
|
-
return region && span
|
|
890
|
+
return region && span && self.chosenHighlightOnHover
|
|
881
891
|
? {
|
|
882
892
|
refName: region.refName,
|
|
883
893
|
assemblyName: region.assemblyName,
|
|
@@ -57,7 +57,7 @@ export const withHostViews = withWalkRowViews
|
|
|
57
57
|
},
|
|
58
58
|
get tubeMapPicture() {
|
|
59
59
|
const drawing = self.layoutResult?.tubeMap
|
|
60
|
-
return drawing ? tubeMapPicture(drawing
|
|
60
|
+
return drawing ? tubeMapPicture(drawing) : undefined
|
|
61
61
|
},
|
|
62
62
|
get tubeMapReference() {
|
|
63
63
|
const drawing = self.layoutResult?.tubeMap
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// How much of the drawing a pan or zoom leaves on screen in each axis, or
|
|
2
|
+
// all of it when it is smaller
|
|
3
|
+
export const KEEP_IN_VIEW_PX = 48
|
|
4
|
+
|
|
5
|
+
// The shift that brings a span starting at `lo`, `size` px long, back to
|
|
6
|
+
// overlapping [0, extent] by `margin` px
|
|
7
|
+
function shiftIntoView(lo: number, size: number, extent: number) {
|
|
8
|
+
const margin = Math.min(KEEP_IN_VIEW_PX, size, extent)
|
|
9
|
+
return Math.max(0, margin - (lo + size)) - Math.max(0, lo - (extent - margin))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface DrawingBox {
|
|
13
|
+
minX: number
|
|
14
|
+
minY: number
|
|
15
|
+
w: number
|
|
16
|
+
h: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Placement {
|
|
20
|
+
scaleX: number
|
|
21
|
+
scaleY: number
|
|
22
|
+
translateX: number
|
|
23
|
+
translateY: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function keepInView(
|
|
27
|
+
bounds: DrawingBox,
|
|
28
|
+
t: Placement,
|
|
29
|
+
view: { width: number; height: number },
|
|
30
|
+
) {
|
|
31
|
+
return {
|
|
32
|
+
translateX:
|
|
33
|
+
t.translateX +
|
|
34
|
+
shiftIntoView(
|
|
35
|
+
bounds.minX * t.scaleX + t.translateX,
|
|
36
|
+
bounds.w * t.scaleX,
|
|
37
|
+
view.width,
|
|
38
|
+
),
|
|
39
|
+
translateY:
|
|
40
|
+
t.translateY +
|
|
41
|
+
shiftIntoView(
|
|
42
|
+
bounds.minY * t.scaleY + t.translateY,
|
|
43
|
+
bounds.h * t.scaleY,
|
|
44
|
+
view.height,
|
|
45
|
+
),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function onScreen(
|
|
50
|
+
bounds: DrawingBox,
|
|
51
|
+
t: Placement,
|
|
52
|
+
view: { width: number; height: number },
|
|
53
|
+
) {
|
|
54
|
+
const left = bounds.minX * t.scaleX + t.translateX
|
|
55
|
+
const top = bounds.minY * t.scaleY + t.translateY
|
|
56
|
+
return (
|
|
57
|
+
left < view.width &&
|
|
58
|
+
left + bounds.w * t.scaleX > 0 &&
|
|
59
|
+
top < view.height &&
|
|
60
|
+
top + bounds.h * t.scaleY > 0
|
|
61
|
+
)
|
|
62
|
+
}
|