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.
Files changed (70) hide show
  1. package/README.md +10 -0
  2. package/dist/AddHighlightModel/ProteinToMsaHoverSync.js +8 -4
  3. package/dist/AddHighlightModel/findConnectedMsaView.d.ts +23 -0
  4. package/dist/AddHighlightModel/findConnectedMsaView.js +23 -0
  5. package/dist/LaunchProteinView/components/AlphaFoldDBSearch.js +3 -3
  6. package/dist/LaunchProteinView/components/AlphaFoldDBSearchStatus.d.ts +2 -4
  7. package/dist/LaunchProteinView/components/IdentifierSelector.js +2 -36
  8. package/dist/LaunchProteinView/components/IsoformSequencesToggle.d.ts +2 -5
  9. package/dist/LaunchProteinView/components/MSATable.d.ts +2 -5
  10. package/dist/LaunchProteinView/components/TabPanel.js +4 -2
  11. package/dist/LaunchProteinView/components/TranscriptSelector.d.ts +2 -4
  12. package/dist/LaunchProteinView/components/TranscriptSelector.js +16 -32
  13. package/dist/LaunchProteinView/hooks/useAlphaFoldDBSearch.d.ts +2 -8
  14. package/dist/LaunchProteinView/hooks/useIsoformProteinSequences.d.ts +2 -4
  15. package/dist/LaunchProteinView/hooks/useTranscriptIsoformSelection.d.ts +2 -8
  16. package/dist/LaunchProteinView/hooks/useTranscriptSelection.d.ts +2 -4
  17. package/dist/LaunchProteinView/hooks/useTranscriptSelection.js +3 -3
  18. package/dist/LaunchProteinView/services/lookupMethods.js +2 -20
  19. package/dist/LaunchProteinView/utils/launchViewUtils.js +8 -6
  20. package/dist/LaunchProteinView/utils/util.d.ts +24 -13
  21. package/dist/LaunchProteinView/utils/util.js +60 -49
  22. package/dist/ProteinView/__fixtures__/structureFixtures.d.ts +7 -0
  23. package/dist/ProteinView/__fixtures__/structureFixtures.js +16 -0
  24. package/dist/ProteinView/applyLociInteractivity.d.ts +4 -1
  25. package/dist/ProteinView/applyLociInteractivity.js +9 -1
  26. package/dist/ProteinView/chooseMappedEntity.d.ts +32 -0
  27. package/dist/ProteinView/chooseMappedEntity.js +71 -0
  28. package/dist/ProteinView/components/FeatureBar.js +1 -0
  29. package/dist/ProteinView/components/ProteinAlignment.js +14 -0
  30. package/dist/ProteinView/extractStructureSequences.d.ts +12 -0
  31. package/dist/ProteinView/extractStructureSequences.js +9 -1
  32. package/dist/ProteinView/loadStructureData.d.ts +2 -1
  33. package/dist/ProteinView/loadStructureData.js +4 -4
  34. package/dist/ProteinView/model.d.ts +33 -9
  35. package/dist/ProteinView/structureModel.d.ts +40 -2
  36. package/dist/ProteinView/structureModel.js +87 -33
  37. package/dist/ProteinView/subscribeMolstarInteraction.d.ts +3 -0
  38. package/dist/ProteinView/subscribeMolstarInteraction.js +1 -0
  39. package/dist/jbrowse-plugin-protein3d.umd.production.min.js +16 -16
  40. package/dist/jbrowse-plugin-protein3d.umd.production.min.js.map +4 -4
  41. package/dist/version.d.ts +1 -1
  42. package/dist/version.js +1 -1
  43. package/package.json +3 -1
  44. package/src/AddHighlightModel/ProteinToMsaHoverSync.tsx +9 -4
  45. package/src/AddHighlightModel/findConnectedMsaView.test.ts +53 -0
  46. package/src/AddHighlightModel/findConnectedMsaView.ts +35 -0
  47. package/src/LaunchProteinView/components/AlphaFoldDBSearch.tsx +5 -5
  48. package/src/LaunchProteinView/components/AlphaFoldDBSearchStatus.tsx +2 -1
  49. package/src/LaunchProteinView/components/IdentifierSelector.tsx +2 -37
  50. package/src/LaunchProteinView/components/IsoformSequencesToggle.tsx +2 -2
  51. package/src/LaunchProteinView/components/MSATable.tsx +2 -2
  52. package/src/LaunchProteinView/components/TabPanel.tsx +4 -2
  53. package/src/LaunchProteinView/components/TranscriptSelector.tsx +15 -33
  54. package/src/LaunchProteinView/hooks/useIsoformProteinSequences.ts +2 -3
  55. package/src/LaunchProteinView/hooks/useTranscriptSelection.ts +11 -13
  56. package/src/LaunchProteinView/services/lookupMethods.ts +2 -21
  57. package/src/LaunchProteinView/utils/launchViewUtils.ts +8 -6
  58. package/src/LaunchProteinView/utils/util.ts +98 -64
  59. package/src/ProteinView/__fixtures__/structureFixtures.ts +29 -0
  60. package/src/ProteinView/applyLociInteractivity.ts +12 -0
  61. package/src/ProteinView/chooseMappedEntity.test.ts +65 -0
  62. package/src/ProteinView/chooseMappedEntity.ts +97 -0
  63. package/src/ProteinView/components/FeatureBar.tsx +1 -0
  64. package/src/ProteinView/components/ProteinAlignment.tsx +19 -0
  65. package/src/ProteinView/extractStructureSequences.ts +20 -3
  66. package/src/ProteinView/loadStructureData.ts +6 -5
  67. package/src/ProteinView/structureLoader.test.ts +12 -9
  68. package/src/ProteinView/structureModel.ts +103 -38
  69. package/src/ProteinView/subscribeMolstarInteraction.ts +4 -0
  70. 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
  ![](img/1.png)
@@ -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
- const connectedMsaViewId = proteinView?.connectedMsaViewId;
13
- const msaView = connectedMsaViewId
14
- ? views.find(f => f.id === connectedMsaViewId)
15
- : undefined;
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: Record<string, {
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 { getDatabaseTypeForId } from '../utils/util';
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: getIdLabel(id) })),
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 { Feature } from '@jbrowse/core/util';
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: Record<string, {
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 { Feature } from '@jbrowse/core/util';
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: Record<string, {
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
- // this is from MUI example
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 }, value === index && React.createElement("div", null, children)));
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: Record<string, {
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, stripStopCodon, } from '../utils/util';
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
- const nonMatches = [];
8
- const noData = [];
9
- for (const f of isoforms) {
10
- const entry = isoformSequences[f.id()];
11
- if (!entry) {
12
- noData.push(f);
13
- }
14
- else if (structureSequence &&
15
- stripStopCodon(entry.seq) === structureSequence) {
16
- matches.push(f);
17
- }
18
- else {
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.toSorted(byLengthDesc).map(f => (React.createElement(MenuItem, { value: f.id(), key: f.id() },
27
- geneName,
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: Record<string, {
24
- feature: Feature;
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: Record<string, {
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: Record<string, {
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?: Record<string, {
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 { useMemo, useState } from 'react';
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 = useMemo(() => isoformSequences !== undefined
10
+ const autoSelection = isoformSequences !== undefined
11
11
  ? selectBestTranscript({
12
12
  options,
13
13
  isoformSequences,
14
14
  structureSequence,
15
15
  })?.id()
16
- : undefined, [options, structureSequence, isoformSequences]);
16
+ : undefined;
17
17
  return { userSelection: userSelection ?? autoSelection, setUserSelection };
18
18
  }
@@ -1,5 +1,5 @@
1
1
  import { jsonfetch } from '../../fetchUtils';
2
- import { getDatabaseTypeForId, isRecognizedDatabaseId, stripTrailingVersion, } from '../utils/util';
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 = buildXrefQuery(id);
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 `init`
98
- // keys below (msaUrl, colorSchemeName) and the connected* props are a runtime
99
- // contract with that plugin's model they are NOT type-checked here because we
100
- // only depend on it at runtime (gated by hasMsaViewPlugin()). If react-msaview
101
- // renames these, the launch silently degrades. Keep in step with that repo.
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
- * Get the database type for a recognized ID (used for UniProt xref queries)
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 declare function selectBestTranscript({ options, isoformSequences, structureSequence, }: {
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: Record<string, {
34
- feature: Feature;
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
- // Ensembl ID patterns - covers human (ENS), mouse (ENSMUS), zebrafish (ENSDAR), etc.
38
- const ensemblGenePattern = /^ENS[A-Z]*G\d+/i;
39
- const ensemblTranscriptPattern = /^ENS[A-Z]*T\d+/i;
40
- const ensemblProteinPattern = /^ENS[A-Z]*P\d+/i;
41
- // NCBI RefSeq ID patterns
42
- const refSeqTranscriptPattern = /^[NX][MR]_\d+/i;
43
- const refSeqProteinPattern = /^[NX]P_\d+/i;
44
- // CCDS pattern
45
- const ccdsPattern = /^CCDS\d+/i;
46
- // HGNC pattern (HGNC:12345)
47
- const hgncPattern = /^HGNC:\d+/i;
48
- /**
49
- * Check if an ID is a recognized database identifier that UniProt can map
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 (ensemblGenePattern.test(id) ||
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
- * Get the database type for a recognized ID (used for UniProt xref queries)
62
- */
63
- export function getDatabaseTypeForId(id) {
64
- if (ensemblGenePattern.test(id) ||
65
- ensemblTranscriptPattern.test(id) ||
66
- ensemblProteinPattern.test(id)) {
67
- return 'ensembl';
68
- }
69
- if (refSeqTranscriptPattern.test(id) || refSeqProteinPattern.test(id)) {
70
- return 'refseq';
71
- }
72
- if (ccdsPattern.test(id)) {
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 (hgncPattern.test(hgncStr)) {
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
- export function selectBestTranscript({ options, isoformSequences, structureSequence, }) {
210
- const exactMatch = options.find(f => structureSequence &&
211
- stripStopCodon(isoformSequences[f.id()]?.seq ?? '') === structureSequence);
212
- const longestWithData = options
213
- .filter(f => !!isoformSequences[f.id()])
214
- .toSorted((a, b) => isoformSequences[b.id()].seq.length -
215
- isoformSequences[a.id()].seq.length)[0];
216
- return exactMatch ?? longestWithData;
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
  }