jbrowse-plugin-msaview 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/blastLaunchView.js +6 -4
- package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -1
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +4 -2
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +8 -3
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +9 -0
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +20 -0
- package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.js +4 -2
- package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js +1 -1
- package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.js +4 -2
- package/dist/LaunchMsaView/components/SubmitCancelActions.d.ts +4 -1
- package/dist/LaunchMsaView/components/SubmitCancelActions.js +34 -10
- package/dist/LaunchMsaView/components/SubmitCancelActions.test.d.ts +1 -0
- package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +55 -0
- package/dist/LaunchMsaViewExtensionPoint/index.js +2 -2
- package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +7 -5
- package/dist/MsaViewPanel/doLaunchOrthologs.js +64 -30
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +105 -0
- package/dist/MsaViewPanel/model.d.ts +9 -0
- package/dist/MsaViewPanel/model.js +6 -0
- package/dist/index.js +4 -1
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +30 -30
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/launchMsaView.d.ts +19 -0
- package/dist/utils/launchMsaView.js +13 -0
- package/dist/utils/pantherOrthologs.d.ts +79 -0
- package/dist/utils/pantherOrthologs.js +262 -0
- package/dist/utils/workspaces.d.ts +34 -0
- package/dist/utils/workspaces.js +100 -0
- package/dist/utils/workspaces.test.d.ts +1 -0
- package/dist/utils/workspaces.test.js +100 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -1
- package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +1 -0
- package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -0
- package/src/LaunchMsaView/components/BlastQuery/blastLaunchView.ts +7 -4
- package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -0
- package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +5 -2
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +20 -4
- package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +52 -0
- package/src/LaunchMsaView/components/OrthologQuery/orthologLaunchView.ts +5 -2
- package/src/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.tsx +1 -0
- package/src/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.ts +5 -2
- package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +95 -0
- package/src/LaunchMsaView/components/SubmitCancelActions.tsx +80 -22
- package/src/LaunchMsaViewExtensionPoint/index.ts +21 -2
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +117 -0
- package/src/MsaViewPanel/doLaunchOrthologs.ts +99 -37
- package/src/MsaViewPanel/model.ts +10 -0
- package/src/index.ts +4 -1
- package/src/utils/launchMsaView.ts +30 -0
- package/src/utils/pantherOrthologs.ts +399 -0
- package/src/utils/workspaces.test.ts +132 -0
- package/src/utils/workspaces.ts +146 -0
- package/src/version.ts +1 -1
|
@@ -50,7 +50,7 @@ const BlastAutomaticPanel = observer(function ({ handleClose, feature, model, ch
|
|
|
50
50
|
React.createElement(Typography, null, "Previous BLAST Results")),
|
|
51
51
|
React.createElement(AccordionDetails, null,
|
|
52
52
|
React.createElement(CachedBlastResults, { model: model, handleClose: handleClose, feature: feature })))) : null),
|
|
53
|
-
React.createElement(SubmitCancelActions, { submitDisabled: !proteinSequence, onSubmit: () => {
|
|
53
|
+
React.createElement(SubmitCancelActions, { model: model, submitDisabled: !proteinSequence, onSubmit: () => {
|
|
54
54
|
try {
|
|
55
55
|
if (selectedTranscript) {
|
|
56
56
|
setLaunchViewError(undefined);
|
|
@@ -79,7 +79,7 @@ const BlastManualPanel = observer(function ({ handleClose, feature, model, child
|
|
|
79
79
|
setTreeText(event.target.value);
|
|
80
80
|
} }),
|
|
81
81
|
React.createElement(QueryRowSelector, { ...queryRow })))),
|
|
82
|
-
React.createElement(SubmitCancelActions, { submitDisabled: !selectedTranscript || !msaText.trim(), onSubmit: () => {
|
|
82
|
+
React.createElement(SubmitCancelActions, { model: model, submitDisabled: !selectedTranscript || !msaText.trim(), onSubmit: () => {
|
|
83
83
|
try {
|
|
84
84
|
if (selectedTranscript) {
|
|
85
85
|
setLaunchViewError(undefined);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util';
|
|
2
|
+
import { launchMsaView } from '../../../utils/launchMsaView';
|
|
3
|
+
import { readLaunchPlacement } from '../../../utils/workspaces';
|
|
2
4
|
export function blastLaunchView({ newViewTitle, view, feature, blastParams, }) {
|
|
3
|
-
getSession(view)
|
|
4
|
-
|
|
5
|
+
launchMsaView(getSession(view), {
|
|
6
|
+
placement: readLaunchPlacement(),
|
|
5
7
|
displayName: newViewTitle,
|
|
6
8
|
connectedViewId: view.id,
|
|
7
9
|
connectedFeature: feature.toJSON(),
|
|
@@ -12,8 +14,8 @@ export function blastLaunchView({ newViewTitle, view, feature, blastParams, }) {
|
|
|
12
14
|
});
|
|
13
15
|
}
|
|
14
16
|
export function blastLaunchViewFromCache({ newViewTitle, view, cached, connectedFeature, }) {
|
|
15
|
-
getSession(view)
|
|
16
|
-
|
|
17
|
+
launchMsaView(getSession(view), {
|
|
18
|
+
placement: readLaunchPlacement(),
|
|
17
19
|
displayName: newViewTitle,
|
|
18
20
|
connectedViewId: view.id,
|
|
19
21
|
connectedFeature,
|
|
@@ -58,7 +58,7 @@ const ManualMSALoader = observer(function PreLoadedMSA2({ model, feature, handle
|
|
|
58
58
|
} })))),
|
|
59
59
|
React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection }),
|
|
60
60
|
React.createElement(QueryRowSelector, { ...queryRow })),
|
|
61
|
-
React.createElement(SubmitCancelActions, { submitDisabled: !selectedTranscript ||
|
|
61
|
+
React.createElement(SubmitCancelActions, { model: model, submitDisabled: !selectedTranscript ||
|
|
62
62
|
(inputMethod === 'file' && !msaFileLocation) ||
|
|
63
63
|
(inputMethod === 'text' && !msaText.trim()), onSubmit: () => {
|
|
64
64
|
try {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util';
|
|
2
|
+
import { launchMsaView } from '../../../utils/launchMsaView';
|
|
3
|
+
import { readLaunchPlacement } from '../../../utils/workspaces';
|
|
2
4
|
export function launchView({ newViewTitle, view, feature, msaFilehandle, treeFilehandle, querySeqName, data, }) {
|
|
3
|
-
getSession(view)
|
|
4
|
-
|
|
5
|
+
launchMsaView(getSession(view), {
|
|
6
|
+
placement: readLaunchPlacement(),
|
|
5
7
|
displayName: newViewTitle,
|
|
6
8
|
connectedViewId: view.id,
|
|
7
9
|
connectedFeature: feature.toJSON(),
|
|
@@ -2,10 +2,12 @@ import React, { useMemo, useState } from 'react';
|
|
|
2
2
|
import { Typography } from '@mui/material';
|
|
3
3
|
import { observer } from 'mobx-react';
|
|
4
4
|
import { makeStyles } from 'tss-react/mui';
|
|
5
|
+
import OrthologSourceSelect, { ORTHOLOG_SOURCE_STORAGE_KEY, } from './OrthologSourceSelect';
|
|
5
6
|
import QuerySpeciesSelect from './QuerySpeciesSelect';
|
|
6
7
|
import { orthologLaunchView } from './orthologLaunchView';
|
|
7
8
|
import TextField2 from '../../../components/TextField2';
|
|
8
9
|
import { defaultMaxSpecies } from '../../../utils/ncbiOrthologs';
|
|
10
|
+
import { useLocalStorage } from '../../../utils/useLocalStorage';
|
|
9
11
|
import { getGeneDisplayName, getGeneIdentifiers, getLinearGenomeView, getTranscriptDisplayName, } from '../../util';
|
|
10
12
|
import MsaAlgorithmSelect from '../BlastQuery/MsaAlgorithmSelect';
|
|
11
13
|
import LaunchPanelContent from '../LaunchPanelContent';
|
|
@@ -22,6 +24,7 @@ const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
|
|
|
22
24
|
const view = getLinearGenomeView(model);
|
|
23
25
|
const [launchViewError, setLaunchViewError] = useState();
|
|
24
26
|
const [taxId, setTaxId] = useState(9606);
|
|
27
|
+
const [source, setSource] = useLocalStorage(ORTHOLOG_SOURCE_STORAGE_KEY, 'ncbi');
|
|
25
28
|
const [msaAlgorithm, setMsaAlgorithm] = useState('clustalo');
|
|
26
29
|
const [maxSpecies, setMaxSpecies] = useState(String(defaultMaxSpecies));
|
|
27
30
|
const geneCandidates = useMemo(() => getGeneIdentifiers(feature), [feature]);
|
|
@@ -32,15 +35,16 @@ const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
|
|
|
32
35
|
const rowCountValid = Number.isInteger(rowCount) && rowCount >= 2;
|
|
33
36
|
return (React.createElement(React.Fragment, null,
|
|
34
37
|
React.createElement(LaunchPanelContent, { error: e },
|
|
35
|
-
React.createElement(Typography, { variant: "body2" }, "
|
|
38
|
+
React.createElement(Typography, { variant: "body2" }, "Precomputed orthologs, one gene per species, looked up rather than searched for. No BLAST job to queue."),
|
|
36
39
|
React.createElement("div", null,
|
|
40
|
+
React.createElement(OrthologSourceSelect, { className: classes.selectField, value: source, onChange: setSource }),
|
|
37
41
|
React.createElement(QuerySpeciesSelect, { className: classes.selectField, value: taxId, assemblyName: view.assemblyNames[0], onChange: setTaxId }),
|
|
38
42
|
React.createElement(MsaAlgorithmSelect, { className: classes.selectField, value: msaAlgorithm, onChange: setMsaAlgorithm }),
|
|
39
43
|
React.createElement(TextField2, { variant: "outlined", label: "Rows to align", className: classes.selectField, type: "number", value: maxSpecies, onChange: event => {
|
|
40
44
|
setMaxSpecies(event.target.value);
|
|
41
|
-
}, error: !rowCountValid, helperText:
|
|
45
|
+
}, error: !rowCountValid, helperText: `the closest N species ${source === 'panther' ? 'PANTHER' : 'NCBI'} has` })),
|
|
42
46
|
React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection })),
|
|
43
|
-
React.createElement(SubmitCancelActions, { submitDisabled: !proteinSequence || !rowCountValid, onSubmit: () => {
|
|
47
|
+
React.createElement(SubmitCancelActions, { model: model, submitDisabled: !proteinSequence || !rowCountValid, onSubmit: () => {
|
|
44
48
|
try {
|
|
45
49
|
if (selectedTranscript) {
|
|
46
50
|
setLaunchViewError(undefined);
|
|
@@ -50,6 +54,7 @@ const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
|
|
|
50
54
|
newViewTitle: `Orthologs - ${getGeneDisplayName(feature)} - ${getTranscriptDisplayName(selectedTranscript)}`,
|
|
51
55
|
orthologParams: {
|
|
52
56
|
taxId,
|
|
57
|
+
source,
|
|
53
58
|
maxSpecies: rowCount,
|
|
54
59
|
geneCandidates,
|
|
55
60
|
msaAlgorithm,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { OrthologSource } from '../../../MsaViewPanel/model';
|
|
3
|
+
export declare const ORTHOLOG_SOURCE_STORAGE_KEY = "msaview-ortholog-source";
|
|
4
|
+
export declare const orthologSourceLabels: Record<OrthologSource, string>;
|
|
5
|
+
export default function OrthologSourceSelect({ value, onChange, className, }: {
|
|
6
|
+
value: OrthologSource;
|
|
7
|
+
onChange: (val: OrthologSource) => void;
|
|
8
|
+
className?: string;
|
|
9
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { MenuItem } from '@mui/material';
|
|
3
|
+
import TextField2 from '../../../components/TextField2';
|
|
4
|
+
export const ORTHOLOG_SOURCE_STORAGE_KEY = 'msaview-ortholog-source';
|
|
5
|
+
export const orthologSourceLabels = {
|
|
6
|
+
ncbi: 'NCBI orthologs',
|
|
7
|
+
panther: 'PANTHER',
|
|
8
|
+
};
|
|
9
|
+
// Which species a source can answer for, in the words a reader picking one
|
|
10
|
+
// needs: NCBI's ortholog sets stop at vertebrates and insects, PANTHER's run
|
|
11
|
+
// from human to yeast and Arabidopsis.
|
|
12
|
+
const hints = {
|
|
13
|
+
ncbi: 'vertebrates and insects',
|
|
14
|
+
panther: 'also yeast, worm, fly and plants',
|
|
15
|
+
};
|
|
16
|
+
export default function OrthologSourceSelect({ value, onChange, className, }) {
|
|
17
|
+
return (React.createElement(TextField2, { variant: "outlined", label: "Source", className: className, select: true, value: value, helperText: hints[value], onChange: event => {
|
|
18
|
+
onChange(event.target.value);
|
|
19
|
+
} }, Object.keys(orthologSourceLabels).map(val => (React.createElement(MenuItem, { value: val, key: val }, orthologSourceLabels[val])))));
|
|
20
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util';
|
|
2
|
+
import { launchMsaView } from '../../../utils/launchMsaView';
|
|
3
|
+
import { readLaunchPlacement } from '../../../utils/workspaces';
|
|
2
4
|
export function orthologLaunchView({ newViewTitle, view, feature, orthologParams, }) {
|
|
3
|
-
getSession(view)
|
|
4
|
-
|
|
5
|
+
launchMsaView(getSession(view), {
|
|
6
|
+
placement: readLaunchPlacement(),
|
|
5
7
|
displayName: newViewTitle,
|
|
6
8
|
connectedViewId: view.id,
|
|
7
9
|
connectedFeature: feature.toJSON(),
|
|
@@ -58,7 +58,7 @@ const PreLoadedMSA = observer(function ({ model, feature, handleClose, }) {
|
|
|
58
58
|
msaList ? (React.createElement("div", null,
|
|
59
59
|
React.createElement(SanitizedHTML, { html: selectedDataset.description }),
|
|
60
60
|
React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection }))) : null)) : null),
|
|
61
|
-
React.createElement(SubmitCancelActions, { submitDisabled: !selectedTranscript || !msaData?.length, onSubmit: () => {
|
|
61
|
+
React.createElement(SubmitCancelActions, { model: model, submitDisabled: !selectedTranscript || !msaData?.length, onSubmit: () => {
|
|
62
62
|
try {
|
|
63
63
|
if (selectedTranscript && msaData) {
|
|
64
64
|
const querySeqName = `${selectedId}_${assemblyNames[0]}`;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util';
|
|
2
|
+
import { launchMsaView } from '../../../utils/launchMsaView';
|
|
3
|
+
import { readLaunchPlacement } from '../../../utils/workspaces';
|
|
2
4
|
export function preCalculatedLaunchView({ newViewTitle, view, feature, data, querySeqName, }) {
|
|
3
|
-
getSession(view)
|
|
4
|
-
|
|
5
|
+
launchMsaView(getSession(view), {
|
|
6
|
+
placement: readLaunchPlacement(),
|
|
5
7
|
displayName: newViewTitle,
|
|
6
8
|
treeAreaWidth: 200,
|
|
7
9
|
querySeqName,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
|
|
2
|
+
import type { AbstractTrackModel } from '@jbrowse/core/util';
|
|
3
|
+
export default function SubmitCancelActions({ onSubmit, onCancel, submitDisabled, submitLabel, cancelLabel, model, }: {
|
|
3
4
|
onSubmit: () => void;
|
|
4
5
|
onCancel: () => void;
|
|
5
6
|
submitDisabled?: boolean;
|
|
6
7
|
submitLabel?: string;
|
|
7
8
|
cancelLabel?: string;
|
|
9
|
+
/** omitted by a panel that submits something other than a view launch */
|
|
10
|
+
model?: AbstractTrackModel;
|
|
8
11
|
}): React.JSX.Element;
|
|
@@ -1,11 +1,35 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { getSession } from '@jbrowse/core/util';
|
|
3
|
+
import { Button, Checkbox, DialogActions, FormControlLabel, } from '@mui/material';
|
|
4
|
+
import { readLaunchPlacement, sessionSupportsPlacement, writeLaunchPlacement, } from '../../utils/workspaces';
|
|
5
|
+
/**
|
|
6
|
+
* Where the launch puts the view, offered wherever a launch is submitted.
|
|
7
|
+
*
|
|
8
|
+
* A checkbox rather than a menu of the three placements: the choice a reader
|
|
9
|
+
* has at this point is "beside the genome view or under it", and `newTab` is a
|
|
10
|
+
* spec's to state, not a thing to pick before you have seen the alignment.
|
|
11
|
+
*
|
|
12
|
+
* Absent entirely on a host that cannot tile — an embedded session, or a
|
|
13
|
+
* release that places views its own way — because the box would do nothing
|
|
14
|
+
* there and every launch would quietly ignore it.
|
|
15
|
+
*/
|
|
16
|
+
function PlacementToggle({ model }) {
|
|
17
|
+
const session = getSession(model);
|
|
18
|
+
const [sideBySide, setSideBySide] = useState(() => readLaunchPlacement() === 'splitRight');
|
|
19
|
+
return sessionSupportsPlacement(session) ? (React.createElement(FormControlLabel, { label: "Open beside the genome view", control: React.createElement(Checkbox, { checked: sideBySide, onChange: event => {
|
|
20
|
+
const { checked } = event.target;
|
|
21
|
+
setSideBySide(checked);
|
|
22
|
+
writeLaunchPlacement(checked ? 'splitRight' : 'stack');
|
|
23
|
+
} }) })) : null;
|
|
24
|
+
}
|
|
25
|
+
export default function SubmitCancelActions({ onSubmit, onCancel, submitDisabled, submitLabel = 'Submit', cancelLabel = 'Cancel', model, }) {
|
|
26
|
+
return (React.createElement(DialogActions, { sx: { flexWrap: 'wrap', rowGap: 1 } },
|
|
27
|
+
model ? React.createElement(PlacementToggle, { model: model }) : null,
|
|
28
|
+
React.createElement("div", { style: { display: 'flex', gap: 8, marginLeft: 'auto' } },
|
|
29
|
+
React.createElement(Button, { sx: { flexShrink: 0 }, color: "primary", variant: "contained", disabled: submitDisabled, onClick: () => {
|
|
30
|
+
onSubmit();
|
|
31
|
+
} }, submitLabel),
|
|
32
|
+
React.createElement(Button, { sx: { flexShrink: 0 }, color: "secondary", variant: "contained", onClick: () => {
|
|
33
|
+
onCancel();
|
|
34
|
+
} }, cancelLabel))));
|
|
11
35
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { cleanup, render, screen } from '@testing-library/react';
|
|
4
|
+
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
|
5
|
+
import SubmitCancelActions from './SubmitCancelActions';
|
|
6
|
+
import { LAUNCH_PLACEMENT_KEY } from '../../utils/workspaces';
|
|
7
|
+
// getSession walks the MST tree, and this component only wants the two actions
|
|
8
|
+
// off the far end of that walk
|
|
9
|
+
vi.mock('@jbrowse/core/util', () => ({
|
|
10
|
+
getSession: (model) => model.session,
|
|
11
|
+
}));
|
|
12
|
+
function trackModel(session) {
|
|
13
|
+
return { session };
|
|
14
|
+
}
|
|
15
|
+
const tiling = { setUseWorkspaces() { }, setPendingMove() { } };
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
localStorage.clear();
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
cleanup();
|
|
21
|
+
});
|
|
22
|
+
function toggle() {
|
|
23
|
+
return screen.queryByRole('checkbox');
|
|
24
|
+
}
|
|
25
|
+
test('a host that can tile offers the choice, checked by default', () => {
|
|
26
|
+
render(React.createElement(SubmitCancelActions, { model: trackModel(tiling), onSubmit: () => { }, onCancel: () => { } }));
|
|
27
|
+
expect(toggle()).toBeTruthy();
|
|
28
|
+
expect(toggle().checked).toBe(true);
|
|
29
|
+
expect(localStorage.getItem(LAUNCH_PLACEMENT_KEY)).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
// the box would do nothing on an embedded session, and a control that silently
|
|
32
|
+
// does nothing is worse than one that is not there
|
|
33
|
+
test('a host that cannot tile does not offer it', () => {
|
|
34
|
+
render(React.createElement(SubmitCancelActions, { model: trackModel({}), onSubmit: () => { }, onCancel: () => { } }));
|
|
35
|
+
expect(toggle()).toBeNull();
|
|
36
|
+
expect(screen.getByText('Submit')).toBeTruthy();
|
|
37
|
+
});
|
|
38
|
+
test('a panel that launches nothing passes no model, and gets no box', () => {
|
|
39
|
+
render(React.createElement(SubmitCancelActions, { onSubmit: () => { }, onCancel: () => { } }));
|
|
40
|
+
expect(toggle()).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
test('clicking it writes the placement the next launch will read', () => {
|
|
43
|
+
render(React.createElement(SubmitCancelActions, { model: trackModel(tiling), onSubmit: () => { }, onCancel: () => { } }));
|
|
44
|
+
toggle().click();
|
|
45
|
+
expect(localStorage.getItem(LAUNCH_PLACEMENT_KEY)).toBe('stack');
|
|
46
|
+
expect(toggle().checked).toBe(false);
|
|
47
|
+
toggle().click();
|
|
48
|
+
expect(localStorage.getItem(LAUNCH_PLACEMENT_KEY)).toBe('splitRight');
|
|
49
|
+
expect(toggle().checked).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
test('a stored choice is what the box opens on', () => {
|
|
52
|
+
localStorage.setItem(LAUNCH_PLACEMENT_KEY, 'stack');
|
|
53
|
+
render(React.createElement(SubmitCancelActions, { model: trackModel(tiling), onSubmit: () => { }, onCancel: () => { } }));
|
|
54
|
+
expect(toggle().checked).toBe(false);
|
|
55
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { launchMsaView } from '../utils/launchMsaView';
|
|
1
2
|
export default function LaunchMsaViewExtensionPointF(pluginManager) {
|
|
2
3
|
pluginManager.addToExtensionPoint('LaunchView-MsaView', (args) => {
|
|
3
4
|
const { session, data, msaFileLocation, msaIndexedLocation, msaName, treeFileLocation, querySeqName, ...rest } = args;
|
|
@@ -14,8 +15,7 @@ export default function LaunchMsaViewExtensionPointF(pluginManager) {
|
|
|
14
15
|
// directly, and so is orthologParams (the model's own autorun picks it up).
|
|
15
16
|
// Only sources needing launch-time resolution go through `init`: msaUrl
|
|
16
17
|
// (AlphaFold sniff) and the name-indexed bgzip block (no native loader).
|
|
17
|
-
session
|
|
18
|
-
type: 'MsaView',
|
|
18
|
+
launchMsaView(session, {
|
|
19
19
|
...rest,
|
|
20
20
|
data,
|
|
21
21
|
...(treeFileLocation
|
|
@@ -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
|
|
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
|
2
2
|
import { doLaunchOrthologs } from './doLaunchOrthologs';
|
|
3
3
|
import { launchMSA } from '../utils/msa';
|
|
4
4
|
import { defaultMaxSpecies, fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils/ncbiOrthologs';
|
|
5
|
+
import { fetchPantherOrthologs } from '../utils/pantherOrthologs';
|
|
5
6
|
import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
6
7
|
// Every network call is mocked and nothing else is. What is under test is the
|
|
7
8
|
// argument shaping either side of those calls -- which species get asked for,
|
|
@@ -14,11 +15,15 @@ vi.mock('../utils/ncbiOrthologs', async (importOriginal) => ({
|
|
|
14
15
|
fetchProteinForGene: vi.fn(),
|
|
15
16
|
fetchOrthologRows: vi.fn(),
|
|
16
17
|
}));
|
|
18
|
+
vi.mock('../utils/pantherOrthologs', () => ({
|
|
19
|
+
fetchPantherOrthologs: vi.fn(),
|
|
20
|
+
}));
|
|
17
21
|
vi.mock('../utils/msa', () => ({ launchMSA: vi.fn() }));
|
|
18
22
|
vi.mock('../utils/taxonomyNames', () => ({ fetchTaxonomyInfo: vi.fn() }));
|
|
19
23
|
const mockResolveGeneId = vi.mocked(resolveGeneId);
|
|
20
24
|
const mockFetchProtein = vi.mocked(fetchProteinForGene);
|
|
21
25
|
const mockFetchRows = vi.mocked(fetchOrthologRows);
|
|
26
|
+
const mockFetchPanther = vi.mocked(fetchPantherOrthologs);
|
|
22
27
|
const mockLaunchMSA = vi.mocked(launchMSA);
|
|
23
28
|
const mockFetchTaxonomy = vi.mocked(fetchTaxonomyInfo);
|
|
24
29
|
const HUMAN = 9606;
|
|
@@ -199,3 +204,103 @@ describe('the Accession that drives the domain overlay', () => {
|
|
|
199
204
|
expect(queryMetadata(result).Accession).toBeUndefined();
|
|
200
205
|
});
|
|
201
206
|
});
|
|
207
|
+
// The second source. What is under test is the dispatch and what the PANTHER
|
|
208
|
+
// result becomes on the query row -- the rows themselves are shaped upstream,
|
|
209
|
+
// and the tail of the launch (labels, aligner, metadata) is the same code the
|
|
210
|
+
// NCBI tests above already cover.
|
|
211
|
+
describe('the PANTHER source', () => {
|
|
212
|
+
const YEAST = 559292;
|
|
213
|
+
const found = {
|
|
214
|
+
matched: 'CDC28',
|
|
215
|
+
query: {
|
|
216
|
+
code: 'YEAST',
|
|
217
|
+
accession: 'P00546',
|
|
218
|
+
geneRef: 'SGD=S000000364',
|
|
219
|
+
sequence: 'MSGELANYKRLEKVGEGTYGVVYKA',
|
|
220
|
+
},
|
|
221
|
+
rows: [
|
|
222
|
+
{
|
|
223
|
+
taxId: HUMAN,
|
|
224
|
+
label: 'human',
|
|
225
|
+
scientificName: 'Homo sapiens',
|
|
226
|
+
commonName: 'human',
|
|
227
|
+
geneId: 'HGNC=1771',
|
|
228
|
+
protein: 'P24941',
|
|
229
|
+
sequence: 'MENFQKVEKIGEGTYGVVYKARNK',
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
beforeEach(() => {
|
|
234
|
+
mockFetchPanther.mockResolvedValue(found);
|
|
235
|
+
mockFetchTaxonomy.mockResolvedValue(new Map([[YEAST, { sciname: 'Saccharomyces cerevisiae' }]]));
|
|
236
|
+
});
|
|
237
|
+
test('source omitted is NCBI, so an old launch never reaches PANTHER', async () => {
|
|
238
|
+
await doLaunchOrthologs({ self: makeModel(params()) });
|
|
239
|
+
expect(mockFetchPanther).not.toHaveBeenCalled();
|
|
240
|
+
expect(mockResolveGeneId).toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
test('source panther asks PANTHER with the same species semantics, and skips NCBI', async () => {
|
|
243
|
+
await doLaunchOrthologs({
|
|
244
|
+
self: makeModel({
|
|
245
|
+
taxId: YEAST,
|
|
246
|
+
source: 'panther',
|
|
247
|
+
geneCandidates: ['CDC28'],
|
|
248
|
+
msaAlgorithm: 'clustalo',
|
|
249
|
+
taxa: [HUMAN, YEAST],
|
|
250
|
+
maxSpecies: 7,
|
|
251
|
+
}),
|
|
252
|
+
});
|
|
253
|
+
expect(mockResolveGeneId).not.toHaveBeenCalled();
|
|
254
|
+
expect(mockFetchRows).not.toHaveBeenCalled();
|
|
255
|
+
const { candidates, taxId, taxa, exclude, limit } = mockFetchPanther.mock.calls[0][0];
|
|
256
|
+
expect(candidates).toEqual(['CDC28']);
|
|
257
|
+
expect(taxId).toBe(YEAST);
|
|
258
|
+
expect([...taxa]).toEqual([HUMAN, YEAST]);
|
|
259
|
+
expect(exclude).toBe(YEAST);
|
|
260
|
+
expect(limit).toBe(7);
|
|
261
|
+
});
|
|
262
|
+
test("the query row is PANTHER's own entry for the gene when no sequence was supplied, and carries its UniProt accession for the domain overlay", async () => {
|
|
263
|
+
const result = await doLaunchOrthologs({
|
|
264
|
+
self: makeModel({
|
|
265
|
+
taxId: YEAST,
|
|
266
|
+
source: 'panther',
|
|
267
|
+
geneCandidates: ['CDC28'],
|
|
268
|
+
msaAlgorithm: 'clustalo',
|
|
269
|
+
}),
|
|
270
|
+
});
|
|
271
|
+
expect(queryRowName()).toBe('Saccharomyces_cerevisiae_query');
|
|
272
|
+
expect(queryRowSent()).toBe(found.query.sequence);
|
|
273
|
+
expect(queryMetadata(result)).toEqual({
|
|
274
|
+
'Gene ID': 'SGD=S000000364',
|
|
275
|
+
Accession: 'P00546',
|
|
276
|
+
});
|
|
277
|
+
expect(JSON.parse(result.treeMetadata).human).toMatchObject({
|
|
278
|
+
Accession: 'P24941',
|
|
279
|
+
'Gene ID': 'HGNC=1771',
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
test('a supplied sequence still wins, and a different isoform earns no Accession', async () => {
|
|
283
|
+
const result = await doLaunchOrthologs({
|
|
284
|
+
self: makeModel({
|
|
285
|
+
taxId: YEAST,
|
|
286
|
+
source: 'panther',
|
|
287
|
+
geneCandidates: ['CDC28'],
|
|
288
|
+
msaAlgorithm: 'clustalo',
|
|
289
|
+
proteinSequence: 'MDIFFERENTISOFORM',
|
|
290
|
+
}),
|
|
291
|
+
});
|
|
292
|
+
expect(queryRowSent()).toBe('MDIFFERENTISOFORM');
|
|
293
|
+
expect(queryMetadata(result).Accession).toBeUndefined();
|
|
294
|
+
});
|
|
295
|
+
test('names PANTHER when it has no protein for the query row', async () => {
|
|
296
|
+
mockFetchPanther.mockResolvedValue({ ...found, query: undefined });
|
|
297
|
+
await expect(doLaunchOrthologs({
|
|
298
|
+
self: makeModel({
|
|
299
|
+
taxId: YEAST,
|
|
300
|
+
source: 'panther',
|
|
301
|
+
geneCandidates: ['CDC28'],
|
|
302
|
+
msaAlgorithm: 'clustalo',
|
|
303
|
+
}),
|
|
304
|
+
})).rejects.toThrow(/PANTHER returned no representative protein/);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
@@ -17,9 +17,18 @@ export interface BlastParams {
|
|
|
17
17
|
selectedTranscript?: Feature;
|
|
18
18
|
proteinSequence: string;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Where the ortholog set comes from. NCBI's sets cover vertebrates and
|
|
22
|
+
* insects; PANTHER's span its 144 reference proteomes, human to yeast to
|
|
23
|
+
* Arabidopsis, so a gene from outside NCBI's scope aligns only through it.
|
|
24
|
+
*/
|
|
25
|
+
export declare const orthologSources: readonly ["ncbi", "panther"];
|
|
26
|
+
export type OrthologSource = (typeof orthologSources)[number];
|
|
20
27
|
export interface OrthologParams {
|
|
21
28
|
/** NCBI taxon id of the assembly the query gene came from */
|
|
22
29
|
taxId: number;
|
|
30
|
+
/** `ncbi` when omitted, so every launch written before this key keeps its meaning */
|
|
31
|
+
source?: OrthologSource;
|
|
23
32
|
/**
|
|
24
33
|
* taxon ids to include as rows. The query taxon has its own row already, so
|
|
25
34
|
* it is excluded from this set whether or not it is named.
|