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.
- package/README.md +73 -62
- package/dist/AddHighlightModel/GenomeTo1DProteinHoverHighlight.js +2 -2
- package/dist/AddHighlightModel/HighlightComponents.js +3 -3
- package/dist/AddHighlightModel/Protein1DToGenomeHoverHighlight.js +6 -13
- package/dist/AddHighlightModel/ProteinToGenomeHighlight.js +21 -6
- package/dist/LaunchProteinView/components/AlphaFoldDBSearch.js +7 -2
- package/dist/LaunchProteinView/components/FoldseekSearch.js +13 -5
- package/dist/LaunchProteinView/components/LaunchProteinViewDialog.js +5 -5
- package/dist/LaunchProteinView/components/PdbSearch.js +2 -1
- package/dist/LaunchProteinView/components/UserProvidedStructure.js +7 -2
- package/dist/LaunchProteinView/hooks/useAlphaFoldDBSearch.js +2 -1
- package/dist/LaunchProteinView/hooks/useTranscriptIsoformSelection.js +2 -1
- package/dist/LaunchProteinView/hooks/useTranscriptSelection.js +15 -3
- package/dist/LaunchProteinView/index.js +9 -3
- package/dist/LaunchProteinView/services/foldseekApi.js +17 -6
- package/dist/LaunchProteinView/utils/calculateProteinSequence.js +19 -7
- package/dist/LaunchProteinViewExtensionPoint/resolveShortLaunch.js +41 -25
- package/dist/Protein1DLinkage/index.js +15 -4
- package/dist/Protein1DLinkage/linkage.js +11 -15
- package/dist/ProteinView/autoScroll.js +24 -23
- package/dist/ProteinView/components/FeatureBar.js +1 -6
- package/dist/ProteinView/components/FeatureTypeLabel.js +2 -5
- package/dist/ProteinView/components/ProteinAlignment.js +14 -33
- package/dist/ProteinView/components/ProteinFeatureTrack.js +2 -2
- package/dist/ProteinView/components/ProteinViewHeader.js +1 -1
- package/dist/ProteinView/components/SplitString.js +3 -21
- package/dist/ProteinView/entityAlignedTo.js +37 -0
- package/dist/ProteinView/frameSelection.js +34 -22
- package/dist/ProteinView/model.js +9 -20
- package/dist/ProteinView/proteinToGenomeMapping.js +38 -69
- package/dist/ProteinView/residueRanges.js +69 -0
- package/dist/ProteinView/structureModel.js +237 -91
- package/dist/ProteinView/subscribeMolstarInteraction.js +5 -8
- package/dist/ProteinView/viewInteractions.js +9 -7
- package/dist/jbrowse-plugin-protein3d.umd.production.min.js +15 -15
- package/dist/jbrowse-plugin-protein3d.umd.production.min.js.map +4 -4
- package/dist/mappings.js +23 -1
- package/dist/{molstar-chunk-MJTX2EDA.js → molstar-chunk-EKIOVJTB.js} +2 -2
- package/dist/{molstar-chunk-MJTX2EDA.js.map → molstar-chunk-EKIOVJTB.js.map} +4 -4
- package/dist/version.js +1 -1
- package/package.json +12 -11
- package/src/AddHighlightModel/GenomeTo1DProteinHoverHighlight.tsx +5 -2
- package/src/AddHighlightModel/HighlightComponents.tsx +3 -6
- package/src/AddHighlightModel/Protein1DToGenomeHoverHighlight.tsx +15 -20
- package/src/AddHighlightModel/ProteinToGenomeHighlight.tsx +43 -29
- package/src/LaunchProteinView/components/AlphaFoldDBSearch.tsx +8 -1
- package/src/LaunchProteinView/components/FoldseekSearch.tsx +18 -3
- package/src/LaunchProteinView/components/LaunchProteinViewDialog.tsx +7 -0
- package/src/LaunchProteinView/components/PdbSearch.tsx +3 -0
- package/src/LaunchProteinView/components/UserProvidedStructure.tsx +8 -1
- package/src/LaunchProteinView/hooks/useAlphaFoldDBSearch.ts +3 -0
- package/src/LaunchProteinView/hooks/useTranscriptIsoformSelection.ts +4 -0
- package/src/LaunchProteinView/hooks/useTranscriptSelection.test.ts +41 -0
- package/src/LaunchProteinView/hooks/useTranscriptSelection.ts +27 -2
- package/src/LaunchProteinView/index.ts +15 -8
- package/src/LaunchProteinView/menuTarget.test.ts +68 -0
- package/src/LaunchProteinView/services/foldseekApi.test.ts +28 -0
- package/src/LaunchProteinView/services/foldseekApi.ts +23 -10
- package/src/LaunchProteinView/utils/calculateProteinSequence.ts +22 -5
- package/src/LaunchProteinViewExtensionPoint/index.ts +10 -9
- package/src/LaunchProteinViewExtensionPoint/resolveShortLaunch.test.ts +131 -0
- package/src/LaunchProteinViewExtensionPoint/resolveShortLaunch.ts +59 -32
- package/src/Protein1DLinkage/index.ts +17 -6
- package/src/Protein1DLinkage/linkage.test.ts +33 -18
- package/src/Protein1DLinkage/linkage.ts +16 -20
- package/src/ProteinView/autoScroll.test.ts +68 -13
- package/src/ProteinView/autoScroll.ts +47 -23
- package/src/ProteinView/components/FeatureBar.tsx +1 -6
- package/src/ProteinView/components/FeatureTypeLabel.tsx +1 -8
- package/src/ProteinView/components/ProteinAlignment.tsx +14 -43
- package/src/ProteinView/components/ProteinFeatureTrack.tsx +0 -3
- package/src/ProteinView/components/ProteinViewHeader.tsx +1 -0
- package/src/ProteinView/components/SplitString.tsx +12 -27
- package/src/ProteinView/entityAlignedTo.test.ts +56 -0
- package/src/ProteinView/entityAlignedTo.ts +60 -0
- package/src/ProteinView/frameSelection.test.ts +20 -6
- package/src/ProteinView/frameSelection.ts +67 -35
- package/src/ProteinView/geneExplorerLinkage.test.ts +64 -20
- package/src/ProteinView/model.test.ts +9 -7
- package/src/ProteinView/model.ts +9 -22
- package/src/ProteinView/proteinToGenomeMapping.ts +46 -97
- package/src/ProteinView/proteinViewSpec.ts +6 -4
- package/src/ProteinView/residueRanges.test.ts +100 -0
- package/src/ProteinView/residueRanges.ts +98 -0
- package/src/ProteinView/structureModel.test.ts +321 -13
- package/src/ProteinView/structureModel.ts +282 -118
- package/src/ProteinView/subscribeMolstarInteraction.test.ts +32 -0
- package/src/ProteinView/subscribeMolstarInteraction.ts +24 -2
- package/src/ProteinView/viewInteractions.test.ts +30 -13
- package/src/ProteinView/viewInteractions.ts +8 -7
- package/src/mappings.ts +26 -1
- package/src/version.ts +1 -1
- package/dist/AddHighlightModel/ProteinToGenomeHighlightInner.js +0 -26
- package/src/AddHighlightModel/ProteinToGenomeHighlightInner.tsx +0 -58
- 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
|
-
|
|
3
|
+
A 3-D protein structure viewer for JBrowse 2, linked residue by residue to the
|
|
4
|
+
genome.
|
|
5
|
+
|
|
6
|
+

|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-

|
|
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
|
-
|
|
71
|
+
Working on the plugin:
|
|
33
72
|
|
|
34
|
-
[
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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,
|
|
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,
|
|
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
|
|
6
|
+
import ProteinToGenomeHighlight from './ProteinToGenomeHighlight';
|
|
7
7
|
const HighlightComponents = observer(function Highlight({ model, }) {
|
|
8
8
|
return (React.createElement(React.Fragment, null,
|
|
9
|
-
React.createElement(
|
|
10
|
-
React.createElement(
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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({
|
|
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({
|
|
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
|
|
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({
|
|
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
|
-
|
|
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
|
-
?
|
|
13
|
-
|
|
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
|
-
: {
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
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, '');
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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. */
|