@jbrowse/plugin-wiggle
Version:
JBrowse 2 wiggle adapters, tracks, etc.
166 lines (165 loc) • 7.24 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useCallback, useEffect, useState } from 'react';
import { ResizeHandle } from '@jbrowse/core/ui';
import { getContainingView } from '@jbrowse/core/util';
import Flatbush from '@jbrowse/core/util/flatbush';
import { makeStyles } from '@jbrowse/core/util/tss-react';
import { Menu, MenuItem, alpha } from '@mui/material';
import { autorun } from 'mobx';
import { observer } from 'mobx-react';
import { SIDEBAR_BACKGROUND_OPACITY } from "./constants.js";
const useStyles = makeStyles()(theme => ({
resizeHandle: {
position: 'absolute',
top: 0,
height: '100%',
width: 4,
zIndex: 101,
background: 'transparent',
'&:hover': {
background: theme.palette.divider,
},
},
treeBackground: {
position: 'absolute',
top: 0,
left: 0,
background: alpha(theme.palette.background.paper, SIDEBAR_BACKGROUND_OPACITY),
},
}));
function getDescendantNames(node) {
if (!node.children?.length) {
return [node.data.name];
}
return node.children.flatMap(child => getDescendantNames(child));
}
const TreeSidebar = observer(function TreeSidebar({ model, }) {
const { classes } = useStyles();
const { width: viewWidth } = getContainingView(model);
const [nodeIndex, setNodeIndex] = useState(null);
const [nodeData, setNodeData] = useState([]);
const [menuAnchor, setMenuAnchor] = useState(null);
const { hierarchy, treeAreaWidth, height, scrollTop, showTree, sources } = model;
const treeCanvasRef = useCallback((ref) => {
model.setTreeCanvasRef(ref);
}, [model, treeAreaWidth, height]);
const mouseoverCanvasRef = useCallback((ref) => {
model.setMouseoverCanvasRef(ref);
}, [model, viewWidth, height]);
useEffect(() => {
return autorun(function treeSpatialIndexAutorun() {
const { treeAreaWidth: _t, hierarchy: h, totalHeight: th } = model;
th;
if (!h) {
setNodeIndex(null);
setNodeData([]);
return;
}
const nodes = h.descendants().filter(node => node.children?.length);
const index = new Flatbush(nodes.length);
const hitRadius = 8;
for (const node of nodes) {
const x = node.y;
const y = node.x;
index.add(x - hitRadius, y - hitRadius, x + hitRadius, y + hitRadius);
}
index.finish();
setNodeIndex(index);
setNodeData(nodes);
}, { name: 'TreeSpatialIndex' });
}, [model]);
const handleMouseMove = useCallback((event) => {
if (!hierarchy || !nodeIndex) {
return;
}
const rect = event.currentTarget.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top + scrollTop;
const results = nodeIndex.search(x, y, x, y);
const node = results.length > 0 ? nodeData[results[0]] : undefined;
if (node) {
const descendantNames = getDescendantNames(node);
model.setHoveredTreeNode({
node,
descendantNames,
});
}
else {
model.setHoveredTreeNode(undefined);
}
}, [hierarchy, nodeIndex, nodeData, scrollTop, model]);
const handleMouseLeave = useCallback(() => {
model.setHoveredTreeNode(undefined);
}, [model]);
const handleClick = useCallback((event) => {
if (!hierarchy || !nodeIndex) {
return;
}
const rect = event.currentTarget.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top + scrollTop;
const results = nodeIndex.search(x, y, x, y);
const node = results.length > 0 ? nodeData[results[0]] : undefined;
if (node) {
const descendantNames = getDescendantNames(node);
setMenuAnchor({
x: event.clientX,
y: event.clientY,
names: descendantNames,
});
}
}, [hierarchy, nodeIndex, nodeData, scrollTop]);
const handleCloseMenu = useCallback(() => {
setMenuAnchor(null);
}, []);
if (!hierarchy || !showTree || !sources?.length) {
return null;
}
return (_jsxs(_Fragment, { children: [_jsxs("div", { style: {
position: 'sticky',
top: 0,
left: 0,
height: 0,
zIndex: 100,
}, children: [_jsx("div", { className: classes.treeBackground, style: {
width: treeAreaWidth,
height,
} }), _jsx("canvas", { ref: treeCanvasRef, width: treeAreaWidth * 2, height: height * 2, style: {
width: treeAreaWidth,
height,
position: 'absolute',
top: 0,
left: 0,
pointerEvents: 'none',
} }), _jsx("canvas", { ref: mouseoverCanvasRef, width: viewWidth, height: height, style: {
width: viewWidth,
height,
position: 'absolute',
top: 0,
left: 0,
zIndex: 1,
pointerEvents: 'none',
} }), _jsx("div", { onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave, onClick: handleClick, style: {
position: 'absolute',
top: 0,
left: 0,
width: treeAreaWidth,
height,
zIndex: 2,
cursor: 'pointer',
} })] }), _jsx(ResizeHandle, { onDrag: distance => {
model.setTreeAreaWidth(Math.max(50, treeAreaWidth + distance));
return undefined;
}, className: classes.resizeHandle, style: {
left: treeAreaWidth,
}, vertical: true }), _jsxs(Menu, { open: !!menuAnchor, onClose: handleCloseMenu, anchorReference: "anchorPosition", anchorPosition: menuAnchor ? { top: menuAnchor.y, left: menuAnchor.x } : undefined, children: [model.subtreeFilter?.length ? (_jsx(MenuItem, { onClick: () => {
model.setSubtreeFilter(undefined);
handleCloseMenu();
}, children: "Clear subtree filter" })) : null, _jsxs(MenuItem, { onClick: () => {
if (menuAnchor) {
model.setSubtreeFilter(menuAnchor.names);
}
handleCloseMenu();
}, children: ["Show only subtree (", menuAnchor?.names.length, " samples)"] })] })] }));
});
export default TreeSidebar;