jbrowse-plugin-msaview 3.6.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/LaunchMsaView/cleanProteinSequence.test.d.ts +1 -0
- package/dist/LaunchMsaView/cleanProteinSequence.test.js +77 -0
- package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +29 -32
- package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +10 -15
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +14 -13
- package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +25 -22
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +14 -19
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +2 -1
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +18 -2
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.test.d.ts +1 -0
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.test.js +13 -0
- package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js +19 -14
- package/dist/LaunchMsaView/components/SubmitCancelActions.d.ts +3 -1
- package/dist/LaunchMsaView/components/SubmitCancelActions.js +10 -7
- package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +20 -2
- package/dist/LaunchMsaView/components/calculateProteinSequence.d.ts +8 -1
- package/dist/LaunchMsaView/components/calculateProteinSequence.js +25 -20
- package/dist/LaunchMsaView/components/launchConnectedView.d.ts +23 -0
- package/dist/LaunchMsaView/components/launchConnectedView.js +39 -0
- package/dist/LaunchMsaView/detectQueryRow.js +4 -7
- package/dist/LaunchMsaView/util.d.ts +6 -0
- package/dist/LaunchMsaView/util.js +7 -1
- package/dist/LaunchMsaViewExtensionPoint/index.test.js +15 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.js +12 -3
- package/dist/MsaViewPanel/applyRegion.d.ts +23 -0
- package/dist/MsaViewPanel/applyRegion.js +19 -0
- package/dist/MsaViewPanel/applyRegion.test.d.ts +1 -0
- package/dist/MsaViewPanel/applyRegion.test.js +45 -0
- package/dist/MsaViewPanel/doLaunchBlast.js +2 -1
- package/dist/MsaViewPanel/doLaunchBlast.test.js +12 -0
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +1 -1
- package/dist/MsaViewPanel/genomeToMSA.js +3 -3
- package/dist/MsaViewPanel/genomeToMSA.test.js +69 -23
- package/dist/MsaViewPanel/launchAutoruns.test.d.ts +1 -0
- package/dist/MsaViewPanel/launchAutoruns.test.js +36 -0
- package/dist/MsaViewPanel/model.d.ts +18 -14
- package/dist/MsaViewPanel/model.js +17 -0
- package/dist/MsaViewPanel/msaDataStore.d.ts +5 -3
- package/dist/MsaViewPanel/msaDataStore.js +3 -12
- package/dist/MsaViewPanel/msaDataStore.test.js +8 -1
- package/dist/MsaViewPanel/observeProteinHighlights.test.js +4 -4
- package/dist/MsaViewPanel/regionSnapshot.test.d.ts +1 -0
- package/dist/MsaViewPanel/regionSnapshot.test.js +29 -0
- package/dist/MsaViewPanel/resolveConnectedTranscript.d.ts +41 -4
- package/dist/MsaViewPanel/resolveConnectedTranscript.js +103 -68
- package/dist/MsaViewPanel/resolveConnectedTranscript.test.d.ts +1 -0
- package/dist/MsaViewPanel/resolveConnectedTranscript.test.js +149 -0
- package/dist/MsaViewPanel/runLaunch.d.ts +8 -4
- package/dist/MsaViewPanel/runLaunch.js +15 -4
- package/dist/MsaViewPanel/runLaunch.test.js +16 -0
- package/dist/MsaViewPanel/storedData.test.js +23 -0
- package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +3 -2
- package/dist/MsaViewPanel/util.d.ts +23 -23
- package/dist/MsaViewPanel/util.js +23 -24
- package/dist/index.js +17 -10
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +36 -36
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.d.ts +26 -2
- package/dist/utils/blastCache.js +51 -39
- package/dist/utils/blastCache.test.js +46 -19
- package/dist/utils/domainCache.d.ts +1 -1
- package/dist/utils/domainCache.js +18 -14
- package/dist/utils/ebiJobDispatcher.d.ts +11 -0
- package/dist/utils/ebiJobDispatcher.js +12 -13
- package/dist/utils/ebiJobDispatcher.test.js +17 -3
- package/dist/utils/eutils.d.ts +14 -0
- package/dist/utils/eutils.js +48 -14
- package/dist/utils/eutils.test.d.ts +1 -0
- package/dist/utils/eutils.test.js +31 -0
- package/dist/utils/idb.d.ts +16 -3
- package/dist/utils/idb.js +51 -5
- package/dist/utils/idb.test.d.ts +1 -0
- package/dist/utils/idb.test.js +78 -0
- package/dist/utils/ncbiDomains.js +4 -4
- package/dist/utils/ncbiOrthologs.d.ts +1 -1
- package/dist/utils/ncbiOrthologs.js +18 -17
- package/dist/utils/ncbiOrthologs.test.js +17 -1
- package/dist/utils/ncbiTaxonomy.js +14 -15
- package/dist/utils/taxonomyNames.js +23 -19
- package/dist/utils/taxonomyNames.test.d.ts +1 -0
- package/dist/utils/taxonomyNames.test.js +33 -0
- package/dist/utils/unirefHomologs.js +2 -2
- package/dist/utils/unirefHomologs.test.js +8 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -9
- package/src/LaunchMsaView/cleanProteinSequence.test.ts +91 -0
- package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +35 -32
- package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +10 -14
- package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +16 -15
- package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +34 -21
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +18 -21
- package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.test.ts +16 -0
- package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +24 -2
- package/src/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.tsx +19 -13
- package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +27 -2
- package/src/LaunchMsaView/components/SubmitCancelActions.tsx +12 -7
- package/src/LaunchMsaView/components/calculateProteinSequence.ts +28 -18
- package/src/LaunchMsaView/components/launchConnectedView.ts +56 -0
- package/src/LaunchMsaView/detectQueryRow.ts +5 -7
- package/src/LaunchMsaView/util.ts +7 -1
- package/src/LaunchMsaViewExtensionPoint/index.test.ts +16 -0
- package/src/LaunchMsaViewExtensionPoint/index.ts +28 -1
- package/src/MsaViewPanel/afterCreateAutoruns.ts +12 -3
- package/src/MsaViewPanel/applyRegion.test.ts +55 -0
- package/src/MsaViewPanel/applyRegion.ts +33 -0
- package/src/MsaViewPanel/doLaunchBlast.test.ts +20 -1
- package/src/MsaViewPanel/doLaunchBlast.ts +2 -1
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +1 -1
- package/src/MsaViewPanel/genomeToMSA.test.ts +75 -23
- package/src/MsaViewPanel/genomeToMSA.ts +3 -2
- package/src/MsaViewPanel/launchAutoruns.test.ts +45 -0
- package/src/MsaViewPanel/model.ts +22 -0
- package/src/MsaViewPanel/msaDataStore.test.ts +14 -2
- package/src/MsaViewPanel/msaDataStore.ts +4 -12
- package/src/MsaViewPanel/observeProteinHighlights.test.ts +4 -4
- package/src/MsaViewPanel/regionSnapshot.test.ts +34 -0
- package/src/MsaViewPanel/resolveConnectedTranscript.test.ts +183 -0
- package/src/MsaViewPanel/resolveConnectedTranscript.ts +154 -71
- package/src/MsaViewPanel/runLaunch.test.ts +19 -0
- package/src/MsaViewPanel/runLaunch.ts +21 -4
- package/src/MsaViewPanel/storedData.test.ts +32 -0
- package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +3 -2
- package/src/MsaViewPanel/util.ts +38 -29
- package/src/index.ts +19 -10
- package/src/utils/blastCache.test.ts +59 -20
- package/src/utils/blastCache.ts +62 -39
- package/src/utils/domainCache.ts +27 -15
- package/src/utils/ebiJobDispatcher.test.ts +18 -3
- package/src/utils/ebiJobDispatcher.ts +12 -16
- package/src/utils/eutils.test.ts +39 -0
- package/src/utils/eutils.ts +64 -14
- package/src/utils/idb.test.ts +106 -0
- package/src/utils/idb.ts +57 -5
- package/src/utils/ncbiDomains.ts +4 -4
- package/src/utils/ncbiOrthologs.test.ts +20 -0
- package/src/utils/ncbiOrthologs.ts +26 -28
- package/src/utils/ncbiTaxonomy.ts +17 -20
- package/src/utils/taxonomyNames.test.ts +41 -0
- package/src/utils/taxonomyNames.ts +33 -19
- package/src/utils/unirefHomologs.test.ts +11 -1
- package/src/utils/unirefHomologs.ts +2 -2
- package/src/version.ts +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/blastLaunchView.d.ts +0 -16
- package/dist/LaunchMsaView/components/BlastQuery/blastLaunchView.js +0 -31
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.d.ts +0 -16
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +0 -16
- package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.d.ts +0 -9
- package/dist/LaunchMsaView/components/OrthologQuery/orthologLaunchView.js +0 -15
- package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.d.ts +0 -13
- package/dist/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.js +0 -22
- package/src/LaunchMsaView/components/BlastQuery/blastLaunchView.ts +0 -59
- package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +0 -43
- package/src/LaunchMsaView/components/OrthologQuery/orthologLaunchView.ts +0 -31
- package/src/LaunchMsaView/components/PreLoadedMSA/preCalculatedLaunchView.ts +0 -42
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { getSession } from '@jbrowse/core/util';
|
|
3
3
|
import { Button, Checkbox, DialogActions, FormControlLabel, } from '@mui/material';
|
|
4
|
-
import { sessionSupportsPlacement, writeLaunchPlacement, } from '../../utils/workspaces';
|
|
4
|
+
import { readLaunchPlacement, sessionSupportsPlacement, writeLaunchPlacement, } from '../../utils/workspaces';
|
|
5
5
|
import { useLaunchPlacement } from './launchPlacement';
|
|
6
6
|
/**
|
|
7
7
|
* Where the launch puts the view, offered wherever a launch is submitted.
|
|
@@ -21,10 +21,9 @@ function PlacementToggle({ checked, onChange, }) {
|
|
|
21
21
|
}
|
|
22
22
|
export default function SubmitCancelActions({ onSubmit, onCancel, submitDisabled, hint, submitLabel = 'Submit', cancelLabel = 'Cancel', model, }) {
|
|
23
23
|
const [sideBySide, setSideBySide] = useLaunchPlacement();
|
|
24
|
-
// The stored value is what the next
|
|
25
|
-
// rather than on the click:
|
|
26
|
-
//
|
|
27
|
-
// of.
|
|
24
|
+
// The stored value is what the next dialog opens on, so it is written on
|
|
25
|
+
// submit rather than on the click: a dialog the user backed out of must not
|
|
26
|
+
// move where every future launch lands.
|
|
28
27
|
const offerPlacement = !!model && sessionSupportsPlacement(getSession(model));
|
|
29
28
|
return (React.createElement(DialogActions, { sx: { flexWrap: 'wrap', rowGap: 1 } },
|
|
30
29
|
offerPlacement ? (React.createElement(PlacementToggle, { checked: sideBySide, onChange: setSideBySide })) : null,
|
|
@@ -37,9 +36,13 @@ export default function SubmitCancelActions({ onSubmit, onCancel, submitDisabled
|
|
|
37
36
|
hint,
|
|
38
37
|
React.createElement(Button, { sx: { flexShrink: 0 }, color: "primary", variant: "contained", disabled: submitDisabled, onClick: () => {
|
|
39
38
|
if (offerPlacement) {
|
|
40
|
-
|
|
39
|
+
const placement = sideBySide ? 'splitRight' : 'stack';
|
|
40
|
+
writeLaunchPlacement(placement);
|
|
41
|
+
onSubmit(placement);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
onSubmit(readLaunchPlacement());
|
|
41
45
|
}
|
|
42
|
-
onSubmit();
|
|
43
46
|
} }, submitLabel),
|
|
44
47
|
React.createElement(Button, { sx: { flexShrink: 0 }, color: "secondary", variant: "contained", onClick: () => {
|
|
45
48
|
onCancel();
|
|
@@ -40,13 +40,31 @@ test('a panel that launches nothing passes no model, and gets no box', () => {
|
|
|
40
40
|
render(React.createElement(SubmitCancelActions, { onSubmit: () => { }, onCancel: () => { } }));
|
|
41
41
|
expect(toggle()).toBeNull();
|
|
42
42
|
});
|
|
43
|
-
test('submitting
|
|
44
|
-
|
|
43
|
+
test('submitting hands the launch the placement and remembers it', () => {
|
|
44
|
+
const onSubmit = vi.fn();
|
|
45
|
+
render(React.createElement(SubmitCancelActions, { model: trackModel(tiling), onSubmit: onSubmit, onCancel: () => { } }));
|
|
45
46
|
toggle().click();
|
|
46
47
|
expect(toggle().checked).toBe(false);
|
|
47
48
|
screen.getByText('Submit').click();
|
|
49
|
+
expect(onSubmit).toHaveBeenCalledWith('stack');
|
|
48
50
|
expect(localStorage.getItem(LAUNCH_PLACEMENT_KEY)).toBe('stack');
|
|
49
51
|
});
|
|
52
|
+
// an unticked box used to reach the launch only through storage, so a browser
|
|
53
|
+
// refusing localStorage launched side by side anyway
|
|
54
|
+
test('the box decides the launch even where storage throws', () => {
|
|
55
|
+
const onSubmit = vi.fn();
|
|
56
|
+
const setItem = vi
|
|
57
|
+
.spyOn(Storage.prototype, 'setItem')
|
|
58
|
+
.mockImplementation(() => {
|
|
59
|
+
throw new Error('SecurityError');
|
|
60
|
+
});
|
|
61
|
+
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
62
|
+
render(React.createElement(SubmitCancelActions, { model: trackModel(tiling), onSubmit: onSubmit, onCancel: () => { } }));
|
|
63
|
+
toggle().click();
|
|
64
|
+
screen.getByText('Submit').click();
|
|
65
|
+
expect(onSubmit).toHaveBeenCalledWith('stack');
|
|
66
|
+
setItem.mockRestore();
|
|
67
|
+
});
|
|
50
68
|
// the box is a property of this launch until it is launched; a dialog the user
|
|
51
69
|
// backed out of should not have moved where every future one lands
|
|
52
70
|
test('cancelling leaves the stored placement alone', () => {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Feat } from './types';
|
|
2
2
|
import type { Feature } from '@jbrowse/core/util';
|
|
3
|
-
|
|
3
|
+
import type { TranslExcept } from '@jbrowse/core/util/geneticCodes';
|
|
4
|
+
export declare function calculateProteinSequence({ cds, sequence, geneticCodeId, translExcept, }: {
|
|
4
5
|
cds: Feat[];
|
|
5
6
|
sequence: string;
|
|
6
7
|
geneticCodeId?: number;
|
|
8
|
+
translExcept?: TranslExcept[];
|
|
7
9
|
}): string;
|
|
8
10
|
export declare function revlist(list: Feat[], seqlen: number): {
|
|
9
11
|
start: number;
|
|
@@ -11,6 +13,11 @@ export declare function revlist(list: Feat[], seqlen: number): {
|
|
|
11
13
|
type?: string;
|
|
12
14
|
phase?: number;
|
|
13
15
|
}[];
|
|
16
|
+
/**
|
|
17
|
+
* The translation core's own feature panel shows: the contig's or the
|
|
18
|
+
* feature's genetic code, its alternative initiators, and any `transl_except`
|
|
19
|
+
* (RefSeq's selenocysteines), read off the transcript or its CDS as core does.
|
|
20
|
+
*/
|
|
14
21
|
export declare function getProteinSequenceFromFeature({ feature, seq, assemblyGeneticCodeId, }: {
|
|
15
22
|
seq: string;
|
|
16
23
|
feature: Feature;
|
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
import { revcom } from '@jbrowse/core/util';
|
|
2
2
|
import { convertCodingSequenceToPeptides } from '@jbrowse/core/util/convertCodingSequenceToPeptides';
|
|
3
|
-
import { getGeneticCode, parseTranslTable, } from '@jbrowse/core/util/geneticCodes';
|
|
3
|
+
import { getGeneticCode, parseTranslTable, relativizeTranslExcept, } from '@jbrowse/core/util/geneticCodes';
|
|
4
4
|
// `@jbrowse/core/util/convertCodingSequenceToPeptides` and
|
|
5
|
-
// `@jbrowse/core/util/geneticCodes` are deep paths, so
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// global was never assigned and PluginLoader error-paged the whole app.
|
|
13
|
-
export function calculateProteinSequence({ cds, sequence, geneticCodeId, }) {
|
|
14
|
-
// `starts` is deliberately not passed: @jbrowse/core 4.3.0's signature has no
|
|
15
|
-
// such parameter, so alternative initiators (GTG under table 11, ATA under
|
|
16
|
-
// table 2) render as their internal residue rather than M. Core main added it;
|
|
17
|
-
// pass it here when msaview's @jbrowse/core floor reaches that release.
|
|
18
|
-
const { codonTable } = getGeneticCode(geneticCodeId);
|
|
5
|
+
// `@jbrowse/core/util/geneticCodes` are deep paths absent from ReExports, so
|
|
6
|
+
// they are bundled rather than resolved out of the host's JBrowseExports: every
|
|
7
|
+
// host runs the version this build installs. Importing translation from the
|
|
8
|
+
// `@jbrowse/core/util` barrel instead is what error-paged the whole app when a
|
|
9
|
+
// core build dropped `defaultCodonTable`.
|
|
10
|
+
export function calculateProteinSequence({ cds, sequence, geneticCodeId, translExcept, }) {
|
|
11
|
+
const { codonTable, starts } = getGeneticCode(geneticCodeId);
|
|
19
12
|
return convertCodingSequenceToPeptides({
|
|
20
13
|
cds,
|
|
21
14
|
sequence,
|
|
22
15
|
codonTable,
|
|
16
|
+
starts,
|
|
17
|
+
translExcept,
|
|
23
18
|
});
|
|
24
19
|
}
|
|
25
20
|
// The CDS list is sorted by start, so adjacent comparison is the whole job.
|
|
@@ -38,8 +33,13 @@ export function revlist(list, seqlen) {
|
|
|
38
33
|
}))
|
|
39
34
|
.toSorted((a, b) => a.start - b.start);
|
|
40
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* The translation core's own feature panel shows: the contig's or the
|
|
38
|
+
* feature's genetic code, its alternative initiators, and any `transl_except`
|
|
39
|
+
* (RefSeq's selenocysteines), read off the transcript or its CDS as core does.
|
|
40
|
+
*/
|
|
41
41
|
export function getProteinSequenceFromFeature({ feature, seq, assemblyGeneticCodeId, }) {
|
|
42
|
-
const { subfeatures, start, strand } = feature.toJSON();
|
|
42
|
+
const { subfeatures, start, end, strand } = feature.toJSON();
|
|
43
43
|
const cds = dedupe(subfeatures
|
|
44
44
|
?.toSorted((a, b) => a.start - b.start)
|
|
45
45
|
.map(sub => ({
|
|
@@ -48,19 +48,24 @@ export function getProteinSequenceFromFeature({ feature, seq, assemblyGeneticCod
|
|
|
48
48
|
end: sub.end - start,
|
|
49
49
|
}))
|
|
50
50
|
.filter(subfeature => subfeature.type === 'CDS') ?? []);
|
|
51
|
-
// RefSeq declares transl_table=2 on a mitochondrial CDS, usually on the CDS
|
|
52
|
-
// rather than the transcript. GENCODE and UCSC declare nothing, so without
|
|
53
|
-
// the assembly's code all 13 human mitochondrial proteins read TGA as a stop
|
|
54
|
-
// and ATA as I.
|
|
55
51
|
const cdsSubfeature = feature
|
|
56
52
|
.get('subfeatures')
|
|
57
53
|
?.find((f) => f.get('type')?.toLowerCase() === 'cds');
|
|
58
54
|
const geneticCodeId = parseTranslTable(feature.get('transl_table')) ??
|
|
59
55
|
parseTranslTable(cdsSubfeature?.get('transl_table')) ??
|
|
60
56
|
assemblyGeneticCodeId;
|
|
57
|
+
const rawTranslExcept = feature.get('transl_except') ?? cdsSubfeature?.get('transl_except');
|
|
61
58
|
return calculateProteinSequence({
|
|
62
59
|
cds: strand === -1 ? revlist(cds, seq.length) : cds,
|
|
63
60
|
sequence: strand === -1 ? revcom(seq) : seq,
|
|
64
61
|
geneticCodeId,
|
|
62
|
+
translExcept: rawTranslExcept
|
|
63
|
+
? relativizeTranslExcept({
|
|
64
|
+
raw: rawTranslExcept,
|
|
65
|
+
featureStart: start,
|
|
66
|
+
featureLength: end - start,
|
|
67
|
+
strand,
|
|
68
|
+
})
|
|
69
|
+
: undefined,
|
|
65
70
|
});
|
|
66
71
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MsaViewPlacement } from '../../utils/workspaces';
|
|
2
|
+
import type { Feature } from '@jbrowse/core/util';
|
|
3
|
+
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view';
|
|
4
|
+
/** how a view whose alignment the plugin builds opens */
|
|
5
|
+
export declare const builtAlignmentLook: {
|
|
6
|
+
drawNodeBubbles: boolean;
|
|
7
|
+
colWidth: number;
|
|
8
|
+
rowHeight: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Every dialog launch: an MSA view tied to the genome view it came from, and
|
|
12
|
+
* through `feature` to the transcript whose codons its query row is read by.
|
|
13
|
+
*/
|
|
14
|
+
export declare function launchConnectedView({ view, feature, placement, ...snapshot }: {
|
|
15
|
+
view: LinearGenomeViewModel;
|
|
16
|
+
feature?: Feature;
|
|
17
|
+
placement: MsaViewPlacement;
|
|
18
|
+
} & Record<string, unknown>): void;
|
|
19
|
+
/** runs a panel's launch, closing the dialog on success and keeping the error */
|
|
20
|
+
export declare function useLaunchSubmit(handleClose: () => void): {
|
|
21
|
+
launchError: unknown;
|
|
22
|
+
submit: (launch: () => void) => void;
|
|
23
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { getSession } from '@jbrowse/core/util';
|
|
3
|
+
import { launchMsaView } from '../../utils/launchMsaView';
|
|
4
|
+
/** how a view whose alignment the plugin builds opens */
|
|
5
|
+
export const builtAlignmentLook = {
|
|
6
|
+
drawNodeBubbles: true,
|
|
7
|
+
colWidth: 10,
|
|
8
|
+
rowHeight: 12,
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Every dialog launch: an MSA view tied to the genome view it came from, and
|
|
12
|
+
* through `feature` to the transcript whose codons its query row is read by.
|
|
13
|
+
*/
|
|
14
|
+
export function launchConnectedView({ view, feature, placement, ...snapshot }) {
|
|
15
|
+
launchMsaView(getSession(view), {
|
|
16
|
+
placement,
|
|
17
|
+
connectedViewId: view.id,
|
|
18
|
+
connectedFeature: feature?.toJSON(),
|
|
19
|
+
...snapshot,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** runs a panel's launch, closing the dialog on success and keeping the error */
|
|
23
|
+
export function useLaunchSubmit(handleClose) {
|
|
24
|
+
const [launchError, setLaunchError] = useState();
|
|
25
|
+
return {
|
|
26
|
+
launchError,
|
|
27
|
+
submit: (launch) => {
|
|
28
|
+
try {
|
|
29
|
+
setLaunchError(undefined);
|
|
30
|
+
launch();
|
|
31
|
+
handleClose();
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
console.error(e);
|
|
35
|
+
setLaunchError(e);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { getUngappedSequence, parseMSA } from 'msa-parsers';
|
|
2
|
+
import { cleanProteinSequence } from './util';
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
4
|
+
* The translation as a search launch would have sent it, so a row that launch
|
|
5
|
+
* built matches exactly; case means nothing on either side.
|
|
5
6
|
*/
|
|
6
7
|
function normalize(seq) {
|
|
7
|
-
return seq
|
|
8
|
-
.replaceAll('*', '')
|
|
9
|
-
.replaceAll('-', '')
|
|
10
|
-
.replaceAll('.', '')
|
|
11
|
-
.toUpperCase();
|
|
8
|
+
return cleanProteinSequence(seq).toUpperCase();
|
|
12
9
|
}
|
|
13
10
|
function identityOverOverlap(a, b) {
|
|
14
11
|
const len = Math.min(a.length, b.length);
|
|
@@ -13,5 +13,11 @@ export declare function getTranscriptDisplayName(val?: Feature): string;
|
|
|
13
13
|
export declare function getGeneDisplayName(val?: Feature): string;
|
|
14
14
|
export declare function getBlastViewTitle(feature: Feature, transcript: Feature): string;
|
|
15
15
|
export declare function getSortedTranscriptFeatures(feature: Feature): Feature[];
|
|
16
|
+
/**
|
|
17
|
+
* A translation as a query, still one residue per codon: g2p numbers codons,
|
|
18
|
+
* and the query row is read through it. So the partial first codon (`&`), an
|
|
19
|
+
* internal stop and an unreadable codon become X, and only what trails the
|
|
20
|
+
* last whole codon is dropped.
|
|
21
|
+
*/
|
|
16
22
|
export declare function cleanProteinSequence(seq: string): string;
|
|
17
23
|
export declare function getGeneIdentifiers(feature: Feature): string[];
|
|
@@ -70,8 +70,14 @@ export function getSortedTranscriptFeatures(feature) {
|
|
|
70
70
|
const transcripts = getTranscriptFeatures(feature);
|
|
71
71
|
return transcripts.toSorted((a, b) => getTranscriptLength(b).len - getTranscriptLength(a).len);
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* A translation as a query, still one residue per codon: g2p numbers codons,
|
|
75
|
+
* and the query row is read through it. So the partial first codon (`&`), an
|
|
76
|
+
* internal stop and an unreadable codon become X, and only what trails the
|
|
77
|
+
* last whole codon is dropped.
|
|
78
|
+
*/
|
|
73
79
|
export function cleanProteinSequence(seq) {
|
|
74
|
-
return seq.
|
|
80
|
+
return seq.replace(/[*&]+$/, '').replaceAll(/[*&]/g, 'X');
|
|
75
81
|
}
|
|
76
82
|
export function getGeneIdentifiers(feature) {
|
|
77
83
|
return uniqueDefined([
|
|
@@ -53,6 +53,21 @@ test('searchParams is a source, stored under the name the dialog uses', () => {
|
|
|
53
53
|
test('a launch naming no source at all is refused', () => {
|
|
54
54
|
expect(() => launch({ connectedViewId: 'lgv1' })).toThrow(/searchParams/);
|
|
55
55
|
});
|
|
56
|
+
test('region and the data-layer keys reach the view as snapshot properties', () => {
|
|
57
|
+
const region = { row: 'Human', start: 245, end: 249 };
|
|
58
|
+
const clades = [{ mrca: ['Human', 'Mouse'], tips: 2, mark: 'bracket' }];
|
|
59
|
+
const snapshot = launch({
|
|
60
|
+
data: { msa: '>Human\nMEEP' },
|
|
61
|
+
region,
|
|
62
|
+
clades,
|
|
63
|
+
relativeTo: 'Human',
|
|
64
|
+
gffFilehandle: { uri: 'http://example.com/a.gff' },
|
|
65
|
+
});
|
|
66
|
+
expect(snapshot.region).toEqual(region);
|
|
67
|
+
expect(snapshot.clades).toEqual(clades);
|
|
68
|
+
expect(snapshot.relativeTo).toBe('Human');
|
|
69
|
+
expect(snapshot.gffFilehandle).toEqual({ uri: 'http://example.com/a.gff' });
|
|
70
|
+
});
|
|
56
71
|
test('one field set is enough to need init', () => {
|
|
57
72
|
const snapshot = launch({ data: { msa: '>a\nMEEP' }, querySeqName: 'QUERY' });
|
|
58
73
|
expect(snapshot.init).toEqual({
|
|
@@ -32,6 +32,9 @@ export function loadStoredData(self) {
|
|
|
32
32
|
if (storedData.treeMetadata) {
|
|
33
33
|
self.setTreeMetadata(storedData.treeMetadata);
|
|
34
34
|
}
|
|
35
|
+
if (storedData.gff) {
|
|
36
|
+
self.setGFF(storedData.gff);
|
|
37
|
+
}
|
|
35
38
|
self.setLastStoredData(currentData(self));
|
|
36
39
|
});
|
|
37
40
|
}
|
|
@@ -59,15 +62,21 @@ export function loadStoredData(self) {
|
|
|
59
62
|
})();
|
|
60
63
|
}
|
|
61
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Everything react-msaview drops from the snapshot once it passes 50kB. The
|
|
67
|
+
* GFF is among them: a local one's filehandle is cleared once it loads, so
|
|
68
|
+
* this row is the only copy.
|
|
69
|
+
*/
|
|
62
70
|
function currentData(self) {
|
|
63
|
-
const { msa, tree, treeMetadata } = self.data;
|
|
64
|
-
return { msa, tree, treeMetadata };
|
|
71
|
+
const { msa, tree, treeMetadata, gff } = self.data;
|
|
72
|
+
return { msa, tree, treeMetadata, gff };
|
|
65
73
|
}
|
|
66
74
|
function sameData(a, b) {
|
|
67
75
|
return (!!a &&
|
|
68
76
|
a.msa === b.msa &&
|
|
69
77
|
a.tree === b.tree &&
|
|
70
|
-
a.treeMetadata === b.treeMetadata
|
|
78
|
+
a.treeMetadata === b.treeMetadata &&
|
|
79
|
+
a.gff === b.gff);
|
|
71
80
|
}
|
|
72
81
|
/**
|
|
73
82
|
* Keep IndexedDB holding what the view holds. The first run writes a new row and
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Region } from 'react-msaview';
|
|
2
|
+
interface RegionModel {
|
|
3
|
+
region?: Region;
|
|
4
|
+
viewInitialized: boolean;
|
|
5
|
+
numColumns: number;
|
|
6
|
+
treeFilehandle?: unknown;
|
|
7
|
+
data: {
|
|
8
|
+
tree?: string;
|
|
9
|
+
};
|
|
10
|
+
zoomToRegion: (region: Region) => void;
|
|
11
|
+
setRegion: (region?: Region) => void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Zoom onto the launch's `region` once, then forget it, so a reloaded session
|
|
15
|
+
* opens where the reader left it rather than back on the region.
|
|
16
|
+
*
|
|
17
|
+
* zoomToRegion is a no-op until the view has a width, and it resolves residues
|
|
18
|
+
* through the visible columns, so it waits for the alignment and for a tree
|
|
19
|
+
* file too: a clade the tree collapses hides columns and would move the
|
|
20
|
+
* target after the zoom.
|
|
21
|
+
*/
|
|
22
|
+
export declare function applyRegion(self: RegionModel): void;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zoom onto the launch's `region` once, then forget it, so a reloaded session
|
|
3
|
+
* opens where the reader left it rather than back on the region.
|
|
4
|
+
*
|
|
5
|
+
* zoomToRegion is a no-op until the view has a width, and it resolves residues
|
|
6
|
+
* through the visible columns, so it waits for the alignment and for a tree
|
|
7
|
+
* file too: a clade the tree collapses hides columns and would move the
|
|
8
|
+
* target after the zoom.
|
|
9
|
+
*/
|
|
10
|
+
export function applyRegion(self) {
|
|
11
|
+
const { region } = self;
|
|
12
|
+
if (region &&
|
|
13
|
+
self.viewInitialized &&
|
|
14
|
+
self.numColumns > 0 &&
|
|
15
|
+
!(self.treeFilehandle && !self.data.tree)) {
|
|
16
|
+
self.zoomToRegion(region);
|
|
17
|
+
self.setRegion(undefined);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { expect, test, vi } from 'vitest';
|
|
2
|
+
import { applyRegion } from './applyRegion';
|
|
3
|
+
const REGION = { row: 'Human', start: 245, end: 249 };
|
|
4
|
+
function makeModel(over = {}) {
|
|
5
|
+
const model = {
|
|
6
|
+
region: REGION,
|
|
7
|
+
viewInitialized: true,
|
|
8
|
+
numColumns: 400,
|
|
9
|
+
treeFilehandle: undefined,
|
|
10
|
+
data: {},
|
|
11
|
+
zoomToRegion: vi.fn(),
|
|
12
|
+
setRegion: vi.fn((arg) => {
|
|
13
|
+
model.region = arg;
|
|
14
|
+
}),
|
|
15
|
+
...over,
|
|
16
|
+
};
|
|
17
|
+
return model;
|
|
18
|
+
}
|
|
19
|
+
test('zooms onto the region once, then forgets it', () => {
|
|
20
|
+
const model = makeModel();
|
|
21
|
+
applyRegion(model);
|
|
22
|
+
applyRegion(model);
|
|
23
|
+
expect(model.zoomToRegion).toHaveBeenCalledTimes(1);
|
|
24
|
+
expect(model.zoomToRegion).toHaveBeenCalledWith(REGION);
|
|
25
|
+
expect(model.region).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
test('waits for the view to have a width', () => {
|
|
28
|
+
const model = makeModel({ viewInitialized: false });
|
|
29
|
+
applyRegion(model);
|
|
30
|
+
expect(model.zoomToRegion).not.toHaveBeenCalled();
|
|
31
|
+
expect(model.region).toEqual(REGION);
|
|
32
|
+
});
|
|
33
|
+
test('waits for the alignment', () => {
|
|
34
|
+
const model = makeModel({ numColumns: 0 });
|
|
35
|
+
applyRegion(model);
|
|
36
|
+
expect(model.zoomToRegion).not.toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
test('waits for a tree file, whose collapsed clades move the columns', () => {
|
|
39
|
+
const model = makeModel({ treeFilehandle: { uri: 'x.nh' } });
|
|
40
|
+
applyRegion(model);
|
|
41
|
+
expect(model.zoomToRegion).not.toHaveBeenCalled();
|
|
42
|
+
model.data.tree = '(a,b);';
|
|
43
|
+
applyRegion(model);
|
|
44
|
+
expect(model.zoomToRegion).toHaveBeenCalledTimes(1);
|
|
45
|
+
});
|
|
@@ -84,8 +84,9 @@ export async function doLaunchBlast({ self, scope, }) {
|
|
|
84
84
|
await saveBlastResult({
|
|
85
85
|
proteinSequence: query,
|
|
86
86
|
blastDatabase: params.blastDatabase,
|
|
87
|
-
msaAlgorithm: params.msaAlgorithm,
|
|
87
|
+
msaAlgorithm: queryRow ? undefined : (params.msaAlgorithm ?? 'browser'),
|
|
88
88
|
searchProgram: params.searchProgram,
|
|
89
|
+
maxHits,
|
|
89
90
|
msa,
|
|
90
91
|
tree,
|
|
91
92
|
treeMetadata: treeMetadataJson,
|
|
@@ -67,6 +67,18 @@ test('bare hits go to the chosen aligner, with the query first', async () => {
|
|
|
67
67
|
expect(result.msa).toBe('aligned');
|
|
68
68
|
expect(result.tree).toBe('tree');
|
|
69
69
|
});
|
|
70
|
+
// the history names the aligner that ran, which for a spec naming none is the
|
|
71
|
+
// in-browser one -- not a key reading `db:undefined:...`
|
|
72
|
+
test('a search naming no aligner is saved as aligned in the browser', async () => {
|
|
73
|
+
blastp.mockResolvedValue({ rid: 'job', hits: [{ ...HIT, sequence: 'MKWV' }] });
|
|
74
|
+
mockLaunchMSA.mockResolvedValue({ msa: 'aligned', tree: '' });
|
|
75
|
+
await launch(makeModel({
|
|
76
|
+
blastDatabase: 'uniprotkb_swissprot',
|
|
77
|
+
maxHits: 20,
|
|
78
|
+
proteinSequence: 'MKWVTF',
|
|
79
|
+
}));
|
|
80
|
+
expect(saveBlastResult).toHaveBeenCalledWith(expect.objectContaining({ msaAlgorithm: 'browser', maxHits: 20 }));
|
|
81
|
+
});
|
|
70
82
|
test('an aligned result skips the aligner and leaves the tree to the browser', async () => {
|
|
71
83
|
phmmer.mockResolvedValue({
|
|
72
84
|
rid: 'job',
|
|
@@ -175,7 +175,7 @@ describe('the query row sequence', () => {
|
|
|
175
175
|
await launch({
|
|
176
176
|
self: makeModel(params({ proteinSequence: 'MAGG*AWGR&' })),
|
|
177
177
|
});
|
|
178
|
-
expect(queryRowSent()).toBe('
|
|
178
|
+
expect(queryRowSent()).toBe('MAGGXAWGR');
|
|
179
179
|
});
|
|
180
180
|
test('throws when neither a sequence nor a representative is available', async () => {
|
|
181
181
|
mockFetchProtein.mockResolvedValue(undefined);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util';
|
|
2
|
-
import { hasHoverPosition, hasQueryRow, transcriptPosToVisibleCol, } from './util';
|
|
2
|
+
import { hasHoverPosition, hasQueryRow, querySeqPosToVisibleCol, transcriptPosToVisibleCol, } from './util';
|
|
3
3
|
export function genomeToMSA({ model }) {
|
|
4
4
|
const { assemblyManager, hovered } = getSession(model);
|
|
5
|
-
const {
|
|
5
|
+
const { transcriptToMsaMap, connectedView, mafRegion } = model;
|
|
6
6
|
if (!connectedView?.initialized ||
|
|
7
7
|
!hasHoverPosition(hovered) ||
|
|
8
8
|
!hasQueryRow(model)) {
|
|
@@ -36,7 +36,7 @@ export function genomeToMSA({ model }) {
|
|
|
36
36
|
genomePos >= mafRegion.end) {
|
|
37
37
|
return undefined;
|
|
38
38
|
}
|
|
39
|
-
return model
|
|
39
|
+
return querySeqPosToVisibleCol(model, genomePos - mafRegion.start);
|
|
40
40
|
}
|
|
41
41
|
// session.hovered is global -- set by whichever LinearGenomeView the cursor
|
|
42
42
|
// was last over, on any assembly -- so the refName gate is load bearing:
|