jbrowse-plugin-msaview 2.7.1 → 2.7.3
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 -5
- package/dist/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.js +1 -1
- package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastPanel.js +1 -1
- package/dist/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.d.ts +1 -1
- package/dist/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.js +5 -2
- package/dist/LaunchMsaView/components/calculateProteinSequence.d.ts +3 -2
- package/dist/LaunchMsaView/components/calculateProteinSequence.js +31 -15
- package/dist/LaunchMsaView/components/geneticCodes.d.ts +15 -0
- package/dist/LaunchMsaView/components/geneticCodes.js +227 -0
- package/dist/LaunchMsaView/components/types.d.ts +1 -0
- package/dist/LaunchMsaView/index.js +32 -26
- package/dist/MsaViewPanel/doLaunchBlast.js +10 -22
- package/dist/MsaViewPanel/genomeToMSA.js +13 -6
- package/dist/MsaViewPanel/genomeToMSA.test.js +35 -6
- package/dist/MsaViewPanel/model.d.ts +68 -66
- package/dist/MsaViewPanel/model.js +14 -12
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +32 -13
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +43 -14
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +108 -18
- package/dist/MsaViewPanel/msaDataStore.js +8 -17
- package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +8 -6
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +31 -31
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.d.ts +1 -2
- package/dist/utils/blastCache.js +9 -22
- package/dist/utils/domainCache.js +6 -15
- package/dist/utils/idb.d.ts +12 -0
- package/dist/utils/idb.js +21 -0
- package/dist/utils/taxonomyNames.js +13 -18
- package/dist/utils/useLocalStorage.d.ts +1 -0
- package/dist/utils/useLocalStorage.js +29 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -3
- package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +6 -8
- package/src/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.tsx +1 -1
- package/src/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastPanel.tsx +1 -1
- package/src/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.ts +4 -2
- package/src/LaunchMsaView/components/calculateProteinSequence.ts +35 -20
- package/src/LaunchMsaView/components/geneticCodes.ts +298 -0
- package/src/LaunchMsaView/components/types.ts +3 -0
- package/src/LaunchMsaView/index.ts +44 -35
- package/src/MsaViewPanel/doLaunchBlast.ts +20 -29
- package/src/MsaViewPanel/genomeToMSA.test.ts +38 -6
- package/src/MsaViewPanel/genomeToMSA.ts +14 -6
- package/src/MsaViewPanel/model.ts +17 -12
- package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +117 -18
- package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +70 -26
- package/src/MsaViewPanel/msaDataStore.ts +17 -17
- package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +8 -6
- package/src/utils/blastCache.ts +20 -23
- package/src/utils/domainCache.ts +14 -18
- package/src/utils/idb.ts +28 -0
- package/src/utils/taxonomyNames.ts +22 -24
- package/src/utils/useLocalStorage.ts +31 -0
- package/src/version.ts +1 -1
- package/dist/MsaViewPanel/blosum62.d.ts +0 -2
- package/dist/MsaViewPanel/blosum62.js +0 -627
- package/src/MsaViewPanel/blosum62.ts +0 -628
|
@@ -11,11 +11,12 @@ const MsaToGenomeHighlight = observer(function MsaToGenomeHighlight2({ model, })
|
|
|
11
11
|
// The persistent click selection always shows. The hover codon is suppressed
|
|
12
12
|
// while hovering the LGV — GenomeMouseoverHighlight handles the single-bp
|
|
13
13
|
// display in that case, so we don't stack a wider codon band on top of it.
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
const highlights = [
|
|
15
|
+
...(msaView?.connectedClickHighlights ?? []),
|
|
16
|
+
...(hasHoverPosition(hovered)
|
|
17
|
+
? []
|
|
18
|
+
: (msaView?.connectedHoverHighlights ?? [])),
|
|
19
|
+
];
|
|
19
20
|
return highlights.length ? (React.createElement(MsaToGenomeHighlightRenderer, { model: model, highlights: highlights })) : null;
|
|
20
21
|
});
|
|
21
22
|
// Inner component: handles the scroll-dependent rendering
|
|
@@ -68,7 +68,7 @@ const CachedBlastResults = observer(function ({ model, handleClose, feature, })
|
|
|
68
68
|
catch (e) {
|
|
69
69
|
setOperationError(e);
|
|
70
70
|
}
|
|
71
|
-
} }, "Clear
|
|
71
|
+
} }, "Clear results for this gene")),
|
|
72
72
|
React.createElement(List, { dense: true, className: classes.resultList }, results.map(result => (React.createElement(ListItem, { key: result.id, disablePadding: true, secondaryAction: React.createElement(IconButton, { edge: "end", size: "small", onClick: async (e) => {
|
|
73
73
|
e.stopPropagation();
|
|
74
74
|
try {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
|
-
import { useLocalStorage } from '@jbrowse/core/util';
|
|
3
2
|
import SettingsIcon from '@mui/icons-material/Settings';
|
|
4
3
|
import { IconButton } from '@mui/material';
|
|
5
4
|
import { makeStyles } from 'tss-react/mui';
|
|
@@ -9,6 +8,7 @@ import NCBIBlastMethodSelector from './NCBIBlastMethodSelector';
|
|
|
9
8
|
import NCBIBlastRIDPanel from './NCBIBlastRIDPanel';
|
|
10
9
|
import NCBISettingsDialog from './NCBISettingsDialog';
|
|
11
10
|
import { BASE_BLAST_URL } from './consts';
|
|
11
|
+
import { useLocalStorage } from '../../../utils/useLocalStorage';
|
|
12
12
|
const useStyles = makeStyles()({
|
|
13
13
|
settingsButton: {
|
|
14
14
|
float: 'right',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import useSWR from 'swr';
|
|
2
|
-
import {
|
|
2
|
+
import { deleteCachedResult, getAllCachedResults, } from '../../../utils/blastCache';
|
|
3
3
|
import { staticSwrConfig } from '../../../utils/swrConfig';
|
|
4
4
|
export function useCachedBlastResults(geneIds) {
|
|
5
5
|
const { data: results, error, isLoading, mutate, } = useSWR(`cached-blast-${geneIds.join(',')}`, async () => {
|
|
@@ -10,8 +10,11 @@ export function useCachedBlastResults(geneIds) {
|
|
|
10
10
|
await deleteCachedResult(id);
|
|
11
11
|
await mutate(results => results?.filter(result => result.id !== id) ?? [], false);
|
|
12
12
|
};
|
|
13
|
+
// deletes only what this hook listed, i.e. the results for these gene ids.
|
|
14
|
+
// The list the user is looking at is gene-scoped, so a store-wide clear here
|
|
15
|
+
// would silently throw away every other gene's cached alignments too
|
|
13
16
|
const handleClearAll = async () => {
|
|
14
|
-
await
|
|
17
|
+
await Promise.all((results ?? []).map(r => deleteCachedResult(r.id)));
|
|
15
18
|
await mutate([], false);
|
|
16
19
|
};
|
|
17
20
|
return {
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import type { Feat } from './types';
|
|
2
2
|
import type { Feature } from '@jbrowse/core/util';
|
|
3
|
-
export declare function
|
|
4
|
-
export declare function calculateProteinSequence({ cds, sequence, }: {
|
|
3
|
+
export declare function calculateProteinSequence({ cds, sequence, geneticCodeId, }: {
|
|
5
4
|
cds: Feat[];
|
|
6
5
|
sequence: string;
|
|
6
|
+
geneticCodeId?: number;
|
|
7
7
|
}): string;
|
|
8
8
|
export declare function revlist(list: Feat[], seqlen: number): {
|
|
9
9
|
start: number;
|
|
10
10
|
end: number;
|
|
11
11
|
type?: string;
|
|
12
|
+
phase?: number;
|
|
12
13
|
}[];
|
|
13
14
|
export declare function getProteinSequenceFromFeature({ feature, seq, }: {
|
|
14
15
|
seq: string;
|
|
@@ -1,18 +1,25 @@
|
|
|
1
|
-
import { dedupe,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import { dedupe, revcom } from '@jbrowse/core/util';
|
|
2
|
+
import { convertCodingSequenceToPeptides } from '@jbrowse/core/util/convertCodingSequenceToPeptides';
|
|
3
|
+
import { getGeneticCode, parseTranslTable } from './geneticCodes';
|
|
4
|
+
// `@jbrowse/core/util/convertCodingSequenceToPeptides` is a deep path, so unlike
|
|
5
|
+
// the `@jbrowse/core/util` barrel it is absent from ReExports and gets bundled
|
|
6
|
+
// rather than resolved out of the host's JBrowseExports. That is what makes
|
|
7
|
+
// reusing core's translation safe across every host a config names: this module
|
|
8
|
+
// previously built its codon table at module scope from the barrel's
|
|
9
|
+
// `defaultCodonTable`, and a core build that dropped that export turned it into
|
|
10
|
+
// `Object.keys(undefined)` while the UMD was still evaluating -- the plugin
|
|
11
|
+
// global was never assigned and PluginLoader error-paged the whole app.
|
|
12
|
+
export function calculateProteinSequence({ cds, sequence, geneticCodeId, }) {
|
|
13
|
+
// `starts` is deliberately not passed: @jbrowse/core 4.3.0's signature has no
|
|
14
|
+
// such parameter, so alternative initiators (GTG under table 11, ATA under
|
|
15
|
+
// table 2) render as their internal residue rather than M. Core main added it;
|
|
16
|
+
// pass it here when msaview's @jbrowse/core floor reaches that release.
|
|
17
|
+
const { codonTable } = getGeneticCode(geneticCodeId);
|
|
18
|
+
return convertCodingSequenceToPeptides({
|
|
19
|
+
cds,
|
|
20
|
+
sequence,
|
|
21
|
+
codonTable,
|
|
22
|
+
});
|
|
16
23
|
}
|
|
17
24
|
export function revlist(list, seqlen) {
|
|
18
25
|
return list
|
|
@@ -33,8 +40,17 @@ export function getProteinSequenceFromFeature({ feature, seq, }) {
|
|
|
33
40
|
end: sub.end - start,
|
|
34
41
|
}))
|
|
35
42
|
.filter(subfeature => subfeature.type === 'CDS') ?? [], feat => `${feat.start}-${feat.end}`);
|
|
43
|
+
// a mitochondrial gene declares e.g. transl_table=2, so it translates with
|
|
44
|
+
// NCBI table 2 rather than the standard code. GFF3 usually carries the
|
|
45
|
+
// attribute on the CDS rather than the transcript, so check both.
|
|
46
|
+
const cdsSubfeature = feature
|
|
47
|
+
.get('subfeatures')
|
|
48
|
+
?.find((f) => f.get('type')?.toLowerCase() === 'cds');
|
|
49
|
+
const geneticCodeId = parseTranslTable(feature.get('transl_table')) ??
|
|
50
|
+
parseTranslTable(cdsSubfeature?.get('transl_table'));
|
|
36
51
|
return calculateProteinSequence({
|
|
37
52
|
cds: strand === -1 ? revlist(cds, seq.length) : cds,
|
|
38
53
|
sequence: strand === -1 ? revcom(seq) : seq,
|
|
54
|
+
geneticCodeId,
|
|
39
55
|
});
|
|
40
56
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface NcbiGeneticCode {
|
|
2
|
+
id: number;
|
|
3
|
+
name: string;
|
|
4
|
+
ncbieaa: string;
|
|
5
|
+
sncbieaa: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const ncbiGeneticCodes: NcbiGeneticCode[];
|
|
8
|
+
export interface GeneticCode {
|
|
9
|
+
id: number;
|
|
10
|
+
name: string;
|
|
11
|
+
codonTable: Record<string, string>;
|
|
12
|
+
starts: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function getGeneticCode(id?: number): GeneticCode;
|
|
15
|
+
export declare function parseTranslTable(value: unknown): number | undefined;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
export const ncbiGeneticCodes = [
|
|
2
|
+
{
|
|
3
|
+
id: 1,
|
|
4
|
+
name: 'Standard',
|
|
5
|
+
ncbieaa: 'FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
|
|
6
|
+
sncbieaa: '---M------**--*----M---------------M----------------------------',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
id: 2,
|
|
10
|
+
name: 'Vertebrate Mitochondrial',
|
|
11
|
+
ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIMMTTTTNNKKSS**VVVVAAAADDEEGGGG',
|
|
12
|
+
sncbieaa: '----------**--------------------MMMM----------**---M------------',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 3,
|
|
16
|
+
name: 'Yeast Mitochondrial',
|
|
17
|
+
ncbieaa: 'FFLLSSSSYY**CCWWTTTTPPPPHHQQRRRRIIMMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
|
|
18
|
+
sncbieaa: '----------**----------------------MM---------------M------------',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 4,
|
|
22
|
+
name: 'Mold Mitochondrial; Protozoan Mitochondrial; Coelenterate Mitochondrial; Mycoplasma; Spiroplasma',
|
|
23
|
+
ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
|
|
24
|
+
sncbieaa: '--MM------**-------M------------MMMM---------------M------------',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 5,
|
|
28
|
+
name: 'Invertebrate Mitochondrial',
|
|
29
|
+
ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIMMTTTTNNKKSSSSVVVVAAAADDEEGGGG',
|
|
30
|
+
sncbieaa: '---M------**--------------------MMMM---------------M------------',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 6,
|
|
34
|
+
name: 'Ciliate Nuclear; Dasycladacean Nuclear; Hexamita Nuclear',
|
|
35
|
+
ncbieaa: 'FFLLSSSSYYQQCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
|
|
36
|
+
sncbieaa: '--------------*--------------------M----------------------------',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 9,
|
|
40
|
+
name: 'Echinoderm Mitochondrial; Flatworm Mitochondrial',
|
|
41
|
+
ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNNKSSSSVVVVAAAADDEEGGGG',
|
|
42
|
+
sncbieaa: '----------**-----------------------M---------------M------------',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 10,
|
|
46
|
+
name: 'Euplotid Nuclear',
|
|
47
|
+
ncbieaa: 'FFLLSSSSYY**CCCWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
|
|
48
|
+
sncbieaa: '----------**-----------------------M----------------------------',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 11,
|
|
52
|
+
name: 'Bacterial, Archaeal and Plant Plastid',
|
|
53
|
+
ncbieaa: 'FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
|
|
54
|
+
sncbieaa: '---M------**--*----M------------MMMM---------------M------------',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 12,
|
|
58
|
+
name: 'Alternative Yeast Nuclear',
|
|
59
|
+
ncbieaa: 'FFLLSSSSYY**CC*WLLLSPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
|
|
60
|
+
sncbieaa: '----------**--*----M---------------M----------------------------',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 13,
|
|
64
|
+
name: 'Ascidian Mitochondrial',
|
|
65
|
+
ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIMMTTTTNNKKSSGGVVVVAAAADDEEGGGG',
|
|
66
|
+
sncbieaa: '---M------**----------------------MM---------------M------------',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 14,
|
|
70
|
+
name: 'Alternative Flatworm Mitochondrial',
|
|
71
|
+
ncbieaa: 'FFLLSSSSYYY*CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNNKSSSSVVVVAAAADDEEGGGG',
|
|
72
|
+
sncbieaa: '-----------*-----------------------M----------------------------',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 15,
|
|
76
|
+
name: 'Blepharisma Macronuclear',
|
|
77
|
+
ncbieaa: 'FFLLSSSSYY*QCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
|
|
78
|
+
sncbieaa: '----------*---*--------------------M----------------------------',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 16,
|
|
82
|
+
name: 'Chlorophycean Mitochondrial',
|
|
83
|
+
ncbieaa: 'FFLLSSSSYY*LCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
|
|
84
|
+
sncbieaa: '----------*---*--------------------M----------------------------',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 21,
|
|
88
|
+
name: 'Trematode Mitochondrial',
|
|
89
|
+
ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIMMTTTTNNNKSSSSVVVVAAAADDEEGGGG',
|
|
90
|
+
sncbieaa: '----------**-----------------------M---------------M------------',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 22,
|
|
94
|
+
name: 'Scenedesmus obliquus Mitochondrial',
|
|
95
|
+
ncbieaa: 'FFLLSS*SYY*LCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
|
|
96
|
+
sncbieaa: '------*---*---*--------------------M----------------------------',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 23,
|
|
100
|
+
name: 'Thraustochytrium Mitochondrial',
|
|
101
|
+
ncbieaa: 'FF*LSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
|
|
102
|
+
sncbieaa: '--*-------**--*-----------------M--M---------------M------------',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 24,
|
|
106
|
+
name: 'Rhabdopleuridae Mitochondrial',
|
|
107
|
+
ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSSKVVVVAAAADDEEGGGG',
|
|
108
|
+
sncbieaa: '---M------**-------M---------------M---------------M------------',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 25,
|
|
112
|
+
name: 'Candidate Division SR1 and Gracilibacteria',
|
|
113
|
+
ncbieaa: 'FFLLSSSSYY**CCGWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
|
|
114
|
+
sncbieaa: '---M------**-----------------------M---------------M------------',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 26,
|
|
118
|
+
name: 'Pachysolen tannophilus Nuclear',
|
|
119
|
+
ncbieaa: 'FFLLSSSSYY**CC*WLLLAPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
|
|
120
|
+
sncbieaa: '----------**--*----M---------------M----------------------------',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: 27,
|
|
124
|
+
name: 'Karyorelict Nuclear',
|
|
125
|
+
ncbieaa: 'FFLLSSSSYYQQCCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
|
|
126
|
+
sncbieaa: '--------------*--------------------M----------------------------',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: 28,
|
|
130
|
+
name: 'Condylostoma Nuclear',
|
|
131
|
+
ncbieaa: 'FFLLSSSSYYQQCCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
|
|
132
|
+
sncbieaa: '----------**--*--------------------M----------------------------',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 29,
|
|
136
|
+
name: 'Mesodinium Nuclear',
|
|
137
|
+
ncbieaa: 'FFLLSSSSYYYYCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
|
|
138
|
+
sncbieaa: '--------------*--------------------M----------------------------',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: 30,
|
|
142
|
+
name: 'Peritrich Nuclear',
|
|
143
|
+
ncbieaa: 'FFLLSSSSYYEECC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
|
|
144
|
+
sncbieaa: '--------------*--------------------M----------------------------',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 31,
|
|
148
|
+
name: 'Blastocrithidia Nuclear',
|
|
149
|
+
ncbieaa: 'FFLLSSSSYYEECCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
|
|
150
|
+
sncbieaa: '----------**-----------------------M----------------------------',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: 32,
|
|
154
|
+
name: 'Balanophoraceae Plastid',
|
|
155
|
+
ncbieaa: 'FFLLSSSSYY*WCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
|
|
156
|
+
sncbieaa: '---M------*---*----M------------MMMM---------------M------------',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: 33,
|
|
160
|
+
name: 'Cephalodiscidae Mitochondrial',
|
|
161
|
+
ncbieaa: 'FFLLSSSSYYY*CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSSKVVVVAAAADDEEGGGG',
|
|
162
|
+
sncbieaa: '---M-------*-------M---------------M---------------M------------',
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
// The codon order shared by every NCBI table -- the Base1/Base2/Base3 comment
|
|
166
|
+
// rows in gc.prt. codon i = BASE1[i] + BASE2[i] + BASE3[i].
|
|
167
|
+
const BASE1 = 'TTTTTTTTTTTTTTTTCCCCCCCCCCCCCCCCAAAAAAAAAAAAAAAAGGGGGGGGGGGGGGGG';
|
|
168
|
+
const BASE2 = 'TTTTCCCCAAAAGGGGTTTTCCCCAAAAGGGGTTTTCCCCAAAAGGGGTTTTCCCCAAAAGGGG';
|
|
169
|
+
const BASE3 = 'TCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAG';
|
|
170
|
+
const CODONS = Array.from({ length: 64 }, (_, i) => BASE1[i] + BASE2[i] + BASE3[i]);
|
|
171
|
+
const ncbiCodeById = new Map(ncbiGeneticCodes.map(t => [t.id, t]));
|
|
172
|
+
// Expand an uppercase codon map so every case combination of a triplet resolves,
|
|
173
|
+
// which is what callers reading raw sequence need.
|
|
174
|
+
function caseExpand(table) {
|
|
175
|
+
const out = {};
|
|
176
|
+
for (const [codon, aa] of Object.entries(table)) {
|
|
177
|
+
const cases = (i) => {
|
|
178
|
+
const n = codon.charAt(i);
|
|
179
|
+
return [n.toUpperCase(), n.toLowerCase()];
|
|
180
|
+
};
|
|
181
|
+
for (const n0 of cases(0)) {
|
|
182
|
+
for (const n1 of cases(1)) {
|
|
183
|
+
for (const n2 of cases(2)) {
|
|
184
|
+
out[n0 + n1 + n2] = aa;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
function buildGeneticCode(id) {
|
|
192
|
+
const def = ncbiCodeById.get(id);
|
|
193
|
+
if (!def && id !== 1) {
|
|
194
|
+
console.warn(`Unknown genetic code (transl_table=${id}); using standard code`);
|
|
195
|
+
}
|
|
196
|
+
const { id: resolvedId, name, ncbieaa, sncbieaa, } = def ?? ncbiCodeById.get(1);
|
|
197
|
+
const table = {};
|
|
198
|
+
const starts = [];
|
|
199
|
+
for (const [i, codon] of CODONS.entries()) {
|
|
200
|
+
table[codon] = ncbieaa[i];
|
|
201
|
+
if (sncbieaa[i] === 'M') {
|
|
202
|
+
starts.push(codon);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return { id: resolvedId, name, codonTable: caseExpand(table), starts };
|
|
206
|
+
}
|
|
207
|
+
const geneticCodeCache = new Map();
|
|
208
|
+
// Resolves the codon map + start set for an NCBI translation-table id, falling
|
|
209
|
+
// back to the standard code (1) for an unrecognized id. Memoized: there are only
|
|
210
|
+
// ~27 tables and each result is immutable.
|
|
211
|
+
export function getGeneticCode(id = 1) {
|
|
212
|
+
let code = geneticCodeCache.get(id);
|
|
213
|
+
if (!code) {
|
|
214
|
+
code = buildGeneticCode(id);
|
|
215
|
+
geneticCodeCache.set(id, code);
|
|
216
|
+
}
|
|
217
|
+
return code;
|
|
218
|
+
}
|
|
219
|
+
// Parses a GFF/GenBank `transl_table` attribute value into an NCBI table id. The
|
|
220
|
+
// GFF adapter yields a string (or an array if the attribute repeated), so this
|
|
221
|
+
// normalizes both; returns undefined for a missing or non-positive-integer value
|
|
222
|
+
// so callers fall back to their default code.
|
|
223
|
+
export function parseTranslTable(value) {
|
|
224
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
225
|
+
const n = Number(raw);
|
|
226
|
+
return Number.isInteger(n) && n > 0 ? n : undefined;
|
|
227
|
+
}
|
|
@@ -4,6 +4,7 @@ import LaunchMsaViewDialog from './components/LaunchMsaViewDialog';
|
|
|
4
4
|
function isDisplay(elt) {
|
|
5
5
|
return elt.name === 'LinearBasicDisplay';
|
|
6
6
|
}
|
|
7
|
+
const GENE_LIKE_TYPES = new Set(['gene', 'mRNA', 'transcript']);
|
|
7
8
|
function extendStateModel(stateModel) {
|
|
8
9
|
return stateModel.views((self) => {
|
|
9
10
|
const superContextMenuItems = self.contextMenuItems;
|
|
@@ -11,35 +12,40 @@ function extendStateModel(stateModel) {
|
|
|
11
12
|
contextMenuItems() {
|
|
12
13
|
const track = getContainingTrack(self);
|
|
13
14
|
const session = getSession(track);
|
|
15
|
+
const launch = (feature) => {
|
|
16
|
+
session.queueDialog(handleClose => [
|
|
17
|
+
LaunchMsaViewDialog,
|
|
18
|
+
{ model: track, handleClose, feature },
|
|
19
|
+
]);
|
|
20
|
+
};
|
|
14
21
|
const info = self.contextMenuInfo;
|
|
15
|
-
const
|
|
22
|
+
const fetchFullFeature = self.fetchFullFeature;
|
|
23
|
+
const legacyFeature = self.contextMenuFeature;
|
|
24
|
+
const onClick = info && fetchFullFeature && self.isGeneLike
|
|
25
|
+
? () => {
|
|
26
|
+
fetchFullFeature(info.item.featureId, info.displayedRegionIndex)
|
|
27
|
+
.then(feature => {
|
|
28
|
+
if (feature) {
|
|
29
|
+
launch(feature);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
session.notify('Could not load feature for MSA view', 'warning');
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
.catch((e) => {
|
|
36
|
+
session.notifyError(`${e}`, e);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
: legacyFeature &&
|
|
40
|
+
GENE_LIKE_TYPES.has(String(legacyFeature.get('type')))
|
|
41
|
+
? () => {
|
|
42
|
+
launch(legacyFeature);
|
|
43
|
+
}
|
|
44
|
+
: undefined;
|
|
16
45
|
return [
|
|
17
46
|
...superContextMenuItems(),
|
|
18
|
-
...(
|
|
19
|
-
? [
|
|
20
|
-
{
|
|
21
|
-
label: 'Launch MSA view',
|
|
22
|
-
icon: AddIcon,
|
|
23
|
-
onClick: () => {
|
|
24
|
-
self
|
|
25
|
-
.fetchFullFeature(info.item.featureId, info.displayedRegionIndex)
|
|
26
|
-
.then(feature => {
|
|
27
|
-
if (feature) {
|
|
28
|
-
session.queueDialog(handleClose => [
|
|
29
|
-
LaunchMsaViewDialog,
|
|
30
|
-
{ model: track, handleClose, feature },
|
|
31
|
-
]);
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
session.notify('Could not load feature for MSA view', 'warning');
|
|
35
|
-
}
|
|
36
|
-
})
|
|
37
|
-
.catch((e) => {
|
|
38
|
-
session.notifyError(`${e}`, e);
|
|
39
|
-
});
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
]
|
|
47
|
+
...(onClick
|
|
48
|
+
? [{ label: 'Launch MSA view', icon: AddIcon, onClick }]
|
|
43
49
|
: []),
|
|
44
50
|
];
|
|
45
51
|
},
|
|
@@ -7,36 +7,26 @@ import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
|
|
|
7
7
|
export async function doLaunchBlast({ self, }) {
|
|
8
8
|
const { baseUrl, blastDatabase, blastProgram, msaAlgorithm, proteinSequence, selectedTranscript, rid: existingRid, } = self.blastParams;
|
|
9
9
|
const cleanedSeq = cleanProteinSequence(proteinSequence);
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const onProgress = (arg) => {
|
|
11
|
+
self.setProgress(arg);
|
|
12
|
+
};
|
|
12
13
|
if (existingRid) {
|
|
14
|
+
// publish it before the first poll so the view can link out to NCBI while
|
|
15
|
+
// the job is still running
|
|
13
16
|
self.setRid(existingRid);
|
|
14
|
-
const result = await queryBlastFromRid({
|
|
15
|
-
rid: existingRid,
|
|
16
|
-
baseUrl,
|
|
17
|
-
onProgress: arg => {
|
|
18
|
-
self.setProgress(arg);
|
|
19
|
-
},
|
|
20
|
-
});
|
|
21
|
-
hits = result.hits;
|
|
22
|
-
rid = result.rid;
|
|
23
17
|
}
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
const { hits, rid } = existingRid
|
|
19
|
+
? await queryBlastFromRid({ rid: existingRid, baseUrl, onProgress })
|
|
20
|
+
: await queryBlast({
|
|
26
21
|
query: cleanedSeq,
|
|
27
22
|
blastDatabase,
|
|
28
23
|
blastProgram,
|
|
29
24
|
baseUrl,
|
|
30
|
-
onProgress
|
|
31
|
-
self.setProgress(arg);
|
|
32
|
-
},
|
|
25
|
+
onProgress,
|
|
33
26
|
onRid: r => {
|
|
34
27
|
self.setRid(r);
|
|
35
28
|
},
|
|
36
29
|
});
|
|
37
|
-
hits = result.hits;
|
|
38
|
-
rid = result.rid;
|
|
39
|
-
}
|
|
40
30
|
self.setProgress('Fetching species taxonomy info...');
|
|
41
31
|
const taxids = hits
|
|
42
32
|
.map(h => h.description[0]?.taxid)
|
|
@@ -57,9 +47,7 @@ export async function doLaunchBlast({ self, }) {
|
|
|
57
47
|
const result = await launchMSA({
|
|
58
48
|
algorithm: msaAlgorithm,
|
|
59
49
|
sequence: [`>QUERY\n${cleanedSeq}`, ...sequences].join('\n'),
|
|
60
|
-
onProgress
|
|
61
|
-
self.setProgress(arg);
|
|
62
|
-
},
|
|
50
|
+
onProgress,
|
|
63
51
|
});
|
|
64
52
|
const treeMetadataJson = JSON.stringify(treeMetadata);
|
|
65
53
|
await saveBlastResult({
|
|
@@ -6,18 +6,25 @@ export function genomeToMSA({ model }) {
|
|
|
6
6
|
if (!connectedView?.initialized || !hasHoverPosition(hovered)) {
|
|
7
7
|
return undefined;
|
|
8
8
|
}
|
|
9
|
-
const { coord
|
|
9
|
+
const { coord, refName } = hovered.hoverPosition;
|
|
10
|
+
// hoverPosition.coord is a 1-based display coordinate (core's pxToBp adds the
|
|
11
|
+
// +1), while g2p and mafRegion are keyed by 0-based genome position
|
|
12
|
+
const genomePos = coord - 1;
|
|
10
13
|
if (mafRegion) {
|
|
11
14
|
if (refName !== mafRegion.refName ||
|
|
12
15
|
!connectedView.assemblyNames.includes(mafRegion.assemblyName) ||
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
genomePos < mafRegion.start ||
|
|
17
|
+
genomePos >= mafRegion.end) {
|
|
15
18
|
return undefined;
|
|
16
19
|
}
|
|
17
|
-
return model.seqPosToVisibleCol(querySeqName,
|
|
20
|
+
return model.seqPosToVisibleCol(querySeqName, genomePos - mafRegion.start);
|
|
18
21
|
}
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
// session.hovered is global -- set by whichever LinearGenomeView the cursor
|
|
23
|
+
// was last over, on any assembly -- so the refName gate is load bearing:
|
|
24
|
+
// without it the same numeric coordinate on an unrelated chromosome matches a
|
|
25
|
+
// g2p key and lights up a column for a different locus
|
|
26
|
+
if (refName === transcriptToMsaMap?.refName) {
|
|
27
|
+
const seqPos = transcriptToMsaMap.g2p[genomePos];
|
|
21
28
|
if (seqPos !== undefined) {
|
|
22
29
|
return model.seqPosToVisibleCol(querySeqName, seqPos);
|
|
23
30
|
}
|
|
@@ -76,8 +76,9 @@ describe('genomeToMSA', () => {
|
|
|
76
76
|
seqPosToVisibleCol: mockSeqPosToVisibleCol,
|
|
77
77
|
};
|
|
78
78
|
const result = genomeToMSA({ model });
|
|
79
|
-
// coord 1005 -
|
|
80
|
-
|
|
79
|
+
// hover coord 1005 is 1-based, so the 0-based genome position is 1004,
|
|
80
|
+
// which is ungapped position 4 of a region starting at 1000
|
|
81
|
+
expect(mockSeqPosToVisibleCol).toHaveBeenCalledWith('hg38.chr1', 4);
|
|
81
82
|
expect(result).toBe(5);
|
|
82
83
|
});
|
|
83
84
|
test('returns undefined when hover refName does not match mafRegion', () => {
|
|
@@ -106,10 +107,11 @@ describe('genomeToMSA', () => {
|
|
|
106
107
|
expect(result).toBeUndefined();
|
|
107
108
|
});
|
|
108
109
|
test('returns undefined when hover coord is before mafRegion start', () => {
|
|
110
|
+
// 1-based coord 1000 is the 0-based base 999, one before the region
|
|
109
111
|
mockGetSession.mockReturnValue({
|
|
110
112
|
hovered: {
|
|
111
113
|
hoverFeature: {},
|
|
112
|
-
hoverPosition: { coord:
|
|
114
|
+
hoverPosition: { coord: 1000, refName: 'chr1' },
|
|
113
115
|
},
|
|
114
116
|
});
|
|
115
117
|
const model = {
|
|
@@ -131,10 +133,11 @@ describe('genomeToMSA', () => {
|
|
|
131
133
|
expect(result).toBeUndefined();
|
|
132
134
|
});
|
|
133
135
|
test('returns undefined when hover coord is at or after mafRegion end', () => {
|
|
136
|
+
// 1-based coord 1011 is the 0-based base 1010, one past the region
|
|
134
137
|
mockGetSession.mockReturnValue({
|
|
135
138
|
hovered: {
|
|
136
139
|
hoverFeature: {},
|
|
137
|
-
hoverPosition: { coord:
|
|
140
|
+
hoverPosition: { coord: 1011, refName: 'chr1' },
|
|
138
141
|
},
|
|
139
142
|
});
|
|
140
143
|
const model = {
|
|
@@ -193,7 +196,9 @@ describe('genomeToMSA', () => {
|
|
|
193
196
|
const model = {
|
|
194
197
|
querySeqName: 'QUERY',
|
|
195
198
|
transcriptToMsaMap: {
|
|
196
|
-
|
|
199
|
+
refName: 'chr1',
|
|
200
|
+
// g2p is keyed by 0-based genome position, the hover coord is 1-based
|
|
201
|
+
g2p: { 1004: 10 },
|
|
197
202
|
},
|
|
198
203
|
mafRegion: undefined,
|
|
199
204
|
connectedView: { initialized: true },
|
|
@@ -203,6 +208,29 @@ describe('genomeToMSA', () => {
|
|
|
203
208
|
expect(mockSeqPosToVisibleCol).toHaveBeenCalledWith('QUERY', 10);
|
|
204
209
|
expect(result).toBe(10);
|
|
205
210
|
});
|
|
211
|
+
test('returns undefined when the hover is on another refName', () => {
|
|
212
|
+
// session.hovered is global, so a hover on an unrelated chromosome can
|
|
213
|
+
// carry a coordinate that happens to be a g2p key
|
|
214
|
+
mockGetSession.mockReturnValue({
|
|
215
|
+
hovered: {
|
|
216
|
+
hoverFeature: {},
|
|
217
|
+
hoverPosition: { coord: 1005, refName: 'chr2' },
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
const mockSeqPosToVisibleCol = vi.fn();
|
|
221
|
+
const model = {
|
|
222
|
+
querySeqName: 'QUERY',
|
|
223
|
+
transcriptToMsaMap: {
|
|
224
|
+
refName: 'chr1',
|
|
225
|
+
g2p: { 1004: 10 },
|
|
226
|
+
},
|
|
227
|
+
mafRegion: undefined,
|
|
228
|
+
connectedView: { initialized: true },
|
|
229
|
+
seqPosToVisibleCol: mockSeqPosToVisibleCol,
|
|
230
|
+
};
|
|
231
|
+
expect(genomeToMSA({ model })).toBeUndefined();
|
|
232
|
+
expect(mockSeqPosToVisibleCol).not.toHaveBeenCalled();
|
|
233
|
+
});
|
|
206
234
|
test('returns undefined when g2p has no mapping for coord', () => {
|
|
207
235
|
mockGetSession.mockReturnValue({
|
|
208
236
|
hovered: {
|
|
@@ -213,7 +241,8 @@ describe('genomeToMSA', () => {
|
|
|
213
241
|
const model = {
|
|
214
242
|
querySeqName: 'QUERY',
|
|
215
243
|
transcriptToMsaMap: {
|
|
216
|
-
|
|
244
|
+
refName: 'chr1',
|
|
245
|
+
g2p: { 1000: 0 }, // No entry for 1004
|
|
217
246
|
},
|
|
218
247
|
mafRegion: undefined,
|
|
219
248
|
connectedView: { initialized: true },
|