UNPKG

@jbrowse/plugin-wiggle

Version:

JBrowse 2 wiggle adapters, tracks, etc.

166 lines (165 loc) 7.24 kB
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;