jbrowse-plugin-msaview 2.8.2 → 2.10.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/components/{NCBIBlastQuery/NCBIBlastRIDPanel.d.ts → BlastQuery/BlastAutomaticPanel.d.ts} +2 -3
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastAutomaticPanel.js → BlastQuery/BlastAutomaticPanel.js} +6 -25
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastManualPanel.d.ts → BlastQuery/BlastManualPanel.d.ts} +2 -3
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastManualPanel.js → BlastQuery/BlastManualPanel.js} +7 -4
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastMethodSelector.d.ts → BlastQuery/BlastMethodSelector.d.ts} +2 -2
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastMethodSelector.js → BlastQuery/BlastMethodSelector.js} +1 -2
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastPanel.d.ts → BlastQuery/BlastPanel.d.ts} +3 -12
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastPanel.js → BlastQuery/BlastPanel.js} +14 -16
- package/dist/LaunchMsaView/components/BlastQuery/BlastSettingsDialog.d.ts +8 -0
- package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBISettingsDialog.js → BlastQuery/BlastSettingsDialog.js} +13 -9
- package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.js +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +12 -0
- package/dist/LaunchMsaView/components/BlastQuery/consts.js +17 -0
- package/dist/LaunchMsaView/components/LaunchMsaViewDialog.js +3 -3
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +15 -35
- package/dist/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.d.ts +15 -0
- package/dist/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.js +67 -0
- package/dist/LaunchMsaView/index.js +19 -1
- package/dist/MsaViewPanel/components/JobLink.d.ts +5 -0
- package/dist/MsaViewPanel/components/JobLink.js +13 -0
- package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +1 -2
- package/dist/MsaViewPanel/components/LoadingBLAST.js +8 -15
- package/dist/MsaViewPanel/components/MsaViewPanel.js +1 -1
- package/dist/MsaViewPanel/doLaunchBlast.js +11 -19
- package/dist/MsaViewPanel/doLaunchOrthologs.js +43 -12
- package/dist/MsaViewPanel/doLaunchOrthologs.test.d.ts +1 -0
- package/dist/MsaViewPanel/doLaunchOrthologs.test.js +201 -0
- package/dist/MsaViewPanel/model.d.ts +20 -12
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +31 -33
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/blastCache.d.ts +8 -4
- package/dist/utils/blastCache.js +4 -5
- package/dist/utils/ebiBlast.d.ts +52 -0
- package/dist/utils/ebiBlast.js +63 -0
- package/dist/utils/ebiJobDispatcher.d.ts +33 -0
- package/dist/utils/ebiJobDispatcher.js +80 -0
- package/dist/utils/ebiJobDispatcher.test.d.ts +1 -0
- package/dist/utils/ebiJobDispatcher.test.js +46 -0
- package/dist/utils/eutils.d.ts +9 -0
- package/dist/utils/eutils.js +18 -0
- package/dist/utils/fetch.js +24 -1
- package/dist/utils/msa.d.ts +1 -1
- package/dist/utils/msa.js +25 -32
- package/dist/utils/ncbiOrthologs.d.ts +17 -75
- package/dist/utils/ncbiOrthologs.js +67 -63
- package/dist/utils/ncbiOrthologs.test.js +82 -2
- package/dist/utils/ncbiTaxonomy.d.ts +11 -0
- package/dist/utils/ncbiTaxonomy.js +33 -0
- package/dist/utils/types.d.ts +9 -14
- package/dist/utils/useLocalStorage.d.ts +1 -0
- package/dist/utils/useLocalStorage.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastAutomaticPanel.tsx → BlastQuery/BlastAutomaticPanel.tsx} +13 -59
- package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastManualPanel.tsx → BlastQuery/BlastManualPanel.tsx} +7 -6
- package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastMethodSelector.tsx → BlastQuery/BlastMethodSelector.tsx} +2 -7
- package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastPanel.tsx → BlastQuery/BlastPanel.tsx} +21 -25
- package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBISettingsDialog.tsx → BlastQuery/BlastSettingsDialog.tsx} +26 -13
- package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.tsx +1 -1
- package/src/LaunchMsaView/components/BlastQuery/consts.ts +22 -0
- package/src/LaunchMsaView/components/LaunchMsaViewDialog.tsx +5 -7
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +35 -73
- package/src/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.tsx +92 -0
- package/src/LaunchMsaView/index.ts +20 -2
- package/src/MsaViewPanel/components/JobLink.tsx +17 -0
- package/src/MsaViewPanel/components/LoadingBLAST.tsx +10 -41
- package/src/MsaViewPanel/components/MsaViewPanel.tsx +1 -1
- package/src/MsaViewPanel/doLaunchBlast.ts +12 -28
- package/src/MsaViewPanel/doLaunchOrthologs.test.ts +247 -0
- package/src/MsaViewPanel/doLaunchOrthologs.ts +56 -13
- package/src/MsaViewPanel/model.ts +10 -7
- package/src/utils/blastCache.ts +8 -9
- package/src/utils/ebiBlast.ts +114 -0
- package/src/utils/ebiJobDispatcher.test.ts +54 -0
- package/src/utils/ebiJobDispatcher.ts +120 -0
- package/src/utils/eutils.ts +19 -0
- package/src/utils/fetch.ts +26 -1
- package/src/utils/msa.ts +26 -47
- package/src/utils/ncbiOrthologs.test.ts +96 -2
- package/src/utils/ncbiOrthologs.ts +83 -71
- package/src/utils/ncbiTaxonomy.ts +37 -0
- package/src/utils/types.ts +8 -13
- package/src/utils/useLocalStorage.ts +1 -1
- package/src/version.ts +1 -1
- package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastAutomaticPanel.d.ts +0 -10
- package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastRIDPanel.js +0 -74
- package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBISettingsDialog.d.ts +0 -5
- package/dist/LaunchMsaView/components/NCBIBlastQuery/consts.d.ts +0 -7
- package/dist/LaunchMsaView/components/NCBIBlastQuery/consts.js +0 -4
- package/dist/MsaViewPanel/components/RIDLink.d.ts +0 -6
- package/dist/MsaViewPanel/components/RIDLink.js +0 -12
- package/dist/utils/ncbiBlast.d.ts +0 -30
- package/dist/utils/ncbiBlast.js +0 -84
- package/src/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastRIDPanel.tsx +0 -137
- package/src/LaunchMsaView/components/NCBIBlastQuery/consts.ts +0 -10
- package/src/MsaViewPanel/components/RIDLink.tsx +0 -19
- package/src/utils/ncbiBlast.ts +0 -143
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.d.ts +0 -0
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.d.ts +0 -0
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.js +0 -0
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.d.ts +0 -0
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.js +0 -0
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.d.ts +0 -0
- /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.js +0 -0
- /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.tsx +0 -0
- /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.ts +0 -0
- /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.ts +0 -0
|
@@ -14,68 +14,41 @@
|
|
|
14
14
|
//
|
|
15
15
|
// Mirrors jb2hubs' website/src/components/proteinMsa.ts assembler, trimmed to
|
|
16
16
|
// what the launch dialog needs and using this plugin's fetch/eutils helpers.
|
|
17
|
-
import { NCBI_EMAIL, NCBI_TOOL } from './eutils';
|
|
17
|
+
import { NCBI_EMAIL, NCBI_TOOL, efetchPost } from './eutils';
|
|
18
18
|
import { jsonfetch, textfetch } from './fetch';
|
|
19
19
|
// v2, not v2alpha: the alpha path still answers /orthologs but 404s
|
|
20
20
|
// /product_report, so an assembler pointed at it silently resolves zero
|
|
21
21
|
// representative proteins and reports "no orthologs" for every gene.
|
|
22
22
|
const DATASETS = 'https://api.ncbi.nlm.nih.gov/datasets/v2';
|
|
23
23
|
const EUTILS = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils';
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
24
|
+
// NCBI's ortholog report IS the species panel. There is no list here to keep in
|
|
25
|
+
// step with what NCBI knows, and the panel widens by itself as NCBI annotates
|
|
26
|
+
// more genomes.
|
|
27
27
|
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
28
|
+
// It used to be a hand-written 23-species list intersected with the report, and
|
|
29
|
+
// the intersection is what made the alignment thin: NCBI publishes 165 orthologs
|
|
30
|
+
// for human NLRP1 and 865 for CFTR, so the list kept 12 of the first and 19 of
|
|
31
|
+
// the second. It also carried four species -- fruitfly, yeast, C. elegans and
|
|
32
|
+
// arabidopsis -- that the endpoint has never once returned for a human gene:
|
|
33
|
+
// checked against NLRP1, TP53, ACTB, BRCA1, PIK3CA, APOE and NOTCH1, whose fly
|
|
34
|
+
// ortholog is famous and still absent. NCBI's ortholog sets are vertebrate
|
|
35
|
+
// scoped.
|
|
34
36
|
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// five, and Rat not among them, since NLRP1 is absent in Rattus norvegicus. The
|
|
41
|
-
// same query against this list returns twelve. An inflammasome gene is not an
|
|
42
|
-
// unusual case; anything immune, reproductive or lineage-specific behaves the
|
|
43
|
-
// same way, and those are the genes a person opens an ortholog alignment on.
|
|
37
|
+
// The report's OWN order is the ladder the list was hand-built to approximate,
|
|
38
|
+
// and it is per gene. CFTR opens human, mouse, rat, zebrafish, pig, sheep,
|
|
39
|
+
// rabbit, chicken, cattle, ferret, dog, rhesus; NLRP1, which has no ortholog
|
|
40
|
+
// outside placental mammals, opens human, mouse, rhesus, shrew mouse, chimp,
|
|
41
|
+
// dog, cattle, horse. So `limit` takes a prefix and never needs a rank table.
|
|
44
42
|
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// "absent in this species", which is the question a gap in the alignment raises.
|
|
43
|
+
// The rows are SUBMITTED in that order, not drawn in it -- the view lays rows out
|
|
44
|
+
// by the guide tree the aligner returns.
|
|
48
45
|
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
{ label: 'Chimpanzee', taxId: 9598 },
|
|
56
|
-
{ label: 'Gorilla', taxId: 9595 },
|
|
57
|
-
{ label: 'Rhesus macaque', taxId: 9544 },
|
|
58
|
-
{ label: 'Marmoset', taxId: 9483 },
|
|
59
|
-
{ label: 'Mouse', taxId: 10090 },
|
|
60
|
-
{ label: 'Rat', taxId: 10116 },
|
|
61
|
-
{ label: 'Guinea pig', taxId: 10141 },
|
|
62
|
-
{ label: 'Rabbit', taxId: 9986 },
|
|
63
|
-
{ label: 'Cat', taxId: 9685 },
|
|
64
|
-
{ label: 'Dog', taxId: 9615 },
|
|
65
|
-
{ label: 'Horse', taxId: 9796 },
|
|
66
|
-
{ label: 'Pig', taxId: 9823 },
|
|
67
|
-
{ label: 'Cow', taxId: 9913 },
|
|
68
|
-
{ label: 'Sheep', taxId: 9940 },
|
|
69
|
-
{ label: 'Opossum', taxId: 13616 },
|
|
70
|
-
{ label: 'Chicken', taxId: 9031 },
|
|
71
|
-
{ label: 'Frog', taxId: 8364 },
|
|
72
|
-
{ label: 'Zebrafish', taxId: 7955 },
|
|
73
|
-
{ label: 'Fruitfly', taxId: 7227 },
|
|
74
|
-
{ label: 'C. elegans', taxId: 6239 },
|
|
75
|
-
{ label: 'Yeast', taxId: 4932 },
|
|
76
|
-
{ label: 'Arabidopsis', taxId: 3702 },
|
|
77
|
-
];
|
|
78
|
-
export const COMMON_TAX_RANK = new Map(COMMON_SPECIES.map((s, i) => [s.taxId, i]));
|
|
46
|
+
// What limits the row count is the aligner, and it is linear in rows at roughly
|
|
47
|
+
// half a second each for a ~1400aa protein: 165 NLRP1 orthologs align at EBI in
|
|
48
|
+
// 88s, 865 CFTR orthologs in 407s. `defaultMaxSpecies` keeps a default run under
|
|
49
|
+
// a minute; EBI's own ceiling is 4000 sequences and 4MB, which even CFTR's full
|
|
50
|
+
// set (1.3MB) sits inside.
|
|
51
|
+
export const defaultMaxSpecies = 100;
|
|
79
52
|
function ncbiUrl(url) {
|
|
80
53
|
const sep = url.includes('?') ? '&' : '?';
|
|
81
54
|
return `${url}${sep}tool=${NCBI_TOOL}&email=${encodeURIComponent(NCBI_EMAIL)}`;
|
|
@@ -107,13 +80,22 @@ export async function resolveGeneId(candidates, taxId) {
|
|
|
107
80
|
}
|
|
108
81
|
return undefined;
|
|
109
82
|
}
|
|
110
|
-
/**
|
|
111
|
-
|
|
83
|
+
/**
|
|
84
|
+
* One ortholog gene per species, in NCBI's report order, capped at `limit`.
|
|
85
|
+
*
|
|
86
|
+
* `taxa` narrows the set when a caller wants specific species; omitted, every
|
|
87
|
+
* species NCBI has an ortholog for is a candidate. `exclude` drops the query
|
|
88
|
+
* taxon, which the QUERY row already represents.
|
|
89
|
+
*/
|
|
90
|
+
export async function fetchOrthologGenes(geneId, { taxa, exclude, limit = defaultMaxSpecies, } = {}) {
|
|
112
91
|
const json = await jsonfetch(ncbiUrl(`${DATASETS}/gene/id/${geneId}/orthologs?returned_content=COMPLETE`));
|
|
113
92
|
const byTaxon = new Map();
|
|
114
93
|
for (const { gene } of json.reports ?? []) {
|
|
115
94
|
const taxId = Number(gene?.tax_id);
|
|
116
|
-
if (gene?.gene_id &&
|
|
95
|
+
if (gene?.gene_id &&
|
|
96
|
+
taxId !== exclude &&
|
|
97
|
+
(taxa?.has(taxId) ?? true) &&
|
|
98
|
+
!byTaxon.has(taxId)) {
|
|
117
99
|
byTaxon.set(taxId, {
|
|
118
100
|
taxId,
|
|
119
101
|
geneId: gene.gene_id,
|
|
@@ -121,10 +103,26 @@ export async function fetchOrthologGenes(geneId, taxa) {
|
|
|
121
103
|
commonName: gene.common_name,
|
|
122
104
|
});
|
|
123
105
|
}
|
|
106
|
+
if (byTaxon.size >= limit) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
124
109
|
}
|
|
125
|
-
return [...byTaxon.values()]
|
|
126
|
-
(COMMON_TAX_RANK.get(b.taxId) ?? Infinity));
|
|
110
|
+
return [...byTaxon.values()];
|
|
127
111
|
}
|
|
112
|
+
// Two ceilings sit between a gene id list and its product report, and both fail
|
|
113
|
+
// by returning less rather than by erroring, so a caller that ignores them just
|
|
114
|
+
// draws a thinner alignment.
|
|
115
|
+
//
|
|
116
|
+
// The ids go in the URL PATH, and NCBI answers HTTP 414 above roughly 8KB of
|
|
117
|
+
// them -- CFTR's 865 orthologs join to 8609 characters and 414 on the nose.
|
|
118
|
+
// `PRODUCT_REPORT_CHUNK` keeps a request well inside that.
|
|
119
|
+
//
|
|
120
|
+
// Then the endpoint paginates at 20 with the count in `total_count` and the rest
|
|
121
|
+
// behind `next_page_token`, which is invisible to a caller reading `reports`.
|
|
122
|
+
// `page_size` covers a chunk in one request. The old 23-species panel never
|
|
123
|
+
// reached this: the query taxon is excluded and NCBI has no ortholog for the
|
|
124
|
+
// four invertebrate entries, so its ceiling was 19.
|
|
125
|
+
const PRODUCT_REPORT_CHUNK = 150;
|
|
128
126
|
/**
|
|
129
127
|
* geneId -> representative protein accession: MANE Select where flagged, else
|
|
130
128
|
* the longest isoform. A stable, comparable choice across species — picking
|
|
@@ -132,8 +130,9 @@ export async function fetchOrthologGenes(geneId, taxa) {
|
|
|
132
130
|
*/
|
|
133
131
|
export async function fetchRepresentativeProteins(geneIds) {
|
|
134
132
|
const byGene = new Map();
|
|
135
|
-
|
|
136
|
-
const
|
|
133
|
+
for (let i = 0; i < geneIds.length; i += PRODUCT_REPORT_CHUNK) {
|
|
134
|
+
const chunk = geneIds.slice(i, i + PRODUCT_REPORT_CHUNK);
|
|
135
|
+
const json = await jsonfetch(ncbiUrl(`${DATASETS}/gene/id/${chunk.join(',')}/product_report?page_size=${chunk.length}`));
|
|
137
136
|
for (const { product } of json.reports ?? []) {
|
|
138
137
|
const candidates = (product?.transcripts ?? [])
|
|
139
138
|
.map(t => ({
|
|
@@ -210,11 +209,11 @@ export async function fetchProteinForGene(geneId) {
|
|
|
210
209
|
* accessions and sequences. Everything here is a precomputed lookup, so this
|
|
211
210
|
* returns in seconds rather than the 10+ minutes a BLAST submission costs.
|
|
212
211
|
*/
|
|
213
|
-
export async function fetchOrthologRows({ geneId, taxa, onProgress, }) {
|
|
212
|
+
export async function fetchOrthologRows({ geneId, taxa, exclude, limit, onProgress, }) {
|
|
214
213
|
onProgress('Finding orthologs across species...');
|
|
215
|
-
const genes = await fetchOrthologGenes(geneId, taxa);
|
|
214
|
+
const genes = await fetchOrthologGenes(geneId, { taxa, exclude, limit });
|
|
216
215
|
if (genes.length < 2) {
|
|
217
|
-
throw new Error(`Only ${genes.length} ortholog(s) found
|
|
216
|
+
throw new Error(`Only ${genes.length} ortholog(s) found for this gene — not enough to align`);
|
|
218
217
|
}
|
|
219
218
|
onProgress('Selecting a representative protein per species...');
|
|
220
219
|
const proteinByGene = await fetchRepresentativeProteins(genes.map(g => g.geneId));
|
|
@@ -224,7 +223,12 @@ export async function fetchOrthologRows({ geneId, taxa, onProgress, }) {
|
|
|
224
223
|
}
|
|
225
224
|
onProgress(`Fetching ${withProtein.length} protein sequences...`);
|
|
226
225
|
const accessions = withProtein.map(g => proteinByGene.get(g.geneId));
|
|
227
|
-
const seqByAcc = parseFasta(await textfetch(
|
|
226
|
+
const seqByAcc = parseFasta(await textfetch(...efetchPost({
|
|
227
|
+
db: 'protein',
|
|
228
|
+
id: accessions.join(','),
|
|
229
|
+
rettype: 'fasta',
|
|
230
|
+
retmode: 'text',
|
|
231
|
+
})));
|
|
228
232
|
const labels = dedupeLabels(withProtein.map(g => g.commonName ?? g.scientificName));
|
|
229
233
|
const rows = withProtein
|
|
230
234
|
.map((g, i) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, expect, test } from 'vitest';
|
|
2
|
-
import { dedupeLabels, parseFasta } from './ncbiOrthologs';
|
|
1
|
+
import { afterEach, describe, expect, test, vi } from 'vitest';
|
|
2
|
+
import { 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
|
|
@@ -39,3 +39,83 @@ describe('parseFasta', () => {
|
|
|
39
39
|
expect(parseFasta('Error: CEFetchPApplication::proxy_stream()').size).toBe(0);
|
|
40
40
|
});
|
|
41
41
|
});
|
|
42
|
+
// The two ceilings that make a widened species set silently return fewer rows.
|
|
43
|
+
// Both are shaped like a successful response, so only a test that counts what
|
|
44
|
+
// came back sees them.
|
|
45
|
+
describe('the NCBI request ceilings', () => {
|
|
46
|
+
const stubFetch = (handler) => {
|
|
47
|
+
const seen = [];
|
|
48
|
+
vi.stubGlobal('fetch', (url) => {
|
|
49
|
+
seen.push(url);
|
|
50
|
+
return Promise.resolve({
|
|
51
|
+
ok: true,
|
|
52
|
+
status: 200,
|
|
53
|
+
json: () => Promise.resolve(handler(url)),
|
|
54
|
+
text: () => Promise.resolve(JSON.stringify(handler(url))),
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
return seen;
|
|
58
|
+
};
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
vi.unstubAllGlobals();
|
|
61
|
+
});
|
|
62
|
+
const orthologReport = (n) => ({
|
|
63
|
+
total_count: n,
|
|
64
|
+
reports: Array.from({ length: n }, (_, i) => ({
|
|
65
|
+
gene: {
|
|
66
|
+
gene_id: String(1000 + i),
|
|
67
|
+
tax_id: String(2000 + i),
|
|
68
|
+
taxname: `Species ${i}`,
|
|
69
|
+
},
|
|
70
|
+
})),
|
|
71
|
+
});
|
|
72
|
+
test('fetchOrthologGenes takes a prefix of NCBI report order, not a filtered intersection', async () => {
|
|
73
|
+
stubFetch(() => orthologReport(165));
|
|
74
|
+
const genes = await fetchOrthologGenes('22861', { limit: 10 });
|
|
75
|
+
expect(genes.length).toBe(10);
|
|
76
|
+
// the report's own order, which leads with the reference organisms
|
|
77
|
+
expect(genes.map(g => g.geneId)).toEqual(Array.from({ length: 10 }, (_, i) => String(1000 + i)));
|
|
78
|
+
});
|
|
79
|
+
test('fetchOrthologGenes drops the query taxon without spending a row on it', async () => {
|
|
80
|
+
stubFetch(() => orthologReport(165));
|
|
81
|
+
const genes = await fetchOrthologGenes('22861', { exclude: 2000, limit: 3 });
|
|
82
|
+
expect(genes.map(g => g.taxId)).toEqual([2001, 2002, 2003]);
|
|
83
|
+
});
|
|
84
|
+
test('fetchRepresentativeProteins chunks the gene ids below the URI length NCBI 414s at', async () => {
|
|
85
|
+
// 865 CFTR ortholog gene ids join to 8609 characters, which NCBI answers
|
|
86
|
+
// with HTTP 414 rather than a short result
|
|
87
|
+
const ids = Array.from({ length: 400 }, (_, i) => String(100000 + i));
|
|
88
|
+
const seen = stubFetch(url => ({
|
|
89
|
+
reports: (/id\/([^/]+)\//.exec(url)?.[1] ?? '').split(',').map(id => ({
|
|
90
|
+
product: {
|
|
91
|
+
gene_id: id,
|
|
92
|
+
transcripts: [{ protein: { accession_version: `NP_${id}.1` } }],
|
|
93
|
+
},
|
|
94
|
+
})),
|
|
95
|
+
}));
|
|
96
|
+
const byGene = await fetchRepresentativeProteins(ids);
|
|
97
|
+
expect(byGene.size).toBe(400);
|
|
98
|
+
expect(seen.length).toBeGreaterThan(1);
|
|
99
|
+
expect(Math.max(...seen.map(u => u.length))).toBeLessThan(8000);
|
|
100
|
+
});
|
|
101
|
+
test('fetchRepresentativeProteins asks for a page big enough to hold its chunk', async () => {
|
|
102
|
+
// the endpoint paginates at 20 by default and hides the rest behind
|
|
103
|
+
// next_page_token, so a caller reading only `reports` loses everything past
|
|
104
|
+
// the first page and reports no protein for those genes
|
|
105
|
+
const ids = Array.from({ length: 50 }, (_, i) => String(100000 + i));
|
|
106
|
+
const seen = stubFetch(url => ({
|
|
107
|
+
reports: (/id\/([^/]+)\//.exec(url)?.[1] ?? '')
|
|
108
|
+
.split(',')
|
|
109
|
+
.slice(0, Number(/page_size=(\d+)/.exec(url)?.[1] ?? 20))
|
|
110
|
+
.map(id => ({
|
|
111
|
+
product: {
|
|
112
|
+
gene_id: id,
|
|
113
|
+
transcripts: [{ protein: { accession_version: `NP_${id}.1` } }],
|
|
114
|
+
},
|
|
115
|
+
})),
|
|
116
|
+
}));
|
|
117
|
+
const byGene = await fetchRepresentativeProteins(ids);
|
|
118
|
+
expect(byGene.size).toBe(50);
|
|
119
|
+
expect(seen.every(u => /page_size=\d+/.test(u))).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Free text -> NCBI taxon id. A bare number is taken as the id itself; anything
|
|
3
|
+
* else is searched against db=taxonomy, which resolves a scientific name
|
|
4
|
+
* (`Danio rerio`), a common name (`zebrafish`) and a genus alike.
|
|
5
|
+
*
|
|
6
|
+
* This replaces a fixed list of species the dialog used to offer. The query
|
|
7
|
+
* taxon has to match the assembly the user is browsing -- `resolveGeneId`
|
|
8
|
+
* searches `SYMBOL[Gene Name] AND <taxid>[taxid]` -- so a list that stops at 23
|
|
9
|
+
* species silently resolves the wrong organism's gene for anyone outside it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveTaxId(query: string): Promise<number | undefined>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { NCBI_EMAIL, NCBI_TOOL } from './eutils';
|
|
2
|
+
import { jsonfetch } from './fetch';
|
|
3
|
+
const EUTILS = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils';
|
|
4
|
+
/**
|
|
5
|
+
* Free text -> NCBI taxon id. A bare number is taken as the id itself; anything
|
|
6
|
+
* else is searched against db=taxonomy, which resolves a scientific name
|
|
7
|
+
* (`Danio rerio`), a common name (`zebrafish`) and a genus alike.
|
|
8
|
+
*
|
|
9
|
+
* This replaces a fixed list of species the dialog used to offer. The query
|
|
10
|
+
* taxon has to match the assembly the user is browsing -- `resolveGeneId`
|
|
11
|
+
* searches `SYMBOL[Gene Name] AND <taxid>[taxid]` -- so a list that stops at 23
|
|
12
|
+
* species silently resolves the wrong organism's gene for anyone outside it.
|
|
13
|
+
*/
|
|
14
|
+
export async function resolveTaxId(query) {
|
|
15
|
+
const term = query.trim();
|
|
16
|
+
if (!term) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
if (/^\d+$/.test(term)) {
|
|
20
|
+
return Number(term);
|
|
21
|
+
}
|
|
22
|
+
const search = new URLSearchParams({
|
|
23
|
+
db: 'taxonomy',
|
|
24
|
+
term,
|
|
25
|
+
retmode: 'json',
|
|
26
|
+
retmax: '1',
|
|
27
|
+
tool: NCBI_TOOL,
|
|
28
|
+
email: NCBI_EMAIL,
|
|
29
|
+
});
|
|
30
|
+
const json = await jsonfetch(`${EUTILS}/esearch.fcgi?${search.toString()}`);
|
|
31
|
+
const id = json.esearchresult?.idlist?.[0];
|
|
32
|
+
return id ? Number(id) : undefined;
|
|
33
|
+
}
|
package/dist/utils/types.d.ts
CHANGED
|
@@ -5,19 +5,14 @@ export interface BlastHitDescription {
|
|
|
5
5
|
taxid?: number;
|
|
6
6
|
title?: string;
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}[];
|
|
18
|
-
}[];
|
|
19
|
-
};
|
|
20
|
-
};
|
|
21
|
-
};
|
|
8
|
+
/**
|
|
9
|
+
* The shape utils/ebiBlast.ts normalizes EBI's hits into. It keeps the field
|
|
10
|
+
* names NCBI's JSON2 used, so everything downstream of the search — row naming,
|
|
11
|
+
* taxonomy lookup, the MSA — was left untouched when the backend moved.
|
|
12
|
+
*/
|
|
13
|
+
export interface BlastHit {
|
|
14
|
+
description: BlastHitDescription[];
|
|
15
|
+
hsps: {
|
|
16
|
+
hseq: string;
|
|
22
17
|
}[];
|
|
23
18
|
}
|
|
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
|
|
4
4
|
// throw "(0, PR.useLocalStorage) is not a function" on hosts built during that
|
|
5
5
|
// window. Same failure mode as `defaultCodonTable`; keeping our own copy takes
|
|
6
6
|
// this plugin out of the whack-a-mole.
|
|
7
|
-
function readLocalStorage(key, initialValue) {
|
|
7
|
+
export function readLocalStorage(key, initialValue) {
|
|
8
8
|
try {
|
|
9
9
|
const item = globalThis.localStorage.getItem(key);
|
|
10
10
|
return item === null ? initialValue : JSON.parse(item);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "2.
|
|
1
|
+
export declare const version = "2.10.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.
|
|
1
|
+
export const version = '2.10.0';
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.
|
|
2
|
+
"version": "2.10.0",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"name": "jbrowse-plugin-msaview",
|
|
5
5
|
"repository": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"eslint-plugin-unicorn": "^72.0.0",
|
|
44
44
|
"mobx": "^6.16.1",
|
|
45
45
|
"mobx-react": "^9.2.2",
|
|
46
|
-
"msa-parsers": "^5.
|
|
46
|
+
"msa-parsers": "^5.9.0",
|
|
47
47
|
"pixelmatch": "^7.2.0",
|
|
48
48
|
"pngjs": "^7.0.0",
|
|
49
49
|
"prettier": "^3.9.6",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"puppeteer": "^25.3.0",
|
|
52
52
|
"react": "^19.2.8",
|
|
53
53
|
"react-dom": "^19.2.8",
|
|
54
|
-
"react-msaview": "^5.
|
|
54
|
+
"react-msaview": "^5.9.0",
|
|
55
55
|
"rimraf": "^6.1.3",
|
|
56
56
|
"rxjs": "^7.8.2",
|
|
57
57
|
"serve": "^14.2.6",
|
|
@@ -14,7 +14,7 @@ import { makeStyles } from 'tss-react/mui'
|
|
|
14
14
|
import CachedBlastResults from './CachedBlastResults'
|
|
15
15
|
import MsaAlgorithmSelect from './MsaAlgorithmSelect'
|
|
16
16
|
import { blastLaunchView } from './blastLaunchView'
|
|
17
|
-
import { blastDatabaseOptions,
|
|
17
|
+
import { blastDatabaseOptions, defaultBlastDatabase } from './consts'
|
|
18
18
|
import { useCachedBlastResults } from './useCachedBlastResults'
|
|
19
19
|
import TextField2 from '../../../components/TextField2'
|
|
20
20
|
import {
|
|
@@ -27,20 +27,13 @@ import SubmitCancelActions from '../SubmitCancelActions'
|
|
|
27
27
|
import TranscriptSelector from '../TranscriptSelector'
|
|
28
28
|
import { useTranscriptSelection } from '../useTranscriptSelection'
|
|
29
29
|
|
|
30
|
-
import type { BlastDatabase,
|
|
30
|
+
import type { BlastDatabase, MsaAlgorithm } from './consts'
|
|
31
31
|
import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
|
|
32
32
|
|
|
33
33
|
const useStyles = makeStyles()({
|
|
34
34
|
selectField: {
|
|
35
35
|
width: 150,
|
|
36
36
|
},
|
|
37
|
-
databaseFieldContainer: {
|
|
38
|
-
display: 'flex',
|
|
39
|
-
},
|
|
40
|
-
clusterSeqMessage: {
|
|
41
|
-
marginLeft: 4,
|
|
42
|
-
alignContent: 'center',
|
|
43
|
-
},
|
|
44
37
|
cachedResultsAccordion: {
|
|
45
38
|
marginTop: 20,
|
|
46
39
|
},
|
|
@@ -49,16 +42,14 @@ const useStyles = makeStyles()({
|
|
|
49
42
|
},
|
|
50
43
|
})
|
|
51
44
|
|
|
52
|
-
const
|
|
45
|
+
const BlastAutomaticPanel = observer(function ({
|
|
53
46
|
handleClose,
|
|
54
47
|
feature,
|
|
55
48
|
model,
|
|
56
49
|
children,
|
|
57
|
-
baseUrl,
|
|
58
50
|
}: {
|
|
59
51
|
model: AbstractTrackModel
|
|
60
52
|
feature: Feature
|
|
61
|
-
baseUrl: string
|
|
62
53
|
handleClose: () => void
|
|
63
54
|
children: React.ReactNode
|
|
64
55
|
}) {
|
|
@@ -66,11 +57,9 @@ const NCBIBlastAutomaticPanel = observer(function ({
|
|
|
66
57
|
const view = getLinearGenomeView(model)
|
|
67
58
|
const [launchViewError, setLaunchViewError] = useState<unknown>()
|
|
68
59
|
const [selectedBlastDatabase, setSelectedBlastDatabase] =
|
|
69
|
-
useState<BlastDatabase>(
|
|
60
|
+
useState<BlastDatabase>(defaultBlastDatabase)
|
|
70
61
|
const [selectedMsaAlgorithm, setSelectedMsaAlgorithm] =
|
|
71
62
|
useState<MsaAlgorithm>('clustalo')
|
|
72
|
-
const [selectedBlastProgram, setSelectedBlastProgram] =
|
|
73
|
-
useState<BlastProgram>('quick-blastp')
|
|
74
63
|
|
|
75
64
|
const geneIds = useMemo(() => getGeneIdentifiers(feature), [feature])
|
|
76
65
|
const { results: cachedResults, error: cachedResultsError } =
|
|
@@ -90,11 +79,7 @@ const NCBIBlastAutomaticPanel = observer(function ({
|
|
|
90
79
|
select
|
|
91
80
|
value={selectedBlastDatabase}
|
|
92
81
|
onChange={event => {
|
|
93
|
-
|
|
94
|
-
setSelectedBlastDatabase(newDb)
|
|
95
|
-
if (newDb === 'nr_cluster_seq') {
|
|
96
|
-
setSelectedBlastProgram('blastp')
|
|
97
|
-
}
|
|
82
|
+
setSelectedBlastDatabase(event.target.value as BlastDatabase)
|
|
98
83
|
}}
|
|
99
84
|
>
|
|
100
85
|
{blastDatabaseOptions.map(val => (
|
|
@@ -110,45 +95,16 @@ const NCBIBlastAutomaticPanel = observer(function ({
|
|
|
110
95
|
onChange={setSelectedMsaAlgorithm}
|
|
111
96
|
/>
|
|
112
97
|
|
|
113
|
-
<div className={classes.databaseFieldContainer}>
|
|
114
|
-
<TextField2
|
|
115
|
-
variant="outlined"
|
|
116
|
-
label="BLAST program"
|
|
117
|
-
disabled={selectedBlastDatabase === 'nr_cluster_seq'}
|
|
118
|
-
className={classes.selectField}
|
|
119
|
-
select
|
|
120
|
-
value={selectedBlastProgram}
|
|
121
|
-
onChange={event => {
|
|
122
|
-
setSelectedBlastProgram(event.target.value as BlastProgram)
|
|
123
|
-
}}
|
|
124
|
-
>
|
|
125
|
-
{blastPrograms.map(val => (
|
|
126
|
-
<MenuItem value={val} key={val}>
|
|
127
|
-
{val}
|
|
128
|
-
</MenuItem>
|
|
129
|
-
))}
|
|
130
|
-
</TextField2>
|
|
131
|
-
{selectedBlastDatabase === 'nr_cluster_seq' ? (
|
|
132
|
-
<Typography
|
|
133
|
-
variant="subtitle2"
|
|
134
|
-
className={classes.clusterSeqMessage}
|
|
135
|
-
>
|
|
136
|
-
Can only use blastp on nr_cluster_seq
|
|
137
|
-
</Typography>
|
|
138
|
-
) : null}
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
98
|
<TranscriptSelector feature={feature} {...transcriptSelection} />
|
|
142
99
|
|
|
143
100
|
<Typography className={classes.infoText}>
|
|
144
|
-
This panel will automatically submit a query to
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
submitting BLAST yourself and downloading the resulting files
|
|
101
|
+
This panel will automatically submit a blastp query to EBI, which
|
|
102
|
+
searches UniProtKB. Searches usually finish in under a minute, and
|
|
103
|
+
swissprot returns curated sequences that align more cleanly than the
|
|
104
|
+
many near-identical entries a TrEMBL search brings back. After
|
|
105
|
+
completion, all the hits will be run through a multiple sequence
|
|
106
|
+
alignment. Searching NCBI's nr needs the manual approach: NCBI no
|
|
107
|
+
longer lets a browser read responses from Blast.cgi.
|
|
152
108
|
</Typography>
|
|
153
109
|
|
|
154
110
|
{cachedResults.length > 0 ? (
|
|
@@ -177,8 +133,6 @@ const NCBIBlastAutomaticPanel = observer(function ({
|
|
|
177
133
|
view,
|
|
178
134
|
newViewTitle: getBlastViewTitle(feature, selectedTranscript),
|
|
179
135
|
blastParams: {
|
|
180
|
-
baseUrl,
|
|
181
|
-
blastProgram: selectedBlastProgram,
|
|
182
136
|
blastDatabase: selectedBlastDatabase,
|
|
183
137
|
msaAlgorithm: selectedMsaAlgorithm,
|
|
184
138
|
selectedTranscript,
|
|
@@ -198,4 +152,4 @@ const NCBIBlastAutomaticPanel = observer(function ({
|
|
|
198
152
|
)
|
|
199
153
|
})
|
|
200
154
|
|
|
201
|
-
export default
|
|
155
|
+
export default BlastAutomaticPanel
|
|
@@ -5,6 +5,7 @@ import { Button, DialogActions, Typography } from '@mui/material'
|
|
|
5
5
|
import { observer } from 'mobx-react'
|
|
6
6
|
import { makeStyles } from 'tss-react/mui'
|
|
7
7
|
|
|
8
|
+
import { BASE_BLAST_URL } from './consts'
|
|
8
9
|
import ExternalLink from '../../../components/ExternalLink'
|
|
9
10
|
import { cleanProteinSequence, getLinearGenomeView } from '../../util'
|
|
10
11
|
import LaunchPanelContent from '../LaunchPanelContent'
|
|
@@ -24,17 +25,15 @@ const useStyles = makeStyles()({
|
|
|
24
25
|
},
|
|
25
26
|
})
|
|
26
27
|
|
|
27
|
-
const
|
|
28
|
+
const BlastManualPanel = observer(function ({
|
|
28
29
|
handleClose,
|
|
29
30
|
feature,
|
|
30
31
|
model,
|
|
31
32
|
children,
|
|
32
|
-
baseUrl,
|
|
33
33
|
}: {
|
|
34
34
|
children: React.ReactNode
|
|
35
35
|
model: AbstractTrackModel
|
|
36
36
|
feature: Feature
|
|
37
|
-
baseUrl: string
|
|
38
37
|
handleClose: () => void
|
|
39
38
|
}) {
|
|
40
39
|
const { classes } = useStyles()
|
|
@@ -43,8 +42,10 @@ const NCBIBlastManualPanel = observer(function ({
|
|
|
43
42
|
const { proteinSequence, error } = transcriptSelection
|
|
44
43
|
|
|
45
44
|
const s2 = cleanProteinSequence(proteinSequence)
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
// a link the user follows to NCBI's own site, not something we fetch — which
|
|
46
|
+
// is exactly why this route still works when the automatic one cannot
|
|
47
|
+
const link = `${BASE_BLAST_URL}?PAGE_TYPE=BlastSearch&PAGE=Proteins&PROGRAM=blastp&QUERY=${s2}`
|
|
48
|
+
const link2 = `${BASE_BLAST_URL}?PAGE_TYPE=BlastSearch&PAGE=Proteins&PROGRAM=blastp&QUERY=${shorten2(s2, 10)}`
|
|
48
49
|
|
|
49
50
|
return (
|
|
50
51
|
<>
|
|
@@ -83,4 +84,4 @@ const NCBIBlastManualPanel = observer(function ({
|
|
|
83
84
|
)
|
|
84
85
|
})
|
|
85
86
|
|
|
86
|
-
export default
|
|
87
|
+
export default BlastManualPanel
|
|
@@ -2,9 +2,9 @@ import React from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { FormControl, FormControlLabel, Radio, RadioGroup } from '@mui/material'
|
|
4
4
|
|
|
5
|
-
import type { BlastLookupMethod } from './
|
|
5
|
+
import type { BlastLookupMethod } from './BlastPanel'
|
|
6
6
|
|
|
7
|
-
export default function
|
|
7
|
+
export default function BlastMethodSelector({
|
|
8
8
|
lookupMethod,
|
|
9
9
|
setLookupMethod,
|
|
10
10
|
}: {
|
|
@@ -25,11 +25,6 @@ export default function NCBIBlastMethodSelector({
|
|
|
25
25
|
control={<Radio />}
|
|
26
26
|
label="Automatic"
|
|
27
27
|
/>
|
|
28
|
-
<FormControlLabel
|
|
29
|
-
value="rid"
|
|
30
|
-
control={<Radio />}
|
|
31
|
-
label="Load from RID"
|
|
32
|
-
/>
|
|
33
28
|
<FormControlLabel value="manual" control={<Radio />} label="Manual" />
|
|
34
29
|
</RadioGroup>
|
|
35
30
|
</FormControl>
|