oncoprintjs
Version:
A data visualization for cancer genomic data.
1,359 lines (1,236 loc) • 88.8 kB
text/typescript
/* 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 = [];