jbrowse-plugin-msaview 3.3.0 → 3.4.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 (97) hide show
  1. package/dist/AddHighlightModel/GenomeMouseoverHighlight.js +1 -1
  2. package/dist/AddHighlightModel/MsaToGenomeHighlight.js +1 -1
  3. package/dist/AddHighlightModel/index.js +1 -1
  4. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +61 -17
  5. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -1
  6. package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.js +2 -2
  7. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.d.ts +12 -0
  8. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +21 -2
  9. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.d.ts +1 -0
  10. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.js +29 -0
  11. package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +1 -1
  12. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +28 -0
  13. package/dist/LaunchMsaView/components/BlastQuery/consts.js +21 -0
  14. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -1
  15. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +3 -3
  16. package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +1 -1
  17. package/dist/LaunchMsaView/components/useFeatureSequence.js +1 -1
  18. package/dist/LaunchMsaView/detectQueryRow.d.ts +15 -2
  19. package/dist/LaunchMsaView/detectQueryRow.js +20 -21
  20. package/dist/LaunchMsaView/detectQueryRow.test.js +15 -15
  21. package/dist/LaunchMsaView/useQueryRowName.js +5 -8
  22. package/dist/MsaViewPanel/afterCreateAutoruns.js +2 -2
  23. package/dist/MsaViewPanel/components/ErrorBoundary.d.ts +2 -2
  24. package/dist/MsaViewPanel/components/JobLink.js +7 -1
  25. package/dist/MsaViewPanel/components/LaunchProgress.d.ts +17 -0
  26. package/dist/MsaViewPanel/components/LaunchProgress.js +41 -0
  27. package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -3
  28. package/dist/MsaViewPanel/components/MsaViewPanel.test.d.ts +1 -0
  29. package/dist/MsaViewPanel/components/MsaViewPanel.test.js +68 -0
  30. package/dist/MsaViewPanel/doLaunchBlast.d.ts +1 -1
  31. package/dist/MsaViewPanel/doLaunchBlast.js +84 -52
  32. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +1 -1
  33. package/dist/MsaViewPanel/genomeToMSA.js +4 -2
  34. package/dist/MsaViewPanel/genomeToMSA.test.js +34 -0
  35. package/dist/MsaViewPanel/model.d.ts +32 -11
  36. package/dist/MsaViewPanel/observeProteinHighlights.test.js +11 -0
  37. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +1 -0
  38. package/dist/MsaViewPanel/util.d.ts +18 -0
  39. package/dist/MsaViewPanel/util.js +17 -0
  40. package/dist/jbrowse-plugin-msaview.umd.production.min.js +47 -35
  41. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  42. package/dist/utils/blastCache.d.ts +10 -6
  43. package/dist/utils/blastCache.js +15 -3
  44. package/dist/utils/ebiBlast.d.ts +1 -1
  45. package/dist/utils/msa.d.ts +12 -0
  46. package/dist/utils/msa.js +35 -12
  47. package/dist/utils/msaRows.d.ts +31 -0
  48. package/dist/utils/msaRows.js +67 -0
  49. package/dist/utils/phmmer.d.ts +53 -0
  50. package/dist/utils/phmmer.js +118 -0
  51. package/dist/utils/taxonomyNames.d.ts +1 -1
  52. package/dist/utils/taxonomyNames.js +6 -1
  53. package/dist/version.d.ts +1 -1
  54. package/dist/version.js +1 -1
  55. package/package.json +27 -21
  56. package/src/AddHighlightModel/GenomeMouseoverHighlight.tsx +1 -1
  57. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +1 -1
  58. package/src/AddHighlightModel/index.tsx +1 -1
  59. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +88 -30
  60. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -1
  61. package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +4 -4
  62. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.ts +50 -0
  63. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +23 -3
  64. package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +1 -1
  65. package/src/LaunchMsaView/components/BlastQuery/consts.ts +40 -0
  66. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -1
  67. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +5 -5
  68. package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +1 -1
  69. package/src/LaunchMsaView/components/useFeatureSequence.ts +1 -1
  70. package/src/LaunchMsaView/detectQueryRow.test.ts +17 -15
  71. package/src/LaunchMsaView/detectQueryRow.ts +34 -23
  72. package/src/LaunchMsaView/useQueryRowName.ts +6 -9
  73. package/src/MsaViewPanel/afterCreateAutoruns.ts +2 -2
  74. package/src/MsaViewPanel/components/ErrorBoundary.tsx +2 -1
  75. package/src/MsaViewPanel/components/JobLink.tsx +7 -2
  76. package/src/MsaViewPanel/components/LaunchProgress.tsx +62 -0
  77. package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +83 -0
  78. package/src/MsaViewPanel/components/MsaViewPanel.tsx +7 -4
  79. package/src/MsaViewPanel/doLaunchBlast.ts +127 -69
  80. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +2 -2
  81. package/src/MsaViewPanel/doLaunchOrthologs.ts +1 -1
  82. package/src/MsaViewPanel/genomeToMSA.test.ts +37 -0
  83. package/src/MsaViewPanel/genomeToMSA.ts +6 -2
  84. package/src/MsaViewPanel/model.ts +28 -5
  85. package/src/MsaViewPanel/observeProteinHighlights.test.ts +13 -0
  86. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +1 -0
  87. package/src/MsaViewPanel/util.ts +18 -0
  88. package/src/utils/blastCache.ts +33 -12
  89. package/src/utils/ebiBlast.ts +1 -1
  90. package/src/utils/msa.ts +43 -12
  91. package/src/utils/msaRows.ts +95 -0
  92. package/src/utils/phmmer.ts +174 -0
  93. package/src/utils/taxonomyNames.ts +6 -1
  94. package/src/version.ts +1 -1
  95. package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +0 -6
  96. package/dist/MsaViewPanel/components/LoadingBLAST.js +0 -26
  97. package/src/MsaViewPanel/components/LoadingBLAST.tsx +0 -48
@@ -2,12 +2,18 @@ import React from 'react';
2
2
  import { Typography } from '@mui/material';
3
3
  import ExternalLink from '../../components/ExternalLink';
4
4
  import { ebiBlastResultUrl } from '../../utils/ebiBlast';
5
+ import { isPhmmerJobId, phmmerResultUrl } from '../../utils/phmmer';
5
6
  function JobLink({ jobId }) {
7
+ // read off the job id rather than the launch params, so a link rebuilt for an
8
+ // old cached job still points at the tool that actually ran it
9
+ const url = isPhmmerJobId(jobId)
10
+ ? phmmerResultUrl(jobId)
11
+ : ebiBlastResultUrl(jobId);
6
12
  return (React.createElement(Typography, null,
7
13
  "Job ",
8
14
  jobId,
9
15
  " (",
10
- React.createElement(ExternalLink, { href: ebiBlastResultUrl(jobId) }, "see status"),
16
+ React.createElement(ExternalLink, { href: url }, "see status"),
11
17
  ")"));
12
18
  }
13
19
  export default JobLink;
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import type { JBrowsePluginMsaViewModel } from '../model';
3
+ /**
4
+ * What a view shows while it is still building its alignment, and what it shows
5
+ * when that fails.
6
+ *
7
+ * Every launch that resolves something leaves its request on the model until it
8
+ * succeeds -- `blastParams`, `orthologParams`, `init` -- so one still being
9
+ * there IS "no alignment yet", and the error a failed launch records is only
10
+ * readable here. This used to key on `blastParams` alone, which left an ortholog
11
+ * launch rendering an empty MSAView for the minutes its alignment takes and, on
12
+ * failure, forever: the error was set and nothing drew it.
13
+ */
14
+ declare const LaunchProgress: ({ model, }: {
15
+ model: JBrowsePluginMsaViewModel;
16
+ }) => React.JSX.Element;
17
+ export default LaunchProgress;
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { ErrorMessage, LoadingEllipses } from '@jbrowse/core/ui';
3
+ import { Typography } from '@mui/material';
4
+ import { observer } from 'mobx-react';
5
+ import { makeStyles } from 'tss-react/mui';
6
+ import JobLink from './JobLink';
7
+ const useStyles = makeStyles()({
8
+ margin: {
9
+ padding: 20,
10
+ },
11
+ });
12
+ /**
13
+ * What a view shows while it is still building its alignment, and what it shows
14
+ * when that fails.
15
+ *
16
+ * Every launch that resolves something leaves its request on the model until it
17
+ * succeeds -- `blastParams`, `orthologParams`, `init` -- so one still being
18
+ * there IS "no alignment yet", and the error a failed launch records is only
19
+ * readable here. This used to key on `blastParams` alone, which left an ortholog
20
+ * launch rendering an empty MSAView for the minutes its alignment takes and, on
21
+ * failure, forever: the error was set and nothing drew it.
22
+ */
23
+ const LaunchProgress = observer(function LaunchProgress2({ model, }) {
24
+ const { blastParams, orthologParams, progress, rid, error } = model;
25
+ const { classes } = useStyles();
26
+ const message = blastParams
27
+ ? 'Running EBI BLAST'
28
+ : orthologParams
29
+ ? 'Building ortholog alignment'
30
+ : 'Loading alignment';
31
+ return (React.createElement("div", { className: classes.margin }, error ? (React.createElement(React.Fragment, null,
32
+ React.createElement(Typography, { variant: "h5" },
33
+ message,
34
+ " failed"),
35
+ rid ? React.createElement(JobLink, { jobId: rid }) : null,
36
+ React.createElement(ErrorMessage, { error: error }))) : (React.createElement(React.Fragment, null,
37
+ React.createElement(LoadingEllipses, { message: message, variant: "h5" }),
38
+ rid ? React.createElement(JobLink, { jobId: rid }) : null,
39
+ React.createElement(Typography, null, progress || 'Initializing')))));
40
+ });
41
+ export default LaunchProgress;
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
4
4
  import { MSAView } from 'react-msaview';
5
5
  import { makeStyles } from 'tss-react/mui';
6
6
  import { ErrorBoundary } from './ErrorBoundary';
7
- import LoadingBLAST from './LoadingBLAST';
7
+ import LaunchProgress from './LaunchProgress';
8
8
  const useStyles = makeStyles()({
9
9
  loadingContainer: {
10
10
  padding: 20,
@@ -12,9 +12,12 @@ const useStyles = makeStyles()({
12
12
  });
13
13
  const MsaViewPanel = observer(function MsaViewPanel2({ model, }) {
14
14
  const { classes } = useStyles();
15
- const { blastParams, loadingStoredData } = model;
15
+ const { blastParams, orthologParams, init, loadingStoredData } = model;
16
+ // an unresolved launch request means there is no alignment to draw yet, so all
17
+ // three gate the same panel -- see LaunchProgress
18
+ const launching = !!(blastParams ?? orthologParams ?? init);
16
19
  return (React.createElement(ErrorBoundary, null,
17
- React.createElement("div", null, blastParams ? (React.createElement(LoadingBLAST, { model: model })) : loadingStoredData ? (React.createElement("div", { className: classes.loadingContainer },
20
+ React.createElement("div", null, launching ? (React.createElement(LaunchProgress, { model: model })) : loadingStoredData ? (React.createElement("div", { className: classes.loadingContainer },
18
21
  React.createElement(LoadingEllipses, { message: "Loading MSA data", variant: "h6" }))) : (React.createElement(MSAView, { model: model })))));
19
22
  });
20
23
  export default MsaViewPanel;
@@ -0,0 +1,68 @@
1
+ // @vitest-environment jsdom
2
+ import React from 'react';
3
+ import { cleanup, render, screen } from '@testing-library/react';
4
+ import { afterEach, expect, test, vi } from 'vitest';
5
+ import MsaViewPanel from './MsaViewPanel';
6
+ // react-msaview's MSAView is the "nothing is launching" branch and wants a real
7
+ // MST model; a marker is enough to say the panel reached it
8
+ vi.mock('react-msaview', () => ({
9
+ MSAView: () => React.createElement("div", null, "the alignment"),
10
+ }));
11
+ function panel(model) {
12
+ return render(React.createElement(MsaViewPanel, { model: model }));
13
+ }
14
+ afterEach(() => {
15
+ cleanup();
16
+ });
17
+ test('an alignment with no pending launch draws itself', () => {
18
+ panel({ progress: '' });
19
+ expect(screen.getByText('the alignment')).toBeTruthy();
20
+ });
21
+ test('a running BLAST shows its progress, not an empty alignment', () => {
22
+ panel({
23
+ blastParams: { proteinSequence: 'MKV' },
24
+ progress: 'Submitting query',
25
+ });
26
+ expect(screen.queryByText('the alignment')).toBeNull();
27
+ expect(screen.getByText(/Running EBI BLAST/)).toBeTruthy();
28
+ expect(screen.getByText('Submitting query')).toBeTruthy();
29
+ });
30
+ // the bug this file was written for: an ortholog launch sets orthologParams
31
+ // rather than blastParams, and the panel keyed on blastParams alone -- so it
32
+ // rendered an empty MSAView for the minutes the alignment takes, and drew
33
+ // nothing at all when the launch failed
34
+ test('a running ortholog launch shows its progress', () => {
35
+ panel({
36
+ orthologParams: { taxId: 9606 },
37
+ progress: 'Resolving orthologs',
38
+ });
39
+ expect(screen.queryByText('the alignment')).toBeNull();
40
+ expect(screen.getByText(/Building ortholog alignment/)).toBeTruthy();
41
+ expect(screen.getByText('Resolving orthologs')).toBeTruthy();
42
+ });
43
+ test('a failed ortholog launch shows why', () => {
44
+ panel({
45
+ orthologParams: { taxId: 9606 },
46
+ progress: '',
47
+ error: new Error('Only 1 ortholog(s) found for this gene'),
48
+ });
49
+ expect(screen.getByText(/Only 1 ortholog\(s\) found/)).toBeTruthy();
50
+ });
51
+ test('a failed init shows why', () => {
52
+ panel({
53
+ init: { msaName: 'ENST00000288602' },
54
+ progress: '',
55
+ error: new Error('No alignment named ENST00000288602 in msa.fa.gz'),
56
+ });
57
+ expect(screen.queryByText('the alignment')).toBeNull();
58
+ expect(screen.getByText(/No alignment named ENST00000288602/)).toBeTruthy();
59
+ });
60
+ test('a running job links out to it', () => {
61
+ panel({
62
+ blastParams: { proteinSequence: 'MKV' },
63
+ progress: 'Re-checking BLAST status in... 7',
64
+ rid: 'ncbiblast-R20260826-123456-0001-abc',
65
+ });
66
+ const link = screen.getByRole('link');
67
+ expect(link.getAttribute('href')).toContain('jobId=ncbiblast-R20260826-123456-0001-abc');
68
+ });
@@ -2,7 +2,7 @@ import type { JBrowsePluginMsaViewModel } from './model';
2
2
  export declare function doLaunchBlast({ self, }: {
3
3
  self: JBrowsePluginMsaViewModel;
4
4
  }): Promise<{
5
- treeMetadata: string;
6
5
  msa: string;
7
6
  tree: string;
7
+ treeMetadata: string;
8
8
  }>;
@@ -2,29 +2,72 @@ import { makeId, strip } from '../LaunchMsaView/components/util';
2
2
  import { cleanProteinSequence } from '../LaunchMsaView/util';
3
3
  import { saveBlastResult } from '../utils/blastCache';
4
4
  import { queryEbiBlast } from '../utils/ebiBlast';
5
- import { launchMSA } from '../utils/msa';
5
+ import { launchMSA, launchTree } from '../utils/msa';
6
+ import { buildPhmmerMsa, buildRowMetadata } from '../utils/msaRows';
7
+ import { queryPhmmer } from '../utils/phmmer';
6
8
  import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
7
9
  export async function doLaunchBlast({ self, }) {
8
- const { blastDatabase, msaAlgorithm, proteinSequence, selectedTranscript } = self.blastParams;
9
- const cleanedSeq = cleanProteinSequence(proteinSequence);
10
+ // kept whole rather than destructured: the database's type depends on
11
+ // searchProgram, and pulling the two apart loses the link between them
12
+ const params = self.blastParams;
13
+ const { selectedTranscript } = params;
14
+ const cleanedSeq = cleanProteinSequence(params.proteinSequence);
10
15
  const onProgress = (arg) => {
11
16
  self.setProgress(arg);
12
17
  };
18
+ // publish the job id before the first poll so the view can link out while the
19
+ // job is still running
20
+ const onRid = (r) => {
21
+ self.setRid(r);
22
+ };
23
+ const { msa, tree, treeMetadata, rid } = params.searchProgram === 'phmmer'
24
+ ? await runPhmmer({
25
+ query: cleanedSeq,
26
+ database: params.blastDatabase,
27
+ onProgress,
28
+ onRid,
29
+ })
30
+ : await runBlast({
31
+ query: cleanedSeq,
32
+ blastDatabase: params.blastDatabase,
33
+ msaAlgorithm: params.msaAlgorithm,
34
+ onProgress,
35
+ onRid,
36
+ });
37
+ const treeMetadataJson = JSON.stringify(treeMetadata);
38
+ await saveBlastResult({
39
+ proteinSequence: cleanedSeq,
40
+ blastDatabase: params.blastDatabase,
41
+ msaAlgorithm: params.msaAlgorithm,
42
+ searchProgram: params.searchProgram,
43
+ msa,
44
+ tree,
45
+ treeMetadata: treeMetadataJson,
46
+ rid,
47
+ geneId: selectedTranscript?.get('parentId'),
48
+ transcriptId: selectedTranscript?.id(),
49
+ transcriptName: selectedTranscript?.get('name') ?? selectedTranscript?.get('id'),
50
+ geneName: selectedTranscript?.get('gene_name') ??
51
+ selectedTranscript?.get('parentId'),
52
+ });
53
+ return { msa, tree, treeMetadata: treeMetadataJson };
54
+ }
55
+ /**
56
+ * BLAST returns each hit already aligned to the query, but pairwise and one hit
57
+ * at a time, so the alignments are stripped back off and every hit is realigned
58
+ * together by a dedicated aligner.
59
+ */
60
+ async function runBlast({ query, blastDatabase, msaAlgorithm, onProgress, onRid, }) {
13
61
  const { hits, rid } = await queryEbiBlast({
14
- query: cleanedSeq,
62
+ query,
15
63
  blastDatabase,
16
64
  onProgress,
17
- // publish the job id before the first poll so the view can link out while
18
- // the job is still running
19
- onRid: r => {
20
- self.setRid(r);
21
- },
65
+ onRid,
22
66
  });
23
- self.setProgress('Fetching species taxonomy info...');
24
- const taxids = hits
67
+ onProgress('Fetching species taxonomy info...');
68
+ const taxonomyInfo = await fetchTaxonomyInfo(hits
25
69
  .map(h => h.description[0]?.taxid)
26
- .filter((t) => t !== undefined);
27
- const taxonomyInfo = await fetchTaxonomyInfo(taxids);
70
+ .filter((t) => t !== undefined));
28
71
  const treeMetadata = {};
29
72
  const sequences = hits.map(h => {
30
73
  const desc = h.description[0] ?? {
@@ -33,52 +76,41 @@ export async function doLaunchBlast({ self, }) {
33
76
  sciname: 'unknown',
34
77
  };
35
78
  const rowName = makeId(desc, taxonomyInfo);
36
- const seq = strip(h.hsps[0]?.hseq ?? '');
37
79
  treeMetadata[rowName] = buildRowMetadata(desc, taxonomyInfo);
38
- return `>${rowName}\n${seq}`;
80
+ return `>${rowName}\n${strip(h.hsps[0]?.hseq ?? '')}`;
39
81
  });
40
82
  const result = await launchMSA({
41
83
  algorithm: msaAlgorithm,
42
- sequence: [`>QUERY\n${cleanedSeq}`, ...sequences].join('\n'),
84
+ sequence: [`>QUERY\n${query}`, ...sequences].join('\n'),
43
85
  onProgress,
44
86
  });
45
- const treeMetadataJson = JSON.stringify(treeMetadata);
46
- await saveBlastResult({
47
- proteinSequence: cleanedSeq,
48
- blastDatabase,
49
- msaAlgorithm,
50
- msa: result.msa,
51
- tree: result.tree,
52
- treeMetadata: treeMetadataJson,
53
- rid,
54
- geneId: selectedTranscript?.get('parentId'),
55
- transcriptId: selectedTranscript?.id(),
56
- transcriptName: selectedTranscript?.get('name') ?? selectedTranscript?.get('id'),
57
- geneName: selectedTranscript?.get('gene_name') ??
58
- selectedTranscript?.get('parentId'),
87
+ return { ...result, treeMetadata, rid };
88
+ }
89
+ /**
90
+ * phmmer aligns every hit to a profile of the query as it searches, so its own
91
+ * output is the MSA and there is no realignment step — the hits keep the
92
+ * placement HMMER gave them, and the query row is derived from the alignment's
93
+ * match columns rather than being aligned back in afterwards. That leaves no
94
+ * aligner run to take a tree from, so the tree is built from this alignment.
95
+ */
96
+ async function runPhmmer({ query, database, onProgress, onRid, }) {
97
+ const { rows, queryRow, rid } = await queryPhmmer({
98
+ query,
99
+ database,
100
+ onProgress,
101
+ onRid,
102
+ });
103
+ onProgress('Fetching species taxonomy info...');
104
+ const taxonomyInfo = await fetchTaxonomyInfo(rows.map(r => r.taxid).filter((t) => t !== undefined));
105
+ const { msa, treeMetadata } = buildPhmmerMsa({
106
+ rows,
107
+ queryRow,
108
+ taxonomyInfo,
59
109
  });
60
110
  return {
61
- ...result,
62
- treeMetadata: treeMetadataJson,
111
+ msa,
112
+ tree: await launchTree({ alignment: msa, onProgress }),
113
+ treeMetadata,
114
+ rid,
63
115
  };
64
116
  }
65
- function buildRowMetadata(desc, taxonomyInfo) {
66
- const metadata = {};
67
- const taxInfo = desc.taxid ? taxonomyInfo.get(desc.taxid) : undefined;
68
- if (taxInfo?.sciname) {
69
- metadata['Scientific name'] = taxInfo.sciname;
70
- }
71
- if (taxInfo?.commonName) {
72
- metadata['Common name'] = taxInfo.commonName;
73
- }
74
- if (desc.accession) {
75
- metadata.Accession = desc.accession;
76
- }
77
- if (desc.id) {
78
- metadata.ID = desc.id;
79
- }
80
- if (desc.title) {
81
- metadata.Description = desc.title;
82
- }
83
- return metadata;
84
- }
@@ -1,9 +1,9 @@
1
1
  import { beforeEach, describe, expect, test, vi } from 'vitest';
2
- import { doLaunchOrthologs } from './doLaunchOrthologs';
3
2
  import { launchMSA } from '../utils/msa';
4
3
  import { defaultMaxSpecies, fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils/ncbiOrthologs';
5
4
  import { fetchPantherOrthologs } from '../utils/pantherOrthologs';
6
5
  import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
6
+ import { doLaunchOrthologs } from './doLaunchOrthologs';
7
7
  // Every network call is mocked and nothing else is. What is under test is the
8
8
  // argument shaping either side of those calls -- which species get asked for,
9
9
  // what becomes the QUERY row, and whether the row earns the Accession that
@@ -1,9 +1,11 @@
1
1
  import { getSession } from '@jbrowse/core/util';
2
- import { hasHoverPosition } from './util';
2
+ import { hasHoverPosition, hasQueryRow } from './util';
3
3
  export function genomeToMSA({ model }) {
4
4
  const { hovered } = getSession(model);
5
5
  const { querySeqName, transcriptToMsaMap, connectedView, mafRegion } = model;
6
- if (!connectedView?.initialized || !hasHoverPosition(hovered)) {
6
+ if (!connectedView?.initialized ||
7
+ !hasHoverPosition(hovered) ||
8
+ !hasQueryRow(model)) {
7
9
  return undefined;
8
10
  }
9
11
  const { coord, refName } = hovered.hoverPosition;
@@ -19,6 +19,7 @@ describe('genomeToMSA', () => {
19
19
  });
20
20
  const model = {
21
21
  querySeqName: 'hg38.chr1',
22
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
22
23
  transcriptToMsaMap: undefined,
23
24
  mafRegion: {
24
25
  refName: 'chr1',
@@ -38,6 +39,7 @@ describe('genomeToMSA', () => {
38
39
  });
39
40
  const model = {
40
41
  querySeqName: 'hg38.chr1',
42
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
41
43
  transcriptToMsaMap: undefined,
42
44
  mafRegion: {
43
45
  refName: 'chr1',
@@ -62,6 +64,7 @@ describe('genomeToMSA', () => {
62
64
  const mockSeqPosToVisibleCol = vi.fn().mockReturnValue(5);
63
65
  const model = {
64
66
  querySeqName: 'hg38.chr1',
67
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
65
68
  transcriptToMsaMap: undefined,
66
69
  mafRegion: {
67
70
  refName: 'chr1',
@@ -90,6 +93,7 @@ describe('genomeToMSA', () => {
90
93
  });
91
94
  const model = {
92
95
  querySeqName: 'hg38.chr1',
96
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
93
97
  transcriptToMsaMap: undefined,
94
98
  mafRegion: {
95
99
  refName: 'chr1',
@@ -116,6 +120,7 @@ describe('genomeToMSA', () => {
116
120
  });
117
121
  const model = {
118
122
  querySeqName: 'hg38.chr1',
123
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
119
124
  transcriptToMsaMap: undefined,
120
125
  mafRegion: {
121
126
  refName: 'chr1',
@@ -142,6 +147,7 @@ describe('genomeToMSA', () => {
142
147
  });
143
148
  const model = {
144
149
  querySeqName: 'hg38.chr1',
150
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
145
151
  transcriptToMsaMap: undefined,
146
152
  mafRegion: {
147
153
  refName: 'chr1',
@@ -167,6 +173,7 @@ describe('genomeToMSA', () => {
167
173
  });
168
174
  const model = {
169
175
  querySeqName: 'hg38.chr1',
176
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
170
177
  transcriptToMsaMap: undefined,
171
178
  mafRegion: {
172
179
  refName: 'chr1',
@@ -195,6 +202,7 @@ describe('genomeToMSA', () => {
195
202
  const mockSeqPosToVisibleCol = vi.fn().mockReturnValue(10);
196
203
  const model = {
197
204
  querySeqName: 'QUERY',
205
+ rows: [['QUERY', 'MKVLTAEEK']],
198
206
  transcriptToMsaMap: {
199
207
  refName: 'chr1',
200
208
  // g2p is keyed by 0-based genome position, the hover coord is 1-based
@@ -220,6 +228,7 @@ describe('genomeToMSA', () => {
220
228
  const mockSeqPosToVisibleCol = vi.fn();
221
229
  const model = {
222
230
  querySeqName: 'QUERY',
231
+ rows: [['QUERY', 'MKVLTAEEK']],
223
232
  transcriptToMsaMap: {
224
233
  refName: 'chr1',
225
234
  g2p: { 1004: 10 },
@@ -240,6 +249,7 @@ describe('genomeToMSA', () => {
240
249
  });
241
250
  const model = {
242
251
  querySeqName: 'QUERY',
252
+ rows: [['QUERY', 'MKVLTAEEK']],
243
253
  transcriptToMsaMap: {
244
254
  refName: 'chr1',
245
255
  g2p: { 1000: 0 }, // No entry for 1004
@@ -261,6 +271,7 @@ describe('genomeToMSA', () => {
261
271
  });
262
272
  const model = {
263
273
  querySeqName: 'QUERY',
274
+ rows: [['QUERY', 'MKVLTAEEK']],
264
275
  transcriptToMsaMap: undefined,
265
276
  mafRegion: undefined,
266
277
  connectedView: { initialized: true },
@@ -269,4 +280,27 @@ describe('genomeToMSA', () => {
269
280
  const result = genomeToMSA({ model });
270
281
  expect(result).toBeUndefined();
271
282
  });
283
+ // seqPosToVisibleCol answers 0 for a row name it does not know, so without a
284
+ // guard an alignment whose query row is missing -- the default 'QUERY' on an
285
+ // uploaded file, or the empty name the manual panel leaves when it matches
286
+ // nothing -- lights column 0 on every genome hover
287
+ test('returns undefined when querySeqName names no row here', () => {
288
+ mockGetSession.mockReturnValue({
289
+ hovered: {
290
+ hoverFeature: {},
291
+ hoverPosition: { coord: 1005, refName: 'chr1' },
292
+ },
293
+ });
294
+ const seqPosToVisibleCol = vi.fn(() => 0);
295
+ const model = {
296
+ querySeqName: 'QUERY',
297
+ rows: [['some_other_row', 'MKVLTAEEK']],
298
+ transcriptToMsaMap: { refName: 'chr1', g2p: { 1004: 3 } },
299
+ mafRegion: undefined,
300
+ connectedView: { initialized: true },
301
+ seqPosToVisibleCol,
302
+ };
303
+ expect(genomeToMSA({ model })).toBeUndefined();
304
+ expect(seqPosToVisibleCol).not.toHaveBeenCalled();
305
+ });
272
306
  });
@@ -1,6 +1,6 @@
1
1
  export type { MSAFormat } from 'msa-parsers';
2
+ import type { BlastDatabase, MsaAlgorithm, PhmmerDatabase } from '../LaunchMsaView/components/BlastQuery/consts';
2
3
  import type { MafRegion, MsaViewInitState } from './types';
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';
@@ -11,12 +11,31 @@ export interface IRegion {
11
11
  start: number;
12
12
  end: number;
13
13
  }
14
- export interface BlastParams {
15
- blastDatabase: BlastDatabase;
16
- msaAlgorithm: MsaAlgorithm;
14
+ /**
15
+ * A search to run, discriminated by the program that runs it: the two arms
16
+ * differ in which databases they name and in whether an aligner runs at all, so
17
+ * splitting them is what lets doLaunchBlast read the database without asserting
18
+ * whose it is.
19
+ *
20
+ * The field is still `blastDatabase` rather than `database`: it is persisted in
21
+ * session snapshots and in the IndexedDB result cache, so renaming it would
22
+ * orphan every row already written.
23
+ */
24
+ export type BlastParams = {
17
25
  selectedTranscript?: Feature;
18
26
  proteinSequence: string;
19
- }
27
+ } & ({
28
+ /** absent on params written before phmmer existed, which were all blastp */
29
+ searchProgram?: 'blastp';
30
+ blastDatabase: BlastDatabase;
31
+ msaAlgorithm: MsaAlgorithm;
32
+ } | {
33
+ searchProgram: 'phmmer';
34
+ /** phmmer names its databases its own way: `swissprot`, not `uniprotkb_swissprot` */
35
+ blastDatabase: PhmmerDatabase;
36
+ /** phmmer aligns as it searches, so there is no aligner to choose */
37
+ msaAlgorithm?: undefined;
38
+ });
20
39
  /**
21
40
  * Where the ortholog set comes from. NCBI's sets cover vertebrates and
22
41
  * insects; PANTHER's span its 144 reference proteomes, human to yeast to
@@ -66,7 +85,7 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
66
85
  id: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<string>, [undefined]>;
67
86
  displayName: import("@jbrowse/mobx-state-tree").IMaybe<import("@jbrowse/mobx-state-tree").ISimpleType<string>>;
68
87
  minimized: import("@jbrowse/mobx-state-tree").IType<boolean | undefined, boolean, boolean>;
69
- }, "height" | "id" | "type" | "autoTreeAreaWidth" | "drawLabels" | "drawNodeBubbles" | "drawTree" | "labelsAlignRight" | "showBranchLen" | "treeAreaWidth" | "treeWidth" | "bgColor" | "colorSchemeName" | "msaFormat" | "showColumnStats" | "allowedGappyness" | "colWidth" | "collapsed" | "currentAlignment" | "data" | "drawMsaLetters" | "featureFilters" | "gffFilehandle" | "hideGaps" | "highlightColumns" | "msaFilehandle" | "relativeTo" | "rowHeight" | "scrollX" | "scrollY" | "scrollZoom" | "showDomainLegend" | "showDomains" | "showOnly" | "subFeatureRows" | "treeFilehandle" | "treeMetadataFilehandle" | "turnedOffTracks"> & Omit<Omit<Omit<{}, "autoTreeAreaWidth" | "drawLabels" | "drawNodeBubbles" | "drawTree" | "labelsAlignRight" | "showBranchLen" | "treeAreaWidth" | "treeWidth"> & {
88
+ }, "id" | "type" | "autoTreeAreaWidth" | "drawLabels" | "drawNodeBubbles" | "drawTree" | "labelsAlignRight" | "showBranchLen" | "treeAreaWidth" | "treeWidth" | "bgColor" | "colorSchemeName" | "msaFormat" | "showColumnStats" | "allowedGappyness" | "colWidth" | "collapsed" | "currentAlignment" | "data" | "drawMsaLetters" | "featureFilters" | "gffFilehandle" | "height" | "hideGaps" | "highlightColumns" | "msaFilehandle" | "relativeTo" | "rowHeight" | "scrollX" | "scrollY" | "scrollZoom" | "showDomainLegend" | "showDomains" | "showOnly" | "subFeatureRows" | "treeFilehandle" | "treeMetadataFilehandle" | "turnedOffTracks"> & Omit<Omit<Omit<{}, "autoTreeAreaWidth" | "drawLabels" | "drawNodeBubbles" | "drawTree" | "labelsAlignRight" | "showBranchLen" | "treeAreaWidth" | "treeWidth"> & {
70
89
  drawLabels: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
71
90
  labelsAlignRight: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
72
91
  treeAreaWidth: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<number>, [undefined]>;
@@ -80,7 +99,7 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
80
99
  colorSchemeName: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<string>, [undefined]>;
81
100
  showColumnStats: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
82
101
  msaFormat: import("@jbrowse/mobx-state-tree").IMaybe<import("@jbrowse/mobx-state-tree").ISimpleType<import("msa-parsers").MSAFormat>>;
83
- }, "height" | "id" | "type" | "allowedGappyness" | "colWidth" | "collapsed" | "currentAlignment" | "data" | "drawMsaLetters" | "featureFilters" | "gffFilehandle" | "hideGaps" | "highlightColumns" | "msaFilehandle" | "relativeTo" | "rowHeight" | "scrollX" | "scrollY" | "scrollZoom" | "showDomainLegend" | "showDomains" | "showOnly" | "subFeatureRows" | "treeFilehandle" | "treeMetadataFilehandle" | "turnedOffTracks"> & {
102
+ }, "id" | "type" | "allowedGappyness" | "colWidth" | "collapsed" | "currentAlignment" | "data" | "drawMsaLetters" | "featureFilters" | "gffFilehandle" | "height" | "hideGaps" | "highlightColumns" | "msaFilehandle" | "relativeTo" | "rowHeight" | "scrollX" | "scrollY" | "scrollZoom" | "showDomainLegend" | "showDomains" | "showOnly" | "subFeatureRows" | "treeFilehandle" | "treeMetadataFilehandle" | "turnedOffTracks"> & {
84
103
  id: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<string>, [undefined]>;
85
104
  showDomains: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
86
105
  showDomainLegend: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>, [undefined]>;
@@ -486,10 +505,7 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
486
505
  setTreeMetadata(treeMetadata?: string): void;
487
506
  setGFF(gff?: string): void;
488
507
  }, import("@jbrowse/mobx-state-tree")._NotCustomized, {
489
- tree: string | undefined;
490
- msa: string | undefined;
491
- treeMetadata: string | undefined;
492
- gff: string | undefined;
508
+ [k: string]: string | undefined;
493
509
  }>, [undefined]>;
494
510
  featureFilters: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").IMapType<import("@jbrowse/mobx-state-tree").ISimpleType<boolean>>, [undefined]>;
495
511
  relativeTo: import("@jbrowse/mobx-state-tree").IMaybe<import("@jbrowse/mobx-state-tree").ISimpleType<string>>;
@@ -559,6 +575,7 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
559
575
  highlightedColumns: number[] | undefined;
560
576
  minimapHeight: number;
561
577
  conservationTrackHeight: number;
578
+ sequenceLogoTrackHeight: number;
562
579
  marginLeft: number;
563
580
  error: unknown;
564
581
  annotations: import("msa-parsers").Annotation[];
@@ -655,6 +672,7 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
655
672
  }[];
656
673
  readonly colClustalX: Record<string, string>[];
657
674
  readonly conservation: number[];
675
+ readonly alphabetMaxBits: number;
658
676
  readonly propertyConservation: number[];
659
677
  readonly hierarchy: import("react-msaview").HierarchyNode<import("react-msaview").NodeWithIdsAndLength>;
660
678
  readonly totalHeight: number;
@@ -669,6 +687,7 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
669
687
  readonly dataInitialized: boolean;
670
688
  readonly blocksX: number[];
671
689
  readonly blocksY: number[];
690
+ readonly visibleMsaHeight: number;
672
691
  } & {
673
692
  readonly blocks2d: (readonly [number, number])[];
674
693
  readonly isLoading: boolean;
@@ -704,6 +723,7 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
704
723
  }): void;
705
724
  } & {
706
725
  readonly labelWidthMap: Map<string, number>;
726
+ readonly labelWidthScale: number;
707
727
  readonly labelsWidth: number;
708
728
  readonly secondaryStructureConsensus: string | undefined;
709
729
  readonly seqConsensus: string | undefined;
@@ -770,6 +790,7 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
770
790
  } & {
771
791
  setHeaderHeight(arg: number): void;
772
792
  setConservationTrackHeight(arg: number): void;
793
+ setSequenceLogoTrackHeight(arg: number): void;
773
794
  reset(): void;
774
795
  exportSVG(opts: {
775
796
  theme: import("@mui/material").Theme;
@@ -18,6 +18,7 @@ function makeModel({ highlightColumns } = {}) {
18
18
  const calls = [];
19
19
  const model = {
20
20
  querySeqName: 'query',
21
+ rows: [['query', 'MKVLTAEEK']],
21
22
  connectedViewId: CONNECTED,
22
23
  // g2p is indexed by genome coord; identity keeps the arithmetic out of the way
23
24
  transcriptToMsaMap: {
@@ -195,6 +196,16 @@ describe('scope and redundant writes', () => {
195
196
  run();
196
197
  expect(calls).toEqual([]);
197
198
  });
199
+ // seqPosToGlobalCol answers 0 for a row name it does not know, so without a
200
+ // guard a structure hover would light column 0 of whatever row is first
201
+ test('a query row this alignment does not have contributes no column', () => {
202
+ const { model, calls } = makeModel();
203
+ Object.assign(model, { rows: [['some_other_row', 'MKVLTAEEK']] });
204
+ const run = observeProteinHighlights(model);
205
+ session({ hover: [{ start: 10, end: 12 }] });
206
+ run();
207
+ expect(calls).toEqual([]);
208
+ });
198
209
  test('nothing happens until the view is connected and mapped', () => {
199
210
  const { calls } = makeModel();
200
211
  const bare = {
@@ -21,6 +21,7 @@ function makeModel() {
21
21
  const calls = [];
22
22
  const model = {
23
23
  querySeqName: 'hg38.chr1',
24
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
24
25
  transcriptToMsaMap: undefined,
25
26
  mafRegion,
26
27
  connectedView: { initialized: true, assemblyNames: ['hg38'] },