jbrowse-plugin-msaview 3.2.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 (106) 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 +9 -4
  16. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +9 -0
  17. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +20 -0
  18. package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +1 -1
  19. package/dist/LaunchMsaView/components/useFeatureSequence.js +1 -1
  20. package/dist/LaunchMsaView/detectQueryRow.d.ts +15 -2
  21. package/dist/LaunchMsaView/detectQueryRow.js +20 -21
  22. package/dist/LaunchMsaView/detectQueryRow.test.js +15 -15
  23. package/dist/LaunchMsaView/useQueryRowName.js +5 -8
  24. package/dist/MsaViewPanel/afterCreateAutoruns.js +2 -2
  25. package/dist/MsaViewPanel/components/ErrorBoundary.d.ts +2 -2
  26. package/dist/MsaViewPanel/components/JobLink.js +7 -1
  27. package/dist/MsaViewPanel/components/LaunchProgress.d.ts +17 -0
  28. package/dist/MsaViewPanel/components/LaunchProgress.js +41 -0
  29. package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -3
  30. package/dist/MsaViewPanel/components/MsaViewPanel.test.d.ts +1 -0
  31. package/dist/MsaViewPanel/components/MsaViewPanel.test.js +68 -0
  32. package/dist/MsaViewPanel/doLaunchBlast.d.ts +1 -1
  33. package/dist/MsaViewPanel/doLaunchBlast.js +84 -52
  34. package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +7 -5
  35. package/dist/MsaViewPanel/doLaunchOrthologs.js +64 -30
  36. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +106 -1
  37. package/dist/MsaViewPanel/genomeToMSA.js +4 -2
  38. package/dist/MsaViewPanel/genomeToMSA.test.js +34 -0
  39. package/dist/MsaViewPanel/model.d.ts +41 -11
  40. package/dist/MsaViewPanel/model.js +6 -0
  41. package/dist/MsaViewPanel/observeProteinHighlights.test.js +11 -0
  42. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +1 -0
  43. package/dist/MsaViewPanel/util.d.ts +18 -0
  44. package/dist/MsaViewPanel/util.js +17 -0
  45. package/dist/jbrowse-plugin-msaview.umd.production.min.js +47 -35
  46. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  47. package/dist/utils/blastCache.d.ts +10 -6
  48. package/dist/utils/blastCache.js +15 -3
  49. package/dist/utils/ebiBlast.d.ts +1 -1
  50. package/dist/utils/msa.d.ts +12 -0
  51. package/dist/utils/msa.js +35 -12
  52. package/dist/utils/msaRows.d.ts +31 -0
  53. package/dist/utils/msaRows.js +67 -0
  54. package/dist/utils/pantherOrthologs.d.ts +79 -0
  55. package/dist/utils/pantherOrthologs.js +262 -0
  56. package/dist/utils/phmmer.d.ts +53 -0
  57. package/dist/utils/phmmer.js +118 -0
  58. package/dist/utils/taxonomyNames.d.ts +1 -1
  59. package/dist/utils/taxonomyNames.js +6 -1
  60. package/dist/version.d.ts +1 -1
  61. package/dist/version.js +1 -1
  62. package/package.json +27 -21
  63. package/src/AddHighlightModel/GenomeMouseoverHighlight.tsx +1 -1
  64. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +1 -1
  65. package/src/AddHighlightModel/index.tsx +1 -1
  66. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +88 -30
  67. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -1
  68. package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +4 -4
  69. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.ts +50 -0
  70. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +23 -3
  71. package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +1 -1
  72. package/src/LaunchMsaView/components/BlastQuery/consts.ts +40 -0
  73. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -1
  74. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +21 -5
  75. package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +52 -0
  76. package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +1 -1
  77. package/src/LaunchMsaView/components/useFeatureSequence.ts +1 -1
  78. package/src/LaunchMsaView/detectQueryRow.test.ts +17 -15
  79. package/src/LaunchMsaView/detectQueryRow.ts +34 -23
  80. package/src/LaunchMsaView/useQueryRowName.ts +6 -9
  81. package/src/MsaViewPanel/afterCreateAutoruns.ts +2 -2
  82. package/src/MsaViewPanel/components/ErrorBoundary.tsx +2 -1
  83. package/src/MsaViewPanel/components/JobLink.tsx +7 -2
  84. package/src/MsaViewPanel/components/LaunchProgress.tsx +62 -0
  85. package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +83 -0
  86. package/src/MsaViewPanel/components/MsaViewPanel.tsx +7 -4
  87. package/src/MsaViewPanel/doLaunchBlast.ts +127 -69
  88. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +119 -2
  89. package/src/MsaViewPanel/doLaunchOrthologs.ts +100 -38
  90. package/src/MsaViewPanel/genomeToMSA.test.ts +37 -0
  91. package/src/MsaViewPanel/genomeToMSA.ts +6 -2
  92. package/src/MsaViewPanel/model.ts +38 -5
  93. package/src/MsaViewPanel/observeProteinHighlights.test.ts +13 -0
  94. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +1 -0
  95. package/src/MsaViewPanel/util.ts +18 -0
  96. package/src/utils/blastCache.ts +33 -12
  97. package/src/utils/ebiBlast.ts +1 -1
  98. package/src/utils/msa.ts +43 -12
  99. package/src/utils/msaRows.ts +95 -0
  100. package/src/utils/pantherOrthologs.ts +399 -0
  101. package/src/utils/phmmer.ts +174 -0
  102. package/src/utils/taxonomyNames.ts +6 -1
  103. package/src/version.ts +1 -1
  104. package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +0 -6
  105. package/dist/MsaViewPanel/components/LoadingBLAST.js +0 -26
  106. package/src/MsaViewPanel/components/LoadingBLAST.tsx +0 -48
@@ -1,5 +1,5 @@
1
1
  import { useMemo, useState } from 'react';
2
- import { detectQueryRow, getMsaRowNames } from './detectQueryRow';
2
+ import { findQueryRow } from './detectQueryRow';
3
3
  /**
4
4
  * The MSA row name to launch with, found by sequence rather than typed.
5
5
  *
@@ -12,15 +12,12 @@ export function useQueryRowName(msaText, proteinSequence) {
12
12
  const [override, setOverride] = useState();
13
13
  // parsing runs on every keystroke in the paste box otherwise, and an
14
14
  // alignment of a few hundred rows is not free
15
- const { detected, names } = useMemo(() => ({
16
- detected: detectQueryRow(msaText, proteinSequence),
17
- names: getMsaRowNames(msaText),
18
- }), [msaText, proteinSequence]);
15
+ const { names, match } = useMemo(() => findQueryRow(msaText, proteinSequence), [msaText, proteinSequence]);
19
16
  return {
20
- detected,
17
+ detected: match,
21
18
  names,
22
- querySeqName: override ?? detected?.name ?? '',
19
+ querySeqName: override ?? match?.name ?? '',
23
20
  setQuerySeqName: setOverride,
24
- isAutoDetected: override === undefined && !!detected,
21
+ isAutoDetected: override === undefined && !!match,
25
22
  };
26
23
  }
@@ -6,7 +6,7 @@ import { genomeToMSA } from './genomeToMSA';
6
6
  import { loadProteinDomains } from './loadProteinDomains';
7
7
  import { cleanupOldData, generateDataStoreId, retrieveMsaData, storeMsaData, } from './msaDataStore';
8
8
  import { getProteinViews } from './structureConnection';
9
- import { getUniprotIdFromAlphaFoldUrl } from './util';
9
+ import { getUniprotIdFromAlphaFoldUrl, hasQueryRow } from './util';
10
10
  export function loadStoredData(self) {
11
11
  const { dataStoreId, rows } = self;
12
12
  if (dataStoreId && rows.length === 0) {
@@ -219,7 +219,7 @@ export function syncGenomeHoverToMsaColumn(self) {
219
219
  */
220
220
  function genomeHighlightsToVisibleColumns(self, field) {
221
221
  const { connectedViewId, transcriptToMsaMap, querySeqName } = self;
222
- if (!transcriptToMsaMap) {
222
+ if (!transcriptToMsaMap || !hasQueryRow(self)) {
223
223
  return [];
224
224
  }
225
225
  const { g2p } = transcriptToMsaMap;
@@ -1,5 +1,5 @@
1
- import type { ReactNode } from 'react';
2
1
  import React, { Component } from 'react';
2
+ import type { ReactNode } from 'react';
3
3
  interface Props {
4
4
  children: ReactNode;
5
5
  }
@@ -14,6 +14,6 @@ export declare class ErrorBoundary extends Component<Props, State> {
14
14
  error: unknown;
15
15
  };
16
16
  componentDidCatch(error: unknown, info: React.ErrorInfo): void;
17
- render(): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
17
+ render(): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
18
18
  }
19
19
  export {};
@@ -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
- }
@@ -3,12 +3,14 @@ import type { JBrowsePluginMsaViewModel } from './model';
3
3
  * The no-search-job alternative to doLaunchBlast.
4
4
  *
5
5
  * BLAST spends 10+ minutes answering "what looks like this sequence" and
6
- * returns a redundant, accession-labelled hit list. This asks NCBI the question
7
- * the alignment actually wants — "what is this gene's ortholog in each species"
8
- * — which NCBI has already computed, so the whole NCBI half returns in about a
9
- * second and only the EBI alignment (~10s) costs real time.
6
+ * returns a redundant, accession-labelled hit list. This asks the question the
7
+ * alignment actually wants — "what is this gene's ortholog in each species" —
8
+ * which NCBI and PANTHER have already computed, so the lookup returns in
9
+ * seconds and only the EBI alignment (~10s) costs real time. `source` picks
10
+ * which of the two answers: NCBI for vertebrates and insects, PANTHER for
11
+ * everything else (yeast, worm, plants, and a fly gene's vertebrate relatives).
10
12
  *
11
- * The query row is the user's OWN selected transcript, not NCBI's
13
+ * The query row is the user's OWN selected transcript, not the source's
12
14
  * representative protein for the query species, because `connectedFeature`
13
15
  * maps genome coordinates through that row — swapping in a different isoform
14
16
  * would silently break the genome<->MSA linkage. The query species is therefore
@@ -1,57 +1,55 @@
1
1
  import { cleanProteinSequence } from '../LaunchMsaView/util';
2
2
  import { launchMSA } from '../utils/msa';
3
3
  import { dedupeLabels, fetchOrthologRows, fetchProteinForGene, resolveGeneId, } from '../utils/ncbiOrthologs';
4
+ import { fetchPantherOrthologs } from '../utils/pantherOrthologs';
4
5
  import { fetchTaxonomyInfo } from '../utils/taxonomyNames';
5
6
  /**
6
7
  * The no-search-job alternative to doLaunchBlast.
7
8
  *
8
9
  * BLAST spends 10+ minutes answering "what looks like this sequence" and
9
- * returns a redundant, accession-labelled hit list. This asks NCBI the question
10
- * the alignment actually wants — "what is this gene's ortholog in each species"
11
- * — which NCBI has already computed, so the whole NCBI half returns in about a
12
- * second and only the EBI alignment (~10s) costs real time.
10
+ * returns a redundant, accession-labelled hit list. This asks the question the
11
+ * alignment actually wants — "what is this gene's ortholog in each species" —
12
+ * which NCBI and PANTHER have already computed, so the lookup returns in
13
+ * seconds and only the EBI alignment (~10s) costs real time. `source` picks
14
+ * which of the two answers: NCBI for vertebrates and insects, PANTHER for
15
+ * everything else (yeast, worm, plants, and a fly gene's vertebrate relatives).
13
16
  *
14
- * The query row is the user's OWN selected transcript, not NCBI's
17
+ * The query row is the user's OWN selected transcript, not the source's
15
18
  * representative protein for the query species, because `connectedFeature`
16
19
  * maps genome coordinates through that row — swapping in a different isoform
17
20
  * would silently break the genome<->MSA linkage. The query species is therefore
18
21
  * excluded from the ortholog set rather than appearing twice.
19
22
  */
20
23
  export async function doLaunchOrthologs({ self, }) {
21
- const { taxId, taxa, maxSpecies, geneCandidates, msaAlgorithm, proteinSequence, } = self.orthologParams;
24
+ const { taxId, taxa, maxSpecies, geneCandidates, msaAlgorithm, proteinSequence, source = 'ncbi', } = self.orthologParams;
22
25
  const onProgress = (arg) => {
23
26
  self.setProgress(arg);
24
27
  };
25
- onProgress('Resolving gene at NCBI...');
26
- const resolved = await resolveGeneId(geneCandidates, taxId);
27
- if (!resolved) {
28
- 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
+ const request = {
29
+ taxId,
30
+ geneCandidates,
31
+ taxa: taxa ? new Set(taxa) : undefined,
32
+ // the query species is represented by the query row below
33
+ exclude: taxId,
34
+ limit: maxSpecies,
35
+ onProgress,
36
+ };
37
+ const { geneId, representative, rows } = source === 'panther'
38
+ ? await findPantherOrthologs(request)
39
+ : await findNcbiOrthologs(request);
30
40
  // The query row. The dialog always supplies it — it is the user's OWN
31
41
  // selected transcript, which is what makes `connectedFeature` map genome
32
42
  // coordinates through this row. A launch that has no transcript to translate
33
- // (a session spec naming only a gene) falls back to NCBI's representative
34
- // protein for the resolved gene, which is the same choice made for every
35
- // other row, so the alignment is the one NCBI would build for that gene.
36
- const representative = await fetchRepresentativeQueryProtein(resolved.geneId);
43
+ // (a session spec naming only a gene) falls back to the source's
44
+ // representative protein for the resolved gene, which is the same choice
45
+ // made for every other row, so the alignment is the one the source would
46
+ // build for that gene.
37
47
  const cleanedSeq = proteinSequence
38
48
  ? cleanProteinSequence(proteinSequence)
39
49
  : representative?.sequence;
40
50
  if (!cleanedSeq) {
41
- throw new Error(`No query protein: none was supplied and NCBI returned no representative protein for gene ${resolved.geneId}.`);
51
+ throw new Error(`No query protein: none was supplied and ${source === 'panther' ? 'PANTHER' : 'NCBI'} returned no representative protein for gene ${geneId}.`);
42
52
  }
43
- // Every species NCBI has an ortholog for, when a launch names none, capped at
44
- // maxSpecies. A launch that wants specific species lists them; one that just
45
- // wants "this gene across species" gets NCBI's own order, which leads with the
46
- // reference organisms.
47
- const rows = await fetchOrthologRows({
48
- geneId: resolved.geneId,
49
- taxa: taxa ? new Set(taxa) : undefined,
50
- // the query species is represented by the query row above
51
- exclude: taxId,
52
- limit: maxSpecies,
53
- onProgress,
54
- });
55
53
  // The query row is named for its species like every other row, with a suffix
56
54
  // marking it as the one the genome view is linked to. A bare `QUERY` among
57
55
  // ninety-nine named species reads as a row whose species failed to resolve,
@@ -66,7 +64,7 @@ export async function doLaunchOrthologs({ self, }) {
66
64
  const queryLabel = await queryRowLabel(taxId, rows);
67
65
  self.setQuerySeqName(queryLabel);
68
66
  const treeMetadata = {
69
- [queryLabel]: buildQueryMetadata(self, resolved.geneId, cleanedSeq, representative),
67
+ [queryLabel]: buildQueryMetadata(self, geneId, cleanedSeq, representative),
70
68
  };
71
69
  for (const row of rows) {
72
70
  treeMetadata[row.label] = buildRowMetadata(row);
@@ -84,6 +82,42 @@ export async function doLaunchOrthologs({ self, }) {
84
82
  treeMetadata: JSON.stringify(treeMetadata),
85
83
  };
86
84
  }
85
+ /**
86
+ * Every species NCBI has an ortholog for, when a launch names none, capped at
87
+ * `limit`. A launch that wants specific species lists them; one that just
88
+ * wants "this gene across species" gets NCBI's own order, which leads with the
89
+ * reference organisms.
90
+ */
91
+ async function findNcbiOrthologs({ taxId, geneCandidates, onProgress, ...rest }) {
92
+ onProgress('Resolving gene at NCBI...');
93
+ const resolved = await resolveGeneId(geneCandidates, taxId);
94
+ if (!resolved) {
95
+ 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.`);
96
+ }
97
+ const representative = await fetchRepresentativeQueryProtein(resolved.geneId);
98
+ const rows = await fetchOrthologRows({
99
+ geneId: resolved.geneId,
100
+ onProgress,
101
+ ...rest,
102
+ });
103
+ return { geneId: resolved.geneId, representative, rows };
104
+ }
105
+ /**
106
+ * One `matchortho` call resolves the gene, names its own UniProt entry and
107
+ * lists an ortholog per genome, so the representative protein needs no second
108
+ * lookup here.
109
+ */
110
+ async function findPantherOrthologs({ geneCandidates, ...rest }) {
111
+ const found = await fetchPantherOrthologs({
112
+ candidates: geneCandidates,
113
+ ...rest,
114
+ });
115
+ return {
116
+ geneId: found.query?.geneRef ?? found.matched,
117
+ representative: found.query,
118
+ rows: found.rows,
119
+ };
120
+ }
87
121
  /**
88
122
  * `<species>_query`, unique against the ortholog labels. Falls back to the bare
89
123
  * marker when NCBI cannot name the taxon, which is a naming failure and must not
@@ -120,7 +154,7 @@ async function fetchRepresentativeQueryProtein(geneId) {
120
154
  /**
121
155
  * The query row carries an Accession — which is what drives the automatic CDD
122
156
  * overlay (afterCreateAutoruns.autoLoadProteinDomains -> loadProteinDomains) —
123
- * ONLY when its sequence is byte-identical to the RefSeq protein that accession
157
+ * ONLY when its sequence is byte-identical to the protein that accession
124
158
  * names. Attaching it unconditionally would put every domain box at an offset
125
159
  * whenever the user picked a non-representative isoform, which is a silently
126
160
  * wrong figure rather than a missing one. A launch that took the representative