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.
- package/dist/AddHighlightModel/MsaToGenomeHighlight.js +6 -5
- package/dist/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.js +1 -1
- package/dist/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.d.ts +1 -1
- package/dist/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.js +5 -2
- 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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +6 -8
- package/src/LaunchMsaView/components/NCBIBlastQuery/CachedBlastResults.tsx +1 -1
- package/src/LaunchMsaView/components/NCBIBlastQuery/useCachedBlastResults.ts +4 -2
- 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/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
|
@@ -1,27 +1,56 @@
|
|
|
1
|
+
import { getCodonRanges } from 'g2p_mapper';
|
|
1
2
|
import { gappedToUngappedPosition } from './structureConnection';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
15
|
+
return [];
|
|
7
16
|
}
|
|
8
17
|
const ungappedPos = gappedToUngappedPosition(querySeq, mouseCol);
|
|
9
18
|
if (ungappedPos === undefined) {
|
|
10
|
-
return
|
|
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
|
-
:
|
|
24
|
+
? [{ refName: mafRegion.refName, start: genomePos, end: genomePos + 1 }]
|
|
25
|
+
: [];
|
|
17
26
|
}
|
|
18
27
|
if (transcriptToMsaMap) {
|
|
19
|
-
const { refName,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
96
|
+
p2gCodon: forwardCodons(1),
|
|
88
97
|
},
|
|
89
98
|
rows: [['QUERY', 'MKAA']],
|
|
90
99
|
};
|
|
91
|
-
//
|
|
92
|
-
const result = msaCoordToGenomeCoord({ model, coord:
|
|
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('
|
|
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
|
-
|
|
109
|
+
p2gCodon: forwardCodons(4),
|
|
101
110
|
},
|
|
102
111
|
rows: [['QUERY', 'MKAA']],
|
|
103
112
|
};
|
|
104
|
-
|
|
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:
|
|
109
|
-
end:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 =
|
|
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
|
|
48
|
+
test('genome hover at coord 1005 highlights MSA column 4', () => {
|
|
49
49
|
const { model, calls } = makeModel();
|
|
50
50
|
const run = syncGenomeHoverToMsaColumn(model);
|
|
51
|
-
|
|
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([
|
|
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([
|
|
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([
|
|
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([
|
|
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();
|