jbrowse-plugin-msaview 2.7.2 → 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 (44) 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/useCachedBlastResults.d.ts +1 -1
  4. package/dist/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.js +5 -2
  5. package/dist/MsaViewPanel/doLaunchBlast.js +10 -22
  6. package/dist/MsaViewPanel/genomeToMSA.js +13 -6
  7. package/dist/MsaViewPanel/genomeToMSA.test.js +35 -6
  8. package/dist/MsaViewPanel/model.d.ts +68 -66
  9. package/dist/MsaViewPanel/model.js +14 -12
  10. package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +32 -13
  11. package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +43 -14
  12. package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +108 -18
  13. package/dist/MsaViewPanel/msaDataStore.js +8 -17
  14. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +8 -6
  15. package/dist/jbrowse-plugin-msaview.umd.production.min.js +31 -31
  16. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  17. package/dist/utils/blastCache.d.ts +1 -2
  18. package/dist/utils/blastCache.js +9 -22
  19. package/dist/utils/domainCache.js +6 -15
  20. package/dist/utils/idb.d.ts +12 -0
  21. package/dist/utils/idb.js +21 -0
  22. package/dist/utils/taxonomyNames.js +13 -18
  23. package/dist/version.d.ts +1 -1
  24. package/dist/version.js +1 -1
  25. package/package.json +2 -2
  26. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +6 -8
  27. package/src/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.tsx +1 -1
  28. package/src/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.ts +4 -2
  29. package/src/MsaViewPanel/doLaunchBlast.ts +20 -29
  30. package/src/MsaViewPanel/genomeToMSA.test.ts +38 -6
  31. package/src/MsaViewPanel/genomeToMSA.ts +14 -6
  32. package/src/MsaViewPanel/model.ts +17 -12
  33. package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +117 -18
  34. package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +70 -26
  35. package/src/MsaViewPanel/msaDataStore.ts +17 -17
  36. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +8 -6
  37. package/src/utils/blastCache.ts +20 -23
  38. package/src/utils/domainCache.ts +14 -18
  39. package/src/utils/idb.ts +28 -0
  40. package/src/utils/taxonomyNames.ts +22 -24
  41. package/src/version.ts +1 -1
  42. package/dist/MsaViewPanel/blosum62.d.ts +0 -2
  43. package/dist/MsaViewPanel/blosum62.js +0 -627
  44. package/src/MsaViewPanel/blosum62.ts +0 -628
@@ -1,27 +1,56 @@
1
+ import { getCodonRanges } from 'g2p_mapper';
1
2
  import { gappedToUngappedPosition } from './structureConnection';
2
- export function msaCoordToGenomeCoord({ model, coord: mouseCol, }) {
3
- const { querySeqName, transcriptToMsaMap, mafRegion } = model;
4
- const querySeq = model.rows.find(f => f[0] === querySeqName)?.[1];
3
+ /**
4
+ * The genome regions covered by MSA column `coord` of the query row, in 0-based
5
+ * half-open coordinates (what bpToPx and navTo take).
6
+ *
7
+ * Usually one region -- one codon, or one base in a MAF alignment -- but a
8
+ * codon split across an exon boundary yields one region per contiguous piece,
9
+ * which is why this returns a list.
10
+ */
11
+ export function msaCoordToGenomeRegions({ model, coord: mouseCol, }) {
12
+ const { querySeqName, transcriptToMsaMap, mafRegion, rows } = model;
13
+ const querySeq = rows.find(f => f[0] === querySeqName)?.[1];
5
14
  if (!querySeq) {
6
- return undefined;
15
+ return [];
7
16
  }
8
17
  const ungappedPos = gappedToUngappedPosition(querySeq, mouseCol);
9
18
  if (ungappedPos === undefined) {
10
- return undefined;
19
+ return [];
11
20
  }
12
21
  if (mafRegion) {
13
22
  const genomePos = mafRegion.start + ungappedPos;
14
23
  return genomePos < mafRegion.end
15
- ? { refName: mafRegion.refName, start: genomePos, end: genomePos + 1 }
16
- : undefined;
24
+ ? [{ refName: mafRegion.refName, start: genomePos, end: genomePos + 1 }]
25
+ : [];
17
26
  }
18
27
  if (transcriptToMsaMap) {
19
- const { refName, p2g } = transcriptToMsaMap;
20
- const s = p2g[ungappedPos];
21
- const e = p2g[ungappedPos + 1];
22
- return s !== undefined && e !== undefined
23
- ? { refName, start: Math.min(s, e), end: Math.max(s, e) }
24
- : undefined;
28
+ const { refName, p2gCodon } = transcriptToMsaMap;
29
+ // p2gCodon holds every genomic base of the codon, so the range is exact on
30
+ // either strand. Deriving it from consecutive p2g entries instead
31
+ // (p2g[pos]..p2g[pos+1]) was off by one base on the reverse strand -- where
32
+ // p2g stores the codon's *highest* coordinate -- dropped the final residue,
33
+ // whose successor has no p2g entry, and spanned the whole intron for a
34
+ // codon split across an exon boundary.
35
+ return (getCodonRanges(p2gCodon, ungappedPos)?.map(([start, end]) => ({
36
+ refName,
37
+ start,
38
+ end,
39
+ })) ?? []);
25
40
  }
26
- return undefined;
41
+ return [];
42
+ }
43
+ /**
44
+ * A single region spanning the codon at MSA column `coord`, for navigation. For
45
+ * a codon split across an exon boundary this spans the intervening intron.
46
+ */
47
+ export function msaCoordToGenomeCoord(args) {
48
+ const regions = msaCoordToGenomeRegions(args);
49
+ const first = regions[0];
50
+ const last = regions.at(-1);
51
+ // getCodonRanges returns ranges sorted ascending, so first.start..last.end
52
+ // bounds the codon
53
+ return first && last
54
+ ? { refName: first.refName, start: first.start, end: last.end }
55
+ : undefined;
27
56
  }
@@ -1,5 +1,14 @@
1
+ import { genomeToTranscriptSeqMapping } from 'g2p_mapper';
1
2
  import { describe, expect, test } from 'vitest';
2
- import { msaCoordToGenomeCoord } from './msaCoordToGenomeCoord';
3
+ import { msaCoordToGenomeCoord, msaCoordToGenomeRegions, } from './msaCoordToGenomeCoord';
4
+ // codon at protein position i covers three consecutive genome bases starting at
5
+ // 100 + i * 3, i.e. a single-exon forward-strand transcript
6
+ function forwardCodons(n) {
7
+ return Object.fromEntries(Array.from({ length: n }, (_, i) => [
8
+ i,
9
+ [100 + i * 3, 101 + i * 3, 102 + i * 3],
10
+ ]));
11
+ }
3
12
  describe('msaCoordToGenomeCoord', () => {
4
13
  test('returns undefined when neither transcriptToMsaMap nor mafRegion is defined', () => {
5
14
  const model = {
@@ -16,7 +25,7 @@ describe('msaCoordToGenomeCoord', () => {
16
25
  querySeqName: 'QUERY',
17
26
  transcriptToMsaMap: {
18
27
  refName: 'chr1',
19
- p2g: { 0: 100, 1: 103 },
28
+ p2gCodon: forwardCodons(2),
20
29
  },
21
30
  rows: [['OTHER', 'MKAA']],
22
31
  };
@@ -28,7 +37,7 @@ describe('msaCoordToGenomeCoord', () => {
28
37
  querySeqName: 'QUERY',
29
38
  transcriptToMsaMap: {
30
39
  refName: 'chr1',
31
- p2g: { 0: 100, 1: 103 },
40
+ p2gCodon: forwardCodons(2),
32
41
  },
33
42
  rows: [['QUERY', 'M-KA']],
34
43
  };
@@ -41,7 +50,7 @@ describe('msaCoordToGenomeCoord', () => {
41
50
  querySeqName: 'QUERY',
42
51
  transcriptToMsaMap: {
43
52
  refName: 'chr1',
44
- p2g: { 0: 100, 1: 103, 2: 106, 3: 109 },
53
+ p2gCodon: forwardCodons(4),
45
54
  },
46
55
  rows: [['QUERY', 'MKAA']],
47
56
  };
@@ -58,7 +67,7 @@ describe('msaCoordToGenomeCoord', () => {
58
67
  querySeqName: 'QUERY',
59
68
  transcriptToMsaMap: {
60
69
  refName: 'chr1',
61
- p2g: { 0: 100, 1: 103, 2: 106, 3: 109 },
70
+ p2gCodon: forwardCodons(4),
62
71
  },
63
72
  rows: [['QUERY', 'M-K-AA']],
64
73
  // 012345 gapped positions
@@ -79,34 +88,33 @@ describe('msaCoordToGenomeCoord', () => {
79
88
  end: 109,
80
89
  });
81
90
  });
82
- test('returns undefined when p2g mapping is incomplete', () => {
91
+ test('returns undefined when the position has no codon mapping', () => {
83
92
  const model = {
84
93
  querySeqName: 'QUERY',
85
94
  transcriptToMsaMap: {
86
95
  refName: 'chr1',
87
- p2g: { 0: 100 }, // Missing entry for position 1
96
+ p2gCodon: forwardCodons(1),
88
97
  },
89
98
  rows: [['QUERY', 'MKAA']],
90
99
  };
91
- // Position 0 needs p2g[0] and p2g[1], but p2g[1] is missing
92
- const result = msaCoordToGenomeCoord({ model, coord: 0 });
100
+ // ungapped position 1 has no entry in p2gCodon
101
+ const result = msaCoordToGenomeCoord({ model, coord: 1 });
93
102
  expect(result).toBeUndefined();
94
103
  });
95
- test('handles reverse strand (start > end in p2g)', () => {
104
+ test('maps the final residue, whose codon has no successor', () => {
96
105
  const model = {
97
106
  querySeqName: 'QUERY',
98
107
  transcriptToMsaMap: {
99
108
  refName: 'chr1',
100
- p2g: { 0: 109, 1: 106, 2: 103, 3: 100 }, // Reverse strand
109
+ p2gCodon: forwardCodons(4),
101
110
  },
102
111
  rows: [['QUERY', 'MKAA']],
103
112
  };
104
- // Should return min/max correctly
105
- const result = msaCoordToGenomeCoord({ model, coord: 0 });
113
+ const result = msaCoordToGenomeCoord({ model, coord: 3 });
106
114
  expect(result).toEqual({
107
115
  refName: 'chr1',
108
- start: 106, // min(109, 106)
109
- end: 109, // max(109, 106)
116
+ start: 109,
117
+ end: 112,
110
118
  });
111
119
  });
112
120
  test('returns undefined for out of bounds coord', () => {
@@ -114,7 +122,7 @@ describe('msaCoordToGenomeCoord', () => {
114
122
  querySeqName: 'QUERY',
115
123
  transcriptToMsaMap: {
116
124
  refName: 'chr1',
117
- p2g: { 0: 100, 1: 103 },
125
+ p2gCodon: forwardCodons(2),
118
126
  },
119
127
  rows: [['QUERY', 'MK']],
120
128
  };
@@ -127,7 +135,7 @@ describe('msaCoordToGenomeCoord', () => {
127
135
  querySeqName: 'SEQ2',
128
136
  transcriptToMsaMap: {
129
137
  refName: 'chr1',
130
- p2g: { 0: 200, 1: 203 },
138
+ p2gCodon: { 0: [200, 201, 202], 1: [203, 204, 205] },
131
139
  },
132
140
  rows: [
133
141
  ['SEQ1', 'AAAA'],
@@ -142,6 +150,88 @@ describe('msaCoordToGenomeCoord', () => {
142
150
  end: 203,
143
151
  });
144
152
  });
153
+ // The mapping comes from the real g2p_mapper rather than hand-written
154
+ // fixtures: on the reverse strand p2g stores the codon's *highest*
155
+ // coordinate, which is what the old p2g[pos]..p2g[pos+1] arithmetic got wrong
156
+ describe('real g2p_mapper mappings', () => {
157
+ test('forward strand, single exon', () => {
158
+ const { p2gCodon, refName } = genomeToTranscriptSeqMapping({
159
+ refName: 'chr1',
160
+ start: 100,
161
+ end: 112,
162
+ strand: 1,
163
+ subfeatures: [{ refName: 'chr1', type: 'CDS', start: 100, end: 112 }],
164
+ });
165
+ const model = {
166
+ querySeqName: 'QUERY',
167
+ transcriptToMsaMap: { refName, p2gCodon },
168
+ rows: [['QUERY', 'MKAA']],
169
+ };
170
+ expect(msaCoordToGenomeCoord({ model, coord: 0 })).toEqual({
171
+ refName: 'chr1',
172
+ start: 100,
173
+ end: 103,
174
+ });
175
+ expect(msaCoordToGenomeCoord({ model, coord: 3 })).toEqual({
176
+ refName: 'chr1',
177
+ start: 109,
178
+ end: 112,
179
+ });
180
+ });
181
+ test('reverse strand codon covers the last three bases of the CDS', () => {
182
+ const { p2gCodon, refName } = genomeToTranscriptSeqMapping({
183
+ refName: 'chr1',
184
+ start: 100,
185
+ end: 112,
186
+ strand: -1,
187
+ subfeatures: [{ refName: 'chr1', type: 'CDS', start: 100, end: 112 }],
188
+ });
189
+ const model = {
190
+ querySeqName: 'QUERY',
191
+ transcriptToMsaMap: { refName, p2gCodon },
192
+ rows: [['QUERY', 'MKAA']],
193
+ };
194
+ // the first residue is translated from the 3' end of the genome region
195
+ expect(msaCoordToGenomeCoord({ model, coord: 0 })).toEqual({
196
+ refName: 'chr1',
197
+ start: 109,
198
+ end: 112,
199
+ });
200
+ expect(msaCoordToGenomeCoord({ model, coord: 3 })).toEqual({
201
+ refName: 'chr1',
202
+ start: 100,
203
+ end: 103,
204
+ });
205
+ });
206
+ test('codon split across an exon boundary yields one region per piece', () => {
207
+ // exon 1 contributes 4 bases, so residue 1 straddles the intron
208
+ const { p2gCodon, refName } = genomeToTranscriptSeqMapping({
209
+ refName: 'chr1',
210
+ start: 100,
211
+ end: 210,
212
+ strand: 1,
213
+ subfeatures: [
214
+ { refName: 'chr1', type: 'CDS', start: 100, end: 104 },
215
+ { refName: 'chr1', type: 'CDS', start: 200, end: 202 },
216
+ ],
217
+ });
218
+ const model = {
219
+ querySeqName: 'QUERY',
220
+ transcriptToMsaMap: { refName, p2gCodon },
221
+ rows: [['QUERY', 'MK']],
222
+ };
223
+ expect(msaCoordToGenomeRegions({ model, coord: 1 })).toEqual([
224
+ { refName: 'chr1', start: 103, end: 104 },
225
+ { refName: 'chr1', start: 200, end: 202 },
226
+ ]);
227
+ // the single-region form bounds the pieces, for navigation
228
+ expect(msaCoordToGenomeCoord({ model, coord: 1 })).toEqual({
229
+ refName: 'chr1',
230
+ start: 103,
231
+ end: 202,
232
+ });
233
+ });
234
+ });
145
235
  // MAF region tests
146
236
  describe('mafRegion', () => {
147
237
  test('returns genome position for mafRegion mapping', () => {
@@ -217,7 +307,7 @@ describe('msaCoordToGenomeCoord', () => {
217
307
  querySeqName: 'hg38.chr1',
218
308
  transcriptToMsaMap: {
219
309
  refName: 'chr2',
220
- p2g: { 0: 5000, 1: 5003 },
310
+ p2gCodon: { 0: [5000, 5001, 5002], 1: [5003, 5004, 5005] },
221
311
  },
222
312
  mafRegion: {
223
313
  refName: 'chr1',
@@ -1,22 +1,13 @@
1
- import { openDB } from 'idb';
1
+ import { createDbOpener } from '../utils/idb';
2
2
  const DB_NAME = 'jbrowse-msaview-data';
3
3
  const DB_VERSION = 1;
4
4
  const STORE_NAME = 'msa-data';
5
- let dbPromise;
6
- function getDB() {
7
- dbPromise ??= openDB(DB_NAME, DB_VERSION, {
8
- upgrade(db) {
9
- if (!db.objectStoreNames.contains(STORE_NAME)) {
10
- const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' });
11
- store.createIndex('timestamp', 'timestamp', { unique: false });
12
- }
13
- },
14
- }).catch((e) => {
15
- dbPromise = undefined;
16
- throw e;
17
- });
18
- return dbPromise;
19
- }
5
+ const getDB = createDbOpener(DB_NAME, DB_VERSION, db => {
6
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
7
+ const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' });
8
+ store.createIndex('timestamp', 'timestamp', { unique: false });
9
+ }
10
+ });
20
11
  export function generateDataStoreId() {
21
12
  return `msa-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
22
13
  }
@@ -41,7 +32,7 @@ export async function storeMsaData(id, data) {
41
32
  export async function retrieveMsaData(id) {
42
33
  try {
43
34
  const db = await getDB();
44
- const result = (await db.get(STORE_NAME, id));
35
+ const result = await db.get(STORE_NAME, id);
45
36
  if (result) {
46
37
  return {
47
38
  msa: result.msa,
@@ -45,12 +45,14 @@ describe('syncGenomeHoverToMsaColumn (real genomeToMSA mapping)', () => {
45
45
  beforeEach(() => {
46
46
  vi.clearAllMocks();
47
47
  });
48
- test('genome hover at coord 1005 highlights MSA column 5', () => {
48
+ test('genome hover at coord 1005 highlights MSA column 4', () => {
49
49
  const { model, calls } = makeModel();
50
50
  const run = syncGenomeHoverToMsaColumn(model);
51
- hoverGenome(1005); // 1005 - mafRegion.start(1000) = ungapped 5
51
+ // the hover coord is 1-based, so 1005 is the 0-based base 1004, i.e.
52
+ // ungapped offset 4 into a region starting at 1000
53
+ hoverGenome(1005);
52
54
  run();
53
- expect(calls).toEqual([5]);
55
+ expect(calls).toEqual([4]);
54
56
  });
55
57
  test('moving the genome hover moves the highlighted column', () => {
56
58
  const { model, calls } = makeModel();
@@ -59,7 +61,7 @@ describe('syncGenomeHoverToMsaColumn (real genomeToMSA mapping)', () => {
59
61
  run();
60
62
  hoverGenome(1007);
61
63
  run();
62
- expect(calls).toEqual([2, 7]);
64
+ expect(calls).toEqual([1, 6]);
63
65
  });
64
66
  test('leaving the genome clears the column it set', () => {
65
67
  const { model, calls } = makeModel();
@@ -68,7 +70,7 @@ describe('syncGenomeHoverToMsaColumn (real genomeToMSA mapping)', () => {
68
70
  run();
69
71
  clearGenomeHover();
70
72
  run();
71
- expect(calls).toEqual([4, undefined]);
73
+ expect(calls).toEqual([3, undefined]);
72
74
  });
73
75
  test('a hover outside the maf region clears a previously-set column once', () => {
74
76
  const { model, calls } = makeModel();
@@ -78,7 +80,7 @@ describe('syncGenomeHoverToMsaColumn (real genomeToMSA mapping)', () => {
78
80
  hoverGenome(5000); // outside [1000,1010) -> genomeToMSA returns undefined
79
81
  run();
80
82
  run();
81
- expect(calls).toEqual([4, undefined]);
83
+ expect(calls).toEqual([3, undefined]);
82
84
  });
83
85
  test('never touches mouseCol when the genome never provides a column, so a direct MSA hover survives unrelated session hovers', () => {
84
86
  const { model, calls } = makeModel();