jbrowse-plugin-msaview 3.3.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 +3 -3
- 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.test.js +1 -1
- package/dist/MsaViewPanel/genomeToMSA.js +4 -2
- package/dist/MsaViewPanel/genomeToMSA.test.js +34 -0
- package/dist/MsaViewPanel/model.d.ts +32 -11
- 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/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 +5 -5
- 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 +2 -2
- package/src/MsaViewPanel/doLaunchOrthologs.ts +1 -1
- package/src/MsaViewPanel/genomeToMSA.test.ts +37 -0
- package/src/MsaViewPanel/genomeToMSA.ts +6 -2
- package/src/MsaViewPanel/model.ts +28 -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/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
|
@@ -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
|
-
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
2
|
-
import { doLaunchOrthologs } from './doLaunchOrthologs';
|
|
3
2
|
import { launchMSA } from '../utils/msa';
|
|
4
3
|
import { defaultMaxSpecies, fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils/ncbiOrthologs';
|
|
5
4
|
import { fetchPantherOrthologs } from '../utils/pantherOrthologs';
|
|
6
5
|
import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
6
|
+
import { doLaunchOrthologs } from './doLaunchOrthologs';
|
|
7
7
|
// Every network call is mocked and nothing else is. What is under test is the
|
|
8
8
|
// argument shaping either side of those calls -- which species get asked for,
|
|
9
9
|
// what becomes the QUERY row, and whether the row earns the Accession that
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util';
|
|
2
|
-
import { hasHoverPosition } from './util';
|
|
2
|
+
import { hasHoverPosition, hasQueryRow } from './util';
|
|
3
3
|
export function genomeToMSA({ model }) {
|
|
4
4
|
const { hovered } = getSession(model);
|
|
5
5
|
const { querySeqName, transcriptToMsaMap, connectedView, mafRegion } = model;
|
|
6
|
-
if (!connectedView?.initialized ||
|
|
6
|
+
if (!connectedView?.initialized ||
|
|
7
|
+
!hasHoverPosition(hovered) ||
|
|
8
|
+
!hasQueryRow(model)) {
|
|
7
9
|
return undefined;
|
|
8
10
|
}
|
|
9
11
|
const { coord, refName } = hovered.hoverPosition;
|
|
@@ -19,6 +19,7 @@ describe('genomeToMSA', () => {
|
|
|
19
19
|
});
|
|
20
20
|
const model = {
|
|
21
21
|
querySeqName: 'hg38.chr1',
|
|
22
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
22
23
|
transcriptToMsaMap: undefined,
|
|
23
24
|
mafRegion: {
|
|
24
25
|
refName: 'chr1',
|
|
@@ -38,6 +39,7 @@ describe('genomeToMSA', () => {
|
|
|
38
39
|
});
|
|
39
40
|
const model = {
|
|
40
41
|
querySeqName: 'hg38.chr1',
|
|
42
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
41
43
|
transcriptToMsaMap: undefined,
|
|
42
44
|
mafRegion: {
|
|
43
45
|
refName: 'chr1',
|
|
@@ -62,6 +64,7 @@ describe('genomeToMSA', () => {
|
|
|
62
64
|
const mockSeqPosToVisibleCol = vi.fn().mockReturnValue(5);
|
|
63
65
|
const model = {
|
|
64
66
|
querySeqName: 'hg38.chr1',
|
|
67
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
65
68
|
transcriptToMsaMap: undefined,
|
|
66
69
|
mafRegion: {
|
|
67
70
|
refName: 'chr1',
|
|
@@ -90,6 +93,7 @@ describe('genomeToMSA', () => {
|
|
|
90
93
|
});
|
|
91
94
|
const model = {
|
|
92
95
|
querySeqName: 'hg38.chr1',
|
|
96
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
93
97
|
transcriptToMsaMap: undefined,
|
|
94
98
|
mafRegion: {
|
|
95
99
|
refName: 'chr1',
|
|
@@ -116,6 +120,7 @@ describe('genomeToMSA', () => {
|
|
|
116
120
|
});
|
|
117
121
|
const model = {
|
|
118
122
|
querySeqName: 'hg38.chr1',
|
|
123
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
119
124
|
transcriptToMsaMap: undefined,
|
|
120
125
|
mafRegion: {
|
|
121
126
|
refName: 'chr1',
|
|
@@ -142,6 +147,7 @@ describe('genomeToMSA', () => {
|
|
|
142
147
|
});
|
|
143
148
|
const model = {
|
|
144
149
|
querySeqName: 'hg38.chr1',
|
|
150
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
145
151
|
transcriptToMsaMap: undefined,
|
|
146
152
|
mafRegion: {
|
|
147
153
|
refName: 'chr1',
|
|
@@ -167,6 +173,7 @@ describe('genomeToMSA', () => {
|
|
|
167
173
|
});
|
|
168
174
|
const model = {
|
|
169
175
|
querySeqName: 'hg38.chr1',
|
|
176
|
+
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
170
177
|
transcriptToMsaMap: undefined,
|
|
171
178
|
mafRegion: {
|
|
172
179
|
refName: 'chr1',
|
|
@@ -195,6 +202,7 @@ describe('genomeToMSA', () => {
|
|
|
195
202
|
const mockSeqPosToVisibleCol = vi.fn().mockReturnValue(10);
|
|
196
203
|
const model = {
|
|
197
204
|
querySeqName: 'QUERY',
|
|
205
|
+
rows: [['QUERY', 'MKVLTAEEK']],
|
|
198
206
|
transcriptToMsaMap: {
|
|
199
207
|
refName: 'chr1',
|
|
200
208
|
// g2p is keyed by 0-based genome position, the hover coord is 1-based
|
|
@@ -220,6 +228,7 @@ describe('genomeToMSA', () => {
|
|
|
220
228
|
const mockSeqPosToVisibleCol = vi.fn();
|
|
221
229
|
const model = {
|
|
222
230
|
querySeqName: 'QUERY',
|
|
231
|
+
rows: [['QUERY', 'MKVLTAEEK']],
|
|
223
232
|
transcriptToMsaMap: {
|
|
224
233
|
refName: 'chr1',
|
|
225
234
|
g2p: { 1004: 10 },
|
|
@@ -240,6 +249,7 @@ describe('genomeToMSA', () => {
|
|
|
240
249
|
});
|
|
241
250
|
const model = {
|
|
242
251
|
querySeqName: 'QUERY',
|
|
252
|
+
rows: [['QUERY', 'MKVLTAEEK']],
|
|
243
253
|
transcriptToMsaMap: {
|
|
244
254
|
refName: 'chr1',
|
|
245
255
|
g2p: { 1000: 0 }, // No entry for 1004
|
|
@@ -261,6 +271,7 @@ describe('genomeToMSA', () => {
|
|
|
261
271
|
});
|
|
262
272
|
const model = {
|
|
263
273
|
querySeqName: 'QUERY',
|
|
274
|
+
rows: [['QUERY', 'MKVLTAEEK']],
|
|
264
275
|
transcriptToMsaMap: undefined,
|
|
265
276
|
mafRegion: undefined,
|
|
266
277
|
connectedView: { initialized: true },
|
|
@@ -269,4 +280,27 @@ describe('genomeToMSA', () => {
|
|
|
269
280
|
const result = genomeToMSA({ model });
|
|
270
281
|
expect(result).toBeUndefined();
|
|
271
282
|
});
|
|
283
|
+
// seqPosToVisibleCol answers 0 for a row name it does not know, so without a
|
|
284
|
+
// guard an alignment whose query row is missing -- the default 'QUERY' on an
|
|
285
|
+
// uploaded file, or the empty name the manual panel leaves when it matches
|
|
286
|
+
// nothing -- lights column 0 on every genome hover
|
|
287
|
+
test('returns undefined when querySeqName names no row here', () => {
|
|
288
|
+
mockGetSession.mockReturnValue({
|
|
289
|
+
hovered: {
|
|
290
|
+
hoverFeature: {},
|
|
291
|
+
hoverPosition: { coord: 1005, refName: 'chr1' },
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
const seqPosToVisibleCol = vi.fn(() => 0);
|
|
295
|
+
const model = {
|
|
296
|
+
querySeqName: 'QUERY',
|
|
297
|
+
rows: [['some_other_row', 'MKVLTAEEK']],
|
|
298
|
+
transcriptToMsaMap: { refName: 'chr1', g2p: { 1004: 3 } },
|
|
299
|
+
mafRegion: undefined,
|
|
300
|
+
connectedView: { initialized: true },
|
|
301
|
+
seqPosToVisibleCol,
|
|
302
|
+
};
|
|
303
|
+
expect(genomeToMSA({ model })).toBeUndefined();
|
|
304
|
+
expect(seqPosToVisibleCol).not.toHaveBeenCalled();
|
|
305
|
+
});
|
|
272
306
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { MSAFormat } from 'msa-parsers';
|
|
2
|
+
import type { BlastDatabase, MsaAlgorithm, PhmmerDatabase } from '../LaunchMsaView/components/BlastQuery/consts';
|
|
2
3
|
import type { MafRegion, MsaViewInitState } from './types';
|
|
3
|
-
import type { BlastDatabase, MsaAlgorithm } from '../LaunchMsaView/components/BlastQuery/consts';
|
|
4
4
|
import type { Feature } from '@jbrowse/core/util';
|
|
5
5
|
import type { Instance } from '@jbrowse/mobx-state-tree';
|
|
6
6
|
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view';
|
|
@@ -11,12 +11,31 @@ export interface IRegion {
|
|
|
11
11
|
start: number;
|
|
12
12
|
end: number;
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
/**
|
|
15
|
+
* A search to run, discriminated by the program that runs it: the two arms
|
|
16
|
+
* differ in which databases they name and in whether an aligner runs at all, so
|
|
17
|
+
* splitting them is what lets doLaunchBlast read the database without asserting
|
|
18
|
+
* whose it is.
|
|
19
|
+
*
|
|
20
|
+
* The field is still `blastDatabase` rather than `database`: it is persisted in
|
|
21
|
+
* session snapshots and in the IndexedDB result cache, so renaming it would
|
|
22
|
+
* orphan every row already written.
|
|
23
|
+
*/
|
|
24
|
+
export type BlastParams = {
|
|
17
25
|
selectedTranscript?: Feature;
|
|
18
26
|
proteinSequence: string;
|
|
19
|
-
}
|
|
27
|
+
} & ({
|
|
28
|
+
/** absent on params written before phmmer existed, which were all blastp */
|
|
29
|
+
searchProgram?: 'blastp';
|
|
30
|
+
blastDatabase: BlastDatabase;
|
|
31
|
+
msaAlgorithm: MsaAlgorithm;
|
|
32
|
+
} | {
|
|
33
|
+
searchProgram: 'phmmer';
|
|
34
|
+
/** phmmer names its databases its own way: `swissprot`, not `uniprotkb_swissprot` */
|
|
35
|
+
blastDatabase: PhmmerDatabase;
|
|
36
|
+
/** phmmer aligns as it searches, so there is no aligner to choose */
|
|
37
|
+
msaAlgorithm?: undefined;
|
|
38
|
+
});
|
|
20
39
|
/**
|
|
21
40
|
* Where the ortholog set comes from. NCBI's sets cover vertebrates and
|
|
22
41
|
* insects; PANTHER's span its 144 reference proteomes, human to yeast to
|
|
@@ -66,7 +85,7 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
|
|
|
66
85
|
id: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<string>, [undefined]>;
|
|
67
86
|
displayName: import("@jbrowse/mobx-state-tree").IMaybe<import("@jbrowse/mobx-state-tree").ISimpleType<string>>;
|
|
68
87
|
minimized: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
|
|
69
|
-
}, "
|
|
88
|
+
}, "id" | "type" | "autoTreeAreaWidth" | "drawLabels" | "drawNodeBubbles" | "drawTree" | "labelsAlignRight" | "showBranchLen" | "treeAreaWidth" | "treeWidth" | "bgColor" | "colorSchemeName" | "msaFormat" | "showColumnStats" | "allowedGappyness" | "colWidth" | "collapsed" | "currentAlignment" | "data" | "drawMsaLetters" | "featureFilters" | "gffFilehandle" | "height" | "hideGaps" | "highlightColumns" | "msaFilehandle" | "relativeTo" | "rowHeight" | "scrollX" | "scrollY" | "scrollZoom" | "showDomainLegend" | "showDomains" | "showOnly" | "subFeatureRows" | "treeFilehandle" | "treeMetadataFilehandle" | "turnedOffTracks"> & Omit<Omit<Omit<{}, "autoTreeAreaWidth" | "drawLabels" | "drawNodeBubbles" | "drawTree" | "labelsAlignRight" | "showBranchLen" | "treeAreaWidth" | "treeWidth"> & {
|
|
70
89
|
drawLabels: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
|
|
71
90
|
labelsAlignRight: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
|
|
72
91
|
treeAreaWidth: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<number>, [undefined]>;
|
|
@@ -80,7 +99,7 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
|
|
|
80
99
|
colorSchemeName: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<string>, [undefined]>;
|
|
81
100
|
showColumnStats: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
|
|
82
101
|
msaFormat: import("@jbrowse/mobx-state-tree").IMaybe<import("@jbrowse/mobx-state-tree").ISimpleType<import("msa-parsers").MSAFormat>>;
|
|
83
|
-
}, "
|
|
102
|
+
}, "id" | "type" | "allowedGappyness" | "colWidth" | "collapsed" | "currentAlignment" | "data" | "drawMsaLetters" | "featureFilters" | "gffFilehandle" | "height" | "hideGaps" | "highlightColumns" | "msaFilehandle" | "relativeTo" | "rowHeight" | "scrollX" | "scrollY" | "scrollZoom" | "showDomainLegend" | "showDomains" | "showOnly" | "subFeatureRows" | "treeFilehandle" | "treeMetadataFilehandle" | "turnedOffTracks"> & {
|
|
84
103
|
id: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<string>, [undefined]>;
|
|
85
104
|
showDomains: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
|
|
86
105
|
showDomainLegend: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
|
|
@@ -486,10 +505,7 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
|
|
|
486
505
|
setTreeMetadata(treeMetadata?: string): void;
|
|
487
506
|
setGFF(gff?: string): void;
|
|
488
507
|
}, import("@jbrowse/mobx-state-tree")._NotCustomized, {
|
|
489
|
-
|
|
490
|
-
msa: string | undefined;
|
|
491
|
-
treeMetadata: string | undefined;
|
|
492
|
-
gff: string | undefined;
|
|
508
|
+
[k: string]: string | undefined;
|
|
493
509
|
}>, [undefined]>;
|
|
494
510
|
featureFilters: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").IMapType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>>, [undefined]>;
|
|
495
511
|
relativeTo: import("@jbrowse/mobx-state-tree").IMaybe<import("@jbrowse/mobx-state-tree").ISimpleType<string>>;
|
|
@@ -559,6 +575,7 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
|
|
|
559
575
|
highlightedColumns: number[] | undefined;
|
|
560
576
|
minimapHeight: number;
|
|
561
577
|
conservationTrackHeight: number;
|
|
578
|
+
sequenceLogoTrackHeight: number;
|
|
562
579
|
marginLeft: number;
|
|
563
580
|
error: unknown;
|
|
564
581
|
annotations: import("msa-parsers").Annotation[];
|
|
@@ -655,6 +672,7 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
|
|
|
655
672
|
}[];
|
|
656
673
|
readonly colClustalX: Record<string, string>[];
|
|
657
674
|
readonly conservation: number[];
|
|
675
|
+
readonly alphabetMaxBits: number;
|
|
658
676
|
readonly propertyConservation: number[];
|
|
659
677
|
readonly hierarchy: import("react-msaview").HierarchyNode<import("react-msaview").NodeWithIdsAndLength>;
|
|
660
678
|
readonly totalHeight: number;
|
|
@@ -669,6 +687,7 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
|
|
|
669
687
|
readonly dataInitialized: boolean;
|
|
670
688
|
readonly blocksX: number[];
|
|
671
689
|
readonly blocksY: number[];
|
|
690
|
+
readonly visibleMsaHeight: number;
|
|
672
691
|
} & {
|
|
673
692
|
readonly blocks2d: (readonly [number, number])[];
|
|
674
693
|
readonly isLoading: boolean;
|
|
@@ -704,6 +723,7 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
|
|
|
704
723
|
}): void;
|
|
705
724
|
} & {
|
|
706
725
|
readonly labelWidthMap: Map<string, number>;
|
|
726
|
+
readonly labelWidthScale: number;
|
|
707
727
|
readonly labelsWidth: number;
|
|
708
728
|
readonly secondaryStructureConsensus: string | undefined;
|
|
709
729
|
readonly seqConsensus: string | undefined;
|
|
@@ -770,6 +790,7 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
|
|
|
770
790
|
} & {
|
|
771
791
|
setHeaderHeight(arg: number): void;
|
|
772
792
|
setConservationTrackHeight(arg: number): void;
|
|
793
|
+
setSequenceLogoTrackHeight(arg: number): void;
|
|
773
794
|
reset(): void;
|
|
774
795
|
exportSVG(opts: {
|
|
775
796
|
theme: import("@mui/material").Theme;
|
|
@@ -18,6 +18,7 @@ function makeModel({ highlightColumns } = {}) {
|
|
|
18
18
|
const calls = [];
|
|
19
19
|
const model = {
|
|
20
20
|
querySeqName: 'query',
|
|
21
|
+
rows: [['query', 'MKVLTAEEK']],
|
|
21
22
|
connectedViewId: CONNECTED,
|
|
22
23
|
// g2p is indexed by genome coord; identity keeps the arithmetic out of the way
|
|
23
24
|
transcriptToMsaMap: {
|
|
@@ -195,6 +196,16 @@ describe('scope and redundant writes', () => {
|
|
|
195
196
|
run();
|
|
196
197
|
expect(calls).toEqual([]);
|
|
197
198
|
});
|
|
199
|
+
// seqPosToGlobalCol answers 0 for a row name it does not know, so without a
|
|
200
|
+
// guard a structure hover would light column 0 of whatever row is first
|
|
201
|
+
test('a query row this alignment does not have contributes no column', () => {
|
|
202
|
+
const { model, calls } = makeModel();
|
|
203
|
+
Object.assign(model, { rows: [['some_other_row', 'MKVLTAEEK']] });
|
|
204
|
+
const run = observeProteinHighlights(model);
|
|
205
|
+
session({ hover: [{ start: 10, end: 12 }] });
|
|
206
|
+
run();
|
|
207
|
+
expect(calls).toEqual([]);
|
|
208
|
+
});
|
|
198
209
|
test('nothing happens until the view is connected and mapped', () => {
|
|
199
210
|
const { calls } = makeModel();
|
|
200
211
|
const bare = {
|