jbrowse-plugin-msaview 2.8.1 → 2.9.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 (91) 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 +1 -1
  16. package/dist/LaunchMsaViewExtensionPoint/index.js +11 -4
  17. package/dist/MsaViewPanel/components/JobLink.d.ts +5 -0
  18. package/dist/MsaViewPanel/components/JobLink.js +13 -0
  19. package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +1 -2
  20. package/dist/MsaViewPanel/components/LoadingBLAST.js +8 -15
  21. package/dist/MsaViewPanel/components/MsaViewPanel.js +1 -1
  22. package/dist/MsaViewPanel/doLaunchBlast.js +11 -19
  23. package/dist/MsaViewPanel/doLaunchOrthologs.js +44 -21
  24. package/dist/MsaViewPanel/doLaunchOrthologs.test.d.ts +1 -0
  25. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +127 -0
  26. package/dist/MsaViewPanel/model.d.ts +15 -7
  27. package/dist/jbrowse-plugin-msaview.umd.production.min.js +32 -32
  28. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  29. package/dist/utils/blastCache.d.ts +8 -4
  30. package/dist/utils/blastCache.js +4 -5
  31. package/dist/utils/ebiBlast.d.ts +52 -0
  32. package/dist/utils/ebiBlast.js +63 -0
  33. package/dist/utils/ebiJobDispatcher.d.ts +33 -0
  34. package/dist/utils/ebiJobDispatcher.js +55 -0
  35. package/dist/utils/fetch.js +24 -1
  36. package/dist/utils/msa.d.ts +1 -1
  37. package/dist/utils/msa.js +25 -32
  38. package/dist/utils/types.d.ts +9 -14
  39. package/dist/utils/useLocalStorage.d.ts +1 -0
  40. package/dist/utils/useLocalStorage.js +1 -1
  41. package/dist/version.d.ts +1 -1
  42. package/dist/version.js +1 -1
  43. package/package.json +1 -1
  44. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastAutomaticPanel.tsx → BlastQuery/BlastAutomaticPanel.tsx} +13 -59
  45. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastManualPanel.tsx → BlastQuery/BlastManualPanel.tsx} +7 -6
  46. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastMethodSelector.tsx → BlastQuery/BlastMethodSelector.tsx} +2 -7
  47. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBIBlastPanel.tsx → BlastQuery/BlastPanel.tsx} +21 -25
  48. package/src/LaunchMsaView/components/{NCBIBlastQuery/NCBISettingsDialog.tsx → BlastQuery/BlastSettingsDialog.tsx} +26 -13
  49. package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.tsx +1 -1
  50. package/src/LaunchMsaView/components/BlastQuery/consts.ts +22 -0
  51. package/src/LaunchMsaView/components/LaunchMsaViewDialog.tsx +5 -7
  52. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +2 -2
  53. package/src/LaunchMsaViewExtensionPoint/index.ts +31 -4
  54. package/src/MsaViewPanel/components/JobLink.tsx +17 -0
  55. package/src/MsaViewPanel/components/LoadingBLAST.tsx +10 -41
  56. package/src/MsaViewPanel/components/MsaViewPanel.tsx +1 -1
  57. package/src/MsaViewPanel/doLaunchBlast.ts +12 -28
  58. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +159 -0
  59. package/src/MsaViewPanel/doLaunchOrthologs.ts +49 -20
  60. package/src/MsaViewPanel/model.ts +15 -8
  61. package/src/utils/blastCache.ts +8 -9
  62. package/src/utils/ebiBlast.ts +114 -0
  63. package/src/utils/ebiJobDispatcher.ts +91 -0
  64. package/src/utils/fetch.ts +26 -1
  65. package/src/utils/msa.ts +26 -47
  66. package/src/utils/types.ts +8 -13
  67. package/src/utils/useLocalStorage.ts +1 -1
  68. package/src/version.ts +1 -1
  69. package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastAutomaticPanel.d.ts +0 -10
  70. package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastRIDPanel.js +0 -74
  71. package/dist/LaunchMsaView/components/NCBIBlastQuery/NCBISettingsDialog.d.ts +0 -5
  72. package/dist/LaunchMsaView/components/NCBIBlastQuery/consts.d.ts +0 -7
  73. package/dist/LaunchMsaView/components/NCBIBlastQuery/consts.js +0 -4
  74. package/dist/MsaViewPanel/components/RIDLink.d.ts +0 -6
  75. package/dist/MsaViewPanel/components/RIDLink.js +0 -12
  76. package/dist/utils/ncbiBlast.d.ts +0 -30
  77. package/dist/utils/ncbiBlast.js +0 -84
  78. package/src/LaunchMsaView/components/NCBIBlastQuery/NCBIBlastRIDPanel.tsx +0 -137
  79. package/src/LaunchMsaView/components/NCBIBlastQuery/consts.ts +0 -10
  80. package/src/MsaViewPanel/components/RIDLink.tsx +0 -19
  81. package/src/utils/ncbiBlast.ts +0 -143
  82. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/CachedBlastResults.d.ts +0 -0
  83. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.d.ts +0 -0
  84. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.js +0 -0
  85. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.d.ts +0 -0
  86. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.js +0 -0
  87. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.d.ts +0 -0
  88. /package/dist/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.js +0 -0
  89. /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/MsaAlgorithmSelect.tsx +0 -0
  90. /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/blastLaunchView.ts +0 -0
  91. /package/src/LaunchMsaView/components/{NCBIBlastQuery → BlastQuery}/useCachedBlastResults.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  import { cleanProteinSequence } from '../LaunchMsaView/util';
2
2
  import { launchMSA } from '../utils/msa';
3
- import { fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils/ncbiOrthologs';
3
+ import { COMMON_SPECIES, fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils/ncbiOrthologs';
4
4
  /**
5
5
  * The no-search-job alternative to doLaunchBlast.
6
6
  *
@@ -18,7 +18,6 @@ import { fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils
18
18
  */
19
19
  export async function doLaunchOrthologs({ self, }) {
20
20
  const { taxId, taxa, geneCandidates, msaAlgorithm, proteinSequence } = self.orthologParams;
21
- const cleanedSeq = cleanProteinSequence(proteinSequence);
22
21
  const onProgress = (arg) => {
23
22
  self.setProgress(arg);
24
23
  };
@@ -27,15 +26,33 @@ export async function doLaunchOrthologs({ self, }) {
27
26
  if (!resolved) {
28
27
  throw new Error(`Could not resolve any of ${geneCandidates.join(', ')} to an NCBI gene in taxon ${taxId}. Try the NCBI BLAST tab, which needs no gene identifier.`);
29
28
  }
30
- // the query species is represented by the user's own transcript below
31
- const wanted = new Set(taxa.filter(t => t !== taxId));
29
+ // The query row. The dialog always supplies it — it is the user's OWN
30
+ // selected transcript, which is what makes `connectedFeature` map genome
31
+ // coordinates through this row. A launch that has no transcript to translate
32
+ // (a session spec naming only a gene) falls back to NCBI's representative
33
+ // protein for the resolved gene, which is the same choice made for every
34
+ // other row, so the alignment is the one NCBI would build for that gene.
35
+ const representative = await fetchRepresentativeQueryProtein(resolved.geneId);
36
+ const cleanedSeq = proteinSequence
37
+ ? cleanProteinSequence(proteinSequence)
38
+ : representative?.sequence;
39
+ if (!cleanedSeq) {
40
+ throw new Error(`No query protein: none was supplied and NCBI returned no representative protein for gene ${resolved.geneId}.`);
41
+ }
42
+ // Every species the panel offers, when a launch names none. A spec that wants
43
+ // a narrower comparison says so; one that just wants "this gene across
44
+ // species" should not have to enumerate the list the dialog would have
45
+ // checked for it.
46
+ const wantedTaxa = taxa ?? COMMON_SPECIES.map(s => s.taxId);
47
+ // the query species is represented by the query row above
48
+ const wanted = new Set(wantedTaxa.filter(t => t !== taxId));
32
49
  const rows = await fetchOrthologRows({
33
50
  geneId: resolved.geneId,
34
51
  taxa: wanted,
35
52
  onProgress,
36
53
  });
37
54
  const treeMetadata = {
38
- QUERY: await buildQueryMetadata(self, resolved.geneId, cleanedSeq),
55
+ QUERY: buildQueryMetadata(self, resolved.geneId, cleanedSeq, representative),
39
56
  };
40
57
  for (const row of rows) {
41
58
  treeMetadata[row.label] = buildRowMetadata(row);
@@ -54,31 +71,37 @@ export async function doLaunchOrthologs({ self, }) {
54
71
  };
55
72
  }
56
73
  /**
57
- * The query row is the user's own translated transcript, so it carries an
58
- * Accession — which is what drives the automatic CDD overlay
59
- * (afterCreateAutoruns.autoLoadProteinDomains -> loadProteinDomains) — ONLY
60
- * when its sequence is byte-identical to the RefSeq protein that accession
74
+ * A failed lookup only costs the query row its domain overlay and, for a launch
75
+ * that supplied no sequence of its own, the alignment — so it is reported by
76
+ * returning nothing rather than by throwing here.
77
+ */
78
+ async function fetchRepresentativeQueryProtein(geneId) {
79
+ try {
80
+ return await fetchProteinForGene(geneId);
81
+ }
82
+ catch (e) {
83
+ console.warn('[msaview-orthologs] query protein lookup failed:', e);
84
+ return undefined;
85
+ }
86
+ }
87
+ /**
88
+ * The query row carries an Accession — which is what drives the automatic CDD
89
+ * overlay (afterCreateAutoruns.autoLoadProteinDomains -> loadProteinDomains) —
90
+ * ONLY when its sequence is byte-identical to the RefSeq protein that accession
61
91
  * names. Attaching it unconditionally would put every domain box at an offset
62
92
  * whenever the user picked a non-representative isoform, which is a silently
63
- * wrong figure rather than a missing one.
93
+ * wrong figure rather than a missing one. A launch that took the representative
94
+ * protein as its query row passes that test by construction.
64
95
  */
65
- async function buildQueryMetadata(self, geneId, proteinSequence) {
96
+ function buildQueryMetadata(self, geneId, proteinSequence, representative) {
66
97
  const transcript = self.orthologParams?.selectedTranscript;
67
98
  const metadata = { 'Gene ID': geneId };
68
99
  const name = transcript?.get('name') ?? transcript?.get('id');
69
100
  if (name) {
70
101
  metadata.Transcript = name;
71
102
  }
72
- try {
73
- const representative = await fetchProteinForGene(geneId);
74
- if (representative?.sequence === proteinSequence) {
75
- metadata.Accession = representative.accession;
76
- }
77
- }
78
- catch (e) {
79
- // a failed lookup only costs the query row its domain overlay, so it must
80
- // not take down an alignment that is otherwise complete
81
- console.warn('[msaview-orthologs] query protein lookup failed:', e);
103
+ if (representative?.sequence === proteinSequence) {
104
+ metadata.Accession = representative.accession;
82
105
  }
83
106
  return metadata;
84
107
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,127 @@
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+ import { doLaunchOrthologs } from './doLaunchOrthologs';
3
+ import { launchMSA } from '../utils/msa';
4
+ import { COMMON_SPECIES, fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils/ncbiOrthologs';
5
+ // Every network call is mocked and nothing else is. What is under test is the
6
+ // argument shaping either side of those calls -- which taxa get asked for, what
7
+ // becomes the QUERY row, and whether the row earns the Accession that drives the
8
+ // CDD overlay -- so the real COMMON_SPECIES list and the real
9
+ // cleanProteinSequence stay in the picture.
10
+ vi.mock('../utils/ncbiOrthologs', async (importOriginal) => ({
11
+ ...(await importOriginal()),
12
+ resolveGeneId: vi.fn(),
13
+ fetchProteinForGene: vi.fn(),
14
+ fetchOrthologRows: vi.fn(),
15
+ }));
16
+ vi.mock('../utils/msa', () => ({ launchMSA: vi.fn() }));
17
+ const mockResolveGeneId = vi.mocked(resolveGeneId);
18
+ const mockFetchProtein = vi.mocked(fetchProteinForGene);
19
+ const mockFetchRows = vi.mocked(fetchOrthologRows);
20
+ const mockLaunchMSA = vi.mocked(launchMSA);
21
+ const HUMAN = 9606;
22
+ const GENE_ID = '22861';
23
+ const REPRESENTATIVE = { accession: 'NP_127497.1', sequence: 'MAGGAWGRLACY' };
24
+ function makeModel(orthologParams) {
25
+ return {
26
+ orthologParams,
27
+ setProgress: () => { },
28
+ };
29
+ }
30
+ function params(extra = {}) {
31
+ return {
32
+ taxId: HUMAN,
33
+ geneCandidates: ['NLRP1'],
34
+ msaAlgorithm: 'clustalo',
35
+ ...extra,
36
+ };
37
+ }
38
+ // What fetchOrthologRows was asked for, which is the only place the taxa
39
+ // default is observable.
40
+ function taxaAskedFor() {
41
+ return [...mockFetchRows.mock.calls[0][0].taxa].sort((a, b) => a - b);
42
+ }
43
+ // The QUERY row as it went to the aligner, read back out of the FASTA rather
44
+ // than out of an intermediate, since the FASTA is what the alignment is of.
45
+ function queryRowSent() {
46
+ return mockLaunchMSA.mock.calls[0][0].sequence.split('\n')[1];
47
+ }
48
+ function queryMetadata(result) {
49
+ return JSON.parse(result.treeMetadata).QUERY;
50
+ }
51
+ beforeEach(() => {
52
+ vi.clearAllMocks();
53
+ mockResolveGeneId.mockResolvedValue({ geneId: GENE_ID, matched: 'NLRP1' });
54
+ mockFetchProtein.mockResolvedValue(REPRESENTATIVE);
55
+ mockFetchRows.mockResolvedValue([]);
56
+ mockLaunchMSA.mockResolvedValue({ msa: '', tree: '' });
57
+ });
58
+ describe('taxa', () => {
59
+ test('omitted asks for every species the dialog offers, less the query', async () => {
60
+ await doLaunchOrthologs({ self: makeModel(params()) });
61
+ expect(taxaAskedFor()).toEqual(COMMON_SPECIES.map(s => s.taxId)
62
+ .filter(t => t !== HUMAN)
63
+ .sort((a, b) => a - b));
64
+ });
65
+ test('given is taken as written, less the query', async () => {
66
+ await doLaunchOrthologs({
67
+ self: makeModel(params({ taxa: [HUMAN, 10090, 9615] })),
68
+ });
69
+ expect(taxaAskedFor()).toEqual([9615, 10090]);
70
+ });
71
+ test('an empty list is a request for no rows, not a request for all of them', async () => {
72
+ await doLaunchOrthologs({ self: makeModel(params({ taxa: [] })) });
73
+ expect(taxaAskedFor()).toEqual([]);
74
+ });
75
+ });
76
+ describe('the QUERY row', () => {
77
+ test('omitted proteinSequence falls back to the representative protein', async () => {
78
+ await doLaunchOrthologs({ self: makeModel(params()) });
79
+ expect(queryRowSent()).toBe(REPRESENTATIVE.sequence);
80
+ });
81
+ test('a supplied sequence is used, and is cleaned first', async () => {
82
+ await doLaunchOrthologs({
83
+ self: makeModel(params({ proteinSequence: 'MAGG*AWGR&' })),
84
+ });
85
+ expect(queryRowSent()).toBe('MAGGAWGR');
86
+ });
87
+ test('throws when neither a sequence nor a representative is available', async () => {
88
+ mockFetchProtein.mockResolvedValue(undefined);
89
+ await expect(doLaunchOrthologs({ self: makeModel(params()) })).rejects.toThrow(/No query protein/);
90
+ expect(mockLaunchMSA).not.toHaveBeenCalled();
91
+ });
92
+ test('a failed representative lookup does not take down a launch that brought its own sequence', async () => {
93
+ vi.spyOn(console, 'warn').mockImplementation(() => { });
94
+ mockFetchProtein.mockRejectedValue(new Error('429'));
95
+ await doLaunchOrthologs({
96
+ self: makeModel(params({ proteinSequence: REPRESENTATIVE.sequence })),
97
+ });
98
+ expect(queryRowSent()).toBe(REPRESENTATIVE.sequence);
99
+ });
100
+ });
101
+ // The Accession is what afterCreateAutoruns.autoLoadProteinDomains keys the CDD
102
+ // overlay off, and attaching it to a row that is NOT the protein it names draws
103
+ // every domain box at an offset. So the byte-identity guard is the assertion
104
+ // here, in both directions.
105
+ describe('the Accession that drives the domain overlay', () => {
106
+ test('is attached when the query row IS the representative protein', async () => {
107
+ const result = await doLaunchOrthologs({ self: makeModel(params()) });
108
+ expect(queryMetadata(result)).toMatchObject({
109
+ 'Gene ID': GENE_ID,
110
+ Accession: REPRESENTATIVE.accession,
111
+ });
112
+ });
113
+ test('is withheld from a non-representative isoform', async () => {
114
+ const result = await doLaunchOrthologs({
115
+ self: makeModel(params({ proteinSequence: 'MDIFFERENTISOFORM' })),
116
+ });
117
+ expect(queryMetadata(result).Accession).toBeUndefined();
118
+ });
119
+ test('is withheld when the representative lookup failed', async () => {
120
+ vi.spyOn(console, 'warn').mockImplementation(() => { });
121
+ mockFetchProtein.mockRejectedValue(new Error('429'));
122
+ const result = await doLaunchOrthologs({
123
+ self: makeModel(params({ proteinSequence: REPRESENTATIVE.sequence })),
124
+ });
125
+ expect(queryMetadata(result).Accession).toBeUndefined();
126
+ });
127
+ });
@@ -1,6 +1,6 @@
1
1
  export type { MSAFormat } from 'msa-parsers';
2
2
  import type { MafRegion, MsaViewInitState } from './types';
3
- import type { BlastDatabase, BlastProgram, MsaAlgorithm } from '../LaunchMsaView/components/NCBIBlastQuery/consts';
3
+ import type { BlastDatabase, MsaAlgorithm } from '../LaunchMsaView/components/BlastQuery/consts';
4
4
  import type { Feature } from '@jbrowse/core/util';
5
5
  import type { Instance } from '@jbrowse/mobx-state-tree';
6
6
  import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view';
@@ -12,24 +12,32 @@ export interface IRegion {
12
12
  end: number;
13
13
  }
14
14
  export interface BlastParams {
15
- baseUrl: string;
16
15
  blastDatabase: BlastDatabase;
17
16
  msaAlgorithm: MsaAlgorithm;
18
- blastProgram: BlastProgram;
19
17
  selectedTranscript?: Feature;
20
18
  proteinSequence: string;
21
- rid?: string;
22
19
  }
23
20
  export interface OrthologParams {
24
21
  /** NCBI taxon id of the assembly the query gene came from */
25
22
  taxId: number;
26
- /** taxon ids to include as rows (the query taxon is represented by QUERY) */
27
- taxa: number[];
23
+ /**
24
+ * taxon ids to include as rows (the query taxon is represented by QUERY).
25
+ * Omitted means every species the launch dialog offers, which is what a
26
+ * launch that just wants "this gene across species" wants.
27
+ */
28
+ taxa?: number[];
28
29
  /** candidate gene identifiers off the feature, tried in order */
29
30
  geneCandidates: string[];
30
31
  msaAlgorithm: MsaAlgorithm;
31
32
  selectedTranscript?: Feature;
32
- proteinSequence: string;
33
+ /**
34
+ * The QUERY row. The launch dialog always supplies it, translated from the
35
+ * transcript the user picked, which is what `connectedFeature` maps genome
36
+ * coordinates through. Omitted — a session spec naming a gene and nothing
37
+ * else — the query row becomes NCBI's representative protein for the resolved
38
+ * gene, the same choice every other row makes.
39
+ */
40
+ proteinSequence?: string;
33
41
  }
34
42
  /**
35
43
  * #stateModel MsaViewPlugin