jbrowse-plugin-msaview 3.2.0 → 3.4.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/AddHighlightModel/GenomeMouseoverHighlight.js +1 -1
- package/dist/AddHighlightModel/MsaToGenomeHighlight.js +1 -1
- package/dist/AddHighlightModel/index.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +61 -17
- package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.js +2 -2
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.d.ts +12 -0
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +21 -2
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.d.ts +1 -0
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.js +29 -0
- package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +28 -0
- package/dist/LaunchMsaView/components/BlastQuery/consts.js +21 -0
- package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -1
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +9 -4
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +9 -0
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +20 -0
- package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +1 -1
- package/dist/LaunchMsaView/components/useFeatureSequence.js +1 -1
- package/dist/LaunchMsaView/detectQueryRow.d.ts +15 -2
- package/dist/LaunchMsaView/detectQueryRow.js +20 -21
- package/dist/LaunchMsaView/detectQueryRow.test.js +15 -15
- package/dist/LaunchMsaView/useQueryRowName.js +5 -8
- package/dist/MsaViewPanel/afterCreateAutoruns.js +2 -2
- package/dist/MsaViewPanel/components/ErrorBoundary.d.ts +2 -2
- package/dist/MsaViewPanel/components/JobLink.js +7 -1
- package/dist/MsaViewPanel/components/LaunchProgress.d.ts +17 -0
- package/dist/MsaViewPanel/components/LaunchProgress.js +41 -0
- package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -3
- package/dist/MsaViewPanel/components/MsaViewPanel.test.d.ts +1 -0
- package/dist/MsaViewPanel/components/MsaViewPanel.test.js +68 -0
- package/dist/MsaViewPanel/doLaunchBlast.d.ts +1 -1
- package/dist/MsaViewPanel/doLaunchBlast.js +84 -52
- package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +7 -5
- package/dist/MsaViewPanel/doLaunchOrthologs.js +64 -30
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +106 -1
- package/dist/MsaViewPanel/genomeToMSA.js +4 -2
- package/dist/MsaViewPanel/genomeToMSA.test.js +34 -0
- package/dist/MsaViewPanel/model.d.ts +41 -11
- package/dist/MsaViewPanel/model.js +6 -0
- package/dist/MsaViewPanel/observeProteinHighlights.test.js +11 -0
- package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +1 -0
- package/dist/MsaViewPanel/util.d.ts +18 -0
- package/dist/MsaViewPanel/util.js +17 -0
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +47 -35
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.d.ts +10 -6
- package/dist/utils/blastCache.js +15 -3
- package/dist/utils/ebiBlast.d.ts +1 -1
- package/dist/utils/msa.d.ts +12 -0
- package/dist/utils/msa.js +35 -12
- package/dist/utils/msaRows.d.ts +31 -0
- package/dist/utils/msaRows.js +67 -0
- package/dist/utils/pantherOrthologs.d.ts +79 -0
- package/dist/utils/pantherOrthologs.js +262 -0
- package/dist/utils/phmmer.d.ts +53 -0
- package/dist/utils/phmmer.js +118 -0
- package/dist/utils/taxonomyNames.d.ts +1 -1
- package/dist/utils/taxonomyNames.js +6 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +27 -21
- package/src/AddHighlightModel/GenomeMouseoverHighlight.tsx +1 -1
- package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +1 -1
- package/src/AddHighlightModel/index.tsx +1 -1
- package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +88 -30
- package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -1
- package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +4 -4
- package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.ts +50 -0
- package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +23 -3
- package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +1 -1
- package/src/LaunchMsaView/components/BlastQuery/consts.ts +40 -0
- package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -1
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +21 -5
- package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +52 -0
- package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +1 -1
- package/src/LaunchMsaView/components/useFeatureSequence.ts +1 -1
- package/src/LaunchMsaView/detectQueryRow.test.ts +17 -15
- package/src/LaunchMsaView/detectQueryRow.ts +34 -23
- package/src/LaunchMsaView/useQueryRowName.ts +6 -9
- package/src/MsaViewPanel/afterCreateAutoruns.ts +2 -2
- package/src/MsaViewPanel/components/ErrorBoundary.tsx +2 -1
- package/src/MsaViewPanel/components/JobLink.tsx +7 -2
- package/src/MsaViewPanel/components/LaunchProgress.tsx +62 -0
- package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +83 -0
- package/src/MsaViewPanel/components/MsaViewPanel.tsx +7 -4
- package/src/MsaViewPanel/doLaunchBlast.ts +127 -69
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +119 -2
- package/src/MsaViewPanel/doLaunchOrthologs.ts +100 -38
- package/src/MsaViewPanel/genomeToMSA.test.ts +37 -0
- package/src/MsaViewPanel/genomeToMSA.ts +6 -2
- package/src/MsaViewPanel/model.ts +38 -5
- package/src/MsaViewPanel/observeProteinHighlights.test.ts +13 -0
- package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +1 -0
- package/src/MsaViewPanel/util.ts +18 -0
- package/src/utils/blastCache.ts +33 -12
- package/src/utils/ebiBlast.ts +1 -1
- package/src/utils/msa.ts +43 -12
- package/src/utils/msaRows.ts +95 -0
- package/src/utils/pantherOrthologs.ts +399 -0
- package/src/utils/phmmer.ts +174 -0
- package/src/utils/taxonomyNames.ts +6 -1
- package/src/version.ts +1 -1
- package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +0 -6
- package/dist/MsaViewPanel/components/LoadingBLAST.js +0 -26
- package/src/MsaViewPanel/components/LoadingBLAST.tsx +0 -48
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMemo, useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { findQueryRow } from './detectQueryRow';
|
|
3
3
|
/**
|
|
4
4
|
* The MSA row name to launch with, found by sequence rather than typed.
|
|
5
5
|
*
|
|
@@ -12,15 +12,12 @@ export function useQueryRowName(msaText, proteinSequence) {
|
|
|
12
12
|
const [override, setOverride] = useState();
|
|
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
|
-
const {
|
|
16
|
-
detected: detectQueryRow(msaText, proteinSequence),
|
|
17
|
-
names: getMsaRowNames(msaText),
|
|
18
|
-
}), [msaText, proteinSequence]);
|
|
15
|
+
const { names, match } = useMemo(() => findQueryRow(msaText, proteinSequence), [msaText, proteinSequence]);
|
|
19
16
|
return {
|
|
20
|
-
detected,
|
|
17
|
+
detected: match,
|
|
21
18
|
names,
|
|
22
|
-
querySeqName: override ??
|
|
19
|
+
querySeqName: override ?? match?.name ?? '',
|
|
23
20
|
setQuerySeqName: setOverride,
|
|
24
|
-
isAutoDetected: override === undefined && !!
|
|
21
|
+
isAutoDetected: override === undefined && !!match,
|
|
25
22
|
};
|
|
26
23
|
}
|
|
@@ -6,7 +6,7 @@ import { genomeToMSA } from './genomeToMSA';
|
|
|
6
6
|
import { loadProteinDomains } from './loadProteinDomains';
|
|
7
7
|
import { cleanupOldData, generateDataStoreId, retrieveMsaData, storeMsaData, } from './msaDataStore';
|
|
8
8
|
import { getProteinViews } from './structureConnection';
|
|
9
|
-
import { getUniprotIdFromAlphaFoldUrl } from './util';
|
|
9
|
+
import { getUniprotIdFromAlphaFoldUrl, hasQueryRow } from './util';
|
|
10
10
|
export function loadStoredData(self) {
|
|
11
11
|
const { dataStoreId, rows } = self;
|
|
12
12
|
if (dataStoreId && rows.length === 0) {
|
|
@@ -219,7 +219,7 @@ export function syncGenomeHoverToMsaColumn(self) {
|
|
|
219
219
|
*/
|
|
220
220
|
function genomeHighlightsToVisibleColumns(self, field) {
|
|
221
221
|
const { connectedViewId, transcriptToMsaMap, querySeqName } = self;
|
|
222
|
-
if (!transcriptToMsaMap) {
|
|
222
|
+
if (!transcriptToMsaMap || !hasQueryRow(self)) {
|
|
223
223
|
return [];
|
|
224
224
|
}
|
|
225
225
|
const { g2p } = transcriptToMsaMap;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
2
1
|
import React, { Component } from 'react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
3
|
interface Props {
|
|
4
4
|
children: ReactNode;
|
|
5
5
|
}
|
|
@@ -14,6 +14,6 @@ export declare class ErrorBoundary extends Component<Props, State> {
|
|
|
14
14
|
error: unknown;
|
|
15
15
|
};
|
|
16
16
|
componentDidCatch(error: unknown, info: React.ErrorInfo): void;
|
|
17
|
-
render(): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
|
|
17
|
+
render(): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
|
|
18
18
|
}
|
|
19
19
|
export {};
|
|
@@ -2,12 +2,18 @@ import React from 'react';
|
|
|
2
2
|
import { Typography } from '@mui/material';
|
|
3
3
|
import ExternalLink from '../../components/ExternalLink';
|
|
4
4
|
import { ebiBlastResultUrl } from '../../utils/ebiBlast';
|
|
5
|
+
import { isPhmmerJobId, phmmerResultUrl } from '../../utils/phmmer';
|
|
5
6
|
function JobLink({ jobId }) {
|
|
7
|
+
// read off the job id rather than the launch params, so a link rebuilt for an
|
|
8
|
+
// old cached job still points at the tool that actually ran it
|
|
9
|
+
const url = isPhmmerJobId(jobId)
|
|
10
|
+
? phmmerResultUrl(jobId)
|
|
11
|
+
: ebiBlastResultUrl(jobId);
|
|
6
12
|
return (React.createElement(Typography, null,
|
|
7
13
|
"Job ",
|
|
8
14
|
jobId,
|
|
9
15
|
" (",
|
|
10
|
-
React.createElement(ExternalLink, { href:
|
|
16
|
+
React.createElement(ExternalLink, { href: url }, "see status"),
|
|
11
17
|
")"));
|
|
12
18
|
}
|
|
13
19
|
export default JobLink;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { JBrowsePluginMsaViewModel } from '../model';
|
|
3
|
+
/**
|
|
4
|
+
* What a view shows while it is still building its alignment, and what it shows
|
|
5
|
+
* when that fails.
|
|
6
|
+
*
|
|
7
|
+
* Every launch that resolves something leaves its request on the model until it
|
|
8
|
+
* succeeds -- `blastParams`, `orthologParams`, `init` -- so one still being
|
|
9
|
+
* there IS "no alignment yet", and the error a failed launch records is only
|
|
10
|
+
* readable here. This used to key on `blastParams` alone, which left an ortholog
|
|
11
|
+
* launch rendering an empty MSAView for the minutes its alignment takes and, on
|
|
12
|
+
* failure, forever: the error was set and nothing drew it.
|
|
13
|
+
*/
|
|
14
|
+
declare const LaunchProgress: ({ model, }: {
|
|
15
|
+
model: JBrowsePluginMsaViewModel;
|
|
16
|
+
}) => React.JSX.Element;
|
|
17
|
+
export default LaunchProgress;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ErrorMessage, LoadingEllipses } from '@jbrowse/core/ui';
|
|
3
|
+
import { Typography } from '@mui/material';
|
|
4
|
+
import { observer } from 'mobx-react';
|
|
5
|
+
import { makeStyles } from 'tss-react/mui';
|
|
6
|
+
import JobLink from './JobLink';
|
|
7
|
+
const useStyles = makeStyles()({
|
|
8
|
+
margin: {
|
|
9
|
+
padding: 20,
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* What a view shows while it is still building its alignment, and what it shows
|
|
14
|
+
* when that fails.
|
|
15
|
+
*
|
|
16
|
+
* Every launch that resolves something leaves its request on the model until it
|
|
17
|
+
* succeeds -- `blastParams`, `orthologParams`, `init` -- so one still being
|
|
18
|
+
* there IS "no alignment yet", and the error a failed launch records is only
|
|
19
|
+
* readable here. This used to key on `blastParams` alone, which left an ortholog
|
|
20
|
+
* launch rendering an empty MSAView for the minutes its alignment takes and, on
|
|
21
|
+
* failure, forever: the error was set and nothing drew it.
|
|
22
|
+
*/
|
|
23
|
+
const LaunchProgress = observer(function LaunchProgress2({ model, }) {
|
|
24
|
+
const { blastParams, orthologParams, progress, rid, error } = model;
|
|
25
|
+
const { classes } = useStyles();
|
|
26
|
+
const message = blastParams
|
|
27
|
+
? 'Running EBI BLAST'
|
|
28
|
+
: orthologParams
|
|
29
|
+
? 'Building ortholog alignment'
|
|
30
|
+
: 'Loading alignment';
|
|
31
|
+
return (React.createElement("div", { className: classes.margin }, error ? (React.createElement(React.Fragment, null,
|
|
32
|
+
React.createElement(Typography, { variant: "h5" },
|
|
33
|
+
message,
|
|
34
|
+
" failed"),
|
|
35
|
+
rid ? React.createElement(JobLink, { jobId: rid }) : null,
|
|
36
|
+
React.createElement(ErrorMessage, { error: error }))) : (React.createElement(React.Fragment, null,
|
|
37
|
+
React.createElement(LoadingEllipses, { message: message, variant: "h5" }),
|
|
38
|
+
rid ? React.createElement(JobLink, { jobId: rid }) : null,
|
|
39
|
+
React.createElement(Typography, null, progress || 'Initializing')))));
|
|
40
|
+
});
|
|
41
|
+
export default LaunchProgress;
|
|
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
|
|
|
4
4
|
import { MSAView } from 'react-msaview';
|
|
5
5
|
import { makeStyles } from 'tss-react/mui';
|
|
6
6
|
import { ErrorBoundary } from './ErrorBoundary';
|
|
7
|
-
import
|
|
7
|
+
import LaunchProgress from './LaunchProgress';
|
|
8
8
|
const useStyles = makeStyles()({
|
|
9
9
|
loadingContainer: {
|
|
10
10
|
padding: 20,
|
|
@@ -12,9 +12,12 @@ const useStyles = makeStyles()({
|
|
|
12
12
|
});
|
|
13
13
|
const MsaViewPanel = observer(function MsaViewPanel2({ model, }) {
|
|
14
14
|
const { classes } = useStyles();
|
|
15
|
-
const { blastParams, loadingStoredData } = model;
|
|
15
|
+
const { blastParams, orthologParams, init, loadingStoredData } = model;
|
|
16
|
+
// an unresolved launch request means there is no alignment to draw yet, so all
|
|
17
|
+
// three gate the same panel -- see LaunchProgress
|
|
18
|
+
const launching = !!(blastParams ?? orthologParams ?? init);
|
|
16
19
|
return (React.createElement(ErrorBoundary, null,
|
|
17
|
-
React.createElement("div", null,
|
|
20
|
+
React.createElement("div", null, launching ? (React.createElement(LaunchProgress, { model: model })) : loadingStoredData ? (React.createElement("div", { className: classes.loadingContainer },
|
|
18
21
|
React.createElement(LoadingEllipses, { message: "Loading MSA data", variant: "h6" }))) : (React.createElement(MSAView, { model: model })))));
|
|
19
22
|
});
|
|
20
23
|
export default MsaViewPanel;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { cleanup, render, screen } from '@testing-library/react';
|
|
4
|
+
import { afterEach, expect, test, vi } from 'vitest';
|
|
5
|
+
import MsaViewPanel from './MsaViewPanel';
|
|
6
|
+
// react-msaview's MSAView is the "nothing is launching" branch and wants a real
|
|
7
|
+
// MST model; a marker is enough to say the panel reached it
|
|
8
|
+
vi.mock('react-msaview', () => ({
|
|
9
|
+
MSAView: () => React.createElement("div", null, "the alignment"),
|
|
10
|
+
}));
|
|
11
|
+
function panel(model) {
|
|
12
|
+
return render(React.createElement(MsaViewPanel, { model: model }));
|
|
13
|
+
}
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
cleanup();
|
|
16
|
+
});
|
|
17
|
+
test('an alignment with no pending launch draws itself', () => {
|
|
18
|
+
panel({ progress: '' });
|
|
19
|
+
expect(screen.getByText('the alignment')).toBeTruthy();
|
|
20
|
+
});
|
|
21
|
+
test('a running BLAST shows its progress, not an empty alignment', () => {
|
|
22
|
+
panel({
|
|
23
|
+
blastParams: { proteinSequence: 'MKV' },
|
|
24
|
+
progress: 'Submitting query',
|
|
25
|
+
});
|
|
26
|
+
expect(screen.queryByText('the alignment')).toBeNull();
|
|
27
|
+
expect(screen.getByText(/Running EBI BLAST/)).toBeTruthy();
|
|
28
|
+
expect(screen.getByText('Submitting query')).toBeTruthy();
|
|
29
|
+
});
|
|
30
|
+
// the bug this file was written for: an ortholog launch sets orthologParams
|
|
31
|
+
// rather than blastParams, and the panel keyed on blastParams alone -- so it
|
|
32
|
+
// rendered an empty MSAView for the minutes the alignment takes, and drew
|
|
33
|
+
// nothing at all when the launch failed
|
|
34
|
+
test('a running ortholog launch shows its progress', () => {
|
|
35
|
+
panel({
|
|
36
|
+
orthologParams: { taxId: 9606 },
|
|
37
|
+
progress: 'Resolving orthologs',
|
|
38
|
+
});
|
|
39
|
+
expect(screen.queryByText('the alignment')).toBeNull();
|
|
40
|
+
expect(screen.getByText(/Building ortholog alignment/)).toBeTruthy();
|
|
41
|
+
expect(screen.getByText('Resolving orthologs')).toBeTruthy();
|
|
42
|
+
});
|
|
43
|
+
test('a failed ortholog launch shows why', () => {
|
|
44
|
+
panel({
|
|
45
|
+
orthologParams: { taxId: 9606 },
|
|
46
|
+
progress: '',
|
|
47
|
+
error: new Error('Only 1 ortholog(s) found for this gene'),
|
|
48
|
+
});
|
|
49
|
+
expect(screen.getByText(/Only 1 ortholog\(s\) found/)).toBeTruthy();
|
|
50
|
+
});
|
|
51
|
+
test('a failed init shows why', () => {
|
|
52
|
+
panel({
|
|
53
|
+
init: { msaName: 'ENST00000288602' },
|
|
54
|
+
progress: '',
|
|
55
|
+
error: new Error('No alignment named ENST00000288602 in msa.fa.gz'),
|
|
56
|
+
});
|
|
57
|
+
expect(screen.queryByText('the alignment')).toBeNull();
|
|
58
|
+
expect(screen.getByText(/No alignment named ENST00000288602/)).toBeTruthy();
|
|
59
|
+
});
|
|
60
|
+
test('a running job links out to it', () => {
|
|
61
|
+
panel({
|
|
62
|
+
blastParams: { proteinSequence: 'MKV' },
|
|
63
|
+
progress: 'Re-checking BLAST status in... 7',
|
|
64
|
+
rid: 'ncbiblast-R20260826-123456-0001-abc',
|
|
65
|
+
});
|
|
66
|
+
const link = screen.getByRole('link');
|
|
67
|
+
expect(link.getAttribute('href')).toContain('jobId=ncbiblast-R20260826-123456-0001-abc');
|
|
68
|
+
});
|
|
@@ -2,29 +2,72 @@ import { makeId, strip } from '../LaunchMsaView/components/util';
|
|
|
2
2
|
import { cleanProteinSequence } from '../LaunchMsaView/util';
|
|
3
3
|
import { saveBlastResult } from '../utils/blastCache';
|
|
4
4
|
import { queryEbiBlast } from '../utils/ebiBlast';
|
|
5
|
-
import { launchMSA } from '../utils/msa';
|
|
5
|
+
import { launchMSA, launchTree } from '../utils/msa';
|
|
6
|
+
import { buildPhmmerMsa, buildRowMetadata } from '../utils/msaRows';
|
|
7
|
+
import { queryPhmmer } from '../utils/phmmer';
|
|
6
8
|
import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
7
9
|
export async function doLaunchBlast({ self, }) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
// kept whole rather than destructured: the database's type depends on
|
|
11
|
+
// searchProgram, and pulling the two apart loses the link between them
|
|
12
|
+
const params = self.blastParams;
|
|
13
|
+
const { selectedTranscript } = params;
|
|
14
|
+
const cleanedSeq = cleanProteinSequence(params.proteinSequence);
|
|
10
15
|
const onProgress = (arg) => {
|
|
11
16
|
self.setProgress(arg);
|
|
12
17
|
};
|
|
18
|
+
// publish the job id before the first poll so the view can link out while the
|
|
19
|
+
// job is still running
|
|
20
|
+
const onRid = (r) => {
|
|
21
|
+
self.setRid(r);
|
|
22
|
+
};
|
|
23
|
+
const { msa, tree, treeMetadata, rid } = params.searchProgram === 'phmmer'
|
|
24
|
+
? await runPhmmer({
|
|
25
|
+
query: cleanedSeq,
|
|
26
|
+
database: params.blastDatabase,
|
|
27
|
+
onProgress,
|
|
28
|
+
onRid,
|
|
29
|
+
})
|
|
30
|
+
: await runBlast({
|
|
31
|
+
query: cleanedSeq,
|
|
32
|
+
blastDatabase: params.blastDatabase,
|
|
33
|
+
msaAlgorithm: params.msaAlgorithm,
|
|
34
|
+
onProgress,
|
|
35
|
+
onRid,
|
|
36
|
+
});
|
|
37
|
+
const treeMetadataJson = JSON.stringify(treeMetadata);
|
|
38
|
+
await saveBlastResult({
|
|
39
|
+
proteinSequence: cleanedSeq,
|
|
40
|
+
blastDatabase: params.blastDatabase,
|
|
41
|
+
msaAlgorithm: params.msaAlgorithm,
|
|
42
|
+
searchProgram: params.searchProgram,
|
|
43
|
+
msa,
|
|
44
|
+
tree,
|
|
45
|
+
treeMetadata: treeMetadataJson,
|
|
46
|
+
rid,
|
|
47
|
+
geneId: selectedTranscript?.get('parentId'),
|
|
48
|
+
transcriptId: selectedTranscript?.id(),
|
|
49
|
+
transcriptName: selectedTranscript?.get('name') ?? selectedTranscript?.get('id'),
|
|
50
|
+
geneName: selectedTranscript?.get('gene_name') ??
|
|
51
|
+
selectedTranscript?.get('parentId'),
|
|
52
|
+
});
|
|
53
|
+
return { msa, tree, treeMetadata: treeMetadataJson };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* BLAST returns each hit already aligned to the query, but pairwise and one hit
|
|
57
|
+
* at a time, so the alignments are stripped back off and every hit is realigned
|
|
58
|
+
* together by a dedicated aligner.
|
|
59
|
+
*/
|
|
60
|
+
async function runBlast({ query, blastDatabase, msaAlgorithm, onProgress, onRid, }) {
|
|
13
61
|
const { hits, rid } = await queryEbiBlast({
|
|
14
|
-
query
|
|
62
|
+
query,
|
|
15
63
|
blastDatabase,
|
|
16
64
|
onProgress,
|
|
17
|
-
|
|
18
|
-
// the job is still running
|
|
19
|
-
onRid: r => {
|
|
20
|
-
self.setRid(r);
|
|
21
|
-
},
|
|
65
|
+
onRid,
|
|
22
66
|
});
|
|
23
|
-
|
|
24
|
-
const
|
|
67
|
+
onProgress('Fetching species taxonomy info...');
|
|
68
|
+
const taxonomyInfo = await fetchTaxonomyInfo(hits
|
|
25
69
|
.map(h => h.description[0]?.taxid)
|
|
26
|
-
.filter((t) => t !== undefined);
|
|
27
|
-
const taxonomyInfo = await fetchTaxonomyInfo(taxids);
|
|
70
|
+
.filter((t) => t !== undefined));
|
|
28
71
|
const treeMetadata = {};
|
|
29
72
|
const sequences = hits.map(h => {
|
|
30
73
|
const desc = h.description[0] ?? {
|
|
@@ -33,52 +76,41 @@ export async function doLaunchBlast({ self, }) {
|
|
|
33
76
|
sciname: 'unknown',
|
|
34
77
|
};
|
|
35
78
|
const rowName = makeId(desc, taxonomyInfo);
|
|
36
|
-
const seq = strip(h.hsps[0]?.hseq ?? '');
|
|
37
79
|
treeMetadata[rowName] = buildRowMetadata(desc, taxonomyInfo);
|
|
38
|
-
return `>${rowName}\n${
|
|
80
|
+
return `>${rowName}\n${strip(h.hsps[0]?.hseq ?? '')}`;
|
|
39
81
|
});
|
|
40
82
|
const result = await launchMSA({
|
|
41
83
|
algorithm: msaAlgorithm,
|
|
42
|
-
sequence: [`>QUERY\n${
|
|
84
|
+
sequence: [`>QUERY\n${query}`, ...sequences].join('\n'),
|
|
43
85
|
onProgress,
|
|
44
86
|
});
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
87
|
+
return { ...result, treeMetadata, rid };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* phmmer aligns every hit to a profile of the query as it searches, so its own
|
|
91
|
+
* output is the MSA and there is no realignment step — the hits keep the
|
|
92
|
+
* placement HMMER gave them, and the query row is derived from the alignment's
|
|
93
|
+
* match columns rather than being aligned back in afterwards. That leaves no
|
|
94
|
+
* aligner run to take a tree from, so the tree is built from this alignment.
|
|
95
|
+
*/
|
|
96
|
+
async function runPhmmer({ query, database, onProgress, onRid, }) {
|
|
97
|
+
const { rows, queryRow, rid } = await queryPhmmer({
|
|
98
|
+
query,
|
|
99
|
+
database,
|
|
100
|
+
onProgress,
|
|
101
|
+
onRid,
|
|
102
|
+
});
|
|
103
|
+
onProgress('Fetching species taxonomy info...');
|
|
104
|
+
const taxonomyInfo = await fetchTaxonomyInfo(rows.map(r => r.taxid).filter((t) => t !== undefined));
|
|
105
|
+
const { msa, treeMetadata } = buildPhmmerMsa({
|
|
106
|
+
rows,
|
|
107
|
+
queryRow,
|
|
108
|
+
taxonomyInfo,
|
|
59
109
|
});
|
|
60
110
|
return {
|
|
61
|
-
|
|
62
|
-
|
|
111
|
+
msa,
|
|
112
|
+
tree: await launchTree({ alignment: msa, onProgress }),
|
|
113
|
+
treeMetadata,
|
|
114
|
+
rid,
|
|
63
115
|
};
|
|
64
116
|
}
|
|
65
|
-
function buildRowMetadata(desc, taxonomyInfo) {
|
|
66
|
-
const metadata = {};
|
|
67
|
-
const taxInfo = desc.taxid ? taxonomyInfo.get(desc.taxid) : undefined;
|
|
68
|
-
if (taxInfo?.sciname) {
|
|
69
|
-
metadata['Scientific name'] = taxInfo.sciname;
|
|
70
|
-
}
|
|
71
|
-
if (taxInfo?.commonName) {
|
|
72
|
-
metadata['Common name'] = taxInfo.commonName;
|
|
73
|
-
}
|
|
74
|
-
if (desc.accession) {
|
|
75
|
-
metadata.Accession = desc.accession;
|
|
76
|
-
}
|
|
77
|
-
if (desc.id) {
|
|
78
|
-
metadata.ID = desc.id;
|
|
79
|
-
}
|
|
80
|
-
if (desc.title) {
|
|
81
|
-
metadata.Description = desc.title;
|
|
82
|
-
}
|
|
83
|
-
return metadata;
|
|
84
|
-
}
|
|
@@ -3,12 +3,14 @@ import type { JBrowsePluginMsaViewModel } from './model';
|
|
|
3
3
|
* The no-search-job alternative to doLaunchBlast.
|
|
4
4
|
*
|
|
5
5
|
* BLAST spends 10+ minutes answering "what looks like this sequence" and
|
|
6
|
-
* returns a redundant, accession-labelled hit list. This asks
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* returns a redundant, accession-labelled hit list. This asks the question the
|
|
7
|
+
* alignment actually wants — "what is this gene's ortholog in each species" —
|
|
8
|
+
* which NCBI and PANTHER have already computed, so the lookup returns in
|
|
9
|
+
* seconds and only the EBI alignment (~10s) costs real time. `source` picks
|
|
10
|
+
* which of the two answers: NCBI for vertebrates and insects, PANTHER for
|
|
11
|
+
* everything else (yeast, worm, plants, and a fly gene's vertebrate relatives).
|
|
10
12
|
*
|
|
11
|
-
* The query row is the user's OWN selected transcript, not
|
|
13
|
+
* The query row is the user's OWN selected transcript, not the source's
|
|
12
14
|
* representative protein for the query species, because `connectedFeature`
|
|
13
15
|
* maps genome coordinates through that row — swapping in a different isoform
|
|
14
16
|
* would silently break the genome<->MSA linkage. The query species is therefore
|
|
@@ -1,57 +1,55 @@
|
|
|
1
1
|
import { cleanProteinSequence } from '../LaunchMsaView/util';
|
|
2
2
|
import { launchMSA } from '../utils/msa';
|
|
3
3
|
import { dedupeLabels, fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils/ncbiOrthologs';
|
|
4
|
+
import { fetchPantherOrthologs } from '../utils/pantherOrthologs';
|
|
4
5
|
import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
5
6
|
/**
|
|
6
7
|
* The no-search-job alternative to doLaunchBlast.
|
|
7
8
|
*
|
|
8
9
|
* BLAST spends 10+ minutes answering "what looks like this sequence" and
|
|
9
|
-
* returns a redundant, accession-labelled hit list. This asks
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* returns a redundant, accession-labelled hit list. This asks the question the
|
|
11
|
+
* alignment actually wants — "what is this gene's ortholog in each species" —
|
|
12
|
+
* which NCBI and PANTHER have already computed, so the lookup returns in
|
|
13
|
+
* seconds and only the EBI alignment (~10s) costs real time. `source` picks
|
|
14
|
+
* which of the two answers: NCBI for vertebrates and insects, PANTHER for
|
|
15
|
+
* everything else (yeast, worm, plants, and a fly gene's vertebrate relatives).
|
|
13
16
|
*
|
|
14
|
-
* The query row is the user's OWN selected transcript, not
|
|
17
|
+
* The query row is the user's OWN selected transcript, not the source's
|
|
15
18
|
* representative protein for the query species, because `connectedFeature`
|
|
16
19
|
* maps genome coordinates through that row — swapping in a different isoform
|
|
17
20
|
* would silently break the genome<->MSA linkage. The query species is therefore
|
|
18
21
|
* excluded from the ortholog set rather than appearing twice.
|
|
19
22
|
*/
|
|
20
23
|
export async function doLaunchOrthologs({ self, }) {
|
|
21
|
-
const { taxId, taxa, maxSpecies, geneCandidates, msaAlgorithm, proteinSequence, } = self.orthologParams;
|
|
24
|
+
const { taxId, taxa, maxSpecies, geneCandidates, msaAlgorithm, proteinSequence, source = 'ncbi', } = self.orthologParams;
|
|
22
25
|
const onProgress = (arg) => {
|
|
23
26
|
self.setProgress(arg);
|
|
24
27
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const request = {
|
|
29
|
+
taxId,
|
|
30
|
+
geneCandidates,
|
|
31
|
+
taxa: taxa ? new Set(taxa) : undefined,
|
|
32
|
+
// the query species is represented by the query row below
|
|
33
|
+
exclude: taxId,
|
|
34
|
+
limit: maxSpecies,
|
|
35
|
+
onProgress,
|
|
36
|
+
};
|
|
37
|
+
const { geneId, representative, rows } = source === 'panther'
|
|
38
|
+
? await findPantherOrthologs(request)
|
|
39
|
+
: await findNcbiOrthologs(request);
|
|
30
40
|
// The query row. The dialog always supplies it — it is the user's OWN
|
|
31
41
|
// selected transcript, which is what makes `connectedFeature` map genome
|
|
32
42
|
// coordinates through this row. A launch that has no transcript to translate
|
|
33
|
-
// (a session spec naming only a gene) falls back to
|
|
34
|
-
// protein for the resolved gene, which is the same choice
|
|
35
|
-
// other row, so the alignment is the one
|
|
36
|
-
|
|
43
|
+
// (a session spec naming only a gene) falls back to the source's
|
|
44
|
+
// representative protein for the resolved gene, which is the same choice
|
|
45
|
+
// made for every other row, so the alignment is the one the source would
|
|
46
|
+
// build for that gene.
|
|
37
47
|
const cleanedSeq = proteinSequence
|
|
38
48
|
? cleanProteinSequence(proteinSequence)
|
|
39
49
|
: representative?.sequence;
|
|
40
50
|
if (!cleanedSeq) {
|
|
41
|
-
throw new Error(`No query protein: none was supplied and NCBI returned no representative protein for gene ${
|
|
51
|
+
throw new Error(`No query protein: none was supplied and ${source === 'panther' ? 'PANTHER' : 'NCBI'} returned no representative protein for gene ${geneId}.`);
|
|
42
52
|
}
|
|
43
|
-
// Every species NCBI has an ortholog for, when a launch names none, capped at
|
|
44
|
-
// maxSpecies. A launch that wants specific species lists them; one that just
|
|
45
|
-
// wants "this gene across species" gets NCBI's own order, which leads with the
|
|
46
|
-
// reference organisms.
|
|
47
|
-
const rows = await fetchOrthologRows({
|
|
48
|
-
geneId: resolved.geneId,
|
|
49
|
-
taxa: taxa ? new Set(taxa) : undefined,
|
|
50
|
-
// the query species is represented by the query row above
|
|
51
|
-
exclude: taxId,
|
|
52
|
-
limit: maxSpecies,
|
|
53
|
-
onProgress,
|
|
54
|
-
});
|
|
55
53
|
// The query row is named for its species like every other row, with a suffix
|
|
56
54
|
// marking it as the one the genome view is linked to. A bare `QUERY` among
|
|
57
55
|
// ninety-nine named species reads as a row whose species failed to resolve,
|
|
@@ -66,7 +64,7 @@ export async function doLaunchOrthologs({ self, }) {
|
|
|
66
64
|
const queryLabel = await queryRowLabel(taxId, rows);
|
|
67
65
|
self.setQuerySeqName(queryLabel);
|
|
68
66
|
const treeMetadata = {
|
|
69
|
-
[queryLabel]: buildQueryMetadata(self,
|
|
67
|
+
[queryLabel]: buildQueryMetadata(self, geneId, cleanedSeq, representative),
|
|
70
68
|
};
|
|
71
69
|
for (const row of rows) {
|
|
72
70
|
treeMetadata[row.label] = buildRowMetadata(row);
|
|
@@ -84,6 +82,42 @@ export async function doLaunchOrthologs({ self, }) {
|
|
|
84
82
|
treeMetadata: JSON.stringify(treeMetadata),
|
|
85
83
|
};
|
|
86
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Every species NCBI has an ortholog for, when a launch names none, capped at
|
|
87
|
+
* `limit`. A launch that wants specific species lists them; one that just
|
|
88
|
+
* wants "this gene across species" gets NCBI's own order, which leads with the
|
|
89
|
+
* reference organisms.
|
|
90
|
+
*/
|
|
91
|
+
async function findNcbiOrthologs({ taxId, geneCandidates, onProgress, ...rest }) {
|
|
92
|
+
onProgress('Resolving gene at NCBI...');
|
|
93
|
+
const resolved = await resolveGeneId(geneCandidates, taxId);
|
|
94
|
+
if (!resolved) {
|
|
95
|
+
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.`);
|
|
96
|
+
}
|
|
97
|
+
const representative = await fetchRepresentativeQueryProtein(resolved.geneId);
|
|
98
|
+
const rows = await fetchOrthologRows({
|
|
99
|
+
geneId: resolved.geneId,
|
|
100
|
+
onProgress,
|
|
101
|
+
...rest,
|
|
102
|
+
});
|
|
103
|
+
return { geneId: resolved.geneId, representative, rows };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* One `matchortho` call resolves the gene, names its own UniProt entry and
|
|
107
|
+
* lists an ortholog per genome, so the representative protein needs no second
|
|
108
|
+
* lookup here.
|
|
109
|
+
*/
|
|
110
|
+
async function findPantherOrthologs({ geneCandidates, ...rest }) {
|
|
111
|
+
const found = await fetchPantherOrthologs({
|
|
112
|
+
candidates: geneCandidates,
|
|
113
|
+
...rest,
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
geneId: found.query?.geneRef ?? found.matched,
|
|
117
|
+
representative: found.query,
|
|
118
|
+
rows: found.rows,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
87
121
|
/**
|
|
88
122
|
* `<species>_query`, unique against the ortholog labels. Falls back to the bare
|
|
89
123
|
* marker when NCBI cannot name the taxon, which is a naming failure and must not
|
|
@@ -120,7 +154,7 @@ async function fetchRepresentativeQueryProtein(geneId) {
|
|
|
120
154
|
/**
|
|
121
155
|
* The query row carries an Accession — which is what drives the automatic CDD
|
|
122
156
|
* overlay (afterCreateAutoruns.autoLoadProteinDomains -> loadProteinDomains) —
|
|
123
|
-
* ONLY when its sequence is byte-identical to the
|
|
157
|
+
* ONLY when its sequence is byte-identical to the protein that accession
|
|
124
158
|
* names. Attaching it unconditionally would put every domain box at an offset
|
|
125
159
|
* whenever the user picked a non-representative isoform, which is a silently
|
|
126
160
|
* wrong figure rather than a missing one. A launch that took the representative
|