jbrowse-plugin-protein3d 0.15.1 → 0.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/LaunchProteinViewExtensionPoint/index.js +14 -22
- package/dist/ProteinView/autoScroll.js +2 -3
- package/dist/ProteinView/components/AlignmentRuler.js +5 -5
- package/dist/ProteinView/components/ChainSelect.js +8 -4
- package/dist/ProteinView/components/ColorKey.js +6 -6
- package/dist/ProteinView/components/ColumnOverlays.js +46 -0
- package/dist/ProteinView/components/FeatureBar.js +34 -42
- package/dist/ProteinView/components/FeatureTypeLabel.js +8 -14
- package/dist/ProteinView/components/HeaderStructureRow.js +45 -11
- package/dist/ProteinView/components/MismatchShading.js +20 -0
- package/dist/ProteinView/components/ProteinAlignment.js +124 -102
- package/dist/ProteinView/components/ProteinAlignmentHelpButton.js +9 -8
- package/dist/ProteinView/components/ProteinAlignmentHelpDialog.js +2 -2
- package/dist/ProteinView/components/ProteinFeatureTrack.js +9 -19
- package/dist/ProteinView/components/ProteinViewHeader.js +26 -18
- package/dist/ProteinView/components/ResidueValueTrack.js +23 -30
- package/dist/ProteinView/components/SplitString.js +4 -94
- package/dist/ProteinView/constants.js +10 -2
- package/dist/ProteinView/describeCoverage.js +22 -0
- package/dist/ProteinView/hooks/useProteinFeatureTrackData.js +11 -20
- package/dist/ProteinView/mismatchRuns.js +33 -0
- package/dist/ProteinView/structureModel.js +26 -12
- package/dist/ProteinView/structureUniProt.js +36 -0
- package/dist/jbrowse-plugin-protein3d.umd.production.min.js +15 -15
- package/dist/jbrowse-plugin-protein3d.umd.production.min.js.map +4 -4
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/LaunchProteinViewExtensionPoint/index.ts +67 -74
- package/src/LaunchProteinViewExtensionPoint/launchViewSnapshot.test.ts +42 -0
- package/src/ProteinView/autoScroll.test.ts +1 -0
- package/src/ProteinView/autoScroll.ts +3 -4
- package/src/ProteinView/components/AlignmentRuler.tsx +5 -5
- package/src/ProteinView/components/ChainSelect.tsx +26 -17
- package/src/ProteinView/components/ColorKey.tsx +12 -3
- package/src/ProteinView/components/ColumnOverlays.tsx +158 -0
- package/src/ProteinView/components/FeatureBar.tsx +34 -44
- package/src/ProteinView/components/FeatureTypeLabel.tsx +9 -17
- package/src/ProteinView/components/HeaderStructureRow.tsx +94 -12
- package/src/ProteinView/components/MismatchShading.tsx +36 -0
- package/src/ProteinView/components/ProteinAlignment.tsx +252 -216
- package/src/ProteinView/components/ProteinAlignmentHelpButton.tsx +16 -13
- package/src/ProteinView/components/ProteinAlignmentHelpDialog.tsx +14 -12
- package/src/ProteinView/components/ProteinFeatureTrack.tsx +22 -81
- package/src/ProteinView/components/ProteinViewHeader.tsx +52 -34
- package/src/ProteinView/components/ResidueValueTrack.tsx +43 -46
- package/src/ProteinView/components/SplitString.tsx +6 -182
- package/src/ProteinView/constants.ts +12 -2
- package/src/ProteinView/describeCoverage.test.ts +72 -0
- package/src/ProteinView/describeCoverage.ts +24 -0
- package/src/ProteinView/hooks/packLanes.test.ts +2 -2
- package/src/ProteinView/hooks/useProteinFeatureTrackData.ts +15 -33
- package/src/ProteinView/mismatchRuns.test.ts +27 -0
- package/src/ProteinView/mismatchRuns.ts +55 -0
- package/src/ProteinView/structureModel.ts +26 -11
- package/src/ProteinView/{hooks/useStructureUniProt.ts → structureUniProt.ts} +24 -37
- package/src/version.ts +1 -1
- package/dist/ProteinView/components/HeaderStructureInfo.js +0 -30
- package/dist/ProteinView/components/HoverMarker.js +0 -20
- package/dist/ProteinView/hooks/useAlignmentColumnHover.js +0 -23
- package/dist/ProteinView/hooks/useStructureUniProt.js +0 -39
- package/src/ProteinView/components/HeaderStructureInfo.tsx +0 -58
- package/src/ProteinView/components/HoverMarker.tsx +0 -36
- package/src/ProteinView/hooks/useAlignmentColumnHover.ts +0 -32
|
@@ -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,
|
|
21
|
-
const requested =
|
|
22
|
-
?
|
|
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',
|
|
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 *
|
|
21
|
-
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 {
|
|
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:
|
|
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 *
|
|
38
|
+
left: col * columnWidth,
|
|
39
39
|
top: 0,
|
|
40
|
-
|
|
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(
|
|
15
|
-
|
|
16
|
-
|
|
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:
|
|
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
|
-
|
|
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 {
|
|
32
|
-
const {
|
|
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
|
-
|
|
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
|
-
|
|
54
|
+
setIsHovered(true);
|
|
55
|
+
model.setAlignmentHoverRange({
|
|
56
|
+
start: alignmentStart,
|
|
57
|
+
end: alignmentEnd,
|
|
58
|
+
});
|
|
68
59
|
}, onMouseLeave: () => {
|
|
69
|
-
|
|
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:
|
|
76
|
-
backgroundColor:
|
|
77
|
-
opacity: isHovered ||
|
|
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:
|
|
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,
|
|
9
|
-
const
|
|
10
|
-
const
|
|
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:
|
|
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:
|
|
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:
|
|
29
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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;
|