UNPKG

oncoprintjs

Version:

A data visualization for cancer genomic data.

634 lines (601 loc) 22.7 kB
import $ from 'jquery'; import menuDotsIcon from '../img/menudots.svg'; import OncoprintModel, { GAP_MODE_ENUM, TrackId, TrackProp, TrackSortDirection, } from './oncoprintmodel'; import { CLOSE_MENUS_EVENT as HEADER_VIEW_CLOSE_MENUS_EVENT } from './oncoprintheaderview'; import ClickEvent = JQuery.ClickEvent; const TOGGLE_BTN_CLASS = 'oncoprintjs__track_options__toggle_btn_img'; const TOGGLE_BTN_OPEN_CLASS = 'oncoprintjs__track_options__open'; const DROPDOWN_CLASS = 'oncoprintjs__track_options__dropdown'; const SEPARATOR_CLASS = 'oncoprintjs__track_options__separator'; const NTH_CLASS_PREFIX = 'nth-'; export const CLOSE_MENUS_EVENT = 'oncoprint-track-options-view.do-close-menus'; type TrackCallback = (trackId: TrackId) => void; export default class OncoprintTrackOptionsView { private $ctr: JQuery; private $buttons_ctr: JQuery; private $dropdown_ctr: JQuery; private img_size: number; private rendering_suppressed = false; private track_options_$elts: TrackProp<{ $div: JQuery; $img: JQuery; $dropdown: JQuery; }> = {}; private menu_shown: TrackProp<boolean> = {}; private clickHandler: () => void; private interaction_disabled = false; constructor( private $div: JQuery, private moveUpCallback: TrackCallback, private moveDownCallback: TrackCallback, private removeCallback: TrackCallback, private sortChangeCallback: ( trackId: TrackId, sortDirection: TrackSortDirection ) => void, private unexpandCallback: TrackCallback, private showGapsCallback: ( trackId: TrackId, showGaps: GAP_MODE_ENUM ) => void ) { const position = $div.css('position'); if (position !== 'absolute' && position !== 'relative') { console.log( 'WARNING: div passed to OncoprintTrackOptionsView must be absolute or relative positioned - layout problems will occur' ); } this.$ctr = $('<div></div>') .css({ position: 'absolute', 'overflow-y': 'hidden', 'overflow-x': 'hidden', }) .appendTo(this.$div); this.$buttons_ctr = $('<div></div>') .css({ position: 'absolute' }) .appendTo(this.$ctr); this.$dropdown_ctr = $('<div></div>') .css({ position: 'absolute' }) .appendTo(this.$div); const self = this; this.clickHandler = function() { $(document).trigger(CLOSE_MENUS_EVENT); }; $(document).on('click', this.clickHandler); } private renderAllOptions(model: OncoprintModel) { if (this.rendering_suppressed) { return; } const self = this; $(document).off(CLOSE_MENUS_EVENT); $(document).on(CLOSE_MENUS_EVENT, function() { self.hideAllMenus(); }); this.$buttons_ctr.empty(); this.$dropdown_ctr.empty(); this.scroll(model.getVertScroll()); var tracks = model.getTracks(); var minimum_track_height = Number.POSITIVE_INFINITY; for (let i = 0; i < tracks.length; i++) { minimum_track_height = Math.min( minimum_track_height, model.getTrackHeight(tracks[i]) ); } this.img_size = Math.floor(minimum_track_height * 0.75); for (let i = 0; i < tracks.length; i++) { this.renderTrackOptions(model, tracks[i], i); } } private scroll(scroll_y: number) { if (this.rendering_suppressed) { return; } this.$buttons_ctr.css({ top: -scroll_y }); this.$dropdown_ctr.css({ top: -scroll_y }); this.hideAllMenus(); } private resize(model: OncoprintModel, getCellViewHeight: () => number) { if (this.rendering_suppressed) { return; } this.$div.css({ width: this.getWidth(), height: getCellViewHeight() }); this.$ctr.css({ width: this.getWidth(), height: getCellViewHeight() }); } private hideTrackMenu(track_id: TrackId) { this.menu_shown[track_id] = false; const $elts = this.track_options_$elts[track_id]; $elts.$dropdown.css({ 'z-index': 1 }); $elts.$dropdown.css({ border: '1px solid rgba(125,125,125,0)' }); $elts.$img.css({ border: '1px solid rgba(125,125,125,0)' }); $elts.$dropdown.fadeOut(100); } private showTrackMenu(track_id: TrackId) { this.menu_shown[track_id] = true; const $elts = this.track_options_$elts[track_id]; $elts.$dropdown.css({ 'z-index': 10 }); $elts.$dropdown.css({ border: '1px solid rgba(125,125,125,1)' }); $elts.$img.css({ border: '1px solid rgba(125,125,125,1)' }); $elts.$dropdown.fadeIn(100); } private hideAllMenus() { for (const track_id in this.track_options_$elts) { if (this.track_options_$elts.hasOwnProperty(track_id)) { this.hideTrackMenu(parseInt(track_id, 10)); } } } private hideMenusExcept(track_id: TrackId) { for (const _other_track_id in this.track_options_$elts) { if (this.track_options_$elts.hasOwnProperty(_other_track_id)) { const other_track_id = parseInt(_other_track_id, 10); if (other_track_id === track_id) { continue; } this.hideTrackMenu(other_track_id); } } $(document).trigger(HEADER_VIEW_CLOSE_MENUS_EVENT); } private static $makeDropdownOption( text: string, weight: string, disabled?: boolean, callback?: (evt: ClickEvent) => void ) { const li = $('<li>') .text(text) .css({ 'font-weight': weight, 'font-size': 12, 'border-bottom': '1px solid rgba(0,0,0,0.3)', }); if (!disabled) { if (callback) { li.addClass('clickable'); li.css({ cursor: 'pointer' }); li.click(callback).hover( function() { $(this).css({ 'background-color': 'rgb(200,200,200)' }); }, function() { $(this).css({ 'background-color': 'rgba(255,255,255,0)', }); } ); } else { li.click(function(evt) { evt.stopPropagation(); }); } } else { li.addClass('disabled'); li.css({ color: 'rgb(200, 200, 200)', cursor: 'default' }); } return li; } private static $makeDropdownSeparator() { return $('<li>') .css({ 'border-top': '1px solid black' }) .addClass(SEPARATOR_CLASS); } // 11/2/2023 we are removing sort arrow // leaving commented out if it needs to be restored based on complaint private static renderSortArrow( $sortarrow: JQuery, model: OncoprintModel, track_id: TrackId ) { // let sortarrow_char = ''; // if (model.isTrackSortDirectionChangeable(track_id)) { // sortarrow_char = { // '1': // '<i class="fa fa-signal" aria-hidden="true" title="Sorted ascending"></i>', // '-1': // '<i class="fa fa-signal" style="transform: scaleX(-1);" aria-hidden="true" title="Sorted descending"></i>', // '0': '', // }[model.getTrackSortDirection(track_id)]; // } // $sortarrow.html(sortarrow_char); } private renderTrackOptions( model: OncoprintModel, track_id: TrackId, index: number ) { let $div: JQuery, $img: JQuery, $sortarrow: JQuery, $dropdown: JQuery; const top = model.getZoomedTrackTops(track_id); $div = $('<div>') .appendTo(this.$buttons_ctr) .css({ position: 'absolute', left: '0px', top: top + 'px', 'white-space': 'nowrap', }); $img = $('<img/>') .appendTo($div) .attr({ src: menuDotsIcon, alt: 'Menu Dots Icon', width: this.img_size, height: this.img_size, }) .css({ float: 'left', cursor: 'pointer', border: '1px solid rgba(125,125,125,0)', }) .addClass(TOGGLE_BTN_CLASS) .addClass(NTH_CLASS_PREFIX + (index + 1)); $sortarrow = $('<span>') .appendTo($div) .css({ position: 'absolute', top: Math.floor(this.img_size / 4) + 'px', }); $dropdown = $('<ul>') .appendTo(this.$dropdown_ctr) .css({ position: 'absolute', width: 120, display: 'none', 'list-style-type': 'none', 'padding-left': '6', 'padding-right': '6', float: 'right', 'background-color': 'rgb(255,255,255)', left: '0px', top: top + this.img_size + 'px', }) .addClass(DROPDOWN_CLASS) .addClass(NTH_CLASS_PREFIX + (index + 1)); this.track_options_$elts[track_id] = { $div: $div, $img: $img, $dropdown: $dropdown, }; OncoprintTrackOptionsView.renderSortArrow($sortarrow, model, track_id); const self = this; $img.hover( function(evt) { if (!self.menu_shown[track_id]) { $(this).css({ border: '1px solid rgba(125,125,125,0.3)' }); } }, function(evt) { if (!self.menu_shown[track_id]) { $(this).css({ border: '1px solid rgba(125,125,125,0)' }); } } ); $img.click(function(evt) { evt.stopPropagation(); if ($dropdown.is(':visible')) { $img.addClass(TOGGLE_BTN_OPEN_CLASS); self.hideTrackMenu(track_id); } else { $img.removeClass(TOGGLE_BTN_OPEN_CLASS); self.showTrackMenu(track_id); } self.hideMenusExcept(track_id); }); const movingDisabled = model.getTrackMovable(track_id) && model.isTrackInClusteredGroup(track_id); if (model.getTrackMovable(track_id)) { $dropdown.append( OncoprintTrackOptionsView.$makeDropdownOption( 'Move up', 'normal', movingDisabled, function(evt) { evt.stopPropagation(); self.moveUpCallback(track_id); } ) ); $dropdown.append( OncoprintTrackOptionsView.$makeDropdownOption( 'Move down', 'normal', movingDisabled, function(evt) { evt.stopPropagation(); self.moveDownCallback(track_id); } ) ); } if (model.isTrackRemovable(track_id)) { $dropdown.append( OncoprintTrackOptionsView.$makeDropdownOption( 'Remove track', 'normal', false, function(evt) { evt.stopPropagation(); self.removeCallback(track_id); } ) ); } if (model.isTrackSortDirectionChangeable(track_id)) { $dropdown.append( OncoprintTrackOptionsView.$makeDropdownSeparator() ); let $sort_inc_li: JQuery; let $sort_dec_li: JQuery; let $dont_sort_li: JQuery; $sort_inc_li = OncoprintTrackOptionsView.$makeDropdownOption( 'Sort a-Z', model.getTrackSortDirection(track_id) === 1 ? 'bold' : 'normal', false, function(evt) { evt.stopPropagation(); $sort_inc_li.css('font-weight', 'bold'); $sort_dec_li.css('font-weight', 'normal'); $dont_sort_li.css('font-weight', 'normal'); self.sortChangeCallback(track_id, 1); OncoprintTrackOptionsView.renderSortArrow( $sortarrow, model, track_id ); } ); $sort_dec_li = OncoprintTrackOptionsView.$makeDropdownOption( 'Sort Z-a', model.getTrackSortDirection(track_id) === -1 ? 'bold' : 'normal', false, function(evt) { evt.stopPropagation(); $sort_inc_li.css('font-weight', 'normal'); $sort_dec_li.css('font-weight', 'bold'); $dont_sort_li.css('font-weight', 'normal'); self.sortChangeCallback(track_id, -1); OncoprintTrackOptionsView.renderSortArrow( $sortarrow, model, track_id ); } ); $dont_sort_li = OncoprintTrackOptionsView.$makeDropdownOption( "Don't sort track", model.getTrackSortDirection(track_id) === 0 ? 'bold' : 'normal', false, function(evt) { evt.stopPropagation(); $sort_inc_li.css('font-weight', 'normal'); $sort_dec_li.css('font-weight', 'normal'); $dont_sort_li.css('font-weight', 'bold'); self.sortChangeCallback(track_id, 0); OncoprintTrackOptionsView.renderSortArrow( $sortarrow, model, track_id ); } ); $dropdown.append($sort_inc_li); $dropdown.append($sort_dec_li); $dropdown.append($dont_sort_li); } if (model.isTrackExpandable(track_id)) { $dropdown.append( OncoprintTrackOptionsView.$makeDropdownOption( model.getExpandButtonText(track_id), 'normal', false, function(evt) { evt.stopPropagation(); // close the menu to discourage clicking again, as it // may take a moment to finish expanding self.renderAllOptions(model); model.expandTrack(track_id); } ) ); } if (model.isTrackExpanded(track_id)) { $dropdown.append( OncoprintTrackOptionsView.$makeDropdownOption( 'Remove expansion', 'normal', false, function(evt) { evt.stopPropagation(); self.unexpandCallback(track_id); } ) ); } if (model.getTrackCanShowGaps(track_id)) { $dropdown.append( OncoprintTrackOptionsView.$makeDropdownSeparator() ); const $show_gaps_percent_opt = OncoprintTrackOptionsView.$makeDropdownOption( model.getTrackShowGaps(track_id) === GAP_MODE_ENUM.SHOW_GAPS_PERCENT ? 'Hide gaps (w/%)' : 'Show Gaps (w/%)', model.getTrackShowGaps(track_id) === GAP_MODE_ENUM.SHOW_GAPS_PERCENT ? 'bold' : 'normal', false, function(evt) { evt.stopPropagation(); $show_gaps_opt.css('font-weight', 'bold'); const mode: GAP_MODE_ENUM = [ GAP_MODE_ENUM.SHOW_GAPS_PERCENT, ].includes(model.getTrackShowGaps(track_id)) ? GAP_MODE_ENUM.HIDE_GAPS : GAP_MODE_ENUM.SHOW_GAPS_PERCENT; self.showGapsCallback(track_id, mode); } ); const $show_gaps_opt = OncoprintTrackOptionsView.$makeDropdownOption( model.getTrackShowGaps(track_id) === GAP_MODE_ENUM.SHOW_GAPS ? 'Hide gaps' : 'Show Gaps', model.getTrackShowGaps(track_id) === GAP_MODE_ENUM.SHOW_GAPS ? 'bold' : 'normal', false, function(evt) { evt.stopPropagation(); $show_gaps_opt.css('font-weight', 'bold'); const mode: GAP_MODE_ENUM = [ GAP_MODE_ENUM.SHOW_GAPS, ].includes(model.getTrackShowGaps(track_id)) ? GAP_MODE_ENUM.HIDE_GAPS : GAP_MODE_ENUM.SHOW_GAPS; self.showGapsCallback(track_id, mode); } ); $dropdown.append($show_gaps_opt); $dropdown.append($show_gaps_percent_opt); } // Add custom options const custom_options = model.getTrackCustomOptions(track_id); if (custom_options && custom_options.length > 0) { for (var i = 0; i < custom_options.length; i++) { (function() { // wrapped in function to prevent scope issues var option = custom_options[i]; if (option.separator) { $dropdown.append( OncoprintTrackOptionsView.$makeDropdownSeparator() ); } else { $dropdown.append( OncoprintTrackOptionsView.$makeDropdownOption( option.label || '', option.weight || 'normal', option.disabled, option.onClick && function(evt) { evt.stopPropagation(); option.onClick(track_id); } ) ); } })(); } } if ($dropdown.is(':empty')) { // if no options, then delete elements $div.remove(); $dropdown.remove(); } } public enableInteraction() { this.interaction_disabled = false; } public disableInteraction() { this.interaction_disabled = true; } public suppressRendering() { this.rendering_suppressed = true; } public releaseRendering( model: OncoprintModel, getCellViewHeight: () => number ) { this.rendering_suppressed = false; this.renderAllOptions(model); this.resize(model, getCellViewHeight); this.scroll(model.getVertScroll()); } public setScroll(model: OncoprintModel) { this.setVertScroll(model); } public setHorzScroll(model: OncoprintModel) {} public setVertScroll(model: OncoprintModel) { this.scroll(model.getVertScroll()); } public setZoom(model: OncoprintModel, getCellViewHeight: () => number) { this.setVertZoom(model, getCellViewHeight); } public setVertZoom(model: OncoprintModel, getCellViewHeight: () => number) { this.renderAllOptions(model); this.resize(model, getCellViewHeight); } public setTrackGroupHeader( model: OncoprintModel, getCellViewHeight: () => number ) { this.renderAllOptions(model); this.resize(model, getCellViewHeight); } public sort(model: OncoprintModel, getCellViewHeight: () => number) { this.renderAllOptions(model); this.resize(model, getCellViewHeight); } public setViewport(model: OncoprintModel, getCellViewHeight: () => number) { this.renderAllOptions(model); this.resize(model, getCellViewHeight); this.scroll(model.getVertScroll()); } public getWidth() { if (this.$buttons_ctr.is(':empty')) { return 0; } else { return 18 + this.img_size; } } public setTrackShowGaps( model: OncoprintModel, getCellViewHeight: () => number ) { this.renderAllOptions(model); this.resize(model, getCellViewHeight); } public addTracks(model: OncoprintModel, getCellViewHeight: () => number) { this.setTrackShowGaps(model, getCellViewHeight); this.renderAllOptions(model); this.resize(model, getCellViewHeight); } public moveTrack(model: OncoprintModel, getCellViewHeight: () => number) { this.renderAllOptions(model); this.resize(model, getCellViewHeight); } public setTrackGroupOrder(model: OncoprintModel) { this.renderAllOptions(model); } public setSortConfig(model: OncoprintModel) { this.renderAllOptions(model); } public removeTrack( model: OncoprintModel, track_id: TrackId, getCellViewHeight: () => number ) { delete this.track_options_$elts[track_id]; this.renderAllOptions(model); this.resize(model, getCellViewHeight); } public destroy() { $(document).off('click', this.clickHandler); $(document).off(CLOSE_MENUS_EVENT); } public setTrackCustomOptions(model: OncoprintModel) { this.renderAllOptions(model); } public setTrackMovable(model: OncoprintModel) { this.renderAllOptions(model); } }