jbrowse-plugin-protein3d 0.15.1 → 0.15.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 (66) hide show
  1. package/dist/LaunchProteinView/components/wiggleBandColors.js +1 -0
  2. package/dist/LaunchProteinViewExtensionPoint/index.js +14 -22
  3. package/dist/ProteinView/autoScroll.js +2 -3
  4. package/dist/ProteinView/components/AlignmentRuler.js +5 -5
  5. package/dist/ProteinView/components/ChainSelect.js +8 -4
  6. package/dist/ProteinView/components/ColorKey.js +6 -6
  7. package/dist/ProteinView/components/ColumnOverlays.js +46 -0
  8. package/dist/ProteinView/components/FeatureBar.js +34 -42
  9. package/dist/ProteinView/components/FeatureTypeLabel.js +8 -14
  10. package/dist/ProteinView/components/HeaderStructureRow.js +45 -11
  11. package/dist/ProteinView/components/MismatchShading.js +20 -0
  12. package/dist/ProteinView/components/ProteinAlignment.js +124 -102
  13. package/dist/ProteinView/components/ProteinAlignmentHelpButton.js +9 -8
  14. package/dist/ProteinView/components/ProteinAlignmentHelpDialog.js +2 -2
  15. package/dist/ProteinView/components/ProteinFeatureTrack.js +9 -19
  16. package/dist/ProteinView/components/ProteinViewHeader.js +26 -18
  17. package/dist/ProteinView/components/ResidueValueTrack.js +23 -30
  18. package/dist/ProteinView/components/SplitString.js +4 -94
  19. package/dist/ProteinView/constants.js +10 -2
  20. package/dist/ProteinView/describeCoverage.js +22 -0
  21. package/dist/ProteinView/hooks/useProteinFeatureTrackData.js +11 -20
  22. package/dist/ProteinView/mismatchRuns.js +33 -0
  23. package/dist/ProteinView/structureModel.js +26 -12
  24. package/dist/ProteinView/structureUniProt.js +36 -0
  25. package/dist/jbrowse-plugin-protein3d.umd.production.min.js +15 -15
  26. package/dist/jbrowse-plugin-protein3d.umd.production.min.js.map +4 -4
  27. package/dist/version.js +1 -1
  28. package/package.json +1 -1
  29. package/src/LaunchProteinView/components/wiggleBandColors.test.ts +9 -0
  30. package/src/LaunchProteinView/components/wiggleBandColors.ts +1 -0
  31. package/src/LaunchProteinViewExtensionPoint/index.ts +67 -74
  32. package/src/LaunchProteinViewExtensionPoint/launchViewSnapshot.test.ts +42 -0
  33. package/src/ProteinView/autoScroll.test.ts +1 -0
  34. package/src/ProteinView/autoScroll.ts +3 -4
  35. package/src/ProteinView/components/AlignmentRuler.tsx +5 -5
  36. package/src/ProteinView/components/ChainSelect.tsx +26 -17
  37. package/src/ProteinView/components/ColorKey.tsx +12 -3
  38. package/src/ProteinView/components/ColumnOverlays.tsx +158 -0
  39. package/src/ProteinView/components/FeatureBar.tsx +34 -44
  40. package/src/ProteinView/components/FeatureTypeLabel.tsx +9 -17
  41. package/src/ProteinView/components/HeaderStructureRow.tsx +94 -12
  42. package/src/ProteinView/components/MismatchShading.tsx +36 -0
  43. package/src/ProteinView/components/ProteinAlignment.tsx +252 -216
  44. package/src/ProteinView/components/ProteinAlignmentHelpButton.tsx +16 -13
  45. package/src/ProteinView/components/ProteinAlignmentHelpDialog.tsx +14 -12
  46. package/src/ProteinView/components/ProteinFeatureTrack.tsx +22 -81
  47. package/src/ProteinView/components/ProteinViewHeader.tsx +52 -34
  48. package/src/ProteinView/components/ResidueValueTrack.tsx +43 -46
  49. package/src/ProteinView/components/SplitString.tsx +6 -182
  50. package/src/ProteinView/constants.ts +12 -2
  51. package/src/ProteinView/describeCoverage.test.ts +72 -0
  52. package/src/ProteinView/describeCoverage.ts +24 -0
  53. package/src/ProteinView/hooks/packLanes.test.ts +2 -2
  54. package/src/ProteinView/hooks/useProteinFeatureTrackData.ts +15 -33
  55. package/src/ProteinView/mismatchRuns.test.ts +27 -0
  56. package/src/ProteinView/mismatchRuns.ts +55 -0
  57. package/src/ProteinView/structureModel.ts +26 -11
  58. package/src/ProteinView/{hooks/useStructureUniProt.ts → structureUniProt.ts} +24 -37
  59. package/src/version.ts +1 -1
  60. package/dist/ProteinView/components/HeaderStructureInfo.js +0 -30
  61. package/dist/ProteinView/components/HoverMarker.js +0 -20
  62. package/dist/ProteinView/hooks/useAlignmentColumnHover.js +0 -23
  63. package/dist/ProteinView/hooks/useStructureUniProt.js +0 -39
  64. package/src/ProteinView/components/HeaderStructureInfo.tsx +0 -58
  65. package/src/ProteinView/components/HoverMarker.tsx +0 -36
  66. package/src/ProteinView/hooks/useAlignmentColumnHover.ts +0 -32
@@ -25,5 +25,6 @@ export function thresholdBandColor(field, bands) {
25
25
  .filter(band => band.upTo !== Infinity)
26
26
  .map(band => band.upTo + 1e-6),
27
27
  range: bands.map(band => band.color),
28
+ labels: bands.map(band => band.label),
28
29
  };
29
30
  }
@@ -3,6 +3,16 @@ import { resolveShortLaunch, } from './resolveShortLaunch';
3
3
  import { maybeLaunchSideBySide } from '../LaunchProteinView/utils/sideBySide';
4
4
  import { coerceColorScheme } from '../ProteinView/applyColorTheme';
5
5
  import { proteinViewSnapshot } from '../ProteinView/proteinViewSpec';
6
+ export function launchViewSnapshot({ alignmentAlgorithm, colorScheme, ...settings }, structures) {
7
+ return proteinViewSnapshot({
8
+ ...settings,
9
+ alignmentAlgorithm: alignmentAlgorithm === undefined
10
+ ? undefined
11
+ : coerceAlignmentAlgorithm(alignmentAlgorithm),
12
+ colorScheme: colorScheme === undefined ? undefined : coerceColorScheme(colorScheme),
13
+ structures,
14
+ });
15
+ }
6
16
  export default function LaunchProteinViewExtensionPointF(pluginManager) {
7
17
  // v4 hosts declare `init`; v5 reads the settings off the view object and
8
18
  // warns about the nesting
@@ -17,9 +27,9 @@ export default function LaunchProteinViewExtensionPointF(pluginManager) {
17
27
  // its extendee at each exit now, like jbrowse-components' own
18
28
  // LaunchDotplotView does.
19
29
  async (args) => {
20
- const { session, url, uniprotId, pdbId, transcriptId, userProvidedTranscriptSequence, feature, connectedViewId, connectedView, alignmentAlgorithm, colorScheme, displayName, height, showControls, showHighlight, showAlignment, showProteinTracks, compactTracks, autoScrollAlignment, zoomToBaseLevel, sideBySide, initialSelection, initialResidues, initialTranscriptResidues, } = args;
21
- const requested = args.structures?.length
22
- ? args.structures
30
+ const { session, url, uniprotId, pdbId, structures: requestedStructures, transcriptId, userProvidedTranscriptSequence, feature, connectedViewId, connectedView, sideBySide, initialSelection, initialResidues, initialTranscriptResidues, ...settings } = args;
31
+ const requested = requestedStructures?.length
32
+ ? requestedStructures
23
33
  : [
24
34
  {
25
35
  url,
@@ -89,25 +99,7 @@ export default function LaunchProteinViewExtensionPointF(pluginManager) {
89
99
  feature: s.feature ?? resolved?.feature ?? feature,
90
100
  connectedViewId: s.connectedViewId ?? resolvedConnectedViewId,
91
101
  }));
92
- const proteinView = session.addView('ProteinView', proteinViewSnapshot({
93
- // a URL param is untrusted text; the model properties are enumerations
94
- alignmentAlgorithm: alignmentAlgorithm === undefined
95
- ? undefined
96
- : coerceAlignmentAlgorithm(alignmentAlgorithm),
97
- colorScheme: colorScheme === undefined
98
- ? undefined
99
- : coerceColorScheme(colorScheme),
100
- displayName,
101
- height,
102
- showControls,
103
- showHighlight,
104
- showAlignment,
105
- showProteinTracks,
106
- compactTracks,
107
- autoScrollAlignment,
108
- zoomToBaseLevel,
109
- structures,
110
- }));
102
+ const proteinView = session.addView('ProteinView', launchViewSnapshot(settings, structures));
111
103
  if (ownsConnectedView) {
112
104
  maybeLaunchSideBySide(session, proteinView.id, sideBySide);
113
105
  }
@@ -1,5 +1,4 @@
1
1
  import { reaction } from 'mobx';
2
- import { CHAR_WIDTH } from './constants';
3
2
  export function followHoverTarget({ x, width, scrollLeft, clientWidth, }) {
4
3
  const visible = x >= scrollLeft && x + width <= scrollLeft + clientWidth;
5
4
  return visible ? undefined : x + width / 2 - clientWidth / 2;
@@ -17,8 +16,8 @@ export function followHover(model, getContainer) {
17
16
  model.autoScrollAlignment &&
18
17
  !model.isMouseInAlignment) {
19
18
  const target = followHoverTarget({
20
- x: pos * CHAR_WIDTH,
21
- width: CHAR_WIDTH,
19
+ x: pos * model.columnWidth,
20
+ width: model.columnWidth,
22
21
  scrollLeft: container.scrollLeft,
23
22
  clientWidth: container.clientWidth,
24
23
  });
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { observer } from 'mobx-react';
3
- import { CHAR_WIDTH, ROW_HEIGHT } from '../constants';
3
+ import { ROW_HEIGHT } from '../constants';
4
4
  /** Which alignment columns get a tick, and which of those a label, in the
5
5
  * structure's author residue numbering (see residueNumber), so the ruler reads
6
6
  * like the paper and Mol*'s hover label. Pure so it can be tested without the
@@ -26,18 +26,18 @@ export function rulerTicks(alignmentToStructure, columns, residueNumber) {
26
26
  return ticks;
27
27
  }
28
28
  const AlignmentRuler = observer(function AlignmentRuler({ model, columns, }) {
29
+ const { columnWidth } = model;
29
30
  const ticks = rulerTicks(model.pairwiseAlignmentToStructurePosition, columns, pos => model.residueNumber(pos));
30
31
  return (React.createElement("div", { style: {
31
32
  position: 'relative',
32
- height: ROW_HEIGHT,
33
- width: columns * CHAR_WIDTH,
33
+ height: '100%',
34
34
  color: '#888',
35
35
  fontSize: 8,
36
36
  } }, ticks.map(({ col, label }) => (React.createElement("span", { key: col, style: {
37
37
  position: 'absolute',
38
- left: col * CHAR_WIDTH,
38
+ left: col * columnWidth,
39
39
  top: 0,
40
- height: ROW_HEIGHT,
40
+ bottom: 0,
41
41
  borderLeft: '1px solid #aaa',
42
42
  paddingLeft: 2,
43
43
  lineHeight: `${ROW_HEIGHT}px`,
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { MenuItem, TextField } from '@mui/material';
2
+ import { MenuItem, TextField, Tooltip } from '@mui/material';
3
3
  import { observer } from 'mobx-react';
4
4
  import { entityLabel } from 'p2s_mapper';
5
5
  // Which chain the transcript maps to. The structure picks the protein chain
@@ -11,8 +11,12 @@ const ChainSelect = observer(function ChainSelect({ model, }) {
11
11
  if (!entities || entities.length < 2 || !userProvidedTranscriptSequence) {
12
12
  return null;
13
13
  }
14
- return (React.createElement(TextField, { select: true, size: "small", label: "Mapped chain", "data-testid": "protein-mapped-chain", value: model.pendingEntityId ?? mappedEntity?.entityId ?? '', onChange: event => {
15
- model.chooseEntity(event.target.value);
16
- }, sx: { minWidth: 200, mr: 1 } }, entities.map(entity => (React.createElement(MenuItem, { key: entity.entityId, value: entity.entityId }, entityLabel(entity))))));
14
+ return (React.createElement(Tooltip, { title: "Mapped chain: the one the transcript maps to", placement: "left" },
15
+ React.createElement(TextField, { select: true, size: "small", variant: "standard", "data-testid": "protein-mapped-chain", value: model.pendingEntityId ?? mappedEntity?.entityId ?? '', onChange: event => {
16
+ model.chooseEntity(event.target.value);
17
+ }, slotProps: {
18
+ input: { disableUnderline: true, sx: { fontSize: 12 } },
19
+ htmlInput: { 'aria-label': 'Mapped chain' },
20
+ }, sx: { minWidth: 60 } }, entities.map(entity => (React.createElement(MenuItem, { key: entity.entityId, value: entity.entityId, dense: true }, entityLabel(entity)))))));
17
21
  });
18
22
  export default ChainSelect;
@@ -5,8 +5,8 @@ const swatch = {
5
5
  height: 8,
6
6
  border: '1px solid rgba(0,0,0,0.3)',
7
7
  };
8
- function KeyRow({ title, testId, children, }) {
9
- return (React.createElement(Typography, { variant: "caption", color: "textSecondary", component: "div", "data-testid": testId, style: {
8
+ function KeyRow({ title, testId, color, children, }) {
9
+ return (React.createElement(Typography, { variant: "caption", color: color, component: "div", "data-testid": testId, style: {
10
10
  display: 'flex',
11
11
  flexWrap: 'wrap',
12
12
  alignItems: 'center',
@@ -19,13 +19,13 @@ function KeyRow({ title, testId, children, }) {
19
19
  ":"),
20
20
  children));
21
21
  }
22
- export function ColorKey({ title, entries, testId = 'track-legend', }) {
23
- return (React.createElement(KeyRow, { title: title, testId: testId }, entries.map(({ label, color }, i) => (React.createElement("span", { key: `${i}-${label}`, style: { display: 'inline-flex', alignItems: 'center', gap: 3 } },
22
+ export function ColorKey({ title, entries, testId = 'track-legend', color = 'textSecondary', }) {
23
+ return (React.createElement(KeyRow, { title: title, testId: testId, color: color }, entries.map(({ label, color }, i) => (React.createElement("span", { key: `${i}-${label}`, style: { display: 'inline-flex', alignItems: 'center', gap: 3 } },
24
24
  React.createElement("span", { style: { ...swatch, background: color } }),
25
25
  label)))));
26
26
  }
27
- export function GradientKey({ title, minLabel, maxLabel, colors, testId = 'track-legend', }) {
28
- return (React.createElement(KeyRow, { title: title, testId: testId },
27
+ export function GradientKey({ title, minLabel, maxLabel, colors, testId = 'track-legend', color = 'textSecondary', }) {
28
+ return (React.createElement(KeyRow, { title: title, testId: testId, color: color },
29
29
  minLabel,
30
30
  React.createElement("span", { style: {
31
31
  ...swatch,
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { observer } from 'mobx-react';
3
+ import { HOVER_COLOR, HOVER_RANGE_COLOR, MATCH_COLOR, SELECTION_COLOR, SELECTION_OUTLINE, } from '../constants';
4
+ import { positionRuns } from '../residueRanges';
5
+ function Band({ start, end, columnWidth, background, border, }) {
6
+ return (React.createElement("div", { style: {
7
+ position: 'absolute',
8
+ top: 0,
9
+ bottom: 0,
10
+ left: start * columnWidth,
11
+ width: (end - start + 1) * columnWidth,
12
+ background,
13
+ border,
14
+ boxSizing: 'border-box',
15
+ pointerEvents: 'none',
16
+ } }));
17
+ }
18
+ function Layer({ zIndex, children, }) {
19
+ return (React.createElement("div", { style: { position: 'absolute', inset: 0, pointerEvents: 'none', zIndex } }, children));
20
+ }
21
+ /**
22
+ * The persistent state, drawn beneath every row: a band on top would tint the
23
+ * pLDDT cells and feature bars inside it, and those colours are data. The
24
+ * aligned portion (`showHighlight`) lights the sequence rows, `matchHeight`
25
+ * tall, and the selection's fill goes over it; ColumnOverlays draws the
26
+ * selection's outline over the rows.
27
+ */
28
+ export const SelectionBackdrop = observer(function SelectionBackdrop({ model, matchHeight, }) {
29
+ const { clickAlignmentRanges, columnWidth, showHighlight, alignmentMatchSet, } = model;
30
+ return (React.createElement(Layer, { zIndex: 0 },
31
+ showHighlight && alignmentMatchSet ? (React.createElement("div", { style: { position: 'relative', height: matchHeight } }, positionRuns(alignmentMatchSet).map(run => (React.createElement(Band, { key: run.start, start: run.start, end: run.end - 1, columnWidth: columnWidth, background: MATCH_COLOR }))))) : null,
32
+ clickAlignmentRanges.map(range => (React.createElement(Band, { key: range.start, start: range.start, end: range.end, columnWidth: columnWidth, background: SELECTION_COLOR })))));
33
+ });
34
+ /**
35
+ * The interaction state, drawn once over every row of the panel so the
36
+ * sequence, the ruler and the tracks all agree on which columns are hovered
37
+ * and selected. Columns are inclusive.
38
+ */
39
+ const ColumnOverlays = observer(function ColumnOverlays({ model, }) {
40
+ const { clickAlignmentRanges, alignmentHoverRange, alignmentHoverPos, columnWidth, } = model;
41
+ return (React.createElement(Layer, { zIndex: 1 },
42
+ clickAlignmentRanges.map(range => (React.createElement(Band, { key: range.start, start: range.start, end: range.end, columnWidth: columnWidth, background: "transparent", border: SELECTION_OUTLINE }))),
43
+ alignmentHoverRange ? (React.createElement(Band, { start: alignmentHoverRange.start, end: alignmentHoverRange.end, columnWidth: columnWidth, background: HOVER_RANGE_COLOR })) : null,
44
+ alignmentHoverPos === undefined ? null : (React.createElement(Band, { start: alignmentHoverPos, end: alignmentHoverPos, columnWidth: columnWidth, background: HOVER_COLOR }))));
45
+ });
46
+ export default ColumnOverlays;
@@ -26,58 +26,50 @@ function FeatureTooltipContent({ feature, layout, model, }) {
26
26
  last)) : null,
27
27
  feature.description ? React.createElement("div", null, feature.description) : null));
28
28
  }
29
- const FeatureBar = observer(function FeatureBar({ layout, top, model, }) {
29
+ // `selected` comes from the track rather than each bar reading the model's
30
+ // selection itself, so a click re-renders the two bars it changes instead of
31
+ // every bar of every track (p53 has 1,363 natural variants).
32
+ const FeatureBar = observer(function FeatureBar({ layout, top, selected, model, }) {
30
33
  const [isHovered, setIsHovered] = useState(false);
31
- const { selectedFeatureId } = model;
32
- const { feature, left, width } = layout;
33
- const isSelected = selectedFeatureId === feature.uniqueId;
34
- const handleMouseEnter = () => {
35
- setIsHovered(true);
36
- model.setAlignmentHoverRange({
37
- start: layout.alignmentStart,
38
- end: layout.alignmentEnd,
39
- });
40
- };
41
- const handleMouseLeave = () => {
42
- setIsHovered(false);
43
- model.setAlignmentHoverRange(undefined);
44
- };
45
- const handleClick = () => {
46
- if (isSelected) {
47
- model.setSelectedFeatureId(undefined);
48
- model.setClickedStructureRanges([]);
49
- }
50
- else {
51
- model.setSelectedFeatureId(feature.uniqueId);
52
- clickProteinToGenome({
53
- model,
54
- structureSeqPos: layout.structureStart,
55
- structureSeqEndPos: layout.structureEnd,
56
- }).catch((e) => {
57
- console.error(e);
58
- model.setError(e);
59
- });
60
- }
61
- };
62
- const color = getFeatureColor(feature.type);
34
+ const { feature, alignmentStart, alignmentEnd } = layout;
35
+ const { columnWidth, trackHeight } = model;
63
36
  return (React.createElement(Tooltip, { title: React.createElement(FeatureTooltipContent, { feature: feature, layout: layout, model: model }), followCursor: true },
64
37
  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: () => {
65
- handleClick();
38
+ if (selected) {
39
+ model.setSelectedFeatureId(undefined);
40
+ model.setClickedStructureRanges([]);
41
+ }
42
+ else {
43
+ model.setSelectedFeatureId(feature.uniqueId);
44
+ clickProteinToGenome({
45
+ model,
46
+ structureSeqPos: layout.structureStart,
47
+ structureSeqEndPos: layout.structureEnd,
48
+ }).catch((e) => {
49
+ console.error(e);
50
+ model.setError(e);
51
+ });
52
+ }
66
53
  }, onMouseEnter: () => {
67
- handleMouseEnter();
54
+ setIsHovered(true);
55
+ model.setAlignmentHoverRange({
56
+ start: alignmentStart,
57
+ end: alignmentEnd,
58
+ });
68
59
  }, onMouseLeave: () => {
69
- handleMouseLeave();
60
+ setIsHovered(false);
61
+ model.setAlignmentHoverRange(undefined);
70
62
  }, style: {
71
63
  position: 'absolute',
72
- left,
64
+ left: alignmentStart * columnWidth,
73
65
  top,
74
- width,
75
- height: model.trackHeight,
76
- backgroundColor: color,
77
- opacity: isHovered || isSelected ? 0.9 : 0.6,
66
+ width: (alignmentEnd - alignmentStart + 1) * columnWidth,
67
+ height: trackHeight,
68
+ backgroundColor: getFeatureColor(feature.type),
69
+ opacity: isHovered || selected ? 0.9 : 0.6,
78
70
  cursor: 'pointer',
79
71
  borderRadius: 2,
80
- border: isSelected
72
+ border: selected
81
73
  ? SELECTED_BORDER
82
74
  : isHovered
83
75
  ? HOVERED_BORDER
@@ -5,33 +5,27 @@ import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
5
5
  import { IconButton, Tooltip } from '@mui/material';
6
6
  import { observer } from 'mobx-react';
7
7
  import { HIDE_BUTTON_COLOR } from '../constants';
8
- const FeatureTypeLabel = observer(function FeatureTypeLabel({ type, laneCount, expanded, model, }) {
9
- const lanes = expanded ? laneCount : 1;
10
- const canExpand = laneCount > 1;
8
+ const FeatureTypeLabel = observer(function FeatureTypeLabel({ type, laneCount, model, }) {
9
+ const expanded = model.expandedFeatureTypes.has(type);
10
+ const iconSize = model.trackHeight;
11
11
  return (React.createElement(Tooltip, { title: type, placement: "left" },
12
12
  React.createElement("div", { style: {
13
- height: lanes * (model.trackHeight + model.trackGap),
14
- fontSize: 9,
15
- fontFamily: 'monospace',
16
- textAlign: 'right',
17
- paddingRight: 4,
13
+ height: '100%',
18
14
  whiteSpace: 'nowrap',
19
15
  display: 'flex',
20
16
  alignItems: expanded ? 'flex-start' : 'center',
21
17
  justifyContent: 'flex-end',
22
18
  gap: 2,
23
19
  } },
24
- React.createElement(IconButton, { onClick: e => {
25
- e.stopPropagation();
20
+ React.createElement(IconButton, { onClick: () => {
26
21
  model.hideFeatureType(type);
27
22
  }, title: `Hide ${type} track`, sx: { p: 0, color: HIDE_BUTTON_COLOR } },
28
- React.createElement(CloseIcon, { sx: { fontSize: model.trackHeight } })),
29
- canExpand ? (React.createElement(IconButton, { onClick: e => {
30
- e.stopPropagation();
23
+ React.createElement(CloseIcon, { sx: { fontSize: iconSize } })),
24
+ laneCount > 1 ? (React.createElement(IconButton, { onClick: () => {
31
25
  model.toggleFeatureTypeExpanded(type);
32
26
  }, title: expanded
33
27
  ? `Collapse ${type} track`
34
- : `Expand ${type} track (${laneCount} overlapping rows)`, sx: { p: 0, color: HIDE_BUTTON_COLOR } }, expanded ? (React.createElement(UnfoldLessIcon, { sx: { fontSize: model.trackHeight } })) : (React.createElement(UnfoldMoreIcon, { sx: { fontSize: model.trackHeight } })))) : null,
28
+ : `Expand ${type} track (${laneCount} overlapping rows)`, sx: { p: 0, color: HIDE_BUTTON_COLOR } }, expanded ? (React.createElement(UnfoldLessIcon, { sx: { fontSize: iconSize } })) : (React.createElement(UnfoldMoreIcon, { sx: { fontSize: iconSize } })))) : null,
35
29
  React.createElement("span", null, type))));
36
30
  });
37
31
  export default FeatureTypeLabel;
@@ -4,43 +4,77 @@ import CloseIcon from '@mui/icons-material/Close';
4
4
  import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
5
5
  import Chip from '@mui/material/Chip';
6
6
  import IconButton from '@mui/material/IconButton';
7
+ import Link from '@mui/material/Link';
7
8
  import Tooltip from '@mui/material/Tooltip';
8
9
  import Typography from '@mui/material/Typography';
9
10
  import { observer } from 'mobx-react';
10
- import { LOW_IDENTITY_OVER_SHORTER, SHORT_ALIGNMENT_IDENTITY, SHORT_ALIGNMENT_RESIDUES, describeCoveredRange, describeTranscriptCoverage, isLowSimilarity, } from 'p2s_mapper';
11
+ import { LOW_IDENTITY_OVER_SHORTER, SHORT_ALIGNMENT_IDENTITY, SHORT_ALIGNMENT_RESIDUES, describeAlignmentQuality, describeCoveredRange, describeTranscriptCoverage, isLowSimilarity, uniprotEntryUrl, } from 'p2s_mapper';
12
+ import ChainSelect from './ChainSelect';
13
+ import { describeCoverage } from '../describeCoverage';
11
14
  const LOW_SIMILARITY_EXPLANATION = `Under ${Math.round(LOW_IDENTITY_OVER_SHORTER * 100)}% of the shorter sequence is identical (${Math.round(SHORT_ALIGNMENT_IDENTITY * 100)}% for an alignment of fewer than ${SHORT_ALIGNMENT_RESIDUES} residues): an alignment this weak is what two unrelated proteins produce, so the positions it maps may be unrelated. Check the mapped chain, the transcript isoform, or import a curated alignment.`;
12
- const StructureRow = observer(function StructureRow({ model, structure, }) {
15
+ // Which UniProt entry the structure's mapped chain is, and so which one its
16
+ // feature tracks annotate. For an AlphaFold model that is in the filename, but
17
+ // for a PDB entry it is resolved via SIFTS and is otherwise invisible.
18
+ const UniProtLink = observer(function UniProtLink({ structure, }) {
19
+ const { uniprotId, uniprotName } = structure.uniProtEntry;
20
+ return uniprotId ? (React.createElement(Tooltip, { title: `The mapped chain is UniProt ${uniprotId}${uniprotName ? ` (${uniprotName})` : ''}, which its feature tracks annotate` },
21
+ React.createElement(Link, { variant: "caption", href: uniprotEntryUrl(uniprotId), target: "_blank", rel: "noreferrer", noWrap: true, sx: { minWidth: 40 } },
22
+ "UniProt ",
23
+ uniprotId))) : null;
24
+ });
25
+ const StructureRow = observer(function StructureRow({ model, structure, readout, }) {
13
26
  const { label, alignmentQuality: quality, statusMessage } = structure;
14
- const coveredRange = quality ? describeCoveredRange(quality) : undefined;
15
27
  const switchable = model.showAlignment && model.structures.length > 1;
16
28
  const open = model.alignmentStructure === structure;
17
- return (React.createElement("div", { "data-testid": "structure-row", "data-label": label, "data-open": switchable ? open : undefined, style: { display: 'flex', alignItems: 'center', gap: 8, minHeight: 24 } },
29
+ const coveredRange = quality ? describeCoveredRange(quality) : undefined;
30
+ return (React.createElement("div", { "data-testid": "structure-row", "data-label": label, "data-open": switchable ? open : undefined, style: {
31
+ display: 'flex',
32
+ alignItems: 'center',
33
+ gap: 8,
34
+ minHeight: 24,
35
+ minWidth: 0,
36
+ } },
18
37
  switchable ? (React.createElement(Tooltip, { title: open ? 'Alignment shown below' : 'Show alignment' },
19
38
  React.createElement("span", null,
20
39
  React.createElement(IconButton, { size: "small", "aria-label": `Show ${label} alignment`, "aria-expanded": open, disabled: open, onClick: () => {
21
40
  model.openAlignmentOf(structure);
22
41
  }, sx: { p: 0 } }, open ? (React.createElement(ExpandMoreIcon, { fontSize: "small" })) : (React.createElement(ChevronRightIcon, { fontSize: "small" })))))) : null,
23
- React.createElement(Typography, { variant: "caption", sx: { fontWeight: 'bold' } }, label),
42
+ React.createElement(Typography, { variant: "caption", sx: { fontWeight: 'bold', flexShrink: 0 } }, label),
24
43
  statusMessage ? (React.createElement(Typography, { variant: "caption", color: "error", "data-testid": "structure-status" }, statusMessage)) : null,
25
- quality ? (React.createElement(Typography, { variant: "caption", color: "textSecondary", "data-testid": "header-alignment-quality" },
26
- describeTranscriptCoverage(quality),
27
- coveredRange ? `, ${coveredRange}` : '')) : null,
44
+ readout ? (React.createElement(Typography, { variant: "caption", noWrap: true, title: readout, "data-testid": "structure-hover" }, readout)) : quality ? (React.createElement(Tooltip, { title: quality.aligned === 0
45
+ ? ''
46
+ : `${describeTranscriptCoverage(quality)}${coveredRange ? `, ${coveredRange}` : ''}; ${describeAlignmentQuality(quality)}` },
47
+ React.createElement(Typography, { variant: "caption", color: "textSecondary", noWrap: true, "data-testid": "header-alignment-quality" }, describeCoverage(quality)))) : null,
48
+ React.createElement(UniProtLink, { structure: structure }),
28
49
  React.createElement("div", { style: { flex: 1 } }),
50
+ open && model.showAlignment ? React.createElement(ChainSelect, { model: structure }) : null,
29
51
  quality && isLowSimilarity(quality) ? (React.createElement(Tooltip, { title: LOW_SIMILARITY_EXPLANATION },
30
52
  React.createElement(Chip, { size: "small", color: "warning", variant: "outlined", label: "low similarity", "data-testid": "header-low-similarity" }))) : null,
31
53
  React.createElement(Tooltip, { title: `Remove ${label}` },
32
54
  React.createElement(IconButton, { size: "small", "aria-label": `Remove ${label}`, onClick: () => {
33
55
  model.removeStructure(structure);
34
- } },
56
+ }, sx: { p: 0.25 } },
35
57
  React.createElement(CloseIcon, { fontSize: "small" })))));
36
58
  });
37
59
  /**
38
60
  * One line per structure, in the header the reader always sees. The identity
39
61
  * and coverage readout used to live only inside the pairwise panel, which the
40
62
  * same reader can hide — so how much of the transcript a structure speaks for,
41
- * and whether the mapping is chance, were one click away from invisible.
63
+ * and whether the mapping is chance, were one click away from invisible. The
64
+ * open structure's row also carries what used to head its alignment panel:
65
+ * the mapped-chain picker.
66
+ *
67
+ * A hovered residue is read out on its structure's row, in place of the
68
+ * coverage line until the pointer leaves. A genome or MSA hover that reaches
69
+ * one mapped structure but not another says so, since a crystal that lacks
70
+ * the residue is the point of showing several.
42
71
  */
43
72
  const HeaderStructureRows = observer(function HeaderStructureRows({ model, }) {
44
- return (React.createElement("div", null, model.structures.map((structure, idx) => (React.createElement(StructureRow, { key: idx, model: model, structure: structure })))));
73
+ const { structures } = model;
74
+ const connectedHover = structures.some(s => s.hoverPosition && s.hoverPosition.source !== 'structure');
75
+ return (React.createElement("div", { style: { flex: 1, minWidth: 0 } }, structures.map((structure, idx) => (React.createElement(StructureRow, { key: idx, model: model, structure: structure, readout: structure.hoverString ||
76
+ (connectedHover && structure.genomeToTranscriptSeqMapping
77
+ ? 'not in structure'
78
+ : '') })))));
45
79
  });
46
80
  export default HeaderStructureRows;
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { observer } from 'mobx-react';
3
+ import { DIFFERENT_RESIDUE_COLOR, SIMILAR_RESIDUE_COLOR } from '../constants';
4
+ import { mismatchRuns } from '../mismatchRuns';
5
+ const MismatchShading = observer(function MismatchShading({ model, }) {
6
+ const { alignment, columnWidth } = model;
7
+ return alignment
8
+ ? mismatchRuns(alignment).map(run => (React.createElement("div", { key: run.start, style: {
9
+ position: 'absolute',
10
+ top: 0,
11
+ bottom: 0,
12
+ left: run.start * columnWidth,
13
+ width: (run.end - run.start) * columnWidth,
14
+ background: run.kind === 'similar'
15
+ ? SIMILAR_RESIDUE_COLOR
16
+ : DIFFERENT_RESIDUE_COLOR,
17
+ } })))
18
+ : null;
19
+ });
20
+ export default MismatchShading;