UNPKG

oncoprintjs

Version:

A data visualization for cancer genomic data.

1,478 lines (1,372 loc) 48.7 kB
/* Rule: * * condition: function from datum to boolean * shapes - a list of Shapes * legend_label * exclude_from_legend * * Shape: * type * x * y * ... shape-specific attrs ... * * Attrs by shape: * * rectangle: x, y, width, height, stroke, stroke-width, fill * triangle: x1, y1, x2, y2, x3, y3, stroke, stroke-width, fill * ellipse: x, y, width, height, stroke, stroke-width, fill * line: x1, y1, x2, y2, stroke, stroke-width */ import { ComputedShapeParams, Ellipse, Line, Rectangle, Shape, ShapeParams, Triangle, } from './oncoprintshape'; import heatmapColors from './heatmapcolors'; import binarysearch from './binarysearch'; import { Omit, cloneShallow, ifndef, objectValues, shallowExtend, z_comparator, } from './utils'; import { ActiveRules, ColumnProp, Datum, RuleSetId } from './oncoprintmodel'; import _ from 'lodash'; import extractrgba, { hexToRGBA, rgbaToHex } from './extractrgba'; export type RuleSetParams = | ILinearInterpRuleSetParams | ICategoricalRuleSetParams | IGradientRuleSetParams | IBarRuleSetParams | IStackedBarRuleSetParams | IGradientAndCategoricalRuleSetParams | IGeneticAlterationRuleSetParams; interface IGeneralRuleSetParams { type?: RuleSetType; legend_label?: string; legend_base_color?: RGBAColor; exclude_from_legend?: boolean; na_z?: number; // z index of na shapes (defaults to 1) na_legend_label?: string; // legend label associated to NA (defaults to 'No data') na_shapes?: ShapeParams[]; // defaults to single strikethrough line } interface ILinearInterpRuleSetParams extends IGeneralRuleSetParams { log_scale?: boolean; value_key: string; value_range: [number, number]; } // all colors are hex, rgb, or rgba export interface ICategoricalRuleSetParams extends IGeneralRuleSetParams { type: RuleSetType.CATEGORICAL; category_key: string; // key into data which gives category category_to_color?: { [category: string]: RGBAColor }; universal_rule_categories?: { [category: string]: any }; } export interface IGradientRuleSetParams extends ILinearInterpRuleSetParams { type: RuleSetType.GRADIENT; // either `colormap_name` or `colors` needs to be present colors?: RGBAColor[]; // [r,g,b,a][] colormap_name?: string; // name of a colormap found in src/js/heatmapcolors.js value_stop_points: number[]; null_color?: RGBAColor; null_legend_label?: string; } // TODO: it would be more elegant to create multiple inheritance (if possible) since // IGradientAndCategoricalRuleSetParams is a IGradientRuleSetParams and // ICategoricalRuleSetParams with a different `type` field. export interface IGradientAndCategoricalRuleSetParams extends IGeneralRuleSetParams { type: RuleSetType.GRADIENT_AND_CATEGORICAL; // either `colormap_name` or `colors` needs to be present colors?: RGBAColor[]; colormap_name?: string; // name of a colormap found in src/js/heatmapcolors.js value_stop_points: number[]; null_color?: RGBAColor; log_scale?: boolean; value_key: string; value_range: [number, number]; category_key: string; // key into data which gives category category_to_color?: { [category: string]: RGBAColor }; } export interface IBarRuleSetParams extends ILinearInterpRuleSetParams { type: RuleSetType.BAR; fill?: RGBAColor; negative_fill?: RGBAColor; } export interface IStackedBarRuleSetParams extends IGeneralRuleSetParams { type: RuleSetType.STACKED_BAR; value_key: string; categories: string[]; fills?: RGBAColor[]; } export interface IGeneticAlterationRuleSetParams extends IGeneralRuleSetParams { type: RuleSetType.GENE; rule_params: GeneticAlterationRuleParams; } type GeneticAlterationSingleRuleParams = { shapes: ShapeParams[]; legend_label: string; exclude_from_legend?: boolean; legend_order?: number; }; export type GeneticAlterationRuleParams = { always?: GeneticAlterationSingleRuleParams; conditional: { [datumKey: string]: { [commaSeparatedDatumValues: string]: GeneticAlterationSingleRuleParams; }; }; }; export type RGBAColor = [number, number, number, number]; //[0,255] x [0,255] x [0,255] x [0,1] type RuleParams = { shapes: ShapeParams[]; legend_label?: string; exclude_from_legend?: boolean; legend_config?: RuleLegendConfig; legend_order?: number; legend_base_color?: RGBAColor; }; type RuleLegendConfig = | { type: 'rule'; target: any } | { type: 'number'; range: [number, number]; range_type: LinearInterpRangeType; positive_color: RGBAColor; negative_color: RGBAColor; interpFn: (val: number) => number; } // range: [lower, upper] | { type: 'gradient'; range: [number, number]; colorFn: (val: number) => RGBAColor; }; export enum RuleSetType { CATEGORICAL = 'categorical', GRADIENT = 'gradient', GRADIENT_AND_CATEGORICAL = 'gradient+categorical', BAR = 'bar', STACKED_BAR = 'stacked_bar', GENE = 'gene', } export type RuleId = number; export type RuleWithId = { id: RuleId; rule: Rule; }; function makeIdCounter() { let id = 0; return function() { id += 1; return id; }; } function intRange(length: number) { const ret = []; for (let i = 0; i < length; i++) { ret.push(i); } return ret; } function makeUniqueColorGetter(init_used_colors: string[]) { init_used_colors = init_used_colors || []; const colors = [ '#3366cc', '#dc3912', '#ff9900', '#109618', '#990099', '#0099c6', '#dd4477', '#66aa00', '#b82e2e', '#316395', '#994499', '#22aa99', '#aaaa11', '#6633cc', '#e67300', '#8b0707', '#651067', '#329262', '#5574a6', '#3b3eac', '#b77322', '#16d620', '#b91383', '#f4359e', '#9c5935', '#a9c413', '#2a778d', '#668d1c', '#bea413', '#0c5922', '#743411', ]; // Source: D3 let index = 0; const used_colors: { [color: string]: boolean } = {}; for (let i = 0; i < init_used_colors.length; i++) { used_colors[init_used_colors[i]] = true; } return function(color?: string) { if (color) { // calling with an argument adds it to the used colors record used_colors[color] = true; } else { // calling without an argument returns a new unused color let next_color = colors[index % colors.length]; while (used_colors[next_color]) { const darker_next_color = darkenHexColor(next_color); if (darker_next_color === next_color) { break; } next_color = darker_next_color; } used_colors[next_color] = true; index += 1; return hexToRGBA(next_color); } return undefined; }; } function makeNAShapes(z: number): ShapeParams[] { return [ { type: 'rectangle', fill: [255, 255, 255, 1], z: z, }, { type: 'line', stroke: [190, 190, 190, 1], 'stroke-width': 1, x1: 0, x2: 100, y1: 50, y2: 50, z: z, }, ]; } const NA_STRING = 'na'; const NA_LABEL = 'No data'; function colorToHex(color: string) { let r; let g; let b; const rgba_match = color.match( /^[\s]*rgba\([\s]*([0-9]+)[\s]*,[\s]*([0-9]+)[\s]*,[\s]*([0-9]+)[\s]*,[\s]*([0-9.]+)[\s]*\)[\s]*$/ ); if (rgba_match && rgba_match.length === 5) { r = parseInt(rgba_match[1]).toString(16); g = parseInt(rgba_match[2]).toString(16); b = parseInt(rgba_match[3]).toString(16); if (r.length === 1) { r = '0' + r; } if (g.length === 1) { g = '0' + g; } if (b.length === 1) { b = '0' + b; } return '#' + r + g + b; } const rgb_match = color.match( /^[\s]*rgb\([\s]*([0-9]+)[\s]*,[\s]*([0-9]+)[\s]*,[\s]*([0-9]+)[\s]*\)[\s]*$/ ); if (rgb_match && rgb_match.length === 4) { r = parseInt(rgb_match[1]).toString(16); g = parseInt(rgb_match[2]).toString(16); b = parseInt(rgb_match[3]).toString(16); if (r.length === 1) { r = '0' + r; } if (g.length === 1) { g = '0' + g; } if (b.length === 1) { b = '0' + b; } return '#' + r + g + b; } return color; } function darkenHexChannel(c: string) { let numC = parseInt(c, 16); numC *= 0.95; numC = Math.round(numC); c = numC.toString(16); if (c.length === 1) { c = '0' + c; } return c; } function darkenHexColor(color: string) { let r = color[1] + color[2]; let g = color[3] + color[4]; let b = color[5] + color[6]; r = darkenHexChannel(r); g = darkenHexChannel(g); b = darkenHexChannel(b); return '#' + r + g + b; } export class RuleSet { static getRuleSetId = makeIdCounter(); static getRuleId = makeIdCounter(); public rule_set_id: RuleSetId; public legend_label?: string; protected legend_base_color?: RGBAColor; public exclude_from_legend?: boolean; protected active_rule_ids: ActiveRules; protected rules_with_id: RuleWithId[]; protected universal_rule?: RuleWithId; constructor(params: Omit<RuleSetParams, 'type'>) { /* params: * - legend_label * - exclude_from_legend */ this.rule_set_id = RuleSet.getRuleSetId(); this.legend_label = params.legend_label; this.legend_base_color = params.legend_base_color; this.exclude_from_legend = params.exclude_from_legend; this.active_rule_ids = {}; this.rules_with_id = []; } public getLegendLabel() { return this.legend_label; } public getRuleSetId() { return this.rule_set_id; } public addRules(list_of_params: RuleParams[]) { const self = this; return list_of_params.map(function(params) { return self._addRule(params); }); } public _addRule(params: RuleParams, rule_id?: RuleId) { if (typeof rule_id === 'undefined') { rule_id = RuleSet.getRuleId(); } this.rules_with_id.push({ id: rule_id, rule: new Rule(params) }); return rule_id; } public setUniversalRule(r: RuleWithId) { this.universal_rule = r; } public removeRule(rule_id: RuleId) { var index = -1; for (let i = 0; i < this.rules_with_id.length; i++) { if (this.rules_with_id[i].id === rule_id) { index = i; break; } } if (index > -1) { this.rules_with_id.splice(index, 1); } delete this.active_rule_ids[rule_id]; } public getRuleWithId(rule_id: RuleId) { let ret = null; for (let i = 0; i < this.rules_with_id.length; i++) { if (this.rules_with_id[i].id === rule_id) { ret = this.rules_with_id[i]; break; } } return ret; } public isExcludedFromLegend() { return this.exclude_from_legend; } public getRule(rule_id: RuleId): Rule { return this.getRuleWithId(rule_id).rule; } public getRecentlyUsedRules() { const self = this; return Object.keys(this.active_rule_ids).map(function(rule_id) { return self.getRule(parseInt(rule_id, 10)); }); } public applyRulesToDatum( rules_with_id: RuleWithId[], datum: Datum, cell_width: number, cell_height: number ) { let shapes: ComputedShapeParams[] = []; const rules_len = rules_with_id.length; for (let j = 0; j < rules_len; j++) { shapes = shapes.concat( rules_with_id[j].rule.apply(datum, cell_width, cell_height) ); } return shapes; } public getSpecificRulesForDatum(datum?: Datum): RuleWithId[] { throw 'Not implemented on base class'; } public getUniversalRule() { return this.universal_rule; } public getUniversalShapes(cell_width: number, cell_height: number) { if (this.getUniversalRule()) { const shapes = this.getUniversalRule().rule.apply( {}, // a universal rule does not rely on anything specific to the data cell_width, cell_height ); shapes.sort(z_comparator); return shapes; } else { return []; } } public getSpecificShapesForDatum( data: Datum[], cell_width: number, cell_height: number, out_active_rules?: ActiveRules | undefined, data_id_key?: string & keyof Datum, important_ids?: ColumnProp<boolean> ) { // Returns a list of lists of concrete shapes, in the same order as data // optional parameter important_ids determines which ids count towards active rules (optional parameter data_id_key // is used for this too) const ret = []; for (var i = 0; i < data.length; i++) { const datum = data[i]; const should_mark_active = !important_ids || !!important_ids[datum[data_id_key!]]; const rules = this.getSpecificRulesForDatum(datum); if (typeof out_active_rules !== 'undefined' && should_mark_active) { for (let j = 0; j < rules.length; j++) { out_active_rules[rules[j].id] = true; } } const shapes = this.applyRulesToDatum( rules, data[i], cell_width, cell_height ); shapes.sort(z_comparator); ret.push(shapes); } // mark universal rule as active if ( this.getUniversalRule() && typeof out_active_rules !== 'undefined' ) { out_active_rules[this.getUniversalRule().id] = true; } return ret; } } class LookupRuleSet extends RuleSet { private lookup_map_by_key_and_value: { [key: string]: { [value: string]: RuleWithId }; } = {}; private lookup_map_by_key: { [key: string]: RuleWithId } = {}; private rule_id_to_conditions: { [ruleId: number]: { key: string; value: string }[]; } = {}; public getSpecificRulesForDatum(datum?: Datum) { if (typeof datum === 'undefined') { return this.rules_with_id; } let ret: RuleWithId[] = []; for (var key in datum) { if (key in datum && typeof datum[key] !== 'undefined') { var key_rule = this.lookup_map_by_key[key]; if (typeof key_rule !== 'undefined') { ret.push(key_rule); } var key_and_value_rule = (this.lookup_map_by_key_and_value[key] && this.lookup_map_by_key_and_value[key][datum[key]]) || undefined; if (typeof key_and_value_rule !== 'undefined') { ret.push(key_and_value_rule); } } } return ret; } private indexRuleForLookup( condition_key: string, condition_value: string, rule_with_id: RuleWithId ) { if (condition_key === null) { this.setUniversalRule(rule_with_id); } else { if (condition_value === null) { this.lookup_map_by_key[condition_key] = rule_with_id; } else { this.lookup_map_by_key_and_value[condition_key] = this.lookup_map_by_key_and_value[condition_key] || {}; this.lookup_map_by_key_and_value[condition_key][ condition_value ] = rule_with_id; } } this.rule_id_to_conditions[rule_with_id.id] = this.rule_id_to_conditions[rule_with_id.id] || []; this.rule_id_to_conditions[rule_with_id.id].push({ key: condition_key, value: condition_value, }); } public addRule( condition_key: string, condition_value: any, params: RuleParams ) { const rule_id = this._addRule(params); this.indexRuleForLookup( condition_key, condition_value, this.getRuleWithId(rule_id) ); return rule_id; } public linkExistingRule( condition_key: string, condition_value: string, existing_rule_id: RuleId ) { this.indexRuleForLookup( condition_key, condition_value, this.getRuleWithId(existing_rule_id) ); } public removeRule(rule_id: RuleId) { super.removeRule(rule_id); while (this.rule_id_to_conditions[rule_id].length > 0) { var condition = this.rule_id_to_conditions[rule_id].pop(); if (condition.key === null) { // universal rule this.universal_rule = undefined; } else { if (condition.value === null) { delete this.lookup_map_by_key[condition.key]; } else { delete this.lookup_map_by_key_and_value[condition.key][ condition.value ]; } } } delete this.rule_id_to_conditions[rule_id]; } } type ConditionRuleSetCondition = (d: Datum) => boolean; class ConditionRuleSet extends RuleSet { private rule_id_to_condition: { [ruleId: number]: ConditionRuleSetCondition; } = {}; constructor(params: RuleSetParams, omitNArule?: boolean) { super(params); if (!omitNArule) { this.addRule( function(d) { return d[NA_STRING] === true; }, { shapes: params.na_shapes || makeNAShapes(params.na_z || 1000), legend_label: params.na_legend_label || NA_LABEL, exclude_from_legend: false, legend_config: { type: 'rule', target: { na: true } }, legend_order: Number.POSITIVE_INFINITY, } ); } } public getSpecificRulesForDatum(datum?: Datum) { if (typeof datum === 'undefined') { return this.rules_with_id; } const ret = []; for (let i = 0; i < this.rules_with_id.length; i++) { if (this.rule_id_to_condition[this.rules_with_id[i].id](datum)) { ret.push(this.rules_with_id[i]); } } return ret; } public addRule( condition: ConditionRuleSetCondition, params: RuleParams, rule_id?: RuleId ) { rule_id = this._addRule(params, rule_id); this.rule_id_to_condition[rule_id] = condition; return rule_id; } public removeRule(rule_id: RuleId) { super.removeRule(rule_id); delete this.rule_id_to_condition[rule_id]; } } class CategoricalRuleSet extends LookupRuleSet { public readonly category_key: string; private readonly category_to_color: { [category: string]: RGBAColor }; private readonly getUnusedColor: (color?: string) => RGBAColor; private readonly universal_rule_categories?: { [category: string]: any }; constructor( params: Omit<ICategoricalRuleSetParams, 'type'>, omitNArule?: boolean ) { super(params); if (!omitNArule) { this.addRule(NA_STRING, true, { shapes: params.na_shapes || makeNAShapes(params.na_z || 1000), legend_label: params.na_legend_label || NA_LABEL, exclude_from_legend: false, legend_config: { type: 'rule', target: { na: true } }, legend_order: Number.POSITIVE_INFINITY, }); } this.category_key = params.category_key; this.universal_rule_categories = params.universal_rule_categories; this.category_to_color = cloneShallow( ifndef(params.category_to_color, {}) ); this.getUnusedColor = makeUniqueColorGetter( objectValues(this.category_to_color).map(rgbaToHex) ); for (const category of Object.keys(this.category_to_color)) { const color = this.category_to_color[category]; this.addCategoryRule(category, color); this.getUnusedColor(rgbaToHex(color)); } } private addCategoryRule(category: string, color: RGBAColor) { const legend_rule_target: any = {}; legend_rule_target[this.category_key] = category; const rule_params: RuleParams = { shapes: [ { type: 'rectangle', fill: color, }, ], legend_label: category, exclude_from_legend: false, legend_config: { type: 'rule', target: legend_rule_target }, }; if ( this.universal_rule_categories && this.universal_rule_categories.hasOwnProperty(category) ) { // add universal rule this.addRule(null, category, rule_params); } else { this.addRule(this.category_key, category, rule_params); } } public getSpecificShapesForDatum( data: Datum, cell_width: number, cell_height: number, out_active_rules: ActiveRules | undefined, data_id_key: string & keyof Datum, important_ids?: ColumnProp<boolean> ) { // First ensure there is a color for all categories for (let i = 0, data_len = data.length; i < data_len; i++) { if (data[i][NA_STRING]) { continue; } const category = data[i][this.category_key]; if (!(category in this.category_to_color)) { const color = this.getUnusedColor(); this.category_to_color[category] = color; this.addCategoryRule(category, color); } } // Then propagate the call up return super.getSpecificShapesForDatum( data, cell_width, cell_height, out_active_rules, data_id_key, important_ids ); } } export enum LinearInterpRangeType { ALL = 'ALL', // all values positive, negative and zero NON_NEGATIVE = 'NON_NEGATIVE', // value range all positive values inclusive zero (0) NON_POSITIVE = 'NON_POSITIVE', // value range all negative values inclusive zero (0) } class LinearInterpRuleSet extends ConditionRuleSet { protected value_key: string; protected value_range: [number, number]; protected log_scale?: boolean; protected type: string; protected makeInterpFn: () => (valToConvert: number) => number; protected inferred_value_range: [number, number]; constructor(params: ILinearInterpRuleSetParams) { super(params); this.value_key = params.value_key; this.value_range = params.value_range; this.log_scale = params.log_scale; // boolean this.type = params.type; this.makeInterpFn = function() { const range = this.getEffectiveValueRange(); const rangeType = this.getValueRangeType(); const plotType = this.type; if (this.log_scale) { var shift_to_make_pos = Math.abs(range[0]) + 1; var log_range = Math.log(range[1] + shift_to_make_pos) - Math.log(range[0] + shift_to_make_pos); var log_range_lower = Math.log(range[0] + shift_to_make_pos); return function(val: number) { return ( (Math.log(val + shift_to_make_pos) - log_range_lower) / log_range ); }; } else { return function(val) { var range_spread = range[1] - range[0], range_lower = range[0], range_higher = range[1]; if (plotType === 'bar') { if (rangeType === LinearInterpRangeType.NON_POSITIVE) { // when data only contains non positive values return (val - range_higher) / range_spread; } else if ( rangeType === LinearInterpRangeType.NON_NEGATIVE ) { // when data only contains non negative values return (val - range_lower) / range_spread; } else if (rangeType === LinearInterpRangeType.ALL) { range_spread = Math.abs(range[0]) > range[1] ? Math.abs(range[0]) : range[1]; return val / range_spread; } } else { return (val - range_lower) / range_spread; } return undefined; }; } }; } protected getEffectiveValueRange(): [number, number] { const ret = (this.value_range && this.value_range.slice()) || [ undefined, undefined, ]; if (typeof ret[0] === 'undefined') { ret[0] = this.inferred_value_range[0]; } if (typeof ret[1] === 'undefined') { ret[1] = this.inferred_value_range[1]; } if (ret[0] === ret[1]) { // Make sure non-empty interval ret[0] -= ret[0] / 2; ret[1] += ret[1] / 2; } return ret as [number, number]; } protected getValueRangeType() { var range = this.getEffectiveValueRange(); if (range[0] < 0 && range[1] <= 0) { return LinearInterpRangeType.NON_POSITIVE; } else if (range[0] >= 0 && range[1] > 0) { return LinearInterpRangeType.NON_NEGATIVE; } else { return LinearInterpRangeType.ALL; } } public getSpecificShapesForDatum( data: Datum, cell_width: number, cell_height: number, out_active_rules: ActiveRules | undefined, data_id_key: string & keyof Datum, important_ids?: ColumnProp<boolean> ) { // First find value range let value_min = Number.POSITIVE_INFINITY; let value_max = Number.NEGATIVE_INFINITY; for (var i = 0, datalen = data.length; i < datalen; i++) { const d = data[i]; if (isNaN(d[this.value_key])) { continue; } value_min = Math.min(value_min, d[this.value_key]); value_max = Math.max(value_max, d[this.value_key]); } if (value_min === Number.POSITIVE_INFINITY) { value_min = 0; } if (value_max === Number.NEGATIVE_INFINITY) { value_max = 0; } this.inferred_value_range = [value_min, value_max]; this.updateLinearRules(); // Then propagate the call up return super.getSpecificShapesForDatum( data, cell_width, cell_height, out_active_rules, data_id_key, important_ids ); } protected updateLinearRules() { throw 'Not implemented in abstract class'; } } class GradientRuleSet extends LinearInterpRuleSet { private colors: RGBAColor[] = []; private value_stop_points: number[]; private null_color?: RGBAColor; private gradient_rule: RuleId; constructor(params: Omit<IGradientRuleSetParams, 'type'>) { super(params); if (params.colors) { this.colors = params.colors || []; } else if (params.colormap_name) { this.colors = heatmapColors[params.colormap_name] || []; } if (this.colors.length === 0) { this.colors.push([0, 0, 0, 1], [255, 0, 0, 1]); } this.value_stop_points = params.value_stop_points; this.null_color = params.null_color || [211, 211, 211, 1]; var self = this; var value_key = this.value_key; this.addRule( function(d) { return d[NA_STRING] !== true && d[value_key] === null; }, { shapes: [ { type: 'rectangle', fill: self.null_color, }, ], legend_label: params.null_legend_label || 'Not a number', exclude_from_legend: false, legend_config: { type: 'rule', target: { [value_key]: null } }, } ); } static linInterpColors( t: number, begin_color: RGBAColor, end_color: RGBAColor ): RGBAColor { // 0 <= t <= 1 return [ Math.round(begin_color[0] * (1 - t) + end_color[0] * t), Math.round(begin_color[1] * (1 - t) + end_color[1] * t), Math.round(begin_color[2] * (1 - t) + end_color[2] * t), begin_color[3] * (1 - t) + end_color[3] * t, ]; } private makeColorFn( colors: RGBAColor[], interpFn: (valToConvert: number) => number ) { const value_stop_points = this.value_stop_points; let stop_points: number[]; if (value_stop_points) { stop_points = value_stop_points.map(interpFn); } else { stop_points = intRange(colors.length).map(function(x) { return x / (colors.length - 1); }); } return function(t: number): RGBAColor { // 0 <= t <= 1 var begin_interval_index = binarysearch( stop_points, t, function(x) { return x; }, true ); if (begin_interval_index === -1) { return [0, 0, 0, 1]; } var end_interval_index = Math.min( colors.length - 1, begin_interval_index + 1 ); var spread = stop_points[end_interval_index] - stop_points[begin_interval_index]; if (spread === 0) { return colors[end_interval_index]; } else { var interval_t = (t - stop_points[begin_interval_index]) / spread; var begin_color = colors[begin_interval_index]; var end_color = colors[end_interval_index]; return GradientRuleSet.linInterpColors( interval_t, begin_color, end_color ); } }; } protected updateLinearRules() { let rule_id; if (typeof this.gradient_rule !== 'undefined') { rule_id = this.gradient_rule; this.removeRule(this.gradient_rule); } const interpFn = this.makeInterpFn(); const colorFn = this.makeColorFn(this.colors, interpFn); const value_key = this.value_key; const null_color = this.null_color; this.gradient_rule = this.addRule( function(d) { return d[NA_STRING] !== true && d[value_key] !== null; }, { shapes: [ { type: 'rectangle', fill: function(d) { var t = interpFn(d[value_key]); return colorFn(t) as RGBAColor; }, }, ], exclude_from_legend: false, legend_config: { type: 'gradient' as 'gradient', range: this.getEffectiveValueRange(), colorFn: colorFn, }, }, rule_id ); } } class BarRuleSet extends LinearInterpRuleSet { private fill: RGBAColor; private negative_fill: RGBAColor; private bar_rule?: RuleId; constructor(params: IBarRuleSetParams) { super(params); this.fill = params.fill || [0, 128, 0, 1]; // green this.negative_fill = params.negative_fill || [255, 0, 0, 1]; //red } protected updateLinearRules() { let rule_id; if (typeof this.bar_rule !== 'undefined') { rule_id = this.bar_rule; this.removeRule(this.bar_rule); } const interpFn = this.makeInterpFn(); const value_key = this.value_key; const positive_color = this.fill; const negative_color = this.negative_fill; const yPosFn = this.getYPosPercentagesFn(); const cellHeightFn = this.getCellHeightPercentagesFn(); this.bar_rule = this.addRule( function(d) { return d[NA_STRING] !== true; }, { shapes: [ { type: 'rectangle', y: function(d) { var t = interpFn(d[value_key]); return yPosFn(t); }, height: function(d) { var t = interpFn(d[value_key]); return cellHeightFn(t); }, fill: function(d) { return d[value_key] < 0 ? negative_color : positive_color; }, }, ], exclude_from_legend: false, legend_config: { type: 'number' as 'number', range: this.getEffectiveValueRange(), range_type: this.getValueRangeType(), positive_color: positive_color, negative_color: negative_color, interpFn: interpFn, }, }, rule_id ); } public getYPosPercentagesFn() { let ret; switch (this.getValueRangeType()) { case LinearInterpRangeType.NON_POSITIVE: ret = function(t: number) { return 0; }; break; case LinearInterpRangeType.NON_NEGATIVE: ret = function(t: number) { return (1 - t) * 100; }; break; case LinearInterpRangeType.ALL: ret = function(t: number) { return Math.min(1 - t, 1) * 50; }; break; } return ret; } public getCellHeightPercentagesFn() { let ret; switch (this.getValueRangeType()) { case LinearInterpRangeType.NON_POSITIVE: ret = function(t: number) { return -t * 100; }; break; case LinearInterpRangeType.NON_NEGATIVE: ret = function(t: number) { return t * 100; }; break; case LinearInterpRangeType.ALL: ret = function(t: number) { return Math.abs(t) * 50; }; break; } return ret; } } class StackedBarRuleSet extends ConditionRuleSet { constructor(params: IStackedBarRuleSetParams) { super(params); const value_key = params.value_key; const fills = params.fills || []; const categories = params.categories || []; const getUnusedColor = makeUniqueColorGetter(fills.map(rgbaToHex)); // Initialize with default values while (fills.length < categories.length) { fills.push(getUnusedColor()); } const self = this; for (let i = 0; i < categories.length; i++) { (function(I) { const legend_target: any = {}; legend_target[value_key] = {}; for (let j = 0; j < categories.length; j++) { legend_target[value_key][categories[j]] = 0; } legend_target[value_key][categories[I]] = 1; self.addRule( function(d) { return d[NA_STRING] !== true; }, { shapes: [ { type: 'rectangle', fill: fills[I], width: 100, height: function(d) { var total = 0; for ( var j = 0; j < categories.length; j++ ) { total += parseFloat( d[value_key][categories[j]] ); } return ( (parseFloat( d[value_key][categories[I]] ) * 100) / total ); }, y: function(d) { var total = 0; var prev_vals_sum = 0; for ( var j = 0; j < categories.length; j++ ) { var new_val = parseFloat( d[value_key][categories[j]] ); if (j < I) { prev_vals_sum += new_val; } total += new_val; } return (prev_vals_sum * 100) / total; }, }, ], exclude_from_legend: false, legend_config: { type: 'rule', target: legend_target }, legend_label: categories[I], } ); })(i); } } } export class GeneticAlterationRuleSet extends LookupRuleSet { constructor(params: IGeneticAlterationRuleSetParams) { super(params); this.addRulesFromParams(params); this.addRule(NA_STRING, true, { shapes: params.na_shapes || makeNAShapes(params.na_z || 1), legend_label: params.na_legend_label || NA_LABEL, exclude_from_legend: false, legend_config: { type: 'rule', target: { na: true } }, legend_order: Number.POSITIVE_INFINITY, }); } private addRulesFromParams(params: IGeneticAlterationRuleSetParams) { const rule_params = params.rule_params; _.forEach( rule_params.conditional, ( datumValuesToRuleParams: GeneticAlterationRuleParams['conditional']['datumKey'], datumKey: string ) => { _.forEach( datumValuesToRuleParams, ( ruleParams: GeneticAlterationSingleRuleParams, commaSeparatedDatumValues: string ) => { const equiv_values = commaSeparatedDatumValues.split( ',' ); const legend_rule_target: any = {}; legend_rule_target[ equiv_values[0] ] = commaSeparatedDatumValues; const rule_id = this.addRule( datumKey, equiv_values[0] === '*' ? null : equiv_values[0], shallowExtend(ruleParams, { shapes: ruleParams.shapes, legend_config: { type: 'rule' as 'rule', target: legend_rule_target, }, legend_base_color: ifndef( this.legend_base_color, [255, 255, 255, 1] ) as [number, number, number, number], }) ); for (let i = 1; i < equiv_values.length; i++) { this.linkExistingRule( datumKey, equiv_values[i] === '*' ? null : equiv_values[i], rule_id ); } } ); } ); if (rule_params.always) { this.addRule( null, null, shallowExtend(rule_params.always, { shapes: rule_params.always.shapes, legend_config: { type: 'rule' as 'rule', target: {} }, }) ); } } } export class Rule { private shapes: Shape[]; public legend_label: string; public legend_base_color?: RGBAColor; public exclude_from_legend?: boolean; private legend_config?: RuleLegendConfig; public legend_order?: number; constructor(params: RuleParams) { this.shapes = params.shapes.map(function(shape) { if (shape.type === 'rectangle') { return new Rectangle(shape); } else if (shape.type === 'triangle') { return new Triangle(shape); } else if (shape.type === 'ellipse') { return new Ellipse(shape); } else if (shape.type === 'line') { return new Line(shape); } return undefined; }); this.legend_label = typeof params.legend_label === 'undefined' ? '' : params.legend_label; this.legend_base_color = params.legend_base_color; this.exclude_from_legend = params.exclude_from_legend; this.legend_config = params.legend_config; this.legend_order = params.legend_order; } public getLegendConfig() { return this.legend_config; } public apply(d: Datum, cell_width: number, cell_height: number) { // Gets concrete shapes (i.e. computed // real values from percentages) const concrete_shapes = []; for (let i = 0, shapes_len = this.shapes.length; i < shapes_len; i++) { concrete_shapes.push( this.shapes[i].getComputedParams(d, cell_width, cell_height) ); } return concrete_shapes; } public isExcludedFromLegend() { return this.exclude_from_legend; } } class GradientCategoricalRuleSet extends RuleSet { private gradientRuleSet: GradientRuleSet; private categoricalRuleSet: CategoricalRuleSet; constructor(params: IGradientAndCategoricalRuleSetParams) { super(params); // For the GradientCategoricalRuleSet a datum must always have a // value and may have a category attribute. A datum is 'NA' // when not meeting the requirements for the GradientRuleSet. // To achieve correct evaluation, the CategoricalRuleSet is // asked not to contribute an `NA` rule (via `true` flag). this.gradientRuleSet = new GradientRuleSet(params); this.categoricalRuleSet = new CategoricalRuleSet(params, true); } // RuleSet API public getSpecificShapesForDatum( data: Datum, cell_width: number, cell_height: number, out_active_rules: ActiveRules | undefined, data_id_key: string & keyof Datum, important_ids?: ColumnProp<boolean> ) { const shapes = []; // check the type of datum (categorical or continuous) and delegate // fetching of shapes to the appropriate RuleSet class for (let i = 0; i < data.length; i++) { const datum = data[i]; if (this.isCategorical(datum)) { shapes.push( this.categoricalRuleSet.getSpecificShapesForDatum( [datum], cell_width, cell_height, out_active_rules, data_id_key, important_ids )[0] ); } else { shapes.push( this.gradientRuleSet.getSpecificShapesForDatum( [datum], cell_width, cell_height, out_active_rules, data_id_key, important_ids )[0] ); } } return shapes; } // RuleSet API public getSpecificRulesForDatum(datum?: Datum) { const categoricalRules = this.categoricalRuleSet.getSpecificRulesForDatum( datum ); const gradientRules = this.gradientRuleSet.getSpecificRulesForDatum( datum ); const rules = categoricalRules.concat(gradientRules); return rules; } // helper function public isCategorical(datum: Datum) { // A categorical value is recognized by presence of a category attribute. // Note: a categorical datum still requires a continuous value (used for clustering). return datum[this.categoricalRuleSet.category_key] !== undefined; } } export default function(params: RuleSetParams) { let ret: RuleSet; switch (params.type) { case RuleSetType.CATEGORICAL: ret = new CategoricalRuleSet(params as ICategoricalRuleSetParams); break; case RuleSetType.GRADIENT: ret = new GradientRuleSet(params as IGradientRuleSetParams); break; case RuleSetType.GRADIENT_AND_CATEGORICAL: ret = new GradientCategoricalRuleSet( params as IGradientAndCategoricalRuleSetParams ); break; case RuleSetType.BAR: ret = new BarRuleSet(params as IBarRuleSetParams); break; case RuleSetType.STACKED_BAR: ret = new StackedBarRuleSet(params as IStackedBarRuleSetParams); break; case RuleSetType.GENE: default: ret = new GeneticAlterationRuleSet( params as IGeneticAlterationRuleSetParams ); break; } return ret; }