jbrowse-plugin-msaview 2.10.2 → 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/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.d.ts +11 -0
- package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +73 -16
- package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +8 -1
- package/dist/LaunchMsaView/components/BlastQuery/consts.js +8 -1
- package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +7 -14
- package/dist/LaunchMsaView/components/QueryRowSelector.d.ts +16 -0
- package/dist/LaunchMsaView/components/QueryRowSelector.js +38 -0
- package/dist/LaunchMsaView/detectQueryRow.d.ts +23 -0
- package/dist/LaunchMsaView/detectQueryRow.js +94 -0
- package/dist/LaunchMsaView/detectQueryRow.test.d.ts +1 -0
- package/dist/LaunchMsaView/detectQueryRow.test.js +65 -0
- package/dist/LaunchMsaView/useQueryRowName.d.ts +15 -0
- package/dist/LaunchMsaView/useQueryRowName.js +26 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +28 -12
- package/dist/MsaViewPanel/afterCreateAutoruns.js +99 -50
- package/dist/MsaViewPanel/model.d.ts +12 -76
- package/dist/MsaViewPanel/model.js +1 -1
- package/dist/MsaViewPanel/observeProteinHighlights.test.d.ts +1 -0
- package/dist/MsaViewPanel/observeProteinHighlights.test.js +209 -0
- package/dist/MsaViewPanel/structureConnection.d.ts +6 -0
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +29 -29
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/ncbiDomains.d.ts +2 -30
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -3
- package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +126 -30
- package/src/LaunchMsaView/components/BlastQuery/consts.ts +8 -1
- package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +7 -37
- package/src/LaunchMsaView/components/QueryRowSelector.tsx +93 -0
- package/src/LaunchMsaView/detectQueryRow.test.ts +79 -0
- package/src/LaunchMsaView/detectQueryRow.ts +132 -0
- package/src/LaunchMsaView/useQueryRowName.ts +33 -0
- package/src/MsaViewPanel/afterCreateAutoruns.ts +109 -54
- package/src/MsaViewPanel/model.ts +1 -1
- package/src/MsaViewPanel/observeProteinHighlights.test.ts +264 -0
- package/src/MsaViewPanel/structureConnection.ts +7 -0
- package/src/version.ts +1 -1
|
@@ -5,39 +5,11 @@ export type DomainMatch = InterProScanResults['matches'][number];
|
|
|
5
5
|
* domain and site annotations, keyed by both the versioned and primary
|
|
6
6
|
* accession so callers can look up by whichever NCBI returned.
|
|
7
7
|
*/
|
|
8
|
-
export declare function parseCddDomains(xml: string): Map<string,
|
|
9
|
-
signature: {
|
|
10
|
-
entry?: {
|
|
11
|
-
name: string;
|
|
12
|
-
description: string;
|
|
13
|
-
accession: string;
|
|
14
|
-
featureType?: string;
|
|
15
|
-
};
|
|
16
|
-
};
|
|
17
|
-
locations: {
|
|
18
|
-
start: number;
|
|
19
|
-
end: number;
|
|
20
|
-
strand?: number;
|
|
21
|
-
}[];
|
|
22
|
-
}[]>;
|
|
8
|
+
export declare function parseCddDomains(xml: string): Map<string, import("msa-parsers").InterProScanMatch[]>;
|
|
23
9
|
/**
|
|
24
10
|
* Fetch pre-computed CDD domain and site annotations for NCBI protein
|
|
25
11
|
* accessions. These come baked into the GenPept records, so a single batched
|
|
26
12
|
* efetch returns them with no job submission or polling. Results are cached in
|
|
27
13
|
* IndexedDB so reopening a view doesn't refetch.
|
|
28
14
|
*/
|
|
29
|
-
export declare function fetchProteinDomains(accessions: string[]): Promise<Map<string,
|
|
30
|
-
signature: {
|
|
31
|
-
entry?: {
|
|
32
|
-
name: string;
|
|
33
|
-
description: string;
|
|
34
|
-
accession: string;
|
|
35
|
-
featureType?: string;
|
|
36
|
-
};
|
|
37
|
-
};
|
|
38
|
-
locations: {
|
|
39
|
-
start: number;
|
|
40
|
-
end: number;
|
|
41
|
-
strand?: number;
|
|
42
|
-
}[];
|
|
43
|
-
}[]>>;
|
|
15
|
+
export declare function fetchProteinDomains(accessions: string[]): Promise<Map<string, import("msa-parsers").InterProScanMatch[]>>;
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "
|
|
1
|
+
export declare const version = "3.1.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '
|
|
1
|
+
export const version = '3.1.0';
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "
|
|
2
|
+
"version": "3.1.0",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"name": "jbrowse-plugin-msaview",
|
|
5
5
|
"repository": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"git-cliff": "^2.13.1",
|
|
44
44
|
"mobx": "^6.16.1",
|
|
45
45
|
"mobx-react": "^9.2.2",
|
|
46
|
-
"msa-parsers": "^
|
|
46
|
+
"msa-parsers": "^6.0.0",
|
|
47
47
|
"pixelmatch": "^7.2.0",
|
|
48
48
|
"pngjs": "^7.0.0",
|
|
49
49
|
"prettier": "^3.9.6",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"puppeteer": "^25.3.0",
|
|
52
52
|
"react": "^19.2.8",
|
|
53
53
|
"react-dom": "^19.2.8",
|
|
54
|
-
"react-msaview": "^
|
|
54
|
+
"react-msaview": "^6.0.0",
|
|
55
55
|
"rimraf": "^6.1.3",
|
|
56
56
|
"rxjs": "^7.8.2",
|
|
57
57
|
"serve": "^14.2.6",
|
|
@@ -68,6 +68,8 @@
|
|
|
68
68
|
"prebuild": "pnpm clean",
|
|
69
69
|
"lint": "eslint src --report-unused-disable-directives --max-warnings 0",
|
|
70
70
|
"check-text-source": "node scripts/check-text-source.mjs",
|
|
71
|
+
"check-ebi-params": "node scripts/check-ebi-params.mjs",
|
|
72
|
+
"check-mui-imports": "node scripts/check-mui-imports.mjs",
|
|
71
73
|
"pretest": "rm -rf .test-jbrowse && npx @jbrowse/cli create .test-jbrowse --nightly",
|
|
72
74
|
"test": "vitest run",
|
|
73
75
|
"test:watch": "vitest",
|
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
import React from 'react'
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
2
|
|
|
3
3
|
import { shorten2 } from '@jbrowse/core/util'
|
|
4
|
-
import {
|
|
4
|
+
import { Alert, Typography } from '@mui/material'
|
|
5
5
|
import { observer } from 'mobx-react'
|
|
6
6
|
import { makeStyles } from 'tss-react/mui'
|
|
7
7
|
|
|
8
8
|
import { BASE_BLAST_URL } from './consts'
|
|
9
9
|
import ExternalLink from '../../../components/ExternalLink'
|
|
10
|
-
import
|
|
10
|
+
import TextField2 from '../../../components/TextField2'
|
|
11
|
+
import { useQueryRowName } from '../../useQueryRowName'
|
|
12
|
+
import {
|
|
13
|
+
cleanProteinSequence,
|
|
14
|
+
getGeneDisplayName,
|
|
15
|
+
getLinearGenomeView,
|
|
16
|
+
} from '../../util'
|
|
11
17
|
import LaunchPanelContent from '../LaunchPanelContent'
|
|
18
|
+
import { launchView } from '../ManualMSALoader/launchView'
|
|
19
|
+
import QueryRowSelector from '../QueryRowSelector'
|
|
20
|
+
import SubmitCancelActions from '../SubmitCancelActions'
|
|
12
21
|
import TranscriptSelector from '../TranscriptSelector'
|
|
13
22
|
import { useTranscriptSelection } from '../useTranscriptSelection'
|
|
14
23
|
|
|
@@ -17,14 +26,33 @@ import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
|
|
|
17
26
|
const useStyles = makeStyles()({
|
|
18
27
|
ncbiLink: {
|
|
19
28
|
wordBreak: 'break-all',
|
|
20
|
-
margin: 30,
|
|
21
|
-
maxWidth: 600,
|
|
22
29
|
},
|
|
23
|
-
|
|
30
|
+
textAreaFont: {
|
|
31
|
+
fontFamily: 'Courier New',
|
|
32
|
+
},
|
|
33
|
+
msaInput: {
|
|
34
|
+
marginBottom: 20,
|
|
35
|
+
},
|
|
36
|
+
step: {
|
|
24
37
|
marginTop: 20,
|
|
25
38
|
},
|
|
39
|
+
stepBody: {
|
|
40
|
+
marginLeft: 20,
|
|
41
|
+
marginTop: 8,
|
|
42
|
+
},
|
|
26
43
|
})
|
|
27
44
|
|
|
45
|
+
/**
|
|
46
|
+
* The route to NCBI's `nr`, which no plugin version can query directly: NCBI
|
|
47
|
+
* stopped sending Access-Control-Allow-Origin to third-party origins, so the
|
|
48
|
+
* browser cannot read Blast.cgi at all (see docs/blast.md).
|
|
49
|
+
*
|
|
50
|
+
* That makes the round trip through NCBI's own site the whole feature rather
|
|
51
|
+
* than a fallback, so the panel walks it end to end. It used to hand the user a
|
|
52
|
+
* link, tell them to "paste the results into JBrowse", and offer only a Close
|
|
53
|
+
* button -- leaving them to find the Manual upload tab, re-pick the transcript
|
|
54
|
+
* they had already chosen here, and hand-type the row name.
|
|
55
|
+
*/
|
|
28
56
|
const BlastManualPanel = observer(function ({
|
|
29
57
|
handleClose,
|
|
30
58
|
feature,
|
|
@@ -38,8 +66,13 @@ const BlastManualPanel = observer(function ({
|
|
|
38
66
|
}) {
|
|
39
67
|
const { classes } = useStyles()
|
|
40
68
|
const view = getLinearGenomeView(model)
|
|
69
|
+
const [launchViewError, setLaunchViewError] = useState<unknown>()
|
|
70
|
+
const [msaText, setMsaText] = useState('')
|
|
71
|
+
const [treeText, setTreeText] = useState('')
|
|
72
|
+
|
|
41
73
|
const transcriptSelection = useTranscriptSelection({ feature, view })
|
|
42
|
-
const { proteinSequence, error } = transcriptSelection
|
|
74
|
+
const { proteinSequence, selectedTranscript, error } = transcriptSelection
|
|
75
|
+
const queryRow = useQueryRowName(msaText, proteinSequence)
|
|
43
76
|
|
|
44
77
|
const s2 = cleanProteinSequence(proteinSequence)
|
|
45
78
|
// a link the user follows to NCBI's own site, not something we fetch — which
|
|
@@ -49,37 +82,100 @@ const BlastManualPanel = observer(function ({
|
|
|
49
82
|
|
|
50
83
|
return (
|
|
51
84
|
<>
|
|
52
|
-
<LaunchPanelContent error={error}>
|
|
85
|
+
<LaunchPanelContent error={launchViewError ?? error}>
|
|
53
86
|
{children}
|
|
54
87
|
|
|
55
88
|
<TranscriptSelector feature={feature} {...transcriptSelection} />
|
|
56
89
|
|
|
57
|
-
{
|
|
58
|
-
<
|
|
59
|
-
|
|
90
|
+
<div className={classes.step}>
|
|
91
|
+
<Typography variant="subtitle2">1. Run the search at NCBI</Typography>
|
|
92
|
+
<div className={classes.stepBody}>
|
|
93
|
+
{proteinSequence ? (
|
|
94
|
+
<div className={classes.ncbiLink}>
|
|
95
|
+
<ExternalLink href={link}>{link2}</ExternalLink>
|
|
96
|
+
</div>
|
|
97
|
+
) : (
|
|
98
|
+
<Alert severity="info">
|
|
99
|
+
Pick a transcript above to get a link carrying its protein
|
|
100
|
+
sequence.
|
|
101
|
+
</Alert>
|
|
102
|
+
)}
|
|
60
103
|
</div>
|
|
61
|
-
|
|
104
|
+
</div>
|
|
62
105
|
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
106
|
+
<div className={classes.step}>
|
|
107
|
+
<Typography variant="subtitle2">2. Align the hits</Typography>
|
|
108
|
+
<div className={classes.stepBody}>
|
|
109
|
+
<Typography>
|
|
110
|
+
On the results page click "Multiple Alignment" to run COBALT,
|
|
111
|
+
NCBI's aligner. Download the alignment (.aln) and, if you want the
|
|
112
|
+
tree drawn, the Newick tree (.nh).
|
|
113
|
+
</Typography>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className={classes.step}>
|
|
118
|
+
<Typography variant="subtitle2">
|
|
119
|
+
3. Paste the results back here
|
|
120
|
+
</Typography>
|
|
121
|
+
<div className={classes.stepBody}>
|
|
122
|
+
<TextField2
|
|
123
|
+
variant="outlined"
|
|
124
|
+
label="Alignment"
|
|
125
|
+
multiline
|
|
126
|
+
minRows={5}
|
|
127
|
+
maxRows={10}
|
|
128
|
+
fullWidth
|
|
129
|
+
className={classes.msaInput}
|
|
130
|
+
slotProps={{ input: { className: classes.textAreaFont } }}
|
|
131
|
+
placeholder="Paste the .aln contents here"
|
|
132
|
+
value={msaText}
|
|
133
|
+
onChange={event => {
|
|
134
|
+
setMsaText(event.target.value)
|
|
135
|
+
}}
|
|
136
|
+
/>
|
|
137
|
+
<TextField2
|
|
138
|
+
variant="outlined"
|
|
139
|
+
label="Tree (optional)"
|
|
140
|
+
multiline
|
|
141
|
+
minRows={3}
|
|
142
|
+
maxRows={10}
|
|
143
|
+
fullWidth
|
|
144
|
+
slotProps={{ input: { className: classes.textAreaFont } }}
|
|
145
|
+
placeholder="Paste the .nh Newick tree here"
|
|
146
|
+
value={treeText}
|
|
147
|
+
onChange={event => {
|
|
148
|
+
setTreeText(event.target.value)
|
|
149
|
+
}}
|
|
150
|
+
/>
|
|
151
|
+
|
|
152
|
+
<QueryRowSelector {...queryRow} />
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
70
155
|
</LaunchPanelContent>
|
|
71
156
|
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
157
|
+
<SubmitCancelActions
|
|
158
|
+
submitDisabled={!selectedTranscript || !msaText.trim()}
|
|
159
|
+
onSubmit={() => {
|
|
160
|
+
try {
|
|
161
|
+
if (selectedTranscript) {
|
|
162
|
+
setLaunchViewError(undefined)
|
|
163
|
+
launchView({
|
|
164
|
+
newViewTitle: getGeneDisplayName(selectedTranscript),
|
|
165
|
+
view,
|
|
166
|
+
feature: selectedTranscript,
|
|
167
|
+
querySeqName: queryRow.querySeqName,
|
|
168
|
+
data: { msa: msaText, tree: treeText },
|
|
169
|
+
})
|
|
170
|
+
handleClose()
|
|
171
|
+
}
|
|
172
|
+
} catch (e) {
|
|
173
|
+
console.error(e)
|
|
174
|
+
setLaunchViewError(e)
|
|
175
|
+
}
|
|
176
|
+
}}
|
|
177
|
+
onCancel={handleClose}
|
|
178
|
+
/>
|
|
83
179
|
</>
|
|
84
180
|
)
|
|
85
181
|
})
|
|
@@ -9,10 +9,17 @@ export const BASE_BLAST_URL = 'https://blast.ncbi.nlm.nih.gov/Blast.cgi'
|
|
|
9
9
|
export const msaAlgorithms = ['clustalo', 'muscle', 'kalign', 'mafft'] as const
|
|
10
10
|
export type MsaAlgorithm = (typeof msaAlgorithms)[number]
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* EBI rejects a submission naming a database outside its own list with a 400,
|
|
14
|
+
* so every value here has to appear in
|
|
15
|
+
* https://www.ebi.ac.uk/Tools/services/rest/ncbiblast/parameterdetails/database
|
|
16
|
+
* -- `uniprotkb_reference_proteomes` did not, and 3.0.0 shipped it as a dead
|
|
17
|
+
* menu entry.
|
|
18
|
+
*/
|
|
12
19
|
export const blastDatabaseOptions = [
|
|
13
20
|
'uniprotkb_swissprot',
|
|
14
21
|
'uniprotkb',
|
|
15
|
-
'
|
|
22
|
+
'pan_proteomes',
|
|
16
23
|
'uniprotkb_trembl',
|
|
17
24
|
] as const
|
|
18
25
|
export type BlastDatabase = (typeof blastDatabaseOptions)[number]
|
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
2
|
|
|
3
3
|
import { FileSelector } from '@jbrowse/core/ui'
|
|
4
|
-
import {
|
|
5
|
-
Alert,
|
|
6
|
-
FormControl,
|
|
7
|
-
FormControlLabel,
|
|
8
|
-
Radio,
|
|
9
|
-
RadioGroup,
|
|
10
|
-
} from '@mui/material'
|
|
4
|
+
import { FormControl, FormControlLabel, Radio, RadioGroup } from '@mui/material'
|
|
11
5
|
import { observer } from 'mobx-react'
|
|
12
6
|
import { makeStyles } from 'tss-react/mui'
|
|
13
7
|
|
|
14
8
|
import { launchView } from './launchView'
|
|
15
9
|
import TextField2 from '../../../components/TextField2'
|
|
10
|
+
import { useQueryRowName } from '../../useQueryRowName'
|
|
16
11
|
import { getGeneDisplayName, getLinearGenomeView } from '../../util'
|
|
17
12
|
import LaunchPanelContent from '../LaunchPanelContent'
|
|
13
|
+
import QueryRowSelector from '../QueryRowSelector'
|
|
18
14
|
import SubmitCancelActions from '../SubmitCancelActions'
|
|
19
15
|
import TranscriptSelector from '../TranscriptSelector'
|
|
20
16
|
import { useTranscriptSelection } from '../useTranscriptSelection'
|
|
@@ -38,12 +34,6 @@ const useStyles = makeStyles()({
|
|
|
38
34
|
msaInput: {
|
|
39
35
|
marginBottom: 20,
|
|
40
36
|
},
|
|
41
|
-
queryNameInput: {
|
|
42
|
-
marginTop: 20,
|
|
43
|
-
},
|
|
44
|
-
warningAlert: {
|
|
45
|
-
marginTop: 10,
|
|
46
|
-
},
|
|
47
37
|
})
|
|
48
38
|
|
|
49
39
|
const ManualMSALoader = observer(function PreLoadedMSA2({
|
|
@@ -63,9 +53,9 @@ const ManualMSALoader = observer(function PreLoadedMSA2({
|
|
|
63
53
|
const [treeText, setTreeText] = useState('')
|
|
64
54
|
const [msaFileLocation, setMsaFileLocation] = useState<FileLocation>()
|
|
65
55
|
const [treeFileLocation, setTreeFileLocation] = useState<FileLocation>()
|
|
66
|
-
const [querySeqName, setQuerySeqName] = useState('')
|
|
67
56
|
const transcriptSelection = useTranscriptSelection({ feature, view })
|
|
68
|
-
const { selectedTranscript, error } = transcriptSelection
|
|
57
|
+
const { selectedTranscript, proteinSequence, error } = transcriptSelection
|
|
58
|
+
const queryRow = useQueryRowName(msaText, proteinSequence)
|
|
69
59
|
|
|
70
60
|
const e = launchViewError ?? error
|
|
71
61
|
return (
|
|
@@ -143,27 +133,7 @@ const ManualMSALoader = observer(function PreLoadedMSA2({
|
|
|
143
133
|
|
|
144
134
|
<TranscriptSelector feature={feature} {...transcriptSelection} />
|
|
145
135
|
|
|
146
|
-
<
|
|
147
|
-
variant="outlined"
|
|
148
|
-
name="MSA row name"
|
|
149
|
-
fullWidth
|
|
150
|
-
required
|
|
151
|
-
className={classes.queryNameInput}
|
|
152
|
-
placeholder="Row name in MSA that corresponds to the selected transcript"
|
|
153
|
-
helperText="Required: Specify the name of the row in your MSA that should be aligned with the selected transcript"
|
|
154
|
-
value={querySeqName}
|
|
155
|
-
onChange={event => {
|
|
156
|
-
setQuerySeqName(event.target.value)
|
|
157
|
-
}}
|
|
158
|
-
/>
|
|
159
|
-
|
|
160
|
-
{!querySeqName.trim() ? (
|
|
161
|
-
<Alert severity="warning" className={classes.warningAlert}>
|
|
162
|
-
Without specifying the MSA row name, clicking on the MSA will not
|
|
163
|
-
navigate to the corresponding genome position, and hovering
|
|
164
|
-
highlights will not work.
|
|
165
|
-
</Alert>
|
|
166
|
-
) : null}
|
|
136
|
+
<QueryRowSelector {...queryRow} />
|
|
167
137
|
</LaunchPanelContent>
|
|
168
138
|
|
|
169
139
|
<SubmitCancelActions
|
|
@@ -180,7 +150,7 @@ const ManualMSALoader = observer(function PreLoadedMSA2({
|
|
|
180
150
|
newViewTitle: getGeneDisplayName(selectedTranscript),
|
|
181
151
|
view,
|
|
182
152
|
feature: selectedTranscript,
|
|
183
|
-
querySeqName: querySeqName
|
|
153
|
+
querySeqName: queryRow.querySeqName,
|
|
184
154
|
...(inputMethod === 'file'
|
|
185
155
|
? {
|
|
186
156
|
msaFilehandle: msaFileLocation,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { Alert, MenuItem } from '@mui/material'
|
|
4
|
+
import { makeStyles } from 'tss-react/mui'
|
|
5
|
+
|
|
6
|
+
import TextField2 from '../../components/TextField2'
|
|
7
|
+
|
|
8
|
+
import type { QueryRowMatch } from '../detectQueryRow'
|
|
9
|
+
|
|
10
|
+
const useStyles = makeStyles()({
|
|
11
|
+
field: {
|
|
12
|
+
marginTop: 20,
|
|
13
|
+
},
|
|
14
|
+
alert: {
|
|
15
|
+
marginTop: 10,
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Which MSA row corresponds to the selected transcript. Clicking and hovering in
|
|
21
|
+
* the alignment reach the genome only through this name, and a wrong one fails
|
|
22
|
+
* silently -- the view opens, renders, and never navigates -- so the field fills
|
|
23
|
+
* itself in from the pasted alignment and offers that alignment's own row names
|
|
24
|
+
* rather than a free text box the user can typo.
|
|
25
|
+
*/
|
|
26
|
+
export default function QueryRowSelector({
|
|
27
|
+
names,
|
|
28
|
+
detected,
|
|
29
|
+
querySeqName,
|
|
30
|
+
setQuerySeqName,
|
|
31
|
+
isAutoDetected,
|
|
32
|
+
}: {
|
|
33
|
+
names: string[]
|
|
34
|
+
detected?: QueryRowMatch
|
|
35
|
+
querySeqName: string
|
|
36
|
+
setQuerySeqName: (arg: string) => void
|
|
37
|
+
isAutoDetected: boolean
|
|
38
|
+
}) {
|
|
39
|
+
const { classes } = useStyles()
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
{names.length > 0 ? (
|
|
44
|
+
<TextField2
|
|
45
|
+
variant="outlined"
|
|
46
|
+
label="MSA row matching the selected transcript"
|
|
47
|
+
select
|
|
48
|
+
fullWidth
|
|
49
|
+
className={classes.field}
|
|
50
|
+
value={names.includes(querySeqName) ? querySeqName : ''}
|
|
51
|
+
onChange={event => {
|
|
52
|
+
setQuerySeqName(event.target.value)
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
{names.map(name => (
|
|
56
|
+
<MenuItem value={name} key={name}>
|
|
57
|
+
{name}
|
|
58
|
+
{detected?.name === name ? ' — matches your protein' : ''}
|
|
59
|
+
</MenuItem>
|
|
60
|
+
))}
|
|
61
|
+
</TextField2>
|
|
62
|
+
) : (
|
|
63
|
+
<TextField2
|
|
64
|
+
variant="outlined"
|
|
65
|
+
label="MSA row matching the selected transcript"
|
|
66
|
+
fullWidth
|
|
67
|
+
className={classes.field}
|
|
68
|
+
helperText="Paste an alignment above and this fills in on its own"
|
|
69
|
+
value={querySeqName}
|
|
70
|
+
onChange={event => {
|
|
71
|
+
setQuerySeqName(event.target.value)
|
|
72
|
+
}}
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{isAutoDetected && detected ? (
|
|
77
|
+
<Alert severity="success" className={classes.alert}>
|
|
78
|
+
Matched <strong>{detected.name}</strong> to your protein sequence
|
|
79
|
+
{detected.quality === 'exact'
|
|
80
|
+
? ''
|
|
81
|
+
: `, covering ${Math.round(detected.identity * 100)}% of it`}
|
|
82
|
+
. Clicking the alignment will navigate the genome view.
|
|
83
|
+
</Alert>
|
|
84
|
+
) : names.length > 0 && !querySeqName ? (
|
|
85
|
+
<Alert severity="warning" className={classes.alert}>
|
|
86
|
+
No row matched your protein sequence — pick the one for your gene
|
|
87
|
+
above. Without it the alignment still renders, but clicking it will
|
|
88
|
+
not navigate the genome view.
|
|
89
|
+
</Alert>
|
|
90
|
+
) : null}
|
|
91
|
+
</>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { detectQueryRow, getMsaRowNames } from './detectQueryRow'
|
|
4
|
+
|
|
5
|
+
const protein = 'MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD'
|
|
6
|
+
|
|
7
|
+
// COBALT renames the query `Query_1`, so only the residues identify it
|
|
8
|
+
const clustal = `CLUSTAL W (1.81) multiple sequence alignment
|
|
9
|
+
|
|
10
|
+
Query_1 MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD
|
|
11
|
+
sp|P02769|ALBU MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPYD
|
|
12
|
+
sp|Q5XLE4|OTHE MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLWWCPFD
|
|
13
|
+
`
|
|
14
|
+
|
|
15
|
+
const fasta = `>Query_1
|
|
16
|
+
MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD
|
|
17
|
+
>sp|P02769|ALBU_BOVIN
|
|
18
|
+
MKWVTFISLLLLFSSAYSRG--RRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPYD
|
|
19
|
+
`
|
|
20
|
+
|
|
21
|
+
describe('detectQueryRow', () => {
|
|
22
|
+
test('finds the query by sequence when the aligner renamed it', () => {
|
|
23
|
+
expect(detectQueryRow(clustal, protein)).toMatchObject({
|
|
24
|
+
name: 'Query_1',
|
|
25
|
+
quality: 'exact',
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('ignores gaps in the aligned row', () => {
|
|
30
|
+
expect(detectQueryRow(fasta, protein)?.name).toBe('Query_1')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('tolerates the trailing stop codon the translation carries', () => {
|
|
34
|
+
expect(detectQueryRow(clustal, `${protein}*`)?.name).toBe('Query_1')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('matches a row that is the query trimmed to the aligned region', () => {
|
|
38
|
+
const trimmed = `>hit_one\nWRONGWRONGWRONGWRONG\n>aligned_query\n${protein.slice(5, 40)}\n`
|
|
39
|
+
expect(detectQueryRow(trimmed, protein)).toMatchObject({
|
|
40
|
+
name: 'aligned_query',
|
|
41
|
+
quality: 'partial',
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// the failure that matters: silently wiring the view to a homolog would look
|
|
46
|
+
// like it worked, and every navigation afterwards would land in the wrong place
|
|
47
|
+
test('returns nothing when only diverged homologs are present', () => {
|
|
48
|
+
const homologsOnly = `>hit_one
|
|
49
|
+
MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD
|
|
50
|
+
>hit_two
|
|
51
|
+
MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD
|
|
52
|
+
`
|
|
53
|
+
expect(
|
|
54
|
+
detectQueryRow(homologsOnly, 'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWW'),
|
|
55
|
+
).toBeUndefined()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('returns nothing rather than throwing on a half-pasted alignment', () => {
|
|
59
|
+
expect(detectQueryRow('>partial\nMKWV', protein)).toBeUndefined()
|
|
60
|
+
expect(detectQueryRow('not an alignment at all', protein)).toBeUndefined()
|
|
61
|
+
expect(detectQueryRow('', protein)).toBeUndefined()
|
|
62
|
+
expect(detectQueryRow(clustal, '')).toBeUndefined()
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('getMsaRowNames', () => {
|
|
67
|
+
test('lists the rows for the override dropdown', () => {
|
|
68
|
+
expect(getMsaRowNames(clustal)).toEqual([
|
|
69
|
+
'Query_1',
|
|
70
|
+
'sp|P02769|ALBU',
|
|
71
|
+
'sp|Q5XLE4|OTHE',
|
|
72
|
+
])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('is empty rather than throwing while the user is still pasting', () => {
|
|
76
|
+
expect(getMsaRowNames('CLUSTAL W')).toEqual([])
|
|
77
|
+
expect(getMsaRowNames('')).toEqual([])
|
|
78
|
+
})
|
|
79
|
+
})
|