jbrowse-plugin-msaview 2.4.5 → 2.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/dist/LaunchMsaView/util.js +1 -1
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +8 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.js +22 -0
- package/dist/MsaViewPanel/model.d.ts +139 -500
- package/dist/MsaViewPanel/model.js +4 -36
- package/dist/MsaViewPanel/pairwiseAlignment.js +2 -9
- package/dist/MsaViewPanel/structureConnection.d.ts +0 -6
- package/dist/MsaViewPanel/structureConnection.js +0 -16
- package/dist/MsaViewPanel/structureConnection.test.js +1 -51
- package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.d.ts +1 -0
- package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +92 -0
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +28 -28
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/msa.js +9 -8
- package/dist/utils/ncbiBlast.js +5 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -9
- package/src/LaunchMsaView/util.ts +1 -3
- package/src/MsaViewPanel/afterCreateAutoruns.ts +22 -0
- package/src/MsaViewPanel/model.ts +4 -42
- package/src/MsaViewPanel/pairwiseAlignment.ts +2 -7
- package/src/MsaViewPanel/structureConnection.test.ts +1 -61
- package/src/MsaViewPanel/structureConnection.ts +0 -22
- package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +112 -0
- package/src/utils/msa.ts +10 -8
- package/src/utils/ncbiBlast.ts +5 -6
- package/src/version.ts +1 -1
package/dist/utils/msa.js
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { textfetch, timeout } from './fetch';
|
|
2
2
|
const base = `https://www.ebi.ac.uk/Tools/services/rest`;
|
|
3
|
+
const email = 'colin.diesh@gmail.com';
|
|
3
4
|
const algorithms = {
|
|
4
5
|
clustalo: {
|
|
5
|
-
params: { email
|
|
6
|
+
params: { email },
|
|
6
7
|
msaResult: 'aln-clustal_num',
|
|
7
8
|
treeResult: 'phylotree',
|
|
8
9
|
},
|
|
9
10
|
muscle: {
|
|
10
|
-
params: { email
|
|
11
|
+
params: { email, format: 'clw', tree: 'tree1' },
|
|
11
12
|
msaResult: 'fa',
|
|
12
13
|
treeResult: 'phylotree',
|
|
13
14
|
},
|
|
14
15
|
kalign: {
|
|
15
|
-
params: { email
|
|
16
|
+
params: { email, stype: 'protein' },
|
|
16
17
|
msaResult: 'fa',
|
|
17
18
|
treeResult: 'phylotree',
|
|
18
19
|
},
|
|
19
20
|
mafft: {
|
|
20
|
-
params: { email
|
|
21
|
+
params: { email, stype: 'protein' },
|
|
21
22
|
msaResult: 'fa',
|
|
22
23
|
treeResult: 'phylotree',
|
|
23
24
|
},
|
|
@@ -25,10 +26,6 @@ const algorithms = {
|
|
|
25
26
|
async function wait({ onProgress, jobId, algorithm, }) {
|
|
26
27
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
27
28
|
while (true) {
|
|
28
|
-
for (let i = 0; i < 10; i++) {
|
|
29
|
-
await timeout(1000);
|
|
30
|
-
onProgress(`Re-checking MSA status in... ${10 - i}`);
|
|
31
|
-
}
|
|
32
29
|
const result = await textfetch(`${base}/${algorithm}/status/${jobId}`);
|
|
33
30
|
if (result === 'FINISHED') {
|
|
34
31
|
break;
|
|
@@ -36,6 +33,10 @@ async function wait({ onProgress, jobId, algorithm, }) {
|
|
|
36
33
|
else if (result.includes('FAILURE')) {
|
|
37
34
|
throw new Error(`Failed to run: jobId ${jobId}`);
|
|
38
35
|
}
|
|
36
|
+
for (let i = 0; i < 10; i++) {
|
|
37
|
+
onProgress(`Re-checking MSA status in... ${10 - i}`);
|
|
38
|
+
await timeout(1000);
|
|
39
|
+
}
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
export async function launchMSA({ algorithm, sequence, onProgress, }) {
|
package/dist/utils/ncbiBlast.js
CHANGED
|
@@ -58,16 +58,16 @@ async function initialQuery({ query, blastProgram, blastDatabase, baseUrl, }) {
|
|
|
58
58
|
async function waitForRid({ rid, onProgress, baseUrl, }) {
|
|
59
59
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
60
60
|
while (true) {
|
|
61
|
-
const iter = 20;
|
|
62
|
-
for (let i = 0; i < iter; i++) {
|
|
63
|
-
await timeout(1000);
|
|
64
|
-
onProgress(`Re-checking BLAST status in... ${iter - i}`);
|
|
65
|
-
}
|
|
66
61
|
const res = await textfetch(`${baseUrl}?CMD=Get&FORMAT_OBJECT=SearchInfo&RID=${rid}`);
|
|
67
62
|
const statusMatch = /\s+Status=(\S+)/m.exec(res);
|
|
68
63
|
const status = statusMatch?.[1];
|
|
69
64
|
const hasHits = /\s+ThereAreHits=yes/m.test(res);
|
|
70
65
|
if (status === 'WAITING') {
|
|
66
|
+
const iter = 20;
|
|
67
|
+
for (let i = 0; i < iter; i++) {
|
|
68
|
+
onProgress(`Re-checking BLAST status in... ${iter - i}`);
|
|
69
|
+
await timeout(1000);
|
|
70
|
+
}
|
|
71
71
|
continue;
|
|
72
72
|
}
|
|
73
73
|
if (status === 'FAILED') {
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "2.
|
|
1
|
+
export declare const version = "2.5.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.
|
|
1
|
+
export const version = '2.5.0';
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.
|
|
2
|
+
"version": "2.5.0",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"name": "jbrowse-plugin-msaview",
|
|
5
5
|
"repository": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"g2p_mapper": "^2.1.5",
|
|
21
21
|
"idb": "^8.0.3",
|
|
22
22
|
"pako-esm2": "^2.0.2",
|
|
23
|
-
"react-msaview": "^5.
|
|
23
|
+
"react-msaview": "^5.1.1",
|
|
24
24
|
"swr": "^2.4.1"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@eslint/js": "^10.0.1",
|
|
29
29
|
"@fal-works/esbuild-plugin-global-externals": "^2.1.2",
|
|
30
30
|
"@jbrowse/core": "^4.3.0",
|
|
31
|
-
"@jbrowse/mobx-state-tree": "^5.
|
|
31
|
+
"@jbrowse/mobx-state-tree": "^5.10.2",
|
|
32
32
|
"@jbrowse/plugin-linear-genome-view": "^4.3.0",
|
|
33
33
|
"@mui/icons-material": "^7.3.11",
|
|
34
34
|
"@mui/material": "^7.3.11",
|
|
@@ -36,19 +36,19 @@
|
|
|
36
36
|
"@mui/x-data-grid": "^8.28.7",
|
|
37
37
|
"@types/node": "^25.9.1",
|
|
38
38
|
"@types/react": "^19.2.15",
|
|
39
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
40
|
-
"@typescript-eslint/parser": "^8.
|
|
39
|
+
"@typescript-eslint/eslint-plugin": "^8.60.0",
|
|
40
|
+
"@typescript-eslint/parser": "^8.60.0",
|
|
41
41
|
"esbuild": "^0.28.0",
|
|
42
42
|
"eslint": "^10.4.0",
|
|
43
43
|
"eslint-plugin-import-x": "^4.16.2",
|
|
44
44
|
"eslint-plugin-react": "^7.37.5",
|
|
45
45
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
46
46
|
"eslint-plugin-unicorn": "^64.0.0",
|
|
47
|
-
"mobx": "^6.15.
|
|
48
|
-
"mobx-react": "^9.2.
|
|
47
|
+
"mobx": "^6.15.4",
|
|
48
|
+
"mobx-react": "^9.2.2",
|
|
49
49
|
"prettier": "^3.8.3",
|
|
50
50
|
"pretty-bytes": "^7.1.0",
|
|
51
|
-
"puppeteer": "^
|
|
51
|
+
"puppeteer": "^25.1.0",
|
|
52
52
|
"react": "^19.2.6",
|
|
53
53
|
"react-dom": "^19.2.6",
|
|
54
54
|
"rimraf": "^6.1.3",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"serve": "^14.2.6",
|
|
57
57
|
"tss-react": "^4.9.21",
|
|
58
58
|
"typescript": "^6.0.3",
|
|
59
|
-
"typescript-eslint": "^8.
|
|
59
|
+
"typescript-eslint": "^8.60.0",
|
|
60
60
|
"vitest": "^4.1.7"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
@@ -40,9 +40,7 @@ export function getTranscriptLength(feature: Feature) {
|
|
|
40
40
|
const cdsLen = sum(
|
|
41
41
|
feature
|
|
42
42
|
.get('subfeatures')
|
|
43
|
-
?.filter(
|
|
44
|
-
f => (f.get('type') as string | undefined)?.toLowerCase() === 'cds',
|
|
45
|
-
)
|
|
43
|
+
?.filter(f => f.get('type') === 'CDS')
|
|
46
44
|
.map(s => s.get('end') - s.get('start')) ?? [],
|
|
47
45
|
)
|
|
48
46
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getSession } from '@jbrowse/core/util'
|
|
2
2
|
|
|
3
3
|
import { doLaunchBlast } from './doLaunchBlast'
|
|
4
|
+
import { genomeToMSA } from './genomeToMSA'
|
|
4
5
|
import {
|
|
5
6
|
cleanupOldData,
|
|
6
7
|
generateDataStoreId,
|
|
@@ -145,6 +146,27 @@ export function processInit(self: JBrowsePluginMsaViewModel) {
|
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Mirror the connected genome view's hover position onto the MSA's hovered
|
|
151
|
+
* column. Returns the autorun body so it can keep a flag tracking whether the
|
|
152
|
+
* MSA's mouseCol was set by this sync: that way an unrelated session hover
|
|
153
|
+
* change clears the column only when the genome put it there, never wiping a
|
|
154
|
+
* column the user is hovering directly in the MSA.
|
|
155
|
+
*/
|
|
156
|
+
export function syncGenomeHoverToMsaColumn(self: JBrowsePluginMsaViewModel) {
|
|
157
|
+
let genomeDrivenCol = false
|
|
158
|
+
return () => {
|
|
159
|
+
const col = genomeToMSA({ model: self })
|
|
160
|
+
if (col !== undefined) {
|
|
161
|
+
self.setMousePos(col)
|
|
162
|
+
genomeDrivenCol = true
|
|
163
|
+
} else if (genomeDrivenCol) {
|
|
164
|
+
self.setMousePos(undefined)
|
|
165
|
+
genomeDrivenCol = false
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
148
170
|
export function highlightConnectedStructures(self: JBrowsePluginMsaViewModel) {
|
|
149
171
|
const { mouseCol, connectedProteinViews } = self
|
|
150
172
|
if (connectedProteinViews.length === 0) {
|
|
@@ -16,14 +16,11 @@ import {
|
|
|
16
16
|
processInit,
|
|
17
17
|
runCleanup,
|
|
18
18
|
storeDataToIndexedDB,
|
|
19
|
+
syncGenomeHoverToMsaColumn,
|
|
19
20
|
} from './afterCreateAutoruns'
|
|
20
|
-
import { genomeToMSA } from './genomeToMSA'
|
|
21
21
|
import { msaCoordToGenomeCoord } from './msaCoordToGenomeCoord'
|
|
22
22
|
import { buildAlignmentMaps, runPairwiseAlignment } from './pairwiseAlignment'
|
|
23
|
-
import {
|
|
24
|
-
getProteinViews,
|
|
25
|
-
ungappedToGappedPosition,
|
|
26
|
-
} from './structureConnection'
|
|
23
|
+
import { getProteinViews } from './structureConnection'
|
|
27
24
|
import { getCanonicalRefName } from './util'
|
|
28
25
|
|
|
29
26
|
import type { ProteinView, StructureConnection } from './structureConnection'
|
|
@@ -209,41 +206,6 @@ export default function stateModelFactory() {
|
|
|
209
206
|
}))
|
|
210
207
|
|
|
211
208
|
.views(self => ({
|
|
212
|
-
/**
|
|
213
|
-
* #getter
|
|
214
|
-
*/
|
|
215
|
-
get structureHoverCol(): number | undefined {
|
|
216
|
-
for (const conn of self.connectedProteinViews) {
|
|
217
|
-
const structure = conn.proteinView.structures[conn.structureIdx]
|
|
218
|
-
const structurePos = structure?.hoverPosition?.structureSeqPos
|
|
219
|
-
if (structurePos !== undefined) {
|
|
220
|
-
const msaUngapped = conn.structureToMsa[structurePos]
|
|
221
|
-
if (msaUngapped !== undefined) {
|
|
222
|
-
const seq = self.getSequenceByRowName(conn.msaRowName)
|
|
223
|
-
if (seq) {
|
|
224
|
-
const globalCol = ungappedToGappedPosition(seq, msaUngapped)
|
|
225
|
-
if (globalCol !== undefined) {
|
|
226
|
-
return self.globalColToVisibleCol(globalCol)
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return undefined
|
|
233
|
-
},
|
|
234
|
-
}))
|
|
235
|
-
|
|
236
|
-
.views(self => ({
|
|
237
|
-
/**
|
|
238
|
-
* #getter
|
|
239
|
-
*/
|
|
240
|
-
get mouseCol2(): number | undefined {
|
|
241
|
-
return (
|
|
242
|
-
self.structureHoverCol ??
|
|
243
|
-
genomeToMSA({ model: self as JBrowsePluginMsaViewModel })
|
|
244
|
-
)
|
|
245
|
-
},
|
|
246
|
-
|
|
247
209
|
/**
|
|
248
210
|
* #getter
|
|
249
211
|
*/
|
|
@@ -394,14 +356,13 @@ export default function stateModelFactory() {
|
|
|
394
356
|
ungappedMsaSequence,
|
|
395
357
|
structureSequence,
|
|
396
358
|
)
|
|
397
|
-
const { seq1ToSeq2
|
|
359
|
+
const { seq1ToSeq2 } = buildAlignmentMaps(alignment)
|
|
398
360
|
|
|
399
361
|
const connection: StructureConnection = {
|
|
400
362
|
proteinViewId,
|
|
401
363
|
structureIdx,
|
|
402
364
|
msaRowName: rowName,
|
|
403
365
|
msaToStructure: Object.fromEntries(seq1ToSeq2),
|
|
404
|
-
structureToMsa: Object.fromEntries(seq2ToSeq1),
|
|
405
366
|
}
|
|
406
367
|
|
|
407
368
|
self.connectedStructures.push(connection)
|
|
@@ -503,6 +464,7 @@ export default function stateModelFactory() {
|
|
|
503
464
|
}),
|
|
504
465
|
)
|
|
505
466
|
}
|
|
467
|
+
addDisposer(self, autorun(syncGenomeHoverToMsaColumn(self)))
|
|
506
468
|
},
|
|
507
469
|
}))
|
|
508
470
|
}
|
|
@@ -128,13 +128,8 @@ function buildConsensus(alignedSeq1: string, alignedSeq2: string) {
|
|
|
128
128
|
for (let i = 0; i < alignedSeq1.length; i++) {
|
|
129
129
|
const a = alignedSeq1[i]!
|
|
130
130
|
const b = alignedSeq2[i]!
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
} else if (a.toUpperCase() === b.toUpperCase()) {
|
|
134
|
-
consensus += '|'
|
|
135
|
-
} else {
|
|
136
|
-
consensus += ' '
|
|
137
|
-
}
|
|
131
|
+
const match = a !== '-' && b !== '-' && a.toUpperCase() === b.toUpperCase()
|
|
132
|
+
consensus += match ? '|' : ' '
|
|
138
133
|
}
|
|
139
134
|
return consensus
|
|
140
135
|
}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
gappedToUngappedPosition,
|
|
5
|
-
ungappedToGappedPosition,
|
|
6
|
-
} from './structureConnection'
|
|
3
|
+
import { gappedToUngappedPosition } from './structureConnection'
|
|
7
4
|
|
|
8
5
|
describe('gappedToUngappedPosition', () => {
|
|
9
6
|
test('returns correct ungapped position for non-gap character', () => {
|
|
@@ -63,60 +60,3 @@ describe('gappedToUngappedPosition', () => {
|
|
|
63
60
|
expect(gappedToUngappedPosition(seq, 2)).toBeUndefined()
|
|
64
61
|
})
|
|
65
62
|
})
|
|
66
|
-
|
|
67
|
-
describe('ungappedToGappedPosition', () => {
|
|
68
|
-
test('returns correct gapped position', () => {
|
|
69
|
-
const seq = 'M-KA-A'
|
|
70
|
-
// 0 12 34 (gapped)
|
|
71
|
-
// 0 1 23 (ungapped)
|
|
72
|
-
expect(ungappedToGappedPosition(seq, 0)).toBe(0) // M
|
|
73
|
-
expect(ungappedToGappedPosition(seq, 1)).toBe(2) // K
|
|
74
|
-
expect(ungappedToGappedPosition(seq, 2)).toBe(3) // A
|
|
75
|
-
expect(ungappedToGappedPosition(seq, 3)).toBe(5) // A
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
test('returns undefined for out-of-bounds ungapped position', () => {
|
|
79
|
-
const seq = 'M-KA'
|
|
80
|
-
expect(ungappedToGappedPosition(seq, 4)).toBeUndefined()
|
|
81
|
-
expect(ungappedToGappedPosition(seq, 100)).toBeUndefined()
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
test('handles sequence with no gaps', () => {
|
|
85
|
-
const seq = 'MKAA'
|
|
86
|
-
expect(ungappedToGappedPosition(seq, 0)).toBe(0)
|
|
87
|
-
expect(ungappedToGappedPosition(seq, 1)).toBe(1)
|
|
88
|
-
expect(ungappedToGappedPosition(seq, 2)).toBe(2)
|
|
89
|
-
expect(ungappedToGappedPosition(seq, 3)).toBe(3)
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
test('handles sequence with leading gaps', () => {
|
|
93
|
-
const seq = '--MKA'
|
|
94
|
-
expect(ungappedToGappedPosition(seq, 0)).toBe(2) // M
|
|
95
|
-
expect(ungappedToGappedPosition(seq, 1)).toBe(3) // K
|
|
96
|
-
expect(ungappedToGappedPosition(seq, 2)).toBe(4) // A
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
test('handles empty sequence', () => {
|
|
100
|
-
expect(ungappedToGappedPosition('', 0)).toBeUndefined()
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
test('handles all-gap sequence', () => {
|
|
104
|
-
const seq = '---'
|
|
105
|
-
expect(ungappedToGappedPosition(seq, 0)).toBeUndefined()
|
|
106
|
-
})
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
describe('gappedToUngappedPosition and ungappedToGappedPosition are inverses', () => {
|
|
110
|
-
test('round-trip conversion works', () => {
|
|
111
|
-
const seq = 'M-KA--YL-S'
|
|
112
|
-
// For each non-gap position, converting to ungapped and back should return original
|
|
113
|
-
for (let i = 0; i < seq.length; i++) {
|
|
114
|
-
if (seq[i] !== '-') {
|
|
115
|
-
const ungapped = gappedToUngappedPosition(seq, i)
|
|
116
|
-
expect(ungapped).toBeDefined()
|
|
117
|
-
const backToGapped = ungappedToGappedPosition(seq, ungapped!)
|
|
118
|
-
expect(backToGapped).toBe(i)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
})
|
|
122
|
-
})
|
|
@@ -40,8 +40,6 @@ export interface StructureConnection {
|
|
|
40
40
|
msaRowName: string
|
|
41
41
|
/** Map from MSA ungapped position to structure sequence position */
|
|
42
42
|
msaToStructure: Record<number, number>
|
|
43
|
-
/** Map from structure sequence position to MSA ungapped position */
|
|
44
|
-
structureToMsa: Record<number, number>
|
|
45
43
|
}
|
|
46
44
|
|
|
47
45
|
/**
|
|
@@ -69,23 +67,3 @@ export function gappedToUngappedPosition(
|
|
|
69
67
|
|
|
70
68
|
return ungapped
|
|
71
69
|
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Helper to convert ungapped position to gapped MSA column for a specific row
|
|
75
|
-
*/
|
|
76
|
-
export function ungappedToGappedPosition(
|
|
77
|
-
sequence: string,
|
|
78
|
-
ungappedPosition: number,
|
|
79
|
-
): number | undefined {
|
|
80
|
-
let ungapped = 0
|
|
81
|
-
for (let i = 0; i < sequence.length; i++) {
|
|
82
|
-
const element = sequence[i]
|
|
83
|
-
if (element !== '-') {
|
|
84
|
-
if (ungapped === ungappedPosition) {
|
|
85
|
-
return i
|
|
86
|
-
}
|
|
87
|
-
ungapped++
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return undefined
|
|
91
|
-
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { getSession } from '@jbrowse/core/util'
|
|
2
|
+
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { syncGenomeHoverToMsaColumn } from './afterCreateAutoruns'
|
|
5
|
+
|
|
6
|
+
import type { JBrowsePluginMsaViewModel } from './model'
|
|
7
|
+
|
|
8
|
+
// Mock only getSession; keep the rest of the util module real so the
|
|
9
|
+
// afterCreateAutoruns import graph still loads.
|
|
10
|
+
vi.mock('@jbrowse/core/util', async importOriginal => ({
|
|
11
|
+
...(await importOriginal<Record<string, unknown>>()),
|
|
12
|
+
getSession: vi.fn(),
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
const mockGetSession = vi.mocked(getSession)
|
|
16
|
+
|
|
17
|
+
const mafRegion = {
|
|
18
|
+
refName: 'chr1',
|
|
19
|
+
start: 1000,
|
|
20
|
+
end: 1010,
|
|
21
|
+
assemblyName: 'hg38',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// A model wired through the real genomeToMSA path: a connected genome view
|
|
25
|
+
// over a maf region, with seqPosToVisibleCol as identity so the asserted
|
|
26
|
+
// column equals the ungapped offset into the region.
|
|
27
|
+
function makeModel() {
|
|
28
|
+
const calls: (number | undefined)[] = []
|
|
29
|
+
const model = {
|
|
30
|
+
querySeqName: 'hg38.chr1',
|
|
31
|
+
transcriptToMsaMap: undefined,
|
|
32
|
+
mafRegion,
|
|
33
|
+
connectedView: { initialized: true, assemblyNames: ['hg38'] },
|
|
34
|
+
seqPosToVisibleCol: (_name: string, pos: number) => pos,
|
|
35
|
+
setMousePos: (col?: number) => {
|
|
36
|
+
calls.push(col)
|
|
37
|
+
},
|
|
38
|
+
} as unknown as JBrowsePluginMsaViewModel
|
|
39
|
+
return { model, calls }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hoverGenome(coord: number) {
|
|
43
|
+
mockGetSession.mockReturnValue({
|
|
44
|
+
hovered: { hoverFeature: {}, hoverPosition: { coord, refName: 'chr1' } },
|
|
45
|
+
} as unknown as ReturnType<typeof getSession>)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function clearGenomeHover() {
|
|
49
|
+
mockGetSession.mockReturnValue({
|
|
50
|
+
hovered: null,
|
|
51
|
+
} as unknown as ReturnType<typeof getSession>)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('syncGenomeHoverToMsaColumn (real genomeToMSA mapping)', () => {
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
vi.clearAllMocks()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('genome hover at coord 1005 highlights MSA column 5', () => {
|
|
60
|
+
const { model, calls } = makeModel()
|
|
61
|
+
const run = syncGenomeHoverToMsaColumn(model)
|
|
62
|
+
|
|
63
|
+
hoverGenome(1005) // 1005 - mafRegion.start(1000) = ungapped 5
|
|
64
|
+
run()
|
|
65
|
+
expect(calls).toEqual([5])
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('moving the genome hover moves the highlighted column', () => {
|
|
69
|
+
const { model, calls } = makeModel()
|
|
70
|
+
const run = syncGenomeHoverToMsaColumn(model)
|
|
71
|
+
|
|
72
|
+
hoverGenome(1002)
|
|
73
|
+
run()
|
|
74
|
+
hoverGenome(1007)
|
|
75
|
+
run()
|
|
76
|
+
expect(calls).toEqual([2, 7])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('leaving the genome clears the column it set', () => {
|
|
80
|
+
const { model, calls } = makeModel()
|
|
81
|
+
const run = syncGenomeHoverToMsaColumn(model)
|
|
82
|
+
|
|
83
|
+
hoverGenome(1004)
|
|
84
|
+
run()
|
|
85
|
+
clearGenomeHover()
|
|
86
|
+
run()
|
|
87
|
+
expect(calls).toEqual([4, undefined])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('a hover outside the maf region clears a previously-set column once', () => {
|
|
91
|
+
const { model, calls } = makeModel()
|
|
92
|
+
const run = syncGenomeHoverToMsaColumn(model)
|
|
93
|
+
|
|
94
|
+
hoverGenome(1004)
|
|
95
|
+
run()
|
|
96
|
+
hoverGenome(5000) // outside [1000,1010) -> genomeToMSA returns undefined
|
|
97
|
+
run()
|
|
98
|
+
run()
|
|
99
|
+
expect(calls).toEqual([4, undefined])
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('never touches mouseCol when the genome never provides a column, so a direct MSA hover survives unrelated session hovers', () => {
|
|
103
|
+
const { model, calls } = makeModel()
|
|
104
|
+
const run = syncGenomeHoverToMsaColumn(model)
|
|
105
|
+
|
|
106
|
+
clearGenomeHover()
|
|
107
|
+
run()
|
|
108
|
+
hoverGenome(9999) // unrelated/out-of-range hover elsewhere
|
|
109
|
+
run()
|
|
110
|
+
expect(calls).toEqual([])
|
|
111
|
+
})
|
|
112
|
+
})
|
package/src/utils/msa.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { textfetch, timeout } from './fetch'
|
|
|
3
3
|
import type { MsaAlgorithm } from '../LaunchMsaView/components/NCBIBlastQuery/consts'
|
|
4
4
|
|
|
5
5
|
const base = `https://www.ebi.ac.uk/Tools/services/rest`
|
|
6
|
+
const email = 'colin.diesh@gmail.com'
|
|
6
7
|
|
|
7
8
|
const algorithms: Record<
|
|
8
9
|
MsaAlgorithm,
|
|
@@ -13,22 +14,22 @@ const algorithms: Record<
|
|
|
13
14
|
}
|
|
14
15
|
> = {
|
|
15
16
|
clustalo: {
|
|
16
|
-
params: { email
|
|
17
|
+
params: { email },
|
|
17
18
|
msaResult: 'aln-clustal_num',
|
|
18
19
|
treeResult: 'phylotree',
|
|
19
20
|
},
|
|
20
21
|
muscle: {
|
|
21
|
-
params: { email
|
|
22
|
+
params: { email, format: 'clw', tree: 'tree1' },
|
|
22
23
|
msaResult: 'fa',
|
|
23
24
|
treeResult: 'phylotree',
|
|
24
25
|
},
|
|
25
26
|
kalign: {
|
|
26
|
-
params: { email
|
|
27
|
+
params: { email, stype: 'protein' },
|
|
27
28
|
msaResult: 'fa',
|
|
28
29
|
treeResult: 'phylotree',
|
|
29
30
|
},
|
|
30
31
|
mafft: {
|
|
31
|
-
params: { email
|
|
32
|
+
params: { email, stype: 'protein' },
|
|
32
33
|
msaResult: 'fa',
|
|
33
34
|
treeResult: 'phylotree',
|
|
34
35
|
},
|
|
@@ -45,10 +46,6 @@ async function wait({
|
|
|
45
46
|
}) {
|
|
46
47
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
47
48
|
while (true) {
|
|
48
|
-
for (let i = 0; i < 10; i++) {
|
|
49
|
-
await timeout(1000)
|
|
50
|
-
onProgress(`Re-checking MSA status in... ${10 - i}`)
|
|
51
|
-
}
|
|
52
49
|
const result = await textfetch(`${base}/${algorithm}/status/${jobId}`)
|
|
53
50
|
|
|
54
51
|
if (result === 'FINISHED') {
|
|
@@ -56,6 +53,11 @@ async function wait({
|
|
|
56
53
|
} else if (result.includes('FAILURE')) {
|
|
57
54
|
throw new Error(`Failed to run: jobId ${jobId}`)
|
|
58
55
|
}
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < 10; i++) {
|
|
58
|
+
onProgress(`Re-checking MSA status in... ${10 - i}`)
|
|
59
|
+
await timeout(1000)
|
|
60
|
+
}
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
|
package/src/utils/ncbiBlast.ts
CHANGED
|
@@ -112,12 +112,6 @@ async function waitForRid({
|
|
|
112
112
|
}) {
|
|
113
113
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
114
114
|
while (true) {
|
|
115
|
-
const iter = 20
|
|
116
|
-
for (let i = 0; i < iter; i++) {
|
|
117
|
-
await timeout(1000)
|
|
118
|
-
onProgress(`Re-checking BLAST status in... ${iter - i}`)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
115
|
const res = await textfetch(
|
|
122
116
|
`${baseUrl}?CMD=Get&FORMAT_OBJECT=SearchInfo&RID=${rid}`,
|
|
123
117
|
)
|
|
@@ -126,6 +120,11 @@ async function waitForRid({
|
|
|
126
120
|
const hasHits = /\s+ThereAreHits=yes/m.test(res)
|
|
127
121
|
|
|
128
122
|
if (status === 'WAITING') {
|
|
123
|
+
const iter = 20
|
|
124
|
+
for (let i = 0; i < iter; i++) {
|
|
125
|
+
onProgress(`Re-checking BLAST status in... ${iter - i}`)
|
|
126
|
+
await timeout(1000)
|
|
127
|
+
}
|
|
129
128
|
continue
|
|
130
129
|
}
|
|
131
130
|
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.
|
|
1
|
+
export const version = '2.5.0'
|