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.
Files changed (59) hide show
  1. package/dist/AddHighlightModel/MsaToGenomeHighlight.js +6 -5
  2. package/dist/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.js +1 -1
  3. package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastPanel.js +1 -1
  4. package/dist/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.d.ts +1 -1
  5. package/dist/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.js +5 -2
  6. package/dist/LaunchMsaView/components/calculateProteinSequence.d.ts +3 -2
  7. package/dist/LaunchMsaView/components/calculateProteinSequence.js +31 -15
  8. package/dist/LaunchMsaView/components/geneticCodes.d.ts +15 -0
  9. package/dist/LaunchMsaView/components/geneticCodes.js +227 -0
  10. package/dist/LaunchMsaView/components/types.d.ts +1 -0
  11. package/dist/LaunchMsaView/index.js +32 -26
  12. package/dist/MsaViewPanel/doLaunchBlast.js +10 -22
  13. package/dist/MsaViewPanel/genomeToMSA.js +13 -6
  14. package/dist/MsaViewPanel/genomeToMSA.test.js +35 -6
  15. package/dist/MsaViewPanel/model.d.ts +68 -66
  16. package/dist/MsaViewPanel/model.js +14 -12
  17. package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +32 -13
  18. package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +43 -14
  19. package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +108 -18
  20. package/dist/MsaViewPanel/msaDataStore.js +8 -17
  21. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +8 -6
  22. package/dist/jbrowse-plugin-msaview.umd.production.min.js +31 -31
  23. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  24. package/dist/utils/blastCache.d.ts +1 -2
  25. package/dist/utils/blastCache.js +9 -22
  26. package/dist/utils/domainCache.js +6 -15
  27. package/dist/utils/idb.d.ts +12 -0
  28. package/dist/utils/idb.js +21 -0
  29. package/dist/utils/taxonomyNames.js +13 -18
  30. package/dist/utils/useLocalStorage.d.ts +1 -0
  31. package/dist/utils/useLocalStorage.js +29 -0
  32. package/dist/version.d.ts +1 -1
  33. package/dist/version.js +1 -1
  34. package/package.json +5 -3
  35. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +6 -8
  36. package/src/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.tsx +1 -1
  37. package/src/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastPanel.tsx +1 -1
  38. package/src/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.ts +4 -2
  39. package/src/LaunchMsaView/components/calculateProteinSequence.ts +35 -20
  40. package/src/LaunchMsaView/components/geneticCodes.ts +298 -0
  41. package/src/LaunchMsaView/components/types.ts +3 -0
  42. package/src/LaunchMsaView/index.ts +44 -35
  43. package/src/MsaViewPanel/doLaunchBlast.ts +20 -29
  44. package/src/MsaViewPanel/genomeToMSA.test.ts +38 -6
  45. package/src/MsaViewPanel/genomeToMSA.ts +14 -6
  46. package/src/MsaViewPanel/model.ts +17 -12
  47. package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +117 -18
  48. package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +70 -26
  49. package/src/MsaViewPanel/msaDataStore.ts +17 -17
  50. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +8 -6
  51. package/src/utils/blastCache.ts +20 -23
  52. package/src/utils/domainCache.ts +14 -18
  53. package/src/utils/idb.ts +28 -0
  54. package/src/utils/taxonomyNames.ts +22 -24
  55. package/src/utils/useLocalStorage.ts +31 -0
  56. package/src/version.ts +1 -1
  57. package/dist/MsaViewPanel/blosum62.d.ts +0 -2
  58. package/dist/MsaViewPanel/blosum62.js +0 -627
  59. 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 clickHighlight = msaView?.connectedClickHighlight;
15
- const hoverHighlight = hasHoverPosition(hovered)
16
- ? undefined
17
- : msaView?.connectedHoverHighlight;
18
- const highlights = [clickHighlight, hoverHighlight].filter((r) => r !== undefined);
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 All")),
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
  export declare function useCachedBlastResults(geneIds: string[]): {
2
- results: any[];
2
+ results: import("../../../utils/blastCache").CachedBlastResult[];
3
3
  error: any;
4
4
  isLoading: boolean;
5
5
  handleDelete: (id: string) => Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import useSWR from 'swr';
2
- import { clearAllCachedResults, deleteCachedResult, getAllCachedResults, } from '../../../utils/blastCache';
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 clearAllCachedResults();
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 stitch(subfeats: Feat[], sequence: string): string;
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, defaultCodonTable, generateCodonTable, revcom, } from '@jbrowse/core/util';
2
- // pure constant: the standard codon table never varies, so build it once rather
3
- // than on every translation (which runs on every panel re-render)
4
- const codonTable = generateCodonTable(defaultCodonTable);
5
- export function stitch(subfeats, sequence) {
6
- return subfeats.map(sub => sequence.slice(sub.start, sub.end)).join('');
7
- }
8
- export function calculateProteinSequence({ cds, sequence, }) {
9
- const str = stitch(cds, sequence);
10
- let protein = '';
11
- for (let i = 0; i < str.length; i += 3) {
12
- // use & symbol for undefined codon, or partial slice
13
- protein += codonTable[str.slice(i, i + 3)] ?? '&';
14
- }
15
- return protein;
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
+ }
@@ -2,6 +2,7 @@ export interface Feat {
2
2
  start: number;
3
3
  end: number;
4
4
  type?: string;
5
+ phase?: number;
5
6
  }
6
7
  export interface SeqState {
7
8
  seq: string;
@@ -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 showMsaMenuItem = info && self.isGeneLike;
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
- ...(showMsaMenuItem
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
- let hits;
11
- let rid;
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
- else {
25
- const result = await queryBlast({
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: arg => {
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: arg => {
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: hoverCoord, refName } = hovered.hoverPosition;
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
- hoverCoord < mafRegion.start ||
14
- hoverCoord >= mafRegion.end) {
16
+ genomePos < mafRegion.start ||
17
+ genomePos >= mafRegion.end) {
15
18
  return undefined;
16
19
  }
17
- return model.seqPosToVisibleCol(querySeqName, hoverCoord - mafRegion.start);
20
+ return model.seqPosToVisibleCol(querySeqName, genomePos - mafRegion.start);
18
21
  }
19
- if (transcriptToMsaMap) {
20
- const seqPos = transcriptToMsaMap.g2p[hoverCoord];
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 - start 1000 = ungapped position 5
80
- expect(mockSeqPosToVisibleCol).toHaveBeenCalledWith('hg38.chr1', 5);
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: 999, refName: 'chr1' },
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: 1010, refName: 'chr1' },
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
- g2p: { 1005: 10 },
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
- g2p: { 1000: 0 }, // No entry for 1005
244
+ refName: 'chr1',
245
+ g2p: { 1000: 0 }, // No entry for 1004
217
246
  },
218
247
  mafRegion: undefined,
219
248
  connectedView: { initialized: true },