jbrowse-plugin-msaview 2.7.2 → 2.7.4
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/AddHighlightModel/MsaToGenomeHighlight.js +6 -5
- package/dist/LaunchMsaView/components/LaunchMsaViewDialog.js +7 -1
- package/dist/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.js +1 -1
- package/dist/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.d.ts +1 -1
- package/dist/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.js +5 -2
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.d.ts +8 -0
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +86 -0
- package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.d.ts +9 -0
- package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.js +13 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +8 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.js +28 -0
- package/dist/MsaViewPanel/doLaunchBlast.js +10 -22
- package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +23 -0
- package/dist/MsaViewPanel/doLaunchOrthologs.js +97 -0
- package/dist/MsaViewPanel/genomeToMSA.js +13 -6
- package/dist/MsaViewPanel/genomeToMSA.test.js +35 -6
- package/dist/MsaViewPanel/model.d.ts +88 -70
- package/dist/MsaViewPanel/model.js +26 -13
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +32 -13
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +43 -14
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +108 -18
- package/dist/MsaViewPanel/msaDataStore.js +8 -17
- package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +8 -6
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +35 -31
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.d.ts +1 -2
- package/dist/utils/blastCache.js +9 -22
- package/dist/utils/domainCache.js +6 -15
- package/dist/utils/idb.d.ts +12 -0
- package/dist/utils/idb.js +21 -0
- package/dist/utils/ncbiOrthologs.d.ts +105 -0
- package/dist/utils/ncbiOrthologs.js +211 -0
- package/dist/utils/ncbiOrthologs.test.d.ts +1 -0
- package/dist/utils/ncbiOrthologs.test.js +41 -0
- package/dist/utils/taxonomyNames.js +13 -18
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +6 -8
- package/src/LaunchMsaView/components/LaunchMsaViewDialog.tsx +13 -2
- package/src/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.tsx +1 -1
- package/src/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.ts +4 -2
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +172 -0
- package/src/LaunchMsaView/components/OrthologQuery/orthologLaunchView.ts +28 -0
- package/src/MsaViewPanel/afterCreateAutoruns.ts +27 -0
- package/src/MsaViewPanel/doLaunchBlast.ts +20 -29
- package/src/MsaViewPanel/doLaunchOrthologs.ts +123 -0
- package/src/MsaViewPanel/genomeToMSA.test.ts +38 -6
- package/src/MsaViewPanel/genomeToMSA.ts +14 -6
- package/src/MsaViewPanel/model.ts +41 -12
- package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +117 -18
- package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +70 -26
- package/src/MsaViewPanel/msaDataStore.ts +17 -17
- package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +8 -6
- package/src/utils/blastCache.ts +20 -23
- package/src/utils/domainCache.ts +14 -18
- package/src/utils/idb.ts +28 -0
- package/src/utils/ncbiOrthologs.test.ts +56 -0
- package/src/utils/ncbiOrthologs.ts +320 -0
- package/src/utils/taxonomyNames.ts +22 -24
- package/src/version.ts +1 -1
- package/dist/MsaViewPanel/blosum62.d.ts +0 -2
- package/dist/MsaViewPanel/blosum62.js +0 -627
- package/src/MsaViewPanel/blosum62.ts +0 -628
|
@@ -11,11 +11,12 @@ const MsaToGenomeHighlight = observer(function MsaToGenomeHighlight2({ model, })
|
|
|
11
11
|
// The persistent click selection always shows. The hover codon is suppressed
|
|
12
12
|
// while hovering the LGV — GenomeMouseoverHighlight handles the single-bp
|
|
13
13
|
// display in that case, so we don't stack a wider codon band on top of it.
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
const highlights = [
|
|
15
|
+
...(msaView?.connectedClickHighlights ?? []),
|
|
16
|
+
...(hasHoverPosition(hovered)
|
|
17
|
+
? []
|
|
18
|
+
: (msaView?.connectedHoverHighlights ?? [])),
|
|
19
|
+
];
|
|
19
20
|
return highlights.length ? (React.createElement(MsaToGenomeHighlightRenderer, { model: model, highlights: highlights })) : null;
|
|
20
21
|
});
|
|
21
22
|
// Inner component: handles the scroll-dependent rendering
|
|
@@ -4,6 +4,7 @@ import { getSession } from '@jbrowse/core/util';
|
|
|
4
4
|
import { Tab, Tabs } from '@mui/material';
|
|
5
5
|
import ManualMSALoader from './ManualMSALoader/ManualMSALoader';
|
|
6
6
|
import NCBIBlastPanel from './NCBIBlastQuery/NCBIBlastPanel';
|
|
7
|
+
import OrthologPanel from './OrthologQuery/OrthologPanel';
|
|
7
8
|
import PreLoadedMSA from './PreLoadedMSA/PreLoadedMSADataPanel';
|
|
8
9
|
import { readMsaDatasets } from './PreLoadedMSA/types';
|
|
9
10
|
import TabPanel from './TabPanel';
|
|
@@ -11,14 +12,19 @@ export default function LaunchMsaViewDialog({ handleClose, feature, model, }) {
|
|
|
11
12
|
const session = getSession(model);
|
|
12
13
|
const datasets = readMsaDatasets(session.jbrowse);
|
|
13
14
|
const hasPreloadedDatasets = !!datasets?.length;
|
|
14
|
-
|
|
15
|
+
// orthologs first, and the default: it answers the same question in ~10s
|
|
16
|
+
// that BLAST takes 10+ minutes to answer worse (see utils/ncbiOrthologs.ts)
|
|
17
|
+
const [value, setValue] = useState('orthologs');
|
|
15
18
|
return (React.createElement(Dialog, { maxWidth: "xl", title: "Launch MSA view", open: true, onClose: handleClose },
|
|
16
19
|
React.createElement(Tabs, { value: value, onChange: (_event, newValue) => {
|
|
17
20
|
setValue(newValue);
|
|
18
21
|
} },
|
|
22
|
+
React.createElement(Tab, { label: "Orthologs (fast)", value: "orthologs" }),
|
|
19
23
|
React.createElement(Tab, { label: "NCBI BLAST query", value: "ncbi_blast" }),
|
|
20
24
|
hasPreloadedDatasets ? (React.createElement(Tab, { label: "Pre-loaded MSA datasets", value: "preloaded_msa" })) : null,
|
|
21
25
|
React.createElement(Tab, { label: "Manual upload", value: "manual_msa" })),
|
|
26
|
+
React.createElement(TabPanel, { value: value, index: "orthologs" },
|
|
27
|
+
React.createElement(OrthologPanel, { handleClose: handleClose, feature: feature, model: model })),
|
|
22
28
|
React.createElement(TabPanel, { value: value, index: "ncbi_blast" },
|
|
23
29
|
React.createElement(NCBIBlastPanel, { handleClose: handleClose, feature: feature, model: model })),
|
|
24
30
|
hasPreloadedDatasets ? (React.createElement(TabPanel, { value: value, index: "preloaded_msa" },
|
|
@@ -68,7 +68,7 @@ const CachedBlastResults = observer(function ({ model, handleClose, feature, })
|
|
|
68
68
|
catch (e) {
|
|
69
69
|
setOperationError(e);
|
|
70
70
|
}
|
|
71
|
-
} }, "Clear
|
|
71
|
+
} }, "Clear results for this gene")),
|
|
72
72
|
React.createElement(List, { dense: true, className: classes.resultList }, results.map(result => (React.createElement(ListItem, { key: result.id, disablePadding: true, secondaryAction: React.createElement(IconButton, { edge: "end", size: "small", onClick: async (e) => {
|
|
73
73
|
e.stopPropagation();
|
|
74
74
|
try {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import useSWR from 'swr';
|
|
2
|
-
import {
|
|
2
|
+
import { deleteCachedResult, getAllCachedResults, } from '../../../utils/blastCache';
|
|
3
3
|
import { staticSwrConfig } from '../../../utils/swrConfig';
|
|
4
4
|
export function useCachedBlastResults(geneIds) {
|
|
5
5
|
const { data: results, error, isLoading, mutate, } = useSWR(`cached-blast-${geneIds.join(',')}`, async () => {
|
|
@@ -10,8 +10,11 @@ export function useCachedBlastResults(geneIds) {
|
|
|
10
10
|
await deleteCachedResult(id);
|
|
11
11
|
await mutate(results => results?.filter(result => result.id !== id) ?? [], false);
|
|
12
12
|
};
|
|
13
|
+
// deletes only what this hook listed, i.e. the results for these gene ids.
|
|
14
|
+
// The list the user is looking at is gene-scoped, so a store-wide clear here
|
|
15
|
+
// would silently throw away every other gene's cached alignments too
|
|
13
16
|
const handleClearAll = async () => {
|
|
14
|
-
await
|
|
17
|
+
await Promise.all((results ?? []).map(r => deleteCachedResult(r.id)));
|
|
15
18
|
await mutate([], false);
|
|
16
19
|
};
|
|
17
20
|
return {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AbstractTrackModel, Feature } from '@jbrowse/core/util';
|
|
3
|
+
declare const OrthologPanel: ({ handleClose, feature, model, }: {
|
|
4
|
+
model: AbstractTrackModel;
|
|
5
|
+
feature: Feature;
|
|
6
|
+
handleClose: () => void;
|
|
7
|
+
}) => React.JSX.Element;
|
|
8
|
+
export default OrthologPanel;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import { Checkbox, FormControlLabel, MenuItem, Typography } from '@mui/material';
|
|
3
|
+
import { observer } from 'mobx-react';
|
|
4
|
+
import { makeStyles } from 'tss-react/mui';
|
|
5
|
+
import { orthologLaunchView } from './orthologLaunchView';
|
|
6
|
+
import TextField2 from '../../../components/TextField2';
|
|
7
|
+
import { COMMON_SPECIES } from '../../../utils/ncbiOrthologs';
|
|
8
|
+
import { getGeneDisplayName, getGeneIdentifiers, getLinearGenomeView, getTranscriptDisplayName, } from '../../util';
|
|
9
|
+
import LaunchPanelContent from '../LaunchPanelContent';
|
|
10
|
+
import MsaAlgorithmSelect from '../NCBIBlastQuery/MsaAlgorithmSelect';
|
|
11
|
+
import SubmitCancelActions from '../SubmitCancelActions';
|
|
12
|
+
import TranscriptSelector from '../TranscriptSelector';
|
|
13
|
+
import { useTranscriptSelection } from '../useTranscriptSelection';
|
|
14
|
+
const useStyles = makeStyles()({
|
|
15
|
+
selectField: {
|
|
16
|
+
width: 180,
|
|
17
|
+
},
|
|
18
|
+
speciesBox: {
|
|
19
|
+
display: 'flex',
|
|
20
|
+
flexWrap: 'wrap',
|
|
21
|
+
maxWidth: 560,
|
|
22
|
+
marginTop: 12,
|
|
23
|
+
},
|
|
24
|
+
species: {
|
|
25
|
+
width: 160,
|
|
26
|
+
},
|
|
27
|
+
infoText: {
|
|
28
|
+
marginTop: 20,
|
|
29
|
+
maxWidth: 620,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
|
|
33
|
+
const { classes } = useStyles();
|
|
34
|
+
const view = getLinearGenomeView(model);
|
|
35
|
+
const [launchViewError, setLaunchViewError] = useState();
|
|
36
|
+
const [taxId, setTaxId] = useState(9606);
|
|
37
|
+
const [msaAlgorithm, setMsaAlgorithm] = useState('clustalo');
|
|
38
|
+
const [excluded, setExcluded] = useState([]);
|
|
39
|
+
const geneCandidates = useMemo(() => getGeneIdentifiers(feature), [feature]);
|
|
40
|
+
const transcriptSelection = useTranscriptSelection({ feature, view });
|
|
41
|
+
const { selectedTranscript, proteinSequence } = transcriptSelection;
|
|
42
|
+
const e = transcriptSelection.error ?? launchViewError;
|
|
43
|
+
const taxa = COMMON_SPECIES.map(s => s.taxId).filter(t => !excluded.includes(t));
|
|
44
|
+
return (React.createElement(React.Fragment, null,
|
|
45
|
+
React.createElement(LaunchPanelContent, { error: e },
|
|
46
|
+
React.createElement(Typography, null, "Builds the alignment from NCBI's precomputed orthologs \u2014 one gene per species \u2014 instead of searching. There is no job to wait on: the NCBI lookups take about a second, and only the multiple alignment at EBI costs real time (~10s), against 10+ minutes for BLAST. Rows come out labelled by species rather than by accession, and NCBI's CDD domains are overlaid automatically."),
|
|
47
|
+
React.createElement("div", null,
|
|
48
|
+
React.createElement(TextField2, { variant: "outlined", label: "Query species", className: classes.selectField, select: true, value: taxId, onChange: event => {
|
|
49
|
+
setTaxId(Number(event.target.value));
|
|
50
|
+
}, helperText: "the species this gene is from" }, COMMON_SPECIES.map(s => (React.createElement(MenuItem, { value: s.taxId, key: s.taxId }, s.label)))),
|
|
51
|
+
React.createElement(MsaAlgorithmSelect, { className: classes.selectField, value: msaAlgorithm, onChange: setMsaAlgorithm })),
|
|
52
|
+
React.createElement(Typography, { variant: "subtitle2", style: { marginTop: 12 } }, "Species to include (those without an ortholog are skipped)"),
|
|
53
|
+
React.createElement("div", { className: classes.speciesBox }, COMMON_SPECIES.map(s => (React.createElement(FormControlLabel, { className: classes.species, key: s.taxId, control: React.createElement(Checkbox, { checked: !excluded.includes(s.taxId), onChange: event => {
|
|
54
|
+
setExcluded(event.target.checked
|
|
55
|
+
? excluded.filter(t => t !== s.taxId)
|
|
56
|
+
: [...excluded, s.taxId]);
|
|
57
|
+
} }), label: s.label })))),
|
|
58
|
+
React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection }),
|
|
59
|
+
React.createElement(Typography, { className: classes.infoText, variant: "body2" }, "The query row is the transcript selected above, not NCBI's representative protein, so the alignment stays linked to the genome view at codon resolution.")),
|
|
60
|
+
React.createElement(SubmitCancelActions, { submitDisabled: !proteinSequence || taxa.length < 2, onSubmit: () => {
|
|
61
|
+
try {
|
|
62
|
+
if (selectedTranscript) {
|
|
63
|
+
setLaunchViewError(undefined);
|
|
64
|
+
orthologLaunchView({
|
|
65
|
+
feature: selectedTranscript,
|
|
66
|
+
view,
|
|
67
|
+
newViewTitle: `Orthologs - ${getGeneDisplayName(feature)} - ${getTranscriptDisplayName(selectedTranscript)}`,
|
|
68
|
+
orthologParams: {
|
|
69
|
+
taxId,
|
|
70
|
+
taxa,
|
|
71
|
+
geneCandidates,
|
|
72
|
+
msaAlgorithm,
|
|
73
|
+
selectedTranscript,
|
|
74
|
+
proteinSequence,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
handleClose();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
console.error(e);
|
|
82
|
+
setLaunchViewError(e);
|
|
83
|
+
}
|
|
84
|
+
}, onCancel: handleClose })));
|
|
85
|
+
});
|
|
86
|
+
export default OrthologPanel;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { OrthologParams } from '../../../MsaViewPanel/model';
|
|
2
|
+
import type { Feature } from '@jbrowse/core/util';
|
|
3
|
+
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view';
|
|
4
|
+
export declare function orthologLaunchView({ newViewTitle, view, feature, orthologParams, }: {
|
|
5
|
+
newViewTitle: string;
|
|
6
|
+
view: LinearGenomeViewModel;
|
|
7
|
+
feature: Feature;
|
|
8
|
+
orthologParams: OrthologParams;
|
|
9
|
+
}): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getSession } from '@jbrowse/core/util';
|
|
2
|
+
export function orthologLaunchView({ newViewTitle, view, feature, orthologParams, }) {
|
|
3
|
+
getSession(view).addView('MsaView', {
|
|
4
|
+
type: 'MsaView',
|
|
5
|
+
displayName: newViewTitle,
|
|
6
|
+
connectedViewId: view.id,
|
|
7
|
+
connectedFeature: feature.toJSON(),
|
|
8
|
+
drawNodeBubbles: true,
|
|
9
|
+
colWidth: 10,
|
|
10
|
+
rowHeight: 12,
|
|
11
|
+
orthologParams,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { JBrowsePluginMsaViewModel } from './model';
|
|
2
2
|
export declare function loadStoredData(self: JBrowsePluginMsaViewModel): void;
|
|
3
3
|
export declare function storeDataToIndexedDB(self: JBrowsePluginMsaViewModel): void;
|
|
4
|
+
/**
|
|
5
|
+
* Same shape as launchBlastIfNeeded, for the ortholog path: the params ARE the
|
|
6
|
+
* request, and clearing them on success is what marks it done. They are left in
|
|
7
|
+
* place on failure so the error stays attributable to a specific request; the
|
|
8
|
+
* autorun's only tracked read is orthologParams itself, so nothing refires
|
|
9
|
+
* until a new request replaces them.
|
|
10
|
+
*/
|
|
11
|
+
export declare function launchOrthologsIfNeeded(self: JBrowsePluginMsaViewModel): void;
|
|
4
12
|
export declare function launchBlastIfNeeded(self: JBrowsePluginMsaViewModel): void;
|
|
5
13
|
/**
|
|
6
14
|
* Once an accession-bearing alignment is present (fresh from BLAST or restored
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util';
|
|
2
2
|
import { doLaunchBlast } from './doLaunchBlast';
|
|
3
|
+
import { doLaunchOrthologs } from './doLaunchOrthologs';
|
|
3
4
|
import { fetchIndexedMsa } from './fetchIndexedMsa';
|
|
4
5
|
import { genomeToMSA } from './genomeToMSA';
|
|
5
6
|
import { loadProteinDomains } from './loadProteinDomains';
|
|
@@ -69,6 +70,33 @@ export function storeDataToIndexedDB(self) {
|
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Same shape as launchBlastIfNeeded, for the ortholog path: the params ARE the
|
|
75
|
+
* request, and clearing them on success is what marks it done. They are left in
|
|
76
|
+
* place on failure so the error stays attributable to a specific request; the
|
|
77
|
+
* autorun's only tracked read is orthologParams itself, so nothing refires
|
|
78
|
+
* until a new request replaces them.
|
|
79
|
+
*/
|
|
80
|
+
export function launchOrthologsIfNeeded(self) {
|
|
81
|
+
if (self.orthologParams) {
|
|
82
|
+
void (async () => {
|
|
83
|
+
try {
|
|
84
|
+
self.setProgress('Resolving orthologs');
|
|
85
|
+
self.setError(undefined);
|
|
86
|
+
const data = await doLaunchOrthologs({ self });
|
|
87
|
+
self.setData(data);
|
|
88
|
+
self.setOrthologParams(undefined);
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
self.setError(e);
|
|
92
|
+
console.error(e);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
self.setProgress('');
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
72
100
|
export function launchBlastIfNeeded(self) {
|
|
73
101
|
if (self.blastParams) {
|
|
74
102
|
void (async () => {
|
|
@@ -7,36 +7,26 @@ import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
|
7
7
|
export async function doLaunchBlast({ self, }) {
|
|
8
8
|
const { baseUrl, blastDatabase, blastProgram, msaAlgorithm, proteinSequence, selectedTranscript, rid: existingRid, } = self.blastParams;
|
|
9
9
|
const cleanedSeq = cleanProteinSequence(proteinSequence);
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const onProgress = (arg) => {
|
|
11
|
+
self.setProgress(arg);
|
|
12
|
+
};
|
|
12
13
|
if (existingRid) {
|
|
14
|
+
// publish it before the first poll so the view can link out to NCBI while
|
|
15
|
+
// the job is still running
|
|
13
16
|
self.setRid(existingRid);
|
|
14
|
-
const result = await queryBlastFromRid({
|
|
15
|
-
rid: existingRid,
|
|
16
|
-
baseUrl,
|
|
17
|
-
onProgress: arg => {
|
|
18
|
-
self.setProgress(arg);
|
|
19
|
-
},
|
|
20
|
-
});
|
|
21
|
-
hits = result.hits;
|
|
22
|
-
rid = result.rid;
|
|
23
17
|
}
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
const { hits, rid } = existingRid
|
|
19
|
+
? await queryBlastFromRid({ rid: existingRid, baseUrl, onProgress })
|
|
20
|
+
: await queryBlast({
|
|
26
21
|
query: cleanedSeq,
|
|
27
22
|
blastDatabase,
|
|
28
23
|
blastProgram,
|
|
29
24
|
baseUrl,
|
|
30
|
-
onProgress
|
|
31
|
-
self.setProgress(arg);
|
|
32
|
-
},
|
|
25
|
+
onProgress,
|
|
33
26
|
onRid: r => {
|
|
34
27
|
self.setRid(r);
|
|
35
28
|
},
|
|
36
29
|
});
|
|
37
|
-
hits = result.hits;
|
|
38
|
-
rid = result.rid;
|
|
39
|
-
}
|
|
40
30
|
self.setProgress('Fetching species taxonomy info...');
|
|
41
31
|
const taxids = hits
|
|
42
32
|
.map(h => h.description[0]?.taxid)
|
|
@@ -57,9 +47,7 @@ export async function doLaunchBlast({ self, }) {
|
|
|
57
47
|
const result = await launchMSA({
|
|
58
48
|
algorithm: msaAlgorithm,
|
|
59
49
|
sequence: [`>QUERY\n${cleanedSeq}`, ...sequences].join('\n'),
|
|
60
|
-
onProgress
|
|
61
|
-
self.setProgress(arg);
|
|
62
|
-
},
|
|
50
|
+
onProgress,
|
|
63
51
|
});
|
|
64
52
|
const treeMetadataJson = JSON.stringify(treeMetadata);
|
|
65
53
|
await saveBlastResult({
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { JBrowsePluginMsaViewModel } from './model';
|
|
2
|
+
/**
|
|
3
|
+
* The no-search-job alternative to doLaunchBlast.
|
|
4
|
+
*
|
|
5
|
+
* BLAST spends 10+ minutes answering "what looks like this sequence" and
|
|
6
|
+
* returns a redundant, accession-labelled hit list. This asks NCBI the question
|
|
7
|
+
* the alignment actually wants — "what is this gene's ortholog in each species"
|
|
8
|
+
* — which NCBI has already computed, so the whole NCBI half returns in about a
|
|
9
|
+
* second and only the EBI alignment (~10s) costs real time.
|
|
10
|
+
*
|
|
11
|
+
* The query row is the user's OWN selected transcript, not NCBI's
|
|
12
|
+
* representative protein for the query species, because `connectedFeature`
|
|
13
|
+
* maps genome coordinates through that row — swapping in a different isoform
|
|
14
|
+
* would silently break the genome<->MSA linkage. The query species is therefore
|
|
15
|
+
* excluded from the ortholog set rather than appearing twice.
|
|
16
|
+
*/
|
|
17
|
+
export declare function doLaunchOrthologs({ self, }: {
|
|
18
|
+
self: JBrowsePluginMsaViewModel;
|
|
19
|
+
}): Promise<{
|
|
20
|
+
treeMetadata: string;
|
|
21
|
+
msa: string;
|
|
22
|
+
tree: string;
|
|
23
|
+
}>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { cleanProteinSequence } from '../LaunchMsaView/util';
|
|
2
|
+
import { launchMSA } from '../utils/msa';
|
|
3
|
+
import { fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils/ncbiOrthologs';
|
|
4
|
+
/**
|
|
5
|
+
* The no-search-job alternative to doLaunchBlast.
|
|
6
|
+
*
|
|
7
|
+
* BLAST spends 10+ minutes answering "what looks like this sequence" and
|
|
8
|
+
* returns a redundant, accession-labelled hit list. This asks NCBI the question
|
|
9
|
+
* the alignment actually wants — "what is this gene's ortholog in each species"
|
|
10
|
+
* — which NCBI has already computed, so the whole NCBI half returns in about a
|
|
11
|
+
* second and only the EBI alignment (~10s) costs real time.
|
|
12
|
+
*
|
|
13
|
+
* The query row is the user's OWN selected transcript, not NCBI's
|
|
14
|
+
* representative protein for the query species, because `connectedFeature`
|
|
15
|
+
* maps genome coordinates through that row — swapping in a different isoform
|
|
16
|
+
* would silently break the genome<->MSA linkage. The query species is therefore
|
|
17
|
+
* excluded from the ortholog set rather than appearing twice.
|
|
18
|
+
*/
|
|
19
|
+
export async function doLaunchOrthologs({ self, }) {
|
|
20
|
+
const { taxId, taxa, geneCandidates, msaAlgorithm, proteinSequence } = self.orthologParams;
|
|
21
|
+
const cleanedSeq = cleanProteinSequence(proteinSequence);
|
|
22
|
+
const onProgress = (arg) => {
|
|
23
|
+
self.setProgress(arg);
|
|
24
|
+
};
|
|
25
|
+
onProgress('Resolving gene at NCBI...');
|
|
26
|
+
const resolved = await resolveGeneId(geneCandidates, taxId);
|
|
27
|
+
if (!resolved) {
|
|
28
|
+
throw new Error(`Could not resolve any of ${geneCandidates.join(', ')} to an NCBI gene in taxon ${taxId}. Try the NCBI BLAST tab, which needs no gene identifier.`);
|
|
29
|
+
}
|
|
30
|
+
// the query species is represented by the user's own transcript below
|
|
31
|
+
const wanted = new Set(taxa.filter(t => t !== taxId));
|
|
32
|
+
const rows = await fetchOrthologRows({
|
|
33
|
+
geneId: resolved.geneId,
|
|
34
|
+
taxa: wanted,
|
|
35
|
+
onProgress,
|
|
36
|
+
});
|
|
37
|
+
const treeMetadata = {
|
|
38
|
+
QUERY: await buildQueryMetadata(self, resolved.geneId, cleanedSeq),
|
|
39
|
+
};
|
|
40
|
+
for (const row of rows) {
|
|
41
|
+
treeMetadata[row.label] = buildRowMetadata(row);
|
|
42
|
+
}
|
|
43
|
+
const result = await launchMSA({
|
|
44
|
+
algorithm: msaAlgorithm,
|
|
45
|
+
sequence: [
|
|
46
|
+
`>QUERY\n${cleanedSeq}`,
|
|
47
|
+
...rows.map(r => `>${r.label}\n${r.sequence}`),
|
|
48
|
+
].join('\n'),
|
|
49
|
+
onProgress,
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
...result,
|
|
53
|
+
treeMetadata: JSON.stringify(treeMetadata),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The query row is the user's own translated transcript, so it carries an
|
|
58
|
+
* Accession — which is what drives the automatic CDD overlay
|
|
59
|
+
* (afterCreateAutoruns.autoLoadProteinDomains -> loadProteinDomains) — ONLY
|
|
60
|
+
* when its sequence is byte-identical to the RefSeq protein that accession
|
|
61
|
+
* names. Attaching it unconditionally would put every domain box at an offset
|
|
62
|
+
* whenever the user picked a non-representative isoform, which is a silently
|
|
63
|
+
* wrong figure rather than a missing one.
|
|
64
|
+
*/
|
|
65
|
+
async function buildQueryMetadata(self, geneId, proteinSequence) {
|
|
66
|
+
const transcript = self.orthologParams?.selectedTranscript;
|
|
67
|
+
const metadata = { 'Gene ID': geneId };
|
|
68
|
+
const name = transcript?.get('name') ?? transcript?.get('id');
|
|
69
|
+
if (name) {
|
|
70
|
+
metadata.Transcript = name;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const representative = await fetchProteinForGene(geneId);
|
|
74
|
+
if (representative?.sequence === proteinSequence) {
|
|
75
|
+
metadata.Accession = representative.accession;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
// a failed lookup only costs the query row its domain overlay, so it must
|
|
80
|
+
// not take down an alignment that is otherwise complete
|
|
81
|
+
console.warn('[msaview-orthologs] query protein lookup failed:', e);
|
|
82
|
+
}
|
|
83
|
+
return metadata;
|
|
84
|
+
}
|
|
85
|
+
function buildRowMetadata(row) {
|
|
86
|
+
const metadata = {
|
|
87
|
+
'Scientific name': row.scientificName,
|
|
88
|
+
// Accession drives the automatic CDD domain overlay
|
|
89
|
+
// (afterCreateAutoruns.autoLoadProteinDomains -> loadProteinDomains)
|
|
90
|
+
Accession: row.protein,
|
|
91
|
+
'Gene ID': row.geneId,
|
|
92
|
+
};
|
|
93
|
+
if (row.commonName) {
|
|
94
|
+
metadata['Common name'] = row.commonName;
|
|
95
|
+
}
|
|
96
|
+
return metadata;
|
|
97
|
+
}
|
|
@@ -6,18 +6,25 @@ export function genomeToMSA({ model }) {
|
|
|
6
6
|
if (!connectedView?.initialized || !hasHoverPosition(hovered)) {
|
|
7
7
|
return undefined;
|
|
8
8
|
}
|
|
9
|
-
const { coord
|
|
9
|
+
const { coord, refName } = hovered.hoverPosition;
|
|
10
|
+
// hoverPosition.coord is a 1-based display coordinate (core's pxToBp adds the
|
|
11
|
+
// +1), while g2p and mafRegion are keyed by 0-based genome position
|
|
12
|
+
const genomePos = coord - 1;
|
|
10
13
|
if (mafRegion) {
|
|
11
14
|
if (refName !== mafRegion.refName ||
|
|
12
15
|
!connectedView.assemblyNames.includes(mafRegion.assemblyName) ||
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
genomePos < mafRegion.start ||
|
|
17
|
+
genomePos >= mafRegion.end) {
|
|
15
18
|
return undefined;
|
|
16
19
|
}
|
|
17
|
-
return model.seqPosToVisibleCol(querySeqName,
|
|
20
|
+
return model.seqPosToVisibleCol(querySeqName, genomePos - mafRegion.start);
|
|
18
21
|
}
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
// session.hovered is global -- set by whichever LinearGenomeView the cursor
|
|
23
|
+
// was last over, on any assembly -- so the refName gate is load bearing:
|
|
24
|
+
// without it the same numeric coordinate on an unrelated chromosome matches a
|
|
25
|
+
// g2p key and lights up a column for a different locus
|
|
26
|
+
if (refName === transcriptToMsaMap?.refName) {
|
|
27
|
+
const seqPos = transcriptToMsaMap.g2p[genomePos];
|
|
21
28
|
if (seqPos !== undefined) {
|
|
22
29
|
return model.seqPosToVisibleCol(querySeqName, seqPos);
|
|
23
30
|
}
|
|
@@ -76,8 +76,9 @@ describe('genomeToMSA', () => {
|
|
|
76
76
|
seqPosToVisibleCol: mockSeqPosToVisibleCol,
|
|
77
77
|
};
|
|
78
78
|
const result = genomeToMSA({ model });
|
|
79
|
-
// coord 1005 -
|
|
80
|
-
|
|
79
|
+
// hover coord 1005 is 1-based, so the 0-based genome position is 1004,
|
|
80
|
+
// which is ungapped position 4 of a region starting at 1000
|
|
81
|
+
expect(mockSeqPosToVisibleCol).toHaveBeenCalledWith('hg38.chr1', 4);
|
|
81
82
|
expect(result).toBe(5);
|
|
82
83
|
});
|
|
83
84
|
test('returns undefined when hover refName does not match mafRegion', () => {
|
|
@@ -106,10 +107,11 @@ describe('genomeToMSA', () => {
|
|
|
106
107
|
expect(result).toBeUndefined();
|
|
107
108
|
});
|
|
108
109
|
test('returns undefined when hover coord is before mafRegion start', () => {
|
|
110
|
+
// 1-based coord 1000 is the 0-based base 999, one before the region
|
|
109
111
|
mockGetSession.mockReturnValue({
|
|
110
112
|
hovered: {
|
|
111
113
|
hoverFeature: {},
|
|
112
|
-
hoverPosition: { coord:
|
|
114
|
+
hoverPosition: { coord: 1000, refName: 'chr1' },
|
|
113
115
|
},
|
|
114
116
|
});
|
|
115
117
|
const model = {
|
|
@@ -131,10 +133,11 @@ describe('genomeToMSA', () => {
|
|
|
131
133
|
expect(result).toBeUndefined();
|
|
132
134
|
});
|
|
133
135
|
test('returns undefined when hover coord is at or after mafRegion end', () => {
|
|
136
|
+
// 1-based coord 1011 is the 0-based base 1010, one past the region
|
|
134
137
|
mockGetSession.mockReturnValue({
|
|
135
138
|
hovered: {
|
|
136
139
|
hoverFeature: {},
|
|
137
|
-
hoverPosition: { coord:
|
|
140
|
+
hoverPosition: { coord: 1011, refName: 'chr1' },
|
|
138
141
|
},
|
|
139
142
|
});
|
|
140
143
|
const model = {
|
|
@@ -193,7 +196,9 @@ describe('genomeToMSA', () => {
|
|
|
193
196
|
const model = {
|
|
194
197
|
querySeqName: 'QUERY',
|
|
195
198
|
transcriptToMsaMap: {
|
|
196
|
-
|
|
199
|
+
refName: 'chr1',
|
|
200
|
+
// g2p is keyed by 0-based genome position, the hover coord is 1-based
|
|
201
|
+
g2p: { 1004: 10 },
|
|
197
202
|
},
|
|
198
203
|
mafRegion: undefined,
|
|
199
204
|
connectedView: { initialized: true },
|
|
@@ -203,6 +208,29 @@ describe('genomeToMSA', () => {
|
|
|
203
208
|
expect(mockSeqPosToVisibleCol).toHaveBeenCalledWith('QUERY', 10);
|
|
204
209
|
expect(result).toBe(10);
|
|
205
210
|
});
|
|
211
|
+
test('returns undefined when the hover is on another refName', () => {
|
|
212
|
+
// session.hovered is global, so a hover on an unrelated chromosome can
|
|
213
|
+
// carry a coordinate that happens to be a g2p key
|
|
214
|
+
mockGetSession.mockReturnValue({
|
|
215
|
+
hovered: {
|
|
216
|
+
hoverFeature: {},
|
|
217
|
+
hoverPosition: { coord: 1005, refName: 'chr2' },
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
const mockSeqPosToVisibleCol = vi.fn();
|
|
221
|
+
const model = {
|
|
222
|
+
querySeqName: 'QUERY',
|
|
223
|
+
transcriptToMsaMap: {
|
|
224
|
+
refName: 'chr1',
|
|
225
|
+
g2p: { 1004: 10 },
|
|
226
|
+
},
|
|
227
|
+
mafRegion: undefined,
|
|
228
|
+
connectedView: { initialized: true },
|
|
229
|
+
seqPosToVisibleCol: mockSeqPosToVisibleCol,
|
|
230
|
+
};
|
|
231
|
+
expect(genomeToMSA({ model })).toBeUndefined();
|
|
232
|
+
expect(mockSeqPosToVisibleCol).not.toHaveBeenCalled();
|
|
233
|
+
});
|
|
206
234
|
test('returns undefined when g2p has no mapping for coord', () => {
|
|
207
235
|
mockGetSession.mockReturnValue({
|
|
208
236
|
hovered: {
|
|
@@ -213,7 +241,8 @@ describe('genomeToMSA', () => {
|
|
|
213
241
|
const model = {
|
|
214
242
|
querySeqName: 'QUERY',
|
|
215
243
|
transcriptToMsaMap: {
|
|
216
|
-
|
|
244
|
+
refName: 'chr1',
|
|
245
|
+
g2p: { 1000: 0 }, // No entry for 1004
|
|
217
246
|
},
|
|
218
247
|
mafRegion: undefined,
|
|
219
248
|
connectedView: { initialized: true },
|