jbrowse-plugin-msaview 3.3.0 → 3.4.1
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/GenomeMouseoverHighlight.js +1 -1
- package/dist/AddHighlightModel/MsaToGenomeHighlight.js +1 -1
- package/dist/AddHighlightModel/index.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +60 -18
- package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.js +2 -2
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.d.ts +12 -0
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +21 -2
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.d.ts +1 -0
- package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.js +29 -0
- package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +28 -0
- package/dist/LaunchMsaView/components/BlastQuery/consts.js +21 -0
- 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.d.ts +1 -0
- package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.test.js +32 -0
- package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -1
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +5 -4
- package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +1 -1
- package/dist/LaunchMsaView/components/useFeatureSequence.js +1 -1
- package/dist/LaunchMsaView/detectQueryRow.d.ts +15 -2
- package/dist/LaunchMsaView/detectQueryRow.js +20 -21
- package/dist/LaunchMsaView/detectQueryRow.test.js +15 -15
- package/dist/LaunchMsaView/useQueryRowName.js +5 -8
- package/dist/LaunchMsaViewExtensionPoint/index.js +11 -6
- package/dist/LaunchMsaViewExtensionPoint/index.test.d.ts +1 -0
- package/dist/LaunchMsaViewExtensionPoint/index.test.js +43 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +15 -0
- package/dist/MsaViewPanel/afterCreateAutoruns.js +95 -72
- package/dist/MsaViewPanel/components/ErrorBoundary.d.ts +2 -2
- package/dist/MsaViewPanel/components/JobLink.js +7 -1
- package/dist/MsaViewPanel/components/LaunchProgress.d.ts +17 -0
- package/dist/MsaViewPanel/components/LaunchProgress.js +50 -0
- package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -3
- package/dist/MsaViewPanel/components/MsaViewPanel.test.d.ts +1 -0
- package/dist/MsaViewPanel/components/MsaViewPanel.test.js +87 -0
- package/dist/MsaViewPanel/doLaunchBlast.d.ts +4 -2
- package/dist/MsaViewPanel/doLaunchBlast.js +86 -56
- package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +3 -1
- package/dist/MsaViewPanel/doLaunchOrthologs.js +6 -5
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +39 -25
- package/dist/MsaViewPanel/genomeToMSA.js +4 -2
- package/dist/MsaViewPanel/genomeToMSA.test.js +34 -0
- package/dist/MsaViewPanel/model.d.ts +190 -19
- package/dist/MsaViewPanel/model.js +51 -1
- package/dist/MsaViewPanel/msaDataStore.d.ts +5 -3
- package/dist/MsaViewPanel/msaDataStore.js +16 -7
- package/dist/MsaViewPanel/msaDataStore.test.d.ts +1 -0
- package/dist/MsaViewPanel/msaDataStore.test.js +44 -0
- package/dist/MsaViewPanel/observeProteinHighlights.test.js +11 -0
- package/dist/MsaViewPanel/runLaunch.d.ts +38 -0
- package/dist/MsaViewPanel/runLaunch.js +65 -0
- package/dist/MsaViewPanel/runLaunch.test.d.ts +1 -0
- package/dist/MsaViewPanel/runLaunch.test.js +129 -0
- package/dist/MsaViewPanel/storedData.test.d.ts +1 -0
- package/dist/MsaViewPanel/storedData.test.js +160 -0
- package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +1 -0
- package/dist/MsaViewPanel/util.d.ts +18 -0
- package/dist/MsaViewPanel/util.js +17 -0
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +47 -35
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.d.ts +10 -6
- package/dist/utils/blastCache.js +47 -3
- package/dist/utils/blastCache.test.d.ts +1 -0
- package/dist/utils/blastCache.test.js +72 -0
- package/dist/utils/ebiBlast.d.ts +5 -3
- package/dist/utils/ebiBlast.js +6 -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/msa.d.ts +15 -1
- package/dist/utils/msa.js +40 -13
- package/dist/utils/msaRows.d.ts +31 -0
- package/dist/utils/msaRows.js +67 -0
- 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 +54 -0
- package/dist/utils/phmmer.js +120 -0
- package/dist/utils/poll.d.ts +6 -1
- package/dist/utils/poll.js +7 -2
- package/dist/utils/taxonomyNames.d.ts +1 -1
- package/dist/utils/taxonomyNames.js +6 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +30 -24
- package/src/AddHighlightModel/GenomeMouseoverHighlight.tsx +1 -1
- package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +1 -1
- package/src/AddHighlightModel/index.tsx +1 -1
- package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +85 -31
- package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -1
- package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +4 -4
- package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.ts +50 -0
- package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +23 -3
- package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +1 -1
- package/src/LaunchMsaView/components/BlastQuery/consts.ts +40 -0
- 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 -1
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +7 -7
- package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +1 -1
- package/src/LaunchMsaView/components/useFeatureSequence.ts +1 -1
- package/src/LaunchMsaView/detectQueryRow.test.ts +17 -15
- package/src/LaunchMsaView/detectQueryRow.ts +34 -23
- package/src/LaunchMsaView/useQueryRowName.ts +6 -9
- package/src/LaunchMsaViewExtensionPoint/index.test.ts +51 -0
- package/src/LaunchMsaViewExtensionPoint/index.ts +21 -6
- package/src/MsaViewPanel/afterCreateAutoruns.ts +102 -68
- package/src/MsaViewPanel/components/ErrorBoundary.tsx +2 -1
- package/src/MsaViewPanel/components/JobLink.tsx +7 -2
- package/src/MsaViewPanel/components/LaunchProgress.tsx +80 -0
- package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +104 -0
- package/src/MsaViewPanel/components/MsaViewPanel.tsx +7 -4
- package/src/MsaViewPanel/doLaunchBlast.ts +134 -72
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +43 -28
- package/src/MsaViewPanel/doLaunchOrthologs.ts +9 -5
- package/src/MsaViewPanel/genomeToMSA.test.ts +37 -0
- package/src/MsaViewPanel/genomeToMSA.ts +6 -2
- package/src/MsaViewPanel/model.ts +82 -6
- package/src/MsaViewPanel/msaDataStore.test.ts +54 -0
- package/src/MsaViewPanel/msaDataStore.ts +22 -13
- package/src/MsaViewPanel/observeProteinHighlights.test.ts +13 -0
- package/src/MsaViewPanel/runLaunch.test.ts +154 -0
- package/src/MsaViewPanel/runLaunch.ts +102 -0
- package/src/MsaViewPanel/storedData.test.ts +196 -0
- package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +1 -0
- package/src/MsaViewPanel/util.ts +18 -0
- package/src/utils/blastCache.test.ts +86 -0
- package/src/utils/blastCache.ts +67 -13
- package/src/utils/ebiBlast.ts +9 -3
- package/src/utils/ebiJobDispatcher.ts +18 -3
- package/src/utils/fetch.ts +29 -2
- package/src/utils/msa.ts +51 -12
- package/src/utils/msaRows.ts +95 -0
- 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 +178 -0
- package/src/utils/poll.ts +8 -1
- package/src/utils/taxonomyNames.ts +6 -1
- package/src/version.ts +1 -1
- package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +0 -6
- package/dist/MsaViewPanel/components/LoadingBLAST.js +0 -26
- package/src/MsaViewPanel/components/LoadingBLAST.tsx +0 -48
|
@@ -53,6 +53,18 @@ function ncbiUrl(url) {
|
|
|
53
53
|
const sep = url.includes('?') ? '&' : '?';
|
|
54
54
|
return `${url}${sep}tool=${NCBI_TOOL}&email=${encodeURIComponent(NCBI_EMAIL)}`;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* A candidate gene reference as the ortholog services know it: a GFF ID prefix
|
|
58
|
+
* (gene:TP53) and a version suffix (NM_000546.6) stripped off. Shared with
|
|
59
|
+
* pantherOrthologs, which asks PANTHER the same question with the same
|
|
60
|
+
* candidates.
|
|
61
|
+
*/
|
|
62
|
+
export function cleanGeneCandidate(raw) {
|
|
63
|
+
return raw
|
|
64
|
+
.trim()
|
|
65
|
+
.replace(/^\w+:/, '')
|
|
66
|
+
.replace(/\.\d+$/, '');
|
|
67
|
+
}
|
|
56
68
|
/**
|
|
57
69
|
* A free-text gene reference -> NCBI gene id. A bare number is taken as the id
|
|
58
70
|
* itself; anything else is searched as a gene name within the query taxon.
|
|
@@ -69,8 +81,7 @@ export async function resolveGeneId(candidates, taxId) {
|
|
|
69
81
|
if (/^\d+$/.test(query)) {
|
|
70
82
|
return { geneId: query, matched: query };
|
|
71
83
|
}
|
|
72
|
-
|
|
73
|
-
const cleaned = query.replace(/^\w+:/, '').replace(/\.\d+$/, '');
|
|
84
|
+
const cleaned = cleanGeneCandidate(query);
|
|
74
85
|
const term = `${cleaned}[Gene Name] AND ${taxId}[taxid]`;
|
|
75
86
|
const json = await jsonfetch(ncbiUrl(`${EUTILS}/esearch.fcgi?db=gene&term=${encodeURIComponent(term)}&retmode=json&retmax=1`));
|
|
76
87
|
const geneId = json.esearchresult?.idlist?.[0];
|
|
@@ -179,15 +190,23 @@ function sanitize(name) {
|
|
|
179
190
|
* Sanitized, unique single-token labels used identically in the FASTA headers,
|
|
180
191
|
* the tree leaf names and the domain GFF seq_ids — that identity is how the
|
|
181
192
|
* viewer pairs a tree leaf to its alignment row to its domain track. Collisions
|
|
182
|
-
* get a numeric suffix rather than silently overwriting a row
|
|
193
|
+
* get a numeric suffix rather than silently overwriting a row, and the suffix
|
|
194
|
+
* climbs past labels already emitted: a species genuinely named `human_2` would
|
|
195
|
+
* otherwise take the label the second `human` is about to get, merging two rows
|
|
196
|
+
* into one.
|
|
183
197
|
*/
|
|
184
198
|
export function dedupeLabels(names) {
|
|
185
|
-
const
|
|
199
|
+
const used = new Set();
|
|
186
200
|
return names.map(name => {
|
|
187
201
|
const base = sanitize(name) || 'row';
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
202
|
+
let label = base;
|
|
203
|
+
let n = 1;
|
|
204
|
+
while (used.has(label)) {
|
|
205
|
+
n += 1;
|
|
206
|
+
label = `${base}_${n}`;
|
|
207
|
+
}
|
|
208
|
+
used.add(label);
|
|
209
|
+
return label;
|
|
191
210
|
});
|
|
192
211
|
}
|
|
193
212
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test, vi } from 'vitest';
|
|
2
|
-
import { dedupeLabels, fetchOrthologGenes, fetchRepresentativeProteins, parseFasta, } from './ncbiOrthologs';
|
|
2
|
+
import { cleanGeneCandidate, dedupeLabels, fetchOrthologGenes, fetchRepresentativeProteins, parseFasta, } from './ncbiOrthologs';
|
|
3
3
|
describe('dedupeLabels', () => {
|
|
4
4
|
test('sanitizes to single tokens', () => {
|
|
5
5
|
// labels are used identically as FASTA headers, Newick leaf names and GFF
|
|
@@ -22,6 +22,28 @@ describe('dedupeLabels', () => {
|
|
|
22
22
|
test('falls back for a name with no usable characters', () => {
|
|
23
23
|
expect(dedupeLabels(['...', '...'])).toEqual(['row', 'row_2']);
|
|
24
24
|
});
|
|
25
|
+
test('climbs past a suffix a literal name already took', () => {
|
|
26
|
+
// a label pairs a tree leaf to its alignment row, so handing the same one
|
|
27
|
+
// to two rows merges two species into one silently. The third label is ugly
|
|
28
|
+
// and unique, which is the trade
|
|
29
|
+
expect(dedupeLabels(['human', 'human', 'human_2'])).toEqual([
|
|
30
|
+
'human',
|
|
31
|
+
'human_2',
|
|
32
|
+
'human_2_2',
|
|
33
|
+
]);
|
|
34
|
+
expect(dedupeLabels(['human_2', 'human', 'human'])).toEqual([
|
|
35
|
+
'human_2',
|
|
36
|
+
'human',
|
|
37
|
+
'human_3',
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('cleanGeneCandidate', () => {
|
|
42
|
+
test('strips what a GFF feature carries that a gene lookup does not want', () => {
|
|
43
|
+
expect(cleanGeneCandidate(' gene:TP53 ')).toBe('TP53');
|
|
44
|
+
expect(cleanGeneCandidate('NM_000546.6')).toBe('NM_000546');
|
|
45
|
+
expect(cleanGeneCandidate('TP53')).toBe('TP53');
|
|
46
|
+
});
|
|
25
47
|
});
|
|
26
48
|
describe('parseFasta', () => {
|
|
27
49
|
test('keys by the first header token and joins wrapped lines', () => {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// GenPept records with CDD Region features, so the overlay attaches as it does
|
|
17
17
|
// to a RefSeq accession.
|
|
18
18
|
import { jsonfetch } from './fetch';
|
|
19
|
-
import { dedupeLabels, defaultMaxSpecies } from './ncbiOrthologs';
|
|
19
|
+
import { cleanGeneCandidate, dedupeLabels, defaultMaxSpecies, } from './ncbiOrthologs';
|
|
20
20
|
import { fetchTaxonomyInfo } from './taxonomyNames';
|
|
21
21
|
const PANTHER = 'https://pantherdb.org/services/oai/pantherdb';
|
|
22
22
|
const UNIPROT = 'https://rest.uniprot.org/uniprotkb';
|
|
@@ -122,14 +122,6 @@ async function fetchSequences(accessions) {
|
|
|
122
122
|
}
|
|
123
123
|
return map;
|
|
124
124
|
}
|
|
125
|
-
// strip a version suffix (NM_000546.6) and any GFF ID prefix (gene:TP53), as
|
|
126
|
-
// resolveGeneId does for NCBI
|
|
127
|
-
function cleanCandidate(raw) {
|
|
128
|
-
return raw
|
|
129
|
-
.trim()
|
|
130
|
-
.replace(/^\w+:/, '')
|
|
131
|
-
.replace(/\.\d+$/, '');
|
|
132
|
-
}
|
|
133
125
|
/**
|
|
134
126
|
* One `matchortho` per candidate until PANTHER maps one. A JBrowse feature
|
|
135
127
|
* carries whatever its GFF/BigBed had — `id()`, `name`, `gene_name` — and only
|
|
@@ -139,7 +131,7 @@ function cleanCandidate(raw) {
|
|
|
139
131
|
async function matchOrthologs(candidates, taxId, targets) {
|
|
140
132
|
let matched;
|
|
141
133
|
for (const raw of candidates) {
|
|
142
|
-
const query =
|
|
134
|
+
const query = cleanGeneCandidate(raw);
|
|
143
135
|
if (!query) {
|
|
144
136
|
continue;
|
|
145
137
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { PhmmerDatabase } from '../LaunchMsaView/components/BlastQuery/consts';
|
|
2
|
+
export interface PhmmerRow {
|
|
3
|
+
accession: string;
|
|
4
|
+
id: string;
|
|
5
|
+
sciname: string;
|
|
6
|
+
taxid?: number;
|
|
7
|
+
title?: string;
|
|
8
|
+
/** the matched envelope on the target, e.g. '503-912', absent if unparseable */
|
|
9
|
+
range?: string;
|
|
10
|
+
/** the row as phmmer aligned it, uppercased with '.' inserts turned into '-' */
|
|
11
|
+
aligned: string;
|
|
12
|
+
}
|
|
13
|
+
export interface PhmmerAlignment {
|
|
14
|
+
rows: PhmmerRow[];
|
|
15
|
+
/**
|
|
16
|
+
* the query, placed into the same columns. phmmer does not put the query in
|
|
17
|
+
* its own output, so this is derived — see buildQueryRow.
|
|
18
|
+
*/
|
|
19
|
+
queryRow: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Human-facing link to a job, shown while it runs and on error.
|
|
23
|
+
*
|
|
24
|
+
* The category has to be sss: jdispatcher serves its shell with a 200 for any
|
|
25
|
+
* category, so /pfa/ and /psa/ look fine to a fetch and render "Page Not Found"
|
|
26
|
+
* in a browser.
|
|
27
|
+
*/
|
|
28
|
+
export declare function phmmerResultUrl(jobId: string): string;
|
|
29
|
+
/** EBI job ids are prefixed with the tool that made them */
|
|
30
|
+
export declare function isPhmmerJobId(jobId: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Exported for testing against a captured .sto — the annotation names (RF, the
|
|
33
|
+
* DE line's OS=/OX=) are the whole risk in this mapping, and nothing else in CI
|
|
34
|
+
* would notice if HMMER or EBI changed one.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parsePhmmerAlignment({ stockholm, query, }: {
|
|
37
|
+
stockholm: string;
|
|
38
|
+
query: string;
|
|
39
|
+
}): PhmmerAlignment;
|
|
40
|
+
export declare function queryPhmmer({ query, database, onProgress, onRid, signal, }: {
|
|
41
|
+
query: string;
|
|
42
|
+
database: PhmmerDatabase;
|
|
43
|
+
onProgress: (arg: string) => void;
|
|
44
|
+
onRid: (arg: string) => void;
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
rows: PhmmerRow[];
|
|
48
|
+
/**
|
|
49
|
+
* the query, placed into the same columns. phmmer does not put the query in
|
|
50
|
+
* its own output, so this is derived — see buildQueryRow.
|
|
51
|
+
*/
|
|
52
|
+
queryRow: string;
|
|
53
|
+
rid: string;
|
|
54
|
+
}>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { StockholmMSA } from 'msa-parsers';
|
|
2
|
+
import { fetchEbiResult, submitEbiJob, waitForEbiJob } from './ebiJobDispatcher';
|
|
3
|
+
const TOOL = 'hmmer3_phmmer';
|
|
4
|
+
/**
|
|
5
|
+
* Human-facing link to a job, shown while it runs and on error.
|
|
6
|
+
*
|
|
7
|
+
* The category has to be sss: jdispatcher serves its shell with a 200 for any
|
|
8
|
+
* category, so /pfa/ and /psa/ look fine to a fetch and render "Page Not Found"
|
|
9
|
+
* in a browser.
|
|
10
|
+
*/
|
|
11
|
+
export function phmmerResultUrl(jobId) {
|
|
12
|
+
return `https://www.ebi.ac.uk/jdispatcher/sss/${TOOL}/summary?jobId=${jobId}`;
|
|
13
|
+
}
|
|
14
|
+
/** EBI job ids are prefixed with the tool that made them */
|
|
15
|
+
export function isPhmmerJobId(jobId) {
|
|
16
|
+
return jobId.startsWith(`${TOOL}-`);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* phmmer aligns every hit to a profile built from the query, one match state
|
|
20
|
+
* per query residue, and marks those columns 'x' in #=GC RF. So the query's own
|
|
21
|
+
* row is exactly recoverable: walk RF, consume a query residue at each match
|
|
22
|
+
* column, gap everywhere else.
|
|
23
|
+
*
|
|
24
|
+
* This is the one piece of real logic here rather than a library call, and it
|
|
25
|
+
* is checked hard: if the match columns do not account for the query exactly,
|
|
26
|
+
* the columns and the query have drifted apart, and a query row that is off by
|
|
27
|
+
* even one residue would silently mis-map every column to the genome. Throwing
|
|
28
|
+
* is much better than drawing that.
|
|
29
|
+
*/
|
|
30
|
+
function buildQueryRow({ rf, query }) {
|
|
31
|
+
let consumed = 0;
|
|
32
|
+
const row = Array.from(rf, c => c === 'x' ? (query[consumed++] ?? '-') : '-').join('');
|
|
33
|
+
if (consumed !== query.length) {
|
|
34
|
+
throw new Error(`phmmer alignment has ${consumed} match columns for a query of ${query.length} residues, so the query row cannot be placed`);
|
|
35
|
+
}
|
|
36
|
+
return row;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* '[subseq from] Albumin OS=Homo sapiens OX=9606 GN=ALB PE=1 SV=2' is what a
|
|
40
|
+
* UniProt target's #=GS DE looks like. Hits from the non-UniProt databases
|
|
41
|
+
* phmmer also offers (PDB, AlphaFold, MEROPS...) carry no OS=/OX= at all, so
|
|
42
|
+
* every field here is optional.
|
|
43
|
+
*/
|
|
44
|
+
function parseDescription(de) {
|
|
45
|
+
const text = (de ?? '').replace('[subseq from] ', '');
|
|
46
|
+
const sciname = /OS=(.*?)\s+(?:OX|GN|PE|SV)=/.exec(text)?.[1];
|
|
47
|
+
const ox = /OX=(\d+)/.exec(text)?.[1];
|
|
48
|
+
return {
|
|
49
|
+
sciname: sciname ?? 'unknown',
|
|
50
|
+
taxid: ox ? Number.parseInt(ox, 10) : undefined,
|
|
51
|
+
title: text.split(' OS=')[0] || undefined,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Target names look like 'sp|P02768|ALBU_HUMAN/1-609' for UniProt databases and
|
|
56
|
+
* like anything at all for the others, so an unrecognized name becomes its own
|
|
57
|
+
* accession rather than being dropped.
|
|
58
|
+
*/
|
|
59
|
+
function parseName(name) {
|
|
60
|
+
const slash = name.lastIndexOf('/');
|
|
61
|
+
const range = slash === -1 ? undefined : /^\d+-\d+$/.exec(name.slice(slash + 1))?.[0];
|
|
62
|
+
const bare = range === undefined ? name : name.slice(0, slash);
|
|
63
|
+
const parts = bare.split('|');
|
|
64
|
+
return parts.length === 3
|
|
65
|
+
? { accession: parts[1], id: parts[2], range }
|
|
66
|
+
: { accession: bare, id: bare, range };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Exported for testing against a captured .sto — the annotation names (RF, the
|
|
70
|
+
* DE line's OS=/OX=) are the whole risk in this mapping, and nothing else in CI
|
|
71
|
+
* would notice if HMMER or EBI changed one.
|
|
72
|
+
*/
|
|
73
|
+
export function parsePhmmerAlignment({ stockholm, query, }) {
|
|
74
|
+
const { gc, gs, seqdata, seqname } = new StockholmMSA(stockholm, 0).getMSA();
|
|
75
|
+
const rf = gc.RF;
|
|
76
|
+
if (!rf) {
|
|
77
|
+
throw new Error('phmmer alignment has no #=GC RF line');
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
queryRow: buildQueryRow({ rf, query }),
|
|
81
|
+
rows: seqname.map(name => ({
|
|
82
|
+
...parseName(name),
|
|
83
|
+
...parseDescription(gs.DE?.[name]?.[0]),
|
|
84
|
+
// insert columns come back lowercase with '.' for gaps; the MSA renderer
|
|
85
|
+
// looks colors up by the literal letter, so lowercase would draw
|
|
86
|
+
// uncolored. The insert columns stay visible as gaps in the query row.
|
|
87
|
+
aligned: (seqdata[name] ?? '').replaceAll('.', '-').toUpperCase(),
|
|
88
|
+
})),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export async function queryPhmmer({ query, database, onProgress, onRid, signal, }) {
|
|
92
|
+
onProgress('Submitting to EBI phmmer...');
|
|
93
|
+
const jobId = await submitEbiJob({
|
|
94
|
+
tool: TOOL,
|
|
95
|
+
params: {
|
|
96
|
+
database,
|
|
97
|
+
sequence: query,
|
|
98
|
+
// the alignment is the whole point of using phmmer here
|
|
99
|
+
alignView: 'true',
|
|
100
|
+
},
|
|
101
|
+
signal,
|
|
102
|
+
});
|
|
103
|
+
onRid(jobId);
|
|
104
|
+
await waitForEbiJob({
|
|
105
|
+
tool: TOOL,
|
|
106
|
+
jobId,
|
|
107
|
+
signal,
|
|
108
|
+
onCountdown: s => {
|
|
109
|
+
onProgress(`Re-checking phmmer status in... ${s}`);
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
const alignment = parsePhmmerAlignment({
|
|
113
|
+
stockholm: await fetchEbiResult({ tool: TOOL, jobId, type: 'sto', signal }),
|
|
114
|
+
query,
|
|
115
|
+
});
|
|
116
|
+
if (alignment.rows.length === 0) {
|
|
117
|
+
throw new Error('No hits found');
|
|
118
|
+
}
|
|
119
|
+
return { rid: jobId, ...alignment };
|
|
120
|
+
}
|
package/dist/utils/poll.d.ts
CHANGED
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
* false when still pending, and throws on failure. Between checks it counts down
|
|
4
4
|
* `intervalSeconds`, calling `onCountdown` each second so the UI can show
|
|
5
5
|
* progress.
|
|
6
|
+
*
|
|
7
|
+
* `signal` ends the loop with an AbortError: the countdown rejects on the tick
|
|
8
|
+
* it is aborted rather than at the end of the interval, so a cancelled launch
|
|
9
|
+
* stops within a second instead of running for the job's lifetime.
|
|
6
10
|
*/
|
|
7
|
-
export declare function pollLoop({ check, intervalSeconds, onCountdown, }: {
|
|
11
|
+
export declare function pollLoop({ check, intervalSeconds, onCountdown, signal, }: {
|
|
8
12
|
check: () => Promise<boolean>;
|
|
9
13
|
intervalSeconds: number;
|
|
10
14
|
onCountdown: (secondsRemaining: number) => void;
|
|
15
|
+
signal?: AbortSignal;
|
|
11
16
|
}): Promise<void>;
|
package/dist/utils/poll.js
CHANGED
|
@@ -4,16 +4,21 @@ import { timeout } from './fetch';
|
|
|
4
4
|
* false when still pending, and throws on failure. Between checks it counts down
|
|
5
5
|
* `intervalSeconds`, calling `onCountdown` each second so the UI can show
|
|
6
6
|
* progress.
|
|
7
|
+
*
|
|
8
|
+
* `signal` ends the loop with an AbortError: the countdown rejects on the tick
|
|
9
|
+
* it is aborted rather than at the end of the interval, so a cancelled launch
|
|
10
|
+
* stops within a second instead of running for the job's lifetime.
|
|
7
11
|
*/
|
|
8
|
-
export async function pollLoop({ check, intervalSeconds, onCountdown, }) {
|
|
12
|
+
export async function pollLoop({ check, intervalSeconds, onCountdown, signal, }) {
|
|
9
13
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
10
14
|
while (true) {
|
|
15
|
+
signal?.throwIfAborted();
|
|
11
16
|
if (await check()) {
|
|
12
17
|
return;
|
|
13
18
|
}
|
|
14
19
|
for (let i = intervalSeconds; i > 0; i--) {
|
|
15
20
|
onCountdown(i);
|
|
16
|
-
await timeout(1000);
|
|
21
|
+
await timeout(1000, signal);
|
|
17
22
|
}
|
|
18
23
|
}
|
|
19
24
|
}
|
|
@@ -2,4 +2,4 @@ export interface TaxonomyInfo {
|
|
|
2
2
|
sciname: string;
|
|
3
3
|
commonName?: string;
|
|
4
4
|
}
|
|
5
|
-
export declare function fetchTaxonomyInfo(
|
|
5
|
+
export declare function fetchTaxonomyInfo(taxidsWithRepeats: number[]): Promise<Map<number, TaxonomyInfo>>;
|
|
@@ -25,7 +25,12 @@ async function saveTaxonomyCache(entries) {
|
|
|
25
25
|
}
|
|
26
26
|
await tx.done;
|
|
27
27
|
}
|
|
28
|
-
export async function fetchTaxonomyInfo(
|
|
28
|
+
export async function fetchTaxonomyInfo(taxidsWithRepeats) {
|
|
29
|
+
// callers pass one taxid per alignment row, and a BLAST hit list is several
|
|
30
|
+
// rows per species: 100 albumin hits are maybe 50 taxa, and asking as they
|
|
31
|
+
// came did 100 IndexedDB reads and sent eutils 100 ids for 50 answers. The
|
|
32
|
+
// result is keyed by taxid, so no caller can tell the difference
|
|
33
|
+
const taxids = [...new Set(taxidsWithRepeats)];
|
|
29
34
|
const result = new Map();
|
|
30
35
|
const uncachedTaxids = [];
|
|
31
36
|
const cachedResults = await getCachedTaxonomies(taxids);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "3.
|
|
1
|
+
export declare const version = "3.4.1";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '3.
|
|
1
|
+
export const version = '3.4.1';
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "3.
|
|
2
|
+
"version": "3.4.1",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"name": "jbrowse-plugin-msaview",
|
|
5
5
|
"repository": {
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@emotion/styled": "^11.14.1",
|
|
20
|
-
"@gmod/bgzf-filehandle": "^6.
|
|
21
|
-
"g2p_mapper": "^2.1.
|
|
20
|
+
"@gmod/bgzf-filehandle": "^6.6.0",
|
|
21
|
+
"g2p_mapper": "^2.1.7",
|
|
22
22
|
"idb": "^8.0.3"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
@@ -28,51 +28,57 @@
|
|
|
28
28
|
"@jbrowse/core": "^4.3.0",
|
|
29
29
|
"@jbrowse/mobx-state-tree": "^5.13.0",
|
|
30
30
|
"@jbrowse/plugin-linear-genome-view": "^4.3.0",
|
|
31
|
-
"@mui/icons-material": "^9.
|
|
32
|
-
"@mui/material": "^9.
|
|
33
|
-
"@mui/system": "^9.
|
|
34
|
-
"@mui/x-data-grid": "^9.
|
|
31
|
+
"@mui/icons-material": "^9.4.0",
|
|
32
|
+
"@mui/material": "^9.4.0",
|
|
33
|
+
"@mui/system": "^9.4.0",
|
|
34
|
+
"@mui/x-data-grid": "^9.12.0",
|
|
35
35
|
"@testing-library/dom": "^10.4.1",
|
|
36
|
-
"@testing-library/react": "^16.3.
|
|
37
|
-
"@types/node": "^26.
|
|
38
|
-
"@types/react": "^19.2.
|
|
39
|
-
"esbuild": "^0.28.
|
|
40
|
-
"eslint": "^10.
|
|
36
|
+
"@testing-library/react": "^16.3.3",
|
|
37
|
+
"@types/node": "^26.4.1",
|
|
38
|
+
"@types/react": "^19.2.18",
|
|
39
|
+
"esbuild": "^0.28.2",
|
|
40
|
+
"eslint": "^10.9.1",
|
|
41
41
|
"eslint-plugin-import-x": "^4.17.1",
|
|
42
42
|
"eslint-plugin-react": "^7.37.5",
|
|
43
43
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
44
|
-
"eslint-plugin-unicorn": "^
|
|
44
|
+
"eslint-plugin-unicorn": "^74.0.0",
|
|
45
45
|
"git-cliff": "^2.13.1",
|
|
46
46
|
"jsdom": "^30.0.1",
|
|
47
47
|
"mobx": "^6.16.1",
|
|
48
48
|
"mobx-react": "^9.2.2",
|
|
49
|
-
"msa-parsers": "^6.
|
|
49
|
+
"msa-parsers": "^6.4.2",
|
|
50
|
+
"oxfmt": "^0.65.0",
|
|
51
|
+
"oxlint": "^1.81.0",
|
|
52
|
+
"oxlint-tsgolint": "^7.0.2001",
|
|
50
53
|
"pixelmatch": "^7.2.0",
|
|
51
54
|
"pngjs": "^7.0.0",
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"puppeteer": "^25.3.0",
|
|
55
|
+
"pretty-bytes": "^7.1.2",
|
|
56
|
+
"puppeteer": "^25.9.0",
|
|
55
57
|
"react": "^19.2.8",
|
|
56
58
|
"react-dom": "^19.2.8",
|
|
57
|
-
"react-msaview": "^6.
|
|
59
|
+
"react-msaview": "^6.4.2",
|
|
58
60
|
"rimraf": "^6.1.3",
|
|
59
61
|
"rxjs": "^7.8.2",
|
|
60
62
|
"serve": "^14.2.6",
|
|
61
63
|
"tss-react": "^4.9.21",
|
|
62
|
-
"typescript": "^6.0.
|
|
63
|
-
"typescript-eslint": "^8.
|
|
64
|
-
"vitest": "^4.1.
|
|
64
|
+
"typescript": "^6.0.3",
|
|
65
|
+
"typescript-eslint": "^8.69.0",
|
|
66
|
+
"vitest": "^4.1.11"
|
|
65
67
|
},
|
|
66
68
|
"scripts": {
|
|
67
69
|
"clean": "rimraf dist",
|
|
68
70
|
"start": "node esbuild.mjs --watch",
|
|
69
|
-
"format": "
|
|
71
|
+
"format": "oxfmt",
|
|
72
|
+
"check-format": "oxfmt --check",
|
|
70
73
|
"build": "tsc && NODE_ENV=production node esbuild.mjs && cp distconfig.json dist/config.json",
|
|
71
74
|
"prebuild": "pnpm clean",
|
|
72
|
-
"lint": "
|
|
75
|
+
"lint": "oxlint --type-aware --deny-warnings",
|
|
76
|
+
"lint:fast": "oxlint --deny-warnings",
|
|
77
|
+
"lint:eslint": "eslint src --report-unused-disable-directives --max-warnings 0",
|
|
73
78
|
"check-text-source": "node scripts/check-text-source.mjs",
|
|
74
79
|
"check-ebi-params": "node scripts/check-ebi-params.mjs",
|
|
75
80
|
"check-mui-imports": "node scripts/check-mui-imports.mjs",
|
|
81
|
+
"check-host-dep-pins": "node scripts/check-host-dep-pins.mjs",
|
|
76
82
|
"pretest": "rm -rf .test-jbrowse && npx @jbrowse/cli create .test-jbrowse --nightly",
|
|
77
83
|
"test": "vitest run",
|
|
78
84
|
"test:watch": "vitest",
|
|
@@ -82,7 +88,7 @@
|
|
|
82
88
|
"test:version": "node scripts/test-versions.mjs run",
|
|
83
89
|
"host-compat": "node scripts/host-compat-probe.mjs --bundle dist/jbrowse-plugin-msaview.umd.production.min.js",
|
|
84
90
|
"check-ci": "node scripts/require-green-ci.mjs",
|
|
85
|
-
"preversion": "pnpm check-ci && pnpm lint && pnpm build && pnpm host-compat",
|
|
91
|
+
"preversion": "pnpm check-ci && pnpm check-host-dep-pins && pnpm lint && pnpm build && pnpm host-compat",
|
|
86
92
|
"version": "node -e \"console.log('export const version = \\'' + require('./package.json').version + '\\'')\" > src/version.ts && git-cliff --tag v$npm_package_version --unreleased --prepend CHANGELOG.md && git add src/version.ts CHANGELOG.md",
|
|
87
93
|
"postversion": "git push --follow-tags"
|
|
88
94
|
}
|
|
@@ -3,8 +3,8 @@ import React from 'react'
|
|
|
3
3
|
import { getSession } from '@jbrowse/core/util'
|
|
4
4
|
import { observer } from 'mobx-react'
|
|
5
5
|
|
|
6
|
-
import { hasHoverPosition, useStyles } from './util'
|
|
7
6
|
import { isMsaView } from '../MsaViewPanel/model'
|
|
7
|
+
import { hasHoverPosition, useStyles } from './util'
|
|
8
8
|
|
|
9
9
|
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
10
10
|
|
|
@@ -3,8 +3,8 @@ import React from 'react'
|
|
|
3
3
|
import { getSession } from '@jbrowse/core/util'
|
|
4
4
|
import { observer } from 'mobx-react'
|
|
5
5
|
|
|
6
|
-
import { hasHoverPosition, useStyles } from './util'
|
|
7
6
|
import { isMsaView } from '../MsaViewPanel/model'
|
|
7
|
+
import { hasHoverPosition, useStyles } from './util'
|
|
8
8
|
|
|
9
9
|
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|
|
10
10
|
|
|
@@ -2,8 +2,8 @@ import React from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { getSession } from '@jbrowse/core/util'
|
|
4
4
|
|
|
5
|
-
import HighlightComponents from './HighlightComponents'
|
|
6
5
|
import { isMsaView } from '../MsaViewPanel/model'
|
|
6
|
+
import HighlightComponents from './HighlightComponents'
|
|
7
7
|
|
|
8
8
|
import type PluginManager from '@jbrowse/core/PluginManager'
|
|
9
9
|
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
|