jbrowse-plugin-msaview 3.4.0 → 3.5.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 +7 -0
- package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +23 -17
- package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -0
- package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +5 -3
- package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +24 -4
- package/dist/LaunchMsaView/components/BlastQuery/consts.js +44 -2
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +7 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +42 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.js +32 -0
- package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -0
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.d.ts +3 -1
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +2 -1
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +10 -4
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +4 -1
- package/dist/LaunchMsaView/components/QueryRowSelector.js +21 -5
- package/dist/LaunchMsaView/detectQueryRow.d.ts +11 -0
- package/dist/LaunchMsaView/detectQueryRow.js +11 -4
- package/dist/LaunchMsaView/detectQueryRow.test.js +30 -0
- package/dist/LaunchMsaView/useQueryRowName.d.ts +1 -0
- package/dist/LaunchMsaView/useQueryRowName.js +5 -1
- package/dist/LaunchMsaView/useQueryRowName.test.d.ts +1 -0
- package/dist/LaunchMsaView/useQueryRowName.test.js +23 -0
- package/dist/LaunchMsaViewExtensionPoint/index.js +19 -12
- package/dist/LaunchMsaViewExtensionPoint/index.test.d.ts +1 -0
- package/dist/LaunchMsaViewExtensionPoint/index.test.js +64 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +16 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.js +138 -86
- package/dist/MsaViewPanel/components/LaunchProgress.js +23 -5
- package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -2
- package/dist/MsaViewPanel/components/MsaViewPanel.test.js +40 -0
- package/dist/MsaViewPanel/doLaunchBlast.d.ts +11 -1
- package/dist/MsaViewPanel/doLaunchBlast.js +79 -96
- package/dist/MsaViewPanel/doLaunchBlast.test.d.ts +1 -0
- package/dist/MsaViewPanel/doLaunchBlast.test.js +158 -0
- package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +3 -1
- package/dist/MsaViewPanel/doLaunchOrthologs.js +46 -10
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +48 -24
- package/dist/MsaViewPanel/genomeToMSA.js +4 -4
- package/dist/MsaViewPanel/genomeToMSA.test.js +67 -0
- package/dist/MsaViewPanel/loadProteinDomains.d.ts +8 -2
- package/dist/MsaViewPanel/loadProteinDomains.js +18 -9
- package/dist/MsaViewPanel/loadProteinDomains.test.d.ts +1 -0
- package/dist/MsaViewPanel/loadProteinDomains.test.js +41 -0
- package/dist/MsaViewPanel/model.d.ts +306 -85
- package/dist/MsaViewPanel/model.js +169 -42
- package/dist/MsaViewPanel/model.test.d.ts +1 -0
- package/dist/MsaViewPanel/model.test.js +100 -0
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +12 -1
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +8 -8
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +133 -32
- package/dist/MsaViewPanel/msaDataStore.d.ts +7 -3
- package/dist/MsaViewPanel/msaDataStore.js +26 -7
- package/dist/MsaViewPanel/msaDataStore.test.d.ts +1 -0
- package/dist/MsaViewPanel/msaDataStore.test.js +44 -0
- package/dist/MsaViewPanel/observeProteinHighlights.test.js +20 -3
- package/dist/MsaViewPanel/processInit.test.d.ts +1 -0
- package/dist/MsaViewPanel/processInit.test.js +72 -0
- package/dist/MsaViewPanel/resolveConnectedTranscript.d.ts +9 -0
- package/dist/MsaViewPanel/resolveConnectedTranscript.js +122 -0
- package/dist/MsaViewPanel/runLaunch.d.ts +39 -0
- package/dist/MsaViewPanel/runLaunch.js +100 -0
- package/dist/MsaViewPanel/runLaunch.test.d.ts +1 -0
- package/dist/MsaViewPanel/runLaunch.test.js +175 -0
- package/dist/MsaViewPanel/storedData.test.d.ts +1 -0
- package/dist/MsaViewPanel/storedData.test.js +160 -0
- package/dist/MsaViewPanel/structureConnection.d.ts +0 -4
- package/dist/MsaViewPanel/structureConnection.js +0 -19
- package/dist/MsaViewPanel/util.d.ts +39 -0
- package/dist/MsaViewPanel/util.js +44 -0
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +46 -45
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.js +32 -0
- package/dist/utils/blastCache.test.d.ts +1 -0
- package/dist/utils/blastCache.test.js +72 -0
- package/dist/utils/browserAlign.d.ts +42 -0
- package/dist/utils/browserAlign.js +297 -0
- package/dist/utils/browserAlign.test.d.ts +1 -0
- package/dist/utils/browserAlign.test.js +85 -0
- package/dist/utils/ebiBlast.d.ts +13 -2
- package/dist/utils/ebiBlast.js +32 -4
- package/dist/utils/ebiJobDispatcher.d.ts +6 -3
- package/dist/utils/ebiJobDispatcher.js +13 -6
- package/dist/utils/fetch.d.ts +8 -1
- package/dist/utils/fetch.js +28 -2
- package/dist/utils/homologSearch.d.ts +31 -0
- package/dist/utils/homologSearch.js +6 -0
- package/dist/utils/msa.d.ts +5 -11
- package/dist/utils/msa.js +19 -30
- package/dist/utils/msaRows.d.ts +11 -8
- package/dist/utils/msaRows.js +14 -12
- package/dist/utils/ncbiDomains.d.ts +2 -2
- package/dist/utils/ncbiOrthologs.d.ts +11 -1
- package/dist/utils/ncbiOrthologs.js +26 -7
- package/dist/utils/ncbiOrthologs.test.js +23 -1
- package/dist/utils/pantherOrthologs.js +2 -10
- package/dist/utils/phmmer.d.ts +12 -1
- package/dist/utils/phmmer.js +52 -13
- package/dist/utils/poll.d.ts +6 -1
- package/dist/utils/poll.js +7 -2
- package/dist/utils/unirefHomologs.d.ts +94 -0
- package/dist/utils/unirefHomologs.js +193 -0
- package/dist/utils/unirefHomologs.test.d.ts +1 -0
- package/dist/utils/unirefHomologs.test.js +118 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +17 -16
- package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +40 -19
- package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -0
- package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +7 -2
- package/src/LaunchMsaView/components/BlastQuery/consts.ts +50 -2
- package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.ts +43 -0
- package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +64 -0
- package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -0
- package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +4 -0
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +12 -5
- package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +4 -1
- package/src/LaunchMsaView/components/QueryRowSelector.tsx +35 -7
- package/src/LaunchMsaView/detectQueryRow.test.ts +34 -0
- package/src/LaunchMsaView/detectQueryRow.ts +22 -4
- package/src/LaunchMsaView/useQueryRowName.test.ts +27 -0
- package/src/LaunchMsaView/useQueryRowName.ts +5 -1
- package/src/LaunchMsaViewExtensionPoint/index.test.ts +74 -0
- package/src/LaunchMsaViewExtensionPoint/index.ts +53 -12
- package/src/MsaViewPanel/afterCreateAutoruns.ts +150 -81
- package/src/MsaViewPanel/components/LaunchProgress.tsx +46 -4
- package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +43 -0
- package/src/MsaViewPanel/components/MsaViewPanel.tsx +8 -2
- package/src/MsaViewPanel/doLaunchBlast.test.ts +207 -0
- package/src/MsaViewPanel/doLaunchBlast.ts +94 -140
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +54 -26
- package/src/MsaViewPanel/doLaunchOrthologs.ts +63 -9
- package/src/MsaViewPanel/genomeToMSA.test.ts +76 -0
- package/src/MsaViewPanel/genomeToMSA.ts +8 -4
- package/src/MsaViewPanel/loadProteinDomains.test.ts +52 -0
- package/src/MsaViewPanel/loadProteinDomains.ts +24 -11
- package/src/MsaViewPanel/model.test.ts +121 -0
- package/src/MsaViewPanel/model.ts +200 -49
- package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +152 -32
- package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +21 -11
- package/src/MsaViewPanel/msaDataStore.test.ts +54 -0
- package/src/MsaViewPanel/msaDataStore.ts +32 -13
- package/src/MsaViewPanel/observeProteinHighlights.test.ts +27 -3
- package/src/MsaViewPanel/processInit.test.ts +84 -0
- package/src/MsaViewPanel/resolveConnectedTranscript.ts +150 -0
- package/src/MsaViewPanel/runLaunch.test.ts +208 -0
- package/src/MsaViewPanel/runLaunch.ts +141 -0
- package/src/MsaViewPanel/storedData.test.ts +196 -0
- package/src/MsaViewPanel/structureConnection.ts +0 -26
- package/src/MsaViewPanel/util.ts +75 -0
- package/src/utils/blastCache.test.ts +86 -0
- package/src/utils/blastCache.ts +34 -1
- package/src/utils/browserAlign.test.ts +102 -0
- package/src/utils/browserAlign.ts +352 -0
- package/src/utils/ebiBlast.ts +42 -2
- package/src/utils/ebiJobDispatcher.ts +18 -3
- package/src/utils/fetch.ts +29 -2
- package/src/utils/homologSearch.ts +51 -0
- package/src/utils/msa.ts +25 -39
- package/src/utils/msaRows.ts +20 -16
- package/src/utils/ncbiDomains.ts +4 -2
- package/src/utils/ncbiOrthologs.test.ts +25 -0
- package/src/utils/ncbiOrthologs.ts +27 -7
- package/src/utils/pantherOrthologs.ts +6 -11
- package/src/utils/phmmer.ts +62 -12
- package/src/utils/poll.ts +8 -1
- package/src/utils/unirefHomologs.test.ts +149 -0
- package/src/utils/unirefHomologs.ts +323 -0
- package/src/version.ts +1 -1
- package/dist/MsaViewPanel/structureConnection.test.js +0 -53
- package/src/MsaViewPanel/structureConnection.test.ts +0 -62
- /package/dist/{MsaViewPanel/structureConnection.test.d.ts → LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -29,6 +29,13 @@ https://jbrowse.org/code/jb2/main/index.html?config=https://unpkg.com/jbrowse-pl
|
|
|
29
29
|
- Can share sessions with other users which will send relevant settings and
|
|
30
30
|
links to files to automatically open your results
|
|
31
31
|
- The tree or the MSA panel can be loaded separately from each other
|
|
32
|
+
- Builds a cross-species alignment for any gene from the genome view's
|
|
33
|
+
right-click menu: precomputed orthologs (NCBI, PANTHER), the gene's UniRef
|
|
34
|
+
cluster across all of UniProtKB, or a phmmer/blastp search at EBI
|
|
35
|
+
- Aligns and builds trees in the browser, so a UniRef launch needs no job at any
|
|
36
|
+
external service; EBI's aligners remain an option
|
|
37
|
+
- Every launch is also a session-spec URL (`orthologParams`, `searchParams`,
|
|
38
|
+
`connectedTranscript`), see [DEVELOPERS.md](DEVELOPERS.md)
|
|
32
39
|
|
|
33
40
|
## File format supports
|
|
34
41
|
|
|
@@ -12,7 +12,8 @@ import { useTranscriptSelection } from '../useTranscriptSelection';
|
|
|
12
12
|
import CachedBlastResults from './CachedBlastResults';
|
|
13
13
|
import MsaAlgorithmSelect from './MsaAlgorithmSelect';
|
|
14
14
|
import { blastLaunchView } from './blastLaunchView';
|
|
15
|
-
import { databaseOptionsFor,
|
|
15
|
+
import { databaseOptionsFor, defaultMaxHits, defaultSearchFor, searchPrograms, } from './consts';
|
|
16
|
+
import { useStoredMsaAlgorithm, useStoredSearchChoice, } from './searchChoiceStorage';
|
|
16
17
|
import { useCachedBlastResults } from './useCachedBlastResults';
|
|
17
18
|
const useStyles = makeStyles()({
|
|
18
19
|
selectField: {
|
|
@@ -38,11 +39,11 @@ const BlastAutomaticPanel = observer(function ({ handleClose, feature, model, ch
|
|
|
38
39
|
// one piece of state, not two: a program and a database that program does not
|
|
39
40
|
// have is a 400 from EBI minutes after Submit, and holding them apart is what
|
|
40
41
|
// would let them drift into that
|
|
41
|
-
const [search, setSearch] =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
42
|
+
const [search, setSearch] = useStoredSearchChoice();
|
|
43
|
+
const [selectedMsaAlgorithm, setSelectedMsaAlgorithm] = useStoredMsaAlgorithm();
|
|
44
|
+
const [maxHits, setMaxHits] = useState(String(defaultMaxHits));
|
|
45
|
+
const hitCount = Number(maxHits);
|
|
46
|
+
const hitCountValid = Number.isInteger(hitCount) && hitCount >= 1;
|
|
46
47
|
const isPhmmer = search.program === 'phmmer';
|
|
47
48
|
const geneIds = useMemo(() => getGeneIdentifiers(feature), [feature]);
|
|
48
49
|
const { results: cachedResults, error: cachedResultsError } = useCachedBlastResults(geneIds);
|
|
@@ -65,28 +66,31 @@ const BlastAutomaticPanel = observer(function ({ handleClose, feature, model, ch
|
|
|
65
66
|
});
|
|
66
67
|
} }, databaseOptionsFor(search.program).map(val => (React.createElement(MenuItem, { value: val, key: val }, val)))),
|
|
67
68
|
isPhmmer ? null : (React.createElement(MsaAlgorithmSelect, { className: classes.selectField, value: selectedMsaAlgorithm, onChange: setSelectedMsaAlgorithm })),
|
|
69
|
+
React.createElement(TextField2, { variant: "outlined", label: "Hits", className: classes.selectField, type: "number", value: maxHits, onChange: event => {
|
|
70
|
+
setMaxHits(event.target.value);
|
|
71
|
+
}, error: !hitCountValid, helperText: "best-scoring sequences to keep" }),
|
|
68
72
|
React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection }),
|
|
69
73
|
React.createElement(Typography, { className: classes.infoText },
|
|
70
74
|
isPhmmer
|
|
71
75
|
? `phmmer searches UniProtKB with a profile HMM built from the query,
|
|
72
76
|
so it aligns the hits as it finds them and that alignment is used
|
|
73
77
|
directly — nothing is realigned afterwards. The tree is then built
|
|
74
|
-
from it
|
|
75
|
-
|
|
78
|
+
from it in the browser. A hit matching the query in more than one
|
|
79
|
+
place appears once per matched region. rp15 spreads the hits
|
|
80
|
+
across all of life; swissprot keeps to the curated set.`
|
|
76
81
|
: `This panel will automatically submit a blastp query to EBI, which
|
|
77
|
-
searches UniProtKB.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
alignment.`,
|
|
82
|
+
searches UniProtKB. swissprot returns curated sequences that align
|
|
83
|
+
more cleanly than the many near-identical entries a TrEMBL search
|
|
84
|
+
brings back. After completion, all the hits will be run through
|
|
85
|
+
the chosen aligner; "in browser" needs no second EBI job.`,
|
|
82
86
|
' ',
|
|
83
|
-
"Searching NCBI's nr needs the manual approach: NCBI no longer lets a browser read responses from Blast.cgi."),
|
|
87
|
+
"EBI's queue is the wait, and it varies from seconds to many minutes. For a homolog panel with no job at all, the Orthologs tab's UniRef source is a lookup. Searching NCBI's nr needs the manual approach: NCBI no longer lets a browser read responses from Blast.cgi."),
|
|
84
88
|
cachedResults.length > 0 ? (React.createElement(Accordion, { className: classes.cachedResultsAccordion },
|
|
85
89
|
React.createElement(AccordionSummary, { expandIcon: React.createElement(ExpandMoreIcon, null) },
|
|
86
90
|
React.createElement(Typography, null, "Previous BLAST Results")),
|
|
87
91
|
React.createElement(AccordionDetails, null,
|
|
88
92
|
React.createElement(CachedBlastResults, { model: model, handleClose: handleClose, feature: feature })))) : null),
|
|
89
|
-
React.createElement(SubmitCancelActions, { model: model, submitDisabled: !proteinSequence, onSubmit: () => {
|
|
93
|
+
React.createElement(SubmitCancelActions, { model: model, submitDisabled: !proteinSequence || !hitCountValid, onSubmit: () => {
|
|
90
94
|
try {
|
|
91
95
|
if (selectedTranscript) {
|
|
92
96
|
setLaunchViewError(undefined);
|
|
@@ -98,14 +102,16 @@ const BlastAutomaticPanel = observer(function ({ handleClose, feature, model, ch
|
|
|
98
102
|
? {
|
|
99
103
|
searchProgram: 'phmmer',
|
|
100
104
|
blastDatabase: search.database,
|
|
101
|
-
|
|
105
|
+
maxHits: hitCount,
|
|
106
|
+
selectedTranscript: selectedTranscript.toJSON(),
|
|
102
107
|
proteinSequence,
|
|
103
108
|
}
|
|
104
109
|
: {
|
|
105
110
|
searchProgram: 'blastp',
|
|
106
111
|
blastDatabase: search.database,
|
|
107
112
|
msaAlgorithm: selectedMsaAlgorithm,
|
|
108
|
-
|
|
113
|
+
maxHits: hitCount,
|
|
114
|
+
selectedTranscript: selectedTranscript.toJSON(),
|
|
109
115
|
proteinSequence,
|
|
110
116
|
},
|
|
111
117
|
});
|
|
@@ -88,6 +88,7 @@ const BlastManualPanel = observer(function ({ handleClose, feature, model, child
|
|
|
88
88
|
view,
|
|
89
89
|
feature: selectedTranscript,
|
|
90
90
|
querySeqName: queryRow.querySeqName,
|
|
91
|
+
querySeqOffset: queryRow.querySeqOffset,
|
|
91
92
|
data: { msa: msaText, tree: treeText },
|
|
92
93
|
});
|
|
93
94
|
handleClose();
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { MenuItem } from '@mui/material';
|
|
3
3
|
import TextField2 from '../../../components/TextField2';
|
|
4
|
-
import { msaAlgorithms } from './consts';
|
|
4
|
+
import { msaAlgorithmLabels, msaAlgorithms } from './consts';
|
|
5
5
|
export default function MsaAlgorithmSelect({ value, onChange, className, }) {
|
|
6
|
-
return (React.createElement(TextField2, { variant: "outlined", label: "MSA Algorithm", className: className, select: true, value: value,
|
|
6
|
+
return (React.createElement(TextField2, { variant: "outlined", label: "MSA Algorithm", className: className, select: true, value: value, helperText: value === 'browser'
|
|
7
|
+
? 'no EBI job; rows aligned to the query'
|
|
8
|
+
: undefined, onChange: event => {
|
|
7
9
|
onChange(event.target.value);
|
|
8
|
-
} }, msaAlgorithms.map(val => (React.createElement(MenuItem, { value: val, key: val }, val)))));
|
|
10
|
+
} }, msaAlgorithms.map(val => (React.createElement(MenuItem, { value: val, key: val }, msaAlgorithmLabels[val])))));
|
|
9
11
|
}
|
|
@@ -5,8 +5,20 @@
|
|
|
5
5
|
* cannot read a response from it at all. See docs/blast.md.
|
|
6
6
|
*/
|
|
7
7
|
export declare const BASE_BLAST_URL = "https://blast.ncbi.nlm.nih.gov/Blast.cgi";
|
|
8
|
-
|
|
8
|
+
/** The aligners EBI's Job Dispatcher runs, each a tool name at their REST api. */
|
|
9
|
+
export declare const ebiMsaAlgorithms: readonly ["clustalo", "muscle", "kalign", "mafft"];
|
|
10
|
+
export type EbiMsaAlgorithm = (typeof ebiMsaAlgorithms)[number];
|
|
11
|
+
/**
|
|
12
|
+
* `browser` is no job at all: each sequence is aligned to the query in the
|
|
13
|
+
* page and the rows merged on the query (utils/browserAlign.ts), with the tree
|
|
14
|
+
* built by react-msaview's neighbour joining. It is the aligner for a launch
|
|
15
|
+
* that must not depend on EBI, and for one that wants to be quick -- a
|
|
16
|
+
* hundred rows take a second or two against a Job Dispatcher queue that has
|
|
17
|
+
* been measured at anything from ten seconds to fifteen minutes.
|
|
18
|
+
*/
|
|
19
|
+
export declare const msaAlgorithms: readonly ["clustalo", "muscle", "kalign", "mafft", "browser"];
|
|
9
20
|
export type MsaAlgorithm = (typeof msaAlgorithms)[number];
|
|
21
|
+
export declare const msaAlgorithmLabels: Record<MsaAlgorithm, string>;
|
|
10
22
|
/**
|
|
11
23
|
* EBI rejects a submission naming a database outside its own list with a 400,
|
|
12
24
|
* so every value here has to appear in
|
|
@@ -21,13 +33,21 @@ export declare const searchPrograms: readonly ["blastp", "phmmer"];
|
|
|
21
33
|
export type SearchProgram = (typeof searchPrograms)[number];
|
|
22
34
|
/**
|
|
23
35
|
* phmmer offers PDB, AlphaFold, Ensembl Genomes, MEROPS and ChEMBL too, but
|
|
24
|
-
* targets outside UniProt carry no
|
|
36
|
+
* targets outside UniProt carry no species in their description, so those rows
|
|
25
37
|
* would lose their species and common name. Only the databases that label their
|
|
26
38
|
* hits are offered.
|
|
39
|
+
*
|
|
40
|
+
* rp15..rp75 are the Representative Proteomes: UniProt's reference proteomes
|
|
41
|
+
* thinned so that no two are more than 15% (35%, 55%, 75%) similar, which is
|
|
42
|
+
* the widest taxonomic spread per hit that any of these databases gives. rp15
|
|
43
|
+
* is the one to reach for when the question is "what is this like across all
|
|
44
|
+
* of life"; swissprot when it is "what is this like in the curated set".
|
|
27
45
|
*/
|
|
28
|
-
export declare const phmmerDatabaseOptions: readonly ["swissprot", "uniprotkb", "uniprotrefprot"];
|
|
46
|
+
export declare const phmmerDatabaseOptions: readonly ["swissprot", "uniprotkb", "uniprotrefprot", "rp75", "rp55", "rp35", "rp15"];
|
|
29
47
|
export type PhmmerDatabase = (typeof phmmerDatabaseOptions)[number];
|
|
30
48
|
export declare const defaultPhmmerDatabase: PhmmerDatabase;
|
|
49
|
+
export declare const defaultMaxHits = 100;
|
|
50
|
+
export declare function snapBlastHitCount(maxHits: number): number;
|
|
31
51
|
/**
|
|
32
52
|
* A program together with a database that program actually has.
|
|
33
53
|
*
|
|
@@ -44,4 +64,4 @@ export type SearchChoice = {
|
|
|
44
64
|
database: PhmmerDatabase;
|
|
45
65
|
};
|
|
46
66
|
export declare function defaultSearchFor(program: SearchProgram): SearchChoice;
|
|
47
|
-
export declare function databaseOptionsFor(program: SearchProgram): readonly ["uniprotkb_swissprot", "uniprotkb", "pan_proteomes", "uniprotkb_trembl"] | readonly ["swissprot", "uniprotkb", "uniprotrefprot"];
|
|
67
|
+
export declare function databaseOptionsFor(program: SearchProgram): readonly ["uniprotkb_swissprot", "uniprotkb", "pan_proteomes", "uniprotkb_trembl"] | readonly ["swissprot", "uniprotkb", "uniprotrefprot", "rp75", "rp55", "rp35", "rp15"];
|
|
@@ -5,7 +5,29 @@
|
|
|
5
5
|
* cannot read a response from it at all. See docs/blast.md.
|
|
6
6
|
*/
|
|
7
7
|
export const BASE_BLAST_URL = 'https://blast.ncbi.nlm.nih.gov/Blast.cgi';
|
|
8
|
-
|
|
8
|
+
/** The aligners EBI's Job Dispatcher runs, each a tool name at their REST api. */
|
|
9
|
+
export const ebiMsaAlgorithms = [
|
|
10
|
+
'clustalo',
|
|
11
|
+
'muscle',
|
|
12
|
+
'kalign',
|
|
13
|
+
'mafft',
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* `browser` is no job at all: each sequence is aligned to the query in the
|
|
17
|
+
* page and the rows merged on the query (utils/browserAlign.ts), with the tree
|
|
18
|
+
* built by react-msaview's neighbour joining. It is the aligner for a launch
|
|
19
|
+
* that must not depend on EBI, and for one that wants to be quick -- a
|
|
20
|
+
* hundred rows take a second or two against a Job Dispatcher queue that has
|
|
21
|
+
* been measured at anything from ten seconds to fifteen minutes.
|
|
22
|
+
*/
|
|
23
|
+
export const msaAlgorithms = [...ebiMsaAlgorithms, 'browser'];
|
|
24
|
+
export const msaAlgorithmLabels = {
|
|
25
|
+
clustalo: 'clustalo (EBI)',
|
|
26
|
+
muscle: 'muscle (EBI)',
|
|
27
|
+
kalign: 'kalign (EBI)',
|
|
28
|
+
mafft: 'mafft (EBI)',
|
|
29
|
+
browser: 'in browser, query-anchored',
|
|
30
|
+
};
|
|
9
31
|
/**
|
|
10
32
|
* EBI rejects a submission naming a database outside its own list with a 400,
|
|
11
33
|
* so every value here has to appear in
|
|
@@ -25,16 +47,36 @@ export const defaultBlastDatabase = 'uniprotkb_swissprot';
|
|
|
25
47
|
export const searchPrograms = ['blastp', 'phmmer'];
|
|
26
48
|
/**
|
|
27
49
|
* phmmer offers PDB, AlphaFold, Ensembl Genomes, MEROPS and ChEMBL too, but
|
|
28
|
-
* targets outside UniProt carry no
|
|
50
|
+
* targets outside UniProt carry no species in their description, so those rows
|
|
29
51
|
* would lose their species and common name. Only the databases that label their
|
|
30
52
|
* hits are offered.
|
|
53
|
+
*
|
|
54
|
+
* rp15..rp75 are the Representative Proteomes: UniProt's reference proteomes
|
|
55
|
+
* thinned so that no two are more than 15% (35%, 55%, 75%) similar, which is
|
|
56
|
+
* the widest taxonomic spread per hit that any of these databases gives. rp15
|
|
57
|
+
* is the one to reach for when the question is "what is this like across all
|
|
58
|
+
* of life"; swissprot when it is "what is this like in the curated set".
|
|
31
59
|
*/
|
|
32
60
|
export const phmmerDatabaseOptions = [
|
|
33
61
|
'swissprot',
|
|
34
62
|
'uniprotkb',
|
|
35
63
|
'uniprotrefprot',
|
|
64
|
+
'rp75',
|
|
65
|
+
'rp55',
|
|
66
|
+
'rp35',
|
|
67
|
+
'rp15',
|
|
36
68
|
];
|
|
37
69
|
export const defaultPhmmerDatabase = 'swissprot';
|
|
70
|
+
/**
|
|
71
|
+
* The hit counts EBI's ncbiblast accepts for `alignments` and `scores`. A value
|
|
72
|
+
* off this list is a 400 at submit time, so a request is rounded up to the next
|
|
73
|
+
* one on it.
|
|
74
|
+
*/
|
|
75
|
+
const blastHitCounts = [5, 10, 20, 50, 100, 150, 200, 250, 500, 750, 1000];
|
|
76
|
+
export const defaultMaxHits = 100;
|
|
77
|
+
export function snapBlastHitCount(maxHits) {
|
|
78
|
+
return blastHitCounts.find(n => n >= maxHits) ?? blastHitCounts.at(-1);
|
|
79
|
+
}
|
|
38
80
|
export function defaultSearchFor(program) {
|
|
39
81
|
return program === 'phmmer'
|
|
40
82
|
? { program, database: defaultPhmmerDatabase }
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MsaAlgorithm, SearchChoice } from './consts';
|
|
2
|
+
export declare const SEARCH_CHOICE_STORAGE_KEY = "msaView-blastSearch";
|
|
3
|
+
export declare const MSA_ALGORITHM_STORAGE_KEY = "msaView-msaAlgorithm";
|
|
4
|
+
export declare function validSearchChoice(stored: unknown): SearchChoice;
|
|
5
|
+
export declare function validMsaAlgorithm(stored: unknown): MsaAlgorithm;
|
|
6
|
+
export declare function useStoredSearchChoice(): readonly [SearchChoice, (choice: SearchChoice) => void];
|
|
7
|
+
export declare function useStoredMsaAlgorithm(): readonly ["clustalo" | "muscle" | "kalign" | "mafft" | "browser", (algorithm: MsaAlgorithm) => void];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useLocalStorage } from '../../../utils/useLocalStorage';
|
|
2
|
+
import { databaseOptionsFor, defaultSearchFor, msaAlgorithms, searchPrograms, } from './consts';
|
|
3
|
+
// The panel reopens on the last search run rather than on the defaults, the way
|
|
4
|
+
// the Orthologs tab remembers its source.
|
|
5
|
+
//
|
|
6
|
+
// The program and its database stay ONE stored value for the reason consts.ts
|
|
7
|
+
// keeps them one state: neither service knows the other's database names. What
|
|
8
|
+
// comes back is checked against the current options before it is used — an
|
|
9
|
+
// option this plugin has since dropped would otherwise reach EBI and come back
|
|
10
|
+
// a 400, minutes after the user pressed Submit.
|
|
11
|
+
export const SEARCH_CHOICE_STORAGE_KEY = 'msaView-blastSearch';
|
|
12
|
+
export const MSA_ALGORITHM_STORAGE_KEY = 'msaView-msaAlgorithm';
|
|
13
|
+
const defaultSearch = defaultSearchFor('blastp');
|
|
14
|
+
const defaultMsaAlgorithm = 'clustalo';
|
|
15
|
+
export function validSearchChoice(stored) {
|
|
16
|
+
const { program, database } = (stored ?? {});
|
|
17
|
+
const known = searchPrograms.find(p => p === program);
|
|
18
|
+
return known && databaseOptionsFor(known).some(d => d === database)
|
|
19
|
+
? { program: known, database }
|
|
20
|
+
: defaultSearch;
|
|
21
|
+
}
|
|
22
|
+
export function validMsaAlgorithm(stored) {
|
|
23
|
+
return msaAlgorithms.find(a => a === stored) ?? defaultMsaAlgorithm;
|
|
24
|
+
}
|
|
25
|
+
export function useStoredSearchChoice() {
|
|
26
|
+
const [stored, setStored] = useLocalStorage(SEARCH_CHOICE_STORAGE_KEY, defaultSearch);
|
|
27
|
+
return [
|
|
28
|
+
validSearchChoice(stored),
|
|
29
|
+
(choice) => {
|
|
30
|
+
setStored(choice);
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
export function useStoredMsaAlgorithm() {
|
|
35
|
+
const [stored, setStored] = useLocalStorage(MSA_ALGORITHM_STORAGE_KEY, defaultMsaAlgorithm);
|
|
36
|
+
return [
|
|
37
|
+
validMsaAlgorithm(stored),
|
|
38
|
+
(algorithm) => {
|
|
39
|
+
setStored(algorithm);
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
import { validMsaAlgorithm, validSearchChoice } from './searchChoiceStorage';
|
|
3
|
+
test('a stored pair both services recognise comes back as it went in', () => {
|
|
4
|
+
expect(validSearchChoice({ program: 'phmmer', database: 'swissprot' })).toEqual({
|
|
5
|
+
program: 'phmmer',
|
|
6
|
+
database: 'swissprot',
|
|
7
|
+
});
|
|
8
|
+
});
|
|
9
|
+
test('a database the stored program does not have falls back to the defaults', () => {
|
|
10
|
+
// uniprotkb_swissprot is blastp's name for it; sending it to phmmer is a 400
|
|
11
|
+
// from EBI minutes after Submit
|
|
12
|
+
expect(validSearchChoice({ program: 'phmmer', database: 'uniprotkb_swissprot' })).toEqual({ program: 'blastp', database: 'uniprotkb_swissprot' });
|
|
13
|
+
// uniprotkb_reference_proteomes shipped as a menu entry EBI rejects, and 3.0.0
|
|
14
|
+
// is old enough that someone's storage still holds it
|
|
15
|
+
expect(validSearchChoice({
|
|
16
|
+
program: 'blastp',
|
|
17
|
+
database: 'uniprotkb_reference_proteomes',
|
|
18
|
+
})).toEqual({ program: 'blastp', database: 'uniprotkb_swissprot' });
|
|
19
|
+
});
|
|
20
|
+
test('storage holding something else entirely falls back rather than throwing', () => {
|
|
21
|
+
for (const stored of [null, undefined, 'blastp', 42, {}, []]) {
|
|
22
|
+
expect(validSearchChoice(stored)).toEqual({
|
|
23
|
+
program: 'blastp',
|
|
24
|
+
database: 'uniprotkb_swissprot',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
test('an unknown algorithm falls back to clustalo', () => {
|
|
29
|
+
expect(validMsaAlgorithm('mafft')).toBe('mafft');
|
|
30
|
+
expect(validMsaAlgorithm('t_coffee')).toBe('clustalo');
|
|
31
|
+
expect(validMsaAlgorithm(null)).toBe('clustalo');
|
|
32
|
+
});
|
|
@@ -69,6 +69,7 @@ const ManualMSALoader = observer(function PreLoadedMSA2({ model, feature, handle
|
|
|
69
69
|
view,
|
|
70
70
|
feature: selectedTranscript,
|
|
71
71
|
querySeqName: queryRow.querySeqName,
|
|
72
|
+
querySeqOffset: queryRow.querySeqOffset,
|
|
72
73
|
...(inputMethod === 'file'
|
|
73
74
|
? {
|
|
74
75
|
msaFilehandle: msaFileLocation,
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { Feature, FileLocation } from '@jbrowse/core/util';
|
|
2
2
|
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view';
|
|
3
|
-
export declare function launchView({ newViewTitle, view, feature, msaFilehandle, treeFilehandle, querySeqName, data, }: {
|
|
3
|
+
export declare function launchView({ newViewTitle, view, feature, msaFilehandle, treeFilehandle, querySeqName, querySeqOffset, data, }: {
|
|
4
4
|
newViewTitle: string;
|
|
5
5
|
view: LinearGenomeViewModel;
|
|
6
6
|
feature: Feature;
|
|
7
7
|
msaFilehandle?: FileLocation;
|
|
8
8
|
treeFilehandle?: FileLocation;
|
|
9
9
|
querySeqName?: string;
|
|
10
|
+
/** transcript residues before the query row's first residue */
|
|
11
|
+
querySeqOffset?: number;
|
|
10
12
|
data?: {
|
|
11
13
|
msa: string;
|
|
12
14
|
tree?: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util';
|
|
2
2
|
import { launchMsaView } from '../../../utils/launchMsaView';
|
|
3
3
|
import { readLaunchPlacement } from '../../../utils/workspaces';
|
|
4
|
-
export function launchView({ newViewTitle, view, feature, msaFilehandle, treeFilehandle, querySeqName, data, }) {
|
|
4
|
+
export function launchView({ newViewTitle, view, feature, msaFilehandle, treeFilehandle, querySeqName, querySeqOffset, data, }) {
|
|
5
5
|
launchMsaView(getSession(view), {
|
|
6
6
|
placement: readLaunchPlacement(),
|
|
7
7
|
displayName: newViewTitle,
|
|
@@ -10,6 +10,7 @@ export function launchView({ newViewTitle, view, feature, msaFilehandle, treeFil
|
|
|
10
10
|
msaFilehandle,
|
|
11
11
|
treeFilehandle,
|
|
12
12
|
querySeqName,
|
|
13
|
+
querySeqOffset,
|
|
13
14
|
data,
|
|
14
15
|
});
|
|
15
16
|
}
|
|
@@ -7,6 +7,7 @@ import { defaultMaxSpecies } from '../../../utils/ncbiOrthologs';
|
|
|
7
7
|
import { useLocalStorage } from '../../../utils/useLocalStorage';
|
|
8
8
|
import { getGeneDisplayName, getGeneIdentifiers, getLinearGenomeView, getTranscriptDisplayName, } from '../../util';
|
|
9
9
|
import MsaAlgorithmSelect from '../BlastQuery/MsaAlgorithmSelect';
|
|
10
|
+
import { useStoredMsaAlgorithm } from '../BlastQuery/searchChoiceStorage';
|
|
10
11
|
import LaunchPanelContent from '../LaunchPanelContent';
|
|
11
12
|
import SubmitCancelActions from '../SubmitCancelActions';
|
|
12
13
|
import TranscriptSelector from '../TranscriptSelector';
|
|
@@ -19,13 +20,18 @@ const useStyles = makeStyles()({
|
|
|
19
20
|
width: 180,
|
|
20
21
|
},
|
|
21
22
|
});
|
|
23
|
+
const rowsHint = {
|
|
24
|
+
ncbi: 'the closest N species NCBI has',
|
|
25
|
+
panther: 'the closest N species PANTHER has',
|
|
26
|
+
uniref: 'one per species, reviewed entries first',
|
|
27
|
+
};
|
|
22
28
|
const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
|
|
23
29
|
const { classes } = useStyles();
|
|
24
30
|
const view = getLinearGenomeView(model);
|
|
25
31
|
const [launchViewError, setLaunchViewError] = useState();
|
|
26
32
|
const [taxId, setTaxId] = useState(9606);
|
|
27
33
|
const [source, setSource] = useLocalStorage(ORTHOLOG_SOURCE_STORAGE_KEY, 'ncbi');
|
|
28
|
-
const [msaAlgorithm, setMsaAlgorithm] =
|
|
34
|
+
const [msaAlgorithm, setMsaAlgorithm] = useStoredMsaAlgorithm();
|
|
29
35
|
const [maxSpecies, setMaxSpecies] = useState(String(defaultMaxSpecies));
|
|
30
36
|
const geneCandidates = useMemo(() => getGeneIdentifiers(feature), [feature]);
|
|
31
37
|
const transcriptSelection = useTranscriptSelection({ feature, view });
|
|
@@ -35,14 +41,14 @@ const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
|
|
|
35
41
|
const rowCountValid = Number.isInteger(rowCount) && rowCount >= 2;
|
|
36
42
|
return (React.createElement(React.Fragment, null,
|
|
37
43
|
React.createElement(LaunchPanelContent, { error: e },
|
|
38
|
-
React.createElement(Typography, { variant: "body2" }, "Precomputed orthologs, one gene per species, looked up rather than searched for. No BLAST job to queue."),
|
|
44
|
+
React.createElement(Typography, { variant: "body2" }, "Precomputed orthologs, one gene per species, looked up rather than searched for. No BLAST job to queue, and with the in-browser aligner no job at all."),
|
|
39
45
|
React.createElement("div", null,
|
|
40
46
|
React.createElement(OrthologSourceSelect, { className: classes.selectField, value: source, onChange: setSource }),
|
|
41
47
|
React.createElement(QuerySpeciesSelect, { className: classes.selectField, value: taxId, assemblyName: view.assemblyNames[0], onChange: setTaxId }),
|
|
42
48
|
React.createElement(MsaAlgorithmSelect, { className: classes.selectField, value: msaAlgorithm, onChange: setMsaAlgorithm }),
|
|
43
49
|
React.createElement(TextField2, { variant: "outlined", label: "Rows to align", className: classes.selectField, type: "number", value: maxSpecies, onChange: event => {
|
|
44
50
|
setMaxSpecies(event.target.value);
|
|
45
|
-
}, error: !rowCountValid, helperText:
|
|
51
|
+
}, error: !rowCountValid, helperText: rowsHint[source] })),
|
|
46
52
|
React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection })),
|
|
47
53
|
React.createElement(SubmitCancelActions, { model: model, submitDisabled: !proteinSequence || !rowCountValid, onSubmit: () => {
|
|
48
54
|
try {
|
|
@@ -58,7 +64,7 @@ const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
|
|
|
58
64
|
maxSpecies: rowCount,
|
|
59
65
|
geneCandidates,
|
|
60
66
|
msaAlgorithm,
|
|
61
|
-
selectedTranscript,
|
|
67
|
+
selectedTranscript: selectedTranscript.toJSON(),
|
|
62
68
|
proteinSequence,
|
|
63
69
|
},
|
|
64
70
|
});
|
|
@@ -5,13 +5,16 @@ export const ORTHOLOG_SOURCE_STORAGE_KEY = 'msaview-ortholog-source';
|
|
|
5
5
|
export const orthologSourceLabels = {
|
|
6
6
|
ncbi: 'NCBI orthologs',
|
|
7
7
|
panther: 'PANTHER',
|
|
8
|
+
uniref: 'UniRef cluster',
|
|
8
9
|
};
|
|
9
10
|
// Which species a source can answer for, in the words a reader picking one
|
|
10
11
|
// needs: NCBI's ortholog sets stop at vertebrates and insects, PANTHER's run
|
|
11
|
-
// from human to yeast and Arabidopsis
|
|
12
|
+
// from human to yeast and Arabidopsis, and a UniRef cluster is every UniProtKB
|
|
13
|
+
// entry within 50% identity of the query, whatever it came from.
|
|
12
14
|
const hints = {
|
|
13
15
|
ncbi: 'vertebrates and insects',
|
|
14
16
|
panther: 'also yeast, worm, fly and plants',
|
|
17
|
+
uniref: 'all of UniProtKB within 50% identity',
|
|
15
18
|
};
|
|
16
19
|
export default function OrthologSourceSelect({ value, onChange, className, }) {
|
|
17
20
|
return (React.createElement(TextField2, { variant: "outlined", label: "Source", className: className, select: true, value: value, helperText: hints[value], onChange: event => {
|
|
@@ -10,6 +10,19 @@ const useStyles = makeStyles()({
|
|
|
10
10
|
marginTop: 10,
|
|
11
11
|
},
|
|
12
12
|
});
|
|
13
|
+
/**
|
|
14
|
+
* Where a trimmed row sits in the protein. The offset is what the view maps
|
|
15
|
+
* through, so the alert says it rather than leaving the user to wonder why the
|
|
16
|
+
* row is shorter than their transcript.
|
|
17
|
+
*/
|
|
18
|
+
function partialCoverage({ identity, offset, }) {
|
|
19
|
+
const covering = `, covering ${Math.round(identity * 100)}% of it`;
|
|
20
|
+
return offset > 0
|
|
21
|
+
? `${covering} from residue ${offset + 1}`
|
|
22
|
+
: offset < 0
|
|
23
|
+
? `${covering} and running ${-offset} residues past its start`
|
|
24
|
+
: covering;
|
|
25
|
+
}
|
|
13
26
|
/**
|
|
14
27
|
* Which MSA row corresponds to the selected transcript. Clicking and hovering in
|
|
15
28
|
* the alignment reach the genome only through this name, and a wrong one fails
|
|
@@ -27,12 +40,15 @@ export default function QueryRowSelector({ names, detected, querySeqName, setQue
|
|
|
27
40
|
detected?.name === name ? ' — matches your protein' : ''))))) : (React.createElement(TextField2, { variant: "outlined", label: "MSA row matching the selected transcript", fullWidth: true, className: classes.field, helperText: "Paste an alignment above and this fills in on its own", value: querySeqName, onChange: event => {
|
|
28
41
|
setQuerySeqName(event.target.value);
|
|
29
42
|
} })),
|
|
30
|
-
isAutoDetected && detected ? (React.createElement(Alert, { severity: "
|
|
43
|
+
isAutoDetected && detected ? (detected.quality === 'similar' ? (React.createElement(Alert, { severity: "warning", className: classes.alert },
|
|
44
|
+
React.createElement("strong", null, detected.name),
|
|
45
|
+
" is",
|
|
46
|
+
' ',
|
|
47
|
+
Math.round(detected.identity * 100),
|
|
48
|
+
"% identical to your protein but is not the same sequence. Clicking the alignment navigates the genome view, approximately: wherever the two differ by an insertion or a deletion, every residue after it lands one codon off.")) : (React.createElement(Alert, { severity: "success", className: classes.alert },
|
|
31
49
|
"Matched ",
|
|
32
50
|
React.createElement("strong", null, detected.name),
|
|
33
51
|
" to your protein sequence",
|
|
34
|
-
detected.quality === 'exact'
|
|
35
|
-
|
|
36
|
-
: `, covering ${Math.round(detected.identity * 100)}% of it`,
|
|
37
|
-
". Clicking the alignment will navigate the genome view.")) : names.length > 0 && !querySeqName ? (React.createElement(Alert, { severity: "warning", className: classes.alert }, "No row matched your protein sequence \u2014 pick the one for your gene above. Without it the alignment still renders, but clicking it will not navigate the genome view.")) : null));
|
|
52
|
+
detected.quality === 'exact' ? '' : partialCoverage(detected),
|
|
53
|
+
". Clicking the alignment will navigate the genome view."))) : names.length > 0 && !querySeqName ? (React.createElement(Alert, { severity: "warning", className: classes.alert }, "No row matched your protein sequence \u2014 pick the one for your gene above. Without it the alignment still renders, but clicking it will not navigate the genome view.")) : null));
|
|
38
54
|
}
|
|
@@ -18,6 +18,17 @@ export interface QueryRowMatch {
|
|
|
18
18
|
quality: MatchQuality;
|
|
19
19
|
/** identity over the compared region, 0-1 */
|
|
20
20
|
identity: number;
|
|
21
|
+
/**
|
|
22
|
+
* Transcript residues before the row's first residue, so transcript position
|
|
23
|
+
* = row position + offset. BLAST reports the aligned region, so a row is
|
|
24
|
+
* often the query with its termini trimmed, and mapping its first residue to
|
|
25
|
+
* the transcript's first residue would put every navigation off by the
|
|
26
|
+
* trimmed amount. Negative when the row is the longer of the two.
|
|
27
|
+
*
|
|
28
|
+
* Zero for a `similar` match, which is the one arm this cannot measure: the
|
|
29
|
+
* row is not the query, so where it starts is a guess.
|
|
30
|
+
*/
|
|
31
|
+
offset: number;
|
|
21
32
|
}
|
|
22
33
|
export interface MsaQueryRow {
|
|
23
34
|
/** every row name, in file order, for the picker to offer */
|
|
@@ -70,20 +70,27 @@ function bestMatch(parsed, names, query) {
|
|
|
70
70
|
}
|
|
71
71
|
if (row === query) {
|
|
72
72
|
// nothing beats an exact match, and a second one would be a duplicate row
|
|
73
|
-
return { name, quality: 'exact', identity: 1 };
|
|
73
|
+
return { name, quality: 'exact', identity: 1, offset: 0 };
|
|
74
74
|
}
|
|
75
75
|
// BLAST reports the aligned region, so the row is often the query trimmed
|
|
76
76
|
// at one or both ends rather than the whole protein
|
|
77
|
-
|
|
77
|
+
const contained = query.indexOf(row);
|
|
78
|
+
const containing = row.indexOf(query);
|
|
79
|
+
if (contained !== -1 || containing !== -1) {
|
|
78
80
|
const coverage = Math.min(row.length, query.length) / Math.max(row.length, query.length);
|
|
79
81
|
if (coverage >= PARTIAL_COVERAGE_FLOOR) {
|
|
80
|
-
candidates.push({
|
|
82
|
+
candidates.push({
|
|
83
|
+
name,
|
|
84
|
+
quality: 'partial',
|
|
85
|
+
identity: coverage,
|
|
86
|
+
offset: contained === -1 ? -containing : contained,
|
|
87
|
+
});
|
|
81
88
|
}
|
|
82
89
|
continue;
|
|
83
90
|
}
|
|
84
91
|
const identity = identityOverOverlap(row, query);
|
|
85
92
|
if (identity >= SIMILARITY_FLOOR) {
|
|
86
|
-
candidates.push({ name, quality: 'similar', identity });
|
|
93
|
+
candidates.push({ name, quality: 'similar', identity, offset: 0 });
|
|
87
94
|
}
|
|
88
95
|
}
|
|
89
96
|
// an exact match returns above, so only these two can be here
|
|
@@ -33,6 +33,36 @@ describe('findQueryRow', () => {
|
|
|
33
33
|
quality: 'partial',
|
|
34
34
|
});
|
|
35
35
|
});
|
|
36
|
+
// the offset is what the view maps genome coordinates through: without it an
|
|
37
|
+
// N-terminally trimmed row puts every click five codons off, silently
|
|
38
|
+
test('says how far into the protein a trimmed row starts', () => {
|
|
39
|
+
const trimmed = `>aligned_query\n${protein.slice(5, 40)}\n>hit_one\nWRONGWRONGWRONGWRONG\n`;
|
|
40
|
+
expect(findQueryRow(trimmed, protein).match).toMatchObject({
|
|
41
|
+
name: 'aligned_query',
|
|
42
|
+
quality: 'partial',
|
|
43
|
+
offset: 5,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
test('an exact match starts where the protein does', () => {
|
|
47
|
+
expect(findQueryRow(clustal, protein).match?.offset).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
test('a row running past the protein gets a negative offset', () => {
|
|
50
|
+
const extended = `>longer_row\nMMM${protein}\n`;
|
|
51
|
+
expect(findQueryRow(extended, protein).match).toMatchObject({
|
|
52
|
+
name: 'longer_row',
|
|
53
|
+
quality: 'partial',
|
|
54
|
+
offset: -3,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
// the one arm with nothing to measure: a 90%-identical row is not the query,
|
|
58
|
+
// so QueryRowSelector warns rather than claiming a match
|
|
59
|
+
test('a merely similar row claims no offset', () => {
|
|
60
|
+
const similar = protein.slice(0, 30) + 'W' + protein.slice(31);
|
|
61
|
+
expect(findQueryRow(`>near\n${similar}\n`, protein).match).toMatchObject({
|
|
62
|
+
quality: 'similar',
|
|
63
|
+
offset: 0,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
36
66
|
// the failure that matters: silently wiring the view to a homolog would look
|
|
37
67
|
// like it worked, and every navigation afterwards would land in the wrong place
|
|
38
68
|
test('returns nothing when only diverged homologs are present', () => {
|
|
@@ -10,6 +10,7 @@ export declare function useQueryRowName(msaText: string, proteinSequence: string
|
|
|
10
10
|
detected: import("./detectQueryRow").QueryRowMatch | undefined;
|
|
11
11
|
names: string[];
|
|
12
12
|
querySeqName: string;
|
|
13
|
+
querySeqOffset: number;
|
|
13
14
|
setQuerySeqName: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
|
|
14
15
|
isAutoDetected: boolean;
|
|
15
16
|
};
|
|
@@ -13,10 +13,14 @@ export function useQueryRowName(msaText, proteinSequence) {
|
|
|
13
13
|
// parsing runs on every keystroke in the paste box otherwise, and an
|
|
14
14
|
// alignment of a few hundred rows is not free
|
|
15
15
|
const { names, match } = useMemo(() => findQueryRow(msaText, proteinSequence), [msaText, proteinSequence]);
|
|
16
|
+
const querySeqName = override ?? match?.name ?? '';
|
|
16
17
|
return {
|
|
17
18
|
detected: match,
|
|
18
19
|
names,
|
|
19
|
-
querySeqName
|
|
20
|
+
querySeqName,
|
|
21
|
+
// only the detected row's offset is known; a row the user picked instead is
|
|
22
|
+
// taken at face value, residue 1 to residue 1
|
|
23
|
+
querySeqOffset: querySeqName === match?.name ? match.offset : 0,
|
|
20
24
|
setQuerySeqName: setOverride,
|
|
21
25
|
isAutoDetected: override === undefined && !!match,
|
|
22
26
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|