jbrowse-plugin-protein3d 0.5.4 → 0.5.6
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/ProteinView/autoScroll.d.ts +30 -0
- package/dist/ProteinView/autoScroll.js +29 -0
- package/dist/ProteinView/components/FeatureBar.d.ts +5 -4
- package/dist/ProteinView/components/FeatureBar.js +9 -29
- package/dist/ProteinView/components/FeatureTypeLabel.d.ts +3 -1
- package/dist/ProteinView/components/FeatureTypeLabel.js +13 -3
- package/dist/ProteinView/components/ProteinAlignment.js +37 -12
- package/dist/ProteinView/components/ProteinFeatureTrack.js +10 -14
- package/dist/ProteinView/components/ProteinViewHeader.js +1 -1
- package/dist/ProteinView/components/ResidueValueTrack.js +3 -11
- package/dist/ProteinView/hooks/useAlignmentColumnHover.d.ts +11 -0
- package/dist/ProteinView/hooks/useAlignmentColumnHover.js +23 -0
- package/dist/ProteinView/hooks/useProteinFeatureTrackData.d.ts +21 -5
- package/dist/ProteinView/hooks/useProteinFeatureTrackData.js +77 -19
- package/dist/ProteinView/hooks/useUniProtFeatures.js +3 -3
- package/dist/ProteinView/model.d.ts +6 -0
- package/dist/ProteinView/model.js +1 -1
- package/dist/ProteinView/structureModel.d.ts +10 -0
- package/dist/ProteinView/structureModel.js +19 -0
- package/dist/jbrowse-plugin-protein3d.umd.production.min.js +13 -13
- package/dist/jbrowse-plugin-protein3d.umd.production.min.js.map +4 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/ProteinView/autoScroll.test.ts +47 -0
- package/src/ProteinView/autoScroll.ts +49 -0
- package/src/ProteinView/components/FeatureBar.tsx +13 -39
- package/src/ProteinView/components/FeatureTypeLabel.tsx +30 -2
- package/src/ProteinView/components/ProteinAlignment.tsx +36 -13
- package/src/ProteinView/components/ProteinFeatureTrack.tsx +30 -26
- package/src/ProteinView/components/ProteinViewHeader.tsx +1 -1
- package/src/ProteinView/components/ResidueValueTrack.tsx +7 -12
- package/src/ProteinView/hooks/packLanes.test.ts +57 -0
- package/src/ProteinView/hooks/useAlignmentColumnHover.ts +32 -0
- package/src/ProteinView/hooks/useProteinFeatureTrackData.ts +98 -26
- package/src/ProteinView/hooks/useUniProtFeatures.ts +3 -3
- package/src/ProteinView/model.ts +1 -1
- package/src/ProteinView/structureModel.ts +18 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure geometry for the alignment panel's auto-scrolling, extracted so it can be
|
|
3
|
+
* unit-tested independently of the DOM/mobx effects that drive it. All values
|
|
4
|
+
* are in pixels within the horizontally-scrolling alignment container.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Target scrollLeft for following a hovered column, but only on a *large jump* —
|
|
8
|
+
* a column that lands more than a full viewport outside the view (e.g. hovering
|
|
9
|
+
* a distant residue in the 3D structure). A column that has merely edged just
|
|
10
|
+
* off-screen during a continuous hover sweep returns undefined, so the panel
|
|
11
|
+
* doesn't feel like it's constantly re-centering. Centers the column when it
|
|
12
|
+
* does jump.
|
|
13
|
+
*/
|
|
14
|
+
export declare function largeJumpScrollTarget({ x, scrollLeft, clientWidth, }: {
|
|
15
|
+
x: number;
|
|
16
|
+
scrollLeft: number;
|
|
17
|
+
clientWidth: number;
|
|
18
|
+
}): number | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Target scrollLeft to bring a selected [start, end) pixel range into view,
|
|
21
|
+
* centering it, but only when it currently lies entirely off-screen. Returns
|
|
22
|
+
* undefined when any part of the range is already visible, so a selection the
|
|
23
|
+
* user can already see is left where it is.
|
|
24
|
+
*/
|
|
25
|
+
export declare function offScreenCenterTarget({ start, end, scrollLeft, clientWidth, }: {
|
|
26
|
+
start: number;
|
|
27
|
+
end: number;
|
|
28
|
+
scrollLeft: number;
|
|
29
|
+
clientWidth: number;
|
|
30
|
+
}): number | undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure geometry for the alignment panel's auto-scrolling, extracted so it can be
|
|
3
|
+
* unit-tested independently of the DOM/mobx effects that drive it. All values
|
|
4
|
+
* are in pixels within the horizontally-scrolling alignment container.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Target scrollLeft for following a hovered column, but only on a *large jump* —
|
|
8
|
+
* a column that lands more than a full viewport outside the view (e.g. hovering
|
|
9
|
+
* a distant residue in the 3D structure). A column that has merely edged just
|
|
10
|
+
* off-screen during a continuous hover sweep returns undefined, so the panel
|
|
11
|
+
* doesn't feel like it's constantly re-centering. Centers the column when it
|
|
12
|
+
* does jump.
|
|
13
|
+
*/
|
|
14
|
+
export function largeJumpScrollTarget({ x, scrollLeft, clientWidth, }) {
|
|
15
|
+
const viewEnd = scrollLeft + clientWidth;
|
|
16
|
+
const gap = Math.max(scrollLeft - x, x - viewEnd);
|
|
17
|
+
return gap > clientWidth ? x - clientWidth / 2 : undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Target scrollLeft to bring a selected [start, end) pixel range into view,
|
|
21
|
+
* centering it, but only when it currently lies entirely off-screen. Returns
|
|
22
|
+
* undefined when any part of the range is already visible, so a selection the
|
|
23
|
+
* user can already see is left where it is.
|
|
24
|
+
*/
|
|
25
|
+
export function offScreenCenterTarget({ start, end, scrollLeft, clientWidth, }) {
|
|
26
|
+
const viewEnd = scrollLeft + clientWidth;
|
|
27
|
+
const visible = end >= scrollLeft && start <= viewEnd;
|
|
28
|
+
return visible ? undefined : (start + end) / 2 - clientWidth / 2;
|
|
29
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type { FeatureLayout } from '../hooks/useProteinFeatureTrackData';
|
|
3
3
|
import type { JBrowsePluginProteinStructureModel } from '../model';
|
|
4
|
-
declare const FeatureBar: ({
|
|
5
|
-
|
|
4
|
+
declare const FeatureBar: ({ layout, top, model, }: {
|
|
5
|
+
layout: FeatureLayout;
|
|
6
|
+
top: number;
|
|
6
7
|
model: JBrowsePluginProteinStructureModel;
|
|
7
|
-
}) => React.JSX.Element
|
|
8
|
+
}) => React.JSX.Element;
|
|
8
9
|
export default FeatureBar;
|
|
@@ -2,26 +2,9 @@ import React, { useState } from 'react';
|
|
|
2
2
|
import { Tooltip } from '@mui/material';
|
|
3
3
|
import { observer } from 'mobx-react';
|
|
4
4
|
import { setMolstarLoci } from '../applyLociInteractivity';
|
|
5
|
-
import {
|
|
5
|
+
import { HOVERED_BORDER, SELECTED_BORDER } from '../constants';
|
|
6
6
|
import { getFeatureColor } from '../hooks/useUniProtFeatures';
|
|
7
7
|
import { clickProteinToGenome } from '../proteinToGenomeMapping';
|
|
8
|
-
/**
|
|
9
|
-
* Maps a feature's structure range onto alignment columns, returning both the
|
|
10
|
-
* alignment range (for hover) and pixel geometry. Returns undefined when either
|
|
11
|
-
* endpoint has no alignment column, so unmappable features aren't drawn at a
|
|
12
|
-
* misleading position.
|
|
13
|
-
*/
|
|
14
|
-
function getFeatureLayout(feature, structurePositionToAlignmentMap) {
|
|
15
|
-
const start = structurePositionToAlignmentMap?.[feature.start - 1];
|
|
16
|
-
const end = structurePositionToAlignmentMap?.[feature.end - 1];
|
|
17
|
-
return start === undefined || end === undefined
|
|
18
|
-
? undefined
|
|
19
|
-
: {
|
|
20
|
-
range: { start, end },
|
|
21
|
-
left: start * CHAR_WIDTH,
|
|
22
|
-
width: Math.max((end - start + 1) * CHAR_WIDTH, 3),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
8
|
function FeatureTooltipContent({ feature }) {
|
|
26
9
|
return (React.createElement("div", null,
|
|
27
10
|
React.createElement("div", null,
|
|
@@ -33,16 +16,17 @@ function FeatureTooltipContent({ feature }) {
|
|
|
33
16
|
feature.end),
|
|
34
17
|
feature.description ? React.createElement("div", null, feature.description) : null));
|
|
35
18
|
}
|
|
36
|
-
const FeatureBar = observer(function FeatureBar({
|
|
19
|
+
const FeatureBar = observer(function FeatureBar({ layout, top, model, }) {
|
|
37
20
|
const [isHovered, setIsHovered] = useState(false);
|
|
38
|
-
const { molstarPluginContext, selectedFeatureId
|
|
21
|
+
const { molstarPluginContext, selectedFeatureId } = model;
|
|
22
|
+
const { feature, left, width } = layout;
|
|
39
23
|
const isSelected = selectedFeatureId === feature.uniqueId;
|
|
40
|
-
const layout = getFeatureLayout(feature, structurePositionToAlignmentMap);
|
|
41
24
|
const handleMouseEnter = () => {
|
|
42
25
|
setIsHovered(true);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
26
|
+
model.setAlignmentHoverRange({
|
|
27
|
+
start: layout.alignmentStart,
|
|
28
|
+
end: layout.alignmentEnd,
|
|
29
|
+
});
|
|
46
30
|
};
|
|
47
31
|
const handleMouseLeave = () => {
|
|
48
32
|
setIsHovered(false);
|
|
@@ -81,10 +65,6 @@ const FeatureBar = observer(function FeatureBar({ feature, model, }) {
|
|
|
81
65
|
model.setClickedStructureRange(undefined);
|
|
82
66
|
}
|
|
83
67
|
};
|
|
84
|
-
if (!layout) {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
const { left, width } = layout;
|
|
88
68
|
const color = getFeatureColor(feature.type);
|
|
89
69
|
return (React.createElement(Tooltip, { title: React.createElement(FeatureTooltipContent, { feature: feature }), followCursor: true },
|
|
90
70
|
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: () => {
|
|
@@ -96,7 +76,7 @@ const FeatureBar = observer(function FeatureBar({ feature, model, }) {
|
|
|
96
76
|
}, style: {
|
|
97
77
|
position: 'absolute',
|
|
98
78
|
left,
|
|
99
|
-
top
|
|
79
|
+
top,
|
|
100
80
|
width,
|
|
101
81
|
height: model.trackHeight,
|
|
102
82
|
backgroundColor: color,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { JBrowsePluginProteinStructureModel } from '../model';
|
|
3
|
-
declare const FeatureTypeLabel: ({ type, labelWidth, model, }: {
|
|
3
|
+
declare const FeatureTypeLabel: ({ type, laneCount, expanded, labelWidth, model, }: {
|
|
4
4
|
type: string;
|
|
5
|
+
laneCount: number;
|
|
6
|
+
expanded: boolean;
|
|
5
7
|
labelWidth: number;
|
|
6
8
|
model: JBrowsePluginProteinStructureModel;
|
|
7
9
|
}) => React.JSX.Element;
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import CloseIcon from '@mui/icons-material/Close';
|
|
3
|
+
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
|
|
4
|
+
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
|
|
3
5
|
import { IconButton, Tooltip } from '@mui/material';
|
|
4
6
|
import { observer } from 'mobx-react';
|
|
5
7
|
import { HIDE_BUTTON_COLOR } from '../constants';
|
|
6
|
-
const FeatureTypeLabel = observer(function FeatureTypeLabel({ type, labelWidth, model, }) {
|
|
8
|
+
const FeatureTypeLabel = observer(function FeatureTypeLabel({ type, laneCount, expanded, labelWidth, model, }) {
|
|
9
|
+
const lanes = expanded ? laneCount : 1;
|
|
10
|
+
const canExpand = laneCount > 1;
|
|
7
11
|
return (React.createElement(Tooltip, { title: type, placement: "left" },
|
|
8
12
|
React.createElement("div", { style: {
|
|
9
|
-
height: model.trackHeight + model.trackGap,
|
|
13
|
+
height: lanes * (model.trackHeight + model.trackGap),
|
|
10
14
|
width: labelWidth - 4,
|
|
11
15
|
fontSize: 9,
|
|
12
16
|
fontFamily: 'monospace',
|
|
@@ -16,7 +20,7 @@ const FeatureTypeLabel = observer(function FeatureTypeLabel({ type, labelWidth,
|
|
|
16
20
|
textOverflow: 'ellipsis',
|
|
17
21
|
whiteSpace: 'nowrap',
|
|
18
22
|
display: 'flex',
|
|
19
|
-
alignItems: 'center',
|
|
23
|
+
alignItems: expanded ? 'flex-start' : 'center',
|
|
20
24
|
justifyContent: 'flex-end',
|
|
21
25
|
gap: 2,
|
|
22
26
|
} },
|
|
@@ -25,6 +29,12 @@ const FeatureTypeLabel = observer(function FeatureTypeLabel({ type, labelWidth,
|
|
|
25
29
|
model.hideFeatureType(type);
|
|
26
30
|
}, title: `Hide ${type} track`, sx: { p: 0, color: HIDE_BUTTON_COLOR } },
|
|
27
31
|
React.createElement(CloseIcon, { sx: { fontSize: model.trackHeight } })),
|
|
32
|
+
canExpand ? (React.createElement(IconButton, { onClick: e => {
|
|
33
|
+
e.stopPropagation();
|
|
34
|
+
model.toggleFeatureTypeExpanded(type);
|
|
35
|
+
}, title: expanded
|
|
36
|
+
? `Collapse ${type} track`
|
|
37
|
+
: `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
38
|
React.createElement("span", { style: { overflow: 'hidden', textOverflow: 'ellipsis' } }, type))));
|
|
29
39
|
});
|
|
30
40
|
export default FeatureTypeLabel;
|
|
@@ -2,6 +2,7 @@ import React, { useEffect, useRef } from 'react';
|
|
|
2
2
|
import { Tooltip, Typography } from '@mui/material';
|
|
3
3
|
import { autorun } from 'mobx';
|
|
4
4
|
import { observer } from 'mobx-react';
|
|
5
|
+
import { largeJumpScrollTarget, offScreenCenterTarget } from '../autoScroll';
|
|
5
6
|
import { CHAR_WIDTH, LABEL_WIDTH, ROW_HEIGHT } from '../constants';
|
|
6
7
|
import ProteinAlignmentHelpButton from './ProteinAlignmentHelpButton';
|
|
7
8
|
import { ProteinFeatureTrackContent, ProteinFeatureTrackLabels, } from './ProteinFeatureTrack';
|
|
@@ -27,31 +28,55 @@ function GutterLabel({ label, title, height, }) {
|
|
|
27
28
|
const ProteinAlignment = observer(function ProteinAlignment({ model, }) {
|
|
28
29
|
const { pairwiseAlignment, showHighlight, showProteinTracks, uniprotId, confidenceCells, hydrophobicityCells, } = model;
|
|
29
30
|
const containerRef = useRef(null);
|
|
30
|
-
const
|
|
31
|
+
const lastScrolledSelectionRef = useRef(undefined);
|
|
31
32
|
const { data: featureData, isLoading: featureLoading, error: featureError, } = useProteinFeatureTrackData(model, uniprotId);
|
|
33
|
+
// Recenter only on a large jump — when the hovered column lands well outside
|
|
34
|
+
// the viewport (e.g. hovering a distant residue in the 3D structure). A column
|
|
35
|
+
// that has merely edged just off-screen during a continuous hover sweep is
|
|
36
|
+
// left alone, so the panel doesn't feel like it's constantly re-centering.
|
|
32
37
|
useEffect(() => autorun(() => {
|
|
33
38
|
const container = containerRef.current;
|
|
34
39
|
if (model.autoScrollAlignment &&
|
|
35
40
|
!model.isMouseInAlignment &&
|
|
36
41
|
model.alignmentHoverPos !== undefined &&
|
|
37
42
|
container) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
const target = largeJumpScrollTarget({
|
|
44
|
+
x: model.alignmentHoverPos * CHAR_WIDTH,
|
|
45
|
+
scrollLeft: container.scrollLeft,
|
|
46
|
+
clientWidth: container.clientWidth,
|
|
41
47
|
});
|
|
48
|
+
if (target !== undefined) {
|
|
49
|
+
container.scrollTo({ left: target, behavior: 'smooth' });
|
|
50
|
+
}
|
|
42
51
|
}
|
|
43
52
|
}), [model]);
|
|
44
|
-
// Scroll
|
|
45
|
-
// declarative `initialSelection`
|
|
46
|
-
//
|
|
47
|
-
//
|
|
53
|
+
// Scroll a selection into view when it changes to an off-screen range — both
|
|
54
|
+
// the declarative `initialSelection` on open and a later click on a distant
|
|
55
|
+
// feature bar, which would otherwise select something the user can't see.
|
|
56
|
+
// Keyed on the range so it fires once per distinct selection and doesn't fight
|
|
57
|
+
// the user's own scrolling afterward.
|
|
48
58
|
useEffect(() => autorun(() => {
|
|
49
59
|
const container = containerRef.current;
|
|
50
60
|
const range = model.clickAlignmentRange;
|
|
51
|
-
if (container
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
if (container) {
|
|
62
|
+
if (range) {
|
|
63
|
+
const key = `${range.start}-${range.end}`;
|
|
64
|
+
if (key !== lastScrolledSelectionRef.current) {
|
|
65
|
+
lastScrolledSelectionRef.current = key;
|
|
66
|
+
const target = offScreenCenterTarget({
|
|
67
|
+
start: range.start * CHAR_WIDTH,
|
|
68
|
+
end: (range.end + 1) * CHAR_WIDTH,
|
|
69
|
+
scrollLeft: container.scrollLeft,
|
|
70
|
+
clientWidth: container.clientWidth,
|
|
71
|
+
});
|
|
72
|
+
if (target !== undefined) {
|
|
73
|
+
container.scrollTo({ left: target, behavior: 'smooth' });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
lastScrolledSelectionRef.current = undefined;
|
|
79
|
+
}
|
|
55
80
|
}
|
|
56
81
|
}), [model]);
|
|
57
82
|
if (!pairwiseAlignment) {
|
|
@@ -4,27 +4,23 @@ import { CHAR_WIDTH } from '../constants';
|
|
|
4
4
|
import FeatureBar from './FeatureBar';
|
|
5
5
|
import FeatureTypeLabel from './FeatureTypeLabel';
|
|
6
6
|
import HoverMarker from './HoverMarker';
|
|
7
|
-
|
|
7
|
+
import useAlignmentColumnHover from '../hooks/useAlignmentColumnHover';
|
|
8
|
+
const FeatureTypeTrackContent = observer(function FeatureTypeTrackContent({ group, model, sequenceLength, expanded, }) {
|
|
9
|
+
const lanes = expanded ? group.laneCount : 1;
|
|
10
|
+
const laneUnit = model.trackHeight + model.trackGap;
|
|
8
11
|
return (React.createElement("div", { style: {
|
|
9
12
|
position: 'relative',
|
|
10
|
-
height: model.trackHeight,
|
|
13
|
+
height: lanes * model.trackHeight + (lanes - 1) * model.trackGap,
|
|
11
14
|
width: sequenceLength * CHAR_WIDTH,
|
|
12
15
|
marginBottom: model.trackGap,
|
|
13
|
-
} },
|
|
16
|
+
} }, group.layouts.map(layout => (React.createElement(FeatureBar, { key: layout.feature.uniqueId, layout: layout, top: (expanded ? layout.lane : 0) * laneUnit, model: model })))));
|
|
14
17
|
});
|
|
15
18
|
export const ProteinFeatureTrackLabels = observer(function ProteinFeatureTrackLabels({ data, labelWidth, model, }) {
|
|
16
|
-
return (React.createElement(React.Fragment, null, data.
|
|
19
|
+
return (React.createElement(React.Fragment, null, data.visibleGroups.map(group => (React.createElement(FeatureTypeLabel, { key: group.type, type: group.type, laneCount: group.laneCount, expanded: model.expandedFeatureTypes.has(group.type), labelWidth: labelWidth, model: model })))));
|
|
17
20
|
});
|
|
18
21
|
export const ProteinFeatureTrackContent = observer(function ProteinFeatureTrackContent({ data, model, }) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (alignmentPos >= 0 && alignmentPos < data.sequenceLength) {
|
|
23
|
-
model.hoverAlignmentPosition(alignmentPos);
|
|
24
|
-
}
|
|
25
|
-
}, onMouseLeave: () => {
|
|
26
|
-
model.setHoveredPosition(undefined);
|
|
27
|
-
} },
|
|
28
|
-
data.visibleTypes.map(type => (React.createElement(FeatureTypeTrackContent, { key: type, features: data.groupedFeatures[type], model: model, sequenceLength: data.sequenceLength }))),
|
|
22
|
+
const hoverHandlers = useAlignmentColumnHover(model, data.sequenceLength);
|
|
23
|
+
return (React.createElement("div", { style: { position: 'relative' }, ...hoverHandlers },
|
|
24
|
+
data.visibleGroups.map(group => (React.createElement(FeatureTypeTrackContent, { key: group.type, group: group, model: model, sequenceLength: data.sequenceLength, expanded: model.expandedFeatureTypes.has(group.type) }))),
|
|
29
25
|
React.createElement(HoverMarker, { model: model })));
|
|
30
26
|
});
|
|
@@ -2,6 +2,7 @@ import React, { useMemo, useState } from 'react';
|
|
|
2
2
|
import { Tooltip } from '@mui/material';
|
|
3
3
|
import { observer } from 'mobx-react';
|
|
4
4
|
import { CHAR_WIDTH } from '../constants';
|
|
5
|
+
import useAlignmentColumnHover from '../hooks/useAlignmentColumnHover';
|
|
5
6
|
/**
|
|
6
7
|
* A per-residue scalar track (e.g. pLDDT, hydrophobicity) rendered as colored
|
|
7
8
|
* cells aligned to the pairwise-alignment columns, matching the UniProt feature
|
|
@@ -17,23 +18,14 @@ const ResidueValueTrack = observer(function ResidueValueTrack({ cells, colorFor,
|
|
|
17
18
|
return map;
|
|
18
19
|
}, [cells]);
|
|
19
20
|
const hoveredValue = hoveredCol === undefined ? undefined : valueByCol.get(hoveredCol);
|
|
21
|
+
const hoverHandlers = useAlignmentColumnHover(model, sequenceLength, setHoveredCol);
|
|
20
22
|
return (React.createElement(Tooltip, { title: hoveredValue === undefined ? '' : formatValue(hoveredValue), followCursor: true },
|
|
21
23
|
React.createElement("div", { style: {
|
|
22
24
|
position: 'relative',
|
|
23
25
|
height: model.trackHeight,
|
|
24
26
|
width: sequenceLength * CHAR_WIDTH,
|
|
25
27
|
marginBottom: model.trackGap,
|
|
26
|
-
},
|
|
27
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
28
|
-
const alignmentPos = Math.floor((e.clientX - rect.left) / CHAR_WIDTH);
|
|
29
|
-
setHoveredCol(alignmentPos);
|
|
30
|
-
if (alignmentPos >= 0 && alignmentPos < sequenceLength) {
|
|
31
|
-
model.hoverAlignmentPosition(alignmentPos);
|
|
32
|
-
}
|
|
33
|
-
}, onMouseLeave: () => {
|
|
34
|
-
setHoveredCol(undefined);
|
|
35
|
-
model.setHoveredPosition(undefined);
|
|
36
|
-
} }, cells.map(cell => (React.createElement("div", { key: cell.col, style: {
|
|
28
|
+
}, ...hoverHandlers }, cells.map(cell => (React.createElement("div", { key: cell.col, style: {
|
|
37
29
|
position: 'absolute',
|
|
38
30
|
left: cell.col * CHAR_WIDTH,
|
|
39
31
|
top: 0,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import type { JBrowsePluginProteinStructureModel } from '../model';
|
|
3
|
+
/**
|
|
4
|
+
* Shared mouse handlers for tracks that map cursor x-position to an alignment
|
|
5
|
+
* column and drive the structure hover. Optionally reports the hovered column
|
|
6
|
+
* (used by tracks that show a per-column tooltip).
|
|
7
|
+
*/
|
|
8
|
+
export default function useAlignmentColumnHover(model: JBrowsePluginProteinStructureModel, sequenceLength: number, onCol?: (col: number | undefined) => void): {
|
|
9
|
+
onMouseMove: (e: React.MouseEvent<HTMLDivElement>) => void;
|
|
10
|
+
onMouseLeave: () => void;
|
|
11
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CHAR_WIDTH } from '../constants';
|
|
2
|
+
/**
|
|
3
|
+
* Shared mouse handlers for tracks that map cursor x-position to an alignment
|
|
4
|
+
* column and drive the structure hover. Optionally reports the hovered column
|
|
5
|
+
* (used by tracks that show a per-column tooltip).
|
|
6
|
+
*/
|
|
7
|
+
export default function useAlignmentColumnHover(model, sequenceLength, onCol) {
|
|
8
|
+
return {
|
|
9
|
+
onMouseMove: (e) => {
|
|
10
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
11
|
+
const col = Math.floor((e.clientX - rect.left) / CHAR_WIDTH);
|
|
12
|
+
const inRange = col >= 0 && col < sequenceLength;
|
|
13
|
+
onCol?.(inRange ? col : undefined);
|
|
14
|
+
if (inRange) {
|
|
15
|
+
model.hoverAlignmentPosition(col);
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
onMouseLeave: () => {
|
|
19
|
+
onCol?.(undefined);
|
|
20
|
+
model.setHoveredPosition(undefined);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
import type { UniProtFeature } from './useUniProtFeatures';
|
|
2
2
|
import type { JBrowsePluginProteinStructureModel } from '../model';
|
|
3
|
-
|
|
3
|
+
export interface FeatureLayout {
|
|
4
|
+
feature: UniProtFeature;
|
|
5
|
+
alignmentStart: number;
|
|
6
|
+
alignmentEnd: number;
|
|
7
|
+
left: number;
|
|
8
|
+
width: number;
|
|
9
|
+
lane: number;
|
|
10
|
+
}
|
|
11
|
+
export interface FeatureGroup {
|
|
12
|
+
type: string;
|
|
13
|
+
layouts: FeatureLayout[];
|
|
14
|
+
laneCount: number;
|
|
15
|
+
}
|
|
4
16
|
export interface FeatureTrackData {
|
|
5
|
-
|
|
6
|
-
visibleTypes: string[];
|
|
7
|
-
groupedFeatures: FeaturesByType;
|
|
17
|
+
visibleGroups: FeatureGroup[];
|
|
8
18
|
sequenceLength: number;
|
|
9
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Greedy interval packing: assigns each feature the first lane whose last
|
|
22
|
+
* feature ends before this one starts, so overlapping features of the same type
|
|
23
|
+
* stack instead of hiding each other. Mutates each layout's lane and returns the
|
|
24
|
+
* lane count (at least 1).
|
|
25
|
+
*/
|
|
26
|
+
export declare function packLanes(layouts: FeatureLayout[]): number;
|
|
10
27
|
export default function useProteinFeatureTrackData(model: JBrowsePluginProteinStructureModel, uniprotId: string | undefined): {
|
|
11
28
|
data: FeatureTrackData | undefined;
|
|
12
29
|
isLoading: boolean;
|
|
13
30
|
error: unknown;
|
|
14
31
|
};
|
|
15
|
-
export {};
|
|
@@ -1,25 +1,83 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { CHAR_WIDTH } from '../constants';
|
|
1
3
|
import useUniProtFeatures from './useUniProtFeatures';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Maps a feature's structure range onto alignment columns and pixel geometry.
|
|
6
|
+
* Returns undefined when either endpoint has no alignment column, so unmappable
|
|
7
|
+
* features aren't drawn at a misleading position.
|
|
8
|
+
*/
|
|
9
|
+
function layoutFeature(feature, structurePositionToAlignmentMap) {
|
|
10
|
+
const alignmentStart = structurePositionToAlignmentMap[feature.start - 1];
|
|
11
|
+
const alignmentEnd = structurePositionToAlignmentMap[feature.end - 1];
|
|
12
|
+
return alignmentStart === undefined || alignmentEnd === undefined
|
|
13
|
+
? undefined
|
|
14
|
+
: {
|
|
15
|
+
feature,
|
|
16
|
+
alignmentStart,
|
|
17
|
+
alignmentEnd,
|
|
18
|
+
left: alignmentStart * CHAR_WIDTH,
|
|
19
|
+
width: Math.max((alignmentEnd - alignmentStart + 1) * CHAR_WIDTH, 3),
|
|
20
|
+
lane: 0,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Greedy interval packing: assigns each feature the first lane whose last
|
|
25
|
+
* feature ends before this one starts, so overlapping features of the same type
|
|
26
|
+
* stack instead of hiding each other. Mutates each layout's lane and returns the
|
|
27
|
+
* lane count (at least 1).
|
|
28
|
+
*/
|
|
29
|
+
export function packLanes(layouts) {
|
|
30
|
+
const laneEnds = [];
|
|
31
|
+
const sorted = [...layouts].sort((a, b) => a.alignmentStart - b.alignmentStart);
|
|
32
|
+
for (const layout of sorted) {
|
|
33
|
+
const free = laneEnds.findIndex(end => end < layout.alignmentStart);
|
|
34
|
+
if (free === -1) {
|
|
35
|
+
layout.lane = laneEnds.length;
|
|
36
|
+
laneEnds.push(layout.alignmentEnd);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
layout.lane = free;
|
|
40
|
+
laneEnds[free] = layout.alignmentEnd;
|
|
41
|
+
}
|
|
7
42
|
}
|
|
8
|
-
return
|
|
43
|
+
return Math.max(laneEnds.length, 1);
|
|
9
44
|
}
|
|
10
45
|
export default function useProteinFeatureTrackData(model, uniprotId) {
|
|
11
46
|
const { features, isLoading, error } = useUniProtFeatures(uniprotId);
|
|
12
|
-
const { pairwiseAlignment, hiddenFeatureTypes } = model;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
47
|
+
const { pairwiseAlignment, hiddenFeatureTypes, structurePositionToAlignmentMap } = model;
|
|
48
|
+
const data = useMemo(() => {
|
|
49
|
+
if (!features || !pairwiseAlignment || !structurePositionToAlignmentMap) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const groups = new Map();
|
|
53
|
+
for (const feature of features) {
|
|
54
|
+
if (!hiddenFeatureTypes.has(feature.type)) {
|
|
55
|
+
const layout = layoutFeature(feature, structurePositionToAlignmentMap);
|
|
56
|
+
if (layout) {
|
|
57
|
+
const list = groups.get(feature.type);
|
|
58
|
+
if (list) {
|
|
59
|
+
list.push(layout);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
groups.set(feature.type, [layout]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const visibleGroups = [...groups].map(([type, layouts]) => ({
|
|
68
|
+
type,
|
|
69
|
+
layouts,
|
|
70
|
+
laneCount: packLanes(layouts),
|
|
71
|
+
}));
|
|
72
|
+
return {
|
|
73
|
+
visibleGroups,
|
|
74
|
+
sequenceLength: pairwiseAlignment.alns[0].seq.length,
|
|
75
|
+
};
|
|
76
|
+
}, [
|
|
77
|
+
features,
|
|
78
|
+
pairwiseAlignment,
|
|
79
|
+
hiddenFeatureTypes,
|
|
80
|
+
structurePositionToAlignmentMap,
|
|
81
|
+
]);
|
|
82
|
+
return { data, isLoading, error };
|
|
25
83
|
}
|
|
@@ -55,8 +55,8 @@ async function fetchUniProtFeatures(url) {
|
|
|
55
55
|
continue;
|
|
56
56
|
}
|
|
57
57
|
const type = parts[2];
|
|
58
|
-
const start = Number.parseInt(parts[3] ?? '
|
|
59
|
-
const end = Number.parseInt(parts[4] ?? '
|
|
58
|
+
const start = Number.parseInt(parts[3] ?? '', 10);
|
|
59
|
+
const end = Number.parseInt(parts[4] ?? '', 10);
|
|
60
60
|
const attributes = parts[8] ?? '';
|
|
61
61
|
let description = '';
|
|
62
62
|
let id;
|
|
@@ -69,7 +69,7 @@ async function fetchUniProtFeatures(url) {
|
|
|
69
69
|
id = value;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
if (type) {
|
|
72
|
+
if (type && start >= 1 && end >= start) {
|
|
73
73
|
const uniqueId = `${type}-${start}-${end}-${features.length}`;
|
|
74
74
|
features.push({
|
|
75
75
|
type,
|
|
@@ -63,6 +63,7 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
|
|
|
63
63
|
} | undefined;
|
|
64
64
|
selectedFeatureId: string | undefined;
|
|
65
65
|
hiddenFeatureTypes: Set<string>;
|
|
66
|
+
expandedFeatureTypes: Set<string>;
|
|
66
67
|
} & {
|
|
67
68
|
setStructureData(data: {
|
|
68
69
|
entities?: import("./extractStructureSequences").Entity[];
|
|
@@ -71,6 +72,7 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
|
|
|
71
72
|
hideFeatureType(type: string): void;
|
|
72
73
|
showFeatureType(type: string): void;
|
|
73
74
|
showAllFeatureTypes(): void;
|
|
75
|
+
toggleFeatureTypeExpanded(type: string): void;
|
|
74
76
|
setLoadedToMolstar(val: boolean): void;
|
|
75
77
|
} & {
|
|
76
78
|
readonly connectedView: (import("@jbrowse/mobx-state-tree").ModelInstanceTypeProps<{
|
|
@@ -819,6 +821,7 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
|
|
|
819
821
|
} | undefined;
|
|
820
822
|
selectedFeatureId: string | undefined;
|
|
821
823
|
hiddenFeatureTypes: Set<string>;
|
|
824
|
+
expandedFeatureTypes: Set<string>;
|
|
822
825
|
} & {
|
|
823
826
|
setStructureData(data: {
|
|
824
827
|
entities?: import("./extractStructureSequences").Entity[];
|
|
@@ -827,6 +830,7 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
|
|
|
827
830
|
hideFeatureType(type: string): void;
|
|
828
831
|
showFeatureType(type: string): void;
|
|
829
832
|
showAllFeatureTypes(): void;
|
|
833
|
+
toggleFeatureTypeExpanded(type: string): void;
|
|
830
834
|
setLoadedToMolstar(val: boolean): void;
|
|
831
835
|
} & {
|
|
832
836
|
readonly connectedView: (import("@jbrowse/mobx-state-tree").ModelInstanceTypeProps<{
|
|
@@ -1454,6 +1458,7 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
|
|
|
1454
1458
|
} | undefined;
|
|
1455
1459
|
selectedFeatureId: string | undefined;
|
|
1456
1460
|
hiddenFeatureTypes: Set<string>;
|
|
1461
|
+
expandedFeatureTypes: Set<string>;
|
|
1457
1462
|
} & {
|
|
1458
1463
|
setStructureData(data: {
|
|
1459
1464
|
entities?: import("./extractStructureSequences").Entity[];
|
|
@@ -1462,6 +1467,7 @@ declare function stateModelFactory(): import("@jbrowse/mobx-state-tree").IModelT
|
|
|
1462
1467
|
hideFeatureType(type: string): void;
|
|
1463
1468
|
showFeatureType(type: string): void;
|
|
1464
1469
|
showAllFeatureTypes(): void;
|
|
1470
|
+
toggleFeatureTypeExpanded(type: string): void;
|
|
1465
1471
|
setLoadedToMolstar(val: boolean): void;
|
|
1466
1472
|
} & {
|
|
1467
1473
|
readonly connectedView: (import("@jbrowse/mobx-state-tree").ModelInstanceTypeProps<{
|
|
@@ -134,6 +134,12 @@ declare const Structure: import("@jbrowse/mobx-state-tree").IModelType<{
|
|
|
134
134
|
* Set of feature track types that are hidden
|
|
135
135
|
*/
|
|
136
136
|
hiddenFeatureTypes: Set<string>;
|
|
137
|
+
/**
|
|
138
|
+
* #volatile
|
|
139
|
+
* Set of feature track types expanded to show every overlapping feature on
|
|
140
|
+
* its own lane (collapsed types draw all features on a single row)
|
|
141
|
+
*/
|
|
142
|
+
expandedFeatureTypes: Set<string>;
|
|
137
143
|
} & {
|
|
138
144
|
setStructureData(data: {
|
|
139
145
|
entities?: Entity[];
|
|
@@ -151,6 +157,10 @@ declare const Structure: import("@jbrowse/mobx-state-tree").IModelType<{
|
|
|
151
157
|
* #action
|
|
152
158
|
*/
|
|
153
159
|
showAllFeatureTypes(): void;
|
|
160
|
+
/**
|
|
161
|
+
* #action
|
|
162
|
+
*/
|
|
163
|
+
toggleFeatureTypeExpanded(type: string): void;
|
|
154
164
|
/**
|
|
155
165
|
* #action
|
|
156
166
|
*/
|