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,8 +1,8 @@
1
1
  import React from 'react';
2
2
  import { getSession } from '@jbrowse/core/util';
3
3
  import { observer } from 'mobx-react';
4
- import { hasHoverPosition, useStyles } from './util';
5
4
  import { isMsaView } from '../MsaViewPanel/model';
5
+ import { hasHoverPosition, useStyles } from './util';
6
6
  const GenomeMouseoverHighlight = observer(function ({ model, }) {
7
7
  const { hovered, views } = getSession(model);
8
8
  const hasMsaView = views.some(s => isMsaView(s) && s.connectedViewId === model.id);
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
2
  import { getSession } from '@jbrowse/core/util';
3
3
  import { observer } from 'mobx-react';
4
- import { hasHoverPosition, useStyles } from './util';
5
4
  import { isMsaView } from '../MsaViewPanel/model';
5
+ import { hasHoverPosition, useStyles } from './util';
6
6
  const MsaToGenomeHighlight = observer(function MsaToGenomeHighlight2({ model, }) {
7
7
  const { views, hovered } = getSession(model);
8
8
  const msaView = views
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { getSession } from '@jbrowse/core/util';
3
- import HighlightComponents from './HighlightComponents';
4
3
  import { isMsaView } from '../MsaViewPanel/model';
4
+ import HighlightComponents from './HighlightComponents';
5
5
  export default function AddHighlightComponentsModelF(pluginManager) {
6
6
  pluginManager.addToExtensionPoint('LinearGenomeView-TracksContainerComponent',
7
7
  // @ts-expect-error
@@ -3,21 +3,27 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
3
3
  import { Accordion, AccordionDetails, AccordionSummary, MenuItem, Typography, } from '@mui/material';
4
4
  import { observer } from 'mobx-react';
5
5
  import { makeStyles } from 'tss-react/mui';
6
- import CachedBlastResults from './CachedBlastResults';
7
- import MsaAlgorithmSelect from './MsaAlgorithmSelect';
8
- import { blastLaunchView } from './blastLaunchView';
9
- import { blastDatabaseOptions, defaultBlastDatabase } from './consts';
10
- import { useCachedBlastResults } from './useCachedBlastResults';
11
6
  import TextField2 from '../../../components/TextField2';
12
7
  import { getBlastViewTitle, getGeneIdentifiers, getLinearGenomeView, } from '../../util';
13
8
  import LaunchPanelContent from '../LaunchPanelContent';
14
9
  import SubmitCancelActions from '../SubmitCancelActions';
15
10
  import TranscriptSelector from '../TranscriptSelector';
16
11
  import { useTranscriptSelection } from '../useTranscriptSelection';
12
+ import CachedBlastResults from './CachedBlastResults';
13
+ import MsaAlgorithmSelect from './MsaAlgorithmSelect';
14
+ import { blastLaunchView } from './blastLaunchView';
15
+ import { databaseOptionsFor, defaultBlastDatabase, defaultSearchFor, searchPrograms, } from './consts';
16
+ import { useCachedBlastResults } from './useCachedBlastResults';
17
17
  const useStyles = makeStyles()({
18
18
  selectField: {
19
19
  width: 150,
20
20
  },
21
+ // wider than the rest because the values are what the user came to read, and
22
+ // `uniprotkb_swissprot` is 19 characters — at 150 the field showed
23
+ // `uniprotkb_swis…`, which does not distinguish it from `uniprotkb_trembl`
24
+ databaseField: {
25
+ width: 230,
26
+ },
21
27
  cachedResultsAccordion: {
22
28
  marginTop: 20,
23
29
  },
@@ -29,8 +35,15 @@ const BlastAutomaticPanel = observer(function ({ handleClose, feature, model, ch
29
35
  const { classes } = useStyles();
30
36
  const view = getLinearGenomeView(model);
31
37
  const [launchViewError, setLaunchViewError] = useState();
32
- const [selectedBlastDatabase, setSelectedBlastDatabase] = useState(defaultBlastDatabase);
38
+ // one piece of state, not two: a program and a database that program does not
39
+ // have is a 400 from EBI minutes after Submit, and holding them apart is what
40
+ // would let them drift into that
41
+ const [search, setSearch] = useState({
42
+ program: 'blastp',
43
+ database: defaultBlastDatabase,
44
+ });
33
45
  const [selectedMsaAlgorithm, setSelectedMsaAlgorithm] = useState('clustalo');
46
+ const isPhmmer = search.program === 'phmmer';
34
47
  const geneIds = useMemo(() => getGeneIdentifiers(feature), [feature]);
35
48
  const { results: cachedResults, error: cachedResultsError } = useCachedBlastResults(geneIds);
36
49
  const transcriptSelection = useTranscriptSelection({ feature, view });
@@ -39,12 +52,35 @@ const BlastAutomaticPanel = observer(function ({ handleClose, feature, model, ch
39
52
  return (React.createElement(React.Fragment, null,
40
53
  React.createElement(LaunchPanelContent, { error: e },
41
54
  children,
42
- React.createElement(TextField2, { variant: "outlined", label: "BLAST database", className: classes.selectField, select: true, value: selectedBlastDatabase, onChange: event => {
43
- setSelectedBlastDatabase(event.target.value);
44
- } }, blastDatabaseOptions.map(val => (React.createElement(MenuItem, { value: val, key: val }, val)))),
45
- React.createElement(MsaAlgorithmSelect, { className: classes.selectField, value: selectedMsaAlgorithm, onChange: setSelectedMsaAlgorithm }),
55
+ React.createElement(TextField2, { variant: "outlined", label: "Search program", className: classes.selectField, select: true, value: search.program, onChange: event => {
56
+ // the two services name their databases differently, so switching
57
+ // program replaces the database rather than keeping a name the new
58
+ // one has never heard of
59
+ setSearch(defaultSearchFor(event.target.value));
60
+ } }, searchPrograms.map(val => (React.createElement(MenuItem, { value: val, key: val }, val)))),
61
+ React.createElement(TextField2, { variant: "outlined", label: "Database", className: classes.databaseField, select: true, value: search.database, onChange: event => {
62
+ setSearch({
63
+ program: search.program,
64
+ database: event.target.value,
65
+ });
66
+ } }, databaseOptionsFor(search.program).map(val => (React.createElement(MenuItem, { value: val, key: val }, val)))),
67
+ isPhmmer ? null : (React.createElement(MsaAlgorithmSelect, { className: classes.selectField, value: selectedMsaAlgorithm, onChange: setSelectedMsaAlgorithm })),
46
68
  React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection }),
47
- React.createElement(Typography, { className: classes.infoText }, "This panel will automatically submit a blastp query to EBI, which searches UniProtKB. Searches usually finish in under a minute, and swissprot returns curated sequences that align more cleanly than the many near-identical entries a TrEMBL search brings back. After completion, all the hits will be run through a multiple sequence alignment. Searching NCBI's nr needs the manual approach: NCBI no longer lets a browser read responses from Blast.cgi."),
69
+ React.createElement(Typography, { className: classes.infoText },
70
+ isPhmmer
71
+ ? `phmmer searches UniProtKB with a profile HMM built from the query,
72
+ so it aligns the hits as it finds them and that alignment is used
73
+ directly — nothing is realigned afterwards. The tree is then built
74
+ from it by neighbour-joining. A hit matching the query in more
75
+ than one place appears once per matched region.`
76
+ : `This panel will automatically submit a blastp query to EBI, which
77
+ searches UniProtKB. Searches usually finish in under a minute, and
78
+ swissprot returns curated sequences that align more cleanly than
79
+ the many near-identical entries a TrEMBL search brings back. After
80
+ completion, all the hits will be run through a multiple sequence
81
+ alignment.`,
82
+ ' ',
83
+ "Searching NCBI's nr needs the manual approach: NCBI no longer lets a browser read responses from Blast.cgi."),
48
84
  cachedResults.length > 0 ? (React.createElement(Accordion, { className: classes.cachedResultsAccordion },
49
85
  React.createElement(AccordionSummary, { expandIcon: React.createElement(ExpandMoreIcon, null) },
50
86
  React.createElement(Typography, null, "Previous BLAST Results")),
@@ -58,12 +94,20 @@ const BlastAutomaticPanel = observer(function ({ handleClose, feature, model, ch
58
94
  feature: selectedTranscript,
59
95
  view,
60
96
  newViewTitle: getBlastViewTitle(feature, selectedTranscript),
61
- blastParams: {
62
- blastDatabase: selectedBlastDatabase,
63
- msaAlgorithm: selectedMsaAlgorithm,
64
- selectedTranscript,
65
- proteinSequence,
66
- },
97
+ blastParams: search.program === 'phmmer'
98
+ ? {
99
+ searchProgram: 'phmmer',
100
+ blastDatabase: search.database,
101
+ selectedTranscript,
102
+ proteinSequence,
103
+ }
104
+ : {
105
+ searchProgram: 'blastp',
106
+ blastDatabase: search.database,
107
+ msaAlgorithm: selectedMsaAlgorithm,
108
+ selectedTranscript,
109
+ proteinSequence,
110
+ },
67
111
  });
68
112
  handleClose();
69
113
  }
@@ -3,7 +3,6 @@ import { shorten2 } from '@jbrowse/core/util';
3
3
  import { Alert, Typography } from '@mui/material';
4
4
  import { observer } from 'mobx-react';
5
5
  import { makeStyles } from 'tss-react/mui';
6
- import { BASE_BLAST_URL } from './consts';
7
6
  import ExternalLink from '../../../components/ExternalLink';
8
7
  import TextField2 from '../../../components/TextField2';
9
8
  import { useQueryRowName } from '../../useQueryRowName';
@@ -14,6 +13,7 @@ import QueryRowSelector from '../QueryRowSelector';
14
13
  import SubmitCancelActions from '../SubmitCancelActions';
15
14
  import TranscriptSelector from '../TranscriptSelector';
16
15
  import { useTranscriptSelection } from '../useTranscriptSelection';
16
+ import { BASE_BLAST_URL } from './consts';
17
17
  const useStyles = makeStyles()({
18
18
  ncbiLink: {
19
19
  wordBreak: 'break-all',
@@ -2,12 +2,12 @@ import React, { useState } from 'react';
2
2
  import SettingsIcon from '@mui/icons-material/Settings';
3
3
  import { IconButton } from '@mui/material';
4
4
  import { makeStyles } from 'tss-react/mui';
5
+ import { DEFAULT_EBI_EMAIL, EBI_EMAIL_STORAGE_KEY, } from '../../../utils/ebiJobDispatcher';
6
+ import { useLocalStorage } from '../../../utils/useLocalStorage';
5
7
  import BlastAutomaticPanel from './BlastAutomaticPanel';
6
8
  import BlastManualPanel from './BlastManualPanel';
7
9
  import BlastMethodSelector from './BlastMethodSelector';
8
10
  import BlastSettingsDialog from './BlastSettingsDialog';
9
- import { DEFAULT_EBI_EMAIL, EBI_EMAIL_STORAGE_KEY, } from '../../../utils/ebiJobDispatcher';
10
- import { useLocalStorage } from '../../../utils/useLocalStorage';
11
11
  const useStyles = makeStyles()({
12
12
  settingsButton: {
13
13
  float: 'right',
@@ -1,5 +1,17 @@
1
1
  import React from 'react';
2
+ import type { CachedBlastResult } from '../../../utils/blastCache';
2
3
  import type { AbstractTrackModel, Feature } from '@jbrowse/core/util';
4
+ /**
5
+ * How the row was produced: `uniprotkb_swissprot / blastp / clustalo`, or
6
+ * `swissprot / phmmer` for a row phmmer aligned as it searched and that
7
+ * therefore ran no aligner. Each part is dropped when absent rather than
8
+ * printed empty — `msaAlgorithm` became optional when phmmer arrived, and a
9
+ * phmmer row read `(undefined)` until this stopped assuming one.
10
+ *
11
+ * `blastProgram` is the older field, written only while the plugin still
12
+ * queried NCBI directly and blastp/quick-blastp was a real choice.
13
+ */
14
+ export declare function describeSearch(result: CachedBlastResult): string;
3
15
  declare const CachedBlastResults: ({ model, handleClose, feature, }: {
4
16
  model: AbstractTrackModel;
5
17
  handleClose: () => void;
@@ -4,9 +4,9 @@ import DeleteIcon from '@mui/icons-material/Delete';
4
4
  import { Button, IconButton, List, ListItem, ListItemButton, ListItemText, Typography, } from '@mui/material';
5
5
  import { observer } from 'mobx-react';
6
6
  import { makeStyles } from 'tss-react/mui';
7
+ import { featureMatchesId, getGeneIdentifiers, getLinearGenomeView, getSortedTranscriptFeatures, } from '../../util';
7
8
  import { blastLaunchViewFromCache } from './blastLaunchView';
8
9
  import { useCachedBlastResults } from './useCachedBlastResults';
9
- import { featureMatchesId, getGeneIdentifiers, getLinearGenomeView, getSortedTranscriptFeatures, } from '../../util';
10
10
  const useStyles = makeStyles()({
11
11
  header: {
12
12
  display: 'flex',
@@ -30,6 +30,25 @@ function getResultDisplayName(result) {
30
30
  ? parts.join(' - ')
31
31
  : (result.geneId ?? result.transcriptId ?? 'Unknown');
32
32
  }
33
+ /**
34
+ * How the row was produced: `uniprotkb_swissprot / blastp / clustalo`, or
35
+ * `swissprot / phmmer` for a row phmmer aligned as it searched and that
36
+ * therefore ran no aligner. Each part is dropped when absent rather than
37
+ * printed empty — `msaAlgorithm` became optional when phmmer arrived, and a
38
+ * phmmer row read `(undefined)` until this stopped assuming one.
39
+ *
40
+ * `blastProgram` is the older field, written only while the plugin still
41
+ * queried NCBI directly and blastp/quick-blastp was a real choice.
42
+ */
43
+ export function describeSearch(result) {
44
+ return [
45
+ result.blastDatabase,
46
+ result.searchProgram ?? result.blastProgram ?? 'blastp',
47
+ result.msaAlgorithm,
48
+ ]
49
+ .filter(Boolean)
50
+ .join(' / ');
51
+ }
33
52
  const CachedBlastResults = observer(function ({ model, handleClose, feature, }) {
34
53
  const { classes } = useStyles();
35
54
  const view = getLinearGenomeView(model);
@@ -83,6 +102,6 @@ const CachedBlastResults = observer(function ({ model, handleClose, feature, })
83
102
  React.createElement(ListItemButton, { onClick: () => {
84
103
  handleUseCached(result);
85
104
  } },
86
- React.createElement(ListItemText, { primary: `${getResultDisplayName(result)} - ${result.blastDatabase}${result.blastProgram ? `/${result.blastProgram}` : ''} (${result.msaAlgorithm})`, secondary: `${new Date(result.timestamp).toLocaleString()} - Seq: ${result.proteinSequence.slice(0, 30)}...` }))))))));
105
+ React.createElement(ListItemText, { primary: `${getResultDisplayName(result)} - ${describeSearch(result)}`, secondary: `${new Date(result.timestamp).toLocaleString()} - Seq: ${result.proteinSequence.slice(0, 30)}...` }))))))));
87
106
  });
88
107
  export default CachedBlastResults;
@@ -0,0 +1,29 @@
1
+ import { expect, test } from 'vitest';
2
+ import { describeSearch } from './CachedBlastResults';
3
+ const base = {
4
+ id: 'k',
5
+ proteinSequence: 'MKV',
6
+ blastDatabase: 'uniprotkb_swissprot',
7
+ msa: '',
8
+ tree: '',
9
+ treeMetadata: '{}',
10
+ rid: 'r',
11
+ timestamp: 0,
12
+ };
13
+ function row(fields) {
14
+ return { ...base, ...fields };
15
+ }
16
+ test('a blastp row names its database, program and aligner', () => {
17
+ expect(describeSearch(row({ searchProgram: 'blastp', msaAlgorithm: 'muscle' }))).toBe('uniprotkb_swissprot / blastp / muscle');
18
+ });
19
+ // phmmer aligns as it searches, so its rows carry no msaAlgorithm at all --
20
+ // which read as "(undefined)" while this assumed one
21
+ test('a phmmer row names no aligner, because none ran', () => {
22
+ expect(describeSearch(row({ blastDatabase: 'swissprot', searchProgram: 'phmmer' }))).toBe('swissprot / phmmer');
23
+ });
24
+ test('a row cached before searchProgram existed reads as blastp', () => {
25
+ expect(describeSearch(row({ msaAlgorithm: 'clustalo' }))).toBe('uniprotkb_swissprot / blastp / clustalo');
26
+ });
27
+ test('a row from the NCBI era keeps the program it recorded', () => {
28
+ expect(describeSearch(row({ blastProgram: 'quick-blastp', msaAlgorithm: 'clustalo' }))).toBe('uniprotkb_swissprot / quick-blastp / clustalo');
29
+ });
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { MenuItem } from '@mui/material';
3
- import { msaAlgorithms } from './consts';
4
3
  import TextField2 from '../../../components/TextField2';
4
+ import { msaAlgorithms } from './consts';
5
5
  export default function MsaAlgorithmSelect({ value, onChange, className, }) {
6
6
  return (React.createElement(TextField2, { variant: "outlined", label: "MSA Algorithm", className: className, select: true, value: value, onChange: event => {
7
7
  onChange(event.target.value);
@@ -17,3 +17,31 @@ export type MsaAlgorithm = (typeof msaAlgorithms)[number];
17
17
  export declare const blastDatabaseOptions: readonly ["uniprotkb_swissprot", "uniprotkb", "pan_proteomes", "uniprotkb_trembl"];
18
18
  export type BlastDatabase = (typeof blastDatabaseOptions)[number];
19
19
  export declare const defaultBlastDatabase: BlastDatabase;
20
+ export declare const searchPrograms: readonly ["blastp", "phmmer"];
21
+ export type SearchProgram = (typeof searchPrograms)[number];
22
+ /**
23
+ * phmmer offers PDB, AlphaFold, Ensembl Genomes, MEROPS and ChEMBL too, but
24
+ * targets outside UniProt carry no OS=/OX= in their description, so those rows
25
+ * would lose their species and common name. Only the databases that label their
26
+ * hits are offered.
27
+ */
28
+ export declare const phmmerDatabaseOptions: readonly ["swissprot", "uniprotkb", "uniprotrefprot"];
29
+ export type PhmmerDatabase = (typeof phmmerDatabaseOptions)[number];
30
+ export declare const defaultPhmmerDatabase: PhmmerDatabase;
31
+ /**
32
+ * A program together with a database that program actually has.
33
+ *
34
+ * The pair travels as one value because neither service knows the other's
35
+ * database names — `swissprot` is a phmmer database and `uniprotkb_swissprot` a
36
+ * blastp one — so a program held apart from its database can drift into a
37
+ * combination EBI answers with a 400, minutes after the user pressed Submit.
38
+ */
39
+ export type SearchChoice = {
40
+ program: 'blastp';
41
+ database: BlastDatabase;
42
+ } | {
43
+ program: 'phmmer';
44
+ database: PhmmerDatabase;
45
+ };
46
+ export declare function defaultSearchFor(program: SearchProgram): SearchChoice;
47
+ export declare function databaseOptionsFor(program: SearchProgram): readonly ["uniprotkb_swissprot", "uniprotkb", "pan_proteomes", "uniprotkb_trembl"] | readonly ["swissprot", "uniprotkb", "uniprotrefprot"];
@@ -22,3 +22,24 @@ export const blastDatabaseOptions = [
22
22
  // curated, so it returns roughly one good sequence per species rather than the
23
23
  // many near-identical TrEMBL entries an alignment reads poorly
24
24
  export const defaultBlastDatabase = 'uniprotkb_swissprot';
25
+ export const searchPrograms = ['blastp', 'phmmer'];
26
+ /**
27
+ * phmmer offers PDB, AlphaFold, Ensembl Genomes, MEROPS and ChEMBL too, but
28
+ * targets outside UniProt carry no OS=/OX= in their description, so those rows
29
+ * would lose their species and common name. Only the databases that label their
30
+ * hits are offered.
31
+ */
32
+ export const phmmerDatabaseOptions = [
33
+ 'swissprot',
34
+ 'uniprotkb',
35
+ 'uniprotrefprot',
36
+ ];
37
+ export const defaultPhmmerDatabase = 'swissprot';
38
+ export function defaultSearchFor(program) {
39
+ return program === 'phmmer'
40
+ ? { program, database: defaultPhmmerDatabase }
41
+ : { program, database: defaultBlastDatabase };
42
+ }
43
+ export function databaseOptionsFor(program) {
44
+ return program === 'phmmer' ? phmmerDatabaseOptions : blastDatabaseOptions;
45
+ }
@@ -3,7 +3,6 @@ import { FileSelector } from '@jbrowse/core/ui';
3
3
  import { FormControl, FormControlLabel, Radio, RadioGroup } from '@mui/material';
4
4
  import { observer } from 'mobx-react';
5
5
  import { makeStyles } from 'tss-react/mui';
6
- import { launchView } from './launchView';
7
6
  import TextField2 from '../../../components/TextField2';
8
7
  import { useQueryRowName } from '../../useQueryRowName';
9
8
  import { getGeneDisplayName, getLinearGenomeView } from '../../util';
@@ -12,6 +11,7 @@ import QueryRowSelector from '../QueryRowSelector';
12
11
  import SubmitCancelActions from '../SubmitCancelActions';
13
12
  import TranscriptSelector from '../TranscriptSelector';
14
13
  import { useTranscriptSelection } from '../useTranscriptSelection';
14
+ import { launchView } from './launchView';
15
15
  const useStyles = makeStyles()({
16
16
  textAreaFont: {
17
17
  fontFamily: 'Courier New',
@@ -2,16 +2,18 @@ import React, { useMemo, useState } from 'react';
2
2
  import { Typography } from '@mui/material';
3
3
  import { observer } from 'mobx-react';
4
4
  import { makeStyles } from 'tss-react/mui';
5
- import QuerySpeciesSelect from './QuerySpeciesSelect';
6
- import { orthologLaunchView } from './orthologLaunchView';
7
5
  import TextField2 from '../../../components/TextField2';
8
6
  import { defaultMaxSpecies } from '../../../utils/ncbiOrthologs';
7
+ import { useLocalStorage } from '../../../utils/useLocalStorage';
9
8
  import { getGeneDisplayName, getGeneIdentifiers, getLinearGenomeView, getTranscriptDisplayName, } from '../../util';
10
9
  import MsaAlgorithmSelect from '../BlastQuery/MsaAlgorithmSelect';
11
10
  import LaunchPanelContent from '../LaunchPanelContent';
12
11
  import SubmitCancelActions from '../SubmitCancelActions';
13
12
  import TranscriptSelector from '../TranscriptSelector';
14
13
  import { useTranscriptSelection } from '../useTranscriptSelection';
14
+ import OrthologSourceSelect, { ORTHOLOG_SOURCE_STORAGE_KEY, } from './OrthologSourceSelect';
15
+ import QuerySpeciesSelect from './QuerySpeciesSelect';
16
+ import { orthologLaunchView } from './orthologLaunchView';
15
17
  const useStyles = makeStyles()({
16
18
  selectField: {
17
19
  width: 180,
@@ -22,6 +24,7 @@ const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
22
24
  const view = getLinearGenomeView(model);
23
25
  const [launchViewError, setLaunchViewError] = useState();
24
26
  const [taxId, setTaxId] = useState(9606);
27
+ const [source, setSource] = useLocalStorage(ORTHOLOG_SOURCE_STORAGE_KEY, 'ncbi');
25
28
  const [msaAlgorithm, setMsaAlgorithm] = useState('clustalo');
26
29
  const [maxSpecies, setMaxSpecies] = useState(String(defaultMaxSpecies));
27
30
  const geneCandidates = useMemo(() => getGeneIdentifiers(feature), [feature]);
@@ -32,13 +35,14 @@ const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
32
35
  const rowCountValid = Number.isInteger(rowCount) && rowCount >= 2;
33
36
  return (React.createElement(React.Fragment, null,
34
37
  React.createElement(LaunchPanelContent, { error: e },
35
- React.createElement(Typography, { variant: "body2" }, "NCBI's precomputed orthologs, one gene per species, looked up rather than searched for. No BLAST job to queue."),
38
+ React.createElement(Typography, { variant: "body2" }, "Precomputed orthologs, one gene per species, looked up rather than searched for. No BLAST job to queue."),
36
39
  React.createElement("div", null,
40
+ React.createElement(OrthologSourceSelect, { className: classes.selectField, value: source, onChange: setSource }),
37
41
  React.createElement(QuerySpeciesSelect, { className: classes.selectField, value: taxId, assemblyName: view.assemblyNames[0], onChange: setTaxId }),
38
42
  React.createElement(MsaAlgorithmSelect, { className: classes.selectField, value: msaAlgorithm, onChange: setMsaAlgorithm }),
39
43
  React.createElement(TextField2, { variant: "outlined", label: "Rows to align", className: classes.selectField, type: "number", value: maxSpecies, onChange: event => {
40
44
  setMaxSpecies(event.target.value);
41
- }, error: !rowCountValid, helperText: "the closest N species NCBI has" })),
45
+ }, error: !rowCountValid, helperText: `the closest N species ${source === 'panther' ? 'PANTHER' : 'NCBI'} has` })),
42
46
  React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection })),
43
47
  React.createElement(SubmitCancelActions, { model: model, submitDisabled: !proteinSequence || !rowCountValid, onSubmit: () => {
44
48
  try {
@@ -50,6 +54,7 @@ const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
50
54
  newViewTitle: `Orthologs - ${getGeneDisplayName(feature)} - ${getTranscriptDisplayName(selectedTranscript)}`,
51
55
  orthologParams: {
52
56
  taxId,
57
+ source,
53
58
  maxSpecies: rowCount,
54
59
  geneCandidates,
55
60
  msaAlgorithm,
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import type { OrthologSource } from '../../../MsaViewPanel/model';
3
+ export declare const ORTHOLOG_SOURCE_STORAGE_KEY = "msaview-ortholog-source";
4
+ export declare const orthologSourceLabels: Record<OrthologSource, string>;
5
+ export default function OrthologSourceSelect({ value, onChange, className, }: {
6
+ value: OrthologSource;
7
+ onChange: (val: OrthologSource) => void;
8
+ className?: string;
9
+ }): React.JSX.Element;
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { MenuItem } from '@mui/material';
3
+ import TextField2 from '../../../components/TextField2';
4
+ export const ORTHOLOG_SOURCE_STORAGE_KEY = 'msaview-ortholog-source';
5
+ export const orthologSourceLabels = {
6
+ ncbi: 'NCBI orthologs',
7
+ panther: 'PANTHER',
8
+ };
9
+ // Which species a source can answer for, in the words a reader picking one
10
+ // needs: NCBI's ortholog sets stop at vertebrates and insects, PANTHER's run
11
+ // from human to yeast and Arabidopsis.
12
+ const hints = {
13
+ ncbi: 'vertebrates and insects',
14
+ panther: 'also yeast, worm, fly and plants',
15
+ };
16
+ export default function OrthologSourceSelect({ value, onChange, className, }) {
17
+ return (React.createElement(TextField2, { variant: "outlined", label: "Source", className: className, select: true, value: value, helperText: hints[value], onChange: event => {
18
+ onChange(event.target.value);
19
+ } }, Object.keys(orthologSourceLabels).map(val => (React.createElement(MenuItem, { value: val, key: val }, orthologSourceLabels[val])))));
20
+ }
@@ -2,8 +2,8 @@
2
2
  import React from 'react';
3
3
  import { cleanup, render, screen } from '@testing-library/react';
4
4
  import { afterEach, beforeEach, expect, test, vi } from 'vitest';
5
- import SubmitCancelActions from './SubmitCancelActions';
6
5
  import { LAUNCH_PLACEMENT_KEY } from '../../utils/workspaces';
6
+ import SubmitCancelActions from './SubmitCancelActions';
7
7
  // getSession walks the MST tree, and this component only wants the two actions
8
8
  // off the far end of that walk
9
9
  vi.mock('@jbrowse/core/util', () => ({
@@ -1,7 +1,7 @@
1
1
  import { getSession } from '@jbrowse/core/util';
2
+ import { useFetch } from '../../utils/useFetch';
2
3
  import { getProteinSequenceFromFeature } from './calculateProteinSequence';
3
4
  import { fetchSeq } from './fetchSeq';
4
- import { useFetch } from '../../utils/useFetch';
5
5
  export function useFeatureSequence({ view, feature, }) {
6
6
  const assemblyName = view?.assemblyNames?.[0];
7
7
  const { data: sequence, error } = useFetch(feature && assemblyName
@@ -19,5 +19,18 @@ export interface QueryRowMatch {
19
19
  /** identity over the compared region, 0-1 */
20
20
  identity: number;
21
21
  }
22
- export declare function detectQueryRow(msaText: string, proteinSequence: string): QueryRowMatch | undefined;
23
- export declare function getMsaRowNames(msaText: string): string[];
22
+ export interface MsaQueryRow {
23
+ /** every row name, in file order, for the picker to offer */
24
+ names: string[];
25
+ /** the row whose residues are the query's, if one of them is */
26
+ match?: QueryRowMatch;
27
+ }
28
+ /**
29
+ * The picker's whole answer for a pasted alignment: its row names, and which of
30
+ * them is the query.
31
+ *
32
+ * One function rather than two because there is one parse. Both answers were
33
+ * wanted on every keystroke in the paste box, and asking separately parsed a
34
+ * few-hundred-row alignment twice per character.
35
+ */
36
+ export declare function findQueryRow(msaText: string, proteinSequence: string): MsaQueryRow;
@@ -36,23 +36,32 @@ const SIMILARITY_FLOOR = 0.9;
36
36
  * user is still typing.
37
37
  */
38
38
  const PARTIAL_COVERAGE_FLOOR = 0.5;
39
- export function detectQueryRow(msaText, proteinSequence) {
40
- const query = normalize(proteinSequence);
41
- if (!query || !msaText.trim()) {
42
- return undefined;
39
+ /**
40
+ * The picker's whole answer for a pasted alignment: its row names, and which of
41
+ * them is the query.
42
+ *
43
+ * One function rather than two because there is one parse. Both answers were
44
+ * wanted on every keystroke in the paste box, and asking separately parsed a
45
+ * few-hundred-row alignment twice per character.
46
+ */
47
+ export function findQueryRow(msaText, proteinSequence) {
48
+ if (!msaText.trim()) {
49
+ return { names: [] };
43
50
  }
44
- let names;
45
51
  let parsed;
46
52
  try {
47
- const msa = parseMSA(msaText);
48
- names = msa.getNames();
49
- parsed = msa;
53
+ parsed = parseMSA(msaText);
50
54
  }
51
55
  catch {
52
56
  // a half-pasted alignment throws here on every keystroke; the caller shows
53
57
  // the field rather than an error
54
- return undefined;
58
+ return { names: [] };
55
59
  }
60
+ const names = parsed.getNames();
61
+ const query = normalize(proteinSequence);
62
+ return { names, match: query ? bestMatch(parsed, names, query) : undefined };
63
+ }
64
+ function bestMatch(parsed, names, query) {
56
65
  const candidates = [];
57
66
  for (const name of names) {
58
67
  const row = normalize(getUngappedSequence(parsed.getRow(name)));
@@ -77,18 +86,8 @@ export function detectQueryRow(msaText, proteinSequence) {
77
86
  candidates.push({ name, quality: 'similar', identity });
78
87
  }
79
88
  }
80
- const order = ['exact', 'partial', 'similar'];
89
+ // an exact match returns above, so only these two can be here
90
+ const order = ['partial', 'similar'];
81
91
  return candidates.sort((a, b) => order.indexOf(a.quality) - order.indexOf(b.quality) ||
82
92
  b.identity - a.identity)[0];
83
93
  }
84
- export function getMsaRowNames(msaText) {
85
- if (!msaText.trim()) {
86
- return [];
87
- }
88
- try {
89
- return parseMSA(msaText).getNames();
90
- }
91
- catch {
92
- return [];
93
- }
94
- }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'vitest';
2
- import { detectQueryRow, getMsaRowNames } from './detectQueryRow';
2
+ import { findQueryRow } from './detectQueryRow';
3
3
  const protein = 'MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD';
4
4
  // COBALT renames the query `Query_1`, so only the residues identify it
5
5
  const clustal = `CLUSTAL W (1.81) multiple sequence alignment
@@ -13,22 +13,22 @@ MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD
13
13
  >sp|P02769|ALBU_BOVIN
14
14
  MKWVTFISLLLLFSSAYSRG--RRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPYD
15
15
  `;
16
- describe('detectQueryRow', () => {
16
+ describe('findQueryRow', () => {
17
17
  test('finds the query by sequence when the aligner renamed it', () => {
18
- expect(detectQueryRow(clustal, protein)).toMatchObject({
18
+ expect(findQueryRow(clustal, protein).match).toMatchObject({
19
19
  name: 'Query_1',
20
20
  quality: 'exact',
21
21
  });
22
22
  });
23
23
  test('ignores gaps in the aligned row', () => {
24
- expect(detectQueryRow(fasta, protein)?.name).toBe('Query_1');
24
+ expect(findQueryRow(fasta, protein).match?.name).toBe('Query_1');
25
25
  });
26
26
  test('tolerates the trailing stop codon the translation carries', () => {
27
- expect(detectQueryRow(clustal, `${protein}*`)?.name).toBe('Query_1');
27
+ expect(findQueryRow(clustal, `${protein}*`).match?.name).toBe('Query_1');
28
28
  });
29
29
  test('matches a row that is the query trimmed to the aligned region', () => {
30
30
  const trimmed = `>hit_one\nWRONGWRONGWRONGWRONG\n>aligned_query\n${protein.slice(5, 40)}\n`;
31
- expect(detectQueryRow(trimmed, protein)).toMatchObject({
31
+ expect(findQueryRow(trimmed, protein).match).toMatchObject({
32
32
  name: 'aligned_query',
33
33
  quality: 'partial',
34
34
  });
@@ -41,25 +41,25 @@ MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD
41
41
  >hit_two
42
42
  MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD
43
43
  `;
44
- expect(detectQueryRow(homologsOnly, 'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWW')).toBeUndefined();
44
+ expect(findQueryRow(homologsOnly, 'WWWWWWWWWWWWWWWWWWWWWWWWWWWWWW').match).toBeUndefined();
45
45
  });
46
46
  test('returns nothing rather than throwing on a half-pasted alignment', () => {
47
- expect(detectQueryRow('>partial\nMKWV', protein)).toBeUndefined();
48
- expect(detectQueryRow('not an alignment at all', protein)).toBeUndefined();
49
- expect(detectQueryRow('', protein)).toBeUndefined();
50
- expect(detectQueryRow(clustal, '')).toBeUndefined();
47
+ expect(findQueryRow('>partial\nMKWV', protein).match).toBeUndefined();
48
+ expect(findQueryRow('not an alignment at all', protein).match).toBeUndefined();
49
+ expect(findQueryRow('', protein).match).toBeUndefined();
50
+ expect(findQueryRow(clustal, '').match).toBeUndefined();
51
51
  });
52
52
  });
53
- describe('getMsaRowNames', () => {
53
+ describe('findQueryRow row names', () => {
54
54
  test('lists the rows for the override dropdown', () => {
55
- expect(getMsaRowNames(clustal)).toEqual([
55
+ expect(findQueryRow(clustal, protein).names).toEqual([
56
56
  'Query_1',
57
57
  'sp|P02769|ALBU',
58
58
  'sp|Q5XLE4|OTHE',
59
59
  ]);
60
60
  });
61
61
  test('is empty rather than throwing while the user is still pasting', () => {
62
- expect(getMsaRowNames('CLUSTAL W')).toEqual([]);
63
- expect(getMsaRowNames('')).toEqual([]);
62
+ expect(findQueryRow('CLUSTAL W', protein).names).toEqual([]);
63
+ expect(findQueryRow('', protein).names).toEqual([]);
64
64
  });
65
65
  });