jbrowse-plugin-msaview 3.7.1 → 3.9.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/MsaToGenomeHighlight.js +6 -22
- package/dist/AddHighlightModel/connectedHighlights.d.ts +9 -0
- package/dist/AddHighlightModel/connectedHighlights.js +15 -0
- package/dist/AddHighlightModel/connectedHighlights.test.d.ts +1 -0
- package/dist/AddHighlightModel/connectedHighlights.test.js +36 -0
- package/dist/AddHighlightModel/index.js +0 -1
- package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +4 -4
- package/dist/LaunchMsaView/components/BlastQuery/consts.js +6 -4
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +0 -2
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +2 -2
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +0 -1
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +1 -1
- package/dist/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.js +5 -5
- package/dist/LaunchMsaView/components/calculateProteinSequence.d.ts +0 -6
- package/dist/LaunchMsaView/components/calculateProteinSequence.js +1 -1
- package/dist/LaunchMsaView/components/types.d.ts +0 -3
- package/dist/LaunchMsaView/launchTarget.d.ts +3 -0
- package/dist/LaunchMsaView/launchTarget.js +1 -0
- package/dist/LaunchMsaView/launchTarget.test.js +13 -0
- package/dist/LaunchMsaView/util.d.ts +0 -1
- package/dist/LaunchMsaView/util.js +1 -1
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +5 -14
- package/dist/MsaViewPanel/afterCreateAutoruns.js +77 -86
- package/dist/MsaViewPanel/components/MsaViewPanel.js +1 -1
- package/dist/MsaViewPanel/doLaunchBlast.js +42 -16
- package/dist/MsaViewPanel/doLaunchBlast.test.js +54 -0
- package/dist/MsaViewPanel/doLaunchOrthologs.js +18 -11
- package/dist/MsaViewPanel/launchAutoruns.test.js +0 -1
- package/dist/MsaViewPanel/model.d.ts +13 -15
- package/dist/MsaViewPanel/model.js +39 -22
- package/dist/MsaViewPanel/model.test.js +3 -4
- package/dist/MsaViewPanel/msaDataStore.d.ts +1 -2
- package/dist/MsaViewPanel/msaDataStore.js +11 -26
- package/dist/MsaViewPanel/msaDataStore.test.js +10 -2
- package/dist/MsaViewPanel/observeProteinHighlights.test.js +1 -0
- package/dist/MsaViewPanel/processInit.test.js +1 -7
- package/dist/MsaViewPanel/regionSnapshot.test.js +0 -1
- package/dist/MsaViewPanel/resolveConnectedTranscript.test.js +0 -1
- package/dist/MsaViewPanel/storedData.test.js +238 -145
- package/dist/MsaViewPanel/transcriptMap.d.ts +7 -0
- package/dist/MsaViewPanel/transcriptMap.js +89 -0
- package/dist/MsaViewPanel/transcriptMap.test.d.ts +1 -0
- package/dist/MsaViewPanel/transcriptMap.test.js +108 -0
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +38 -38
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/browserAlign.d.ts +5 -3
- package/dist/utils/browserAlign.js +29 -23
- package/dist/utils/browserAlign.test.js +14 -1
- package/dist/utils/ebiBlast.d.ts +0 -8
- package/dist/utils/ebiBlast.js +10 -21
- package/dist/utils/ebiJobDispatcher.d.ts +9 -9
- package/dist/utils/ebiJobDispatcher.js +17 -3
- package/dist/utils/eutils.d.ts +0 -2
- package/dist/utils/eutils.js +11 -3
- package/dist/utils/eutils.test.js +22 -0
- package/dist/utils/msa.js +6 -14
- package/dist/utils/msaRows.d.ts +0 -8
- package/dist/utils/msaRows.js +1 -1
- package/dist/utils/ncbiOrthologs.d.ts +7 -5
- package/dist/utils/ncbiOrthologs.js +20 -14
- package/dist/utils/ncbiOrthologs.test.js +13 -0
- package/dist/utils/ncbiTaxonomy.d.ts +2 -2
- package/dist/utils/ncbiTaxonomy.js +5 -5
- package/dist/utils/pantherOrthologs.d.ts +2 -3
- package/dist/utils/pantherOrthologs.js +18 -12
- package/dist/utils/phmmer.js +7 -14
- package/dist/utils/searchCache.d.ts +18 -0
- package/dist/utils/searchCache.js +33 -0
- package/dist/utils/searchCache.test.d.ts +1 -0
- package/dist/utils/searchCache.test.js +80 -0
- package/dist/utils/taxonomyNames.d.ts +1 -1
- package/dist/utils/taxonomyNames.js +7 -2
- package/dist/utils/taxonomyNamesAbort.test.d.ts +1 -0
- package/dist/utils/taxonomyNamesAbort.test.js +36 -0
- package/dist/utils/unirefHomologs.d.ts +1 -6
- package/dist/utils/unirefHomologs.js +2 -2
- package/dist/utils/useFetch.d.ts +1 -1
- package/dist/utils/useFetch.js +5 -5
- package/dist/utils/useFetchAbort.test.d.ts +1 -0
- package/dist/utils/useFetchAbort.test.js +29 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +10 -23
- package/src/AddHighlightModel/connectedHighlights.test.ts +46 -0
- package/src/AddHighlightModel/connectedHighlights.ts +35 -0
- package/src/AddHighlightModel/index.tsx +0 -1
- package/src/LaunchMsaView/components/BlastQuery/consts.ts +6 -4
- package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +2 -2
- package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +1 -1
- package/src/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.tsx +5 -5
- package/src/LaunchMsaView/components/calculateProteinSequence.ts +1 -1
- package/src/LaunchMsaView/components/types.ts +0 -4
- package/src/LaunchMsaView/launchTarget.test.ts +14 -0
- package/src/LaunchMsaView/launchTarget.ts +8 -4
- package/src/LaunchMsaView/util.ts +1 -1
- package/src/MsaViewPanel/afterCreateAutoruns.ts +90 -87
- package/src/MsaViewPanel/components/MsaViewPanel.tsx +1 -1
- package/src/MsaViewPanel/doLaunchBlast.test.ts +71 -0
- package/src/MsaViewPanel/doLaunchBlast.ts +71 -20
- package/src/MsaViewPanel/doLaunchOrthologs.ts +28 -11
- package/src/MsaViewPanel/launchAutoruns.test.ts +0 -1
- package/src/MsaViewPanel/model.test.ts +3 -4
- package/src/MsaViewPanel/model.ts +40 -22
- package/src/MsaViewPanel/msaDataStore.test.ts +13 -2
- package/src/MsaViewPanel/msaDataStore.ts +11 -25
- package/src/MsaViewPanel/observeProteinHighlights.test.ts +1 -0
- package/src/MsaViewPanel/processInit.test.ts +1 -8
- package/src/MsaViewPanel/regionSnapshot.test.ts +0 -1
- package/src/MsaViewPanel/resolveConnectedTranscript.test.ts +0 -1
- package/src/MsaViewPanel/storedData.test.ts +273 -158
- package/src/MsaViewPanel/transcriptMap.test.ts +137 -0
- package/src/MsaViewPanel/transcriptMap.ts +117 -0
- package/src/utils/browserAlign.test.ts +17 -0
- package/src/utils/browserAlign.ts +39 -25
- package/src/utils/ebiBlast.ts +12 -35
- package/src/utils/ebiJobDispatcher.ts +26 -7
- package/src/utils/eutils.test.ts +25 -0
- package/src/utils/eutils.ts +10 -3
- package/src/utils/msa.ts +6 -16
- package/src/utils/msaRows.ts +1 -1
- package/src/utils/ncbiOrthologs.test.ts +16 -0
- package/src/utils/ncbiOrthologs.ts +37 -15
- package/src/utils/ncbiTaxonomy.ts +8 -2
- package/src/utils/pantherOrthologs.ts +28 -12
- package/src/utils/phmmer.ts +7 -16
- package/src/utils/searchCache.test.ts +98 -0
- package/src/utils/searchCache.ts +75 -0
- package/src/utils/taxonomyNames.ts +7 -0
- package/src/utils/taxonomyNamesAbort.test.ts +46 -0
- package/src/utils/unirefHomologs.ts +2 -2
- package/src/utils/useFetch.ts +6 -6
- package/src/utils/useFetchAbort.test.tsx +33 -0
- package/src/version.ts +1 -1
|
@@ -1,41 +1,25 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { getSession } from '@jbrowse/core/util';
|
|
3
3
|
import { observer } from 'mobx-react';
|
|
4
|
-
import {
|
|
4
|
+
import { connectedHighlights } from './connectedHighlights';
|
|
5
5
|
import { hasHoverPosition, useStyles } from './util';
|
|
6
6
|
const MsaToGenomeHighlight = observer(function MsaToGenomeHighlight2({ model, }) {
|
|
7
7
|
const { views, hovered } = getSession(model);
|
|
8
|
-
const
|
|
9
|
-
.filter(isMsaView)
|
|
10
|
-
.find(v => v.connectedViewId === model.id);
|
|
11
|
-
// The persistent click selection always shows. The hover codon is suppressed
|
|
12
|
-
// while hovering the LGV — GenomeMouseoverHighlight handles the single-bp
|
|
13
|
-
// display in that case, so we don't stack a wider codon band on top of it.
|
|
14
|
-
const highlights = [
|
|
15
|
-
...(msaView?.connectedClickHighlights ?? []),
|
|
16
|
-
...(hasHoverPosition(hovered)
|
|
17
|
-
? []
|
|
18
|
-
: (msaView?.connectedHoverHighlights ?? [])),
|
|
19
|
-
];
|
|
8
|
+
const highlights = connectedHighlights(views, model.id, hasHoverPosition(hovered));
|
|
20
9
|
return highlights.length ? (React.createElement(MsaToGenomeHighlightRenderer, { model: model, highlights: highlights })) : null;
|
|
21
10
|
});
|
|
22
|
-
// Inner component: handles the scroll-dependent rendering
|
|
23
11
|
const MsaToGenomeHighlightRenderer = observer(function ({ model, highlights, }) {
|
|
24
12
|
const { classes } = useStyles();
|
|
25
13
|
const { offsetPx } = model;
|
|
26
|
-
return (React.createElement(React.Fragment, null, highlights.map(
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// launcher set on this LGV). Do NOT canonicalize: bpToPx matches
|
|
30
|
-
// displayed regions by exact refName with no alias resolution, so
|
|
31
|
-
// rewriting e.g. "chr17" to the assembly-canonical "17" misses a view
|
|
32
|
-
// whose regions are "chr17". (GenomeMouseoverHighlight does the same.)
|
|
14
|
+
return (React.createElement(React.Fragment, null, highlights.map(r => {
|
|
15
|
+
// bpToPx matches refNames exactly, so canonicalizing "chr17" to "17"
|
|
16
|
+
// would miss a view whose regions say "chr17"
|
|
33
17
|
const s = model.bpToPx({ refName: r.refName, coord: r.start });
|
|
34
18
|
const e = model.bpToPx({ refName: r.refName, coord: r.end });
|
|
35
19
|
if (s && e) {
|
|
36
20
|
const width = Math.max(Math.abs(e.offsetPx - s.offsetPx), 4);
|
|
37
21
|
const left = Math.min(s.offsetPx, e.offsetPx) - offsetPx;
|
|
38
|
-
return (React.createElement("div", { key: `${r.refName}-${r.start}-${r.end}
|
|
22
|
+
return (React.createElement("div", { key: `${r.refName}-${r.start}-${r.end}`, className: classes.highlight, style: { left, width } }));
|
|
39
23
|
}
|
|
40
24
|
return null;
|
|
41
25
|
})));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function connectedHighlights(views, genomeViewId, genomeHovered) {
|
|
2
|
+
const regions = views
|
|
3
|
+
.filter(isLinkedMsaView)
|
|
4
|
+
.filter(v => v.connectedViewId === genomeViewId)
|
|
5
|
+
.flatMap(v => [
|
|
6
|
+
...v.connectedClickHighlights,
|
|
7
|
+
...(genomeHovered ? [] : v.connectedHoverHighlights),
|
|
8
|
+
]);
|
|
9
|
+
return [
|
|
10
|
+
...new Map(regions.map(r => [`${r.refName}:${r.start}-${r.end}`, r])).values(),
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
function isLinkedMsaView(view) {
|
|
14
|
+
return view.type === 'MsaView';
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { connectedHighlights } from './connectedHighlights';
|
|
3
|
+
const region = (start) => ({ refName: 'chr17', start, end: start + 3 });
|
|
4
|
+
function msaView(id, click, hover) {
|
|
5
|
+
return {
|
|
6
|
+
type: 'MsaView',
|
|
7
|
+
connectedViewId: id,
|
|
8
|
+
connectedClickHighlights: [region(click)],
|
|
9
|
+
connectedHoverHighlights: [region(hover)],
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
describe('connectedHighlights', () => {
|
|
13
|
+
const views = [
|
|
14
|
+
{ type: 'LinearGenomeView' },
|
|
15
|
+
msaView('lgv1', 100, 200),
|
|
16
|
+
msaView('lgv2', 300, 400),
|
|
17
|
+
msaView('lgv1', 500, 600),
|
|
18
|
+
];
|
|
19
|
+
it('collects every MSA view linked to the genome view', () => {
|
|
20
|
+
expect(connectedHighlights(views, 'lgv1', false).map(r => r.start)).toEqual([100, 200, 500, 600]);
|
|
21
|
+
});
|
|
22
|
+
it('drops hover codons while the genome view is hovered', () => {
|
|
23
|
+
expect(connectedHighlights(views, 'lgv1', true).map(r => r.start)).toEqual([
|
|
24
|
+
100, 500,
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
it('draws a codon two views both highlight once', () => {
|
|
28
|
+
const both = [msaView('lgv1', 100, 200), msaView('lgv1', 100, 200)];
|
|
29
|
+
expect(connectedHighlights(both, 'lgv1', false).map(r => r.start)).toEqual([
|
|
30
|
+
100, 200,
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
it('returns nothing for an unlinked genome view', () => {
|
|
34
|
+
expect(connectedHighlights(views, 'lgv3', false)).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -6,7 +6,6 @@ export default function AddHighlightComponentsModelF(pluginManager) {
|
|
|
6
6
|
pluginManager.addToExtensionPoint(
|
|
7
7
|
// @ts-expect-error v4 hosts have no contributeToExtensionPoint
|
|
8
8
|
'LinearGenomeView-TracksContainerComponent', (rest, { model }) => {
|
|
9
|
-
// Quick check: don't add any components if no MSA view exists
|
|
10
9
|
const { views } = getSession(model);
|
|
11
10
|
const hasMsaView = views.some(v => isMsaView(v) && v.connectedViewId === model.id);
|
|
12
11
|
if (!hasMsaView) {
|
|
@@ -13,8 +13,10 @@ export type EbiMsaAlgorithm = (typeof ebiMsaAlgorithms)[number];
|
|
|
13
13
|
* page and the rows merged on the query (utils/browserAlign.ts), with the tree
|
|
14
14
|
* built by react-msaview's neighbour joining. It is the aligner for a launch
|
|
15
15
|
* that must not depend on EBI, and for one that wants to be quick -- a
|
|
16
|
-
* hundred rows take a second
|
|
17
|
-
*
|
|
16
|
+
* hundred 400-residue rows take under a second, and a hundred 1400-residue
|
|
17
|
+
* rows eight, against a Job Dispatcher queue that has been measured at
|
|
18
|
+
* anything from ten seconds to fifteen minutes. Past the per-sequence limit in
|
|
19
|
+
* browserAlign.ts it refuses and names an EBI aligner instead.
|
|
18
20
|
*/
|
|
19
21
|
export declare const msaAlgorithms: readonly ["clustalo", "muscle", "kalign", "mafft", "browser"];
|
|
20
22
|
export type MsaAlgorithm = (typeof msaAlgorithms)[number];
|
|
@@ -28,7 +30,6 @@ export declare const msaAlgorithmLabels: Record<MsaAlgorithm, string>;
|
|
|
28
30
|
*/
|
|
29
31
|
export declare const blastDatabaseOptions: readonly ["uniprotkb_swissprot", "uniprotkb", "pan_proteomes", "uniprotkb_trembl"];
|
|
30
32
|
export type BlastDatabase = (typeof blastDatabaseOptions)[number];
|
|
31
|
-
export declare const defaultBlastDatabase: BlastDatabase;
|
|
32
33
|
export declare const searchPrograms: readonly ["blastp", "phmmer"];
|
|
33
34
|
export type SearchProgram = (typeof searchPrograms)[number];
|
|
34
35
|
export declare const searchProgramLabels: Record<SearchProgram, string>;
|
|
@@ -46,7 +47,6 @@ export declare const searchProgramLabels: Record<SearchProgram, string>;
|
|
|
46
47
|
*/
|
|
47
48
|
export declare const phmmerDatabaseOptions: readonly ["swissprot", "uniprotkb", "uniprotrefprot", "rp75", "rp55", "rp35", "rp15"];
|
|
48
49
|
export type PhmmerDatabase = (typeof phmmerDatabaseOptions)[number];
|
|
49
|
-
export declare const defaultPhmmerDatabase: PhmmerDatabase;
|
|
50
50
|
export declare const defaultMaxHits = 100;
|
|
51
51
|
export declare function snapBlastHitCount(maxHits: number): number;
|
|
52
52
|
/**
|
|
@@ -17,8 +17,10 @@ export const ebiMsaAlgorithms = [
|
|
|
17
17
|
* page and the rows merged on the query (utils/browserAlign.ts), with the tree
|
|
18
18
|
* built by react-msaview's neighbour joining. It is the aligner for a launch
|
|
19
19
|
* that must not depend on EBI, and for one that wants to be quick -- a
|
|
20
|
-
* hundred rows take a second
|
|
21
|
-
*
|
|
20
|
+
* hundred 400-residue rows take under a second, and a hundred 1400-residue
|
|
21
|
+
* rows eight, against a Job Dispatcher queue that has been measured at
|
|
22
|
+
* anything from ten seconds to fifteen minutes. Past the per-sequence limit in
|
|
23
|
+
* browserAlign.ts it refuses and names an EBI aligner instead.
|
|
22
24
|
*/
|
|
23
25
|
export const msaAlgorithms = [...ebiMsaAlgorithms, 'browser'];
|
|
24
26
|
export const msaAlgorithmLabels = {
|
|
@@ -43,7 +45,7 @@ export const blastDatabaseOptions = [
|
|
|
43
45
|
];
|
|
44
46
|
// curated, so it returns roughly one good sequence per species rather than the
|
|
45
47
|
// many near-identical TrEMBL entries an alignment reads poorly
|
|
46
|
-
|
|
48
|
+
const defaultBlastDatabase = 'uniprotkb_swissprot';
|
|
47
49
|
export const searchPrograms = ['blastp', 'phmmer'];
|
|
48
50
|
export const searchProgramLabels = {
|
|
49
51
|
blastp: 'blastp (sequence search)',
|
|
@@ -70,7 +72,7 @@ export const phmmerDatabaseOptions = [
|
|
|
70
72
|
'rp35',
|
|
71
73
|
'rp15',
|
|
72
74
|
];
|
|
73
|
-
|
|
75
|
+
const defaultPhmmerDatabase = 'swissprot';
|
|
74
76
|
/**
|
|
75
77
|
* The hit counts EBI's ncbiblast accepts for `alignments` and `scores`. A value
|
|
76
78
|
* off this list is a 400 at submit time, so a request is rounded up to the next
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { MsaAlgorithm, SearchChoice } from './consts';
|
|
2
|
-
export declare const SEARCH_CHOICE_STORAGE_KEY = "msaView-blastSearch";
|
|
3
|
-
export declare const MSA_ALGORITHM_STORAGE_KEY = "msaView-msaAlgorithm";
|
|
4
2
|
export declare function validSearchChoice(stored: unknown): SearchChoice;
|
|
5
3
|
export declare function validMsaAlgorithm(stored: unknown): MsaAlgorithm;
|
|
6
4
|
export declare function useStoredSearchChoice(): readonly [SearchChoice, (choice: SearchChoice) => void];
|
|
@@ -8,8 +8,8 @@ import { databaseOptionsFor, defaultSearchFor, msaAlgorithms, searchPrograms, }
|
|
|
8
8
|
// comes back is checked against the current options before it is used — an
|
|
9
9
|
// option this plugin has since dropped would otherwise reach EBI and come back
|
|
10
10
|
// a 400, minutes after the user pressed Submit.
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const SEARCH_CHOICE_STORAGE_KEY = 'msaView-blastSearch';
|
|
12
|
+
const MSA_ALGORITHM_STORAGE_KEY = 'msaView-msaAlgorithm';
|
|
13
13
|
const defaultSearch = defaultSearchFor('blastp');
|
|
14
14
|
const defaultMsaAlgorithm = 'clustalo';
|
|
15
15
|
export function validSearchChoice(stored) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { OrthologSource } from '../../../MsaViewPanel/model';
|
|
3
|
-
export declare const orthologSourceLabels: Record<OrthologSource, string>;
|
|
4
3
|
export declare function validOrthologSource(stored: unknown): OrthologSource;
|
|
5
4
|
export declare function useStoredOrthologSource(): readonly ["ncbi" | "panther" | "uniref", (source: OrthologSource) => void];
|
|
6
5
|
export default function OrthologSourceSelect({ value, onChange, className, }: {
|
|
@@ -3,7 +3,7 @@ import { MenuItem } from '@mui/material';
|
|
|
3
3
|
import TextField2 from '../../../components/TextField2';
|
|
4
4
|
import { useLocalStorage } from '../../../utils/useLocalStorage';
|
|
5
5
|
const ORTHOLOG_SOURCE_STORAGE_KEY = 'msaview-ortholog-source';
|
|
6
|
-
|
|
6
|
+
const orthologSourceLabels = {
|
|
7
7
|
ncbi: 'NCBI orthologs',
|
|
8
8
|
panther: 'PANTHER',
|
|
9
9
|
uniref: 'UniRef cluster',
|
|
@@ -3,12 +3,12 @@ import TextField2 from '../../../components/TextField2';
|
|
|
3
3
|
import { resolveAssemblySpecies, resolveTaxId, } from '../../../utils/ncbiTaxonomy';
|
|
4
4
|
import { fetchTaxonomyInfo } from '../../../utils/taxonomyNames';
|
|
5
5
|
import { useDebounced, useFetch } from '../../../utils/useFetch';
|
|
6
|
-
async function describeTaxon(query) {
|
|
7
|
-
const taxId = await resolveTaxId(query);
|
|
6
|
+
async function describeTaxon(query, signal) {
|
|
7
|
+
const taxId = await resolveTaxId(query, signal);
|
|
8
8
|
if (!taxId) {
|
|
9
9
|
throw new Error(`No NCBI taxon matches "${query}"`);
|
|
10
10
|
}
|
|
11
|
-
const info = (await fetchTaxonomyInfo([taxId])).get(taxId);
|
|
11
|
+
const info = (await fetchTaxonomyInfo([taxId], signal)).get(taxId);
|
|
12
12
|
const label = [info?.sciname, info?.commonName && `(${info.commonName})`]
|
|
13
13
|
.filter(Boolean)
|
|
14
14
|
.join(' ');
|
|
@@ -40,14 +40,14 @@ export default function QuerySpeciesSelect({ value, assemblyName, onChange, clas
|
|
|
40
40
|
// reported the throttle as "blocked by CORS policy" in the helper text.
|
|
41
41
|
const { data: fromAssembly } = useFetch(assemblyName && typed === undefined
|
|
42
42
|
? [assemblyName, 'assembly-species']
|
|
43
|
-
: null,
|
|
43
|
+
: null, signal => resolveAssemblySpecies(assemblyName, signal), {
|
|
44
44
|
onSuccess: found => {
|
|
45
45
|
if (found) {
|
|
46
46
|
onChange(found.taxId);
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
});
|
|
50
|
-
const { data: fromText, error } = useFetch(debounced?.trim() ? [debounced.trim(), 'taxon'] : null,
|
|
50
|
+
const { data: fromText, error } = useFetch(debounced?.trim() ? [debounced.trim(), 'taxon'] : null, signal => describeTaxon(debounced, signal), {
|
|
51
51
|
onSuccess: ({ taxId }) => {
|
|
52
52
|
onChange(taxId);
|
|
53
53
|
},
|
|
@@ -7,12 +7,6 @@ export declare function calculateProteinSequence({ cds, sequence, geneticCodeId,
|
|
|
7
7
|
geneticCodeId?: number;
|
|
8
8
|
translExcept?: TranslExcept[];
|
|
9
9
|
}): string;
|
|
10
|
-
export declare function revlist(list: Feat[], seqlen: number): {
|
|
11
|
-
start: number;
|
|
12
|
-
end: number;
|
|
13
|
-
type?: string;
|
|
14
|
-
phase?: number;
|
|
15
|
-
}[];
|
|
16
10
|
/**
|
|
17
11
|
* The translation core's own feature panel shows: the contig's or the
|
|
18
12
|
* feature's genetic code, its alternative initiators, and any `transl_except`
|
|
@@ -24,7 +24,7 @@ function cdsId(feat) {
|
|
|
24
24
|
function dedupe(list) {
|
|
25
25
|
return list.filter((item, pos, ary) => !pos || cdsId(item) !== cdsId(ary[pos - 1]));
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
function revlist(list, seqlen) {
|
|
28
28
|
return list
|
|
29
29
|
.map(sub => ({
|
|
30
30
|
...sub,
|
|
@@ -108,6 +108,19 @@ describe('launchTarget', () => {
|
|
|
108
108
|
// the choice is ever stated
|
|
109
109
|
expect(target?.preferredTranscriptId).toBe(mrna.id());
|
|
110
110
|
});
|
|
111
|
+
// the canvas host names the clicked isoform beside the gene it fetches, as
|
|
112
|
+
// its own Collapse introns dialog reads it
|
|
113
|
+
test('carries the isoform a canvas click landed on', () => {
|
|
114
|
+
const host = modernHost('gene');
|
|
115
|
+
const target = launchTarget({
|
|
116
|
+
...host,
|
|
117
|
+
contextMenuInfo: {
|
|
118
|
+
...host.contextMenuInfo,
|
|
119
|
+
subfeature: { featureId: 'mrna-2' },
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
expect(target?.preferredTranscriptId).toBe('mrna-2');
|
|
123
|
+
});
|
|
111
124
|
// a host that has both shapes must not fall through to the legacy branch and
|
|
112
125
|
// launch on a stale feature when the click was not on a gene
|
|
113
126
|
test('a non-gene click on a host carrying both shapes offers nothing', () => {
|
|
@@ -7,7 +7,6 @@ export declare function getTranscriptLength(feature: Feature): {
|
|
|
7
7
|
mod: number;
|
|
8
8
|
};
|
|
9
9
|
export declare function getId(val?: Feature): string;
|
|
10
|
-
export declare function getMatchableIds(val?: Feature): string[];
|
|
11
10
|
export declare function featureMatchesId(feature: Feature, id: string): boolean;
|
|
12
11
|
export declare function getTranscriptDisplayName(val?: Feature): string;
|
|
13
12
|
export declare function getGeneDisplayName(val?: Feature): string;
|
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
import type { JBrowsePluginMsaViewModel } from './model';
|
|
2
2
|
export declare function loadStoredData(self: JBrowsePluginMsaViewModel): void;
|
|
3
3
|
/**
|
|
4
|
-
* Keep IndexedDB holding
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `lastStoredData` is what makes that safe to run on every data change: it is
|
|
11
|
-
* the only thing separating "this is new" from "this is what we just wrote", and
|
|
12
|
-
* it is recorded whether or not the write succeeded, so a browser refusing
|
|
13
|
-
* IndexedDB (private mode) fails once rather than in a loop.
|
|
14
|
-
*
|
|
15
|
-
* A view whose data comes from a filehandle -- or from the indexed block its
|
|
16
|
-
* kept `init` names -- stores nothing at all: the file is the source of truth
|
|
17
|
-
* and it is refetched at startup.
|
|
4
|
+
* Keep IndexedDB holding `unsavedDocuments`. A view writes in place only to a
|
|
5
|
+
* row it created (`ownsDataStoreRow`), and never deletes one, since other
|
|
6
|
+
* views may name it: an emptied set drops the id and cleanupOldData ages the
|
|
7
|
+
* row out. `lastStoredData` is recorded even when a write fails, so a browser
|
|
8
|
+
* refusing IndexedDB fails once rather than in a loop.
|
|
18
9
|
*/
|
|
19
10
|
export declare function storeDataToIndexedDB(self: JBrowsePluginMsaViewModel): void;
|
|
20
11
|
/**
|
|
@@ -8,68 +8,70 @@ import { loadProteinDomains } from './loadProteinDomains';
|
|
|
8
8
|
import { cleanupOldData, generateDataStoreId, retrieveMsaData, storeMsaData, } from './msaDataStore';
|
|
9
9
|
import { runLaunch } from './runLaunch';
|
|
10
10
|
import { getProteinViews } from './structureConnection';
|
|
11
|
+
import { proteinPositionsInRange } from './transcriptMap';
|
|
11
12
|
import { getUniprotIdFromAlphaFoldUrl, hasQueryRow, transcriptPosToVisibleCol, } from './util';
|
|
12
13
|
const EXPIRED_MESSAGE = "This view's alignment is no longer in browser storage. Stored alignments are kept for 7 days after they were last used, and are lost when site data is cleared.";
|
|
13
14
|
const RELAUNCHABLE = ' Retry runs the original search again and rebuilds it.';
|
|
14
15
|
const START_OVER = ' Relaunch it from the gene to rebuild it.';
|
|
16
|
+
const EXPIRED_EXTRAS_WARNING = 'Part of this view is no longer in browser storage: annotations, a tree or row metadata too large for the session. Stored documents are kept for 7 days after they were last used, and are lost when site data is cleared.';
|
|
15
17
|
export function loadStoredData(self) {
|
|
16
|
-
const { dataStoreId
|
|
17
|
-
if (dataStoreId
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
18
|
+
const { dataStoreId } = self;
|
|
19
|
+
if (!dataStoreId) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
void (async () => {
|
|
23
|
+
try {
|
|
24
|
+
self.setLoadingStoredData(true);
|
|
25
|
+
const stored = await retrieveMsaData(dataStoreId);
|
|
26
|
+
if (self.dataStoreId !== dataStoreId) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
transaction(() => {
|
|
30
|
+
if (stored) {
|
|
31
|
+
const { data } = self;
|
|
32
|
+
if (stored.msa && !data.msa) {
|
|
33
|
+
self.setMSA(stored.msa);
|
|
34
|
+
}
|
|
35
|
+
if (stored.tree && !data.tree) {
|
|
36
|
+
self.setTree(stored.tree);
|
|
37
|
+
}
|
|
38
|
+
if (stored.treeMetadata && !data.treeMetadata) {
|
|
39
|
+
self.setTreeMetadata(stored.treeMetadata);
|
|
40
|
+
}
|
|
41
|
+
if (stored.gff && !data.gff) {
|
|
42
|
+
self.setGFF(stored.gff);
|
|
43
|
+
}
|
|
44
|
+
self.setLastStoredData(stored);
|
|
40
45
|
}
|
|
41
46
|
else {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
transaction(() => {
|
|
48
|
-
self.setDataStoreId(undefined);
|
|
47
|
+
self.setDataStoreId(undefined);
|
|
48
|
+
if (hasAlignmentSource(self)) {
|
|
49
|
+
self.addWarning(EXPIRED_EXTRAS_WARNING);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
49
52
|
self.setError(new Error(EXPIRED_MESSAGE +
|
|
50
53
|
((self.blastParams ?? self.orthologParams)
|
|
51
54
|
? RELAUNCHABLE
|
|
52
55
|
: START_OVER)));
|
|
53
|
-
}
|
|
56
|
+
}
|
|
54
57
|
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
console.error('Failed to load MSA data from IndexedDB:', e);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
self.setLoadingStoredData(false);
|
|
65
|
+
}
|
|
66
|
+
})();
|
|
64
67
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
return { msa, tree, treeMetadata, gff };
|
|
68
|
+
function hasAlignmentSource(self) {
|
|
69
|
+
return !!(self.data.msa ||
|
|
70
|
+
self.msaFilehandle ||
|
|
71
|
+
self.init?.msaIndexedLocation);
|
|
72
|
+
}
|
|
73
|
+
function isEmpty(data) {
|
|
74
|
+
return !(data.msa || data.tree || data.treeMetadata || data.gff);
|
|
73
75
|
}
|
|
74
76
|
function sameData(a, b) {
|
|
75
77
|
return (!!a &&
|
|
@@ -79,44 +81,37 @@ function sameData(a, b) {
|
|
|
79
81
|
a.gff === b.gff);
|
|
80
82
|
}
|
|
81
83
|
/**
|
|
82
|
-
* Keep IndexedDB holding
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* `lastStoredData` is what makes that safe to run on every data change: it is
|
|
89
|
-
* the only thing separating "this is new" from "this is what we just wrote", and
|
|
90
|
-
* it is recorded whether or not the write succeeded, so a browser refusing
|
|
91
|
-
* IndexedDB (private mode) fails once rather than in a loop.
|
|
92
|
-
*
|
|
93
|
-
* A view whose data comes from a filehandle -- or from the indexed block its
|
|
94
|
-
* kept `init` names -- stores nothing at all: the file is the source of truth
|
|
95
|
-
* and it is refetched at startup.
|
|
84
|
+
* Keep IndexedDB holding `unsavedDocuments`. A view writes in place only to a
|
|
85
|
+
* row it created (`ownsDataStoreRow`), and never deletes one, since other
|
|
86
|
+
* views may name it: an emptied set drops the id and cleanupOldData ages the
|
|
87
|
+
* row out. `lastStoredData` is recorded even when a write fails, so a browser
|
|
88
|
+
* refusing IndexedDB fails once rather than in a loop.
|
|
96
89
|
*/
|
|
97
90
|
export function storeDataToIndexedDB(self) {
|
|
98
|
-
const {
|
|
99
|
-
const data =
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
91
|
+
const { dataStoreId, ownsDataStoreRow, isStoringData, loadingStoredData, lastStoredData, } = self;
|
|
92
|
+
const data = self.unsavedDocuments;
|
|
93
|
+
if (isStoringData || loadingStoredData || sameData(lastStoredData, data)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (isEmpty(data)) {
|
|
97
|
+
if (lastStoredData) {
|
|
98
|
+
transaction(() => {
|
|
99
|
+
self.setDataStoreId(undefined);
|
|
100
|
+
self.setOwnsDataStoreRow(false);
|
|
101
|
+
self.setLastStoredData(data);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
109
104
|
return;
|
|
110
105
|
}
|
|
111
|
-
// mark as storing synchronously so re-runs of this autorun (e.g. when data
|
|
112
|
-
// observables change while the write is pending) don't kick off a duplicate
|
|
113
|
-
// write and leave an orphan IndexedDB entry
|
|
114
106
|
self.setIsStoringData(true);
|
|
115
107
|
void (async () => {
|
|
116
108
|
try {
|
|
117
|
-
const id = dataStoreId
|
|
109
|
+
const id = dataStoreId && ownsDataStoreRow ? dataStoreId : generateDataStoreId();
|
|
118
110
|
if (await storeMsaData(id, data)) {
|
|
119
|
-
|
|
111
|
+
transaction(() => {
|
|
112
|
+
self.setDataStoreId(id);
|
|
113
|
+
self.setOwnsDataStoreRow(true);
|
|
114
|
+
});
|
|
120
115
|
}
|
|
121
116
|
}
|
|
122
117
|
catch (e) {
|
|
@@ -297,19 +292,15 @@ function genomeHighlightsToVisibleColumns(self, field) {
|
|
|
297
292
|
if (!transcriptToMsaMap || !hasQueryRow(self)) {
|
|
298
293
|
return [];
|
|
299
294
|
}
|
|
300
|
-
const { g2p } = transcriptToMsaMap;
|
|
301
295
|
const columns = new Set();
|
|
302
296
|
for (const view of getProteinViews(getSession(self).views)) {
|
|
303
297
|
for (const structure of view.structures) {
|
|
304
298
|
if (structure.connectedViewId !== connectedViewId) {
|
|
305
299
|
continue;
|
|
306
300
|
}
|
|
307
|
-
for (const
|
|
308
|
-
for (
|
|
309
|
-
const
|
|
310
|
-
const col = proteinPos === undefined
|
|
311
|
-
? undefined
|
|
312
|
-
: transcriptPosToVisibleCol(self, proteinPos);
|
|
301
|
+
for (const { start, end } of structure[field] ?? []) {
|
|
302
|
+
for (const proteinPos of proteinPositionsInRange(transcriptToMsaMap, start, end)) {
|
|
303
|
+
const col = transcriptPosToVisibleCol(self, proteinPos);
|
|
313
304
|
if (col !== undefined) {
|
|
314
305
|
columns.add(col);
|
|
315
306
|
}
|
|
@@ -25,7 +25,7 @@ const MsaViewPanel = observer(function MsaViewPanel2({ model, }) {
|
|
|
25
25
|
const expired = !!request && !!model.error && !model.dataInitialized;
|
|
26
26
|
const launching = pending || expired || (!!init && !model.dataInitialized);
|
|
27
27
|
return (React.createElement(ErrorBoundary, null,
|
|
28
|
-
React.createElement("div", null, launching ? (React.createElement(LaunchProgress, { model: model })) : loadingStoredData ? (React.createElement("div", { className: classes.loadingContainer },
|
|
28
|
+
React.createElement("div", null, launching ? (React.createElement(LaunchProgress, { model: model })) : loadingStoredData && !model.dataInitialized ? (React.createElement("div", { className: classes.loadingContainer },
|
|
29
29
|
React.createElement(LoadingEllipses, { message: "Loading MSA data", variant: "h6" }))) : (React.createElement(MSAView, { model: model })))));
|
|
30
30
|
});
|
|
31
31
|
export default MsaViewPanel;
|