UNPKG

higlass

Version:

HiGlass Hi-C / genomic / large data viewer

288 lines (248 loc) 8.37 kB
// @ts-nocheck import clsx from 'clsx'; import { brushX } from 'd3-brush'; import { select } from 'd3-selection'; import PropTypes from 'prop-types'; import React from 'react'; import slugid from 'slugid'; import HorizontalItem from './HorizontalItem'; import ListWrapper from './ListWrapper'; import SortableList from './SortableList'; // Utils import { IS_TRACK_RANGE_SELECTABLE, or, resetD3BrushStyle, sum } from './utils'; // Styles import styles from '../styles/HorizontalTiledPlot.module.scss'; import stylesPlot from '../styles/TiledPlot.module.scss'; import stylesTrack from '../styles/Track.module.scss'; import { trackHeight, trackWidth } from './utils'; function sourceEvent(event) { return event?.sourceEvent; } class HorizontalTiledPlot extends React.Component { constructor(props) { super(props); this.brushBehavior = brushX() .on('start', this.brushStarted.bind(this)) .on('brush', this.brushed.bind(this)) .on('end', this.brushedEnded.bind(this)); this.state = { // Track which track's controls are visible trackControlsVisible: null, }; } /* -------------------------- Life Cycle Methods -------------------------- */ componentDidMount() { if (this.props.isRangeSelectionActive) { this.addBrush(); } } shouldComponentUpdate(nextProps, nextState) { if (this.rangeSelectionTriggered) { this.rangeSelectionTriggered = false; if ( this.rangeSelectionTriggeredEnd && this.props.rangeSelection !== nextProps.rangeSelection ) { this.moveBrush( nextProps.rangeSelection[0] ? nextProps.rangeSelection[0] : null, true, ); } this.rangeSelectionTriggeredEnd = false; return this.state !== nextState; } if (this.props.rangeSelection !== nextProps.rangeSelection) { this.moveBrush( nextProps.rangeSelection[0] ? nextProps.rangeSelection[0] : null, nextProps.rangeSelectionEnd, ); return this.state !== nextState; } return true; } componentDidUpdate() { if (this.props.isRangeSelectionActive) { this.addBrush(); } else { this.removeBrush(); } } /* ---------------------------- Custom Methods ---------------------------- */ addBrush() { if (!this.brushEl || this.brushElAddedBefore === this.brushEl) { return; } if (this.brushElAddedBefore) { // Remove event listener on old element to avoid memory leaks this.brushElAddedBefore.on('.brush', null); } this.brushEl.call(this.brushBehavior); this.brushElAddedBefore = this.brushEl; resetD3BrushStyle( this.brushEl, stylesTrack['track-range-selection-group-brush-selection'], ); } brushed(event) { // Need to reassign variable to check after reset const rangeSelectionMoved = this.rangeSelectionMoved; this.rangeSelectionMoved = false; if ( !sourceEvent(event) || !this.props.onRangeSelection || rangeSelectionMoved ) return; this.rangeSelectionTriggered = true; this.props.onRangeSelection(event.selection); } brushStarted(event) { if (!sourceEvent(event) || !event.selection) return; this.props.onRangeSelectionStart(); } brushedEnded(event) { if (!this.props.is1dRangeSelection) return; const rangeSelectionMovedEnd = this.rangeSelectionMovedEnd; this.rangeSelectionMovedEnd = false; // Brush end event with a selection if ( event.selection && event.sourceEvent && this.props.onRangeSelection && !rangeSelectionMovedEnd ) { this.rangeSelectionTriggered = true; this.rangeSelectionTriggeredEnd = true; this.props.onRangeSelectionEnd(event.selection); } // Brush end event with no selection, i.e., the selection is reset if (!event.selection) { this.rangeSelectionTriggered = true; this.props.onRangeSelectionReset(); } } moveBrush(rangeSelection, animate = false) { if (!this.brushEl) { return; } const relRange = rangeSelection ? [ this.props.scale(rangeSelection[0]), this.props.scale(rangeSelection[1]), ] : null; this.rangeSelectionMoved = true; this.rangeSelectionMovedEnd = true; if (animate) { this.brushEl.transition().call(this.brushBehavior.move, relRange); } else { this.brushEl.call(this.brushBehavior.move, relRange); } } removeBrush() { if (this.brushElAddedBefore) { // Reset brush selection this.brushElAddedBefore.call(this.brushBehavior.move, null); // Remove brush behavior this.brushElAddedBefore.on('.brush', null); this.brushElAddedBefore = undefined; this.props.onRangeSelectionReset(); } } /* ------------------------------ Rendering ------------------------------- */ render() { const height = this.props.tracks.map((x) => x.height).reduce(sum, 0); const isBrushable = this.props.tracks .map((track) => IS_TRACK_RANGE_SELECTABLE(track)) .reduce(or, false); const rangeSelectorClass = this.props.isRangeSelectionActive ? stylesTrack['track-range-selection-active'] : stylesTrack['track-range-selection']; return ( <div className={clsx( 'horizontal-tiled-plot', styles['horizontal-tiled-plot'], )} > {isBrushable && ( <svg ref={(el) => { this.brushEl = select(el); }} className={rangeSelectorClass} style={{ height, width: this.props.width, }} xmlns="http://www.w3.org/2000/svg" /> )} <ListWrapper className={clsx(stylesPlot.list, stylesPlot.stylizedList)} component={SortableList} editable={this.props.editable} handleConfigTrack={this.props.handleConfigTrack} handleResizeTrack={this.props.handleResizeTrack} height={height} helperClass={stylesPlot.stylizedHelper} itemClass={stylesPlot.stylizedItem} itemReactClass={HorizontalItem} items={this.props.tracks.map((d) => ({ handleMouseEnter: () => { this.setState({ trackControlsVisible: d.uid }); }, handleMouseLeave: () => { this.setState({ trackControlsVisible: null }); }, trackControlsVisible: d.uid === this.state.trackControlsVisible, configMenuVisible: d.uid === this.props.configTrackMenuId, uid: d.uid || slugid.nice(), width: this.props.width, height: trackHeight(d), isCollapsed: d.options?.collapsed, value: d.value, }))} onAddSeries={this.props.onAddSeries} onCloseTrack={this.props.onCloseTrack} onCloseTrackMenuOpened={this.props.onCloseTrackMenuOpened} onCollapseTrack={this.props.onCollapseTrack} onExpandTrack={this.props.onExpandTrack} onConfigTrackMenuOpened={this.props.onConfigTrackMenuOpened} onSortEnd={this.props.handleSortEnd} referenceAncestor={this.props.referenceAncestor} resizeHandles={this.props.resizeHandles} useDragHandle={true} width={this.props.width} /> </div> ); } } HorizontalTiledPlot.propTypes = { configTrackMenuId: PropTypes.string, editable: PropTypes.bool, handleConfigTrack: PropTypes.func, handleResizeTrack: PropTypes.func, handleSortEnd: PropTypes.func, is1dRangeSelection: PropTypes.bool, isRangeSelectionActive: PropTypes.bool, onAddSeries: PropTypes.func, onCloseTrack: PropTypes.func, onCollapseTrack: PropTypes.func, onExpandTrack: PropTypes.func, onCloseTrackMenuOpened: PropTypes.func, onConfigTrackMenuOpened: PropTypes.func, onRangeSelection: PropTypes.func, onRangeSelectionEnd: PropTypes.func, onRangeSelectionReset: PropTypes.func, onRangeSelectionStart: PropTypes.func, rangeSelection: PropTypes.array, rangeSelectionEnd: PropTypes.bool, referenceAncestor: PropTypes.func, resizeHandles: PropTypes.object, scale: PropTypes.func, tracks: PropTypes.array, width: PropTypes.number, }; export default HorizontalTiledPlot;