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.
Files changed (108) hide show
  1. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastRIDPanel.d.ts → BlastQuery/BlastAutomaticPanel.d.ts} +2 -3
  2. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastAutomaticPanel.js → BlastQuery/BlastAutomaticPanel.js} +6 -25
  3. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastManualPanel.d.ts → BlastQuery/BlastManualPanel.d.ts} +2 -3
  4. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastManualPanel.js → BlastQuery/BlastManualPanel.js} +7 -4
  5. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastMethodSelector.d.ts → BlastQuery/BlastMethodSelector.d.ts} +2 -2
  6. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastMethodSelector.js → BlastQuery/BlastMethodSelector.js} +1 -2
  7. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastPanel.d.ts → BlastQuery/BlastPanel.d.ts} +3 -12
  8. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastPanel.js → BlastQuery/BlastPanel.js} +14 -16
  9. package/dist/LaunchMsaView/components/BlastQuery/BlastSettingsDialog.d.ts +8 -0
  10. package/dist/LaunchMsaView/components/{NCBIBlastQuery/NCBISettingsDialog.js → BlastQuery/BlastSettingsDialog.js} +13 -9
  11. package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.js +1 -1
  12. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +12 -0
  13. package/dist/LaunchMsaView/components/BlastQuery/consts.js +17 -0
  14. package/dist/LaunchMsaView/components/LaunchMsaViewDialog.js +3 -3
  15. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +15 -35
  16. package/dist/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.d.ts +15 -0
  17. package/dist/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.js +67 -0
  18. package/dist/LaunchMsaView/index.js +19 -1
  19. package/dist/MsaViewPanel/components/JobLink.d.ts +5 -0
  20. package/dist/MsaViewPanel/components/JobLink.js +13 -0
  21. package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +1 -2
  22. package/dist/MsaViewPanel/components/LoadingBLAST.js +8 -15
  23. package/dist/MsaViewPanel/components/MsaViewPanel.js +1 -1
  24. package/dist/MsaViewPanel/doLaunchBlast.js +11 -19
  25. package/dist/MsaViewPanel/doLaunchOrthologs.js +43 -12
  26. package/dist/MsaViewPanel/doLaunchOrthologs.test.d.ts +1 -0
  27. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +201 -0
  28. package/dist/MsaViewPanel/model.d.ts +20 -12
  29. package/dist/jbrowse-plugin-msaview.umd.production.min.js +31 -33
  30. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  31. package/dist/utils/blastCache.d.ts +8 -4
  32. package/dist/utils/blastCache.js +4 -5
  33. package/dist/utils/ebiBlast.d.ts +52 -0
  34. package/dist/utils/ebiBlast.js +63 -0
  35. package/dist/utils/ebiJobDispatcher.d.ts +33 -0
  36. package/dist/utils/ebiJobDispatcher.js +80 -0
  37. package/dist/utils/ebiJobDispatcher.test.d.ts +1 -0
  38. package/dist/utils/ebiJobDispatcher.test.js +46 -0
  39. package/dist/utils/eutils.d.ts +9 -0
  40. package/dist/utils/eutils.js +18 -0
  41. package/dist/utils/fetch.js +24 -1
  42. package/dist/utils/msa.d.ts +1 -1
  43. package/dist/utils/msa.js +25 -32
  44. package/dist/utils/ncbiOrthologs.d.ts +17 -75
  45. package/dist/utils/ncbiOrthologs.js +67 -63
  46. package/dist/utils/ncbiOrthologs.test.js +82 -2
  47. package/dist/utils/ncbiTaxonomy.d.ts +11 -0
  48. package/dist/utils/ncbiTaxonomy.js +33 -0
  49. package/dist/utils/types.d.ts +9 -14
  50. package/dist/utils/useLocalStorage.d.ts +1 -0
  51. package/dist/utils/useLocalStorage.js +1 -1
  52. package/dist/version.d.ts +1 -1
  53. package/dist/version.js +1 -1
  54. package/package.json +3 -3
  55. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastAutomaticPanel.tsx → BlastQuery/BlastAutomaticPanel.tsx} +13 -59
  56. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastManualPanel.tsx → BlastQuery/BlastManualPanel.tsx} +7 -6
  57. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastMethodSelector.tsx → BlastQuery/BlastMethodSelector.tsx} +2 -7
  58. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastPanel.tsx → BlastQuery/BlastPanel.tsx} +21 -25
  59. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBISettingsDialog.tsx → BlastQuery/BlastSettingsDialog.tsx} +26 -13
  60. package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.tsx +1 -1
  61. package/src/LaunchMsaView/components/BlastQuery/consts.ts +22 -0
  62. package/src/LaunchMsaView/components/LaunchMsaViewDialog.tsx +5 -7
  63. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +35 -73
  64. package/src/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.tsx +92 -0
  65. package/src/LaunchMsaView/index.ts +20 -2
  66. package/src/MsaViewPanel/components/JobLink.tsx +17 -0
  67. package/src/MsaViewPanel/components/LoadingBLAST.tsx +10 -41
  68. package/src/MsaViewPanel/components/MsaViewPanel.tsx +1 -1
  69. package/src/MsaViewPanel/doLaunchBlast.ts +12 -28
  70. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +247 -0
  71. package/src/MsaViewPanel/doLaunchOrthologs.ts +56 -13
  72. package/src/MsaViewPanel/model.ts +10 -7
  73. package/src/utils/blastCache.ts +8 -9
  74. package/src/utils/ebiBlast.ts +114 -0
  75. package/src/utils/ebiJobDispatcher.test.ts +54 -0
  76. package/src/utils/ebiJobDispatcher.ts +120 -0
  77. package/src/utils/eutils.ts +19 -0
  78. package/src/utils/fetch.ts +26 -1
  79. package/src/utils/msa.ts +26 -47
  80. package/src/utils/ncbiOrthologs.test.ts +96 -2
  81. package/src/utils/ncbiOrthologs.ts +83 -71
  82. package/src/utils/ncbiTaxonomy.ts +37 -0
  83. package/src/utils/types.ts +8 -13
  84. package/src/utils/useLocalStorage.ts +1 -1
  85. package/src/version.ts +1 -1
  86. package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastAutomaticPanel.d.ts +0 -10
  87. package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastRIDPanel.js +0 -74
  88. package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBISettingsDialog.d.ts +0 -5
  89. package/dist/LaunchMsaView/components/NCBIBlastQuery/consts.d.ts +0 -7
  90. package/dist/LaunchMsaView/components/NCBIBlastQuery/consts.js +0 -4
  91. package/dist/MsaViewPanel/components/RIDLink.d.ts +0 -6
  92. package/dist/MsaViewPanel/components/RIDLink.js +0 -12
  93. package/dist/utils/ncbiBlast.d.ts +0 -30
  94. package/dist/utils/ncbiBlast.js +0 -84
  95. package/src/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastRIDPanel.tsx +0 -137
  96. package/src/LaunchMsaView/components/NCBIBlastQuery/consts.ts +0 -10
  97. package/src/MsaViewPanel/components/RIDLink.tsx +0 -19
  98. package/src/utils/ncbiBlast.ts +0 -143
  99. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.d.ts +0 -0
  100. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.d.ts +0 -0
  101. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.js +0 -0
  102. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.d.ts +0 -0
  103. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.js +0 -0
  104. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.d.ts +0 -0
  105. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.js +0 -0
  106. /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.tsx +0 -0
  107. /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.ts +0 -0
  108. /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
- // The species panel offered in the launch dialog, ordered from the reference
25
- // outward so a run that finds only close relatives still reads as a ladder.
26
- // Orthologs absent for a given gene are skipped rather than erroring.
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
- // The index order is the order the sequences are SUBMITTED in
29
- // (`COMMON_TAX_RANK` below sorts `fetchOrthologGenes`' return), not the order the
30
- // rows are drawn in: the view lays rows out by the guide tree the aligner returns,
31
- // so a run on this list comes out grouped by relatedness rather than by this
32
- // list's own sequence. Reordering here changes what Clustal is handed, not the
33
- // picture.
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
- // THE MAMMALS EARN THEIR PLACE, and the reason is measured rather than aesthetic.
36
- // The thirteen this list used to hold were one per major clade, which reads well
37
- // on a gene conserved to yeast and produces almost nothing on a gene that is not:
38
- // NCBI publishes 165 orthologs for human NLRP1 and every one of them is a mammal,
39
- // so of the old thirteen only Human, Mouse, Cow, Pig and Dog returned a row --
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
- // Cat, rabbit and opossum are here despite contributing nothing to that gene.
46
- // They are the three that most often separate "absent in this clade" from
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
- // The cost is the run, and it is roughly linear: one NCBI protein fetch per
50
- // species and a Clustal Omega job over what comes back, so ~23 rows is about
51
- // twice the ~13-row wait. Still seconds rather than the minutes BLAST takes,
52
- // which is the comparison the panel's own text makes.
53
- export const COMMON_SPECIES = [
54
- { label: 'Human', taxId: 9606 },
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
- /** One ortholog gene per species, restricted to the requested taxa. */
111
- export async function fetchOrthologGenes(geneId, taxa) {
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 && taxa.has(taxId) && !byTaxon.has(taxId)) {
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()].sort((a, b) => (COMMON_TAX_RANK.get(a.taxId) ?? Infinity) -
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
- if (geneIds.length > 0) {
136
- const json = await jsonfetch(ncbiUrl(`${DATASETS}/gene/id/${geneIds.join(',')}/product_report`));
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 among the selected species — not enough to align`);
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(ncbiUrl(`${EUTILS}/efetch.fcgi?db=protein&id=${accessions.join(',')}&rettype=fasta&retmode=text`)));
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
+ }
@@ -5,19 +5,14 @@ export interface BlastHitDescription {
5
5
  taxid?: number;
6
6
  title?: string;
7
7
  }
8
- export interface BlastResults {
9
- BlastOutput2: {
10
- report: {
11
- results: {
12
- search: {
13
- hits: {
14
- description: BlastHitDescription[];
15
- hsps: {
16
- hseq: string;
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
  }
@@ -1 +1,2 @@
1
+ export declare function readLocalStorage<T>(key: string, initialValue: T): T;
1
2
  export declare function useLocalStorage<T>(key: string, initialValue: T): readonly [T, (value: T) => void];
@@ -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.8.2";
1
+ export declare const version = "2.10.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.8.2';
1
+ export const version = '2.10.0';
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.8.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.7.1",
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.7.2",
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, blastPrograms } from './consts'
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, BlastProgram, MsaAlgorithm } from './consts'
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 NCBIBlastAutomaticPanel = observer(function ({
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>('nr')
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
- const newDb = event.target.value as BlastDatabase
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 NCBI. Using blastp can
145
- take 10+ minutes to run, quick-blastp is generally a lot faster but is
146
- not available for the clustered database. After completion, all the
147
- hits will be run through a multiple sequence alignment. Note: we are
148
- not able to currently run NCBI COBALT automatically on the BLAST
149
- results, even though that is the method NCBI uses on their website. If
150
- you need a COBALT alignment, please use the manual approach of
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 NCBIBlastAutomaticPanel
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 NCBIBlastManualPanel = observer(function ({
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
- const link = `${baseUrl}?PAGE_TYPE=BlastSearch&PAGE=Proteins&PROGRAM=blastp&QUERY=${s2}`
47
- const link2 = `${baseUrl}?PAGE_TYPE=BlastSearch&PAGE=Proteins&PROGRAM=blastp&QUERY=${shorten2(s2, 10)}`
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 NCBIBlastManualPanel
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 './NCBIBlastPanel'
5
+ import type { BlastLookupMethod } from './BlastPanel'
6
6
 
7
- export default function NCBIBlastMethodSelector({
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>