jbrowse-plugin-msaview 3.4.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +23 -17
- package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -0
- package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +5 -3
- package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +24 -4
- package/dist/LaunchMsaView/components/BlastQuery/consts.js +44 -2
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +7 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.js +42 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.js +32 -0
- package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -0
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.d.ts +3 -1
- package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +2 -1
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +10 -4
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +4 -1
- package/dist/LaunchMsaView/components/QueryRowSelector.js +21 -5
- package/dist/LaunchMsaView/detectQueryRow.d.ts +11 -0
- package/dist/LaunchMsaView/detectQueryRow.js +11 -4
- package/dist/LaunchMsaView/detectQueryRow.test.js +30 -0
- package/dist/LaunchMsaView/useQueryRowName.d.ts +1 -0
- package/dist/LaunchMsaView/useQueryRowName.js +5 -1
- package/dist/LaunchMsaView/useQueryRowName.test.d.ts +1 -0
- package/dist/LaunchMsaView/useQueryRowName.test.js +23 -0
- package/dist/LaunchMsaViewExtensionPoint/index.js +19 -12
- package/dist/LaunchMsaViewExtensionPoint/index.test.d.ts +1 -0
- package/dist/LaunchMsaViewExtensionPoint/index.test.js +64 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +16 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.js +138 -86
- package/dist/MsaViewPanel/components/LaunchProgress.js +23 -5
- package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -2
- package/dist/MsaViewPanel/components/MsaViewPanel.test.js +40 -0
- package/dist/MsaViewPanel/doLaunchBlast.d.ts +11 -1
- package/dist/MsaViewPanel/doLaunchBlast.js +79 -96
- package/dist/MsaViewPanel/doLaunchBlast.test.d.ts +1 -0
- package/dist/MsaViewPanel/doLaunchBlast.test.js +158 -0
- package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +3 -1
- package/dist/MsaViewPanel/doLaunchOrthologs.js +46 -10
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +48 -24
- package/dist/MsaViewPanel/genomeToMSA.js +4 -4
- package/dist/MsaViewPanel/genomeToMSA.test.js +67 -0
- package/dist/MsaViewPanel/loadProteinDomains.d.ts +8 -2
- package/dist/MsaViewPanel/loadProteinDomains.js +18 -9
- package/dist/MsaViewPanel/loadProteinDomains.test.d.ts +1 -0
- package/dist/MsaViewPanel/loadProteinDomains.test.js +41 -0
- package/dist/MsaViewPanel/model.d.ts +306 -85
- package/dist/MsaViewPanel/model.js +169 -42
- package/dist/MsaViewPanel/model.test.d.ts +1 -0
- package/dist/MsaViewPanel/model.test.js +100 -0
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +12 -1
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +8 -8
- package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +133 -32
- package/dist/MsaViewPanel/msaDataStore.d.ts +7 -3
- package/dist/MsaViewPanel/msaDataStore.js +26 -7
- package/dist/MsaViewPanel/msaDataStore.test.d.ts +1 -0
- package/dist/MsaViewPanel/msaDataStore.test.js +44 -0
- package/dist/MsaViewPanel/observeProteinHighlights.test.js +20 -3
- package/dist/MsaViewPanel/processInit.test.d.ts +1 -0
- package/dist/MsaViewPanel/processInit.test.js +72 -0
- package/dist/MsaViewPanel/resolveConnectedTranscript.d.ts +9 -0
- package/dist/MsaViewPanel/resolveConnectedTranscript.js +122 -0
- package/dist/MsaViewPanel/runLaunch.d.ts +39 -0
- package/dist/MsaViewPanel/runLaunch.js +100 -0
- package/dist/MsaViewPanel/runLaunch.test.d.ts +1 -0
- package/dist/MsaViewPanel/runLaunch.test.js +175 -0
- package/dist/MsaViewPanel/storedData.test.d.ts +1 -0
- package/dist/MsaViewPanel/storedData.test.js +160 -0
- package/dist/MsaViewPanel/structureConnection.d.ts +0 -4
- package/dist/MsaViewPanel/structureConnection.js +0 -19
- package/dist/MsaViewPanel/util.d.ts +39 -0
- package/dist/MsaViewPanel/util.js +44 -0
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +46 -45
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.js +32 -0
- package/dist/utils/blastCache.test.d.ts +1 -0
- package/dist/utils/blastCache.test.js +72 -0
- package/dist/utils/browserAlign.d.ts +42 -0
- package/dist/utils/browserAlign.js +297 -0
- package/dist/utils/browserAlign.test.d.ts +1 -0
- package/dist/utils/browserAlign.test.js +85 -0
- package/dist/utils/ebiBlast.d.ts +13 -2
- package/dist/utils/ebiBlast.js +32 -4
- package/dist/utils/ebiJobDispatcher.d.ts +6 -3
- package/dist/utils/ebiJobDispatcher.js +13 -6
- package/dist/utils/fetch.d.ts +8 -1
- package/dist/utils/fetch.js +28 -2
- package/dist/utils/homologSearch.d.ts +31 -0
- package/dist/utils/homologSearch.js +6 -0
- package/dist/utils/msa.d.ts +5 -11
- package/dist/utils/msa.js +19 -30
- package/dist/utils/msaRows.d.ts +11 -8
- package/dist/utils/msaRows.js +14 -12
- package/dist/utils/ncbiDomains.d.ts +2 -2
- package/dist/utils/ncbiOrthologs.d.ts +11 -1
- package/dist/utils/ncbiOrthologs.js +26 -7
- package/dist/utils/ncbiOrthologs.test.js +23 -1
- package/dist/utils/pantherOrthologs.js +2 -10
- package/dist/utils/phmmer.d.ts +12 -1
- package/dist/utils/phmmer.js +52 -13
- package/dist/utils/poll.d.ts +6 -1
- package/dist/utils/poll.js +7 -2
- package/dist/utils/unirefHomologs.d.ts +94 -0
- package/dist/utils/unirefHomologs.js +193 -0
- package/dist/utils/unirefHomologs.test.d.ts +1 -0
- package/dist/utils/unirefHomologs.test.js +118 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +17 -16
- package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +40 -19
- package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -0
- package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +7 -2
- package/src/LaunchMsaView/components/BlastQuery/consts.ts +50 -2
- package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.ts +43 -0
- package/src/LaunchMsaView/components/BlastQuery/searchChoiceStorage.ts +64 -0
- package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -0
- package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +4 -0
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +12 -5
- package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +4 -1
- package/src/LaunchMsaView/components/QueryRowSelector.tsx +35 -7
- package/src/LaunchMsaView/detectQueryRow.test.ts +34 -0
- package/src/LaunchMsaView/detectQueryRow.ts +22 -4
- package/src/LaunchMsaView/useQueryRowName.test.ts +27 -0
- package/src/LaunchMsaView/useQueryRowName.ts +5 -1
- package/src/LaunchMsaViewExtensionPoint/index.test.ts +74 -0
- package/src/LaunchMsaViewExtensionPoint/index.ts +53 -12
- package/src/MsaViewPanel/afterCreateAutoruns.ts +150 -81
- package/src/MsaViewPanel/components/LaunchProgress.tsx +46 -4
- package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +43 -0
- package/src/MsaViewPanel/components/MsaViewPanel.tsx +8 -2
- package/src/MsaViewPanel/doLaunchBlast.test.ts +207 -0
- package/src/MsaViewPanel/doLaunchBlast.ts +94 -140
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +54 -26
- package/src/MsaViewPanel/doLaunchOrthologs.ts +63 -9
- package/src/MsaViewPanel/genomeToMSA.test.ts +76 -0
- package/src/MsaViewPanel/genomeToMSA.ts +8 -4
- package/src/MsaViewPanel/loadProteinDomains.test.ts +52 -0
- package/src/MsaViewPanel/loadProteinDomains.ts +24 -11
- package/src/MsaViewPanel/model.test.ts +121 -0
- package/src/MsaViewPanel/model.ts +200 -49
- package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +152 -32
- package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +21 -11
- package/src/MsaViewPanel/msaDataStore.test.ts +54 -0
- package/src/MsaViewPanel/msaDataStore.ts +32 -13
- package/src/MsaViewPanel/observeProteinHighlights.test.ts +27 -3
- package/src/MsaViewPanel/processInit.test.ts +84 -0
- package/src/MsaViewPanel/resolveConnectedTranscript.ts +150 -0
- package/src/MsaViewPanel/runLaunch.test.ts +208 -0
- package/src/MsaViewPanel/runLaunch.ts +141 -0
- package/src/MsaViewPanel/storedData.test.ts +196 -0
- package/src/MsaViewPanel/structureConnection.ts +0 -26
- package/src/MsaViewPanel/util.ts +75 -0
- package/src/utils/blastCache.test.ts +86 -0
- package/src/utils/blastCache.ts +34 -1
- package/src/utils/browserAlign.test.ts +102 -0
- package/src/utils/browserAlign.ts +352 -0
- package/src/utils/ebiBlast.ts +42 -2
- package/src/utils/ebiJobDispatcher.ts +18 -3
- package/src/utils/fetch.ts +29 -2
- package/src/utils/homologSearch.ts +51 -0
- package/src/utils/msa.ts +25 -39
- package/src/utils/msaRows.ts +20 -16
- package/src/utils/ncbiDomains.ts +4 -2
- package/src/utils/ncbiOrthologs.test.ts +25 -0
- package/src/utils/ncbiOrthologs.ts +27 -7
- package/src/utils/pantherOrthologs.ts +6 -11
- package/src/utils/phmmer.ts +62 -12
- package/src/utils/poll.ts +8 -1
- package/src/utils/unirefHomologs.test.ts +149 -0
- package/src/utils/unirefHomologs.ts +323 -0
- package/src/version.ts +1 -1
- package/dist/MsaViewPanel/structureConnection.test.js +0 -53
- package/src/MsaViewPanel/structureConnection.test.ts +0 -62
- /package/dist/{MsaViewPanel/structureConnection.test.d.ts → LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.d.ts} +0 -0
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
import { genomeToTranscriptSeqMapping } from 'g2p_mapper';
|
|
2
|
+
import { MSAModelF } from 'react-msaview';
|
|
2
3
|
import { describe, expect, test } from 'vitest';
|
|
3
4
|
import { msaCoordToGenomeCoord, msaCoordToGenomeRegions, } from './msaCoordToGenomeCoord';
|
|
5
|
+
/**
|
|
6
|
+
* A stand-in for the view model, converting exactly as react-msaview does: a
|
|
7
|
+
* visible column is shifted right past every hidden column before it, and the
|
|
8
|
+
* residue is then counted along the row.
|
|
9
|
+
*/
|
|
10
|
+
function coordModel({ rows, blanks = [], ...rest }) {
|
|
11
|
+
return {
|
|
12
|
+
...rest,
|
|
13
|
+
transcriptToMsaMap: rest.transcriptToMsaMap,
|
|
14
|
+
visibleColToSeqPos(rowName, visibleCol) {
|
|
15
|
+
const seq = rows.find(r => r[0] === rowName)?.[1];
|
|
16
|
+
if (seq === undefined) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
let globalCol = visibleCol;
|
|
20
|
+
for (const blank of blanks) {
|
|
21
|
+
if (blank <= globalCol) {
|
|
22
|
+
globalCol++;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const isBlank = (c) => c === '-' || c === '.';
|
|
26
|
+
if (globalCol >= seq.length || isBlank(seq[globalCol])) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
let seqPos = 0;
|
|
30
|
+
for (let i = 0; i < globalCol; i++) {
|
|
31
|
+
if (!isBlank(seq[i])) {
|
|
32
|
+
seqPos++;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return seqPos;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
4
39
|
// codon at protein position i covers three consecutive genome bases starting at
|
|
5
40
|
// 100 + i * 3, i.e. a single-exon forward-strand transcript
|
|
6
41
|
function forwardCodons(n) {
|
|
@@ -11,49 +46,49 @@ function forwardCodons(n) {
|
|
|
11
46
|
}
|
|
12
47
|
describe('msaCoordToGenomeCoord', () => {
|
|
13
48
|
test('returns undefined when neither transcriptToMsaMap nor mafRegion is defined', () => {
|
|
14
|
-
const model = {
|
|
49
|
+
const model = coordModel({
|
|
15
50
|
querySeqName: 'QUERY',
|
|
16
51
|
transcriptToMsaMap: undefined,
|
|
17
52
|
mafRegion: undefined,
|
|
18
53
|
rows: [['QUERY', 'MKAA']],
|
|
19
|
-
};
|
|
54
|
+
});
|
|
20
55
|
const result = msaCoordToGenomeCoord({ model, coord: 0 });
|
|
21
56
|
expect(result).toBeUndefined();
|
|
22
57
|
});
|
|
23
58
|
test('returns undefined when query row is not found', () => {
|
|
24
|
-
const model = {
|
|
59
|
+
const model = coordModel({
|
|
25
60
|
querySeqName: 'QUERY',
|
|
26
61
|
transcriptToMsaMap: {
|
|
27
62
|
refName: 'chr1',
|
|
28
63
|
p2gCodon: forwardCodons(2),
|
|
29
64
|
},
|
|
30
65
|
rows: [['OTHER', 'MKAA']],
|
|
31
|
-
};
|
|
66
|
+
});
|
|
32
67
|
const result = msaCoordToGenomeCoord({ model, coord: 0 });
|
|
33
68
|
expect(result).toBeUndefined();
|
|
34
69
|
});
|
|
35
70
|
test('returns undefined when coord is a gap', () => {
|
|
36
|
-
const model = {
|
|
71
|
+
const model = coordModel({
|
|
37
72
|
querySeqName: 'QUERY',
|
|
38
73
|
transcriptToMsaMap: {
|
|
39
74
|
refName: 'chr1',
|
|
40
75
|
p2gCodon: forwardCodons(2),
|
|
41
76
|
},
|
|
42
77
|
rows: [['QUERY', 'M-KA']],
|
|
43
|
-
};
|
|
78
|
+
});
|
|
44
79
|
// Position 1 is a gap
|
|
45
80
|
const result = msaCoordToGenomeCoord({ model, coord: 1 });
|
|
46
81
|
expect(result).toBeUndefined();
|
|
47
82
|
});
|
|
48
83
|
test('returns genome region for valid non-gap position', () => {
|
|
49
|
-
const model = {
|
|
84
|
+
const model = coordModel({
|
|
50
85
|
querySeqName: 'QUERY',
|
|
51
86
|
transcriptToMsaMap: {
|
|
52
87
|
refName: 'chr1',
|
|
53
88
|
p2gCodon: forwardCodons(4),
|
|
54
89
|
},
|
|
55
90
|
rows: [['QUERY', 'MKAA']],
|
|
56
|
-
};
|
|
91
|
+
});
|
|
57
92
|
// Position 0 (M) should map to ungapped 0, genome 100-103
|
|
58
93
|
const result = msaCoordToGenomeCoord({ model, coord: 0 });
|
|
59
94
|
expect(result).toEqual({
|
|
@@ -63,7 +98,7 @@ describe('msaCoordToGenomeCoord', () => {
|
|
|
63
98
|
});
|
|
64
99
|
});
|
|
65
100
|
test('handles gapped sequence correctly', () => {
|
|
66
|
-
const model = {
|
|
101
|
+
const model = coordModel({
|
|
67
102
|
querySeqName: 'QUERY',
|
|
68
103
|
transcriptToMsaMap: {
|
|
69
104
|
refName: 'chr1',
|
|
@@ -72,7 +107,7 @@ describe('msaCoordToGenomeCoord', () => {
|
|
|
72
107
|
rows: [['QUERY', 'M-K-AA']],
|
|
73
108
|
// 012345 gapped positions
|
|
74
109
|
// 0 1 23 ungapped positions
|
|
75
|
-
};
|
|
110
|
+
});
|
|
76
111
|
// Gapped position 2 (K) = ungapped 1
|
|
77
112
|
const result = msaCoordToGenomeCoord({ model, coord: 2 });
|
|
78
113
|
expect(result).toEqual({
|
|
@@ -89,27 +124,27 @@ describe('msaCoordToGenomeCoord', () => {
|
|
|
89
124
|
});
|
|
90
125
|
});
|
|
91
126
|
test('returns undefined when the position has no codon mapping', () => {
|
|
92
|
-
const model = {
|
|
127
|
+
const model = coordModel({
|
|
93
128
|
querySeqName: 'QUERY',
|
|
94
129
|
transcriptToMsaMap: {
|
|
95
130
|
refName: 'chr1',
|
|
96
131
|
p2gCodon: forwardCodons(1),
|
|
97
132
|
},
|
|
98
133
|
rows: [['QUERY', 'MKAA']],
|
|
99
|
-
};
|
|
134
|
+
});
|
|
100
135
|
// ungapped position 1 has no entry in p2gCodon
|
|
101
136
|
const result = msaCoordToGenomeCoord({ model, coord: 1 });
|
|
102
137
|
expect(result).toBeUndefined();
|
|
103
138
|
});
|
|
104
139
|
test('maps the final residue, whose codon has no successor', () => {
|
|
105
|
-
const model = {
|
|
140
|
+
const model = coordModel({
|
|
106
141
|
querySeqName: 'QUERY',
|
|
107
142
|
transcriptToMsaMap: {
|
|
108
143
|
refName: 'chr1',
|
|
109
144
|
p2gCodon: forwardCodons(4),
|
|
110
145
|
},
|
|
111
146
|
rows: [['QUERY', 'MKAA']],
|
|
112
|
-
};
|
|
147
|
+
});
|
|
113
148
|
const result = msaCoordToGenomeCoord({ model, coord: 3 });
|
|
114
149
|
expect(result).toEqual({
|
|
115
150
|
refName: 'chr1',
|
|
@@ -118,20 +153,20 @@ describe('msaCoordToGenomeCoord', () => {
|
|
|
118
153
|
});
|
|
119
154
|
});
|
|
120
155
|
test('returns undefined for out of bounds coord', () => {
|
|
121
|
-
const model = {
|
|
156
|
+
const model = coordModel({
|
|
122
157
|
querySeqName: 'QUERY',
|
|
123
158
|
transcriptToMsaMap: {
|
|
124
159
|
refName: 'chr1',
|
|
125
160
|
p2gCodon: forwardCodons(2),
|
|
126
161
|
},
|
|
127
162
|
rows: [['QUERY', 'MK']],
|
|
128
|
-
};
|
|
163
|
+
});
|
|
129
164
|
// Position 10 is out of bounds
|
|
130
165
|
const result = msaCoordToGenomeCoord({ model, coord: 10 });
|
|
131
166
|
expect(result).toBeUndefined();
|
|
132
167
|
});
|
|
133
168
|
test('works with multiple rows, uses querySeqName', () => {
|
|
134
|
-
const model = {
|
|
169
|
+
const model = coordModel({
|
|
135
170
|
querySeqName: 'SEQ2',
|
|
136
171
|
transcriptToMsaMap: {
|
|
137
172
|
refName: 'chr1',
|
|
@@ -142,7 +177,7 @@ describe('msaCoordToGenomeCoord', () => {
|
|
|
142
177
|
['SEQ2', 'MKAA'],
|
|
143
178
|
['SEQ3', 'LLLL'],
|
|
144
179
|
],
|
|
145
|
-
};
|
|
180
|
+
});
|
|
146
181
|
const result = msaCoordToGenomeCoord({ model, coord: 0 });
|
|
147
182
|
expect(result).toEqual({
|
|
148
183
|
refName: 'chr1',
|
|
@@ -162,11 +197,11 @@ describe('msaCoordToGenomeCoord', () => {
|
|
|
162
197
|
strand: 1,
|
|
163
198
|
subfeatures: [{ refName: 'chr1', type: 'CDS', start: 100, end: 112 }],
|
|
164
199
|
});
|
|
165
|
-
const model = {
|
|
200
|
+
const model = coordModel({
|
|
166
201
|
querySeqName: 'QUERY',
|
|
167
202
|
transcriptToMsaMap: { refName, p2gCodon },
|
|
168
203
|
rows: [['QUERY', 'MKAA']],
|
|
169
|
-
};
|
|
204
|
+
});
|
|
170
205
|
expect(msaCoordToGenomeCoord({ model, coord: 0 })).toEqual({
|
|
171
206
|
refName: 'chr1',
|
|
172
207
|
start: 100,
|
|
@@ -186,11 +221,11 @@ describe('msaCoordToGenomeCoord', () => {
|
|
|
186
221
|
strand: -1,
|
|
187
222
|
subfeatures: [{ refName: 'chr1', type: 'CDS', start: 100, end: 112 }],
|
|
188
223
|
});
|
|
189
|
-
const model = {
|
|
224
|
+
const model = coordModel({
|
|
190
225
|
querySeqName: 'QUERY',
|
|
191
226
|
transcriptToMsaMap: { refName, p2gCodon },
|
|
192
227
|
rows: [['QUERY', 'MKAA']],
|
|
193
|
-
};
|
|
228
|
+
});
|
|
194
229
|
// the first residue is translated from the 3' end of the genome region
|
|
195
230
|
expect(msaCoordToGenomeCoord({ model, coord: 0 })).toEqual({
|
|
196
231
|
refName: 'chr1',
|
|
@@ -215,11 +250,11 @@ describe('msaCoordToGenomeCoord', () => {
|
|
|
215
250
|
{ refName: 'chr1', type: 'CDS', start: 200, end: 202 },
|
|
216
251
|
],
|
|
217
252
|
});
|
|
218
|
-
const model = {
|
|
253
|
+
const model = coordModel({
|
|
219
254
|
querySeqName: 'QUERY',
|
|
220
255
|
transcriptToMsaMap: { refName, p2gCodon },
|
|
221
256
|
rows: [['QUERY', 'MK']],
|
|
222
|
-
};
|
|
257
|
+
});
|
|
223
258
|
expect(msaCoordToGenomeRegions({ model, coord: 1 })).toEqual([
|
|
224
259
|
{ refName: 'chr1', start: 103, end: 104 },
|
|
225
260
|
{ refName: 'chr1', start: 200, end: 202 },
|
|
@@ -235,7 +270,7 @@ describe('msaCoordToGenomeCoord', () => {
|
|
|
235
270
|
// MAF region tests
|
|
236
271
|
describe('mafRegion', () => {
|
|
237
272
|
test('returns genome position for mafRegion mapping', () => {
|
|
238
|
-
const model = {
|
|
273
|
+
const model = coordModel({
|
|
239
274
|
querySeqName: 'hg38.chr1',
|
|
240
275
|
transcriptToMsaMap: undefined,
|
|
241
276
|
mafRegion: {
|
|
@@ -245,7 +280,7 @@ describe('msaCoordToGenomeCoord', () => {
|
|
|
245
280
|
assemblyName: 'hg38',
|
|
246
281
|
},
|
|
247
282
|
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
248
|
-
};
|
|
283
|
+
});
|
|
249
284
|
// Position 0 should map to genome 1000
|
|
250
285
|
const result = msaCoordToGenomeCoord({ model, coord: 0 });
|
|
251
286
|
expect(result).toEqual({
|
|
@@ -262,7 +297,7 @@ describe('msaCoordToGenomeCoord', () => {
|
|
|
262
297
|
});
|
|
263
298
|
});
|
|
264
299
|
test('handles gaps in mafRegion sequence', () => {
|
|
265
|
-
const model = {
|
|
300
|
+
const model = coordModel({
|
|
266
301
|
querySeqName: 'hg38.chr1',
|
|
267
302
|
transcriptToMsaMap: undefined,
|
|
268
303
|
mafRegion: {
|
|
@@ -274,7 +309,7 @@ describe('msaCoordToGenomeCoord', () => {
|
|
|
274
309
|
rows: [['hg38.chr1', 'AC--GTAC']],
|
|
275
310
|
// Gapped positions: 0 1 2 3 4 5 6 7
|
|
276
311
|
// Ungapped: 0 1 2 3 4 5
|
|
277
|
-
};
|
|
312
|
+
});
|
|
278
313
|
// Position 2 is a gap, should return undefined
|
|
279
314
|
const result = msaCoordToGenomeCoord({ model, coord: 2 });
|
|
280
315
|
expect(result).toBeUndefined();
|
|
@@ -287,7 +322,7 @@ describe('msaCoordToGenomeCoord', () => {
|
|
|
287
322
|
});
|
|
288
323
|
});
|
|
289
324
|
test('returns undefined when position exceeds mafRegion end', () => {
|
|
290
|
-
const model = {
|
|
325
|
+
const model = coordModel({
|
|
291
326
|
querySeqName: 'hg38.chr1',
|
|
292
327
|
transcriptToMsaMap: undefined,
|
|
293
328
|
mafRegion: {
|
|
@@ -297,13 +332,13 @@ describe('msaCoordToGenomeCoord', () => {
|
|
|
297
332
|
assemblyName: 'hg38',
|
|
298
333
|
},
|
|
299
334
|
rows: [['hg38.chr1', 'ACGTACGTAC']], // 10 chars but region is only 5bp
|
|
300
|
-
};
|
|
335
|
+
});
|
|
301
336
|
// Position 8 would be ungapped 8 = genome 1008, but region ends at 1005
|
|
302
337
|
const result = msaCoordToGenomeCoord({ model, coord: 8 });
|
|
303
338
|
expect(result).toBeUndefined();
|
|
304
339
|
});
|
|
305
340
|
test('mafRegion takes precedence over transcriptToMsaMap', () => {
|
|
306
|
-
const model = {
|
|
341
|
+
const model = coordModel({
|
|
307
342
|
querySeqName: 'hg38.chr1',
|
|
308
343
|
transcriptToMsaMap: {
|
|
309
344
|
refName: 'chr2',
|
|
@@ -316,7 +351,7 @@ describe('msaCoordToGenomeCoord', () => {
|
|
|
316
351
|
assemblyName: 'hg38',
|
|
317
352
|
},
|
|
318
353
|
rows: [['hg38.chr1', 'ACGTACGTAC']],
|
|
319
|
-
};
|
|
354
|
+
});
|
|
320
355
|
// Should use mafRegion, not transcriptToMsaMap
|
|
321
356
|
const result = msaCoordToGenomeCoord({ model, coord: 0 });
|
|
322
357
|
expect(result).toEqual({
|
|
@@ -327,3 +362,69 @@ describe('msaCoordToGenomeCoord', () => {
|
|
|
327
362
|
});
|
|
328
363
|
});
|
|
329
364
|
});
|
|
365
|
+
// mouseCol and mouseClickCol are VISIBLE columns, and react-msaview hides
|
|
366
|
+
// columns as soon as a clade collapses or allowedGappyness drops below 100 --
|
|
367
|
+
// which `hideGaps` leaves on by default. Counting residues along the raw gapped
|
|
368
|
+
// row instead landed every hover and click some codons off.
|
|
369
|
+
describe('columns react-msaview is hiding', () => {
|
|
370
|
+
test('a visible column past a hidden one maps to the residue on screen', () => {
|
|
371
|
+
const model = coordModel({
|
|
372
|
+
querySeqName: 'QUERY',
|
|
373
|
+
transcriptToMsaMap: {
|
|
374
|
+
refName: 'chr1',
|
|
375
|
+
p2gCodon: forwardCodons(4),
|
|
376
|
+
},
|
|
377
|
+
rows: [['QUERY', 'M-KA']],
|
|
378
|
+
blanks: [1],
|
|
379
|
+
});
|
|
380
|
+
expect(msaCoordToGenomeCoord({ model, coord: 1 })).toEqual({
|
|
381
|
+
refName: 'chr1',
|
|
382
|
+
start: 103,
|
|
383
|
+
end: 106,
|
|
384
|
+
});
|
|
385
|
+
expect(msaCoordToGenomeCoord({ model, coord: 2 })).toEqual({
|
|
386
|
+
refName: 'chr1',
|
|
387
|
+
start: 106,
|
|
388
|
+
end: 109,
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
test('a MAF row shifts by the hidden columns too', () => {
|
|
392
|
+
const model = coordModel({
|
|
393
|
+
querySeqName: 'hg38.chr1',
|
|
394
|
+
mafRegion: {
|
|
395
|
+
refName: 'chr1',
|
|
396
|
+
start: 1000,
|
|
397
|
+
end: 1008,
|
|
398
|
+
assemblyName: 'hg38',
|
|
399
|
+
},
|
|
400
|
+
rows: [['hg38.chr1', 'AC--GTAC']],
|
|
401
|
+
blanks: [2, 3],
|
|
402
|
+
});
|
|
403
|
+
expect(msaCoordToGenomeCoord({ model, coord: 2 })).toEqual({
|
|
404
|
+
refName: 'chr1',
|
|
405
|
+
start: 1002,
|
|
406
|
+
end: 1003,
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
// the fixture above says what blanks mean; this says react-msaview agrees,
|
|
410
|
+
// so a change to how it hides columns fails here rather than in the browser
|
|
411
|
+
test('against the real react-msaview model', () => {
|
|
412
|
+
const model = MSAModelF().create({
|
|
413
|
+
type: 'MsaView',
|
|
414
|
+
data: { msa: '>QUERY\nM-KA\n>B\nMWKA\n>C\nM-KA\n' },
|
|
415
|
+
allowedGappyness: 50,
|
|
416
|
+
});
|
|
417
|
+
expect(model.blanks).toEqual([1]);
|
|
418
|
+
expect(model.numColumns).toBe(3);
|
|
419
|
+
const fromView = {
|
|
420
|
+
querySeqName: 'QUERY',
|
|
421
|
+
transcriptToMsaMap: { refName: 'chr1', p2gCodon: forwardCodons(4) },
|
|
422
|
+
visibleColToSeqPos: (rowName, visibleCol) => model.visibleColToSeqPos(rowName, visibleCol),
|
|
423
|
+
};
|
|
424
|
+
expect(msaCoordToGenomeCoord({ model: fromView, coord: 1 })).toEqual({
|
|
425
|
+
refName: 'chr1',
|
|
426
|
+
start: 103,
|
|
427
|
+
end: 106,
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
});
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
/** the documents a view keeps in IndexedDB rather than in its session snapshot */
|
|
2
|
+
export interface MsaDataPayload {
|
|
3
3
|
msa?: string;
|
|
4
4
|
tree?: string;
|
|
5
5
|
treeMetadata?: string;
|
|
6
|
-
}
|
|
6
|
+
}
|
|
7
|
+
export declare function generateDataStoreId(): string;
|
|
8
|
+
export declare function storeMsaData(id: string, data: MsaDataPayload): Promise<boolean>;
|
|
7
9
|
export declare function retrieveMsaData(id: string): Promise<{
|
|
8
10
|
msa: string | undefined;
|
|
9
11
|
tree: string | undefined;
|
|
10
12
|
treeMetadata: string | undefined;
|
|
11
13
|
} | undefined>;
|
|
14
|
+
/** drop one view's row, for a reset that just orphaned it */
|
|
15
|
+
export declare function deleteMsaData(id: string): Promise<void>;
|
|
12
16
|
export declare function cleanupOldData(maxAgeMs?: number): Promise<number>;
|
|
@@ -33,20 +33,39 @@ export async function retrieveMsaData(id) {
|
|
|
33
33
|
try {
|
|
34
34
|
const db = await getDB();
|
|
35
35
|
const result = await db.get(STORE_NAME, id);
|
|
36
|
-
if (result) {
|
|
37
|
-
return
|
|
38
|
-
msa: result.msa,
|
|
39
|
-
tree: result.tree,
|
|
40
|
-
treeMetadata: result.treeMetadata,
|
|
41
|
-
};
|
|
36
|
+
if (!result) {
|
|
37
|
+
return undefined;
|
|
42
38
|
}
|
|
43
|
-
|
|
39
|
+
// reading counts as use, which is what makes cleanupOldData's policy
|
|
40
|
+
// "unused for 7 days" rather than "written 7 days ago" -- a session opened
|
|
41
|
+
// every day used to lose its alignment on the eighth
|
|
42
|
+
try {
|
|
43
|
+
await db.put(STORE_NAME, { ...result, timestamp: Date.now() });
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
console.warn('Failed to refresh MSA data timestamp:', e);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
msa: result.msa,
|
|
50
|
+
tree: result.tree,
|
|
51
|
+
treeMetadata: result.treeMetadata,
|
|
52
|
+
};
|
|
44
53
|
}
|
|
45
54
|
catch (e) {
|
|
46
55
|
console.warn('Failed to retrieve MSA data:', e);
|
|
47
56
|
return undefined;
|
|
48
57
|
}
|
|
49
58
|
}
|
|
59
|
+
/** drop one view's row, for a reset that just orphaned it */
|
|
60
|
+
export async function deleteMsaData(id) {
|
|
61
|
+
try {
|
|
62
|
+
const db = await getDB();
|
|
63
|
+
await db.delete(STORE_NAME, id);
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
console.warn('Failed to delete MSA data:', e);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
50
69
|
export async function cleanupOldData(maxAgeMs = 7 * 24 * 60 * 60 * 1000) {
|
|
51
70
|
try {
|
|
52
71
|
const db = await getDB();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { beforeEach, expect, test, vi } from 'vitest';
|
|
2
|
+
import { retrieveMsaData } from './msaDataStore';
|
|
3
|
+
// An in-memory stand-in for the one object store this module opens, with a
|
|
4
|
+
// switch for the write half so a browser that refuses writes can be played back.
|
|
5
|
+
const { rows, state } = vi.hoisted(() => ({
|
|
6
|
+
rows: new Map(),
|
|
7
|
+
state: { putFails: false },
|
|
8
|
+
}));
|
|
9
|
+
vi.mock('../utils/idb', () => ({
|
|
10
|
+
createDbOpener: () => () => Promise.resolve({
|
|
11
|
+
get: (_store, id) => Promise.resolve(rows.get(id)),
|
|
12
|
+
put: (_store, value) => {
|
|
13
|
+
if (state.putFails) {
|
|
14
|
+
return Promise.reject(new Error('QuotaExceededError'));
|
|
15
|
+
}
|
|
16
|
+
rows.set(value.id, value);
|
|
17
|
+
return Promise.resolve(value.id);
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
rows.clear();
|
|
23
|
+
state.putFails = false;
|
|
24
|
+
});
|
|
25
|
+
// cleanupOldData deletes by timestamp, so without this a session opened every
|
|
26
|
+
// single day still lost its alignment on the eighth
|
|
27
|
+
test('reading a row refreshes its timestamp, making the expiry "unused for 7 days"', async () => {
|
|
28
|
+
rows.set('msa-1', { id: 'msa-1', msa: '>a\nMK', timestamp: 1000 });
|
|
29
|
+
const before = Date.now();
|
|
30
|
+
expect((await retrieveMsaData('msa-1'))?.msa).toBe('>a\nMK');
|
|
31
|
+
expect(rows.get('msa-1').timestamp).toBeGreaterThanOrEqual(before);
|
|
32
|
+
});
|
|
33
|
+
test('a row that is no longer there reads as undefined and writes nothing', async () => {
|
|
34
|
+
expect(await retrieveMsaData('msa-gone')).toBeUndefined();
|
|
35
|
+
expect(rows.size).toBe(0);
|
|
36
|
+
});
|
|
37
|
+
// the refresh is housekeeping; failing it must not turn a readable alignment
|
|
38
|
+
// into a missing one, which is what the caller reports as expired
|
|
39
|
+
test('a refresh that fails still hands back the data it read', async () => {
|
|
40
|
+
vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
41
|
+
rows.set('msa-1', { id: 'msa-1', msa: '>a\nMK', timestamp: 1000 });
|
|
42
|
+
state.putFails = true;
|
|
43
|
+
expect((await retrieveMsaData('msa-1'))?.msa).toBe('>a\nMK');
|
|
44
|
+
});
|
|
@@ -14,10 +14,11 @@ const CONNECTED = 'lgv-1';
|
|
|
14
14
|
* column equals the genome coordinate that produced it and the test reads as
|
|
15
15
|
* "these genome coords lit these columns".
|
|
16
16
|
*/
|
|
17
|
-
function makeModel({ highlightColumns } = {}) {
|
|
17
|
+
function makeModel({ highlightColumns, querySeqOffset = 0, } = {}) {
|
|
18
18
|
const calls = [];
|
|
19
19
|
const model = {
|
|
20
20
|
querySeqName: 'query',
|
|
21
|
+
querySeqOffset,
|
|
21
22
|
rows: [['query', 'MKVLTAEEK']],
|
|
22
23
|
connectedViewId: CONNECTED,
|
|
23
24
|
// g2p is indexed by genome coord; identity keeps the arithmetic out of the way
|
|
@@ -26,8 +27,8 @@ function makeModel({ highlightColumns } = {}) {
|
|
|
26
27
|
},
|
|
27
28
|
highlightColumns,
|
|
28
29
|
highlightedColumns: undefined,
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
seqPosToVisibleCol: (_name, pos) => pos,
|
|
31
|
+
visibleColToSeqPos: (_name, col) => col,
|
|
31
32
|
setHighlightedColumns: (cols) => {
|
|
32
33
|
calls.push(cols);
|
|
33
34
|
model.highlightedColumns = cols;
|
|
@@ -69,6 +70,22 @@ describe('the hover channel', () => {
|
|
|
69
70
|
run();
|
|
70
71
|
expect(calls).toEqual([[10, 11, 12]]);
|
|
71
72
|
});
|
|
73
|
+
// a query row trimmed to what BLAST aligned starts partway into the protein,
|
|
74
|
+
// so the structure's residues sit that many columns to the left
|
|
75
|
+
test('a trimmed query row shifts the lit columns by its offset', () => {
|
|
76
|
+
const { model, calls } = makeModel({ querySeqOffset: 4 });
|
|
77
|
+
const run = observeProteinHighlights(model);
|
|
78
|
+
session({ hover: [{ start: 10, end: 13 }] });
|
|
79
|
+
run();
|
|
80
|
+
expect(calls).toEqual([[6, 7, 8]]);
|
|
81
|
+
});
|
|
82
|
+
test('a residue before a trimmed row lights nothing', () => {
|
|
83
|
+
const { model, calls } = makeModel({ querySeqOffset: 20 });
|
|
84
|
+
const run = observeProteinHighlights(model);
|
|
85
|
+
session({ hover: [{ start: 10, end: 13 }] });
|
|
86
|
+
run();
|
|
87
|
+
expect(calls).toEqual([]);
|
|
88
|
+
});
|
|
72
89
|
test('releasing the hover clears the highlight', () => {
|
|
73
90
|
const { model, calls } = makeModel();
|
|
74
91
|
const run = observeProteinHighlights(model);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { beforeEach, expect, test, vi } from 'vitest';
|
|
2
|
+
import { processInit, storeDataToIndexedDB } from './afterCreateAutoruns';
|
|
3
|
+
import { fetchIndexedMsa } from './fetchIndexedMsa';
|
|
4
|
+
vi.mock('./fetchIndexedMsa', () => ({ fetchIndexedMsa: vi.fn() }));
|
|
5
|
+
vi.mock('./msaDataStore', () => ({
|
|
6
|
+
cleanupOldData: vi.fn(),
|
|
7
|
+
generateDataStoreId: vi.fn(() => 'generated'),
|
|
8
|
+
retrieveMsaData: vi.fn(),
|
|
9
|
+
storeMsaData: vi.fn(async () => true),
|
|
10
|
+
}));
|
|
11
|
+
const INDEXED = {
|
|
12
|
+
msaIndexedLocation: { uri: 'http://example.com/msa.fa.gz' },
|
|
13
|
+
msaName: 'ENST00000288602',
|
|
14
|
+
};
|
|
15
|
+
function makeModel(over = {}) {
|
|
16
|
+
const model = {
|
|
17
|
+
init: INDEXED,
|
|
18
|
+
data: {},
|
|
19
|
+
rows: [],
|
|
20
|
+
msaFilehandle: undefined,
|
|
21
|
+
treeFilehandle: undefined,
|
|
22
|
+
isStoringData: false,
|
|
23
|
+
lastStoredData: undefined,
|
|
24
|
+
setError: vi.fn(),
|
|
25
|
+
setUniprotId: vi.fn(),
|
|
26
|
+
setQuerySeqName: vi.fn(),
|
|
27
|
+
setMSAFilehandle: vi.fn(),
|
|
28
|
+
setDataStoreId: vi.fn(),
|
|
29
|
+
setIsStoringData: vi.fn(),
|
|
30
|
+
setLastStoredData: vi.fn(),
|
|
31
|
+
setInit: vi.fn(),
|
|
32
|
+
setMSA(arg) {
|
|
33
|
+
model.data.msa = arg;
|
|
34
|
+
},
|
|
35
|
+
...over,
|
|
36
|
+
};
|
|
37
|
+
return model;
|
|
38
|
+
}
|
|
39
|
+
const settle = () => new Promise(res => setTimeout(res, 0));
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.clearAllMocks();
|
|
42
|
+
});
|
|
43
|
+
// react-msaview drops an inline alignment over 50kb from the snapshot and an
|
|
44
|
+
// indexed block has no filehandle to reload from, so clearing init left a
|
|
45
|
+
// shared session saying the alignment had expired
|
|
46
|
+
test('an indexed init is kept, so the block can be refetched', async () => {
|
|
47
|
+
vi.mocked(fetchIndexedMsa).mockResolvedValue('>a\nMK');
|
|
48
|
+
const model = makeModel();
|
|
49
|
+
processInit(model);
|
|
50
|
+
await settle();
|
|
51
|
+
expect(model.data.msa).toBe('>a\nMK');
|
|
52
|
+
expect(model.setInit).not.toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
test('an alignment already in hand is not refetched', async () => {
|
|
55
|
+
const model = makeModel({ data: { msa: '>a\nMK' } });
|
|
56
|
+
processInit(model);
|
|
57
|
+
await settle();
|
|
58
|
+
expect(fetchIndexedMsa).not.toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
test('a url init is still resolved once and cleared', async () => {
|
|
61
|
+
const model = makeModel({ init: { msaUrl: 'http://example.com/msa.fa' } });
|
|
62
|
+
processInit(model);
|
|
63
|
+
await settle();
|
|
64
|
+
expect(model.setMSAFilehandle).toHaveBeenCalled();
|
|
65
|
+
expect(model.setInit).toHaveBeenCalledWith(undefined);
|
|
66
|
+
});
|
|
67
|
+
test('an indexed view writes no IndexedDB row', async () => {
|
|
68
|
+
const model = makeModel({ rows: [['a', 'MK']], data: { msa: '>a\nMK' } });
|
|
69
|
+
storeDataToIndexedDB(model);
|
|
70
|
+
await settle();
|
|
71
|
+
expect(model.setDataStoreId).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { JBrowsePluginMsaViewModel } from './model';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve `connectedTranscript` into `connectedFeature` once the connected
|
|
4
|
+
* genome view has its assembly, regions and tracks, then hand the translation
|
|
5
|
+
* to whichever launch is waiting for a query. Left in place on failure so the
|
|
6
|
+
* error stays attributable; the autorun's tracked reads are the view's
|
|
7
|
+
* readiness, so nothing refires until that changes.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveConnectedTranscriptIfNeeded(self: JBrowsePluginMsaViewModel): void;
|