jbrowse-plugin-msaview 3.4.1 → 3.5.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 (134) hide show
  1. package/README.md +7 -0
  2. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +20 -12
  3. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -0
  4. package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +5 -3
  5. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +24 -4
  6. package/dist/LaunchMsaView/components/BlastQuery/consts.js +44 -2
  7. package/dist/LaunchMsaView/components/BlastQuery/searchChoiceStorage.d.ts +1 -1
  8. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -0
  9. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.d.ts +3 -1
  10. package/dist/LaunchMsaView/components/ManualMSALoader/launchView.js +2 -1
  11. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +8 -3
  12. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +4 -1
  13. package/dist/LaunchMsaView/components/QueryRowSelector.js +21 -5
  14. package/dist/LaunchMsaView/detectQueryRow.d.ts +11 -0
  15. package/dist/LaunchMsaView/detectQueryRow.js +11 -4
  16. package/dist/LaunchMsaView/detectQueryRow.test.js +30 -0
  17. package/dist/LaunchMsaView/useQueryRowName.d.ts +1 -0
  18. package/dist/LaunchMsaView/useQueryRowName.js +5 -1
  19. package/dist/LaunchMsaView/useQueryRowName.test.js +23 -0
  20. package/dist/LaunchMsaViewExtensionPoint/index.js +8 -6
  21. package/dist/LaunchMsaViewExtensionPoint/index.test.js +21 -0
  22. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +3 -2
  23. package/dist/MsaViewPanel/afterCreateAutoruns.js +48 -19
  24. package/dist/MsaViewPanel/components/LaunchProgress.js +12 -3
  25. package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -2
  26. package/dist/MsaViewPanel/components/MsaViewPanel.test.js +23 -2
  27. package/dist/MsaViewPanel/doLaunchBlast.d.ts +8 -0
  28. package/dist/MsaViewPanel/doLaunchBlast.js +78 -93
  29. package/dist/MsaViewPanel/doLaunchBlast.test.d.ts +1 -0
  30. package/dist/MsaViewPanel/doLaunchBlast.test.js +158 -0
  31. package/dist/MsaViewPanel/doLaunchOrthologs.js +40 -5
  32. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +10 -0
  33. package/dist/MsaViewPanel/genomeToMSA.js +4 -4
  34. package/dist/MsaViewPanel/genomeToMSA.test.js +67 -0
  35. package/dist/MsaViewPanel/loadProteinDomains.d.ts +8 -2
  36. package/dist/MsaViewPanel/loadProteinDomains.js +18 -9
  37. package/dist/MsaViewPanel/loadProteinDomains.test.d.ts +1 -0
  38. package/dist/MsaViewPanel/loadProteinDomains.test.js +41 -0
  39. package/dist/MsaViewPanel/model.d.ts +160 -89
  40. package/dist/MsaViewPanel/model.js +125 -48
  41. package/dist/MsaViewPanel/model.test.d.ts +1 -0
  42. package/dist/MsaViewPanel/model.test.js +100 -0
  43. package/dist/MsaViewPanel/msaCoordToGenomeCoord.d.ts +12 -1
  44. package/dist/MsaViewPanel/msaCoordToGenomeCoord.js +8 -8
  45. package/dist/MsaViewPanel/msaCoordToGenomeCoord.test.js +133 -32
  46. package/dist/MsaViewPanel/msaDataStore.d.ts +2 -0
  47. package/dist/MsaViewPanel/msaDataStore.js +10 -0
  48. package/dist/MsaViewPanel/observeProteinHighlights.test.js +20 -3
  49. package/dist/MsaViewPanel/processInit.test.d.ts +1 -0
  50. package/dist/MsaViewPanel/processInit.test.js +72 -0
  51. package/dist/MsaViewPanel/resolveConnectedTranscript.d.ts +9 -0
  52. package/dist/MsaViewPanel/resolveConnectedTranscript.js +122 -0
  53. package/dist/MsaViewPanel/runLaunch.d.ts +1 -0
  54. package/dist/MsaViewPanel/runLaunch.js +35 -0
  55. package/dist/MsaViewPanel/runLaunch.test.js +46 -0
  56. package/dist/MsaViewPanel/structureConnection.d.ts +0 -4
  57. package/dist/MsaViewPanel/structureConnection.js +0 -19
  58. package/dist/MsaViewPanel/util.d.ts +39 -0
  59. package/dist/MsaViewPanel/util.js +44 -0
  60. package/dist/jbrowse-plugin-msaview.umd.production.min.js +44 -43
  61. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  62. package/dist/utils/browserAlign.d.ts +42 -0
  63. package/dist/utils/browserAlign.js +297 -0
  64. package/dist/utils/browserAlign.test.d.ts +1 -0
  65. package/dist/utils/browserAlign.test.js +85 -0
  66. package/dist/utils/ebiBlast.d.ts +10 -1
  67. package/dist/utils/ebiBlast.js +27 -1
  68. package/dist/utils/homologSearch.d.ts +31 -0
  69. package/dist/utils/homologSearch.js +6 -0
  70. package/dist/utils/msa.d.ts +3 -11
  71. package/dist/utils/msa.js +14 -29
  72. package/dist/utils/msaRows.d.ts +11 -8
  73. package/dist/utils/msaRows.js +14 -12
  74. package/dist/utils/phmmer.d.ts +11 -1
  75. package/dist/utils/phmmer.js +49 -12
  76. package/dist/utils/unirefHomologs.d.ts +94 -0
  77. package/dist/utils/unirefHomologs.js +193 -0
  78. package/dist/utils/unirefHomologs.test.d.ts +1 -0
  79. package/dist/utils/unirefHomologs.test.js +118 -0
  80. package/dist/version.d.ts +1 -1
  81. package/dist/version.js +1 -1
  82. package/package.json +5 -4
  83. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +38 -13
  84. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -0
  85. package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +7 -2
  86. package/src/LaunchMsaView/components/BlastQuery/consts.ts +50 -2
  87. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -0
  88. package/src/LaunchMsaView/components/ManualMSALoader/launchView.ts +4 -0
  89. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +10 -3
  90. package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +4 -1
  91. package/src/LaunchMsaView/components/QueryRowSelector.tsx +35 -7
  92. package/src/LaunchMsaView/detectQueryRow.test.ts +34 -0
  93. package/src/LaunchMsaView/detectQueryRow.ts +22 -4
  94. package/src/LaunchMsaView/useQueryRowName.test.ts +27 -0
  95. package/src/LaunchMsaView/useQueryRowName.ts +5 -1
  96. package/src/LaunchMsaViewExtensionPoint/index.test.ts +23 -0
  97. package/src/LaunchMsaViewExtensionPoint/index.ts +32 -6
  98. package/src/MsaViewPanel/afterCreateAutoruns.ts +53 -18
  99. package/src/MsaViewPanel/components/LaunchProgress.tsx +26 -2
  100. package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +24 -2
  101. package/src/MsaViewPanel/components/MsaViewPanel.tsx +8 -2
  102. package/src/MsaViewPanel/doLaunchBlast.test.ts +207 -0
  103. package/src/MsaViewPanel/doLaunchBlast.ts +92 -142
  104. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +13 -0
  105. package/src/MsaViewPanel/doLaunchOrthologs.ts +55 -5
  106. package/src/MsaViewPanel/genomeToMSA.test.ts +76 -0
  107. package/src/MsaViewPanel/genomeToMSA.ts +8 -4
  108. package/src/MsaViewPanel/loadProteinDomains.test.ts +52 -0
  109. package/src/MsaViewPanel/loadProteinDomains.ts +24 -11
  110. package/src/MsaViewPanel/model.test.ts +121 -0
  111. package/src/MsaViewPanel/model.ts +153 -55
  112. package/src/MsaViewPanel/msaCoordToGenomeCoord.test.ts +152 -32
  113. package/src/MsaViewPanel/msaCoordToGenomeCoord.ts +21 -11
  114. package/src/MsaViewPanel/msaDataStore.ts +10 -0
  115. package/src/MsaViewPanel/observeProteinHighlights.test.ts +27 -3
  116. package/src/MsaViewPanel/processInit.test.ts +84 -0
  117. package/src/MsaViewPanel/resolveConnectedTranscript.ts +150 -0
  118. package/src/MsaViewPanel/runLaunch.test.ts +54 -0
  119. package/src/MsaViewPanel/runLaunch.ts +39 -0
  120. package/src/MsaViewPanel/structureConnection.ts +0 -26
  121. package/src/MsaViewPanel/util.ts +75 -0
  122. package/src/utils/browserAlign.test.ts +102 -0
  123. package/src/utils/browserAlign.ts +352 -0
  124. package/src/utils/ebiBlast.ts +34 -0
  125. package/src/utils/homologSearch.ts +51 -0
  126. package/src/utils/msa.ts +19 -41
  127. package/src/utils/msaRows.ts +20 -16
  128. package/src/utils/phmmer.ts +57 -11
  129. package/src/utils/unirefHomologs.test.ts +149 -0
  130. package/src/utils/unirefHomologs.ts +323 -0
  131. package/src/version.ts +1 -1
  132. package/dist/MsaViewPanel/structureConnection.test.js +0 -53
  133. package/src/MsaViewPanel/structureConnection.test.ts +0 -62
  134. /package/dist/{MsaViewPanel/structureConnection.test.d.ts → LaunchMsaView/useQueryRowName.test.d.ts} +0 -0
package/README.md CHANGED
@@ -29,6 +29,13 @@ https://jbrowse.org/code/jb2/main/index.html?config=https://unpkg.com/jbrowse-pl
29
29
  - Can share sessions with other users which will send relevant settings and
30
30
  links to files to automatically open your results
31
31
  - The tree or the MSA panel can be loaded separately from each other
32
+ - Builds a cross-species alignment for any gene from the genome view's
33
+ right-click menu: precomputed orthologs (NCBI, PANTHER), the gene's UniRef
34
+ cluster across all of UniProtKB, or a phmmer/blastp search at EBI
35
+ - Aligns and builds trees in the browser, so a UniRef launch needs no job at any
36
+ external service; EBI's aligners remain an option
37
+ - Every launch is also a session-spec URL (`orthologParams`, `searchParams`,
38
+ `connectedTranscript`), see [DEVELOPERS.md](DEVELOPERS.md)
32
39
 
33
40
  ## File format supports
34
41
 
@@ -12,7 +12,7 @@ import { useTranscriptSelection } from '../useTranscriptSelection';
12
12
  import CachedBlastResults from './CachedBlastResults';
13
13
  import MsaAlgorithmSelect from './MsaAlgorithmSelect';
14
14
  import { blastLaunchView } from './blastLaunchView';
15
- import { databaseOptionsFor, defaultSearchFor, searchPrograms } from './consts';
15
+ import { databaseOptionsFor, defaultMaxHits, defaultSearchFor, searchPrograms, } from './consts';
16
16
  import { useStoredMsaAlgorithm, useStoredSearchChoice, } from './searchChoiceStorage';
17
17
  import { useCachedBlastResults } from './useCachedBlastResults';
18
18
  const useStyles = makeStyles()({
@@ -41,6 +41,9 @@ const BlastAutomaticPanel = observer(function ({ handleClose, feature, model, ch
41
41
  // would let them drift into that
42
42
  const [search, setSearch] = useStoredSearchChoice();
43
43
  const [selectedMsaAlgorithm, setSelectedMsaAlgorithm] = useStoredMsaAlgorithm();
44
+ const [maxHits, setMaxHits] = useState(String(defaultMaxHits));
45
+ const hitCount = Number(maxHits);
46
+ const hitCountValid = Number.isInteger(hitCount) && hitCount >= 1;
44
47
  const isPhmmer = search.program === 'phmmer';
45
48
  const geneIds = useMemo(() => getGeneIdentifiers(feature), [feature]);
46
49
  const { results: cachedResults, error: cachedResultsError } = useCachedBlastResults(geneIds);
@@ -63,28 +66,31 @@ const BlastAutomaticPanel = observer(function ({ handleClose, feature, model, ch
63
66
  });
64
67
  } }, databaseOptionsFor(search.program).map(val => (React.createElement(MenuItem, { value: val, key: val }, val)))),
65
68
  isPhmmer ? null : (React.createElement(MsaAlgorithmSelect, { className: classes.selectField, value: selectedMsaAlgorithm, onChange: setSelectedMsaAlgorithm })),
69
+ React.createElement(TextField2, { variant: "outlined", label: "Hits", className: classes.selectField, type: "number", value: maxHits, onChange: event => {
70
+ setMaxHits(event.target.value);
71
+ }, error: !hitCountValid, helperText: "best-scoring sequences to keep" }),
66
72
  React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection }),
67
73
  React.createElement(Typography, { className: classes.infoText },
68
74
  isPhmmer
69
75
  ? `phmmer searches UniProtKB with a profile HMM built from the query,
70
76
  so it aligns the hits as it finds them and that alignment is used
71
77
  directly — nothing is realigned afterwards. The tree is then built
72
- from it by neighbour-joining. A hit matching the query in more
73
- than one place appears once per matched region.`
78
+ from it in the browser. A hit matching the query in more than one
79
+ place appears once per matched region. rp15 spreads the hits
80
+ across all of life; swissprot keeps to the curated set.`
74
81
  : `This panel will automatically submit a blastp query to EBI, which
75
- searches UniProtKB. Searches usually finish in under a minute, and
76
- swissprot returns curated sequences that align more cleanly than
77
- the many near-identical entries a TrEMBL search brings back. After
78
- completion, all the hits will be run through a multiple sequence
79
- alignment.`,
82
+ searches UniProtKB. swissprot returns curated sequences that align
83
+ more cleanly than the many near-identical entries a TrEMBL search
84
+ brings back. After completion, all the hits will be run through
85
+ the chosen aligner; "in browser" needs no second EBI job.`,
80
86
  ' ',
81
- "Searching NCBI's nr needs the manual approach: NCBI no longer lets a browser read responses from Blast.cgi."),
87
+ "EBI's queue is the wait, and it varies from seconds to many minutes. For a homolog panel with no job at all, the Orthologs tab's UniRef source is a lookup. Searching NCBI's nr needs the manual approach: NCBI no longer lets a browser read responses from Blast.cgi."),
82
88
  cachedResults.length > 0 ? (React.createElement(Accordion, { className: classes.cachedResultsAccordion },
83
89
  React.createElement(AccordionSummary, { expandIcon: React.createElement(ExpandMoreIcon, null) },
84
90
  React.createElement(Typography, null, "Previous BLAST Results")),
85
91
  React.createElement(AccordionDetails, null,
86
92
  React.createElement(CachedBlastResults, { model: model, handleClose: handleClose, feature: feature })))) : null),
87
- React.createElement(SubmitCancelActions, { model: model, submitDisabled: !proteinSequence, onSubmit: () => {
93
+ React.createElement(SubmitCancelActions, { model: model, submitDisabled: !proteinSequence || !hitCountValid, onSubmit: () => {
88
94
  try {
89
95
  if (selectedTranscript) {
90
96
  setLaunchViewError(undefined);
@@ -96,14 +102,16 @@ const BlastAutomaticPanel = observer(function ({ handleClose, feature, model, ch
96
102
  ? {
97
103
  searchProgram: 'phmmer',
98
104
  blastDatabase: search.database,
99
- selectedTranscript,
105
+ maxHits: hitCount,
106
+ selectedTranscript: selectedTranscript.toJSON(),
100
107
  proteinSequence,
101
108
  }
102
109
  : {
103
110
  searchProgram: 'blastp',
104
111
  blastDatabase: search.database,
105
112
  msaAlgorithm: selectedMsaAlgorithm,
106
- selectedTranscript,
113
+ maxHits: hitCount,
114
+ selectedTranscript: selectedTranscript.toJSON(),
107
115
  proteinSequence,
108
116
  },
109
117
  });
@@ -88,6 +88,7 @@ const BlastManualPanel = observer(function ({ handleClose, feature, model, child
88
88
  view,
89
89
  feature: selectedTranscript,
90
90
  querySeqName: queryRow.querySeqName,
91
+ querySeqOffset: queryRow.querySeqOffset,
91
92
  data: { msa: msaText, tree: treeText },
92
93
  });
93
94
  handleClose();
@@ -1,9 +1,11 @@
1
1
  import React from 'react';
2
2
  import { MenuItem } from '@mui/material';
3
3
  import TextField2 from '../../../components/TextField2';
4
- import { msaAlgorithms } from './consts';
4
+ import { msaAlgorithmLabels, msaAlgorithms } from './consts';
5
5
  export default function MsaAlgorithmSelect({ value, onChange, className, }) {
6
- return (React.createElement(TextField2, { variant: "outlined", label: "MSA Algorithm", className: className, select: true, value: value, onChange: event => {
6
+ return (React.createElement(TextField2, { variant: "outlined", label: "MSA Algorithm", className: className, select: true, value: value, helperText: value === 'browser'
7
+ ? 'no EBI job; rows aligned to the query'
8
+ : undefined, onChange: event => {
7
9
  onChange(event.target.value);
8
- } }, msaAlgorithms.map(val => (React.createElement(MenuItem, { value: val, key: val }, val)))));
10
+ } }, msaAlgorithms.map(val => (React.createElement(MenuItem, { value: val, key: val }, msaAlgorithmLabels[val])))));
9
11
  }
@@ -5,8 +5,20 @@
5
5
  * cannot read a response from it at all. See docs/blast.md.
6
6
  */
7
7
  export declare const BASE_BLAST_URL = "https://blast.ncbi.nlm.nih.gov/Blast.cgi";
8
- export declare const msaAlgorithms: readonly ["clustalo", "muscle", "kalign", "mafft"];
8
+ /** The aligners EBI's Job Dispatcher runs, each a tool name at their REST api. */
9
+ export declare const ebiMsaAlgorithms: readonly ["clustalo", "muscle", "kalign", "mafft"];
10
+ export type EbiMsaAlgorithm = (typeof ebiMsaAlgorithms)[number];
11
+ /**
12
+ * `browser` is no job at all: each sequence is aligned to the query in the
13
+ * page and the rows merged on the query (utils/browserAlign.ts), with the tree
14
+ * built by react-msaview's neighbour joining. It is the aligner for a launch
15
+ * that must not depend on EBI, and for one that wants to be quick -- a
16
+ * hundred rows take a second or two against a Job Dispatcher queue that has
17
+ * been measured at anything from ten seconds to fifteen minutes.
18
+ */
19
+ export declare const msaAlgorithms: readonly ["clustalo", "muscle", "kalign", "mafft", "browser"];
9
20
  export type MsaAlgorithm = (typeof msaAlgorithms)[number];
21
+ export declare const msaAlgorithmLabels: Record<MsaAlgorithm, string>;
10
22
  /**
11
23
  * EBI rejects a submission naming a database outside its own list with a 400,
12
24
  * so every value here has to appear in
@@ -21,13 +33,21 @@ export declare const searchPrograms: readonly ["blastp", "phmmer"];
21
33
  export type SearchProgram = (typeof searchPrograms)[number];
22
34
  /**
23
35
  * 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
36
+ * targets outside UniProt carry no species in their description, so those rows
25
37
  * would lose their species and common name. Only the databases that label their
26
38
  * hits are offered.
39
+ *
40
+ * rp15..rp75 are the Representative Proteomes: UniProt's reference proteomes
41
+ * thinned so that no two are more than 15% (35%, 55%, 75%) similar, which is
42
+ * the widest taxonomic spread per hit that any of these databases gives. rp15
43
+ * is the one to reach for when the question is "what is this like across all
44
+ * of life"; swissprot when it is "what is this like in the curated set".
27
45
  */
28
- export declare const phmmerDatabaseOptions: readonly ["swissprot", "uniprotkb", "uniprotrefprot"];
46
+ export declare const phmmerDatabaseOptions: readonly ["swissprot", "uniprotkb", "uniprotrefprot", "rp75", "rp55", "rp35", "rp15"];
29
47
  export type PhmmerDatabase = (typeof phmmerDatabaseOptions)[number];
30
48
  export declare const defaultPhmmerDatabase: PhmmerDatabase;
49
+ export declare const defaultMaxHits = 100;
50
+ export declare function snapBlastHitCount(maxHits: number): number;
31
51
  /**
32
52
  * A program together with a database that program actually has.
33
53
  *
@@ -44,4 +64,4 @@ export type SearchChoice = {
44
64
  database: PhmmerDatabase;
45
65
  };
46
66
  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"];
67
+ export declare function databaseOptionsFor(program: SearchProgram): readonly ["uniprotkb_swissprot", "uniprotkb", "pan_proteomes", "uniprotkb_trembl"] | readonly ["swissprot", "uniprotkb", "uniprotrefprot", "rp75", "rp55", "rp35", "rp15"];
@@ -5,7 +5,29 @@
5
5
  * cannot read a response from it at all. See docs/blast.md.
6
6
  */
7
7
  export const BASE_BLAST_URL = 'https://blast.ncbi.nlm.nih.gov/Blast.cgi';
8
- export const msaAlgorithms = ['clustalo', 'muscle', 'kalign', 'mafft'];
8
+ /** The aligners EBI's Job Dispatcher runs, each a tool name at their REST api. */
9
+ export const ebiMsaAlgorithms = [
10
+ 'clustalo',
11
+ 'muscle',
12
+ 'kalign',
13
+ 'mafft',
14
+ ];
15
+ /**
16
+ * `browser` is no job at all: each sequence is aligned to the query in the
17
+ * page and the rows merged on the query (utils/browserAlign.ts), with the tree
18
+ * built by react-msaview's neighbour joining. It is the aligner for a launch
19
+ * that must not depend on EBI, and for one that wants to be quick -- a
20
+ * hundred rows take a second or two against a Job Dispatcher queue that has
21
+ * been measured at anything from ten seconds to fifteen minutes.
22
+ */
23
+ export const msaAlgorithms = [...ebiMsaAlgorithms, 'browser'];
24
+ export const msaAlgorithmLabels = {
25
+ clustalo: 'clustalo (EBI)',
26
+ muscle: 'muscle (EBI)',
27
+ kalign: 'kalign (EBI)',
28
+ mafft: 'mafft (EBI)',
29
+ browser: 'in browser, query-anchored',
30
+ };
9
31
  /**
10
32
  * EBI rejects a submission naming a database outside its own list with a 400,
11
33
  * so every value here has to appear in
@@ -25,16 +47,36 @@ export const defaultBlastDatabase = 'uniprotkb_swissprot';
25
47
  export const searchPrograms = ['blastp', 'phmmer'];
26
48
  /**
27
49
  * 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
50
+ * targets outside UniProt carry no species in their description, so those rows
29
51
  * would lose their species and common name. Only the databases that label their
30
52
  * hits are offered.
53
+ *
54
+ * rp15..rp75 are the Representative Proteomes: UniProt's reference proteomes
55
+ * thinned so that no two are more than 15% (35%, 55%, 75%) similar, which is
56
+ * the widest taxonomic spread per hit that any of these databases gives. rp15
57
+ * is the one to reach for when the question is "what is this like across all
58
+ * of life"; swissprot when it is "what is this like in the curated set".
31
59
  */
32
60
  export const phmmerDatabaseOptions = [
33
61
  'swissprot',
34
62
  'uniprotkb',
35
63
  'uniprotrefprot',
64
+ 'rp75',
65
+ 'rp55',
66
+ 'rp35',
67
+ 'rp15',
36
68
  ];
37
69
  export const defaultPhmmerDatabase = 'swissprot';
70
+ /**
71
+ * The hit counts EBI's ncbiblast accepts for `alignments` and `scores`. A value
72
+ * off this list is a 400 at submit time, so a request is rounded up to the next
73
+ * one on it.
74
+ */
75
+ const blastHitCounts = [5, 10, 20, 50, 100, 150, 200, 250, 500, 750, 1000];
76
+ export const defaultMaxHits = 100;
77
+ export function snapBlastHitCount(maxHits) {
78
+ return blastHitCounts.find(n => n >= maxHits) ?? blastHitCounts.at(-1);
79
+ }
38
80
  export function defaultSearchFor(program) {
39
81
  return program === 'phmmer'
40
82
  ? { program, database: defaultPhmmerDatabase }
@@ -4,4 +4,4 @@ export declare const MSA_ALGORITHM_STORAGE_KEY = "msaView-msaAlgorithm";
4
4
  export declare function validSearchChoice(stored: unknown): SearchChoice;
5
5
  export declare function validMsaAlgorithm(stored: unknown): MsaAlgorithm;
6
6
  export declare function useStoredSearchChoice(): readonly [SearchChoice, (choice: SearchChoice) => void];
7
- export declare function useStoredMsaAlgorithm(): readonly ["clustalo" | "muscle" | "kalign" | "mafft", (algorithm: MsaAlgorithm) => void];
7
+ export declare function useStoredMsaAlgorithm(): readonly ["clustalo" | "muscle" | "kalign" | "mafft" | "browser", (algorithm: MsaAlgorithm) => void];
@@ -69,6 +69,7 @@ const ManualMSALoader = observer(function PreLoadedMSA2({ model, feature, handle
69
69
  view,
70
70
  feature: selectedTranscript,
71
71
  querySeqName: queryRow.querySeqName,
72
+ querySeqOffset: queryRow.querySeqOffset,
72
73
  ...(inputMethod === 'file'
73
74
  ? {
74
75
  msaFilehandle: msaFileLocation,
@@ -1,12 +1,14 @@
1
1
  import type { Feature, FileLocation } from '@jbrowse/core/util';
2
2
  import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view';
3
- export declare function launchView({ newViewTitle, view, feature, msaFilehandle, treeFilehandle, querySeqName, data, }: {
3
+ export declare function launchView({ newViewTitle, view, feature, msaFilehandle, treeFilehandle, querySeqName, querySeqOffset, data, }: {
4
4
  newViewTitle: string;
5
5
  view: LinearGenomeViewModel;
6
6
  feature: Feature;
7
7
  msaFilehandle?: FileLocation;
8
8
  treeFilehandle?: FileLocation;
9
9
  querySeqName?: string;
10
+ /** transcript residues before the query row's first residue */
11
+ querySeqOffset?: number;
10
12
  data?: {
11
13
  msa: string;
12
14
  tree?: string;
@@ -1,7 +1,7 @@
1
1
  import { getSession } from '@jbrowse/core/util';
2
2
  import { launchMsaView } from '../../../utils/launchMsaView';
3
3
  import { readLaunchPlacement } from '../../../utils/workspaces';
4
- export function launchView({ newViewTitle, view, feature, msaFilehandle, treeFilehandle, querySeqName, data, }) {
4
+ export function launchView({ newViewTitle, view, feature, msaFilehandle, treeFilehandle, querySeqName, querySeqOffset, data, }) {
5
5
  launchMsaView(getSession(view), {
6
6
  placement: readLaunchPlacement(),
7
7
  displayName: newViewTitle,
@@ -10,6 +10,7 @@ export function launchView({ newViewTitle, view, feature, msaFilehandle, treeFil
10
10
  msaFilehandle,
11
11
  treeFilehandle,
12
12
  querySeqName,
13
+ querySeqOffset,
13
14
  data,
14
15
  });
15
16
  }
@@ -20,6 +20,11 @@ const useStyles = makeStyles()({
20
20
  width: 180,
21
21
  },
22
22
  });
23
+ const rowsHint = {
24
+ ncbi: 'the closest N species NCBI has',
25
+ panther: 'the closest N species PANTHER has',
26
+ uniref: 'one per species, reviewed entries first',
27
+ };
23
28
  const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
24
29
  const { classes } = useStyles();
25
30
  const view = getLinearGenomeView(model);
@@ -36,14 +41,14 @@ const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
36
41
  const rowCountValid = Number.isInteger(rowCount) && rowCount >= 2;
37
42
  return (React.createElement(React.Fragment, null,
38
43
  React.createElement(LaunchPanelContent, { error: e },
39
- React.createElement(Typography, { variant: "body2" }, "Precomputed orthologs, one gene per species, looked up rather than searched for. No BLAST job to queue."),
44
+ React.createElement(Typography, { variant: "body2" }, "Precomputed orthologs, one gene per species, looked up rather than searched for. No BLAST job to queue, and with the in-browser aligner no job at all."),
40
45
  React.createElement("div", null,
41
46
  React.createElement(OrthologSourceSelect, { className: classes.selectField, value: source, onChange: setSource }),
42
47
  React.createElement(QuerySpeciesSelect, { className: classes.selectField, value: taxId, assemblyName: view.assemblyNames[0], onChange: setTaxId }),
43
48
  React.createElement(MsaAlgorithmSelect, { className: classes.selectField, value: msaAlgorithm, onChange: setMsaAlgorithm }),
44
49
  React.createElement(TextField2, { variant: "outlined", label: "Rows to align", className: classes.selectField, type: "number", value: maxSpecies, onChange: event => {
45
50
  setMaxSpecies(event.target.value);
46
- }, error: !rowCountValid, helperText: `the closest N species ${source === 'panther' ? 'PANTHER' : 'NCBI'} has` })),
51
+ }, error: !rowCountValid, helperText: rowsHint[source] })),
47
52
  React.createElement(TranscriptSelector, { feature: feature, ...transcriptSelection })),
48
53
  React.createElement(SubmitCancelActions, { model: model, submitDisabled: !proteinSequence || !rowCountValid, onSubmit: () => {
49
54
  try {
@@ -59,7 +64,7 @@ const OrthologPanel = observer(function ({ handleClose, feature, model, }) {
59
64
  maxSpecies: rowCount,
60
65
  geneCandidates,
61
66
  msaAlgorithm,
62
- selectedTranscript,
67
+ selectedTranscript: selectedTranscript.toJSON(),
63
68
  proteinSequence,
64
69
  },
65
70
  });
@@ -5,13 +5,16 @@ export const ORTHOLOG_SOURCE_STORAGE_KEY = 'msaview-ortholog-source';
5
5
  export const orthologSourceLabels = {
6
6
  ncbi: 'NCBI orthologs',
7
7
  panther: 'PANTHER',
8
+ uniref: 'UniRef cluster',
8
9
  };
9
10
  // Which species a source can answer for, in the words a reader picking one
10
11
  // needs: NCBI's ortholog sets stop at vertebrates and insects, PANTHER's run
11
- // from human to yeast and Arabidopsis.
12
+ // from human to yeast and Arabidopsis, and a UniRef cluster is every UniProtKB
13
+ // entry within 50% identity of the query, whatever it came from.
12
14
  const hints = {
13
15
  ncbi: 'vertebrates and insects',
14
16
  panther: 'also yeast, worm, fly and plants',
17
+ uniref: 'all of UniProtKB within 50% identity',
15
18
  };
16
19
  export default function OrthologSourceSelect({ value, onChange, className, }) {
17
20
  return (React.createElement(TextField2, { variant: "outlined", label: "Source", className: className, select: true, value: value, helperText: hints[value], onChange: event => {
@@ -10,6 +10,19 @@ const useStyles = makeStyles()({
10
10
  marginTop: 10,
11
11
  },
12
12
  });
13
+ /**
14
+ * Where a trimmed row sits in the protein. The offset is what the view maps
15
+ * through, so the alert says it rather than leaving the user to wonder why the
16
+ * row is shorter than their transcript.
17
+ */
18
+ function partialCoverage({ identity, offset, }) {
19
+ const covering = `, covering ${Math.round(identity * 100)}% of it`;
20
+ return offset > 0
21
+ ? `${covering} from residue ${offset + 1}`
22
+ : offset < 0
23
+ ? `${covering} and running ${-offset} residues past its start`
24
+ : covering;
25
+ }
13
26
  /**
14
27
  * Which MSA row corresponds to the selected transcript. Clicking and hovering in
15
28
  * the alignment reach the genome only through this name, and a wrong one fails
@@ -27,12 +40,15 @@ export default function QueryRowSelector({ names, detected, querySeqName, setQue
27
40
  detected?.name === name ? ' — matches your protein' : ''))))) : (React.createElement(TextField2, { variant: "outlined", label: "MSA row matching the selected transcript", fullWidth: true, className: classes.field, helperText: "Paste an alignment above and this fills in on its own", value: querySeqName, onChange: event => {
28
41
  setQuerySeqName(event.target.value);
29
42
  } })),
30
- isAutoDetected && detected ? (React.createElement(Alert, { severity: "success", className: classes.alert },
43
+ isAutoDetected && detected ? (detected.quality === 'similar' ? (React.createElement(Alert, { severity: "warning", className: classes.alert },
44
+ React.createElement("strong", null, detected.name),
45
+ " is",
46
+ ' ',
47
+ Math.round(detected.identity * 100),
48
+ "% identical to your protein but is not the same sequence. Clicking the alignment navigates the genome view, approximately: wherever the two differ by an insertion or a deletion, every residue after it lands one codon off.")) : (React.createElement(Alert, { severity: "success", className: classes.alert },
31
49
  "Matched ",
32
50
  React.createElement("strong", null, detected.name),
33
51
  " to your protein sequence",
34
- detected.quality === 'exact'
35
- ? ''
36
- : `, covering ${Math.round(detected.identity * 100)}% of it`,
37
- ". Clicking the alignment will navigate the genome view.")) : names.length > 0 && !querySeqName ? (React.createElement(Alert, { severity: "warning", className: classes.alert }, "No row matched your protein sequence \u2014 pick the one for your gene above. Without it the alignment still renders, but clicking it will not navigate the genome view.")) : null));
52
+ detected.quality === 'exact' ? '' : partialCoverage(detected),
53
+ ". Clicking the alignment will navigate the genome view."))) : names.length > 0 && !querySeqName ? (React.createElement(Alert, { severity: "warning", className: classes.alert }, "No row matched your protein sequence \u2014 pick the one for your gene above. Without it the alignment still renders, but clicking it will not navigate the genome view.")) : null));
38
54
  }
@@ -18,6 +18,17 @@ export interface QueryRowMatch {
18
18
  quality: MatchQuality;
19
19
  /** identity over the compared region, 0-1 */
20
20
  identity: number;
21
+ /**
22
+ * Transcript residues before the row's first residue, so transcript position
23
+ * = row position + offset. BLAST reports the aligned region, so a row is
24
+ * often the query with its termini trimmed, and mapping its first residue to
25
+ * the transcript's first residue would put every navigation off by the
26
+ * trimmed amount. Negative when the row is the longer of the two.
27
+ *
28
+ * Zero for a `similar` match, which is the one arm this cannot measure: the
29
+ * row is not the query, so where it starts is a guess.
30
+ */
31
+ offset: number;
21
32
  }
22
33
  export interface MsaQueryRow {
23
34
  /** every row name, in file order, for the picker to offer */
@@ -70,20 +70,27 @@ function bestMatch(parsed, names, query) {
70
70
  }
71
71
  if (row === query) {
72
72
  // nothing beats an exact match, and a second one would be a duplicate row
73
- return { name, quality: 'exact', identity: 1 };
73
+ return { name, quality: 'exact', identity: 1, offset: 0 };
74
74
  }
75
75
  // BLAST reports the aligned region, so the row is often the query trimmed
76
76
  // at one or both ends rather than the whole protein
77
- if (query.includes(row) || row.includes(query)) {
77
+ const contained = query.indexOf(row);
78
+ const containing = row.indexOf(query);
79
+ if (contained !== -1 || containing !== -1) {
78
80
  const coverage = Math.min(row.length, query.length) / Math.max(row.length, query.length);
79
81
  if (coverage >= PARTIAL_COVERAGE_FLOOR) {
80
- candidates.push({ name, quality: 'partial', identity: coverage });
82
+ candidates.push({
83
+ name,
84
+ quality: 'partial',
85
+ identity: coverage,
86
+ offset: contained === -1 ? -containing : contained,
87
+ });
81
88
  }
82
89
  continue;
83
90
  }
84
91
  const identity = identityOverOverlap(row, query);
85
92
  if (identity >= SIMILARITY_FLOOR) {
86
- candidates.push({ name, quality: 'similar', identity });
93
+ candidates.push({ name, quality: 'similar', identity, offset: 0 });
87
94
  }
88
95
  }
89
96
  // an exact match returns above, so only these two can be here
@@ -33,6 +33,36 @@ describe('findQueryRow', () => {
33
33
  quality: 'partial',
34
34
  });
35
35
  });
36
+ // the offset is what the view maps genome coordinates through: without it an
37
+ // N-terminally trimmed row puts every click five codons off, silently
38
+ test('says how far into the protein a trimmed row starts', () => {
39
+ const trimmed = `>aligned_query\n${protein.slice(5, 40)}\n>hit_one\nWRONGWRONGWRONGWRONG\n`;
40
+ expect(findQueryRow(trimmed, protein).match).toMatchObject({
41
+ name: 'aligned_query',
42
+ quality: 'partial',
43
+ offset: 5,
44
+ });
45
+ });
46
+ test('an exact match starts where the protein does', () => {
47
+ expect(findQueryRow(clustal, protein).match?.offset).toBe(0);
48
+ });
49
+ test('a row running past the protein gets a negative offset', () => {
50
+ const extended = `>longer_row\nMMM${protein}\n`;
51
+ expect(findQueryRow(extended, protein).match).toMatchObject({
52
+ name: 'longer_row',
53
+ quality: 'partial',
54
+ offset: -3,
55
+ });
56
+ });
57
+ // the one arm with nothing to measure: a 90%-identical row is not the query,
58
+ // so QueryRowSelector warns rather than claiming a match
59
+ test('a merely similar row claims no offset', () => {
60
+ const similar = protein.slice(0, 30) + 'W' + protein.slice(31);
61
+ expect(findQueryRow(`>near\n${similar}\n`, protein).match).toMatchObject({
62
+ quality: 'similar',
63
+ offset: 0,
64
+ });
65
+ });
36
66
  // the failure that matters: silently wiring the view to a homolog would look
37
67
  // like it worked, and every navigation afterwards would land in the wrong place
38
68
  test('returns nothing when only diverged homologs are present', () => {
@@ -10,6 +10,7 @@ export declare function useQueryRowName(msaText: string, proteinSequence: string
10
10
  detected: import("./detectQueryRow").QueryRowMatch | undefined;
11
11
  names: string[];
12
12
  querySeqName: string;
13
+ querySeqOffset: number;
13
14
  setQuerySeqName: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
14
15
  isAutoDetected: boolean;
15
16
  };
@@ -13,10 +13,14 @@ export function useQueryRowName(msaText, proteinSequence) {
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
15
  const { names, match } = useMemo(() => findQueryRow(msaText, proteinSequence), [msaText, proteinSequence]);
16
+ const querySeqName = override ?? match?.name ?? '';
16
17
  return {
17
18
  detected: match,
18
19
  names,
19
- querySeqName: override ?? match?.name ?? '',
20
+ querySeqName,
21
+ // only the detected row's offset is known; a row the user picked instead is
22
+ // taken at face value, residue 1 to residue 1
23
+ querySeqOffset: querySeqName === match?.name ? match.offset : 0,
20
24
  setQuerySeqName: setOverride,
21
25
  isAutoDetected: override === undefined && !!match,
22
26
  };
@@ -0,0 +1,23 @@
1
+ // @vitest-environment jsdom
2
+ import { act, renderHook } from '@testing-library/react';
3
+ import { describe, expect, test } from 'vitest';
4
+ import { useQueryRowName } from './useQueryRowName';
5
+ const protein = 'MKWVTFISLLLLFSSAYSRGVFRRDTHKSEIAHRFKDLGEEHFKGLVLIAFSQYLQQCPFD';
6
+ const trimmed = `>aligned_query\n${protein.slice(5, 45)}\n>other\nWRONGWRONGWRONGWRONGWRONGWRONGWRONGWRONG\n`;
7
+ describe('useQueryRowName', () => {
8
+ test('carries the detected row and where it starts', () => {
9
+ const { result } = renderHook(() => useQueryRowName(trimmed, protein));
10
+ expect(result.current.querySeqName).toBe('aligned_query');
11
+ expect(result.current.querySeqOffset).toBe(5);
12
+ });
13
+ // the offset was measured against the detected row; another row is a row the
14
+ // user vouched for, and nothing here knows where in the protein it sits
15
+ test('a row the user picks instead is taken at face value', () => {
16
+ const { result } = renderHook(() => useQueryRowName(trimmed, protein));
17
+ act(() => {
18
+ result.current.setQuerySeqName('other');
19
+ });
20
+ expect(result.current.querySeqName).toBe('other');
21
+ expect(result.current.querySeqOffset).toBe(0);
22
+ });
23
+ });
@@ -1,15 +1,16 @@
1
1
  import { launchMsaView } from '../utils/launchMsaView';
2
2
  export default function LaunchMsaViewExtensionPointF(pluginManager) {
3
3
  pluginManager.addToExtensionPoint('LaunchView-MsaView', (args) => {
4
- const { session, data, msaFileLocation, msaIndexedLocation, msaName, treeFileLocation, querySeqName, ...rest } = args;
5
- // `orthologParams` is a fourth source, and unlike the other three it names
6
- // no alignment at all — the view builds one from NCBI at launch, which is
7
- // the dialog's Orthologs tab reached declaratively.
4
+ const { session, data, msaFileLocation, msaIndexedLocation, msaName, treeFileLocation, querySeqName, searchParams, ...rest } = args;
5
+ // `orthologParams` and `searchParams` name no alignment at all — the
6
+ // view builds one at launch, which is the dialog's Orthologs and BLAST
7
+ // tabs reached declaratively.
8
8
  if (!data &&
9
9
  !msaFileLocation &&
10
10
  !msaIndexedLocation &&
11
- !rest.orthologParams) {
12
- throw new Error('No MSA data, file location or orthologParams provided when launching MSA view');
11
+ !rest.orthologParams &&
12
+ !searchParams) {
13
+ throw new Error('No MSA data, file location, orthologParams or searchParams provided when launching MSA view');
13
14
  }
14
15
  // inline data and the tree URL are native react-msaview snapshot props, set
15
16
  // directly, and so is orthologParams (the model's own autorun picks it up).
@@ -27,6 +28,7 @@ export default function LaunchMsaViewExtensionPointF(pluginManager) {
27
28
  };
28
29
  launchMsaView(session, {
29
30
  ...rest,
31
+ ...(searchParams ? { blastParams: searchParams } : {}),
30
32
  data,
31
33
  ...(treeFileLocation
32
34
  ? {
@@ -32,6 +32,27 @@ test('a file location travels through init', () => {
32
32
  querySeqName: undefined,
33
33
  });
34
34
  });
35
+ test('searchParams is a source, stored under the name the dialog uses', () => {
36
+ const snapshot = launch({
37
+ searchParams: {
38
+ searchProgram: 'phmmer',
39
+ blastDatabase: 'rp15',
40
+ accession: 'P04637',
41
+ },
42
+ connectedViewId: 'lgv1',
43
+ connectedTranscript: 'NM_000546.6',
44
+ });
45
+ expect(snapshot.blastParams).toEqual({
46
+ searchProgram: 'phmmer',
47
+ blastDatabase: 'rp15',
48
+ accession: 'P04637',
49
+ });
50
+ expect('searchParams' in snapshot).toBe(false);
51
+ expect(snapshot.connectedTranscript).toBe('NM_000546.6');
52
+ });
53
+ test('a launch naming no source at all is refused', () => {
54
+ expect(() => launch({ connectedViewId: 'lgv1' })).toThrow(/searchParams/);
55
+ });
35
56
  test('one field set is enough to need init', () => {
36
57
  const snapshot = launch({ data: { msa: '>a\nMEEP' }, querySeqName: 'QUERY' });
37
58
  expect(snapshot.init).toEqual({
@@ -12,8 +12,9 @@ export declare function loadStoredData(self: JBrowsePluginMsaViewModel): void;
12
12
  * it is recorded whether or not the write succeeded, so a browser refusing
13
13
  * IndexedDB (private mode) fails once rather than in a loop.
14
14
  *
15
- * A view whose data comes from a filehandle stores nothing at all: the file is
16
- * the source of truth and react-msaview refetches it at startup.
15
+ * A view whose data comes from a filehandle -- or from the indexed block its
16
+ * kept `init` names -- stores nothing at all: the file is the source of truth
17
+ * and it is refetched at startup.
17
18
  */
18
19
  export declare function storeDataToIndexedDB(self: JBrowsePluginMsaViewModel): void;
19
20
  /**