jbrowse-plugin-protein3d 0.13.2 → 0.15.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 (95) hide show
  1. package/README.md +73 -62
  2. package/dist/AddHighlightModel/GenomeTo1DProteinHoverHighlight.js +2 -2
  3. package/dist/AddHighlightModel/HighlightComponents.js +3 -3
  4. package/dist/AddHighlightModel/Protein1DToGenomeHoverHighlight.js +6 -13
  5. package/dist/AddHighlightModel/ProteinToGenomeHighlight.js +21 -6
  6. package/dist/LaunchProteinView/components/AlphaFoldDBSearch.js +7 -2
  7. package/dist/LaunchProteinView/components/FoldseekSearch.js +13 -5
  8. package/dist/LaunchProteinView/components/LaunchProteinViewDialog.js +5 -5
  9. package/dist/LaunchProteinView/components/PdbSearch.js +2 -1
  10. package/dist/LaunchProteinView/components/UserProvidedStructure.js +7 -2
  11. package/dist/LaunchProteinView/hooks/useAlphaFoldDBSearch.js +2 -1
  12. package/dist/LaunchProteinView/hooks/useTranscriptIsoformSelection.js +2 -1
  13. package/dist/LaunchProteinView/hooks/useTranscriptSelection.js +15 -3
  14. package/dist/LaunchProteinView/index.js +9 -3
  15. package/dist/LaunchProteinView/services/foldseekApi.js +17 -6
  16. package/dist/LaunchProteinView/utils/calculateProteinSequence.js +19 -7
  17. package/dist/LaunchProteinViewExtensionPoint/resolveShortLaunch.js +41 -25
  18. package/dist/Protein1DLinkage/index.js +15 -4
  19. package/dist/Protein1DLinkage/linkage.js +11 -15
  20. package/dist/ProteinView/autoScroll.js +24 -23
  21. package/dist/ProteinView/components/FeatureBar.js +1 -6
  22. package/dist/ProteinView/components/FeatureTypeLabel.js +2 -5
  23. package/dist/ProteinView/components/ProteinAlignment.js +14 -33
  24. package/dist/ProteinView/components/ProteinFeatureTrack.js +2 -2
  25. package/dist/ProteinView/components/ProteinViewHeader.js +1 -1
  26. package/dist/ProteinView/components/SplitString.js +3 -21
  27. package/dist/ProteinView/entityAlignedTo.js +37 -0
  28. package/dist/ProteinView/frameSelection.js +34 -22
  29. package/dist/ProteinView/model.js +9 -20
  30. package/dist/ProteinView/proteinToGenomeMapping.js +38 -69
  31. package/dist/ProteinView/residueRanges.js +69 -0
  32. package/dist/ProteinView/structureModel.js +237 -91
  33. package/dist/ProteinView/subscribeMolstarInteraction.js +5 -8
  34. package/dist/ProteinView/viewInteractions.js +9 -7
  35. package/dist/jbrowse-plugin-protein3d.umd.production.min.js +15 -15
  36. package/dist/jbrowse-plugin-protein3d.umd.production.min.js.map +4 -4
  37. package/dist/mappings.js +23 -1
  38. package/dist/{molstar-chunk-MJTX2EDA.js → molstar-chunk-EKIOVJTB.js} +2 -2
  39. package/dist/{molstar-chunk-MJTX2EDA.js.map → molstar-chunk-EKIOVJTB.js.map} +4 -4
  40. package/dist/version.js +1 -1
  41. package/package.json +12 -11
  42. package/src/AddHighlightModel/GenomeTo1DProteinHoverHighlight.tsx +5 -2
  43. package/src/AddHighlightModel/HighlightComponents.tsx +3 -6
  44. package/src/AddHighlightModel/Protein1DToGenomeHoverHighlight.tsx +15 -20
  45. package/src/AddHighlightModel/ProteinToGenomeHighlight.tsx +43 -29
  46. package/src/LaunchProteinView/components/AlphaFoldDBSearch.tsx +8 -1
  47. package/src/LaunchProteinView/components/FoldseekSearch.tsx +18 -3
  48. package/src/LaunchProteinView/components/LaunchProteinViewDialog.tsx +7 -0
  49. package/src/LaunchProteinView/components/PdbSearch.tsx +3 -0
  50. package/src/LaunchProteinView/components/UserProvidedStructure.tsx +8 -1
  51. package/src/LaunchProteinView/hooks/useAlphaFoldDBSearch.ts +3 -0
  52. package/src/LaunchProteinView/hooks/useTranscriptIsoformSelection.ts +4 -0
  53. package/src/LaunchProteinView/hooks/useTranscriptSelection.test.ts +41 -0
  54. package/src/LaunchProteinView/hooks/useTranscriptSelection.ts +27 -2
  55. package/src/LaunchProteinView/index.ts +15 -8
  56. package/src/LaunchProteinView/menuTarget.test.ts +68 -0
  57. package/src/LaunchProteinView/services/foldseekApi.test.ts +28 -0
  58. package/src/LaunchProteinView/services/foldseekApi.ts +23 -10
  59. package/src/LaunchProteinView/utils/calculateProteinSequence.ts +22 -5
  60. package/src/LaunchProteinViewExtensionPoint/index.ts +10 -9
  61. package/src/LaunchProteinViewExtensionPoint/resolveShortLaunch.test.ts +131 -0
  62. package/src/LaunchProteinViewExtensionPoint/resolveShortLaunch.ts +59 -32
  63. package/src/Protein1DLinkage/index.ts +17 -6
  64. package/src/Protein1DLinkage/linkage.test.ts +33 -18
  65. package/src/Protein1DLinkage/linkage.ts +16 -20
  66. package/src/ProteinView/autoScroll.test.ts +68 -13
  67. package/src/ProteinView/autoScroll.ts +47 -23
  68. package/src/ProteinView/components/FeatureBar.tsx +1 -6
  69. package/src/ProteinView/components/FeatureTypeLabel.tsx +1 -8
  70. package/src/ProteinView/components/ProteinAlignment.tsx +14 -43
  71. package/src/ProteinView/components/ProteinFeatureTrack.tsx +0 -3
  72. package/src/ProteinView/components/ProteinViewHeader.tsx +1 -0
  73. package/src/ProteinView/components/SplitString.tsx +12 -27
  74. package/src/ProteinView/entityAlignedTo.test.ts +56 -0
  75. package/src/ProteinView/entityAlignedTo.ts +60 -0
  76. package/src/ProteinView/frameSelection.test.ts +20 -6
  77. package/src/ProteinView/frameSelection.ts +67 -35
  78. package/src/ProteinView/geneExplorerLinkage.test.ts +64 -20
  79. package/src/ProteinView/model.test.ts +9 -7
  80. package/src/ProteinView/model.ts +9 -22
  81. package/src/ProteinView/proteinToGenomeMapping.ts +46 -97
  82. package/src/ProteinView/proteinViewSpec.ts +6 -4
  83. package/src/ProteinView/residueRanges.test.ts +100 -0
  84. package/src/ProteinView/residueRanges.ts +98 -0
  85. package/src/ProteinView/structureModel.test.ts +321 -13
  86. package/src/ProteinView/structureModel.ts +282 -118
  87. package/src/ProteinView/subscribeMolstarInteraction.test.ts +32 -0
  88. package/src/ProteinView/subscribeMolstarInteraction.ts +24 -2
  89. package/src/ProteinView/viewInteractions.test.ts +30 -13
  90. package/src/ProteinView/viewInteractions.ts +8 -7
  91. package/src/mappings.ts +26 -1
  92. package/src/version.ts +1 -1
  93. package/dist/AddHighlightModel/ProteinToGenomeHighlightInner.js +0 -26
  94. package/src/AddHighlightModel/ProteinToGenomeHighlightInner.tsx +0 -58
  95. package/src/ProteinView/components/matchRuns.test.ts +0 -26
package/README.md CHANGED
@@ -1,40 +1,81 @@
1
1
  # jbrowse-plugin-protein3d
2
2
 
3
- This is a 3-D protein structure viewer for JBrowse 2
3
+ A 3-D protein structure viewer for JBrowse 2, linked residue by residue to the
4
+ genome.
5
+
6
+ ![TP53 beside PDB 1TUP, p53's core domain bound to DNA, with the R248 hotspot selected](https://raw.githubusercontent.com/GMOD/jbrowse-plugin-protein3d/readme-figure/readme.png)
7
+
8
+ TP53 beside PDB 1TUP, p53's core domain bound to DNA. R248, a cancer hotspot, is
9
+ selected in magenta on the structure, and its codon is marked on the gene.
10
+ [Open this session](https://jbrowse.org/code/jb2/main/?config=test_data/protein3d_config.json&session=spec-%7B%22views%22%3A%5B%7B%22type%22%3A%22ProteinView%22%2C%22structures%22%3A%5B%7B%22pdbId%22%3A%221TUP%22%2C%22initialResidues%22%3A%7B%22start%22%3A248%2C%22end%22%3A248%7D%7D%5D%2C%22transcriptId%22%3A%22NM_000546.6%22%2C%22sideBySide%22%3Atrue%2C%22zoomToBaseLevel%22%3Afalse%2C%22colorScheme%22%3A%22mapped-chain%22%2C%22connectedView%22%3A%7B%22assembly%22%3A%22hg38%22%2C%22loc%22%3A%22chr17%3A7%2C673%2C700-7%2C674%2C700%22%2C%22tracks%22%3A%5B%7B%22trackId%22%3A%22hg38-ncbiRefSeq%22%2C%22geneGlyphMode%22%3A%22longestCoding%22%7D%2C%22clinvar_ncbi_hg38%22%5D%7D%7D%5D%7D).
11
+ CI re-renders the figure from `main` every hour
12
+ ([scripts/readme-figure.mjs](scripts/readme-figure.mjs)).
13
+
14
+ Right-click a gene and choose to open its protein structure. The plugin looks up
15
+ the AlphaFold model through UniProt, lists the experimental PDB entries SIFTS
16
+ maps to that UniProt entry, and can search Foldseek for related structures. It
17
+ aligns each structure to the transcript's translation, so hovering a residue
18
+ highlights its codon and hovering a codon highlights its residue.
19
+
20
+ ## Try it
21
+
22
+ The [protein browser](https://staging.genomes.jbrowse.org/protein-browser/)
23
+ takes a gene name and opens it in JBrowse with its structure linked to the
24
+ genome. The plugin is also installed by default on
25
+ [genomes.jbrowse.org](https://genomes.jbrowse.org), so it works for any species
26
+ there.
27
+
28
+ ## Documentation
29
+
30
+ Using the plugin:
31
+
32
+ - Tutorials on jbrowse.org:
33
+ [TP53 from prediction to crystal](https://jbrowse.org/jb2/docs/tutorials/tp53_structures/)
34
+ opens an AlphaFold model and two crystal structures superposed beside the
35
+ gene, and reads the R248 hotspot back to its codon;
36
+ [Proteins on genomes.jbrowse.org](https://jbrowse.org/jb2/docs/tutorials/genomes_proteins/)
37
+ launches a structure and an MSA from any gene's right-click menu.
38
+ - [Demos](docs/demos.md): structures that are easy to map wrong, each one link
39
+ away — a peptide bound to a larger partner, a protein bound to DNA, a receptor
40
+ with another protein fused into it, a phosphorylated residue, and a
41
+ mitochondrial protein.
42
+ - [Your own structures](docs/your-own-structures.md): opening a model you folded
43
+ yourself (ColabFold, AlphaFold 3, Boltz…) instead of the AlphaFold DB one,
44
+ from a file, a URL or a generated link per gene, and which features carry
45
+ over.
46
+
47
+ Linking and embedding:
48
+
49
+ - [Launching from a URL or code](docs/launching.md): worked session-spec links,
50
+ the short `uniprotId`/`pdbId` + `transcriptId` form, several structures in one
51
+ view, and the `LaunchView-ProteinView` extension point.
52
+ - [Launch parameters](docs/launch-parameters.md): every argument, the
53
+ `connectedView` settings, and the transcript `feature` shape.
54
+ - [Session snapshots](docs/session-snapshots.md): the saved view's shape, the
55
+ structure shorthands, and which chain maps.
56
+
57
+ How it works, for someone extending the plugin or checking what a number on
58
+ screen means:
4
59
 
5
- The major workflow enabled by this is
6
-
7
- - Right click gene of interest -> launch 3-D protein viewer with linked
8
- mouseover between genome and structure
9
-
10
- It has features to automatically look up a protein structure of interest using
11
- the UniProt ID mapping API to connect to AlphaFoldDB, lists the experimental PDB
12
- entries SIFTS maps to that UniProt entry, and can also use Foldseek to look up
13
- related structures
14
-
15
- ## Coordinate-mapping harness
16
-
17
- A standalone diagnostic page that loads real PDB / AlphaFold structures through
18
- the plugin's actual mapping code and surfaces cases it mishandles (multi-chain
19
- complexes, partial/repeat structures, AlphaFold fragments):
20
-
21
- https://gmod.org/jbrowse-plugin-protein3d/
22
-
23
- Source and details in [harness/](harness/).
24
-
25
- ## Screenshot
26
-
27
- ![](img/1.png)
28
-
29
- Example at
30
- https://jbrowse.org/code/jb2/latest/?config=%2Fucsc%2Fhg38%2Fconfig.json&session=share-aZOIjR_qs4&password=NT4sa
60
+ - [Genome to structure alignment](docs/genome-to-structure-alignment.md): why
61
+ the plugin aligns the transcript's translation to the structure on the fly,
62
+ the precedent for that in SIFTS and G2S, how it picks the chain and isoform,
63
+ and what sequence alignment cannot decide.
64
+ - [UniProt feature tracks](docs/uniprot-feature-tracks.md): where the accession
65
+ and the UniProt-to-structure offset come from for AlphaFold models, PDB
66
+ entries and your own models.
67
+ - [Residue numbering](docs/residue-numbering.md): how a paper's R248 becomes
68
+ position 154 in the file, `label_seq_id` 155 for Mol\*, and the codon on
69
+ chr17, and how a session spec names a residue the literature's way.
31
70
 
32
- ## Demos
71
+ Working on the plugin:
33
72
 
34
- [Structures that are easy to map wrong](docs/demos.md), each one link away: a
35
- peptide bound to a larger partner, a protein bound to DNA, a receptor with
36
- another protein fused into it, a phosphorylated residue, and a mitochondrial
37
- protein.
73
+ - [DEVELOPERS.md](DEVELOPERS.md): running it locally and publishing.
74
+ - [Testing](docs/testing.md): the unit, e2e, documentation and demo checks.
75
+ - [Host compatibility](docs/host-compatibility.md): keeping the published bundle
76
+ working on JBrowse releases years old.
77
+ - [Live checks](docs/live-checks.md): serving a local build to a session on
78
+ jbrowse.org.
38
79
 
39
80
  ## Publication
40
81
 
@@ -46,33 +87,3 @@ Alignments, and Structures With Nucleotide-level Evidence in JBrowse 2. Journal
46
87
  of Molecular Biology, 169645. https://doi.org/10.1016/j.jmb.2026.169645
47
88
 
48
89
  See also https://github.com/GMOD/proteinbrowser for overview
49
-
50
- ## Availability
51
-
52
- This plugin is installed by default on https://genomes.jbrowse.org so you can
53
- use it on any species there
54
-
55
- ## Programmatic usage
56
-
57
- See [DEVELOPERS.md](DEVELOPERS.md)
58
-
59
- ## How it works
60
-
61
- Notes on the parts that are easy to get subtly wrong, written for someone
62
- extending the plugin or checking what a number on screen means:
63
-
64
- - [Genome to structure alignment](docs/genome-to-structure-alignment.md): why
65
- the plugin aligns the transcript's translation to the structure on the fly,
66
- the precedent for that in SIFTS and G2S, how it picks the chain and isoform,
67
- and what sequence alignment cannot decide.
68
- - [Residue numbering](docs/residue-numbering.md): how a paper's R248 becomes
69
- position 154 in the file, `label_seq_id` 155 for Mol\*, and the codon on
70
- chr17, and how a session spec names a residue the literature's way.
71
-
72
- ## Publishing
73
-
74
- just push a new tag using e.g.
75
-
76
- ```
77
- pnpm version minor
78
- ```
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import { getSession } from '@jbrowse/core/util';
3
3
  import { observer } from 'mobx-react';
4
4
  import Highlight from './Highlight';
5
- import { getProteinLinkage, linkageGenomeMapping } from '../Protein1DLinkage';
5
+ import { getProteinLinkage, getProteinLinkageMapping, } from '../Protein1DLinkage';
6
6
  import { genomeHoverToTranscriptPos } from '../ProteinView/util';
7
7
  const GenomeTo1DProteinHoverHighlight = observer(function GenomeTo1DProteinHoverHighlight({ model, }) {
8
8
  const session = getSession(model);
@@ -13,7 +13,7 @@ const GenomeTo1DProteinHoverHighlight = observer(function GenomeTo1DProteinHover
13
13
  if (!assemblyName || !linkage) {
14
14
  return null;
15
15
  }
16
- const proteinPos = genomeHoverToTranscriptPos(hovered, linkageGenomeMapping(linkage));
16
+ const proteinPos = genomeHoverToTranscriptPos(hovered, getProteinLinkageMapping(model));
17
17
  if (proteinPos === undefined) {
18
18
  return null;
19
19
  }
@@ -3,11 +3,11 @@ import { observer } from 'mobx-react';
3
3
  import GenomeMouseoverHighlight from './GenomeMouseoverHighlight';
4
4
  import GenomeTo1DProteinHoverHighlight from './GenomeTo1DProteinHoverHighlight';
5
5
  import Protein1DToGenomeHoverHighlight from './Protein1DToGenomeHoverHighlight';
6
- import { ProteinToGenomeClickHighlight, ProteinToGenomeHoverHighlight, } from './ProteinToGenomeHighlight';
6
+ import ProteinToGenomeHighlight from './ProteinToGenomeHighlight';
7
7
  const HighlightComponents = observer(function Highlight({ model, }) {
8
8
  return (React.createElement(React.Fragment, null,
9
- React.createElement(ProteinToGenomeClickHighlight, { model: model }),
10
- React.createElement(ProteinToGenomeHoverHighlight, { model: model }),
9
+ React.createElement(ProteinToGenomeHighlight, { model: model, field: "clickGenomeHighlights" }),
10
+ React.createElement(ProteinToGenomeHighlight, { model: model, field: "hoverGenomeHighlights" }),
11
11
  React.createElement(Protein1DToGenomeHoverHighlight, { model: model }),
12
12
  React.createElement(GenomeTo1DProteinHoverHighlight, { model: model }),
13
13
  React.createElement(GenomeMouseoverHighlight, { model: model })));
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import { getSession } from '@jbrowse/core/util';
3
3
  import { observer } from 'mobx-react';
4
4
  import Highlight from './Highlight';
5
- import { findProteinLinkedView, genomeHighlightForProteinPosition, getProteinLinkage, } from '../Protein1DLinkage';
5
+ import { findProteinLinkedView, genomeHighlightsForProteinPosition, getProteinLinkage, getProteinLinkageMapping, } from '../Protein1DLinkage';
6
6
  import { checkHovered } from '../ProteinView/util';
7
7
  // A hover on a 1D protein-annotation view names the UniProt entry as its
8
8
  // refName; this paints the codon on the genome view that 1D view was launched
@@ -15,20 +15,13 @@ const Protein1DToGenomeHoverHighlight = observer(function Protein1DToGenomeHover
15
15
  return null;
16
16
  }
17
17
  const { coord, refName } = hovered.hoverPosition;
18
- const linkage = getProteinLinkage(findProteinLinkedView(session, refName));
18
+ const linkedView = findProteinLinkedView(session, refName);
19
+ const linkage = getProteinLinkage(linkedView);
20
+ const mapping = getProteinLinkageMapping(linkedView);
19
21
  const assemblyName = assemblyNames[0];
20
- if (linkage?.connectedViewId !== viewId || !assemblyName) {
22
+ if (linkage?.connectedViewId !== viewId || !mapping || !assemblyName) {
21
23
  return null;
22
24
  }
23
- const genomeHighlight = genomeHighlightForProteinPosition(linkage, coord - 1);
24
- if (!genomeHighlight) {
25
- return null;
26
- }
27
- return (React.createElement(Highlight, { model: model, region: {
28
- start: genomeHighlight.start,
29
- end: genomeHighlight.end,
30
- refName: genomeHighlight.refName,
31
- assemblyName,
32
- } }));
25
+ return (React.createElement(React.Fragment, null, genomeHighlightsForProteinPosition(mapping, coord - 1).map(r => (React.createElement(Highlight, { key: r.start, model: model, region: { ...r, assemblyName } })))));
33
26
  });
34
27
  export default Protein1DToGenomeHoverHighlight;
@@ -1,9 +1,24 @@
1
1
  import React from 'react';
2
+ import { getSession } from '@jbrowse/core/util';
2
3
  import { observer } from 'mobx-react';
3
- import ProteinToGenomeHighlightInner from './ProteinToGenomeHighlightInner';
4
- export const ProteinToGenomeClickHighlight = observer(function ProteinToGenomeClickHighlight({ model, }) {
5
- return (React.createElement(ProteinToGenomeHighlightInner, { model: model, field: "clickGenomeHighlights" }));
6
- });
7
- export const ProteinToGenomeHoverHighlight = observer(function ProteinToGenomeHoverHighlight({ model, }) {
8
- return (React.createElement(ProteinToGenomeHighlightInner, { model: model, field: "hoverGenomeHighlights" }));
4
+ import Highlight from './Highlight';
5
+ import { getProteinViews, getStructuresConnectedTo } from './proteinViewLookup';
6
+ const ProteinToGenomeHighlight = observer(function ProteinToGenomeHighlight({ model, field, }) {
7
+ const session = getSession(model);
8
+ const { assemblyManager } = session;
9
+ const { assemblyNames, id: viewId } = model;
10
+ const assemblyName = assemblyNames[0];
11
+ const assembly = assemblyName ? assemblyManager.get(assemblyName) : undefined;
12
+ // Only structures that declare this genome view as their connection: the
13
+ // regions are transcript coordinates on that view's assembly, so painting
14
+ // them into any other genome view would place a highlight at coordinates
15
+ // that mean nothing there.
16
+ const structures = getStructuresConnectedTo(getProteinViews(session), viewId);
17
+ return assembly && assemblyName ? (React.createElement(React.Fragment, null, structures.flatMap((structure, idx) => structure[field].map((r, idx2) => (React.createElement(Highlight, { key: `${r.refName}-${r.start}-${r.end}-${idx}-${idx2}`, model: model, region: {
18
+ start: r.start,
19
+ end: r.end,
20
+ refName: r.refName,
21
+ assemblyName,
22
+ } })))))) : null;
9
23
  });
24
+ export default ProteinToGenomeHighlight;
@@ -28,9 +28,14 @@ const useStyles = makeStyles()({
28
28
  alignItems: 'flex-start',
29
29
  },
30
30
  });
31
- const AlphaFoldDBSearch = observer(function AlphaFoldDBSearch({ feature, session, view, handleClose, lookup, sideBySide, onSideBySideChange, }) {
31
+ const AlphaFoldDBSearch = observer(function AlphaFoldDBSearch({ feature, preferredTranscriptId, session, view, handleClose, lookup, sideBySide, onSideBySideChange, }) {
32
32
  const { classes } = useStyles();
33
- const state = useAlphaFoldDBSearch({ feature, view, lookup });
33
+ const state = useAlphaFoldDBSearch({
34
+ feature,
35
+ view,
36
+ lookup,
37
+ preferredTranscriptId,
38
+ });
34
39
  return (React.createElement(React.Fragment, null,
35
40
  React.createElement(DialogContent, { className: classes.dialogContent },
36
41
  state.error ? React.createElement(ErrorMessage, { error: state.error }) : null,
@@ -10,7 +10,7 @@ import PartialFailureNotice from './PartialFailureNotice';
10
10
  import TranscriptSelector from './TranscriptSelector';
11
11
  import useFoldseekSearch from '../hooks/useFoldseekSearch';
12
12
  import useTranscriptIsoformSelection from '../hooks/useTranscriptIsoformSelection';
13
- import { DEFAULT_DATABASES } from '../services/foldseekApi';
13
+ import { DEFAULT_DATABASES, foldseekLengthProblem, } from '../services/foldseekApi';
14
14
  const useStyles = makeStyles()({
15
15
  dialogContent: {
16
16
  width: '80em',
@@ -28,13 +28,17 @@ const useStyles = makeStyles()({
28
28
  alignItems: 'flex-start',
29
29
  },
30
30
  });
31
- const FoldseekSearch = observer(function FoldseekSearch({ feature, session, view, handleClose, }) {
31
+ const FoldseekSearch = observer(function FoldseekSearch({ feature, preferredTranscriptId, session, view, handleClose, }) {
32
32
  const { classes } = useStyles();
33
33
  const [userEditedSequence, setUserEditedSequence] = useState();
34
34
  const [selectedDatabases, setSelectedDatabases] = useState(DEFAULT_DATABASES);
35
35
  const [show3Di, setShow3Di] = useState(false);
36
36
  const { results, cleanedAaSequence, di3Sequence, isLoading, isPredicting, error, statusMessage, predictStructure, search, cancel, reset, } = useFoldseekSearch();
37
- const { transcripts, isoformSequences, isLoading: isLoadingIsoforms, error: isoformError, partialFailure: isoformPartialFailure, selectedTranscriptId: effectiveSelectedTranscriptId, setSelectedTranscriptId: setUserSelection, selectedTranscript, selectedIsoform: selectedIsoformData, } = useTranscriptIsoformSelection({ feature, view });
37
+ const { transcripts, isoformSequences, isLoading: isLoadingIsoforms, error: isoformError, partialFailure: isoformPartialFailure, selectedTranscriptId: effectiveSelectedTranscriptId, setSelectedTranscriptId: setUserSelection, selectedTranscript, selectedIsoform: selectedIsoformData, } = useTranscriptIsoformSelection({
38
+ feature,
39
+ view,
40
+ preferredTranscriptId,
41
+ });
38
42
  const cleanedSequence = selectedIsoformData
39
43
  ? stripAllStopCodons(selectedIsoformData.seq)
40
44
  : '';
@@ -53,7 +57,11 @@ const FoldseekSearch = observer(function FoldseekSearch({ feature, session, view
53
57
  invalidatePrediction();
54
58
  };
55
59
  const isBusy = isLoading || isPredicting;
56
- const canSearch = sequence.trim().length > 0 && selectedDatabases.length > 0 && !isBusy;
60
+ const lengthProblem = foldseekLengthProblem(sequence);
61
+ const canSearch = sequence.trim().length > 0 &&
62
+ !lengthProblem &&
63
+ selectedDatabases.length > 0 &&
64
+ !isBusy;
57
65
  // One button: predicting the 3Di alphabet is a step of the search, not a
58
66
  // decision, and making the user click twice only invited a stale prediction.
59
67
  const runSearch = async () => {
@@ -75,7 +83,7 @@ const FoldseekSearch = observer(function FoldseekSearch({ feature, session, view
75
83
  React.createElement(TextField, { label: "Protein sequence (amino acids)", multiline: true, rows: 4, value: sequence, onChange: e => {
76
84
  setUserEditedSequence(e.target.value);
77
85
  invalidatePrediction();
78
- }, placeholder: `MKTVRQERLKSIVRILERSKEPVSGAQLAEEL...`, disabled: isBusy, slotProps: {
86
+ }, placeholder: `MKTVRQERLKSIVRILERSKEPVSGAQLAEEL...`, disabled: isBusy, error: !!lengthProblem, helperText: lengthProblem, slotProps: {
79
87
  input: { className: classes.sequenceInput },
80
88
  } }))) : null,
81
89
  di3Sequence ? (React.createElement("div", { className: classes.di3Section },
@@ -10,7 +10,7 @@ import TabPanel from './TabPanel';
10
10
  import UserProvidedStructure from './UserProvidedStructure';
11
11
  import useUniProtIdLookup from '../hooks/useUniProtIdLookup';
12
12
  import { getLaunchSideBySide, setLaunchSideBySide } from '../utils/sideBySide';
13
- export default function LaunchProteinViewDialog({ handleClose, feature, model, }) {
13
+ export default function LaunchProteinViewDialog({ handleClose, feature, preferredTranscriptId, model, }) {
14
14
  const [choice, setChoice] = useState(0);
15
15
  const session = getSession(model);
16
16
  const view = getContainingView(model);
@@ -36,11 +36,11 @@ export default function LaunchProteinViewDialog({ handleClose, feature, model, }
36
36
  React.createElement(Tab, { value: 2, label: "Foldseek search" }),
37
37
  React.createElement(Tab, { value: 3, label: "File or URL" })),
38
38
  React.createElement(TabPanel, { value: choice, index: 0 },
39
- React.createElement(AlphaFoldDBSearch, { session: session, view: view, feature: feature, handleClose: handleClose, lookup: lookup, sideBySide: sideBySide, onSideBySideChange: changeSideBySide })),
39
+ React.createElement(AlphaFoldDBSearch, { session: session, view: view, feature: feature, preferredTranscriptId: preferredTranscriptId, handleClose: handleClose, lookup: lookup, sideBySide: sideBySide, onSideBySideChange: changeSideBySide })),
40
40
  React.createElement(TabPanel, { value: choice, index: 1 },
41
- React.createElement(PdbSearch, { session: session, view: view, feature: feature, handleClose: handleClose, lookup: lookup, sideBySide: sideBySide, onSideBySideChange: changeSideBySide })),
41
+ React.createElement(PdbSearch, { session: session, view: view, feature: feature, preferredTranscriptId: preferredTranscriptId, handleClose: handleClose, lookup: lookup, sideBySide: sideBySide, onSideBySideChange: changeSideBySide })),
42
42
  React.createElement(TabPanel, { value: choice, index: 2 },
43
- React.createElement(FoldseekSearch, { session: session, view: view, feature: feature, handleClose: handleClose })),
43
+ React.createElement(FoldseekSearch, { session: session, view: view, feature: feature, preferredTranscriptId: preferredTranscriptId, handleClose: handleClose })),
44
44
  React.createElement(TabPanel, { value: choice, index: 3 },
45
- React.createElement(UserProvidedStructure, { session: session, view: view, feature: feature, handleClose: handleClose }))));
45
+ React.createElement(UserProvidedStructure, { session: session, view: view, feature: feature, preferredTranscriptId: preferredTranscriptId, handleClose: handleClose }))));
46
46
  }
@@ -32,7 +32,7 @@ const useStyles = makeStyles()({
32
32
  // A crystal is usually a fragment, often with partners, so the view aligns
33
33
  // the transcript to it after launch rather than expecting a sequence match
34
34
  // here.
35
- const PdbSearch = observer(function PdbSearch({ feature, session, view, handleClose, lookup, sideBySide, onSideBySideChange, }) {
35
+ const PdbSearch = observer(function PdbSearch({ feature, preferredTranscriptId, session, view, handleClose, lookup, sideBySide, onSideBySideChange, }) {
36
36
  const { classes } = useStyles();
37
37
  const { uniprotId, isAutoMode, isLookupLoading } = lookup;
38
38
  const { entries, error: pdbError, isLoading: isPdbLoading, } = usePdbBestStructures(uniprotId);
@@ -66,6 +66,7 @@ const PdbSearch = observer(function PdbSearch({ feature, session, view, handleCl
66
66
  feature,
67
67
  view,
68
68
  structureSequences,
69
+ preferredTranscriptId,
69
70
  resetKey: uniprotId,
70
71
  });
71
72
  const loadingStatuses = [
@@ -28,7 +28,7 @@ function HelpText() {
28
28
  React.createElement(ExternalLink, { href: "https://github.com/sokrypton/ColabFold" }, "ColabFold"),
29
29
  ". This plugin will align the protein sequence calculated from the genome to the protein sequence embedded in the structure file which allows for slight differences in these two representations."));
30
30
  }
31
- const UserProvidedStructure = observer(function UserProvidedStructure({ feature, session, view, handleClose, }) {
31
+ const UserProvidedStructure = observer(function UserProvidedStructure({ feature, preferredTranscriptId, session, view, handleClose, }) {
32
32
  const { classes } = useStyles();
33
33
  const [file, setFile] = useState();
34
34
  const [choice, setChoice] = useState('file');
@@ -45,7 +45,12 @@ const UserProvidedStructure = observer(function UserProvidedStructure({ feature,
45
45
  const { transcripts: options, isoformSequences,
46
46
  // the chain the isoforms are compared against — not blindly chain 0, which
47
47
  // mismatched every heteromer the view itself went on to map correctly
48
- structureSequence, selectedTranscriptId: userSelection, setSelectedTranscriptId: setUserSelection, selectedTranscript, selectedIsoform: protein, error: isoformError, partialFailure: isoformPartialFailure, } = useTranscriptIsoformSelection({ feature, view, structureSequences });
48
+ structureSequence, selectedTranscriptId: userSelection, setSelectedTranscriptId: setUserSelection, selectedTranscript, selectedIsoform: protein, error: isoformError, partialFailure: isoformPartialFailure, } = useTranscriptIsoformSelection({
49
+ feature,
50
+ view,
51
+ structureSequences,
52
+ preferredTranscriptId,
53
+ });
49
54
  const error = isoformError ?? launchError ?? fileError;
50
55
  const canLaunch = !!(activeURL || activeFile) && !!protein && !!selectedTranscript;
51
56
  const sequencesDiffer = !!protein?.seq &&
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
2
2
  import { stripStopCodon } from 'p2s_mapper';
3
3
  import useAlphaFoldData from './useAlphaFoldData';
4
4
  import useTranscriptIsoformSelection from './useTranscriptIsoformSelection';
5
- export default function useAlphaFoldDBSearch({ feature, view, lookup, }) {
5
+ export default function useAlphaFoldDBSearch({ feature, view, lookup, preferredTranscriptId, }) {
6
6
  const { uniprotId, isAutoMode, isLookupLoading } = lookup;
7
7
  const { isLoading: isAlphaFoldLoading, isValidating: isAlphaFoldValidating, error: alphaFoldError, model, noModel, } = useAlphaFoldData({ uniprotId, feature, view });
8
8
  // a stable array, or the isoform picker realigns on every render
@@ -12,6 +12,7 @@ export default function useAlphaFoldDBSearch({ feature, view, lookup, }) {
12
12
  feature,
13
13
  view,
14
14
  structureSequences,
15
+ preferredTranscriptId,
15
16
  resetKey: model?.url,
16
17
  });
17
18
  const loadingStatuses = [
@@ -8,7 +8,7 @@ import { getId, isoformRecords } from '../utils/util';
8
8
  // list transcripts, fetch their protein sequences, pick which chain of the
9
9
  // structure to compare against, auto/manually select a transcript, and resolve
10
10
  // the selection back to its feature + sequence.
11
- export default function useTranscriptIsoformSelection({ feature, view, structureSequences, resetKey, }) {
11
+ export default function useTranscriptIsoformSelection({ feature, view, structureSequences, preferredTranscriptId, resetKey, }) {
12
12
  const transcripts = codingTranscripts(feature);
13
13
  const { isoformSequences, isLoading, error, partialFailure } = useIsoformProteinSequences({
14
14
  feature,
@@ -20,6 +20,7 @@ export default function useTranscriptIsoformSelection({ feature, view, structure
20
20
  options: transcripts,
21
21
  isoformSequences,
22
22
  structureSequence,
23
+ preferredTranscriptId,
23
24
  resetKey,
24
25
  });
25
26
  const selectedTranscript = transcripts.find(f => getId(f) === userSelection);
@@ -1,7 +1,17 @@
1
1
  import { useState } from 'react';
2
2
  import { selectBestTranscript } from 'p2s_mapper';
3
3
  import { rankableIsoforms } from '../utils/util';
4
- export default function useTranscriptSelection({ options, isoformSequences, structureSequence, resetKey, }) {
4
+ /**
5
+ * The isoform the user right-clicked when it translates, else the one whose
6
+ * protein best matches the structure.
7
+ */
8
+ export function defaultTranscriptId({ options, isoformSequences, structureSequence, preferredTranscriptId, }) {
9
+ const isoforms = rankableIsoforms(options, isoformSequences);
10
+ return isoforms.some(i => i.id === preferredTranscriptId && i.seq)
11
+ ? preferredTranscriptId
12
+ : selectBestTranscript({ isoforms, structureSequence });
13
+ }
14
+ export default function useTranscriptSelection({ options, isoformSequences, structureSequence, preferredTranscriptId, resetKey, }) {
5
15
  const [userSelection, setUserSelection] = useState();
6
16
  const [prevResetKey, setPrevResetKey] = useState(resetKey);
7
17
  if (resetKey !== prevResetKey) {
@@ -9,9 +19,11 @@ export default function useTranscriptSelection({ options, isoformSequences, stru
9
19
  setUserSelection(undefined);
10
20
  }
11
21
  const autoSelection = isoformSequences !== undefined
12
- ? selectBestTranscript({
13
- isoforms: rankableIsoforms(options, isoformSequences),
22
+ ? defaultTranscriptId({
23
+ options,
24
+ isoformSequences,
14
25
  structureSequence,
26
+ preferredTranscriptId,
15
27
  })
16
28
  : undefined;
17
29
  return { userSelection: userSelection ?? autoSelection, setUserSelection };
@@ -17,6 +17,7 @@ function canvasTarget(info, fetchFullFeature) {
17
17
  : {
18
18
  type,
19
19
  fetchFeature: () => fetchFullFeature(parentId, displayedRegionIndex),
20
+ preferredTranscriptId: subfeature?.featureId,
20
21
  };
21
22
  }
22
23
  // The hit test only carries a type, so a canvas host learns whether the gene
@@ -27,9 +28,13 @@ function legacyTarget(feature) {
27
28
  const type = root.get('type');
28
29
  return type === undefined || !isCodingFeature(root)
29
30
  ? undefined
30
- : { type, feature: root };
31
+ : {
32
+ type,
33
+ feature: root,
34
+ preferredTranscriptId: root === feature ? undefined : feature.id(),
35
+ };
31
36
  }
32
- function resolveTarget(self) {
37
+ export function resolveTarget(self) {
33
38
  const { contextMenuFeature, contextMenuInfo, fetchFullFeature } = self;
34
39
  return contextMenuInfo && fetchFullFeature
35
40
  ? canvasTarget(contextMenuInfo, fetchFullFeature)
@@ -40,10 +45,11 @@ function resolveTarget(self) {
40
45
  function launchProteinView(self, target) {
41
46
  const track = getContainingTrack(self);
42
47
  const session = getSession(track);
48
+ const { preferredTranscriptId } = target;
43
49
  const openDialog = (feature) => {
44
50
  session.queueDialog(handleClose => [
45
51
  LaunchProteinViewDialog,
46
- { model: track, handleClose, feature },
52
+ { model: track, handleClose, feature, preferredTranscriptId },
47
53
  ]);
48
54
  };
49
55
  if ('feature' in target) {
@@ -13,16 +13,27 @@ export const DEFAULT_DATABASES = [
13
13
  'pdb100',
14
14
  'afdb-swissprot',
15
15
  ];
16
- export async function predict3Di({ aaSequence, signal, }) {
17
- // Clean the sequence - remove FASTA header, whitespace, stop codons, and non-AA chars
18
- const cleanSequence = aaSequence
16
+ export const FOLDSEEK_MAX_RESIDUES = 1200;
17
+ export function cleanFoldseekSequence(aaSequence) {
18
+ return aaSequence
19
19
  .split('\n')
20
20
  .filter(line => !line.startsWith('>'))
21
21
  .join('')
22
- .replace(/\s/g, '')
23
- .replace(/\*/g, '') // Remove stop codons before querying 3Di
24
22
  .toUpperCase()
25
- .replace(/[^ACDEFGHIKLMNPQRSTVWY]/g, ''); // Keep only valid amino acids
23
+ .replace(/[^ACDEFGHIKLMNPQRSTVWY]/g, '');
24
+ }
25
+ export function foldseekLengthProblem(aaSequence) {
26
+ const { length } = cleanFoldseekSequence(aaSequence);
27
+ return length > FOLDSEEK_MAX_RESIDUES
28
+ ? `Foldseek's 3Di predictor takes at most ${FOLDSEEK_MAX_RESIDUES.toLocaleString('en-US')} residues and this sequence has ${length.toLocaleString('en-US')}. Trim it to the region you want to search.`
29
+ : undefined;
30
+ }
31
+ export async function predict3Di({ aaSequence, signal, }) {
32
+ const problem = foldseekLengthProblem(aaSequence);
33
+ if (problem) {
34
+ throw new Error(problem);
35
+ }
36
+ const cleanSequence = cleanFoldseekSequence(aaSequence);
26
37
  const url = `https://3di.foldseek.com/predict/${encodeURIComponent(cleanSequence)}`;
27
38
  const response = await rawfetch(url, { signal });
28
39
  if (!response.ok) {
@@ -1,18 +1,21 @@
1
1
  import { getConf } from '@jbrowse/core/configuration';
2
2
  import { revcom } from '@jbrowse/core/util';
3
3
  import { convertCodingSequenceToPeptides } from '@jbrowse/core/util/convertCodingSequenceToPeptides';
4
- import { getGeneticCode, parseTranslTable, } from '@jbrowse/core/util/geneticCodes';
4
+ import { getGeneticCode, parseTranslTable, relativizeTranslExcept, } from '@jbrowse/core/util/geneticCodes';
5
5
  import { isCDS } from '../codingFeature';
6
- export function calculateProteinSequence({ cds, sequence, geneticCodeId, }) {
7
- // `starts` is deliberately not passed: @jbrowse/core 4.3.0's signature has no
8
- // such parameter, so alternative initiators (GTG under table 11, ATA under
9
- // table 2) render as their internal residue rather than M. Core main added it;
10
- // pass it here when the @jbrowse/core floor reaches that release.
11
- const { codonTable } = getGeneticCode(geneticCodeId);
6
+ /**
7
+ * The translation core's feature panel shows: the genetic code's alternative
8
+ * initiators read as M, and a `transl_except` (RefSeq's selenocysteines) as its
9
+ * named residue.
10
+ */
11
+ export function calculateProteinSequence({ cds, sequence, geneticCodeId, translExcept, }) {
12
+ const { codonTable, starts } = getGeneticCode(geneticCodeId);
12
13
  return convertCodingSequenceToPeptides({
13
14
  cds,
14
15
  sequence,
15
16
  codonTable,
17
+ starts,
18
+ translExcept,
16
19
  });
17
20
  }
18
21
  function revlist(list, seqlen) {
@@ -51,10 +54,19 @@ export function getProteinSequence({ feature, seq, assemblyGeneticCodeId, }) {
51
54
  const geneticCodeId = parseTranslTable(feature.get('transl_table')) ??
52
55
  parseTranslTable(cdsSubfeature?.get('transl_table')) ??
53
56
  assemblyGeneticCodeId;
57
+ const rawTranslExcept = feature.get('transl_except') ?? cdsSubfeature?.get('transl_except');
54
58
  return calculateProteinSequence({
55
59
  cds: strand === -1 ? revlist(cds, seq.length) : cds,
56
60
  sequence: strand === -1 ? revcom(seq) : seq,
57
61
  geneticCodeId,
62
+ translExcept: rawTranslExcept
63
+ ? relativizeTranslExcept({
64
+ raw: rawTranslExcept,
65
+ featureStart,
66
+ featureLength: seq.length,
67
+ strand,
68
+ })
69
+ : undefined,
58
70
  });
59
71
  }
60
72
  /** The genome under one span, with the assembly's code for that contig. */