jbrowse-plugin-protein3d 0.4.14 → 0.5.1
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/dist/LaunchProteinView/components/FoldseekActionMenu.js +15 -14
- package/dist/LaunchProteinView/components/ProteinViewActions.js +27 -15
- package/dist/LaunchProteinView/components/UserProvidedStructure.js +1 -2
- package/dist/LaunchProteinView/hooks/useSafeLaunch.d.ts +9 -0
- package/dist/LaunchProteinView/hooks/useSafeLaunch.js +15 -0
- package/dist/LaunchProteinView/utils/launchViewUtils.d.ts +9 -11
- package/dist/LaunchProteinView/utils/launchViewUtils.js +6 -8
- package/dist/LaunchProteinView/utils/sideBySide.d.ts +5 -0
- package/dist/LaunchProteinView/utils/sideBySide.js +9 -0
- package/dist/LaunchProteinViewExtensionPoint/index.js +9 -5
- package/dist/ProteinView/applyLociInteractivity.d.ts +23 -17
- package/dist/ProteinView/applyLociInteractivity.js +33 -61
- package/dist/ProteinView/components/FeatureBar.d.ts +1 -1
- package/dist/ProteinView/components/FeatureBar.js +35 -33
- package/dist/ProteinView/components/FeatureTypeLabel.js +5 -9
- package/dist/ProteinView/components/ProteinFeatureTrack.js +7 -15
- package/dist/ProteinView/components/ProteinViewHeader.js +9 -1
- package/dist/ProteinView/components/ResidueValueTrack.js +26 -15
- package/dist/ProteinView/hooks/useProteinFeatureTrackData.d.ts +1 -0
- package/dist/ProteinView/hooks/useProteinFeatureTrackData.js +3 -2
- package/dist/ProteinView/model.d.ts +42 -0
- package/dist/ProteinView/structureModel.d.ts +32 -5
- package/dist/ProteinView/structureModel.js +98 -93
- package/dist/ProteinView/subscribeMolstarInteraction.d.ts +8 -0
- package/dist/ProteinView/subscribeMolstarInteraction.js +1 -1
- package/dist/ProteinView/useProteinView.js +9 -1
- package/dist/ProteinView/util.d.ts +0 -5
- package/dist/ProteinView/util.js +0 -11
- 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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/LaunchProteinView/components/FoldseekActionMenu.tsx +22 -17
- package/src/LaunchProteinView/components/ProteinViewActions.tsx +32 -17
- package/src/LaunchProteinView/components/UserProvidedStructure.tsx +1 -6
- package/src/LaunchProteinView/hooks/useSafeLaunch.ts +17 -0
- package/src/LaunchProteinView/utils/launchViewUtils.ts +30 -29
- package/src/LaunchProteinView/utils/sideBySide.ts +14 -0
- package/src/LaunchProteinViewExtensionPoint/index.ts +14 -9
- package/src/ProteinView/applyLociInteractivity.ts +62 -114
- package/src/ProteinView/components/FeatureBar.tsx +36 -44
- package/src/ProteinView/components/FeatureTypeLabel.tsx +6 -11
- package/src/ProteinView/components/ProteinFeatureTrack.tsx +5 -17
- package/src/ProteinView/components/ProteinViewHeader.tsx +15 -0
- package/src/ProteinView/components/ResidueValueTrack.tsx +40 -23
- package/src/ProteinView/hooks/useProteinFeatureTrackData.ts +6 -2
- package/src/ProteinView/structureModel.ts +120 -108
- package/src/ProteinView/subscribeMolstarInteraction.ts +9 -1
- package/src/ProteinView/useProteinView.ts +9 -0
- package/src/ProteinView/util.ts +0 -25
- package/src/version.ts +1 -1
- package/dist/ProteinView/highlightResidueRange.d.ts +0 -14
- package/dist/ProteinView/highlightResidueRange.js +0 -19
- package/src/ProteinView/highlightResidueRange.ts +0 -44
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { ErrorMessage } from '@jbrowse/core/ui';
|
|
3
|
+
import { isSessionWithAddTracks } from '@jbrowse/core/util';
|
|
3
4
|
import { Button, Menu, MenuItem } from '@mui/material';
|
|
5
|
+
import { useSafeLaunch } from '../hooks/useSafeLaunch';
|
|
4
6
|
import { caCoordsToPdb, hasValidCaCoords } from '../utils/caCoordsToPdb';
|
|
5
|
-
import { safeLaunch } from '../utils/launchHelpers';
|
|
6
7
|
import { getConfidenceUrlFromTarget, getUniprotIdFromAlphaFoldTarget, hasMsaViewPlugin, launch1DProteinView, launch3DProteinView, launchMsaView, } from '../utils/launchViewUtils';
|
|
7
8
|
export default function FoldseekActionMenu({ hit, session, view, feature, selectedTranscript, userProvidedTranscriptSequence, onClose, }) {
|
|
8
9
|
const [anchorEl, setAnchorEl] = useState(null);
|
|
9
|
-
const [launchError, setLaunchError] = useState();
|
|
10
10
|
const open = Boolean(anchorEl);
|
|
11
11
|
const uniprotId = getUniprotIdFromAlphaFoldTarget(hit.target);
|
|
12
12
|
const handleClick = (event) => {
|
|
@@ -15,11 +15,8 @@ export default function FoldseekActionMenu({ hit, session, view, feature, select
|
|
|
15
15
|
const handleMenuClose = () => {
|
|
16
16
|
setAnchorEl(null);
|
|
17
17
|
};
|
|
18
|
+
const { runLaunch, launchError } = useSafeLaunch(onClose, handleMenuClose);
|
|
18
19
|
const baseParams = { session, view, feature, selectedTranscript, uniprotId };
|
|
19
|
-
const runLaunch = (fn) => () => {
|
|
20
|
-
handleMenuClose();
|
|
21
|
-
void safeLaunch(fn, onClose, setLaunchError);
|
|
22
|
-
};
|
|
23
20
|
const handleLaunch3D = runLaunch(() => {
|
|
24
21
|
// Use tCa coordinates to generate PDB data if no URL is available
|
|
25
22
|
const pdbData = !hit.structureUrl && hasValidCaCoords(hit.tCa, hit.tSeq)
|
|
@@ -32,24 +29,28 @@ export default function FoldseekActionMenu({ hit, session, view, feature, select
|
|
|
32
29
|
userProvidedTranscriptSequence,
|
|
33
30
|
});
|
|
34
31
|
});
|
|
35
|
-
const handleLaunch1D = runLaunch(async () => {
|
|
36
|
-
await launch1DProteinView({
|
|
37
|
-
...baseParams,
|
|
38
|
-
confidenceUrl: getConfidenceUrlFromTarget(hit.target),
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
32
|
const handleLaunchMSA = runLaunch(() => {
|
|
42
33
|
launchMsaView(baseParams);
|
|
43
34
|
});
|
|
44
|
-
const canLoad = hit.structureUrl
|
|
35
|
+
const canLoad = !!hit.structureUrl || hasValidCaCoords(hit.tCa, hit.tSeq);
|
|
45
36
|
if (!canLoad) {
|
|
46
37
|
return React.createElement("span", null, "-");
|
|
47
38
|
}
|
|
39
|
+
// 1D launch needs an add-tracks session and a uniprotId; narrowing both here
|
|
40
|
+
// gates the menu item and types its handler from a single condition.
|
|
41
|
+
const addTracksSession = isSessionWithAddTracks(session) ? session : undefined;
|
|
48
42
|
return (React.createElement(React.Fragment, null,
|
|
49
43
|
launchError ? React.createElement(ErrorMessage, { error: launchError }) : null,
|
|
50
44
|
React.createElement(Button, { size: "small", variant: "outlined", onClick: handleClick }, "Load"),
|
|
51
45
|
React.createElement(Menu, { anchorEl: anchorEl, open: open, onClose: handleMenuClose },
|
|
52
46
|
React.createElement(MenuItem, { onClick: handleLaunch3D }, "Launch 3D protein view"),
|
|
53
|
-
uniprotId ? (React.createElement(MenuItem, { onClick:
|
|
47
|
+
addTracksSession && uniprotId ? (React.createElement(MenuItem, { onClick: runLaunch(() => launch1DProteinView({
|
|
48
|
+
session: addTracksSession,
|
|
49
|
+
view,
|
|
50
|
+
feature,
|
|
51
|
+
selectedTranscript,
|
|
52
|
+
uniprotId,
|
|
53
|
+
confidenceUrl: getConfidenceUrlFromTarget(hit.target),
|
|
54
|
+
})) }, "Launch 1D protein annotation view")) : null,
|
|
54
55
|
uniprotId && hasMsaViewPlugin() ? (React.createElement(MenuItem, { onClick: handleLaunchMSA }, "Launch MSA view (AlphaFoldDB a3m)")) : null)));
|
|
55
56
|
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { ErrorMessage } from '@jbrowse/core/ui';
|
|
3
|
+
import { isSessionWithAddTracks } from '@jbrowse/core/util';
|
|
3
4
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
|
4
5
|
import SettingsIcon from '@mui/icons-material/Settings';
|
|
5
6
|
import { Button, ButtonGroup, IconButton, Tooltip, Typography } from '@mui/material';
|
|
6
7
|
import LaunchOptionsDialog from './LaunchOptionsDialog';
|
|
7
8
|
import LaunchSettingsDialog from './LaunchSettingsDialog';
|
|
8
9
|
import SequenceMismatchNotice from './SequenceMismatchNotice';
|
|
9
|
-
import {
|
|
10
|
+
import { useSafeLaunch } from '../hooks/useSafeLaunch';
|
|
11
|
+
import { getLaunchMissingReasons } from '../utils/launchHelpers';
|
|
10
12
|
import { hasMsaViewPlugin, launch1DProteinView, launch3DProteinView, launch3DProteinViewWithMsa, launchMsaView, } from '../utils/launchViewUtils';
|
|
11
13
|
export default function ProteinViewActions({ handleClose, uniprotId, userSelectedProteinSequence, selectedTranscript, url, confidenceUrl, feature, view, session, alignmentAlgorithm, onAlignmentAlgorithmChange, sequencesMatch, isLoading, error, }) {
|
|
12
14
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
13
15
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
14
|
-
const [launchError, setLaunchError] = useState();
|
|
15
16
|
// Disable launch while loading — SWR's keepPreviousData would otherwise let
|
|
16
17
|
// a user click Launch on stale results (wrong UniProt ID) during a refetch.
|
|
17
18
|
const canLaunch = !isLoading &&
|
|
@@ -28,6 +29,7 @@ export default function ProteinViewActions({ handleClose, uniprotId, userSelecte
|
|
|
28
29
|
const closeMenu = () => {
|
|
29
30
|
setDialogOpen(false);
|
|
30
31
|
};
|
|
32
|
+
const { runLaunch, launchError } = useSafeLaunch(handleClose, closeMenu);
|
|
31
33
|
const baseParams = {
|
|
32
34
|
session,
|
|
33
35
|
view,
|
|
@@ -41,22 +43,21 @@ export default function ProteinViewActions({ handleClose, uniprotId, userSelecte
|
|
|
41
43
|
userProvidedTranscriptSequence: userSelectedProteinSequence?.seq,
|
|
42
44
|
alignmentAlgorithm,
|
|
43
45
|
};
|
|
44
|
-
const runLaunch = (fn) => () => {
|
|
45
|
-
closeMenu();
|
|
46
|
-
void safeLaunch(fn, handleClose, setLaunchError);
|
|
47
|
-
};
|
|
48
46
|
const handleLaunch3DView = runLaunch(() => {
|
|
49
47
|
launch3DProteinView(launch3DParams);
|
|
50
48
|
});
|
|
51
|
-
const handleLaunch1DView = runLaunch(async () => {
|
|
52
|
-
await launch1DProteinView({ ...baseParams, confidenceUrl });
|
|
53
|
-
});
|
|
54
49
|
const handleLaunchMsa = runLaunch(() => {
|
|
55
50
|
launchMsaView(baseParams);
|
|
56
51
|
});
|
|
57
52
|
const handleLaunch3DWithMsa = runLaunch(() => {
|
|
58
53
|
launch3DProteinViewWithMsa(launch3DParams);
|
|
59
54
|
});
|
|
55
|
+
// The 1D annotation view needs an add-tracks session and a known uniprotId.
|
|
56
|
+
// Narrowing here is the single source of truth: the option only exists when
|
|
57
|
+
// both hold, and its handler is type-checked against those narrowed values —
|
|
58
|
+
// so a 1D launch that can't work is unrepresentable rather than a silent
|
|
59
|
+
// no-op.
|
|
60
|
+
const addTracksSession = isSessionWithAddTracks(session) ? session : undefined;
|
|
60
61
|
const launchOptions = [
|
|
61
62
|
{
|
|
62
63
|
key: '3d',
|
|
@@ -64,12 +65,23 @@ export default function ProteinViewActions({ handleClose, uniprotId, userSelecte
|
|
|
64
65
|
description: 'View protein structure with genome-to-structure coordinate mapping',
|
|
65
66
|
onClick: handleLaunch3DView,
|
|
66
67
|
},
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
...(addTracksSession && uniprotId
|
|
69
|
+
? [
|
|
70
|
+
{
|
|
71
|
+
key: '1d',
|
|
72
|
+
title: 'Launch 1D protein annotation view',
|
|
73
|
+
description: 'View protein features and annotations as a linear track',
|
|
74
|
+
onClick: runLaunch(() => launch1DProteinView({
|
|
75
|
+
session: addTracksSession,
|
|
76
|
+
view,
|
|
77
|
+
feature,
|
|
78
|
+
selectedTranscript,
|
|
79
|
+
uniprotId,
|
|
80
|
+
confidenceUrl,
|
|
81
|
+
})),
|
|
82
|
+
},
|
|
83
|
+
]
|
|
84
|
+
: []),
|
|
73
85
|
...(hasMsaViewPlugin()
|
|
74
86
|
? [
|
|
75
87
|
{
|
|
@@ -11,7 +11,7 @@ import ExternalLink from '../../components/ExternalLink';
|
|
|
11
11
|
import useStructureFileSequence from '../hooks/useStructureFileSequence';
|
|
12
12
|
import useTranscriptIsoformSelection from '../hooks/useTranscriptIsoformSelection';
|
|
13
13
|
import { launch3DProteinView } from '../utils/launchViewUtils';
|
|
14
|
-
import {
|
|
14
|
+
import { stripStopCodon } from '../utils/util';
|
|
15
15
|
const useStyles = makeStyles()(theme => ({
|
|
16
16
|
dialogContent: {
|
|
17
17
|
marginTop: theme.spacing(6),
|
|
@@ -62,7 +62,6 @@ const UserProvidedStructure = observer(function UserProvidedStructure({ feature,
|
|
|
62
62
|
data: structureData,
|
|
63
63
|
userProvidedTranscriptSequence: protein.seq,
|
|
64
64
|
alignmentAlgorithm,
|
|
65
|
-
displayName: `Protein view ${getGeneDisplayName(feature)} - ${getTranscriptDisplayName(selectedTranscript)}`,
|
|
66
65
|
});
|
|
67
66
|
handleClose();
|
|
68
67
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared launch-button wiring for the action components: holds the launch
|
|
3
|
+
* error state and returns a `runLaunch` factory that closes any open menu,
|
|
4
|
+
* runs the launch via safeLaunch, and surfaces failures inline.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useSafeLaunch(onSuccess: () => void, onBeforeLaunch?: () => void): {
|
|
7
|
+
runLaunch: (fn: () => void | Promise<void>) => () => void;
|
|
8
|
+
launchError: unknown;
|
|
9
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { safeLaunch } from '../utils/launchHelpers';
|
|
3
|
+
/**
|
|
4
|
+
* Shared launch-button wiring for the action components: holds the launch
|
|
5
|
+
* error state and returns a `runLaunch` factory that closes any open menu,
|
|
6
|
+
* runs the launch via safeLaunch, and surfaces failures inline.
|
|
7
|
+
*/
|
|
8
|
+
export function useSafeLaunch(onSuccess, onBeforeLaunch) {
|
|
9
|
+
const [launchError, setLaunchError] = useState();
|
|
10
|
+
const runLaunch = (fn) => () => {
|
|
11
|
+
onBeforeLaunch?.();
|
|
12
|
+
void safeLaunch(fn, onSuccess, setLaunchError);
|
|
13
|
+
};
|
|
14
|
+
return { runLaunch, launchError };
|
|
15
|
+
}
|
|
@@ -3,7 +3,7 @@ declare global {
|
|
|
3
3
|
JBrowsePluginMsaView?: unknown;
|
|
4
4
|
}
|
|
5
5
|
}
|
|
6
|
-
import type { AbstractSessionModel, Feature } from '@jbrowse/core/util';
|
|
6
|
+
import type { AbstractSessionModel, Feature, SessionWithAddTracks } from '@jbrowse/core/util';
|
|
7
7
|
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view';
|
|
8
8
|
export declare const ALPHAFOLD_VERSION = "v6";
|
|
9
9
|
export declare function getAlphaFoldStructureUrl(uniprotId: string, version?: string): string;
|
|
@@ -20,28 +20,26 @@ interface LaunchViewParams {
|
|
|
20
20
|
selectedTranscript?: Feature;
|
|
21
21
|
uniprotId?: string;
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
export declare function launch3DProteinView({ session, view, feature, selectedTranscript, uniprotId, url, data, userProvidedTranscriptSequence, alignmentAlgorithm, displayName, connectedMsaViewId, sideBySide, }: LaunchViewParams & {
|
|
23
|
+
interface Launch3DExtraParams {
|
|
25
24
|
url?: string;
|
|
26
25
|
data?: string;
|
|
27
26
|
userProvidedTranscriptSequence?: string;
|
|
28
27
|
alignmentAlgorithm?: string;
|
|
29
28
|
displayName?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function formatViewName(prefix: string, feature: Feature, selectedTranscript?: Feature, uniprotId?: string): string;
|
|
31
|
+
export declare function launch3DProteinView({ session, view, feature, selectedTranscript, uniprotId, url, data, userProvidedTranscriptSequence, alignmentAlgorithm, displayName, connectedMsaViewId, sideBySide, }: LaunchViewParams & Launch3DExtraParams & {
|
|
30
32
|
connectedMsaViewId?: string;
|
|
31
33
|
sideBySide?: boolean;
|
|
32
34
|
}): import("@jbrowse/core/util").AbstractViewModel;
|
|
33
|
-
export declare function launch1DProteinView({ session, view, feature, selectedTranscript, uniprotId, confidenceUrl, }: LaunchViewParams & {
|
|
35
|
+
export declare function launch1DProteinView({ session, view, feature, selectedTranscript, uniprotId, confidenceUrl, }: Omit<LaunchViewParams, 'session' | 'uniprotId'> & {
|
|
36
|
+
session: SessionWithAddTracks;
|
|
37
|
+
uniprotId: string;
|
|
34
38
|
confidenceUrl?: string;
|
|
35
39
|
}): Promise<void>;
|
|
36
40
|
export declare function launchMsaView({ session, view, feature, selectedTranscript, uniprotId, displayName, }: LaunchViewParams & {
|
|
37
41
|
displayName?: string;
|
|
38
42
|
}): import("@jbrowse/core/util").AbstractViewModel | undefined;
|
|
39
43
|
export declare function hasMsaViewPlugin(): boolean;
|
|
40
|
-
export declare function launch3DProteinViewWithMsa(params: LaunchViewParams &
|
|
41
|
-
url?: string;
|
|
42
|
-
data?: string;
|
|
43
|
-
userProvidedTranscriptSequence?: string;
|
|
44
|
-
alignmentAlgorithm?: string;
|
|
45
|
-
displayName?: string;
|
|
46
|
-
}): import("@jbrowse/core/util").AbstractViewModel | undefined;
|
|
44
|
+
export declare function launch3DProteinViewWithMsa(params: LaunchViewParams & Launch3DExtraParams): import("@jbrowse/core/util").AbstractViewModel | undefined;
|
|
47
45
|
export {};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getLaunchSideBySide, launchViewSideBySide } from './sideBySide';
|
|
1
|
+
import { maybeLaunchSideBySide } from './sideBySide';
|
|
3
2
|
import { getGeneDisplayName, getTranscriptDisplayName } from './util';
|
|
4
3
|
import { launchProteinAnnotationView } from '../components/launchProteinAnnotationView';
|
|
5
4
|
export const ALPHAFOLD_VERSION = 'v6';
|
|
@@ -77,15 +76,14 @@ export function launch3DProteinView({ session, view, feature, selectedTranscript
|
|
|
77
76
|
formatViewName('Protein view', feature, selectedTranscript, uniprotId),
|
|
78
77
|
};
|
|
79
78
|
const proteinView = session.addView('ProteinView', snap);
|
|
80
|
-
|
|
81
|
-
launchViewSideBySide(session, proteinView.id);
|
|
82
|
-
}
|
|
79
|
+
maybeLaunchSideBySide(session, proteinView.id, sideBySide);
|
|
83
80
|
return proteinView;
|
|
84
81
|
}
|
|
82
|
+
// The 1D annotation view adds temporary tracks/assemblies, so it requires a
|
|
83
|
+
// SessionWithAddTracks and a known uniprotId. Demanding both in the signature
|
|
84
|
+
// forces callers to narrow up front — there's no silent no-op when a wide
|
|
85
|
+
// session or missing id slips through.
|
|
85
86
|
export async function launch1DProteinView({ session, view, feature, selectedTranscript, uniprotId, confidenceUrl, }) {
|
|
86
|
-
if (!uniprotId || !isSessionWithAddTracks(session)) {
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
87
|
await launchProteinAnnotationView({
|
|
90
88
|
session,
|
|
91
89
|
selectedTranscript,
|
|
@@ -9,3 +9,8 @@ export declare function setLaunchSideBySide(value: boolean): void;
|
|
|
9
9
|
* new right panel). No-op on sessions without workspaces support.
|
|
10
10
|
*/
|
|
11
11
|
export declare function launchViewSideBySide(session: AbstractSessionModel, viewId: string): void;
|
|
12
|
+
/**
|
|
13
|
+
* Apply the side-by-side split honoring an explicit override, falling back to
|
|
14
|
+
* the launch-dialog localStorage preference when undefined.
|
|
15
|
+
*/
|
|
16
|
+
export declare function maybeLaunchSideBySide(session: AbstractSessionModel, viewId: string, sideBySide?: boolean): void;
|
|
@@ -31,3 +31,12 @@ export function launchViewSideBySide(session, viewId) {
|
|
|
31
31
|
session.setUseWorkspaces(true);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Apply the side-by-side split honoring an explicit override, falling back to
|
|
36
|
+
* the launch-dialog localStorage preference when undefined.
|
|
37
|
+
*/
|
|
38
|
+
export function maybeLaunchSideBySide(session, viewId, sideBySide) {
|
|
39
|
+
if (sideBySide ?? getLaunchSideBySide()) {
|
|
40
|
+
launchViewSideBySide(session, viewId);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveShortLaunch } from './resolveShortLaunch';
|
|
2
|
-
import {
|
|
2
|
+
import { maybeLaunchSideBySide } from '../LaunchProteinView/utils/sideBySide';
|
|
3
3
|
export default function LaunchProteinViewExtensionPointF(pluginManager) {
|
|
4
4
|
pluginManager.addToExtensionPoint('LaunchView-ProteinView',
|
|
5
5
|
// LaunchView extension points are typed as transformers `(extendee, props)
|
|
@@ -7,7 +7,7 @@ export default function LaunchProteinViewExtensionPointF(pluginManager) {
|
|
|
7
7
|
// handlers and ignores the return value. Casting away the signature
|
|
8
8
|
// mismatch rather than fabricating a fake return.
|
|
9
9
|
// @ts-expect-error
|
|
10
|
-
async ({ session, url, uniprotId, transcriptId, userProvidedTranscriptSequence, feature, connectedViewId, connectedView, alignmentAlgorithm, displayName, height, showControls, showHighlight, zoomToBaseLevel, sideBySide, }) => {
|
|
10
|
+
async ({ session, url, uniprotId, transcriptId, userProvidedTranscriptSequence, feature, connectedViewId, connectedView, alignmentAlgorithm, displayName, height, showControls, showHighlight, zoomToBaseLevel, sideBySide, initialSelection, }) => {
|
|
11
11
|
// Short-URL form: `uniprotId` + `transcriptId` + `connectedView` (no
|
|
12
12
|
// explicit `url`/`feature`/sequence). Derive the structure URL, the
|
|
13
13
|
// transcript feature, and the translated sequence from the connected
|
|
@@ -31,7 +31,10 @@ export default function LaunchProteinViewExtensionPointF(pluginManager) {
|
|
|
31
31
|
}
|
|
32
32
|
const finalUrl = url ?? resolved?.url;
|
|
33
33
|
if (!finalUrl) {
|
|
34
|
-
|
|
34
|
+
const message = 'No url or uniprotId provided when launching protein view';
|
|
35
|
+
console.error(message);
|
|
36
|
+
session.notify(`Could not launch protein view: ${message}`, 'error');
|
|
37
|
+
return;
|
|
35
38
|
}
|
|
36
39
|
// A session spec launches each view independently with an auto-generated
|
|
37
40
|
// id, so it cannot pre-compute a connectedViewId to cross-reference. When
|
|
@@ -64,11 +67,12 @@ export default function LaunchProteinViewExtensionPointF(pluginManager) {
|
|
|
64
67
|
'',
|
|
65
68
|
feature: resolved?.feature ?? feature,
|
|
66
69
|
connectedViewId: resolvedConnectedViewId,
|
|
70
|
+
initialSelection,
|
|
67
71
|
},
|
|
68
72
|
],
|
|
69
73
|
});
|
|
70
|
-
if (ownsConnectedView
|
|
71
|
-
|
|
74
|
+
if (ownsConnectedView) {
|
|
75
|
+
maybeLaunchSideBySide(session, proteinView.id, sideBySide);
|
|
72
76
|
}
|
|
73
77
|
});
|
|
74
78
|
}
|
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
import type { Structure } from 'molstar/lib/mol-model/structure';
|
|
2
2
|
import type { PluginContext } from 'molstar/lib/mol-plugin/context';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Which residues a highlight/selection should cover, in the plugin's native
|
|
5
|
+
* 0-based structure-sequence coordinates (see coordinates.ts). `range` is the
|
|
6
|
+
* half-open span [start, end); `list` is an explicit set of positions. The one
|
|
7
|
+
* conversion to molstar's 1-based inclusive label_seq_id happens in specToTest
|
|
8
|
+
* below — the single boundary where structure positions cross into molstar.
|
|
9
|
+
*/
|
|
10
|
+
export type ResidueSpec = {
|
|
11
|
+
kind: 'range';
|
|
12
|
+
start: number;
|
|
13
|
+
end: number;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'list';
|
|
6
16
|
residues: number[];
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
mode: InteractivityMode;
|
|
16
|
-
}): Promise<void>;
|
|
17
|
-
export declare function applyLociInteractivitySingle({ structure, selectedResidue, plugin, mode, }: {
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Reconcile one interactivity channel (hover-`highlight` or click-`select`) to
|
|
20
|
+
* the desired residue spec. Passing `undefined` (or an empty `list`) clears the
|
|
21
|
+
* channel, so callers describe the target state declaratively rather than
|
|
22
|
+
* juggling clear/apply calls.
|
|
23
|
+
*/
|
|
24
|
+
export declare function setMolstarLoci({ structure, plugin, channel, spec, }: {
|
|
18
25
|
structure: Structure;
|
|
19
|
-
selectedResidue: number;
|
|
20
26
|
plugin: PluginContext;
|
|
21
|
-
|
|
27
|
+
channel: 'highlight' | 'select';
|
|
28
|
+
spec: ResidueSpec | undefined;
|
|
22
29
|
}): Promise<void>;
|
|
23
|
-
export {};
|
|
@@ -1,67 +1,39 @@
|
|
|
1
1
|
import loadMolstar from './loadMolstar';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
const seqId = (Q) => Q.struct.atomProperty.macromolecular.label_seq_id();
|
|
3
|
+
const specToTest = (spec) => spec.kind === 'range'
|
|
4
|
+
? Q => Q.core.logic.and([
|
|
5
|
+
Q.core.rel.gre([seqId(Q), spec.start + 1]),
|
|
6
|
+
Q.core.rel.lte([seqId(Q), spec.end]),
|
|
7
|
+
])
|
|
8
|
+
: Q => Q.core.logic.or(spec.residues.map(pos => Q.core.rel.eq([seqId(Q), pos + 1])));
|
|
9
|
+
const isActive = (spec) => spec !== undefined &&
|
|
10
|
+
(spec.kind === 'range' ? spec.end > spec.start : spec.residues.length > 0);
|
|
11
|
+
/**
|
|
12
|
+
* Reconcile one interactivity channel (hover-`highlight` or click-`select`) to
|
|
13
|
+
* the desired residue spec. Passing `undefined` (or an empty `list`) clears the
|
|
14
|
+
* channel, so callers describe the target state declaratively rather than
|
|
15
|
+
* juggling clear/apply calls.
|
|
16
|
+
*/
|
|
17
|
+
export async function setMolstarLoci({ structure, plugin, channel, spec, }) {
|
|
18
|
+
const { lociHighlights, lociSelects } = plugin.managers.interactivity;
|
|
19
|
+
if (channel === 'highlight') {
|
|
20
|
+
lociHighlights.clearHighlights();
|
|
11
21
|
}
|
|
12
22
|
else {
|
|
13
|
-
|
|
14
|
-
plugin.managers.interactivity.lociSelects.select({ loci });
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
export async function applyLociInteractivityMultiple({ structure, residues, plugin, mode, }) {
|
|
18
|
-
if (mode === 'clear' || residues.length === 0) {
|
|
19
|
-
clearLoci(plugin);
|
|
20
|
-
return;
|
|
23
|
+
lociSelects.deselectAll();
|
|
21
24
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
clearLoci(plugin);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const { StructureSelection, Script } = await loadMolstar();
|
|
39
|
-
const sel = Script.getStructureSelection(Q => Q.struct.generator.atomGroups({
|
|
40
|
-
'residue-test': Q.core.logic.and([
|
|
41
|
-
Q.core.rel.gre([
|
|
42
|
-
Q.struct.atomProperty.macromolecular.label_seq_id(),
|
|
43
|
-
startResidue,
|
|
44
|
-
]),
|
|
45
|
-
Q.core.rel.lte([
|
|
46
|
-
Q.struct.atomProperty.macromolecular.label_seq_id(),
|
|
47
|
-
endResidue,
|
|
48
|
-
]),
|
|
49
|
-
]),
|
|
50
|
-
'group-by': Q.struct.atomProperty.macromolecular.residueKey(),
|
|
51
|
-
}), structure);
|
|
52
|
-
const loci = StructureSelection.toLociWithSourceUnits(sel);
|
|
53
|
-
applyLoci(plugin, loci, mode);
|
|
54
|
-
}
|
|
55
|
-
export async function applyLociInteractivitySingle({ structure, selectedResidue, plugin, mode, }) {
|
|
56
|
-
if (mode === 'clear') {
|
|
57
|
-
clearLoci(plugin);
|
|
58
|
-
return;
|
|
25
|
+
if (isActive(spec)) {
|
|
26
|
+
const { StructureSelection, Script } = await loadMolstar();
|
|
27
|
+
const sel = Script.getStructureSelection(Q => Q.struct.generator.atomGroups({
|
|
28
|
+
'residue-test': specToTest(spec)(Q),
|
|
29
|
+
'group-by': Q.struct.atomProperty.macromolecular.residueKey(),
|
|
30
|
+
}), structure);
|
|
31
|
+
const loci = StructureSelection.toLociWithSourceUnits(sel);
|
|
32
|
+
if (channel === 'highlight') {
|
|
33
|
+
lociHighlights.highlight({ loci });
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
lociSelects.select({ loci });
|
|
37
|
+
}
|
|
59
38
|
}
|
|
60
|
-
const { StructureSelection } = await loadMolstar();
|
|
61
|
-
const sel = await getMolstarStructureSelection({
|
|
62
|
-
structure,
|
|
63
|
-
selectedResidue: selectedResidue + 1,
|
|
64
|
-
});
|
|
65
|
-
const loci = StructureSelection.toLociWithSourceUnits(sel);
|
|
66
|
-
applyLoci(plugin, loci, mode);
|
|
67
39
|
}
|
|
@@ -4,5 +4,5 @@ import type { JBrowsePluginProteinStructureModel } from '../model';
|
|
|
4
4
|
declare const FeatureBar: ({ feature, model, }: {
|
|
5
5
|
feature: UniProtFeature;
|
|
6
6
|
model: JBrowsePluginProteinStructureModel;
|
|
7
|
-
}) => React.JSX.Element;
|
|
7
|
+
}) => React.JSX.Element | null;
|
|
8
8
|
export default FeatureBar;
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import { Tooltip } from '@mui/material';
|
|
3
3
|
import { observer } from 'mobx-react';
|
|
4
|
+
import { setMolstarLoci } from '../applyLociInteractivity';
|
|
4
5
|
import { CHAR_WIDTH, HOVERED_BORDER, SELECTED_BORDER } from '../constants';
|
|
5
|
-
import { selectResidueRange } from '../highlightResidueRange';
|
|
6
6
|
import { getFeatureColor } from '../hooks/useUniProtFeatures';
|
|
7
7
|
import { clickProteinToGenome } from '../proteinToGenomeMapping';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Maps a feature's structure range onto alignment columns, returning both the
|
|
10
|
+
* alignment range (for hover) and pixel geometry. Returns undefined when either
|
|
11
|
+
* endpoint has no alignment column, so unmappable features aren't drawn at a
|
|
12
|
+
* misleading position.
|
|
13
|
+
*/
|
|
14
|
+
function getFeatureLayout(feature, structurePositionToAlignmentMap) {
|
|
15
|
+
const start = structurePositionToAlignmentMap?.[feature.start - 1];
|
|
16
|
+
const end = structurePositionToAlignmentMap?.[feature.end - 1];
|
|
17
|
+
return start === undefined || end === undefined
|
|
18
|
+
? undefined
|
|
19
|
+
: {
|
|
20
|
+
range: { start, end },
|
|
21
|
+
left: start * CHAR_WIDTH,
|
|
22
|
+
width: Math.max((end - start + 1) * CHAR_WIDTH, 3),
|
|
23
|
+
};
|
|
22
24
|
}
|
|
23
25
|
function FeatureTooltipContent({ feature }) {
|
|
24
26
|
return (React.createElement("div", null,
|
|
@@ -35,11 +37,11 @@ const FeatureBar = observer(function FeatureBar({ feature, model, }) {
|
|
|
35
37
|
const [isHovered, setIsHovered] = useState(false);
|
|
36
38
|
const { molstarPluginContext, selectedFeatureId, structurePositionToAlignmentMap, } = model;
|
|
37
39
|
const isSelected = selectedFeatureId === feature.uniqueId;
|
|
40
|
+
const layout = getFeatureLayout(feature, structurePositionToAlignmentMap);
|
|
38
41
|
const handleMouseEnter = () => {
|
|
39
42
|
setIsHovered(true);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
model.setAlignmentHoverRange(range);
|
|
43
|
+
if (layout) {
|
|
44
|
+
model.setAlignmentHoverRange(layout.range);
|
|
43
45
|
}
|
|
44
46
|
};
|
|
45
47
|
const handleMouseLeave = () => {
|
|
@@ -50,20 +52,17 @@ const FeatureBar = observer(function FeatureBar({ feature, model, }) {
|
|
|
50
52
|
const structure = model.molstarStructure;
|
|
51
53
|
const newSelected = !isSelected;
|
|
52
54
|
if (structure && molstarPluginContext) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
molstarPluginContext.managers.interactivity.lociSelects.deselectAll();
|
|
66
|
-
}
|
|
55
|
+
setMolstarLoci({
|
|
56
|
+
structure,
|
|
57
|
+
plugin: molstarPluginContext,
|
|
58
|
+
channel: 'select',
|
|
59
|
+
spec: newSelected
|
|
60
|
+
? { kind: 'range', start: feature.start - 1, end: feature.end }
|
|
61
|
+
: undefined,
|
|
62
|
+
}).catch((e) => {
|
|
63
|
+
console.error(e);
|
|
64
|
+
model.setError(e);
|
|
65
|
+
});
|
|
67
66
|
}
|
|
68
67
|
if (newSelected) {
|
|
69
68
|
model.setSelectedFeatureId(feature.uniqueId);
|
|
@@ -81,7 +80,10 @@ const FeatureBar = observer(function FeatureBar({ feature, model, }) {
|
|
|
81
80
|
model.setClickedStructureRange(undefined);
|
|
82
81
|
}
|
|
83
82
|
};
|
|
84
|
-
|
|
83
|
+
if (!layout) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const { left, width } = layout;
|
|
85
87
|
const color = getFeatureColor(feature.type);
|
|
86
88
|
return (React.createElement(Tooltip, { title: React.createElement(FeatureTooltipContent, { feature: feature }), followCursor: true },
|
|
87
89
|
React.createElement("div", { "data-testid": `protein-feature-${feature.type}`, "data-feature-id": feature.uniqueId, "data-feature-start": feature.start, "data-feature-end": feature.end, onClick: () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import
|
|
2
|
+
import CloseIcon from '@mui/icons-material/Close';
|
|
3
|
+
import { IconButton, Tooltip } from '@mui/material';
|
|
3
4
|
import { observer } from 'mobx-react';
|
|
4
5
|
import { HIDE_BUTTON_COLOR } from '../constants';
|
|
5
6
|
const FeatureTypeLabel = observer(function FeatureTypeLabel({ type, labelWidth, model, }) {
|
|
@@ -19,16 +20,11 @@ const FeatureTypeLabel = observer(function FeatureTypeLabel({ type, labelWidth,
|
|
|
19
20
|
justifyContent: 'flex-end',
|
|
20
21
|
gap: 2,
|
|
21
22
|
} },
|
|
22
|
-
React.createElement(
|
|
23
|
+
React.createElement(IconButton, { onClick: e => {
|
|
23
24
|
e.stopPropagation();
|
|
24
25
|
model.hideFeatureType(type);
|
|
25
|
-
},
|
|
26
|
-
|
|
27
|
-
color: HIDE_BUTTON_COLOR,
|
|
28
|
-
fontWeight: 'bold',
|
|
29
|
-
fontSize: 8,
|
|
30
|
-
lineHeight: 1,
|
|
31
|
-
}, title: `Hide ${type} track` }, "x"),
|
|
26
|
+
}, title: `Hide ${type} track`, sx: { p: 0, color: HIDE_BUTTON_COLOR } },
|
|
27
|
+
React.createElement(CloseIcon, { sx: { fontSize: model.trackHeight } })),
|
|
32
28
|
React.createElement("span", { style: { overflow: 'hidden', textOverflow: 'ellipsis' } }, type))));
|
|
33
29
|
});
|
|
34
30
|
export default FeatureTypeLabel;
|