UNPKG

oncoprintjs

Version:

A data visualization for cancer genomic data.

1,359 lines (1,236 loc) 88.8 kB
/* jshint browserify: true, asi: true */ import binarysearch from './binarysearch'; import hasElementsInInterval from './haselementsininterval'; import CachedProperty from './CachedProperty'; import { hclusterColumns, hclusterTracks } from './clustering'; import $ from 'jquery'; import * as BucketSort from './bucketsort'; import { cloneShallow, doesCellIntersectPixel, ifndef, z_comparator, } from './utils'; import _ from 'lodash'; import { RuleSet, RuleSetParams, RuleWithId } from './oncoprintruleset'; import { InitParams } from './oncoprint'; import { ComputedShapeParams } from './oncoprintshape'; import { CaseItem, EntityItem } from './workers/clustering-worker'; import PrecomputedComparator from './precomputedcomparator'; import { calculateHeaderTops, calculateTrackTops } from './modelutils'; import { OncoprintGapConfig } from './oncoprintwebglcellview'; export enum GAP_MODE_ENUM { SHOW_GAPS = 'SHOW_GAPS', SHOW_GAPS_PERCENT = 'SHOW_GAPS_PERCENT', HIDE_GAPS = 'HIDE_GAPS', } export type ColumnId = string; export type ColumnIndex = number; export type TrackId = number; export type Datum = any; export type RuleSetId = number; export type TrackGroupHeader = { label: { text: string; // more styling options can go here }; options: CustomTrackGroupOption[]; // for options menu dropdown }; export type TrackGroup = { header?: TrackGroupHeader; tracks: TrackId[]; }; export type TrackGroupIndex = number; export type TrackSortDirection = 0 | 1 | -1; export type TrackSortComparator<D> = (d1: D, d2: D) => number; //returns (0|1|2|-1|-2); for comparison-based sort, where 2 and -2 mean force to end or beginning (resp) no matter what direction sorted in export type TrackSortVector<D> = (d: D) => (number | string)[]; // maps data to vector used for bucket sort - types of elements in each position must be same, i.e. Kth element must always be a number, or always be a string export type TrackTooltipFn<D> = (cell_data: D[]) => HTMLElement | string | any; export type TrackSortSpecificationComparators<D> = { mandatory: TrackSortComparator<D>; // specifies the mandatory order for the track preferred: TrackSortComparator<D>; // specifies the preferred order for the track (can be overridden by mandatory order of higher track) isVector?: false; }; export type TrackSortSpecificationVectors<D> = { mandatory: TrackSortVector<D>; // specifies the mandatory order for the track preferred: TrackSortVector<D>; // specifies the preferred order for the track (can be overridden by mandatory order of higher track) isVector: true; compareEquals?: TrackSortComparator<D>; // specifies a comparator to be applied to sort among equal sort vectors in the *preferred* order (optional). eg sort by sample id if all else equal }; export type TrackSortSpecification<D> = | TrackSortSpecificationComparators<D> | TrackSortSpecificationVectors<D>; export type ActiveRules = { [ruleId: number]: boolean }; export type ActiveRulesCount = { [ruleId: number]: number }; export type TrackSortDirectionChangeCallback = ( track_id: TrackId, dir: number ) => void; export type TrackGapChangeCallBack = ( track_id: TrackId, mode: GAP_MODE_ENUM ) => void; export type CustomTrackOption = { label?: string; separator?: boolean; onClick?: (id: TrackId) => void; weight?: string; disabled?: boolean; gapLabelsFn?: (model: OncoprintModel) => OncoprintGapConfig[]; }; export type CustomTrackGroupOption = { label?: string; separator?: boolean; onClick?: (id: TrackGroupIndex) => void; weight?: () => string; disabled?: () => boolean; }; export type UserTrackSpec<D> = { target_group?: TrackGroupIndex; cell_height?: number; track_padding?: number; has_column_spacing?: boolean; data_id_key?: string & keyof D; tooltipFn?: TrackTooltipFn<D>; movable?: boolean; removable?: boolean; removeCallback?: (track_id: TrackId) => void; onClickRemoveInTrackMenu?: (track_id: TrackId) => void; label?: string; sublabel?: string; gapLabelFn?: (model: OncoprintModel) => string[]; html_label?: string; track_label_color?: string; track_label_circle_color?: string; track_label_font_weight?: string; track_label_left_padding?: number; link_url?: string; description?: string; track_info?: string; sortCmpFn: TrackSortSpecification<D>; sort_direction_changeable?: boolean; onSortDirectionChange?: TrackSortDirectionChangeCallback; onGapChange?: TrackGapChangeCallBack; init_sort_direction?: TrackSortDirection; data?: D[]; rule_set_params?: RuleSetParams; expansion_of?: TrackId; expandCallback?: (id: TrackId) => void; expandButtonTextGetter?: (is_expanded: boolean) => string; important_ids?: string[]; custom_track_options?: CustomTrackOption[]; $track_info_tooltip_elt?: JQuery; track_can_show_gaps?: boolean; show_gaps_on_init?: boolean; }; export type LibraryTrackSpec<D> = UserTrackSpec<D> & { rule_set: RuleSet; track_id: TrackId; }; export type TrackOverlappingCells = { ids: ColumnId[]; track: TrackId; top: number; left: number; }; export type SortConfig = | { type: 'alphabetical'; } | { type: 'order'; order: string[]; } | { type: 'cluster'; track_group_index: number; clusterValueFn: (datum: any) => number; } | { type?: '' }; export type IdentifiedShapeList = { id: ColumnId; shape_list: ComputedShapeParams[]; }; export type ClusterSortResult = { track_group_index: TrackGroupIndex; track_id_order: TrackId[]; }; export type ColumnLabel = { left_padding_percent?: number; text_color?: string; circle_color?: string; angle_in_degrees?: number; text: string; }; class UnionOfSets { // a set, to be passed in as argument, is an object where the values are truthy private union_count: { [key: string]: number } = {}; private sets: { [setId: string]: { [key: string]: boolean } } = {}; private setOfKeys(obj: { [key: string]: any }) { const set: { [key: string]: boolean } = {}; for (const k of Object.keys(obj)) { if (typeof obj[k] !== 'undefined') { set[k] = true; } } return set; } public putSet(id: string, set: { [key: string]: boolean }) { this.removeSet(id); this.sets[id] = set; for (const k of Object.keys(set)) { if (set[k]) { this.union_count[k] = this.union_count[k] || 0; this.union_count[k] += 1; } } } public removeSet(id: string) { const union_count = this.union_count; const old_set = this.sets[id] || {}; for (const k of Object.keys(old_set)) { if (old_set[k]) { union_count[k] -= 1; if (union_count[k] === 0) { delete union_count[k]; } } } delete this.sets[id]; } public getUnion() { return this.setOfKeys(this.union_count); } } function arrayUnique(arr: string[]) { const present: { [elt: string]: boolean } = {}; const unique = []; for (let i = 0; i < arr.length; i++) { if (typeof present[arr[i]] === 'undefined') { present[arr[i]] = true; unique.push(arr[i]); } } return unique; } function copyShallowObject<T>(obj: { [key: string]: T }) { const copy: { [key: string]: T } = {}; for (const key of Object.keys(obj)) { copy[key] = obj[key]; } return copy; } function clamp(x: number, lower: number, upper: number) { return Math.min(upper, Math.max(lower, x)); } const MIN_ZOOM_PIXELS = 100; const MIN_CELL_HEIGHT_PIXELS = 3; export type TrackProp<T> = { [trackId: number]: T }; export type TrackGroupProp<T> = { [trackGroupIndex: number]: T }; export type ColumnProp<T> = { [columnId: string]: T }; export type ColumnIdSet = { [columnId: string]: any }; export type OncoprintDataGroupsByTrackId<T> = Record< string, OncoprintDataGroups<T>[] >; export type OncoprintDataGroups<T> = OncoprintDataGroup<T>[]; export type OncoprintDataGroup<T> = T[]; export default class OncoprintModel { // Global properties private sort_config: SortConfig; public rendering_suppressed_depth: number; public keep_sorted = false; // Rendering properties public readonly max_height: number; private cell_width: number; private horz_zoom: number; private vert_zoom: number; private horz_scroll: number; private vert_scroll: number; private bottom_padding: number; private track_group_padding: number; private cell_padding: number; private cell_padding_on: boolean; private cell_padding_off_cell_width_threshold: number; private cell_padding_off_because_of_zoom: boolean; private id_order: ColumnId[]; private hidden_ids: ColumnProp<boolean>; private highlighted_ids: ColumnId[]; private highlighted_tracks: TrackId[]; private track_group_legend_order: TrackGroupIndex[]; private show_track_sublabels: boolean; private show_track_labels: boolean; private column_labels: ColumnProp<ColumnLabel>; // Track properties private track_important_ids: TrackProp<ColumnProp<boolean>>; // set of "important" ids - only these ids will cause a used rule to become active and thus shown in the legend private track_label: TrackProp<string>; private track_label_color: TrackProp<string>; private track_label_circle_color: TrackProp<string>; private track_label_font_weight: TrackProp<string>; private track_label_left_padding: TrackProp<number>; private track_sublabel: TrackProp<string>; private track_html_label: TrackProp<string>; private track_link_url: TrackProp<string>; private track_description: TrackProp<string>; private cell_height: TrackProp<number>; private track_padding: TrackProp<number>; private track_data_id_key: TrackProp<string>; private track_tooltip_fn: TrackProp<TrackTooltipFn<any>>; private track_movable: TrackProp<boolean>; private track_removable: TrackProp<boolean>; private track_remove_callback: TrackProp<(track_id: TrackId) => void>; private track_remove_option_callback: TrackProp< (track_id: TrackId) => void >; private track_sort_cmp_fn: TrackProp<TrackSortSpecification<Datum>>; private track_sort_direction_changeable: TrackProp<boolean>; private track_sort_direction: TrackProp<TrackSortDirection>; private track_sort_direction_change_callback: TrackProp< TrackSortDirectionChangeCallback >; private track_gap_change_callback: TrackProp<TrackGapChangeCallBack>; private track_data: TrackProp<Datum[]>; private track_rule_set_id: TrackProp<RuleSetId>; private track_active_rules: TrackProp<ActiveRules>; private track_info: TrackProp<string>; private $track_info_tooltip_elt: TrackProp<JQuery>; private track_has_column_spacing: TrackProp<boolean>; private track_expansion_enabled: TrackProp<boolean>; private track_expand_callback: TrackProp<(trackId: TrackId) => void>; private track_expand_button_getter: TrackProp< (is_expanded: boolean) => string >; public track_expansion_tracks: TrackProp<TrackId[]>; private track_expansion_parent: TrackProp<TrackId>; private track_custom_options: TrackProp<CustomTrackOption[]>; private track_can_show_gaps: TrackProp<boolean>; private track_show_gaps: TrackProp<GAP_MODE_ENUM>; // Rule set properties private rule_sets: { [ruleSetId: number]: RuleSet }; private rule_set_active_rules: { [ruleSetId: number]: ActiveRulesCount }; // Cached and recomputed properties private visible_id_order: CachedProperty<ColumnId[]>; private track_id_to_datum: CachedProperty<TrackProp<ColumnProp<Datum>>>; private track_present_ids: CachedProperty<UnionOfSets>; private present_ids: CachedProperty<ColumnProp<boolean>>; private id_to_index: CachedProperty<ColumnProp<number>>; private visible_id_to_index: CachedProperty<ColumnProp<number>>; private track_tops: CachedProperty<TrackProp<number>>; private cell_tops: CachedProperty<TrackProp<number>>; private label_tops: CachedProperty<TrackProp<number>>; private track_tops_zoomed: CachedProperty<TrackProp<number>>; private header_tops_zoomed: CachedProperty<TrackProp<number>>; private cell_tops_zoomed: CachedProperty<TrackProp<number>>; private label_tops_zoomed: CachedProperty<TrackProp<number>>; private column_left: CachedProperty<ColumnProp<number>>; private column_left_always_with_padding: CachedProperty<ColumnProp<number>>; private zoomed_column_left: CachedProperty<ColumnProp<number>>; private column_left_no_padding: CachedProperty<ColumnProp<number>>; private precomputed_comparator: CachedProperty< TrackProp<PrecomputedComparator<Datum>> >; public ids_after_a_gap: CachedProperty<ColumnIdSet>; public data_groups: CachedProperty< OncoprintDataGroupsByTrackId<TrackProp<ColumnProp<Datum>>> >; private column_indexes_after_a_gap: CachedProperty<number[]>; private track_groups: TrackGroup[]; private unclustered_track_group_order?: TrackId[]; private track_group_sort_priority: TrackGroupIndex[]; constructor(params: InitParams) { const model = this; this.sort_config = {}; this.rendering_suppressed_depth = 0; this.max_height = ifndef(params.max_height, 500); this.cell_width = ifndef(params.init_cell_width, 6); this.horz_zoom = ifndef(params.init_horz_zoom, 1); this.vert_zoom = ifndef(params.init_vert_zoom, 1); this.horz_scroll = 0; this.vert_scroll = 0; this.bottom_padding = 0; this.track_group_padding = ifndef(params.init_track_group_padding, 10); this.cell_padding = ifndef(params.init_cell_padding, 3); this.cell_padding_on = ifndef(params.init_cell_padding_on, true); this.cell_padding_off_cell_width_threshold = ifndef( params.cell_padding_off_cell_width_threshold, 2 ); this.cell_padding_off_because_of_zoom = this.getCellWidth() < this.cell_padding_off_cell_width_threshold; this.id_order = []; this.hidden_ids = {}; this.highlighted_ids = []; this.highlighted_tracks = []; this.track_group_legend_order = []; this.show_track_sublabels = false; this.show_track_labels = true; this.column_labels = {}; // Track Properties this.track_important_ids = {}; // a set of "important" ids - only these ids will cause a used rule to become active and thus shown in the legend this.track_label = {}; this.track_label_color = {}; this.track_label_circle_color = {}; this.track_label_font_weight = {}; this.track_label_left_padding = {}; // TODO: consolidate track styling properties into one object (help me typescript) this.track_sublabel = {}; this.track_html_label = {}; this.track_link_url = {}; this.track_description = {}; this.cell_height = {}; this.track_padding = {}; this.track_data_id_key = {}; this.track_tooltip_fn = {}; this.track_movable = {}; this.track_removable = {}; this.track_remove_callback = {}; this.track_remove_option_callback = {}; this.track_sort_cmp_fn = {}; this.track_sort_direction_changeable = {}; this.track_sort_direction = {}; // 1: ascending, -1: descending, 0: not this.track_sort_direction_change_callback = {}; this.track_gap_change_callback = {}; this.track_data = {}; this.track_rule_set_id = {}; // track id -> rule set id this.track_active_rules = {}; // from track id to active rule map (map with rule ids as keys) this.track_info = {}; this.$track_info_tooltip_elt = {}; this.track_has_column_spacing = {}; // track id -> boolean this.track_expansion_enabled = {}; // track id -> boolean or undefined this.track_expand_callback = {}; // track id -> function that adds expansion tracks for its track if set this.track_expand_button_getter = {}; // track id -> function from boolean to string if customized this.track_expansion_tracks = {}; // track id -> array of track ids if applicable this.track_expansion_parent = {}; // track id -> track id if applicable this.track_custom_options = {}; // track id -> { label, onClick, weight, disabled }[] ( see index.d.ts :: CustomTrackOption ) this.track_can_show_gaps = {}; this.track_show_gaps = {}; // Rule Set Properties this.rule_sets = {}; // map from rule set id to rule set this.rule_set_active_rules = {}; // map from rule set id to map from rule id to use count // Cached and Recomputed Properties this.visible_id_order = new CachedProperty([], function( model: OncoprintModel ) { const hidden_ids = model.hidden_ids; return model.id_order.filter(function(id) { return !hidden_ids[id]; }); }); this.track_id_to_datum = new CachedProperty({}, function( model, track_id ) { const curr = model.track_id_to_datum.get(); if (model.getContainingTrackGroup(track_id) !== null) { const map: ColumnProp<Datum> = {}; const data = model.getTrackData(track_id) || []; const data_id_key = model.getTrackDataIdKey(track_id) || ''; for (let i = 0; i < data.length; i++) { map[data[i][data_id_key] as string] = data[i]; } curr[track_id] = map; } else { delete curr[track_id]; } return curr; }); this.track_present_ids = new CachedProperty(new UnionOfSets(), function( model, track_id ) { const union = model.track_present_ids.get(); if (model.getContainingTrackGroup(track_id) !== null) { const ids: ColumnProp<boolean> = {}; const data = model.getTrackData(track_id) || []; const data_id_key = model.getTrackDataIdKey(track_id) || ''; for (let i = 0; i < data.length; i++) { ids[data[i][data_id_key] as string] = true; } union.putSet(track_id, ids); } else { union.removeSet(track_id); } return union; }); this.present_ids = new CachedProperty({}, function() { return model.track_present_ids.get().getUnion(); }); this.track_present_ids.addBoundProperty(this.present_ids); this.id_to_index = new CachedProperty({}, function() { const id_to_index: ColumnProp<number> = {}; const id_order = model.getIdOrder(true); for (let i = 0; i < id_order.length; i++) { id_to_index[id_order[i]] = i; } return id_to_index; }); this.visible_id_to_index = new CachedProperty({}, function() { const id_to_index: ColumnProp<number> = {}; const id_order = model.getIdOrder(); for (let i = 0; i < id_order.length; i++) { id_to_index[id_order[i]] = i; } return id_to_index; }); this.visible_id_order.addBoundProperty(this.visible_id_to_index); this.track_groups = []; this.track_group_sort_priority = []; this.track_tops = new CachedProperty({}, function() { return calculateTrackTops(model, false); }); this.cell_tops = new CachedProperty({}, function() { const track_ids = model.getTracks(); const track_tops = model.track_tops.get(); const cell_tops: TrackProp<number> = {}; for (const id of track_ids) { if (id in track_tops) { cell_tops[id] = track_tops[id] + model.getTrackPadding(id, true); } } return cell_tops; }); this.label_tops = new CachedProperty({}, function() { return model.cell_tops.get(); }); this.track_tops.addBoundProperty(this.cell_tops); this.cell_tops.addBoundProperty(this.label_tops); this.track_tops_zoomed = new CachedProperty({}, function() { return calculateTrackTops(model, true); }); this.header_tops_zoomed = new CachedProperty({}, function() { return calculateHeaderTops(model, true); }); this.cell_tops_zoomed = new CachedProperty({}, function() { const track_ids = model.getTracks(); const track_tops = model.track_tops_zoomed.get(); const cell_tops: TrackProp<number> = {}; for (const id of track_ids) { if (id in track_tops) { cell_tops[id] = track_tops[id] + model.getTrackPadding(id); } } return cell_tops; }); this.label_tops_zoomed = new CachedProperty({}, function() { return model.cell_tops_zoomed.get(); }); this.track_tops.addBoundProperty(this.track_tops_zoomed); this.track_tops_zoomed.addBoundProperty(this.cell_tops_zoomed); this.track_tops_zoomed.addBoundProperty(this.header_tops_zoomed); this.cell_tops_zoomed.addBoundProperty(this.label_tops_zoomed); this.precomputed_comparator = new CachedProperty({}, function( model: OncoprintModel, track_id: TrackId ) { const curr_precomputed_comparator = model.precomputed_comparator.get(); curr_precomputed_comparator[track_id] = new PrecomputedComparator( model.getTrackData(track_id), model.getTrackSortComparator(track_id), model.getTrackSortDirection(track_id), model.getTrackDataIdKey(track_id) ); return curr_precomputed_comparator; }); // track_id -> PrecomputedComparator this.ids_after_a_gap = new CachedProperty({}, function( model: OncoprintModel ) { const gapIds: { [columnId: string]: boolean } = {}; const precomputedComparator = model.precomputed_comparator.get(); const trackIdsWithGaps = model .getTracks() .filter( trackId => model.getTrackShowGaps(trackId) !== GAP_MODE_ENUM.HIDE_GAPS ); const ids = model.visible_id_order.get(); for (let i = 1; i < ids.length; i++) { for (const trackId of trackIdsWithGaps) { const comparator = precomputedComparator[trackId]; if ( comparator.getSortValue(ids[i - 1]).mandatory !== comparator.getSortValue(ids[i]).mandatory ) { gapIds[ids[i]] = true; } } } return gapIds; }); this.data_groups = new CachedProperty({}, function( model: OncoprintModel ) { // multiple tracks can have gaps // the groups will be segemented heirarchically const trackIdsWithGaps = model .getTracks() .filter(trackId => model.getTrackShowGaps(trackId)); const data_groups = _.reduce( model.track_label, ( agg: OncoprintDataGroupsByTrackId< TrackProp<ColumnProp<Datum>> >, label, trackId: number ) => { // key the data by the datum UID const keyedData = _.keyBy( model.track_data[trackId], m => m.uid ); const groups = trackIdsWithGaps.map(id => { // we need the datum in sorted order const data = model.id_order.map(d => keyedData[d]); const indexesAfterGap = model.column_indexes_after_a_gap.get(); // the indexes come AFTER a gap, so we need to include zero up front // in order to get initial slice of data const groupStartIndexes = [0, ...indexesAfterGap]; // using the group start indexes, slice the id data into corresponding groups return groupStartIndexes.map((n, i) => { if (i === groupStartIndexes.length - 1) { // we're at last one, so last group return data.slice(n); } else { return data.slice(n, groupStartIndexes[i + 1]); } }); }); agg[label.trim()] = groups; return agg; }, {} ); return data_groups; }); this.visible_id_order.addBoundProperty(this.ids_after_a_gap); this.precomputed_comparator.addBoundProperty(this.ids_after_a_gap); this.column_indexes_after_a_gap = new CachedProperty([], function( model: OncoprintModel ) { const ids_after_a_gap = model.ids_after_a_gap.get(); const id_to_index = model.getVisibleIdToIndexMap(); return Object.keys(ids_after_a_gap).map(id => id_to_index[id]); }); this.ids_after_a_gap.addBoundProperty(this.column_indexes_after_a_gap); this.column_left = new CachedProperty({}, function() { const cell_width = model.getCellWidth(true); const gap_size = model.getGapSize(); const ids_after_a_gap = model.ids_after_a_gap.get(); const cell_padding = model.getCellPadding(true); const left: ColumnProp<number> = {}; const ids = model.getIdOrder(); let current_left = 0; for (let i = 0; i < ids.length; i++) { if (ids_after_a_gap[ids[i]]) { current_left += gap_size; } left[ids[i]] = current_left; current_left += cell_width + cell_padding; } return left; }); this.ids_after_a_gap.addBoundProperty(this.column_left); this.column_left_always_with_padding = new CachedProperty( {}, function() { const cell_width = model.getCellWidth(true); const gap_size = model.getGapSize(); const ids_after_a_gap = model.ids_after_a_gap.get(); const cell_padding = model.getCellPadding(true, true); const left: ColumnProp<number> = {}; const ids = model.getIdOrder(); let current_left = 0; for (let i = 0; i < ids.length; i++) { if (ids_after_a_gap[ids[i]]) { current_left += gap_size; } left[ids[i]] = current_left; current_left += cell_width + cell_padding; } return left; } ); this.column_left.addBoundProperty(this.column_left_always_with_padding); this.zoomed_column_left = new CachedProperty({}, function() { const cell_width = model.getCellWidth(); const gap_size = model.getGapSize(); const ids_after_a_gap = model.ids_after_a_gap.get(); const cell_padding = model.getCellPadding(); const left: ColumnProp<number> = {}; const ids = model.getIdOrder(); let current_left = 0; for (let i = 0; i < ids.length; i++) { if (ids_after_a_gap[ids[i]]) { current_left += gap_size; } left[ids[i]] = current_left; current_left += cell_width + cell_padding; } return left; }); this.ids_after_a_gap.addBoundProperty(this.zoomed_column_left); this.column_left.addBoundProperty(this.zoomed_column_left); this.column_left_no_padding = new CachedProperty({}, function() { const cell_width = model.getCellWidth(true); const gap_size = model.getGapSize(); const ids_after_a_gap = model.ids_after_a_gap.get(); const left: ColumnProp<number> = {}; const ids = model.getIdOrder(); let current_left = 0; for (let i = 0; i < ids.length; i++) { if (ids_after_a_gap[ids[i]]) { current_left += gap_size; } left[ids[i]] = current_left; current_left += cell_width; } return left; }); this.ids_after_a_gap.addBoundProperty(this.column_left_no_padding); this.column_left.addBoundProperty(this.column_left_no_padding); } public setTrackShowGaps(trackId: TrackId, show: GAP_MODE_ENUM) { this.track_show_gaps[trackId] = show; this.track_gap_change_callback[trackId](trackId, show); this.ids_after_a_gap.update(this); } public getTrackShowGaps(trackId: TrackId) { return this.track_show_gaps[trackId]; } public getTrackCanShowGaps(trackId: TrackId) { return this.track_can_show_gaps[trackId]; } public getColumnIndexesAfterAGap() { return this.column_indexes_after_a_gap.get(); } public setTrackGroupHeader( index: TrackGroupIndex, header?: TrackGroupHeader ) { this.ensureTrackGroupExists(index); this.getTrackGroups()[index].header = header; this.track_tops.update(); } public getTrackGroupHeaderHeight(trackGroup: TrackGroup) { // TODO?: depends on text style settings // TODO?: depends on zoom? i dont think it should if (trackGroup.header) { return 32; } else { return 0; } } public toggleCellPadding() { this.cell_padding_on = !this.cell_padding_on; this.column_left.update(); return this.cell_padding_on; } public getCellPadding(base?: boolean, dont_consider_zoom?: boolean) { return ( this.cell_padding * (base ? 1 : this.horz_zoom) * +this.cell_padding_on * (dont_consider_zoom ? 1 : +!this.cell_padding_off_because_of_zoom) ); } public getHorzZoom() { return this.horz_zoom; } public getIdsInZoomedLeftInterval(left: number, right: number) { const leftIdIndex = this.getClosestColumnIndexToLeft(left, true); const rightIdIndex = this.getClosestColumnIndexToLeft( right, true, true ); return this.getIdOrder().slice(leftIdIndex, rightIdIndex); } public getHorzZoomToFitCols( width: number, left_col_incl: ColumnIndex, right_col_excl: ColumnIndex ) { // in the end, the zoomed width is: // W = z*(right_col_excl - left_col_incl)*baseColumnWidth + #gaps*gapSize // -> z = (width - #gaps*gapSize)/(right_col_excl - left_col_incl)*baseColumnWidth // numerator calculations const allGaps = this.getColumnIndexesAfterAGap(); const gapsBetween = allGaps.filter( g => g >= left_col_incl && g < right_col_excl ); const numerator = width - gapsBetween.length * this.getGapSize(); // denominator calculations const columnWidthWithPadding = this.getCellWidth(true) + this.getCellPadding(true, true); const columnWidthNoPadding = this.getCellWidth(true); const denominatorWithPadding = (right_col_excl - left_col_incl) * columnWidthWithPadding; const denominatorNoPadding = (right_col_excl - left_col_incl) * columnWidthNoPadding; // put them together const zoom_if_cell_padding_on = clamp( numerator / denominatorWithPadding, 0, 1 ); const zoom_if_cell_padding_off = clamp( numerator / denominatorNoPadding, 0, 1 ); let zoom; if (!this.cell_padding_on) { zoom = zoom_if_cell_padding_off; } else { const cell_width = this.getCellWidth(true); if ( cell_width * zoom_if_cell_padding_on < this.cell_padding_off_cell_width_threshold ) { if ( cell_width * zoom_if_cell_padding_off >= this.cell_padding_off_cell_width_threshold ) { // Because of cell padding toggling there's no way to get exactly the desired number of columns. // We can see this by contradiction: if we assume that cell padding is on, and try to fit exactly // our number of columns, we end up turning cell padding off (outer if statement). If we assume that // cell padding is off and try to fit our number of columns, we find that cell padding is on (inner if statement). // So instead lets just make sure to show all the columns by using the smaller zoom coefficient: zoom = zoom_if_cell_padding_on; } else { zoom = zoom_if_cell_padding_off; } } else { zoom = zoom_if_cell_padding_on; } } return zoom; } public getHorzZoomToFit(width: number, ids: ColumnId[]) { ids = ids || []; if (ids.length === 0) { return 1; } const id_to_index_map = this.getVisibleIdToIndexMap(); const indexes = ids.map(function(id) { return id_to_index_map[id]; }); let max = Number.NEGATIVE_INFINITY; let min = Number.POSITIVE_INFINITY; for (let i = 0; i < indexes.length; i++) { max = Math.max(indexes[i], max); min = Math.min(indexes[i], min); } return this.getHorzZoomToFitCols(width, min, max + 1); } public getMinHorzZoom() { return Math.min(MIN_ZOOM_PIXELS / this.getOncoprintWidth(true), 1); } public getMinVertZoom() { // Can't zoom to be smaller than max height // That zoom would be z*this.getOncoprintHeight(true) = max_height if (this.max_height < Number.POSITIVE_INFINITY) { return this.max_height / this.getOncoprintHeight(true); } else { // if no max height, then cant vert zoom return 1; } } public setHorzScroll(s: number) { this.horz_scroll = Math.max(0, s); return this.horz_scroll; } public setVertScroll(s: number) { this.vert_scroll = Math.max(0, s); return this.vert_scroll; } public setScroll(h: number, v: number) { this.setHorzScroll(h); this.setVertScroll(v); } public getHorzScroll() { return this.horz_scroll; } public getVertScroll() { return this.vert_scroll; } public setZoom(zoom_x: number, zoom_y: number) { this.setHorzZoom(zoom_x); this.setVertZoom(zoom_y); } private setCellPaddingOffBecauseOfZoom(val: boolean) { this.cell_padding_off_because_of_zoom = val; this.column_left.update(); } public setHorzZoom(z: number) { const min_zoom = this.getMinHorzZoom(); this.horz_zoom = clamp(z, min_zoom, 1); this.column_left.update(); if ( this.getCellWidth() < this.cell_padding_off_cell_width_threshold && !this.cell_padding_off_because_of_zoom ) { this.setCellPaddingOffBecauseOfZoom(true); } else if ( this.getCellWidth() >= this.cell_padding_off_cell_width_threshold && this.cell_padding_off_because_of_zoom ) { this.setCellPaddingOffBecauseOfZoom(false); } return this.horz_zoom; } public getVertZoom() { return this.vert_zoom; } public setVertZoom(z: number) { const min_zoom = this.getMinVertZoom(); this.vert_zoom = clamp(z, min_zoom, 1); this.track_tops.update(); return this.vert_zoom; } public setShowTrackLabels(s: boolean) { this.show_track_labels = s; } public getShowTrackLabels() { return this.show_track_labels; } public hideTrackLegends(track_ids: TrackId[]) { track_ids = [].concat(track_ids); for (let i = 0; i < track_ids.length; i++) { this.getRuleSet(track_ids[i]).exclude_from_legend = true; } } public showTrackLegends(track_ids: TrackId[]) { track_ids = [].concat(track_ids); for (let i = 0; i < track_ids.length; i++) { this.getRuleSet(track_ids[i]).exclude_from_legend = false; } } private clearTrackActiveRules(track_id: TrackId) { const rule_set_id = this.track_rule_set_id[track_id]; const track_active_rules = this.track_active_rules[track_id]; const rule_set_active_rules = this.rule_set_active_rules[rule_set_id]; const track_active_rule_ids = Object.keys(track_active_rules).map(x => parseInt(x, 10) ); for (let i = 0; i < track_active_rule_ids.length; i++) { const rule_id = track_active_rule_ids[i]; if (rule_set_active_rules.hasOwnProperty(rule_id)) { rule_set_active_rules[rule_id] -= 1; if (rule_set_active_rules[rule_id] <= 0) { delete rule_set_active_rules[rule_id]; } } } this.track_active_rules[track_id] = {}; } private setTrackActiveRules(track_id: TrackId, active_rules: ActiveRules) { this.clearTrackActiveRules(track_id); this.track_active_rules[track_id] = active_rules; const rule_set_id = this.track_rule_set_id[track_id]; const rule_set_active_rules = this.rule_set_active_rules[rule_set_id]; const track_active_rule_ids = Object.keys(active_rules).map(x => parseInt(x, 0) ); for (let i = 0; i < track_active_rule_ids.length; i++) { const rule_id = track_active_rule_ids[i]; rule_set_active_rules[rule_id] = rule_set_active_rules[rule_id] || 0; rule_set_active_rules[rule_id] += 1; } } public getTrackUniversalShapes( track_id: TrackId, use_base_size: boolean ): ComputedShapeParams[] { const ruleSet = this.getRuleSet(track_id); const spacing = this.getTrackHasColumnSpacing(track_id); const width = this.getCellWidth(use_base_size) + (!spacing ? this.getCellPadding(use_base_size, true) : 0); const height = this.getCellHeight(track_id, use_base_size); return ruleSet.getUniversalShapes(width, height); } public getSpecificShapesForData( track_id: TrackId, use_base_size: boolean ): IdentifiedShapeList[] { const active_rules = {}; const data = this.getTrackData(track_id); const id_key = this.getTrackDataIdKey(track_id); const spacing = this.getTrackHasColumnSpacing(track_id); const width = this.getCellWidth(use_base_size) + (!spacing ? this.getCellPadding(use_base_size, true) : 0); const shapes = this.getRuleSet(track_id).getSpecificShapesForDatum( data, width, this.getCellHeight(track_id, use_base_size), active_rules, id_key, this.getTrackImportantIds(track_id) ); this.setTrackActiveRules(track_id, active_rules); return shapes.map(function( shape_list: ComputedShapeParams[], index: number ) { return { id: data[index][id_key], shape_list: shape_list, }; }); } public getActiveRules(rule_set_id: RuleSetId) { const rule_set_active_rules = this.rule_set_active_rules[rule_set_id]; if (rule_set_active_rules) { return this.rule_sets[rule_set_id] .getSpecificRulesForDatum() .filter(function(rule_with_id: RuleWithId) { return !!rule_set_active_rules[rule_with_id.id]; }); } else { return []; } } public setTrackImportantIds(track_id: TrackId, ids?: ColumnId[]) { if (!ids) { this.track_important_ids[track_id] = undefined; } else { this.track_important_ids[track_id] = ids.reduce(function( map: ColumnProp<boolean>, next_id: ColumnId ) { map[next_id] = true; return map; }, {}); } } public getTrackImportantIds(track_id: TrackId) { return this.track_important_ids[track_id]; } public getRuleSets() { // return rule sets, in track group legend order const self = this; const legend_order = this.getTrackGroupLegendOrder(); const used_track_groups: { [trackGroupIndex: number]: boolean } = {}; const track_groups = this.getTrackGroups(); const sorted_track_groups = []; for (let i = 0; i < legend_order.length; i++) { // add track groups in legend order used_track_groups[legend_order[i]] = true; if (track_groups[legend_order[i]]) { sorted_track_groups.push(track_groups[legend_order[i]]); } } for (let i = 0; i < track_groups.length; i++) { // add groups not in legend order to end if (!used_track_groups[i] && track_groups[i]) { sorted_track_groups.push(track_groups[i]); } } const sorted_tracks: TrackId[] = sorted_track_groups.reduce(function( acc: TrackId[], next ) { return acc.concat(next.tracks); }, []); const rule_set_ids: number[] = sorted_tracks.map(function( track_id: TrackId ) { return self.track_rule_set_id[track_id]; }); const unique_rule_set_ids = arrayUnique( rule_set_ids.map(x => x.toString()) ); return unique_rule_set_ids.map(function(rule_set_id) { return self.rule_sets[parseInt(rule_set_id, 10)]; }); } public getTrackHasColumnSpacing(track_id: TrackId) { return !!this.track_has_column_spacing[track_id]; } public getGapSize() { if (this.showGaps()) { switch (this.gapMode()) { case GAP_MODE_ENUM.SHOW_GAPS: return this.getCellWidth(true); case GAP_MODE_ENUM.SHOW_GAPS_PERCENT: return 50; default: return 50; } } else { return this.getCellWidth(true); } } public getCellWidth(base?: boolean) { return this.cell_width * (base ? 1 : this.horz_zoom); } public getCellHeight(track_id: TrackId, base?: boolean) { return this.cell_height[track_id] * (base ? 1 : this.vert_zoom); } public getTrackInfo(track_id: TrackId) { return this.track_info[track_id]; } public setTrackInfo(track_id: TrackId, msg: string) { this.track_info[track_id] = msg; } public getTrackHeight(track_id: TrackId, base?: boolean) { return ( this.getCellHeight(track_id, base) + 2 * this.getTrackPadding(track_id, base) ); } public getTrackPadding(track_id: TrackId, base?: boolean) { return this.track_padding[track_id] * (base ? 1 : this.vert_zoom); } public getBottomPadding() { return this.bottom_padding; } public getTrackSortDirection(track_id: TrackId) { return this.track_sort_direction[track_id]; } public setTrackSortDirection( track_id: TrackId, dir: TrackSortDirection, no_callback?: boolean ) { // see above for dir options this.track_sort_direction[track_id] = dir; if (!no_callback) { this.track_sort_direction_change_callback[track_id](track_id, dir); } this.precomputed_comparator.update(this, track_id); } public resetSortableTracksSortDirection(no_callback?: boolean) { const allTracks = this.getTracks(); for (const trackId of allTracks) { if (this.isTrackSortDirectionChangeable(trackId)) { this.setTrackSortDirection(trackId, 0, no_callback); } } } public setCellPaddingOn(cell_padding_on: boolean) { this.cell_padding_on = cell_padding_on; this.column_left.update(); } public getIdOrder(all?: boolean) { if (all) { return this.id_order; // TODO: should be read-only } else { return this.visible_id_order.get(); } } public getClosestColumnIndexToLeft( left: number, zoomed?: boolean, roundUp?: boolean ) { const idToLeft = zoomed ? this.getZoomedColumnLeft() : this.getColumnLeft(); const ids = this.getIdOrder(); const lastId = ids[ids.length - 1]; if (left > idToLeft[lastId] + this.getCellWidth()) { return ids.length; } else if (left < idToLeft[ids[0]]) { return 0; } else { const index = binarysearch(ids, left, id => idToLeft[id], true); const id = ids[index]; const columnLeft = idToLeft[id]; if (roundUp && left !== columnLeft) { return index + 1; } else { return index; } } } public getIdToIndexMap() { return this.id_to_index.get(); } public getVisibleIdToIndexMap() { return this.visible_id_to_index.get(); } public getHiddenIds() { const hidden_ids = this.hidden_ids; return this.id_order.filter(function(id) { return !!hidden_ids[id]; }); } public isSortAffected( modified_ids: TrackId | TrackId[], group_or_track: 'track' | 'group' ) { modified_ids = [].concat(modified_ids); let group_indexes; const self = this; if (group_or_track === 'track') { group_indexes = modified_ids.map(function(id) { return self.getContainingTrackGroupIndex(id); }); } else { group_indexes = modified_ids; } return ( this.sort_config.type !== 'cluster' || group_indexes.indexOf(this.sort_config.track_group_index) > -1 ); } public setIdOrder(ids: ColumnId[]) { this.id_order = ids.slice(); Object.freeze(this.id_order); this.id_to_index.update(); this.visible_id_order.update(this); this.column_left.update(); } public hideIds(to_hide: ColumnId[], show_others?: boolean) { if (show_others) { this.hidden_ids = {}; } for (let j = 0, len = to_hide.length; j < len; j++) { this.hidden_ids[to_hide[j]] = true; } this.visible_id_order.update(this); this.column_left.update(); } public setHighlightedTracks(track_ids: TrackId[]) { this.highlighted_tracks = track_ids; } public getHighlightedTracks() { const realTracks = _.keyBy(this.getTracks()); return this.highlighted_tracks.filter(trackId => trackId in realTracks); } public setHighlightedIds(ids: ColumnId[]) { this.highlighted_ids = ids; } public getVisibleHighlightedIds() { const visibleIds = this.getVisibleIdToIndexMap(); return this.highlighted_ids.filter(uid => uid in visibleIds); } public restoreClusteredTrackGroupOrder() { if ( this.sort_config.type === 'cluster' && this.unclustered_track_group_order ) { const trackGroupIndex = this.sort_config.track_group_index; this.setTrackGroupOrder( trackGroupIndex, this.unclustered_track_group_order ); } this.unclustered_track_group_order = undefined; } public setTrackGroupOrder(index: TrackGroupIndex, track_order: TrackId[]) { this.track_groups[index].tracks = track_order; this.track_tops.update(); } public moveTrackGroup( from_index: TrackGroupIndex, to_index: TrackGroupIndex ) { const new_groups = []; const new_headers = [];