jbrowse-plugin-protein3d 0.4.14 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/LaunchProteinView/components/FoldseekActionMenu.js +15 -14
  2. package/dist/LaunchProteinView/components/ProteinViewActions.js +27 -15
  3. package/dist/LaunchProteinView/components/UserProvidedStructure.js +1 -2
  4. package/dist/LaunchProteinView/hooks/useSafeLaunch.d.ts +9 -0
  5. package/dist/LaunchProteinView/hooks/useSafeLaunch.js +15 -0
  6. package/dist/LaunchProteinView/utils/launchViewUtils.d.ts +9 -11
  7. package/dist/LaunchProteinView/utils/launchViewUtils.js +6 -8
  8. package/dist/LaunchProteinView/utils/sideBySide.d.ts +5 -0
  9. package/dist/LaunchProteinView/utils/sideBySide.js +9 -0
  10. package/dist/LaunchProteinViewExtensionPoint/index.js +7 -4
  11. package/dist/ProteinView/applyLociInteractivity.d.ts +23 -17
  12. package/dist/ProteinView/applyLociInteractivity.js +33 -61
  13. package/dist/ProteinView/components/FeatureBar.d.ts +1 -1
  14. package/dist/ProteinView/components/FeatureBar.js +35 -33
  15. package/dist/ProteinView/components/FeatureTypeLabel.js +5 -9
  16. package/dist/ProteinView/components/ProteinFeatureTrack.js +7 -15
  17. package/dist/ProteinView/components/ProteinViewHeader.js +9 -1
  18. package/dist/ProteinView/components/ResidueValueTrack.js +26 -15
  19. package/dist/ProteinView/hooks/useProteinFeatureTrackData.d.ts +1 -0
  20. package/dist/ProteinView/hooks/useProteinFeatureTrackData.js +3 -2
  21. package/dist/ProteinView/model.d.ts +12 -0
  22. package/dist/ProteinView/structureModel.d.ts +14 -5
  23. package/dist/ProteinView/structureModel.js +69 -92
  24. package/dist/ProteinView/subscribeMolstarInteraction.d.ts +8 -0
  25. package/dist/ProteinView/subscribeMolstarInteraction.js +1 -1
  26. package/dist/ProteinView/util.d.ts +0 -5
  27. package/dist/ProteinView/util.js +0 -11
  28. package/dist/jbrowse-plugin-protein3d.umd.production.min.js +15 -15
  29. package/dist/jbrowse-plugin-protein3d.umd.production.min.js.map +4 -4
  30. package/dist/version.d.ts +1 -1
  31. package/dist/version.js +1 -1
  32. package/package.json +1 -1
  33. package/src/LaunchProteinView/components/FoldseekActionMenu.tsx +22 -17
  34. package/src/LaunchProteinView/components/ProteinViewActions.tsx +32 -17
  35. package/src/LaunchProteinView/components/UserProvidedStructure.tsx +1 -6
  36. package/src/LaunchProteinView/hooks/useSafeLaunch.ts +17 -0
  37. package/src/LaunchProteinView/utils/launchViewUtils.ts +30 -29
  38. package/src/LaunchProteinView/utils/sideBySide.ts +14 -0
  39. package/src/LaunchProteinViewExtensionPoint/index.ts +8 -9
  40. package/src/ProteinView/applyLociInteractivity.ts +62 -114
  41. package/src/ProteinView/components/FeatureBar.tsx +36 -44
  42. package/src/ProteinView/components/FeatureTypeLabel.tsx +6 -11
  43. package/src/ProteinView/components/ProteinFeatureTrack.tsx +5 -17
  44. package/src/ProteinView/components/ProteinViewHeader.tsx +15 -0
  45. package/src/ProteinView/components/ResidueValueTrack.tsx +40 -23
  46. package/src/ProteinView/hooks/useProteinFeatureTrackData.ts +6 -2
  47. package/src/ProteinView/structureModel.ts +90 -108
  48. package/src/ProteinView/subscribeMolstarInteraction.ts +9 -1
  49. package/src/ProteinView/util.ts +0 -25
  50. package/src/version.ts +1 -1
  51. package/dist/ProteinView/highlightResidueRange.d.ts +0 -14
  52. package/dist/ProteinView/highlightResidueRange.js +0 -19
  53. 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 ?? hasValidCaCoords(hit.tCa, hit.tSeq);
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: handleLaunch1D }, "Launch 1D protein annotation view")) : null,
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 { getLaunchMissingReasons, safeLaunch } from '../utils/launchHelpers';
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
- key: '1d',
69
- title: 'Launch 1D protein annotation view',
70
- description: 'View protein features and annotations as a linear track',
71
- onClick: handleLaunch1DView,
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 { getGeneDisplayName, getTranscriptDisplayName, stripStopCodon, } from '../utils/util';
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
- export declare function formatViewName(prefix: string, feature: Feature, selectedTranscript?: Feature, uniprotId?: string): string;
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 { isSessionWithAddTracks } from '@jbrowse/core/util';
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
- if (sideBySide ?? getLaunchSideBySide()) {
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 { getLaunchSideBySide, launchViewSideBySide, } from '../LaunchProteinView/utils/sideBySide';
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)
@@ -31,7 +31,10 @@ export default function LaunchProteinViewExtensionPointF(pluginManager) {
31
31
  }
32
32
  const finalUrl = url ?? resolved?.url;
33
33
  if (!finalUrl) {
34
- throw new Error('No url or uniprotId provided when launching protein view');
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
@@ -67,8 +70,8 @@ export default function LaunchProteinViewExtensionPointF(pluginManager) {
67
70
  },
68
71
  ],
69
72
  });
70
- if (ownsConnectedView && (sideBySide ?? getLaunchSideBySide())) {
71
- launchViewSideBySide(session, proteinView.id);
73
+ if (ownsConnectedView) {
74
+ maybeLaunchSideBySide(session, proteinView.id, sideBySide);
72
75
  }
73
76
  });
74
77
  }
@@ -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
- type InteractivityMode = 'highlight' | 'select' | 'clear';
4
- export declare function applyLociInteractivityMultiple({ structure, residues, plugin, mode, }: {
5
- structure: Structure;
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
- plugin: PluginContext;
8
- mode: InteractivityMode;
9
- }): Promise<void>;
10
- export declare function applyLociInteractivity({ structure, startResidue, endResidue, plugin, mode, }: {
11
- structure: Structure;
12
- startResidue: number;
13
- endResidue: number;
14
- plugin: PluginContext;
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
- mode: InteractivityMode;
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
- import { getMolstarStructureSelection } from './util';
3
- function clearLoci(plugin) {
4
- plugin.managers.interactivity.lociHighlights.clearHighlights();
5
- plugin.managers.interactivity.lociSelects.deselectAll();
6
- }
7
- function applyLoci(plugin, loci, mode) {
8
- if (mode === 'highlight') {
9
- plugin.managers.interactivity.lociHighlights.clearHighlights();
10
- plugin.managers.interactivity.lociHighlights.highlight({ loci });
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
- plugin.managers.interactivity.lociSelects.deselectAll();
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
- const { StructureSelection, Script } = await loadMolstar();
23
- const sel = Script.getStructureSelection(Q => Q.struct.generator.atomGroups({
24
- 'residue-test': Q.core.logic.or(residues.map(residue => Q.core.rel.eq([
25
- Q.struct.atomProperty.macromolecular.label_seq_id(),
26
- residue,
27
- ]))),
28
- 'group-by': Q.struct.atomProperty.macromolecular.residueKey(),
29
- }), structure);
30
- const loci = StructureSelection.toLociWithSourceUnits(sel);
31
- applyLoci(plugin, loci, mode);
32
- }
33
- export async function applyLociInteractivity({ structure, startResidue, endResidue, plugin, mode, }) {
34
- if (mode === 'clear') {
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
- function getFeatureAlignmentRange(feature, structurePositionToAlignmentMap) {
9
- const startAlignmentPos = structurePositionToAlignmentMap?.[feature.start - 1];
10
- const endAlignmentPos = structurePositionToAlignmentMap?.[feature.end - 1];
11
- return startAlignmentPos !== undefined && endAlignmentPos !== undefined
12
- ? { start: startAlignmentPos, end: endAlignmentPos }
13
- : undefined;
14
- }
15
- function getFeatureGeometry(feature, structurePositionToAlignmentMap) {
16
- const startAlnPos = structurePositionToAlignmentMap?.[feature.start - 1] ?? feature.start - 1;
17
- const endAlnPos = structurePositionToAlignmentMap?.[feature.end - 1] ?? feature.end - 1;
18
- return {
19
- left: startAlnPos * CHAR_WIDTH,
20
- width: Math.max((endAlnPos - startAlnPos + 1) * CHAR_WIDTH, 3),
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
- const range = getFeatureAlignmentRange(feature, structurePositionToAlignmentMap);
41
- if (range) {
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
- if (newSelected) {
54
- selectResidueRange({
55
- structure,
56
- startResidue: feature.start,
57
- endResidue: feature.end,
58
- plugin: molstarPluginContext,
59
- }).catch((e) => {
60
- console.error(e);
61
- model.setError(e);
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
- const { left, width } = getFeatureGeometry(feature, structurePositionToAlignmentMap);
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 { Tooltip } from '@mui/material';
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("span", { onClick: e => {
23
+ React.createElement(IconButton, { onClick: e => {
23
24
  e.stopPropagation();
24
25
  model.hideFeatureType(type);
25
- }, style: {
26
- cursor: 'pointer',
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;