oncoprintjs
Version:
A data visualization for cancer genomic data.
1,443 lines (1,297 loc) • 61.1 kB
text/typescript
import './polyfill';
import OncoprintModel, {
ColumnId,
ColumnLabel,
ColumnProp,
CustomTrackOption,
Datum,
GAP_MODE_ENUM,
LibraryTrackSpec,
SortConfig,
TrackGroupHeader,
TrackGroupIndex,
TrackId,
TrackSortDirection,
TrackSortSpecification,
TrackTooltipFn,
UserTrackSpec,
} from './oncoprintmodel';
import OncoprintWebGLCellView from './oncoprintwebglcellview';
import OncoprintLabelView from './oncoprintlabelview';
import OncoprintRuleSet, { RuleSetParams } from './oncoprintruleset';
import OncoprintTrackOptionsView from './oncoprinttrackoptionsview';
import OncoprintLegendView from './oncoprintlegendrenderer';
import OncoprintToolTip from './oncoprinttooltip';
import OncoprintTrackInfoView from './oncoprinttrackinfoview';
import OncoprintMinimapView, {
MinimapViewportSpec,
} from './oncoprintminimapview';
import svgfactory from './svgfactory';
import $ from 'jquery';
import { clamp } from './utils';
import OncoprintHeaderView from './oncoprintheaderview';
export type InitParams = {
init_cell_width?: number;
init_cell_padding?: number;
cell_padding_off_cell_width_threshold?: number;
init_horz_zoom?: number;
init_vert_zoom?: number;
init_track_group_padding?: number;
init_cell_padding_on?: boolean;
max_height?: number;
};
export type HorzZoomCallback = (zoom: number) => void;
export type MinimapCloseCallback = () => void;
export type CellMouseOverCallback = (
uid: ColumnId | null,
track_id?: TrackId
) => void;
export type CellClickCallback = (
uid: ColumnId | null,
track_id?: TrackId
) => void;
export type ClipboardChangeCallback = (ids: ColumnId[]) => void;
const nextTrackId = (function() {
let ctr = 0;
return function() {
ctr += 1;
return ctr;
};
})();
export default class Oncoprint {
// this is the controller
private lastSortId = 0;
private incrementLastSortId() {
this.lastSortId = (this.lastSortId + 1) % 1000000; // make sure we don't have any overflow problems. Definitely won't have a million sorts pending at once
}
public destroyed: boolean;
public webgl_unavailable: boolean;
private $ctr: JQuery;
private $oncoprint_ctr: JQuery;
private $cell_div: JQuery;
private $header_div: JQuery;
private $legend_div: JQuery;
private $track_options_div: JQuery;
private $track_info_div: JQuery;
private $dummy_scroll_div: JQuery;
private $minimap_div: JQuery;
private $cell_canvas: JQuery;
private $cell_overlay_canvas: JQuery;
public model: OncoprintModel;
public header_view: OncoprintHeaderView;
public cell_view: OncoprintWebGLCellView;
public minimap_view: OncoprintMinimapView;
public track_options_view: OncoprintTrackOptionsView;
public track_info_view: OncoprintTrackInfoView;
public label_view: OncoprintLabelView;
public legend_view: OncoprintLegendView;
private keep_horz_zoomed_to_fit: boolean;
private keep_horz_zoomed_to_fit_ids: ColumnId[];
private pending_resize_and_organize: boolean;
private horz_zoom_callbacks: HorzZoomCallback[];
private minimap_close_callbacks: MinimapCloseCallback[];
private cell_mouse_over_callbacks: CellMouseOverCallback[];
private cell_click_callbacks: CellClickCallback[];
private id_clipboard: ColumnId[];
private clipboard_change_callbacks: ClipboardChangeCallback[];
private target_dummy_scroll_left: number;
private target_dummy_scroll_top: number;
private getCellViewHeight = () =>
this.cell_view.getVisibleAreaHeight(this.model);
constructor(
private ctr_selector: string,
private width: number,
params?: InitParams
) {
params = params || {};
const self = this;
this.destroyed = false;
this.webgl_unavailable =
document
.createElement('canvas')
.getContext('experimental-webgl') === null;
if (this.webgl_unavailable) {
$(ctr_selector).append(
"<p class='oncoprintjs__webgl_unavailable_message'>WebGL context cannot be retrieved, so oncoprint cannot be used. Please visit <a href='http://webglreport.com'>WebGL Report</a> to explore your browsers WebGL capabilities.</p>"
);
return;
}
const $ctr = $('<span></span>')
.css({ position: 'relative', display: 'inline-block' })
.appendTo(ctr_selector);
const $oncoprint_ctr = $('<div></div>')
.css({ position: 'absolute', display: 'inline-block' })
.appendTo($ctr);
const $tooltip_ctr = $('<span></span>')
.css({ position: 'fixed', top: 0, left: 0, 'z-index': 99999 })
.appendTo(ctr_selector);
const $legend_ctr = $('<div></div>')
.css({
position: 'absolute',
display: 'inline-block',
top: 0,
left: 0,
'min-height': 1,
})
.appendTo($ctr);
const $label_canvas = $('<canvas></canvas>')
.css({
display: 'inline-block',
position: 'absolute',
left: '0px',
top: '0px',
})
.addClass('noselect')
.attr({ width: '150', height: '250' }) as JQuery<HTMLCanvasElement>;
const $header_div = $('<div></div>')
.css({ position: 'absolute' })
.addClass('oncoprintjs__header_div');
const $track_options_div = $('<div></div>')
.css({ position: 'absolute', left: '150px', top: '0px' })
.addClass('noselect')
.attr({ width: '50', height: '250' });
const $legend_div = $('<div></div>')
.css({ position: 'absolute', top: '250px', 'min-height': 1 })
.addClass('noselect oncoprint-legend-div');
const $cell_div = $('<div>')
.css({
width: width,
display: 'inline-block',
position: 'absolute',
left: '200px',
top: '0px',
})
.addClass('noselect');
const $cell_canvas = $('<canvas></canvas>')
.attr({ width: '0px', height: '0px' })
.css({ position: 'absolute', top: '0px', left: '0px' })
.addClass('noselect') as JQuery<HTMLCanvasElement>;
const $gap_canvas = $('<canvas></canvas>')
.attr({ width: '0px', height: '0px' })
.css({ position: 'absolute', top: '0px', left: '0px' })
.addClass('noselect gap_canvas') as JQuery<HTMLCanvasElement>;
const $dummy_scroll_div = $('<div>')
.css({
position: 'absolute',
'overflow-x': 'scroll',
'overflow-y': 'scroll',
top: '0',
left: '0px',
height: '1px',
})
.addClass('oncoprintjs__scroll_div');
const $dummy_scroll_div_contents = $('<div>').appendTo(
$dummy_scroll_div
);
const $cell_overlay_canvas = $('<canvas></canvas>')
.attr({ width: '0px', height: '0px' })
.css({ position: 'absolute', top: '0px', left: '0px' })
.addClass('noselect')
.addClass('oncoprintjs__cell_overlay_div') as JQuery<
HTMLCanvasElement
>;
const $column_label_canvas = $('<canvas></canvas>')
.attr({ width: '0px', height: '0px' })
.css({
position: 'absolute',
top: '0px',
left: '0px',
'pointer-events': 'none', // since column label canvas is on top of cell overlay canvas, we need to make it not capture any mouse events
})
.addClass('noselect')
.addClass('oncoprintjs__column_label_canvas') as JQuery<
HTMLCanvasElement
>;
const $track_info_div = $('<div>').css({ position: 'absolute' });
const $minimap_div = $('<div>')
.css({
position: 'absolute',
outline: 'solid 1px black',
display: 'none',
})
.addClass('noselect');
const $minimap_canvas = $('<canvas></canvas>')
.attr('width', 300)
.attr('height', 300)
.css({
position: 'absolute',
top: '0px',
left: '0px',
'z-index': 0,
})
.addClass('noselect') as JQuery<HTMLCanvasElement>;
const $minimap_overlay_canvas = $('<canvas></canvas>')
.attr('width', 300)
.attr('height', 300)
.css({
position: 'absolute',
top: '0px',
left: '0px',
'z-index': 1,
})
.addClass('noselect') as JQuery<HTMLCanvasElement>;
$label_canvas.appendTo($oncoprint_ctr);
$cell_div.appendTo($oncoprint_ctr);
$track_options_div.appendTo($oncoprint_ctr);
$track_info_div.appendTo($oncoprint_ctr);
$header_div.appendTo($oncoprint_ctr); // this needs to go at the end because otherwise canvases cover it up
$legend_div.appendTo($legend_ctr);
$minimap_div.appendTo($ctr);
$cell_canvas.appendTo($cell_div);
$gap_canvas.appendTo($cell_div);
$cell_overlay_canvas.appendTo($cell_div);
$column_label_canvas.appendTo($cell_div); // column labels should show above the overlay canvas because the text should show over the highlights
$dummy_scroll_div.appendTo($cell_div);
$dummy_scroll_div.on('mousemove mousedown mouseup', function(evt) {
$cell_overlay_canvas.trigger(evt);
});
$minimap_canvas.appendTo($minimap_div);
$minimap_overlay_canvas.appendTo($minimap_div);
this.$ctr = $ctr;
this.$oncoprint_ctr = $oncoprint_ctr;
this.$header_div = $header_div;
this.$cell_div = $cell_div;
this.$legend_div = $legend_div;
this.$track_options_div = $track_options_div;
this.$track_info_div = $track_info_div;
this.$dummy_scroll_div = $dummy_scroll_div;
this.$minimap_div = $minimap_div;
this.$cell_canvas = $cell_canvas;
this.$cell_overlay_canvas = $cell_overlay_canvas;
this.model = new OncoprintModel(params);
this.header_view = new OncoprintHeaderView(this.$header_div);
this.cell_view = new OncoprintWebGLCellView(
$cell_div,
$cell_canvas,
$cell_overlay_canvas,
$gap_canvas,
$column_label_canvas,
$dummy_scroll_div_contents,
this.model,
new OncoprintToolTip($tooltip_ctr),
function(left, right) {
const enclosed_ids = self.model.getIdsInZoomedLeftInterval(
left,
right
);
self.setHorzZoom(
self.model.getHorzZoomToFit(
self.cell_view.getVisibleAreaWidth(),
enclosed_ids
)
);
self.$dummy_scroll_div.scrollLeft(
self.model.getZoomedColumnLeft(enclosed_ids[0])
);
},
function(uid, track_id) {
self.doCellMouseOver(uid, track_id);
},
function(uid, track_id) {
self.doCellClick(uid, track_id);
}
);
this.minimap_view = new OncoprintMinimapView(
$minimap_div,
$minimap_canvas,
$minimap_overlay_canvas,
this.model,
this.cell_view,
150,
150,
function(x, y) {
self.setScroll(x, y);
},
function(vp: MinimapViewportSpec) {
self.setViewport(vp);
},
function(val: number) {
self.setHorzZoomCentered(val);
},
function(val: number) {
// Save unzoomed vertical center pre-zoom
const prev_viewport = self.cell_view.getViewportOncoprintSpace(
self.model
);
const center_onc_space =
(prev_viewport.top + prev_viewport.bottom) / 2;
// Execute zoom
self.setVertZoom(val);
// Set scroll to recenter the vertical center
const viewport = self.cell_view.getViewportOncoprintSpace(
self.model
);
const half_viewport_height_zoomed =
(self.model.getVertZoom() *
(viewport.bottom - viewport.top)) /
2;
self.setVertScroll(
center_onc_space * self.model.getVertZoom() -
half_viewport_height_zoomed
);
},
function() {
self.updateHorzZoomToFit();
const left = self.model.getZoomedColumnLeft();
self.setHorzScroll(
Math.min.apply(
null,
self.keep_horz_zoomed_to_fit_ids.map(function(id) {
return left[id];
})
)
);
},
function() {
self.setMinimapVisible(false);
}
);
this.track_options_view = new OncoprintTrackOptionsView(
$track_options_div,
function(track_id: TrackId) {
// move up
const tracks = self.model.getContainingTrackGroup(track_id);
const index = tracks.indexOf(track_id);
if (index > 0) {
let new_previous_track = null;
if (index >= 2) {
new_previous_track = tracks[index - 2];
}
self.moveTrack(track_id, new_previous_track);
}
},
function(track_id: TrackId) {
// move down
const tracks = self.model.getContainingTrackGroup(track_id);
const index = tracks.indexOf(track_id);
if (index < tracks.length - 1) {
self.moveTrack(track_id, tracks[index + 1]);
}
},
function(track_id: TrackId) {
const callback = self.model.getTrackRemoveOptionCallback(
track_id
);
if (callback) {
callback(track_id);
} else {
self.removeTrack(track_id);
}
},
function(track_id, dir) {
self.setTrackSortDirection(track_id, dir);
},
function(track_id: TrackId) {
self.removeExpansionTracksFor(track_id);
},
self.setTrackShowGaps.bind(self)
);
this.track_info_view = new OncoprintTrackInfoView(
$track_info_div,
new OncoprintToolTip($tooltip_ctr)
);
//this.track_info_view = new OncoprintTrackInfoView($track_info_div);
this.label_view = new OncoprintLabelView(
$label_canvas,
this.model,
new OncoprintToolTip($tooltip_ctr, { noselect: true })
);
this.label_view.setDragCallback(function(
target_track,
new_previous_track
) {
self.moveTrack(target_track, new_previous_track);
});
this.legend_view = new OncoprintLegendView($legend_div, 10, 20);
this.keep_horz_zoomed_to_fit = false;
this.keep_horz_zoomed_to_fit_ids = [];
// We need to handle scrolling this way because for some reason huge
// canvas elements have terrible resolution.
this.target_dummy_scroll_left = 0;
this.target_dummy_scroll_top = 0;
(function setUpOncoprintScroll(oncoprint) {
$dummy_scroll_div.scroll(function(e) {
const dummy_scroll_left = $dummy_scroll_div.scrollLeft();
const dummy_scroll_top = $dummy_scroll_div.scrollTop();
if (
dummy_scroll_left !== self.target_dummy_scroll_left ||
dummy_scroll_top !== self.target_dummy_scroll_top
) {
// In setDummyScrollDivScroll, where we intend to set the scroll programmatically without
// triggering the handler, we set target_dummy_scroll_left and target_dummy_scroll_top,
// so if they're not set (we get inside this block), then it's a user-triggered scroll.
//
// Set oncoprint scroll to match
self.target_dummy_scroll_left = dummy_scroll_left;
self.target_dummy_scroll_top = dummy_scroll_top;
const maximum_dummy_scroll_div_scroll = oncoprint.maxDummyScrollDivScroll();
const maximum_div_scroll_left =
maximum_dummy_scroll_div_scroll.left;
const maximum_div_scroll_top =
maximum_dummy_scroll_div_scroll.top;
let scroll_left_prop =
maximum_div_scroll_left > 0
? dummy_scroll_left / maximum_div_scroll_left
: 0;
let scroll_top_prop =
maximum_div_scroll_top > 0
? dummy_scroll_top / maximum_div_scroll_top
: 0;
scroll_left_prop = clamp(scroll_left_prop, 0, 1);
scroll_top_prop = clamp(scroll_top_prop, 0, 1);
const maximum_scroll_left = oncoprint.maxOncoprintScrollLeft();
const maximum_scroll_top = oncoprint.maxOncoprintScrollTop();
const scroll_left = Math.round(
maximum_scroll_left * scroll_left_prop
);
const scroll_top = Math.round(
maximum_scroll_top * scroll_top_prop
);
self.keep_horz_zoomed_to_fit = false;
oncoprint.doSetScroll(scroll_left, scroll_top);
}
});
})(self);
this.horz_zoom_callbacks = [];
this.minimap_close_callbacks = [];
this.cell_mouse_over_callbacks = [];
this.cell_click_callbacks = [];
this.id_clipboard = [];
this.clipboard_change_callbacks = [];
this.pending_resize_and_organize = false;
}
private _SetLegendTop() {
if (this.model.rendering_suppressed_depth > 0) {
return;
}
this.$legend_div.css({
top: this.cell_view.getVisibleAreaHeight(this.model) + 30,
});
}
private setLegendTopAfterTimeout() {
if (this.model.rendering_suppressed_depth > 0) {
return;
}
const self = this;
setTimeout(function() {
self.setHeight();
self._SetLegendTop();
}, 0);
}
private setHeight() {
this.$ctr.css({
'min-height':
this.cell_view.getVisibleAreaHeight(this.model) +
Math.max(
this.$legend_div.outerHeight(),
this.$minimap_div.is(':visible')
? this.$minimap_div.outerHeight()
: 0
) +
30,
});
}
private resizeAndOrganize(onComplete?: () => void) {
if (this.model.rendering_suppressed_depth > 0) {
return;
}
const ctr_width = $(this.ctr_selector).width();
if (ctr_width === 0) {
// dont make any adjustments while oncoprint is offscreen, DOM size calculations would be messed up
this.pending_resize_and_organize = true;
return;
}
this.$track_options_div.css({ left: this.label_view.getWidth() });
this.$header_div.css({
left: 0,
top: 0,
width: this.width,
height: this.cell_view.getVisibleAreaHeight(this.model),
});
this.$track_info_div.css({
left:
this.label_view.getWidth() + this.track_options_view.getWidth(),
});
const cell_div_left =
this.label_view.getWidth() +
this.track_options_view.getWidth() +
this.track_info_view.getWidth();
this.$cell_div.css('left', cell_div_left);
this.cell_view.setWidth(this.width - cell_div_left - 20, this.model);
this._SetLegendTop();
this.legend_view.setWidth(
this.width - this.$minimap_div.outerWidth() - 20,
this.model
);
this.setHeight();
this.$ctr.css({ 'min-width': this.width });
const self = this;
setTimeout(function() {
if (self.keep_horz_zoomed_to_fit) {
self.updateHorzZoomToFit();
}
onComplete && onComplete();
}, 0);
}
private resizeAndOrganizeAfterTimeout(onComplete?: () => void) {
if (this.model.rendering_suppressed_depth > 0) {
return;
}
const self = this;
setTimeout(function() {
self.resizeAndOrganize(onComplete);
}, 0);
}
private maxOncoprintScrollLeft() {
return Math.max(
0,
this.cell_view.getTotalWidth(this.model) -
this.cell_view.getVisibleAreaWidth()
);
}
private maxOncoprintScrollTop() {
return Math.max(
0,
this.cell_view.getTotalHeight(this.model) -
this.cell_view.getVisibleAreaHeight(this.model)
);
}
private maxDummyScrollDivScroll() {
const dummy_scroll_div_client_size = this.cell_view.getDummyScrollDivClientSize();
const maximum_div_scroll_left = Math.max(
0,
this.$dummy_scroll_div[0].scrollWidth -
dummy_scroll_div_client_size.width
);
const maximum_div_scroll_top = Math.max(
0,
this.$dummy_scroll_div[0].scrollHeight -
dummy_scroll_div_client_size.height
);
return { left: maximum_div_scroll_left, top: maximum_div_scroll_top };
}
public setMinimapVisible(visible: boolean) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
if (visible) {
this.$minimap_div.css({
display: 'block',
top: 0,
left:
$(this.ctr_selector).width() -
this.$minimap_div.outerWidth() -
10,
});
this.minimap_view.setMinimapVisible(
true,
this.model,
this.cell_view
);
} else {
this.$minimap_div.css('display', 'none');
this.minimap_view.setMinimapVisible(false);
this.executeMinimapCloseCallbacks();
}
this.resizeAndOrganizeAfterTimeout();
}
public scrollTo(left: number) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.$dummy_scroll_div.scrollLeft(left);
}
public onHorzZoom(callback: HorzZoomCallback) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.horz_zoom_callbacks.push(callback);
}
public onMinimapClose(callback: MinimapCloseCallback) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.minimap_close_callbacks.push(callback);
}
// methods that propagate/delegate to views
public moveTrack(target_track: TrackId, new_previous_track: TrackId) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.model.moveTrack(target_track, new_previous_track);
this.cell_view.moveTrack(this.model);
this.header_view.render(this.model);
this.label_view.moveTrack(this.model, this.getCellViewHeight);
this.track_options_view.moveTrack(this.model, this.getCellViewHeight);
this.track_info_view.moveTrack(this.model, this.getCellViewHeight);
this.minimap_view.moveTrack(this.model, this.cell_view);
if (
this.model.keep_sorted &&
this.model.isSortAffected(
[target_track, new_previous_track],
'track'
)
) {
this.sort();
}
this.resizeAndOrganizeAfterTimeout();
}
public setTrackGroupOrder(
index: TrackGroupIndex,
track_order: TrackId[],
dont_sort?: boolean
) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.model.setTrackGroupOrder(index, track_order);
this.cell_view.setTrackGroupOrder(this.model);
this.header_view.render(this.model);
this.label_view.setTrackGroupOrder(this.model, this.getCellViewHeight);
this.track_options_view.setTrackGroupOrder(this.model);
this.track_info_view.setTrackGroupOrder(
this.model,
this.getCellViewHeight
);
if (
!dont_sort &&
this.model.keep_sorted &&
this.model.isSortAffected(index, 'group')
) {
this.sort();
}
this.resizeAndOrganizeAfterTimeout();
}
public setTrackGroupLegendOrder(group_order: TrackGroupIndex[]) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.model.setTrackGroupLegendOrder(group_order);
this.legend_view.setTrackGroupLegendOrder(this.model);
this.resizeAndOrganizeAfterTimeout();
}
public keepSorted(keep_sorted?: boolean) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
const oldValue = this.model.keep_sorted;
this.model.keep_sorted =
typeof keep_sorted === 'undefined' ? true : keep_sorted;
if (this.model.keep_sorted && this.model.keep_sorted !== oldValue) {
this.sort();
}
}
public addTracks(params_list: UserTrackSpec<Datum>[]) {
if (this.webgl_unavailable || this.destroyed) {
return [];
}
// Update model
const track_ids: TrackId[] = [];
const library_params_list = (params_list as LibraryTrackSpec<
Datum
>[]).map(function(o) {
o.track_id = nextTrackId();
o.rule_set = OncoprintRuleSet(o.rule_set_params);
track_ids.push(o.track_id);
return o;
});
this.model.addTracks(library_params_list);
// Update views
this.cell_view.addTracks(this.model, track_ids);
this.label_view.addTracks(
this.model,
track_ids,
this.getCellViewHeight
);
this.header_view.render(this.model);
this.track_options_view.addTracks(this.model, this.getCellViewHeight);
this.track_info_view.addTracks(this.model, this.getCellViewHeight);
this.legend_view.addTracks(this.model);
this.minimap_view.addTracks(this.model, this.cell_view);
if (
this.model.keep_sorted &&
this.model.isSortAffected(track_ids, 'track')
) {
this.sort();
}
this.resizeAndOrganizeAfterTimeout();
return track_ids;
}
public removeTrack(track_id: TrackId) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
// Update model
this.model.removeTrack(track_id);
// Update views
this.cell_view.removeTrack(this.model, track_id);
this.header_view.render(this.model);
this.label_view.removeTrack(this.model, this.getCellViewHeight);
this.track_options_view.removeTrack(
this.model,
track_id,
this.getCellViewHeight
);
this.track_info_view.removeTrack(this.model, this.getCellViewHeight);
this.legend_view.removeTrack(this.model);
this.minimap_view.removeTrack(this.model, this.cell_view);
if (
this.model.keep_sorted &&
this.model.isSortAffected(track_id, 'track')
) {
this.sort();
}
this.resizeAndOrganizeAfterTimeout();
}
public removeTracks(track_ids: TrackId[]) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
for (let i = 0; i < track_ids.length; i++) {
this.removeTrack(track_ids[i]);
}
}
public getTracks() {
if (this.webgl_unavailable || this.destroyed) {
return [];
}
return this.model.getTracks().slice();
}
public removeAllTracks() {
if (this.webgl_unavailable || this.destroyed) {
return;
}
const track_ids = this.model.getTracks();
this.removeTracks(track_ids);
}
public removeExpansionTracksFor(track_id: TrackId) {
// remove all expansion tracks for this track
this.removeTracks(this.model.track_expansion_tracks[track_id].slice());
}
public disableTrackExpansion(track_id: TrackId) {
this.model.disableTrackExpansion(track_id);
}
public enableTrackExpansion(track_id: TrackId) {
this.model.enableTrackExpansion(track_id);
}
public removeAllExpansionTracksInGroup(index: TrackGroupIndex) {
const tracks_in_group = this.model.getTrackGroups()[index].tracks,
expanded_tracks = [];
let i;
for (i = 0; i < tracks_in_group.length; i++) {
if (this.model.isTrackExpanded(tracks_in_group[i])) {
expanded_tracks.push(tracks_in_group[i]);
}
}
this.suppressRendering();
for (i = 0; i < expanded_tracks.length; i++) {
// assume that the expanded tracks are not themselves removed here
this.removeExpansionTracksFor(expanded_tracks[i]);
}
this.releaseRendering();
}
public setHorzZoomToFit(ids: ColumnId[]) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.keep_horz_zoomed_to_fit = true;
this.updateHorzZoomToFitIds(ids);
this.updateHorzZoomToFit();
}
public updateHorzZoomToFitIds(ids: ColumnId[]) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.keep_horz_zoomed_to_fit_ids = ids.slice();
if (this.keep_horz_zoomed_to_fit) {
this.updateHorzZoomToFit();
}
}
private updateHorzZoomToFit() {
this.setHorzZoom(
this.getHorzZoomToFit(this.keep_horz_zoomed_to_fit_ids),
true
);
}
private getHorzZoomToFit(ids: ColumnId[]) {
ids = ids || [];
return this.model.getHorzZoomToFit(
this.cell_view.getVisibleAreaWidth(),
ids
);
}
private executeHorzZoomCallbacks() {
for (let i = 0; i < this.horz_zoom_callbacks.length; i++) {
this.horz_zoom_callbacks[i](this.model.getHorzZoom());
}
}
private executeMinimapCloseCallbacks() {
for (let i = 0; i < this.minimap_close_callbacks.length; i++) {
this.minimap_close_callbacks[i]();
}
}
private doCellMouseOver(uid: ColumnId, track_id: TrackId) {
if (uid !== null) {
this.highlightTrackLabelOnly(track_id);
} else {
this.highlightTrackLabelOnly(null);
}
for (let i = 0; i < this.cell_mouse_over_callbacks.length; i++) {
this.cell_mouse_over_callbacks[i](uid, track_id);
}
}
private doCellClick(uid: ColumnId, track_id: TrackId) {
for (let i = 0; i < this.cell_click_callbacks.length; i++) {
this.cell_click_callbacks[i](uid, track_id);
}
}
public getHorzZoom() {
if (this.webgl_unavailable || this.destroyed) {
return undefined;
}
return this.model.getHorzZoom();
}
public setHorzZoomCentered(z: number) {
// Save id thats at center pre-zoom
const centerColIndex = this.cell_view.getViewportOncoprintSpace(
this.model
).center_col_index;
const centerId = this.model.getIdOrder()[centerColIndex];
// Execute zoom
this.setHorzZoom(z);
// Set scroll to recenter the saved id
this.setHorzScroll(
this.model.getZoomedColumnLeft(centerId) -
this.cell_view.visible_area_width / 2
);
}
public setHorzZoom(z: number, still_keep_horz_zoomed_to_fit?: boolean) {
if (this.webgl_unavailable || this.destroyed) {
return undefined;
}
this.keep_horz_zoomed_to_fit =
this.keep_horz_zoomed_to_fit && still_keep_horz_zoomed_to_fit;
if (this.model.getHorzZoom() !== z) {
// Update model if new zoom is different
this.model.setHorzZoom(z);
// Update views
this.cell_view.setHorzZoom(this.model);
this.minimap_view.setHorzZoom(this.model, this.cell_view);
this.executeHorzZoomCallbacks();
this.resizeAndOrganizeAfterTimeout();
}
return this.model.getHorzZoom();
}
public getVertZoom() {
if (this.webgl_unavailable || this.destroyed) {
return undefined;
}
return this.model.getVertZoom();
}
public setVertZoom(z: number) {
if (this.webgl_unavailable || this.destroyed) {
return undefined;
}
// Update model
this.model.setVertZoom(z);
// Update views
this.cell_view.setVertZoom(this.model);
this.header_view.render(this.model);
this.label_view.setVertZoom(this.model, this.getCellViewHeight);
this.track_info_view.setVertZoom(this.model, this.getCellViewHeight);
this.track_options_view.setVertZoom(this.model, this.getCellViewHeight);
this.minimap_view.setVertZoom(this.model, this.cell_view);
this.resizeAndOrganizeAfterTimeout();
return this.model.getVertZoom();
}
private doSetScroll(scroll_left: number, scroll_top: number) {
// Update model
scroll_left = Math.min(scroll_left, this.maxOncoprintScrollLeft());
scroll_top = Math.min(scroll_top, this.maxOncoprintScrollTop());
this.model.setScroll(scroll_left, scroll_top);
// Update views
this.cell_view.setScroll(this.model);
this.header_view.setScroll(this.model);
this.label_view.setScroll(this.model, this.getCellViewHeight);
this.track_info_view.setScroll(this.model);
this.track_options_view.setScroll(this.model);
this.minimap_view.setScroll(this.model, this.cell_view);
}
private setDummyScrollDivScroll() {
const scroll_left = this.model.getHorzScroll();
const scroll_top = this.model.getVertScroll();
const maximum_scroll_left = this.maxOncoprintScrollLeft();
const maximum_scroll_top = this.maxOncoprintScrollTop();
let onc_scroll_left_prop =
maximum_scroll_left > 0 ? scroll_left / maximum_scroll_left : 0;
let onc_scroll_top_prop =
maximum_scroll_top > 0 ? scroll_top / maximum_scroll_top : 0;
onc_scroll_left_prop = clamp(onc_scroll_left_prop, 0, 1);
onc_scroll_top_prop = clamp(onc_scroll_top_prop, 0, 1);
const maximum_dummy_scroll_div_scroll = this.maxDummyScrollDivScroll();
const maximum_div_scroll_left = maximum_dummy_scroll_div_scroll.left;
const maximum_div_scroll_top = maximum_dummy_scroll_div_scroll.top;
this.target_dummy_scroll_left = Math.round(
onc_scroll_left_prop * maximum_div_scroll_left
);
this.target_dummy_scroll_top = Math.round(
onc_scroll_top_prop * maximum_div_scroll_top
);
this.$dummy_scroll_div.scrollLeft(this.target_dummy_scroll_left);
this.$dummy_scroll_div.scrollTop(this.target_dummy_scroll_top);
}
public setScroll(scroll_left: number, scroll_top: number) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.doSetScroll(scroll_left, scroll_top);
this.setDummyScrollDivScroll();
}
public setZoom(zoom_x: number, zoom_y: number) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
// Update model
this.model.setZoom(zoom_x, zoom_y);
// Update views
this.cell_view.setZoom(this.model);
this.header_view.render(this.model);
this.label_view.setZoom(this.model, this.getCellViewHeight);
this.track_info_view.setZoom(this.model, this.getCellViewHeight);
this.track_options_view.setZoom(this.model, this.getCellViewHeight);
this.minimap_view.setZoom(this.model, this.cell_view);
}
public setHorzScroll(s: number) {
if (this.webgl_unavailable || this.destroyed) {
return undefined;
}
// Update model
this.model.setHorzScroll(Math.min(s, this.maxOncoprintScrollLeft()));
// Update views
this.cell_view.setHorzScroll(this.model);
this.label_view.setHorzScroll(this.model);
this.track_info_view.setHorzScroll(this.model);
this.track_options_view.setHorzScroll(this.model);
this.minimap_view.setHorzScroll(this.model, this.cell_view);
// Update dummy scroll div
this.setDummyScrollDivScroll();
return this.model.getHorzScroll();
}
public setVertScroll(s: number) {
if (this.webgl_unavailable || this.destroyed) {
return undefined;
}
// Update model
this.model.setVertScroll(Math.min(s, this.maxOncoprintScrollTop()));
// Update views
this.cell_view.setVertScroll(this.model);
this.header_view.setVertScroll(this.model);
this.label_view.setVertScroll(this.model, this.getCellViewHeight);
this.track_info_view.setVertScroll(this.model);
this.track_options_view.setVertScroll(this.model);
this.minimap_view.setVertScroll(this.model, this.cell_view);
// Update dummy scroll div
this.setDummyScrollDivScroll();
return this.model.getVertScroll();
}
public setViewport(vp: MinimapViewportSpec) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
// Zoom
const zoom_x = this.model.getHorzZoomToFitCols(
this.cell_view.getVisibleAreaWidth(),
vp.left_col,
vp.right_col
);
this.setZoom(zoom_x, vp.zoom_y);
// Scroll
const scroll_left = Math.min(
this.model.getZoomedColumnLeft(
this.model.getIdOrder()[vp.left_col]
),
this.maxOncoprintScrollLeft()
);
const scroll_top = Math.min(
vp.scroll_y_proportion * this.model.getOncoprintHeight(),
this.maxOncoprintScrollTop()
);
this.setScroll(scroll_left, scroll_top);
this.executeHorzZoomCallbacks();
}
public getTrackData(track_id: TrackId) {
if (this.webgl_unavailable || this.destroyed) {
return undefined;
}
return this.model.getTrackData(track_id);
}
public getTrackDataIdKey(track_id: TrackId) {
if (this.webgl_unavailable || this.destroyed) {
return undefined;
}
return this.model.getTrackDataIdKey(track_id);
}
/**
* Sets the data for an Oncoprint track.
*
* @param track_id - the ID that identifies the track
* @param {Object[]} data - the list of data for the cells
* @param {string} data_id_key - name of the property of the
* data objects to use as the (column) key
*/
public setTrackData(
track_id: TrackId,
data: Datum[],
data_id_key: string & keyof Datum
) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.model.setTrackData(track_id, data, data_id_key);
this.cell_view.setTrackData(this.model, track_id);
this.header_view.render(this.model);
this.legend_view.setTrackData(this.model);
this.minimap_view.setTrackData(this.model, this.cell_view);
if (
this.model.keep_sorted &&
this.model.isSortAffected(track_id, 'track')
) {
this.sort();
}
this.resizeAndOrganizeAfterTimeout();
}
public setTrackImportantIds(
track_id: TrackId,
ids: ColumnId[] | undefined
) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.model.setTrackImportantIds(track_id, ids);
this.cell_view.setTrackImportantIds(this.model, track_id);
this.legend_view.setTrackImportantIds(this.model);
this.resizeAndOrganizeAfterTimeout();
}
public setTrackGroupSortPriority(priority: TrackGroupIndex[]) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.model.setTrackGroupSortPriority(priority);
this.cell_view.setTrackGroupSortPriority(this.model);
if (this.model.keep_sorted) {
this.sort();
}
this.resizeAndOrganizeAfterTimeout();
}
public resetSortableTracksSortDirection() {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.model.resetSortableTracksSortDirection(true);
if (this.model.keep_sorted) {
this.sort();
}
}
public setTrackSortDirection(track_id: TrackId, dir: TrackSortDirection) {
if (this.webgl_unavailable || this.destroyed) {
return undefined;
}
if (this.model.isTrackSortDirectionChangeable(track_id)) {
this.model.setTrackSortDirection(track_id, dir);
if (
this.model.keep_sorted &&
this.model.isSortAffected(track_id, 'track')
) {
this.sort();
}
if (this.model.getTrackSortDirection(track_id) === 0) {
if (this.model.getTrackShowGaps(track_id)) {
this.setTrackShowGaps(track_id, GAP_MODE_ENUM.HIDE_GAPS);
}
}
}
return this.model.getTrackSortDirection(track_id);
}
public setTrackSortComparator(
track_id: TrackId,
sortCmpFn: TrackSortSpecification<Datum>
) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.model.setTrackSortComparator(track_id, sortCmpFn);
if (
this.model.keep_sorted &&
this.model.isSortAffected(track_id, 'track')
) {
this.sort();
}
}
public getTrackSortDirection(track_id: TrackId) {
if (this.webgl_unavailable || this.destroyed) {
return undefined;
}
return this.model.getTrackSortDirection(track_id);
}
public setTrackInfo(track_id: TrackId, msg: string) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.model.setTrackInfo(track_id, msg);
this.track_info_view.setTrackInfo(this.model, this.getCellViewHeight);
}
public setTrackTooltipFn(
track_id: TrackId,
tooltipFn: TrackTooltipFn<Datum>
) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.model.setTrackTooltipFn(track_id, tooltipFn);
}
public setShowTrackSublabels(show: boolean) {
this.model.setShowTrackSublabels(show);
this.label_view.setShowTrackSublabels(
this.model,
this.getCellViewHeight
);
this.resizeAndOrganizeAfterTimeout();
}
public setTrackShowGaps(track_id: TrackId, gap_mode: GAP_MODE_ENUM) {
this.model.setTrackShowGaps(track_id, gap_mode);
if (
this.model.getTrackSortDirection(track_id) === 0 &&
gap_mode !== GAP_MODE_ENUM.HIDE_GAPS
) {
this.setTrackSortDirection(track_id, 1);
}
this.track_options_view.setTrackShowGaps(
this.model,
this.getCellViewHeight
);
this.cell_view.setTrackShowGaps(this.model);
}
public sort() {
if (this.webgl_unavailable || this.destroyed) {
return;
}
const self = this;
// Increment lastSortId in order to indicate that a new sort is initiated.
this.incrementLastSortId();
const thisSortId = this.lastSortId;
this.model.sort().then(function(clusterSortResult) {
// Make sure lastSortId is still the same as it was when we first called sort()
// If not, then just skip updating.
//
// This prevents bugs due to stale sorts finishing asynchronously in the wrong order.
if (self.lastSortId !== thisSortId) {
return;
}
self.label_view.sort(self.model, self.getCellViewHeight);
self.track_options_view.sort(self.model, self.getCellViewHeight);
self.cell_view.sort(self.model);
self.minimap_view.sort(self.model, self.cell_view);
if (clusterSortResult) {
self.setTrackGroupOrder(
clusterSortResult.track_group_index,
clusterSortResult.track_id_order,
true
);
}
});
}
public shareRuleSet(source_track_id: TrackId, target_track_id: TrackId) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.model.shareRuleSet(source_track_id, target_track_id);
this.cell_view.shareRuleSet(this.model, target_track_id);
this.legend_view.shareRuleSet(this.model);
this.minimap_view.shareRuleSet(this.model, this.cell_view);
}
public setRuleSet(track_id: TrackId, rule_set_params: RuleSetParams) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.model.setRuleSet(track_id, OncoprintRuleSet(rule_set_params));
this.cell_view.setRuleSet(this.model, track_id);
this.legend_view.setRuleSet(this.model);
this.minimap_view.setRuleSet(this.model, this.cell_view);
this.resizeAndOrganizeAfterTimeout();
}
public setSortConfig(params: SortConfig) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.model.setSortConfig(params);
this.cell_view.setSortConfig(this.model);
this.track_options_view.setSortConfig(this.model);
if (this.model.keep_sorted) {
this.sort();
}
}
public setIdOrder(ids: ColumnId[]) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
// Update model
this.model.setIdOrder(ids);
// Update views
this.cell_view.setIdOrder(this.model, ids);
this.minimap_view.setIdOrder(this.model, this.cell_view);
if (this.model.keep_sorted) {
this.sort();
}
}
public setTrackGroupHeader(
index: TrackGroupIndex,
header?: TrackGroupHeader
) {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.model.setTrackGroupHeader(index, header);
this.label_view.setTrackGroupHeader(this.model, this.getCellViewHeight);
this.header_view.render(this.model);
this.track_info_view.setTrackGroupHeader(
this.model,
this.getCellViewHeight
);
this.track_options_view.setTrackGroupHeader(
this.model,
this.getCellViewHeight
);
this.minimap_view.setTrackGroupHeader(this.model, this.cell_view);
this.cell_view.setTrackGroupHeader(this.model);
this.resizeAndOrganizeAfterTimeout();
}
public disableInteraction() {
if (this.webgl_unavailable || this.destroyed) {
return;
}
//this.label_view.disableInteraction();
//this.cell_view.disableInteraction();
this.track_options_view.disableInteraction();
//this.track_info_view.disableInteraction();
//this.legend_view.disableInteraction();
}
public enableInteraction() {
if (this.webgl_unavailable || this.destroyed) {
return;
}
//this.label_view.enableInteraction();
//this.cell_view.enableInteraction();
this.track_options_view.enableInteraction();
//this.track_info_view.enableInteraction();
//this.legend_view.enableInteraction();
}
public suppressRendering() {
if (this.webgl_unavailable || this.destroyed) {
return;
}
this.model.rendering_suppressed_depth += 1;
this.label_view.suppressRendering();
this.header_view.suppressRendering();
this.cell_view.suppressRendering();
this.track_options_view.suppressRendering();
this.tr