jbrowse-plugin-protein3d 0.5.1 → 0.5.3
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 +10 -0
- package/dist/AddHighlightModel/ProteinToMsaHoverSync.js +8 -4
- package/dist/AddHighlightModel/findConnectedMsaView.d.ts +23 -0
- package/dist/AddHighlightModel/findConnectedMsaView.js +23 -0
- package/dist/LaunchProteinView/components/AlphaFoldDBSearch.js +3 -3
- package/dist/LaunchProteinView/components/AlphaFoldDBSearchStatus.d.ts +2 -4
- package/dist/LaunchProteinView/components/IdentifierSelector.js +2 -36
- package/dist/LaunchProteinView/components/IsoformSequencesToggle.d.ts +2 -5
- package/dist/LaunchProteinView/components/MSATable.d.ts +2 -5
- package/dist/LaunchProteinView/components/TabPanel.js +4 -2
- package/dist/LaunchProteinView/components/TranscriptSelector.d.ts +2 -4
- package/dist/LaunchProteinView/components/TranscriptSelector.js +16 -32
- package/dist/LaunchProteinView/hooks/useAlphaFoldDBSearch.d.ts +2 -8
- package/dist/LaunchProteinView/hooks/useIsoformProteinSequences.d.ts +2 -4
- package/dist/LaunchProteinView/hooks/useTranscriptIsoformSelection.d.ts +2 -8
- package/dist/LaunchProteinView/hooks/useTranscriptSelection.d.ts +2 -4
- package/dist/LaunchProteinView/hooks/useTranscriptSelection.js +3 -3
- package/dist/LaunchProteinView/services/lookupMethods.js +2 -20
- package/dist/LaunchProteinView/utils/launchViewUtils.js +8 -6
- package/dist/LaunchProteinView/utils/util.d.ts +24 -13
- package/dist/LaunchProteinView/utils/util.js +60 -49
- package/dist/ProteinView/__fixtures__/structureFixtures.d.ts +7 -0
- package/dist/ProteinView/__fixtures__/structureFixtures.js +16 -0
- package/dist/ProteinView/applyLociInteractivity.d.ts +4 -1
- package/dist/ProteinView/applyLociInteractivity.js +9 -1
- package/dist/ProteinView/chooseMappedEntity.d.ts +32 -0
- package/dist/ProteinView/chooseMappedEntity.js +71 -0
- package/dist/ProteinView/components/FeatureBar.js +1 -0
- package/dist/ProteinView/components/ProteinAlignment.js +14 -0
- package/dist/ProteinView/extractStructureSequences.d.ts +12 -0
- package/dist/ProteinView/extractStructureSequences.js +9 -1
- package/dist/ProteinView/loadStructureData.d.ts +2 -1
- package/dist/ProteinView/loadStructureData.js +4 -4
- package/dist/ProteinView/model.d.ts +33 -9
- package/dist/ProteinView/structureModel.d.ts +40 -2
- package/dist/ProteinView/structureModel.js +87 -33
- package/dist/ProteinView/subscribeMolstarInteraction.d.ts +3 -0
- package/dist/ProteinView/subscribeMolstarInteraction.js +1 -0
- package/dist/jbrowse-plugin-protein3d.umd.production.min.js +16 -16
- package/dist/jbrowse-plugin-protein3d.umd.production.min.js.map +4 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -1
- package/src/AddHighlightModel/ProteinToMsaHoverSync.tsx +9 -4
- package/src/AddHighlightModel/findConnectedMsaView.test.ts +53 -0
- package/src/AddHighlightModel/findConnectedMsaView.ts +35 -0
- package/src/LaunchProteinView/components/AlphaFoldDBSearch.tsx +5 -5
- package/src/LaunchProteinView/components/AlphaFoldDBSearchStatus.tsx +2 -1
- package/src/LaunchProteinView/components/IdentifierSelector.tsx +2 -37
- package/src/LaunchProteinView/components/IsoformSequencesToggle.tsx +2 -2
- package/src/LaunchProteinView/components/MSATable.tsx +2 -2
- package/src/LaunchProteinView/components/TabPanel.tsx +4 -2
- package/src/LaunchProteinView/components/TranscriptSelector.tsx +15 -33
- package/src/LaunchProteinView/hooks/useIsoformProteinSequences.ts +2 -3
- package/src/LaunchProteinView/hooks/useTranscriptSelection.ts +11 -13
- package/src/LaunchProteinView/services/lookupMethods.ts +2 -21
- package/src/LaunchProteinView/utils/launchViewUtils.ts +8 -6
- package/src/LaunchProteinView/utils/util.ts +98 -64
- package/src/ProteinView/__fixtures__/structureFixtures.ts +29 -0
- package/src/ProteinView/applyLociInteractivity.ts +12 -0
- package/src/ProteinView/chooseMappedEntity.test.ts +65 -0
- package/src/ProteinView/chooseMappedEntity.ts +97 -0
- package/src/ProteinView/components/FeatureBar.tsx +1 -0
- package/src/ProteinView/components/ProteinAlignment.tsx +19 -0
- package/src/ProteinView/extractStructureSequences.ts +20 -3
- package/src/ProteinView/loadStructureData.ts +6 -5
- package/src/ProteinView/structureLoader.test.ts +12 -9
- package/src/ProteinView/structureModel.ts +103 -38
- package/src/ProteinView/subscribeMolstarInteraction.ts +4 -0
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,16 @@ It has features to automatically look up a protein structure of interest using
|
|
|
11
11
|
the UniProt ID mapping API to connect to AlphaFoldDB, and can also use Foldseek
|
|
12
12
|
to look up related structures also
|
|
13
13
|
|
|
14
|
+
## Coordinate-mapping harness
|
|
15
|
+
|
|
16
|
+
A standalone diagnostic page that loads real PDB / AlphaFold structures through
|
|
17
|
+
the plugin's actual mapping code and surfaces cases it mishandles (multi-chain
|
|
18
|
+
complexes, partial/repeat structures, AlphaFold fragments):
|
|
19
|
+
|
|
20
|
+
https://gmod.org/jbrowse-plugin-protein3d/
|
|
21
|
+
|
|
22
|
+
Source and details in [harness/](harness/).
|
|
23
|
+
|
|
14
24
|
## Screenshot
|
|
15
25
|
|
|
16
26
|

|
|
@@ -2,6 +2,7 @@ import { useEffect } from 'react';
|
|
|
2
2
|
import { getSession } from '@jbrowse/core/util';
|
|
3
3
|
import { autorun, untracked } from 'mobx';
|
|
4
4
|
import { observer } from 'mobx-react';
|
|
5
|
+
import { findConnectedMsaView } from './findConnectedMsaView';
|
|
5
6
|
import { findStructureRowName } from './msaRowMatch';
|
|
6
7
|
import { getProteinView } from './util';
|
|
7
8
|
import { stripStopCodon } from '../LaunchProteinView/utils/util';
|
|
@@ -9,10 +10,13 @@ const ProteinToMsaHoverSync = observer(function ProteinToMsaHoverSync({ model, }
|
|
|
9
10
|
const session = getSession(model);
|
|
10
11
|
const { views } = session;
|
|
11
12
|
const proteinView = getProteinView(session);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
// pair with the MSA either by an explicit connectedMsaViewId or, in the
|
|
14
|
+
// genome-centric flow, by the genome view this structure and the MSA both
|
|
15
|
+
// connect to (see findConnectedMsaView)
|
|
16
|
+
const msaView = findConnectedMsaView(views, {
|
|
17
|
+
connectedMsaViewId: proteinView?.connectedMsaViewId,
|
|
18
|
+
structureViewId: proteinView?.primaryStructure?.connectedViewId,
|
|
19
|
+
});
|
|
16
20
|
useEffect(() => {
|
|
17
21
|
if (!proteinView || !msaView) {
|
|
18
22
|
return;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface MsaViewLike {
|
|
2
|
+
id: string;
|
|
3
|
+
type: string;
|
|
4
|
+
connectedViewId?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Find the MsaView a protein view should sync hover with. Two ways they pair:
|
|
8
|
+
* - an explicit `connectedMsaViewId` (set when the protein view was launched
|
|
9
|
+
* from an alignment), or
|
|
10
|
+
* - a shared genome view: the protein structure and an MsaView are both
|
|
11
|
+
* connected to the same LinearGenomeView via `connectedViewId` — the
|
|
12
|
+
* genome-centric gene-explorer flow, where no explicit MSA link is set but
|
|
13
|
+
* both views already bridge through the same genome coordinates.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors react-msaview's structureMatchesMsa (the MSA→structure side): a shared
|
|
16
|
+
* genome view is sufficient to connect, so neither side has to thread an
|
|
17
|
+
* explicit cross-view id.
|
|
18
|
+
*/
|
|
19
|
+
export declare function findConnectedMsaView<T extends MsaViewLike>(views: T[], { connectedMsaViewId, structureViewId, }: {
|
|
20
|
+
connectedMsaViewId?: string;
|
|
21
|
+
structureViewId?: string;
|
|
22
|
+
}): T | undefined;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find the MsaView a protein view should sync hover with. Two ways they pair:
|
|
3
|
+
* - an explicit `connectedMsaViewId` (set when the protein view was launched
|
|
4
|
+
* from an alignment), or
|
|
5
|
+
* - a shared genome view: the protein structure and an MsaView are both
|
|
6
|
+
* connected to the same LinearGenomeView via `connectedViewId` — the
|
|
7
|
+
* genome-centric gene-explorer flow, where no explicit MSA link is set but
|
|
8
|
+
* both views already bridge through the same genome coordinates.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors react-msaview's structureMatchesMsa (the MSA→structure side): a shared
|
|
11
|
+
* genome view is sufficient to connect, so neither side has to thread an
|
|
12
|
+
* explicit cross-view id.
|
|
13
|
+
*/
|
|
14
|
+
export function findConnectedMsaView(views, { connectedMsaViewId, structureViewId, }) {
|
|
15
|
+
const msaViews = views.filter(v => v.type === 'MsaView');
|
|
16
|
+
const byExplicitId = connectedMsaViewId
|
|
17
|
+
? msaViews.find(v => v.id === connectedMsaViewId)
|
|
18
|
+
: undefined;
|
|
19
|
+
const bySharedGenomeView = structureViewId
|
|
20
|
+
? msaViews.find(v => v.connectedViewId === structureViewId)
|
|
21
|
+
: undefined;
|
|
22
|
+
return byExplicitId ?? bySharedGenomeView;
|
|
23
|
+
}
|
|
@@ -56,11 +56,11 @@ const AlphaFoldDBSearch = observer(function AlphaFoldDBSearch({ feature, session
|
|
|
56
56
|
React.createElement(ExternalLink, { href: "https://www.uniprot.org/" }, "UniProt"),
|
|
57
57
|
' ',
|
|
58
58
|
"directly and use \"Enter manually\" above, or use \"Search sequence against AlphaFoldDB API\" if available.")),
|
|
59
|
-
state.showStructureSelectors && (React.createElement(React.Fragment, null,
|
|
59
|
+
state.showStructureSelectors && state.isoformSequences ? (React.createElement(React.Fragment, null,
|
|
60
60
|
React.createElement("div", { className: classes.selectorsRow },
|
|
61
61
|
React.createElement(TranscriptSelector, { val: state.userSelection, setVal: state.setUserSelection, structureSequence: state.structureSequence, feature: feature, isoforms: state.transcriptOptions, isoformSequences: state.isoformSequences })),
|
|
62
|
-
state.showSequenceSearchStatus && (React.createElement(SequenceSearchStatus, { isLoading: state.isSequenceSearchLoading, uniprotId: state.uniprotId, url: state.url, hasProteinSequence: !!state.userSelectedProteinSequence, sequenceSearchType: state.sequenceSearchType })),
|
|
63
|
-
state.showAlphaFoldDBSearchStatus && (React.createElement(AlphaFoldDBSearchStatus, { uniprotId: state.uniprotId, selectedTranscript: state.selectedTranscript, structureSequence: state.structureSequence, isoformSequences: state.isoformSequences, url: state.url }))))),
|
|
62
|
+
state.showSequenceSearchStatus && (React.createElement(SequenceSearchStatus, { isLoading: state.isSequenceSearchLoading, uniprotId: state.uniprotId, url: state.url, hasProteinSequence: !!state.userSelectedProteinSequence?.seq, sequenceSearchType: state.sequenceSearchType })),
|
|
63
|
+
state.showAlphaFoldDBSearchStatus && (React.createElement(AlphaFoldDBSearchStatus, { uniprotId: state.uniprotId, selectedTranscript: state.selectedTranscript, structureSequence: state.structureSequence, isoformSequences: state.isoformSequences, url: state.url })))) : null),
|
|
64
64
|
React.createElement(DialogActions, null,
|
|
65
65
|
React.createElement(ProteinViewActions, { handleClose: handleClose, uniprotId: state.uniprotId, userSelectedProteinSequence: state.userSelectedProteinSequence, selectedTranscript: state.selectedTranscript, url: state.url, confidenceUrl: state.confidenceUrl, feature: feature, view: view, session: session, alignmentAlgorithm: alignmentAlgorithm, onAlignmentAlgorithmChange: onAlignmentAlgorithmChange, sequencesMatch: state.sequencesMatch, isLoading: state.isLoading, error: state.error }))));
|
|
66
66
|
});
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import type { IsoformSequences } from '../utils/util';
|
|
2
3
|
import type { Feature } from '@jbrowse/core/util';
|
|
3
4
|
export default function AlphaFoldDBSearchStatus({ uniprotId, selectedTranscript, structureSequence, isoformSequences, url, }: {
|
|
4
5
|
uniprotId?: string;
|
|
5
6
|
selectedTranscript?: Feature;
|
|
6
7
|
structureSequence?: string;
|
|
7
|
-
isoformSequences:
|
|
8
|
-
feature: Feature;
|
|
9
|
-
seq: string;
|
|
10
|
-
}>;
|
|
8
|
+
isoformSequences: IsoformSequences;
|
|
11
9
|
url?: string;
|
|
12
10
|
}): React.JSX.Element;
|
|
@@ -1,46 +1,12 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { Button, FormControl, InputLabel, MenuItem, Select, } from '@mui/material';
|
|
3
|
-
import {
|
|
4
|
-
function getIdLabel(id) {
|
|
5
|
-
const dbType = getDatabaseTypeForId(id);
|
|
6
|
-
if (dbType === 'refseq') {
|
|
7
|
-
if (id.startsWith('NM_') || id.startsWith('XM_')) {
|
|
8
|
-
return `${id} (RefSeq mRNA)`;
|
|
9
|
-
}
|
|
10
|
-
if (id.startsWith('NR_') || id.startsWith('XR_')) {
|
|
11
|
-
return `${id} (RefSeq ncRNA)`;
|
|
12
|
-
}
|
|
13
|
-
if (id.startsWith('NP_') || id.startsWith('XP_')) {
|
|
14
|
-
return `${id} (RefSeq protein)`;
|
|
15
|
-
}
|
|
16
|
-
return `${id} (RefSeq)`;
|
|
17
|
-
}
|
|
18
|
-
if (dbType === 'ensembl') {
|
|
19
|
-
if (id.includes('G')) {
|
|
20
|
-
return `${id} (Ensembl gene)`;
|
|
21
|
-
}
|
|
22
|
-
if (id.includes('T')) {
|
|
23
|
-
return `${id} (Ensembl transcript)`;
|
|
24
|
-
}
|
|
25
|
-
if (id.includes('P')) {
|
|
26
|
-
return `${id} (Ensembl protein)`;
|
|
27
|
-
}
|
|
28
|
-
return `${id} (Ensembl)`;
|
|
29
|
-
}
|
|
30
|
-
if (dbType === 'hgnc') {
|
|
31
|
-
return `${id} (HGNC)`;
|
|
32
|
-
}
|
|
33
|
-
if (dbType === 'ccds') {
|
|
34
|
-
return `${id} (CCDS)`;
|
|
35
|
-
}
|
|
36
|
-
return id;
|
|
37
|
-
}
|
|
3
|
+
import { getDbIdLabel } from '../utils/util';
|
|
38
4
|
export default function IdentifierSelector({ recognizedIds, geneName, selectedId, onSelectedIdChange, }) {
|
|
39
5
|
const [expanded, setExpanded] = useState(false);
|
|
40
6
|
// Build list of selectable options
|
|
41
7
|
const options = [
|
|
42
8
|
{ value: 'auto', label: 'Auto (try all)' },
|
|
43
|
-
...recognizedIds.map(id => ({ value: id, label:
|
|
9
|
+
...recognizedIds.map(id => ({ value: id, label: getDbIdLabel(id) })),
|
|
44
10
|
];
|
|
45
11
|
if (geneName) {
|
|
46
12
|
options.push({
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type { IsoformSequences } from '../utils/util';
|
|
3
3
|
export default function IsoformSequencesToggle({ structureSequence, structureName, isoformSequences, }: {
|
|
4
4
|
structureSequence: string;
|
|
5
5
|
structureName: string;
|
|
6
|
-
isoformSequences:
|
|
7
|
-
feature: Feature;
|
|
8
|
-
seq: string;
|
|
9
|
-
}>;
|
|
6
|
+
isoformSequences: IsoformSequences;
|
|
10
7
|
}): React.JSX.Element;
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type { IsoformSequences } from '../utils/util';
|
|
3
3
|
export default function MSATable({ structureName, structureSequence, isoformSequences, }: {
|
|
4
4
|
structureName: string;
|
|
5
5
|
structureSequence: string;
|
|
6
|
-
isoformSequences:
|
|
7
|
-
feature: Feature;
|
|
8
|
-
seq: string;
|
|
9
|
-
}>;
|
|
6
|
+
isoformSequences: IsoformSequences;
|
|
10
7
|
}): React.JSX.Element;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
//
|
|
2
|
+
// Panels stay mounted and are hidden via the `hidden` attribute rather than
|
|
3
|
+
// unmounted, so switching tabs preserves each tab's in-progress work (typed
|
|
4
|
+
// UniProt ID, fetched results, selected transcript) instead of resetting it.
|
|
3
5
|
export default function TabPanel({ children, value, index, ...other }) {
|
|
4
|
-
return (React.createElement("div", { role: "tabpanel", hidden: value !== index, ...other },
|
|
6
|
+
return (React.createElement("div", { role: "tabpanel", hidden: value !== index, ...other }, children));
|
|
5
7
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import type { IsoformSequences } from '../utils/util';
|
|
2
3
|
import type { Feature } from '@jbrowse/core/util';
|
|
3
4
|
export default function TranscriptSelector({ val, setVal, isoforms, isoformSequences, structureSequence, feature, disabled, }: {
|
|
4
5
|
isoforms: Feature[];
|
|
@@ -6,9 +7,6 @@ export default function TranscriptSelector({ val, setVal, isoforms, isoformSeque
|
|
|
6
7
|
val: string | undefined;
|
|
7
8
|
setVal: (str: string) => void;
|
|
8
9
|
structureSequence?: string;
|
|
9
|
-
isoformSequences:
|
|
10
|
-
feature: Feature;
|
|
11
|
-
seq: string;
|
|
12
|
-
}>;
|
|
10
|
+
isoformSequences: IsoformSequences;
|
|
13
11
|
disabled?: boolean;
|
|
14
12
|
}): React.JSX.Element;
|
|
@@ -1,42 +1,26 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { MenuItem, TextField } from '@mui/material';
|
|
3
|
-
import { getGeneDisplayName, getTranscriptDisplayName,
|
|
3
|
+
import { classifyIsoforms, getGeneDisplayName, getTranscriptDisplayName, } from '../utils/util';
|
|
4
4
|
export default function TranscriptSelector({ val, setVal, isoforms, isoformSequences, structureSequence, feature, disabled, }) {
|
|
5
5
|
const geneName = getGeneDisplayName(feature);
|
|
6
|
-
const matches =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
nonMatches.push(f);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
const byLengthDesc = (a, b) => isoformSequences[b.id()].seq.length - isoformSequences[a.id()].seq.length;
|
|
6
|
+
const { matches, nonMatches, noData } = classifyIsoforms({
|
|
7
|
+
options: isoforms,
|
|
8
|
+
isoformSequences,
|
|
9
|
+
structureSequence,
|
|
10
|
+
});
|
|
11
|
+
const renderOption = ({ feature: f, length }, note = '') => (React.createElement(MenuItem, { value: f.id(), key: f.id() },
|
|
12
|
+
geneName,
|
|
13
|
+
" - ",
|
|
14
|
+
getTranscriptDisplayName(f),
|
|
15
|
+
" (",
|
|
16
|
+
length,
|
|
17
|
+
"aa)",
|
|
18
|
+
note));
|
|
23
19
|
return (React.createElement(TextField, { value: val ?? '', onChange: event => {
|
|
24
20
|
setVal(event.target.value);
|
|
25
21
|
}, label: "Choose transcript isoform", select: true, disabled: disabled },
|
|
26
|
-
matches.
|
|
27
|
-
|
|
28
|
-
" - ",
|
|
29
|
-
getTranscriptDisplayName(f),
|
|
30
|
-
" (",
|
|
31
|
-
isoformSequences[f.id()].seq.length,
|
|
32
|
-
"aa) (matches structure residues)"))),
|
|
33
|
-
nonMatches.toSorted(byLengthDesc).map(f => (React.createElement(MenuItem, { value: f.id(), key: f.id() },
|
|
34
|
-
geneName,
|
|
35
|
-
" - ",
|
|
36
|
-
getTranscriptDisplayName(f),
|
|
37
|
-
" (",
|
|
38
|
-
isoformSequences[f.id()].seq.length,
|
|
39
|
-
"aa)"))),
|
|
22
|
+
matches.map(m => renderOption(m, ' (matches structure residues)')),
|
|
23
|
+
nonMatches.map(m => renderOption(m)),
|
|
40
24
|
noData.map(f => (React.createElement(MenuItem, { value: f.id(), key: f.id(), disabled: true },
|
|
41
25
|
geneName,
|
|
42
26
|
" - ",
|
|
@@ -20,14 +20,8 @@ export default function useAlphaFoldDBSearch({ feature, view, }: {
|
|
|
20
20
|
setUserSelection: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
|
|
21
21
|
transcriptOptions: Feature[];
|
|
22
22
|
selectedTranscript: Feature | undefined;
|
|
23
|
-
isoformSequences:
|
|
24
|
-
|
|
25
|
-
seq: string;
|
|
26
|
-
}> | undefined;
|
|
27
|
-
userSelectedProteinSequence: {
|
|
28
|
-
feature: Feature;
|
|
29
|
-
seq: string;
|
|
30
|
-
} | undefined;
|
|
23
|
+
isoformSequences: import("../utils/util").IsoformSequences | undefined;
|
|
24
|
+
userSelectedProteinSequence: import("../utils/util").IsoformSequence | undefined;
|
|
31
25
|
uniprotEntries: import("../services/lookupMethods").UniProtEntry[];
|
|
32
26
|
recognizedIds: string[];
|
|
33
27
|
geneName: string | undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IsoformSequences } from '../utils/util';
|
|
1
2
|
import type { Feature } from '@jbrowse/core/util';
|
|
2
3
|
export default function useIsoformProteinSequences({ feature, view, }: {
|
|
3
4
|
feature: Feature;
|
|
@@ -6,9 +7,6 @@ export default function useIsoformProteinSequences({ feature, view, }: {
|
|
|
6
7
|
};
|
|
7
8
|
}): {
|
|
8
9
|
isLoading: boolean;
|
|
9
|
-
isoformSequences:
|
|
10
|
-
feature: Feature;
|
|
11
|
-
seq: string;
|
|
12
|
-
}> | undefined;
|
|
10
|
+
isoformSequences: IsoformSequences | undefined;
|
|
13
11
|
error: any;
|
|
14
12
|
};
|
|
@@ -8,17 +8,11 @@ export default function useTranscriptIsoformSelection({ feature, view, structure
|
|
|
8
8
|
resetKey?: string;
|
|
9
9
|
}): {
|
|
10
10
|
transcripts: Feature[];
|
|
11
|
-
isoformSequences:
|
|
12
|
-
feature: Feature;
|
|
13
|
-
seq: string;
|
|
14
|
-
}> | undefined;
|
|
11
|
+
isoformSequences: import("../utils/util").IsoformSequences | undefined;
|
|
15
12
|
isLoading: boolean;
|
|
16
13
|
error: any;
|
|
17
14
|
selectedTranscriptId: string | undefined;
|
|
18
15
|
setSelectedTranscriptId: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
|
|
19
16
|
selectedTranscript: Feature | undefined;
|
|
20
|
-
selectedIsoform:
|
|
21
|
-
feature: Feature;
|
|
22
|
-
seq: string;
|
|
23
|
-
} | undefined;
|
|
17
|
+
selectedIsoform: import("../utils/util").IsoformSequence | undefined;
|
|
24
18
|
};
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
+
import type { IsoformSequences } from '../utils/util';
|
|
1
2
|
import type { Feature } from '@jbrowse/core/util';
|
|
2
3
|
export default function useTranscriptSelection({ options, isoformSequences, structureSequence, resetKey, }: {
|
|
3
4
|
options: Feature[];
|
|
4
|
-
isoformSequences?:
|
|
5
|
-
feature: Feature;
|
|
6
|
-
seq: string;
|
|
7
|
-
}>;
|
|
5
|
+
isoformSequences?: IsoformSequences;
|
|
8
6
|
structureSequence?: string;
|
|
9
7
|
resetKey?: string;
|
|
10
8
|
}): {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState } from 'react';
|
|
2
2
|
import { selectBestTranscript } from '../utils/util';
|
|
3
3
|
export default function useTranscriptSelection({ options, isoformSequences, structureSequence, resetKey, }) {
|
|
4
4
|
const [userSelection, setUserSelection] = useState();
|
|
@@ -7,12 +7,12 @@ export default function useTranscriptSelection({ options, isoformSequences, stru
|
|
|
7
7
|
setPrevResetKey(resetKey);
|
|
8
8
|
setUserSelection(undefined);
|
|
9
9
|
}
|
|
10
|
-
const autoSelection =
|
|
10
|
+
const autoSelection = isoformSequences !== undefined
|
|
11
11
|
? selectBestTranscript({
|
|
12
12
|
options,
|
|
13
13
|
isoformSequences,
|
|
14
14
|
structureSequence,
|
|
15
15
|
})?.id()
|
|
16
|
-
: undefined
|
|
16
|
+
: undefined;
|
|
17
17
|
return { userSelection: userSelection ?? autoSelection, setUserSelection };
|
|
18
18
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsonfetch } from '../../fetchUtils';
|
|
2
|
-
import {
|
|
2
|
+
import { buildUniProtXrefQuery, isRecognizedDatabaseId, stripTrailingVersion, } from '../utils/util';
|
|
3
3
|
const UNIPROT_FIELDS = 'accession,id,gene_names,organism_name,protein_name,reviewed';
|
|
4
4
|
function mapApiResultToEntry(result) {
|
|
5
5
|
return {
|
|
@@ -11,31 +11,13 @@ function mapApiResultToEntry(result) {
|
|
|
11
11
|
isReviewed: result.entryType === 'UniProtKB reviewed (Swiss-Prot)',
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
15
|
-
* Build UniProt xref query for a recognized database ID
|
|
16
|
-
*/
|
|
17
|
-
function buildXrefQuery(id) {
|
|
18
|
-
const dbType = getDatabaseTypeForId(id);
|
|
19
|
-
switch (dbType) {
|
|
20
|
-
case 'ensembl':
|
|
21
|
-
return `xref:ensembl-${id}`;
|
|
22
|
-
case 'refseq':
|
|
23
|
-
return `xref:refseq-${id}`;
|
|
24
|
-
case 'ccds':
|
|
25
|
-
return `xref:ccds-${id}`;
|
|
26
|
-
case 'hgnc':
|
|
27
|
-
return `xref:hgnc-${id.replace('HGNC:', '')}`;
|
|
28
|
-
default:
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
14
|
async function searchUniProt(query, size = 10) {
|
|
33
15
|
const url = `https://rest.uniprot.org/uniprotkb/search?query=${encodeURIComponent(query)}&fields=${UNIPROT_FIELDS}&size=${size}`;
|
|
34
16
|
const data = await jsonfetch(url);
|
|
35
17
|
return data.results.map(mapApiResultToEntry);
|
|
36
18
|
}
|
|
37
19
|
async function searchByXref(id) {
|
|
38
|
-
const query =
|
|
20
|
+
const query = buildUniProtXrefQuery(id);
|
|
39
21
|
if (!query) {
|
|
40
22
|
return { entries: [], error: undefined };
|
|
41
23
|
}
|
|
@@ -94,11 +94,13 @@ export async function launch1DProteinView({ session, view, feature, selectedTran
|
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
96
|
// CROSS-REPO DEPENDENCY: the 'MsaView' view type is registered by
|
|
97
|
-
// jbrowse-plugin-msaview, which wraps the `react-msaview` library. The
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
97
|
+
// jbrowse-plugin-msaview, which wraps the `react-msaview` library. The top-level
|
|
98
|
+
// props here (colorSchemeName, connectedViewId, connectedFeature) are native
|
|
99
|
+
// react-msaview model properties applied directly from the snapshot; only `init`
|
|
100
|
+
// (msaUrl) is a declarative launch contract that the plugin resolves once and
|
|
101
|
+
// clears. These are NOT type-checked here because we only depend on it at runtime
|
|
102
|
+
// (gated by hasMsaViewPlugin()). If react-msaview renames these, the launch
|
|
103
|
+
// silently degrades. Keep in step with that repo.
|
|
102
104
|
export function launchMsaView({ session, view, feature, selectedTranscript, uniprotId, displayName, }) {
|
|
103
105
|
if (!uniprotId) {
|
|
104
106
|
return undefined;
|
|
@@ -109,9 +111,9 @@ export function launchMsaView({ session, view, feature, selectedTranscript, unip
|
|
|
109
111
|
formatViewName('MSA view', feature, selectedTranscript, uniprotId),
|
|
110
112
|
connectedViewId: view.id,
|
|
111
113
|
connectedFeature: selectedTranscript?.toJSON(),
|
|
114
|
+
colorSchemeName: 'percent_identity',
|
|
112
115
|
init: {
|
|
113
116
|
msaUrl: getAlphaFoldMsaUrl(uniprotId),
|
|
114
|
-
colorSchemeName: 'percent_identity',
|
|
115
117
|
},
|
|
116
118
|
});
|
|
117
119
|
}
|
|
@@ -6,14 +6,9 @@ export declare function getId(val?: Feature): string;
|
|
|
6
6
|
export declare function getTranscriptDisplayName(val?: Feature): string;
|
|
7
7
|
export declare function getGeneDisplayName(val?: Feature): string;
|
|
8
8
|
export declare function getUniProtIdFromFeature(f?: Feature): string | undefined;
|
|
9
|
-
/**
|
|
10
|
-
* Check if an ID is a recognized database identifier that UniProt can map
|
|
11
|
-
*/
|
|
12
9
|
export declare function isRecognizedDatabaseId(id: string): boolean;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*/
|
|
16
|
-
export declare function getDatabaseTypeForId(id: string): string | undefined;
|
|
10
|
+
export declare function getDbIdLabel(id: string): string;
|
|
11
|
+
export declare function buildUniProtXrefQuery(id: string): string | undefined;
|
|
17
12
|
export declare function findRecognizedDbIds(f?: Feature): string[];
|
|
18
13
|
export interface FeatureIdentifiers {
|
|
19
14
|
recognizedIds: string[];
|
|
@@ -28,11 +23,27 @@ export interface FeatureIdentifiers {
|
|
|
28
23
|
* geneId and geneName are always extracted from the parent feature 'f'.
|
|
29
24
|
*/
|
|
30
25
|
export declare function extractFeatureIdentifiers(f?: Feature): FeatureIdentifiers;
|
|
31
|
-
export
|
|
26
|
+
export interface IsoformSequence {
|
|
27
|
+
feature: Feature;
|
|
28
|
+
seq: string;
|
|
29
|
+
}
|
|
30
|
+
export type IsoformSequences = Record<string, IsoformSequence>;
|
|
31
|
+
export interface RankedIsoform {
|
|
32
|
+
feature: Feature;
|
|
33
|
+
length: number;
|
|
34
|
+
}
|
|
35
|
+
export interface ClassifiedIsoforms {
|
|
36
|
+
matches: RankedIsoform[];
|
|
37
|
+
nonMatches: RankedIsoform[];
|
|
38
|
+
noData: Feature[];
|
|
39
|
+
}
|
|
40
|
+
export declare function classifyIsoforms({ options, isoformSequences, structureSequence, }: {
|
|
41
|
+
options: Feature[];
|
|
42
|
+
isoformSequences: IsoformSequences;
|
|
43
|
+
structureSequence?: string;
|
|
44
|
+
}): ClassifiedIsoforms;
|
|
45
|
+
export declare function selectBestTranscript(args: {
|
|
32
46
|
options: Feature[];
|
|
33
|
-
isoformSequences:
|
|
34
|
-
|
|
35
|
-
seq: string;
|
|
36
|
-
}>;
|
|
37
|
-
structureSequence: string | undefined;
|
|
47
|
+
isoformSequences: IsoformSequences;
|
|
48
|
+
structureSequence?: string;
|
|
38
49
|
}): Feature | undefined;
|
|
@@ -34,48 +34,36 @@ export function getUniProtIdFromFeature(f) {
|
|
|
34
34
|
}
|
|
35
35
|
return f.get('uniprot') ?? f.get('uniprotId') ?? f.get('uniprotid');
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
37
|
+
const DB_ID_PATTERNS = [
|
|
38
|
+
{ db: 'ensembl', pattern: /^ENS[A-Z]*G\d+/i, label: 'Ensembl gene' },
|
|
39
|
+
{ db: 'ensembl', pattern: /^ENS[A-Z]*T\d+/i, label: 'Ensembl transcript' },
|
|
40
|
+
{ db: 'ensembl', pattern: /^ENS[A-Z]*P\d+/i, label: 'Ensembl protein' },
|
|
41
|
+
{ db: 'refseq', pattern: /^[NX]M_\d+/i, label: 'RefSeq mRNA' },
|
|
42
|
+
{ db: 'refseq', pattern: /^[NX]R_\d+/i, label: 'RefSeq ncRNA' },
|
|
43
|
+
{ db: 'refseq', pattern: /^[NX]P_\d+/i, label: 'RefSeq protein' },
|
|
44
|
+
{ db: 'ccds', pattern: /^CCDS\d+/i, label: 'CCDS' },
|
|
45
|
+
{ db: 'hgnc', pattern: /^HGNC:\d+/i, label: 'HGNC' },
|
|
46
|
+
];
|
|
47
|
+
function matchDbIdPattern(id) {
|
|
48
|
+
return DB_ID_PATTERNS.find(p => p.pattern.test(id));
|
|
49
|
+
}
|
|
50
|
+
// Check if an ID is a recognized database identifier that UniProt can map
|
|
51
51
|
export function isRecognizedDatabaseId(id) {
|
|
52
|
-
return (
|
|
53
|
-
ensemblTranscriptPattern.test(id) ||
|
|
54
|
-
ensemblProteinPattern.test(id) ||
|
|
55
|
-
refSeqTranscriptPattern.test(id) ||
|
|
56
|
-
refSeqProteinPattern.test(id) ||
|
|
57
|
-
ccdsPattern.test(id) ||
|
|
58
|
-
hgncPattern.test(id));
|
|
52
|
+
return matchDbIdPattern(id) !== undefined;
|
|
59
53
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return 'ccds';
|
|
74
|
-
}
|
|
75
|
-
if (hgncPattern.test(id)) {
|
|
76
|
-
return 'hgnc';
|
|
77
|
-
}
|
|
78
|
-
return undefined;
|
|
54
|
+
// Human-readable label for an ID, e.g. "ENST00000123 (Ensembl transcript)".
|
|
55
|
+
// Unrecognized IDs are returned unadorned.
|
|
56
|
+
export function getDbIdLabel(id) {
|
|
57
|
+
const match = matchDbIdPattern(id);
|
|
58
|
+
return match ? `${id} (${match.label})` : id;
|
|
59
|
+
}
|
|
60
|
+
// Build the UniProt xref query fragment for a recognized ID, e.g.
|
|
61
|
+
// "xref:ensembl-ENST00000123". HGNC strips its redundant "HGNC:" prefix.
|
|
62
|
+
export function buildUniProtXrefQuery(id) {
|
|
63
|
+
const match = matchDbIdPattern(id);
|
|
64
|
+
return match
|
|
65
|
+
? `xref:${match.db}-${match.db === 'hgnc' ? id.replace('HGNC:', '') : id}`
|
|
66
|
+
: undefined;
|
|
79
67
|
}
|
|
80
68
|
/**
|
|
81
69
|
* Parse dbxref attribute which can have formats like:
|
|
@@ -154,7 +142,7 @@ export function findRecognizedDbIds(f) {
|
|
|
154
142
|
if (/^\d+$/.test(hgncStr)) {
|
|
155
143
|
recognizedIds.push(`HGNC:${hgncStr}`);
|
|
156
144
|
}
|
|
157
|
-
else if (
|
|
145
|
+
else if (matchDbIdPattern(hgncStr)?.db === 'hgnc') {
|
|
158
146
|
recognizedIds.push(hgncStr);
|
|
159
147
|
}
|
|
160
148
|
}
|
|
@@ -206,12 +194,35 @@ export function extractFeatureIdentifiers(f) {
|
|
|
206
194
|
geneName: typeof geneName === 'string' ? geneName : undefined,
|
|
207
195
|
};
|
|
208
196
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
197
|
+
// The single rule for ranking transcript isoforms against a structure, shared
|
|
198
|
+
// by the picker UI and the auto-selection: partition by whether the translated
|
|
199
|
+
// protein matches the structure residues, with each group ordered longest-first.
|
|
200
|
+
export function classifyIsoforms({ options, isoformSequences, structureSequence, }) {
|
|
201
|
+
const matches = [];
|
|
202
|
+
const nonMatches = [];
|
|
203
|
+
const noData = [];
|
|
204
|
+
for (const feature of options) {
|
|
205
|
+
const entry = isoformSequences[feature.id()];
|
|
206
|
+
const ranked = { feature, length: entry?.seq.length ?? 0 };
|
|
207
|
+
if (!entry) {
|
|
208
|
+
noData.push(feature);
|
|
209
|
+
}
|
|
210
|
+
else if (structureSequence &&
|
|
211
|
+
stripStopCodon(entry.seq) === structureSequence) {
|
|
212
|
+
matches.push(ranked);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
nonMatches.push(ranked);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const byLengthDesc = (a, b) => b.length - a.length;
|
|
219
|
+
return {
|
|
220
|
+
matches: matches.toSorted(byLengthDesc),
|
|
221
|
+
nonMatches: nonMatches.toSorted(byLengthDesc),
|
|
222
|
+
noData,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
export function selectBestTranscript(args) {
|
|
226
|
+
const { matches, nonMatches } = classifyIsoforms(args);
|
|
227
|
+
return (matches[0] ?? nonMatches[0])?.feature;
|
|
217
228
|
}
|