diff --git a/src/js/bucketsort.ts b/src/js/bucketsort.ts index 7bbcc55..956982f 100644 --- a/src/js/bucketsort.ts +++ b/src/js/bucketsort.ts @@ -1,4 +1,4 @@ -import {extendArray, sgndiff} from "./utils"; +import {extendArray, fastParseInt10, sgndiff} from "./utils"; const string_type = typeof ""; @@ -107,7 +107,7 @@ export function stringToVector(string:string) { // character is not numeric if (numberStartIncl > -1) { // if we're in a number, then we need to add the number to the vector - vector.push(parseInt(string.substring(numberStartIncl, i), 10)); + vector.push(fastParseInt10(string, numberStartIncl, i)); // and record no longer in a number numberStartIncl = -1; } @@ -117,7 +117,7 @@ export function stringToVector(string:string) { } if (numberStartIncl > -1) { // if we're in a number at the end of the string, add it to vector - vector.push(parseInt(string.substring(numberStartIncl), 10)); + vector.push(fastParseInt10(string, numberStartIncl)); // no need to reset numberStartIncl because the algorithm is done } return vector; @@ -202,8 +202,8 @@ export function bucketSortHelper( key = vector[vector_index]; if (!(key in buckets)) { keys.push(key); + buckets[key] = []; } - buckets[key] = buckets[key] || []; buckets[key].push(array[i]); } else { // if the vector has no entry at this index, sort earlier, in line w string sorting convention of shorter strings first diff --git a/src/js/extractrgba.ts b/src/js/extractrgba.ts index 1a2768f..e180215 100644 --- a/src/js/extractrgba.ts +++ b/src/js/extractrgba.ts @@ -1,18 +1,40 @@ -export default function extractrgba(str:string) { - let ret = [0, 0, 0, 1]; +import { fastParseInt16 } from "./utils"; +import {RGBAColor} from "./oncoprintruleset"; + +export default function extractrgba(str:string):RGBAColor { if (str[0] === "#") { // hex, convert to rgba - const r = parseInt(str[1] + str[2], 16); - const g = parseInt(str[3] + str[4], 16); - const b = parseInt(str[5] + str[6], 16); - str = 'rgba('+r+','+g+','+b+',1)'; + return hexToRGBA(str); } const match = str.match(/^[\s]*rgba\([\s]*([0-9.]+)[\s]*,[\s]*([0-9.]+)[\s]*,[\s]*([0-9.]+)[\s]*,[\s]*([0-9.]+)[\s]*\)[\s]*$/); if (match && match.length === 5) { - ret = [parseFloat(match[1]) / 255, + return [parseFloat(match[1]) / 255, parseFloat(match[2]) / 255, parseFloat(match[3]) / 255, parseFloat(match[4])]; } - return ret; + throw `could not extract rgba from ${str}`; }; + +export function hexToRGBA(str:string):RGBAColor { + const r = fastParseInt16(str[1] + str[2]); + const g = fastParseInt16(str[3] + str[4]); + const b = fastParseInt16(str[5] + str[6]); + return [r,g,b,1]; +} + +export function rgbaToHex(rgba:RGBAColor):string { + let hexR = rgba[0].toString(16); + let hexG = rgba[1].toString(16); + let hexB = rgba[2].toString(16); + if (hexR.length === 1) { + hexR = '0' + hexR; + } + if (hexG.length === 1) { + hexG = '0' + hexG; + } + if (hexB.length === 1) { + hexB = '0' + hexB; + } + return `#${hexR}${hexG}${hexB}`; +} \ No newline at end of file diff --git a/src/js/makesvgelement.ts b/src/js/makesvgelement.ts index 5241e57..e13bb30 100644 --- a/src/js/makesvgelement.ts +++ b/src/js/makesvgelement.ts @@ -1,7 +1,7 @@ export default function makesvgelement(tag:string, attrs:any) { const el = document.createElementNS('http://www.w3.org/2000/svg', tag); for (const k in attrs) { - if (attrs.hasOwnProperty(k)) { + if (k in attrs) { el.setAttribute(k, attrs[k]); } } diff --git a/src/js/oncoprint.ts b/src/js/oncoprint.ts index a42fa56..325ebf5 100644 --- a/src/js/oncoprint.ts +++ b/src/js/oncoprint.ts @@ -90,7 +90,6 @@ export default class Oncoprint { public label_view: OncoprintLabelView; public legend_view: OncoprintLegendView; - private keep_sorted:boolean; private keep_horz_zoomed_to_fit:boolean; private keep_horz_zoomed_to_fit_ids:ColumnId[]; private pending_resize_and_organize:boolean; @@ -341,8 +340,6 @@ export default class Oncoprint { this.legend_view = new OncoprintLegendView($legend_div, 10, 20); - this.keep_sorted = false; - this.keep_horz_zoomed_to_fit = false; this.keep_horz_zoomed_to_fit_ids = []; @@ -525,7 +522,7 @@ export default class Oncoprint { this.track_info_view.moveTrack(this.model, this.getCellViewHeight); this.minimap_view.moveTrack(this.model, this.cell_view); - if (this.keep_sorted && this.model.isSortAffected([target_track, new_previous_track], "track")) { + if (this.model.keep_sorted && this.model.isSortAffected([target_track, new_previous_track], "track")) { this.sort(); } @@ -542,7 +539,7 @@ export default class Oncoprint { this.track_options_view.setTrackGroupOrder(this.model); this.track_info_view.setTrackGroupOrder(this.model, this.getCellViewHeight); - if (!dont_sort && this.keep_sorted && this.model.isSortAffected(index, "group")) { + if (!dont_sort && this.model.keep_sorted && this.model.isSortAffected(index, "group")) { this.sort(); } @@ -558,12 +555,13 @@ export default class Oncoprint { this.resizeAndOrganizeAfterTimeout(); } - public keepSorted(keep_sorted:boolean) { + public keepSorted(keep_sorted?:boolean) { if(this.webgl_unavailable || this.destroyed) { return; } - this.keep_sorted = (typeof keep_sorted === 'undefined' ? true : keep_sorted); - if (this.keep_sorted) { + const oldValue = this.model.keep_sorted; + this.model.keep_sorted = typeof keep_sorted === 'undefined' ? true : keep_sorted; + if (this.model.keep_sorted && this.model.keep_sorted !== oldValue) { this.sort(); } } @@ -591,7 +589,7 @@ export default class Oncoprint { this.legend_view.addTracks(this.model); this.minimap_view.addTracks(this.model, this.cell_view); - if (this.keep_sorted && this.model.isSortAffected(track_ids, "track")) { + if (this.model.keep_sorted && this.model.isSortAffected(track_ids, "track")) { this.sort(); } this.resizeAndOrganizeAfterTimeout(); @@ -613,7 +611,7 @@ export default class Oncoprint { this.legend_view.removeTrack(this.model); this.minimap_view.removeTrack(this.model, this.cell_view); - if (this.keep_sorted && this.model.isSortAffected(track_id, "track")) { + if (this.model.keep_sorted && this.model.isSortAffected(track_id, "track")) { this.sort(); } this.resizeAndOrganizeAfterTimeout(); @@ -623,13 +621,9 @@ export default class Oncoprint { if(this.webgl_unavailable || this.destroyed) { return; } - this.keepSorted(false); - this.suppressRendering(); for (let i=0; i 0) { + this.model.rendering_suppressed_depth -= 1; + this.model.rendering_suppressed_depth = Math.max(0, this.model.rendering_suppressed_depth); + if (this.model.rendering_suppressed_depth === 0) { + this.model.releaseRendering(); + this.label_view.releaseRendering(this.model, this.getCellViewHeight); + this.header_view.releaseRendering(this.model); + this.cell_view.releaseRendering(this.model); + this.track_options_view.releaseRendering(this.model, this.getCellViewHeight); + this.track_info_view.releaseRendering(this.model, this.getCellViewHeight); + this.legend_view.releaseRendering(this.model); + this.minimap_view.releaseRendering(this.model, this.cell_view); + this.resizeAndOrganizeAfterTimeout(onComplete); + } } } @@ -1287,7 +1284,7 @@ export default class Oncoprint { const everything_group = svgfactory.group(0,0); root.appendChild(everything_group); - const bgrect = svgfactory.bgrect(10,10,'#ffffff'); + const bgrect = svgfactory.bgrect(10,10,[255,255,255,1]); if (with_background) { everything_group.appendChild(bgrect); diff --git a/src/js/oncoprintlegendrenderer.ts b/src/js/oncoprintlegendrenderer.ts index 62cf224..8a3aa94 100644 --- a/src/js/oncoprintlegendrenderer.ts +++ b/src/js/oncoprintlegendrenderer.ts @@ -164,7 +164,10 @@ export default class OncoprintLegendView { const concrete_shapes = rule.apply(config.target, model.getCellWidth(true), this.base_height); if (rule.legend_base_color) { // generate backgrounds - const baseRect = svgfactory.rect(0, 0, model.getCellWidth(true), this.base_height, rule.legend_base_color); + const baseRect = svgfactory.rect(0, 0, model.getCellWidth(true), this.base_height, { + type: "rgba", + value: rule.legend_base_color + }); root.appendChild(baseRect); } // generate shapes @@ -223,7 +226,7 @@ export default class OncoprintLegendView { target_defs.appendChild(gradient); root.appendChild(svgfactory.text(display_range[0].toString(), 0, 0, 12, 'Arial', 'normal')); root.appendChild(svgfactory.text(display_range[1].toString(), 120, 0, 12, 'Arial', 'normal')); - root.appendChild(svgfactory.rect(30,0,60,20,"url(#"+gradient_id+")")); + root.appendChild(svgfactory.rect(30,0,60,20,{type:"gradientId", value: gradient_id})); } return root; } diff --git a/src/js/oncoprintmodel.ts b/src/js/oncoprintmodel.ts index 003639c..309ddea 100644 --- a/src/js/oncoprintmodel.ts +++ b/src/js/oncoprintmodel.ts @@ -6,7 +6,7 @@ import CachedProperty from './CachedProperty'; import {hclusterColumns, hclusterTracks} from './clustering'; import $ from 'jquery'; import * as BucketSort from "./bucketsort"; -import {cloneShallow, doesCellIntersectPixel, ifndef} from "./utils"; +import {cloneShallow, doesCellIntersectPixel, ifndef, z_comparator} from "./utils"; import _ from "lodash"; import {RuleSet, RuleSetParams, RuleWithId} from "./oncoprintruleset"; import {InitParams} from "./oncoprint"; @@ -208,6 +208,7 @@ 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; @@ -828,7 +829,36 @@ export default class OncoprintModel { } }; - public getIdentifiedShapeListList(track_id:TrackId, use_base_size:boolean, sort_by_z:boolean):IdentifiedShapeList[] { + public getTrackUniversalShapes( + track_id:TrackId, + use_base_size:boolean, + sort_by_z:boolean + ):ComputedShapeParams[] { + const universalRule = this.getRuleSet(track_id).getUniversalRule(); + if (!universalRule) { + return []; + } + 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); + const shapes = universalRule.rule.apply( + {}, // a universal rule does not rely on anything specific to the data + width, + height + ) + + if (sort_by_z) { + shapes.sort(z_comparator); + } + + return shapes; + } + + public getSpecificShapesForData( + track_id:TrackId, + use_base_size:boolean, + sort_by_z:boolean + ):IdentifiedShapeList[] { const active_rules = {}; const data = this.getTrackData(track_id); const id_key = this.getTrackDataIdKey(track_id); @@ -840,18 +870,6 @@ export default class OncoprintModel { this.setTrackActiveRules(track_id, active_rules); - - function z_comparator(shapeA:ComputedShapeParams, shapeB:ComputedShapeParams) { - const zA = shapeA.z; - const zB = shapeB.z; - if (zA < zB) { - return -1; - } else if (zA > zB) { - return 1; - } else { - return 0; - } - } return shapes.map(function(shape_list:ComputedShapeParams[], index:number) { if (sort_by_z) { shape_list.sort(z_comparator); @@ -861,12 +879,28 @@ export default class OncoprintModel { shape_list: shape_list }; }); + + /* + return shapes.reduce(function(ret:IdentifiedShapeList[], shape_list:ComputedShapeParams[], index:number) { + if (shape_list.length > 0) { + // only add entry for nonempty shape list + if (sort_by_z) { + shape_list.sort(z_comparator); + } + ret.push({ + id: data[index][id_key], + shape_list: shape_list + }) + } + return ret; + }, []); + */ } 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].getRulesWithId().filter(function(rule_with_id:RuleWithId) { + return this.rule_sets[rule_set_id].getSpecificRulesForDatum().filter(function(rule_with_id:RuleWithId) { return !!rule_set_active_rules[rule_with_id.id]; }); } else { @@ -1106,11 +1140,16 @@ export default class OncoprintModel { return this.track_groups; } - public addTracks(params_list:LibraryTrackSpec[]) { + public async addTracks(params_list:LibraryTrackSpec[]) { for (let i = 0; i < params_list.length; i++) { const params = params_list[i]; this.addTrack(params); } + if (this.rendering_suppressed_depth === 0) { + if (this.keep_sorted) { + await this.sort(); + } + } this.track_tops.update(); } @@ -1208,8 +1247,17 @@ export default class OncoprintModel { this.track_id_to_datum.update(this, track_id); this.track_present_ids.update(this, track_id); this.precomputed_comparator.update(this, track_id); + } - this.setIdOrder(Object.keys(this.present_ids.get())); + public getAllIds() { + return Object.keys(this.present_ids.get()); + } + + public async releaseRendering() { + if (this.keep_sorted) { + await this.sort(); + } + this.track_tops.update(); } private ensureTrackGroupExists(index:TrackGroupIndex) { @@ -1882,7 +1930,7 @@ export default class OncoprintModel { return mandatory_values.concat(preferred_values); } - const ids_with_vectors = this.getIdOrder(true).map(function(id) { + const ids_with_vectors = this.getAllIds().map(function(id) { return { id: id, vector: getVector(id) diff --git a/src/js/oncoprintruleset.ts b/src/js/oncoprintruleset.ts index f4ed917..312ad9f 100644 --- a/src/js/oncoprintruleset.ts +++ b/src/js/oncoprintruleset.ts @@ -25,6 +25,7 @@ import binarysearch from "./binarysearch"; import {Omit, cloneShallow, ifndef, objectValues, shallowExtend} from "./utils"; import {ActiveRules, ColumnProp, Datum, RuleSetId} from "./oncoprintmodel"; import _ from "lodash"; +import extractrgba, {hexToRGBA, rgbaToHex} from "./extractrgba"; export type RuleSetParams = ILinearInterpRuleSetParams | ICategoricalRuleSetParams | IGradientRuleSetParams | @@ -36,7 +37,7 @@ export type RuleSetParams = ILinearInterpRuleSetParams | ICategoricalRuleSetPara interface IGeneralRuleSetParams { type?:RuleSetType; legend_label?: string; - legend_base_color?: 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') @@ -53,7 +54,8 @@ interface ILinearInterpRuleSetParams extends IGeneralRuleSetParams { export interface ICategoricalRuleSetParams extends IGeneralRuleSetParams { type: RuleSetType.CATEGORICAL; category_key: string; // key into data which gives category - category_to_color?: {[category:string]:string}; + category_to_color?: {[category:string]:RGBAColor}; + universal_rule_categories?: {[category:string]:any}; } export interface IGradientRuleSetParams extends ILinearInterpRuleSetParams { @@ -62,7 +64,7 @@ export interface IGradientRuleSetParams extends ILinearInterpRuleSetParams { 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?: string; + null_color?: RGBAColor; null_legend_label?:string; } @@ -72,30 +74,30 @@ export interface IGradientRuleSetParams extends ILinearInterpRuleSetParams { export interface IGradientAndCategoricalRuleSetParams extends IGeneralRuleSetParams { type: RuleSetType.GRADIENT_AND_CATEGORICAL; // either `colormap_name` or `colors` needs to be present - colors?: [number, number, number, number][]; // [r,g,b,a][] + colors?: RGBAColor[]; colormap_name?: string; // name of a colormap found in src/js/heatmapcolors.js value_stop_points: number[]; - null_color?: string; + 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]:string}; + category_to_color?: {[category:string]:RGBAColor}; } export interface IBarRuleSetParams extends ILinearInterpRuleSetParams { type: RuleSetType.BAR; - fill?: string; - negative_fill?: string; + fill?: RGBAColor; + negative_fill?: RGBAColor; } export interface IStackedBarRuleSetParams extends IGeneralRuleSetParams { type: RuleSetType.STACKED_BAR; value_key: string; categories: string[]; - fills?: string[]; + fills?: RGBAColor[]; } export interface IGeneticAlterationRuleSetParams extends IGeneralRuleSetParams { @@ -128,7 +130,7 @@ type RuleParams = { exclude_from_legend?:boolean; legend_config?:RuleLegendConfig; legend_order?:number; - legend_base_color?:string; + legend_base_color?:RGBAColor; }; type RuleLegendConfig = @@ -137,14 +139,14 @@ type RuleLegendConfig = type: "number", range:[number, number], range_type:LinearInterpRangeType, - positive_color:string, - negative_color:string, + positive_color:RGBAColor, + negative_color:RGBAColor, interpFn:(val:number)=>number } | // range: [lower, upper] { type: "gradient", range:[number,number], - colorFn:(val:number)=>string + colorFn:(val:number)=>RGBAColor }; @@ -190,7 +192,7 @@ function makeUniqueColorGetter(init_used_colors:string[]) { "#651067", "#329262", "#5574a6", "#3b3eac", "#b77322", "#16d620", "#b91383", "#f4359e", "#9c5935", "#a9c413", "#2a778d", "#668d1c", - "#bea413", "#0c5922", "#743411"]; // Source: D3 + "#bea413", "#0c5922", "#743411"];; // Source: D3 let index = 0; const used_colors:{[color:string]:boolean} = {}; for (let i=0; i) { /* params: @@ -323,7 +326,6 @@ export class RuleSet { this.exclude_from_legend = params.exclude_from_legend; this.active_rule_ids = {}; this.rules_with_id = []; - } public getLegendLabel() { @@ -349,6 +351,10 @@ export class RuleSet { 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++) { @@ -399,10 +405,14 @@ export class RuleSet { return shapes; } - public getRulesWithId(datum?:Datum):RuleWithId[] { + public getSpecificRulesForDatum(datum?:Datum):RuleWithId[] { throw "Not implemented on base class"; } + public getUniversalRule() { + return this.universal_rule; + } + public apply(data:Datum[], cell_width:number, cell_height:number, out_active_rules?:ActiveRules|undefined, data_id_key?:string&keyof Datum, important_ids?:ColumnProp) { // 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 @@ -411,7 +421,7 @@ export class RuleSet { for (var i = 0; i < data.length; i++) { var datum = data[i]; var should_mark_active = !important_ids || !!important_ids[datum[data_id_key!]]; - var rules = this.getRulesWithId(datum); + var 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; @@ -419,6 +429,10 @@ export class RuleSet { } ret.push(this.applyRulesToDatum(rules, data[i], cell_width, cell_height)); } + // mark universal rule as active + if (this.getUniversalRule()) { + out_active_rules[this.getUniversalRule().id] = true; + } return ret; } } @@ -426,17 +440,15 @@ export class RuleSet { 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 universal_rules:RuleWithId[] = []; private rule_id_to_conditions:{[ruleId:number]:{ key:string, value:string }[] } = {}; - public getRulesWithId(datum?:Datum) { + public getSpecificRulesForDatum(datum?:Datum) { if (typeof datum === 'undefined') { return this.rules_with_id; } let ret:RuleWithId[] = []; - ret = ret.concat(this.universal_rules); for (var key in datum) { - if (typeof datum[key] !== 'undefined' && datum.hasOwnProperty(key)) { + 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); @@ -452,7 +464,7 @@ class LookupRuleSet extends RuleSet { private indexRuleForLookup(condition_key:string, condition_value:string, rule_with_id:RuleWithId) { if (condition_key === null) { - this.universal_rules.push(rule_with_id); + this.setUniversalRule(rule_with_id); } else { if (condition_value === null) { this.lookup_map_by_key[condition_key] = rule_with_id; @@ -483,16 +495,8 @@ class LookupRuleSet extends RuleSet { while (this.rule_id_to_conditions[rule_id].length > 0) { var condition = this.rule_id_to_conditions[rule_id].pop(); if (condition.key === null) { - var index = -1; - for (var i = 0; i < this.universal_rules.length; i++) { - if (this.universal_rules[i].id === rule_id) { - index = i; - break; - } - } - if (index > -1) { - this.universal_rules.splice(index, 1); - } + // universal rule + this.universal_rule = undefined; } else { if (condition.value === null) { delete this.lookup_map_by_key[condition.key]; @@ -526,7 +530,7 @@ class ConditionRuleSet extends RuleSet { } } - public getRulesWithId(datum?:Datum) { + public getSpecificRulesForDatum(datum?:Datum) { if (typeof datum === 'undefined') { return this.rules_with_id; } @@ -553,8 +557,9 @@ class ConditionRuleSet extends RuleSet { class CategoricalRuleSet extends LookupRuleSet { public readonly category_key:string; - private readonly category_to_color:{[category:string]:string}; - private readonly getUnusedColor:(color?:string)=>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, omitNArule?:boolean) { super(params); if (!omitNArule) { @@ -568,16 +573,17 @@ class CategoricalRuleSet extends LookupRuleSet { } 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(colorToHex)); + 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(color); + this.getUnusedColor(rgbaToHex(color)); } } - private addCategoryRule(category:string, color:string) { + private addCategoryRule(category:string, color:RGBAColor) { const legend_rule_target:any = {}; legend_rule_target[this.category_key] = category; const rule_params:RuleParams = { @@ -589,7 +595,12 @@ class CategoricalRuleSet extends LookupRuleSet { exclude_from_legend: false, legend_config: {'type': 'rule', 'target': legend_rule_target} }; - this.addRule(this.category_key, category, rule_params); + 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 apply(data:Datum, cell_width:number, cell_height:number, out_active_rules:ActiveRules|undefined, data_id_key:string&keyof Datum, important_ids?:ColumnProp) { @@ -599,7 +610,7 @@ class CategoricalRuleSet extends LookupRuleSet { continue; } const category = data[i][this.category_key]; - if (!this.category_to_color.hasOwnProperty(category)) { + if (!(category in this.category_to_color)) { const color = this.getUnusedColor(); this.category_to_color[category] = color; @@ -727,7 +738,7 @@ class LinearInterpRuleSet extends ConditionRuleSet { class GradientRuleSet extends LinearInterpRuleSet { private colors:RGBAColor[] = []; private value_stop_points: number[]; - private null_color?:string; + private null_color?:RGBAColor; private gradient_rule:RuleId; constructor(params:Omit) { @@ -742,7 +753,7 @@ class GradientRuleSet extends LinearInterpRuleSet { } this.value_stop_points = params.value_stop_points; - this.null_color = params.null_color || "rgba(211,211,211,1)"; + this.null_color = params.null_color || [211,211,211,1]; var self = this; var value_key = this.value_key; @@ -759,7 +770,7 @@ class GradientRuleSet extends LinearInterpRuleSet { }); } - static linInterpColors(t:number, begin_color:RGBAColor, end_color:RGBAColor) { + 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), @@ -777,21 +788,21 @@ class GradientRuleSet extends LinearInterpRuleSet { } else { stop_points = intRange(colors.length).map(function(x) { return x/(colors.length -1); }); } - return function(t:number) { + 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 "rgba(0,0,0,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 "rgba(" + colors[end_interval_index].join(",") + ")"; + 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 "rgba(" + GradientRuleSet.linInterpColors(interval_t, begin_color, end_color).join(",") + ")"; + return GradientRuleSet.linInterpColors(interval_t, begin_color, end_color); } }; @@ -815,7 +826,7 @@ class GradientRuleSet extends LinearInterpRuleSet { type: 'rectangle', fill: function(d) { var t = interpFn(d[value_key]); - return colorFn(t); + return colorFn(t) as RGBAColor; } }], exclude_from_legend: false, @@ -826,14 +837,14 @@ class GradientRuleSet extends LinearInterpRuleSet { } class BarRuleSet extends LinearInterpRuleSet { - private fill:string; - private negative_fill:string; + private fill:RGBAColor; + private negative_fill:RGBAColor; private bar_rule?:RuleId; constructor(params:IBarRuleSetParams) { super(params); - this.fill = params.fill || 'rgba(0,128,0,1)'; // green - this.negative_fill = params.negative_fill || 'rgba(255,0,0,1)'; //red + this.fill = params.fill || [0,128,0,1]; // green + this.negative_fill = params.negative_fill || [255,0,0,1]; //red } protected updateLinearRules() { @@ -917,7 +928,7 @@ class StackedBarRuleSet extends ConditionRuleSet { const value_key = params.value_key; const fills = params.fills || []; const categories = params.categories || []; - const getUnusedColor = makeUniqueColorGetter(fills); + const getUnusedColor = makeUniqueColorGetter(fills.map(rgbaToHex)); // Initialize with default values while (fills.length < categories.length) { @@ -995,7 +1006,7 @@ export class GeneticAlterationRuleSet extends LookupRuleSet { { shapes: ruleParams.shapes, legend_config: {'type': 'rule' as 'rule', 'target': legend_rule_target}, - legend_base_color: ifndef(this.legend_base_color, "") + legend_base_color: ifndef(this.legend_base_color, [255,255,255,1]) } ) ); @@ -1019,7 +1030,7 @@ export class GeneticAlterationRuleSet extends LookupRuleSet { export class Rule { private shapes:Shape[]; public legend_label:string; - public legend_base_color?:string; + public legend_base_color?:RGBAColor; public exclude_from_legend?:boolean; private legend_config?:RuleLegendConfig; public legend_order?:number; @@ -1093,9 +1104,9 @@ class GradientCategoricalRuleSet extends RuleSet { } // RuleSet API - public getRulesWithId(datum?:Datum) { - const categoricalRules = this.categoricalRuleSet.getRulesWithId(datum); - const gradientRules = this.gradientRuleSet.getRulesWithId(datum); + public getSpecificRulesForDatum(datum?:Datum) { + const categoricalRules = this.categoricalRuleSet.getSpecificRulesForDatum(datum); + const gradientRules = this.gradientRuleSet.getSpecificRulesForDatum(datum); const rules = categoricalRules.concat(gradientRules); return rules; } diff --git a/src/js/oncoprintshape.ts b/src/js/oncoprintshape.ts index 6e39912..a5a434a 100644 --- a/src/js/oncoprintshape.ts +++ b/src/js/oncoprintshape.ts @@ -1,12 +1,14 @@ import {Datum} from "./oncoprintmodel"; +import {RGBAColor} from "./oncoprintruleset"; -type StringParameter = "stroke" | "fill" | "type"; +type StringParameter = "type"; type PercentNumberParameter = "width" | "height" | "x" | "y" | "x1" | "x2" | "x3" | "y1" | "y2" | "y3"; type PlainNumberParameter = "z" | "stroke-width" | "stroke-opacity"; +type RGBAParameter = "stroke" | "fill"; type NumberParameter = PercentNumberParameter | PlainNumberParameter; -type Parameter = StringParameter | NumberParameter; +type Parameter = StringParameter | NumberParameter | RGBAParameter; -const default_parameter_values:{[x in StringParameter]?:string} & {[x in NumberParameter]?:number} = { +const default_parameter_values:{[x in StringParameter]?:string} & {[x in NumberParameter]?:number} & {[x in RGBAParameter]?:RGBAColor} = { 'width': 100, 'height': 100, 'x': 0, @@ -18,8 +20,8 @@ const default_parameter_values:{[x in StringParameter]?:string} & {[x in NumberP 'y1': 0, 'y2': 0, 'y3': 0, - 'stroke': 'rgba(0,0,0,0)', - 'fill': 'rgba(23,23,23,1)', + 'stroke': [0,0,0,0], + 'fill': [23,23,23,1], 'stroke-width': 0, 'stroke-opacity': 0 }; @@ -43,16 +45,23 @@ const hash_parameter_order:Parameter[] = [ type StringParamFunction = (d:Datum)=>string; type NumberParamFunction = (d:Datum)=>number; -type ParamFunction = StringParamFunction | NumberParamFunction; +type RGBAParamFunction = (d:Datum)=>RGBAColor; +type ParamFunction = StringParamFunction | NumberParamFunction | RGBAParamFunction; + +export type ShapeParams = + {[x in StringParameter]?:string|StringParamFunction} & + {[x in NumberParameter]?:number|NumberParamFunction} & + {[x in RGBAParameter]?:RGBAColor|RGBAParamFunction }; -export type ShapeParams = {[x in StringParameter]?:string|StringParamFunction} & {[x in NumberParameter]?:number|NumberParamFunction}; type ShapeParamsWithType = { [x in StringParameter]?:({ type:"function", value:StringParamFunction} | {type:"value", value:string}) } & { [x in NumberParameter]?:({ type:"function", value:NumberParamFunction} | {type:"value", value:number}) +} & { + [x in RGBAParameter]?:({ type:"function", value:RGBAParamFunction} | {type:"value", value:RGBAColor}) }; -export type ComputedShapeParams = {[x in StringParameter]?:string} & {[x in NumberParameter]?:number}; +export type ComputedShapeParams = {[x in StringParameter]?:string} & {[x in NumberParameter]?:number} & {[x in RGBAParameter]?:RGBAColor}; function isPercentParam(param_name:string):param_name is PercentNumberParameter { return param_name in percent_parameter_name_to_dimension_index; @@ -157,7 +166,9 @@ export class Shape { } type SpecificComputedShapeParams = - {[x in ShapeParamType & StringParameter]:string} & {[x in ShapeParamType & NumberParameter]:number}; + {[x in ShapeParamType & StringParameter]:string} & + {[x in ShapeParamType & NumberParameter]:number} & + {[x in ShapeParamType & RGBAParameter]:RGBAColor}; type RectangleParameter = "width" | "height" | "x" | "y" | "z" | "stroke" | "stroke-width" | "fill"; export type ComputedRectangleParams = SpecificComputedShapeParams; diff --git a/src/js/oncoprintshapetosvg.ts b/src/js/oncoprintshapetosvg.ts index d930513..4019f1a 100644 --- a/src/js/oncoprintshapetosvg.ts +++ b/src/js/oncoprintshapetosvg.ts @@ -6,6 +6,7 @@ import { ComputedShapeParams, ComputedTriangleParams } from "./oncoprintshape"; +import {rgbString} from "./utils"; function extractColor(str:string) { if (str.indexOf("rgb(") > -1) { @@ -22,61 +23,54 @@ function extractColor(str:string) { } function rectangleToSVG(params:ComputedRectangleParams, offset_x:number, offset_y:number) { - var stroke_color = extractColor(params.stroke); - var fill_color = extractColor(params.fill); return makeSVGElement('rect', { width: params.width, height: params.height, x: params.x + offset_x, y: params.y + offset_y, - stroke: stroke_color.rgb, - 'stroke-opacity': stroke_color.opacity, + stroke: rgbString(params.stroke), + 'stroke-opacity': params.stroke[3], 'stroke-width': params['stroke-width'], - fill: fill_color.rgb, - 'fill-opacity': fill_color.opacity + fill: rgbString(params.fill), + 'fill-opacity': params.fill[3] }); } function triangleToSVG(params:ComputedTriangleParams, offset_x:number, offset_y:number) { - var stroke_color = extractColor(params.stroke); - var fill_color = extractColor(params.fill); return makeSVGElement('polygon', { points: [[params.x1 + offset_x, params.y1 + offset_y], [params.x2 + offset_x, params.y2 + offset_y], [params.x3 + offset_x, params.y3 + offset_y]].map(function (a) { return a[0] + ',' + a[1]; }).join(' '), - stroke: stroke_color.rgb, - 'stroke-opacity': stroke_color.opacity, + stroke: rgbString(params.stroke), + 'stroke-opacity': params.stroke[3], 'stroke-width': params['stroke-width'], - fill: fill_color.rgb, - 'fill-opacity': fill_color.opacity + fill: rgbString(params.fill), + 'fill-opacity': params.fill[3] }); } function ellipseToSVG(params:ComputedEllipseParams, offset_x:number, offset_y:number) { - var stroke_color = extractColor(params.stroke); - var fill_color = extractColor(params.fill); return makeSVGElement('ellipse', { rx: params.width / 2, height: params.height / 2, cx: params.x + offset_x, cy: params.y + offset_y, - stroke: stroke_color.rgb, - 'stroke-opacity': stroke_color.opacity, + stroke: rgbString(params.stroke), + 'stroke-opacity': params.stroke[3], 'stroke-width': params['stroke-width'], - fill: fill_color.rgb, - 'fill-opacity': fill_color.opacity + fill: rgbString(params.fill), + 'fill-opacity': params.fill[3] }); } function lineToSVG(params:ComputedLineParams, offset_x:number, offset_y:number) { - var stroke_color = extractColor(params.stroke); return makeSVGElement('line', { x1: params.x1 + offset_x, y1: params.y1 + offset_y, x2: params.x2 + offset_x, y2: params.y2 + offset_y, - stroke: stroke_color.rgb, - 'stroke-opacity': stroke_color.opacity, + stroke: rgbString(params.stroke), + 'stroke-opacity': params.stroke[3], 'stroke-width': params['stroke-width'], }); } diff --git a/src/js/oncoprintshapetovertexes.ts b/src/js/oncoprintshapetovertexes.ts index 48631db..db87a4c 100644 --- a/src/js/oncoprintshapetovertexes.ts +++ b/src/js/oncoprintshapetovertexes.ts @@ -1,24 +1,15 @@ -import {ComputedShapeParams} from "./oncoprintshape"; +import {ComputedShapeParams, Rectangle} from "./oncoprintshape"; +import {RGBAColor} from "./oncoprintruleset"; const halfsqrt2 = Math.sqrt(2) / 2; -function extractRGBA(str:string):[number,number,number,number] { - let ret:[number,number,number,number] = [0, 0, 0, 1]; - if (str[0] === "#") { - // hex, convert to rgba - const r = parseInt(str[1] + str[2], 16); - const g = parseInt(str[3] + str[4], 16); - const b = parseInt(str[5] + str[6], 16); - str = 'rgba('+r+','+g+','+b+',1)'; - } - const match = str.match(/^[\s]*rgba\([\s]*([0-9]+)[\s]*,[\s]*([0-9]+)[\s]*,[\s]*([0-9]+)[\s]*,[\s]*([0-9.]+)[\s]*\)[\s]*$/); - if (match && match.length === 5) { - ret = [parseFloat(match[1]) / 255, - parseFloat(match[2]) / 255, - parseFloat(match[3]) / 255, - parseFloat(match[4])]; - } - return ret; +function normalizeRGBA(color:RGBAColor):[number, number, number, number] { + return [ + color[0], + color[1], + color[2], + color[3]*255 + ] } type AddVertexCallback = (vertex:[number,number,number], color:[number,number,number,number])=>void; @@ -27,7 +18,7 @@ function rectangleToVertexes(params:ComputedShapeParams, z_index:number, addVert const x = params.x, y = params.y, height = params.height, width = params.width; // Fill - const fill_rgba = extractRGBA(params.fill); + const fill_rgba = normalizeRGBA(params.fill); addVertex([x,y,z_index], fill_rgba); addVertex([x+width, y, z_index], fill_rgba); addVertex([x+width, y+height, z_index], fill_rgba); @@ -40,7 +31,7 @@ function rectangleToVertexes(params:ComputedShapeParams, z_index:number, addVert const stroke_width = params['stroke-width']; if (stroke_width > 0) { // left side - const stroke_rgba = extractRGBA(params.stroke); + const stroke_rgba = normalizeRGBA(params.stroke); addVertex([x, y, z_index], stroke_rgba); addVertex([x + stroke_width, y, z_index], stroke_rgba); addVertex([x + stroke_width, y + height, z_index], stroke_rgba); @@ -79,7 +70,7 @@ function rectangleToVertexes(params:ComputedShapeParams, z_index:number, addVert } function triangleToVertexes(params:ComputedShapeParams, z_index:number, addVertex:AddVertexCallback) { - const fill_rgba = extractRGBA(params.fill); + const fill_rgba = normalizeRGBA(params.fill); addVertex([params.x1, params.y1, z_index], fill_rgba); addVertex([params.x2, params.y2, z_index], fill_rgba); addVertex([params.x3, params.y3, z_index], fill_rgba); @@ -90,7 +81,7 @@ function ellipseToVertexes(params:ComputedShapeParams, z_index:number, addVertex const horzrad =params.width / 2; const vertrad =params.height / 2; - const fill_rgba = extractRGBA(params.fill); + const fill_rgba = normalizeRGBA(params.fill); addVertex([center.x, center.y, z_index], fill_rgba); addVertex([center.x + horzrad, center.y, z_index], fill_rgba); addVertex([center.x + halfsqrt2 * horzrad, center.y + halfsqrt2 * vertrad, z_index], fill_rgba); @@ -155,7 +146,7 @@ function lineToVertexes(params:ComputedShapeParams, z_index:number, addVertex:Ad const C = [x2 + direction1[0], y2 + direction1[1]]; const D = [x2 + direction2[0], y2 + direction2[1]]; - const stroke_rgba = extractRGBA(params.stroke); + const stroke_rgba = normalizeRGBA(params.stroke); addVertex([A[0], A[1], z_index], stroke_rgba); addVertex([B[0], B[1], z_index], stroke_rgba); addVertex([C[0], C[1], z_index], stroke_rgba); @@ -180,4 +171,27 @@ export default function(oncoprint_shape_computed_params:ComputedShapeParams, z_i } else if (type === "line") { return lineToVertexes(oncoprint_shape_computed_params, z_index, addVertex); } +} + +export function getNumWebGLVertexes(shape:ComputedShapeParams) { + let ret:number; + switch (shape.type) { + case 'rectangle': + if (shape['stroke-width'] > 0) { + ret = 30; + } else { + ret = 6; + } + break; + case 'triangle': + ret = 3; + break; + case 'ellipse': + ret = 24; + break; + case 'line': + ret = 6; + break; + } + return ret; } \ No newline at end of file diff --git a/src/js/oncoprintwebglcellview.ts b/src/js/oncoprintwebglcellview.ts index 9715f03..1bb0528 100644 --- a/src/js/oncoprintwebglcellview.ts +++ b/src/js/oncoprintwebglcellview.ts @@ -1,9 +1,9 @@ import gl_matrix from 'gl-matrix'; import svgfactory from './svgfactory'; import makeSvgElement from './makesvgelement'; -import shapeToVertexes from './oncoprintshapetovertexes'; +import shapeToVertexes, {getNumWebGLVertexes} from './oncoprintshapetovertexes'; import CachedProperty from './CachedProperty'; -import {Shape} from './oncoprintshape'; +import {ComputedShapeParams, Shape} from './oncoprintshape'; import $ from 'jquery'; import OncoprintModel, { ColumnId, ColumnLabel, @@ -18,7 +18,10 @@ import {arrayFindIndex, ifndef, sgndiff} from "./utils"; import MouseUpEvent = JQuery.MouseUpEvent; import MouseMoveEvent = JQuery.MouseMoveEvent; import {CellClickCallback, CellMouseOverCallback} from "./oncoprint"; -import {OMath} from "./polyfill"; +import { + getFragmentShaderSource, getVertexShaderSource +} from "./shaders"; +import _ from 'lodash'; type ColorBankIndex = number; // index into vertex bank (e.g. 0, 4, 8, ...) type ColorBank = number[]; // flat list of color: [c0,c0,c0,c0,v1,v1,v1,c1,c1,c1,c1,...] @@ -58,6 +61,12 @@ export type OncoprintTrackBuffer = WebGLBuffer & { numItems:number; }; // TODO: handle this differently, considered an anti-pattern https://webglfundamentals.org/webgl/lessons/webgl-anti-patterns.html +export type OncoprintVertexTrackBuffer = OncoprintTrackBuffer & { + // the universal shapes vertexes start at index itemSize*numItems, and go on for itemSize*universalShapesNumItems indexes + specificShapesNumItems:number; + universalShapesNumItems:number; +} + const COLUMN_LABEL_ANGLE = 65; const COLUMN_LABEL_MARGIN = 30; @@ -74,6 +83,7 @@ export default class OncoprintWebGLCellView { private mouseMoveHandler:(evt:MouseMoveEvent)=>void; private ctx:OncoprintWebGLContext|null; + private ext:ANGLE_instanced_arrays | null; private overlay_ctx:CanvasRenderingContext2D|null; private column_label_ctx:CanvasRenderingContext2D|null; private mvMatrix:any; @@ -86,12 +96,25 @@ export default class OncoprintWebGLCellView { private maximum_column_label_height = 0; private rendering_suppressed = false; - private identified_shape_list_list:TrackProp = {}; - public vertex_data:TrackProp<{ pos_array:number[], col_array:ColorBankIndex[], col_bank:ColorBank}> = {}; - public vertex_column_array:TrackProp = {}; - private vertex_position_buffer:TrackProp = {}; - private vertex_color_buffer:TrackProp = {}; + private specific_shapes:TrackProp = {}; + private universal_shapes:TrackProp = {}; + public vertex_data:TrackProp<{ + pos_array:Float32Array; + col_array:Float32Array;//ColorBankIndex[], + col_bank:ColorBank, + universal_shapes_start_index:number + }> = {}; + public vertex_column_array:TrackProp = {}; // ColumnIdIndex[] + private vertex_position_buffer:TrackProp = {}; + private vertex_color_buffer:TrackProp = {}; private vertex_column_buffer:TrackProp = {}; + private simple_count_buffer:OncoprintTrackBuffer | null = null; + private is_buffer_empty:TrackProp<{ + position:boolean; + color:boolean; + column:boolean; + color_texture:boolean; + }> = {}; private color_texture:TrackProp<{texture: WebGLTexture, size:number}> = {}; private id_to_first_vertex_index:TrackProp> = {}; // index of first vertex corresponding to given id for given track, e.g. 0, 3, 6, ... @@ -266,6 +289,7 @@ export default class OncoprintWebGLCellView { parent_node.insertBefore(new_canvas, parent_node.childNodes[0]); // keep on bottom since we need overlays to not be hidden this.$canvas = $(new_canvas); this.ctx = null; + this.ext = null; } private getWebGLCanvasContext() { @@ -381,6 +405,9 @@ export default class OncoprintWebGLCellView { private getWebGLContextAndSetUpMatrices() { this.ctx = this.getWebGLCanvasContext(); + if (this.ctx) { + this.ext = this.ctx.getExtension('ANGLE_instanced_arrays'); + } (function initializeMatrices(self) { const mvMatrix = gl_matrix.mat4.create(); gl_matrix.mat4.lookAt(mvMatrix, [0, 0, 1], [0, 0, 0], [0, 1, 0]); @@ -399,107 +426,8 @@ export default class OncoprintWebGLCellView { private setUpShaders(model:OncoprintModel) { const columnsRightAfterGapsSize = this.getColumnIndexesAfterAGap(model).length; - const vertex_shader_source = ` - precision highp float; - attribute float aPosVertex; - attribute float aColVertex; - attribute float aVertexOncoprintColumn; - - uniform float gapSize; - - uniform float columnsRightAfterGaps[${columnsRightAfterGapsSize}]; // sorted in ascending order - - uniform float columnWidth; - uniform float scrollX; - uniform float zoomX; - uniform float scrollY; - uniform float zoomY; - uniform mat4 uMVMatrix; - uniform mat4 uPMatrix; - uniform float offsetY; - uniform float supersamplingRatio; - uniform float positionBitPackBase; - uniform float texSize; - varying float texCoord; - - vec3 getUnpackedPositionVec3() { - float pos0 = floor(aPosVertex / (positionBitPackBase * positionBitPackBase)); - float pos0Contr = pos0 * positionBitPackBase * positionBitPackBase; - float pos1 = floor((aPosVertex - pos0Contr)/positionBitPackBase); - float pos1Contr = pos1 * positionBitPackBase; - float pos2 = aPosVertex - pos0Contr - pos1Contr; - return vec3(pos0, pos1, pos2); - } - - float getGapOffset() { - // first do binary search to compute the number of gaps before this column, G(c) - // G(c) = the index in columnsRightAfterGaps of the first entry thats greater than c - - int lower_incl = 0; - int upper_excl = ${columnsRightAfterGapsSize}; - int numGaps = 0; - - for (int loopDummyVar = 0; loopDummyVar == 0; loopDummyVar += 0) { - if (lower_incl >= upper_excl) { - break; - } - - int middle = (lower_incl + upper_excl)/2; - if (columnsRightAfterGaps[middle] < aVertexOncoprintColumn) { - // G(c) > middle - lower_incl = middle + 1; - } else if (columnsRightAfterGaps[middle] == aVertexOncoprintColumn) { - // G(c) = middle + 1 - numGaps = middle + 1; - break; - } else { - // columnsRightAfterGaps[middle] > column, so G(c) <= middle - if (middle == 0) { - // 0 <= G(c) <= 0 -> G(c) = 0 - numGaps = 0; - break; - } else if (columnsRightAfterGaps[middle-1] < aVertexOncoprintColumn) { - // G(c) = middle - numGaps = middle; - break; - } else { - // columnsRightAfterGaps[middle-1] >= column, so G(c) <= middle-1 - upper_excl = middle; - } - } - } - - // multiply it by the gap size to get the total offset - return float(numGaps)*gapSize; - } - - void main(void) { - gl_Position = vec4(getUnpackedPositionVec3(), 1.0); - gl_Position[0] += aVertexOncoprintColumn*columnWidth; - gl_Position *= vec4(zoomX, zoomY, 1.0, 1.0); - - // gaps should not be affected by zoom: - gl_Position[0] += getGapOffset(); - - // offsetY is given zoomed: - gl_Position[1] += offsetY; - - gl_Position -= vec4(scrollX, scrollY, 0.0, 0.0); - gl_Position[0] *= supersamplingRatio; - gl_Position[1] *= supersamplingRatio; - gl_Position = uPMatrix * uMVMatrix * gl_Position; - - texCoord = (aColVertex + 0.5) / texSize; - }`; - const fragment_shader_source = ` - precision mediump float; - varying float texCoord; - uniform sampler2D uSampler; - void main(void) { - gl_FragColor = texture2D(uSampler, vec2(texCoord, 0.5)); - }`; - const vertex_shader = this.createShader(vertex_shader_source, 'VERTEX_SHADER'); - const fragment_shader = this.createShader(fragment_shader_source, 'FRAGMENT_SHADER'); + const vertex_shader = this.createShader(getVertexShaderSource(columnsRightAfterGapsSize), 'VERTEX_SHADER'); + const fragment_shader = this.createShader(getFragmentShaderSource(), 'FRAGMENT_SHADER'); const shader_program = this.createShaderProgram(vertex_shader, fragment_shader) as OncoprintShaderProgram; shader_program.vertexPositionAttribute = this.ctx.getAttribLocation(shader_program, 'aPosVertex'); @@ -601,39 +529,67 @@ export default class OncoprintWebGLCellView { if (buffers.position.numItems === 0) { continue; } - const first_index = this.id_to_first_vertex_index[track_id][horz_first_id_in_window]; - const first_index_out = horz_first_id_after_window === null ? buffers.position.numItems : this.id_to_first_vertex_index[track_id][horz_first_id_after_window]; - - this.ctx.useProgram(this.shader_program); - this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffers.position); - this.ctx.vertexAttribPointer(this.shader_program.vertexPositionAttribute, buffers.position.itemSize, this.ctx.FLOAT, false, 0, 0); - this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffers.color); - this.ctx.vertexAttribPointer(this.shader_program.vertexColorAttribute, buffers.color.itemSize, this.ctx.FLOAT, false, 0, 0); - - this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffers.column); - this.ctx.vertexAttribPointer(this.shader_program.vertexOncoprintColumnAttribute, buffers.column.itemSize, this.ctx.FLOAT, false, 0, 0); - - this.ctx.activeTexture(this.ctx.TEXTURE0); - this.ctx.bindTexture(this.ctx.TEXTURE_2D, buffers.color_tex.texture); - this.ctx.uniform1i(this.shader_program.samplerUniform, 0); - this.ctx.uniform1f(this.shader_program.texSizeUniform, buffers.color_tex.size); - - this.ctx.uniform1fv(this.shader_program.columnsRightAfterGapsUniform, this.getColumnIndexesAfterAGap(model)); // need min size of 1 - this.ctx.uniform1f(this.shader_program.gapSizeUniform, model.getGapSize()); - - this.ctx.uniformMatrix4fv(this.shader_program.pMatrixUniform, false, this.pMatrix); - this.ctx.uniformMatrix4fv(this.shader_program.mvMatrixUniform, false, this.mvMatrix); - this.ctx.uniform1f(this.shader_program.columnWidthUniform, model.getCellWidth(true) + model.getCellPadding(true)); - this.ctx.uniform1f(this.shader_program.scrollXUniform, scroll_x); - this.ctx.uniform1f(this.shader_program.scrollYUniform, scroll_y); - this.ctx.uniform1f(this.shader_program.zoomXUniform, zoom_x); - this.ctx.uniform1f(this.shader_program.zoomYUniform, zoom_y); - this.ctx.uniform1f(this.shader_program.offsetYUniform, cell_top); - this.ctx.uniform1f(this.shader_program.supersamplingRatioUniform, this.supersampling_ratio); - this.ctx.uniform1f(this.shader_program.positionBitPackBaseUniform, this.position_bit_pack_base); - - this.ctx.drawArrays(this.ctx.TRIANGLES, first_index, first_index_out - first_index); + + for (const forSpecificShapes of [false,true]) { + const shader_program = this.shader_program; + this.ctx.useProgram(shader_program); + + if (forSpecificShapes) { + this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffers.position); + this.ctx.vertexAttribPointer(shader_program.vertexPositionAttribute, buffers.position.itemSize, this.ctx.FLOAT, false, 0, 0); + this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffers.color); + this.ctx.vertexAttribPointer(shader_program.vertexColorAttribute, buffers.color.itemSize, this.ctx.FLOAT, false, 0, 0); + this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffers.column); + this.ctx.vertexAttribPointer(shader_program.vertexOncoprintColumnAttribute, buffers.column.itemSize, this.ctx.FLOAT, false, 0, 0); + // make sure to set divisor 0, otherwise the track will only use the first item in the column buffer + this.ext.vertexAttribDivisorANGLE(shader_program.vertexOncoprintColumnAttribute, 0); + } else { + // set up for drawArraysInstanced + const universalShapesStart = buffers.position.specificShapesNumItems * buffers.position.itemSize; + this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffers.position); + this.ctx.vertexAttribPointer(shader_program.vertexPositionAttribute, buffers.position.itemSize, this.ctx.FLOAT, false, 0, 4*universalShapesStart); + + this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffers.color); + this.ctx.vertexAttribPointer(shader_program.vertexColorAttribute, buffers.color.itemSize, this.ctx.FLOAT, false, 0, 4*universalShapesStart); + + this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this.simple_count_buffer); + this.ctx.vertexAttribPointer(shader_program.vertexOncoprintColumnAttribute, 1, this.ctx.FLOAT, false, 0, 4*horz_first_id_in_window_index); + this.ext.vertexAttribDivisorANGLE(shader_program.vertexOncoprintColumnAttribute, 1); + } + + this.ctx.activeTexture(this.ctx.TEXTURE0); + this.ctx.bindTexture(this.ctx.TEXTURE_2D, buffers.color_tex.texture); + this.ctx.uniform1i(shader_program.samplerUniform, 0); + this.ctx.uniform1f(shader_program.texSizeUniform, buffers.color_tex.size); + + this.ctx.uniform1fv(shader_program.columnsRightAfterGapsUniform, this.getColumnIndexesAfterAGap(model)); // need min size of 1 + this.ctx.uniform1f(shader_program.gapSizeUniform, model.getGapSize()); + + this.ctx.uniformMatrix4fv(shader_program.pMatrixUniform, false, this.pMatrix); + this.ctx.uniformMatrix4fv(shader_program.mvMatrixUniform, false, this.mvMatrix); + this.ctx.uniform1f(shader_program.columnWidthUniform, model.getCellWidth(true) + model.getCellPadding(true)); + this.ctx.uniform1f(shader_program.scrollXUniform, scroll_x); + this.ctx.uniform1f(shader_program.scrollYUniform, scroll_y); + this.ctx.uniform1f(shader_program.zoomXUniform, zoom_x); + this.ctx.uniform1f(shader_program.zoomYUniform, zoom_y); + this.ctx.uniform1f(shader_program.offsetYUniform, cell_top); + this.ctx.uniform1f(shader_program.supersamplingRatioUniform, this.supersampling_ratio); + this.ctx.uniform1f(shader_program.positionBitPackBaseUniform, this.position_bit_pack_base); + if (forSpecificShapes) { + const first_index = this.id_to_first_vertex_index[track_id][horz_first_id_in_window]; + const first_index_out = horz_first_id_after_window === null ? buffers.position.specificShapesNumItems : this.id_to_first_vertex_index[track_id][horz_first_id_after_window]; + this.ctx.drawArrays(this.ctx.TRIANGLES, first_index, first_index_out - first_index); + } else { + this.ext.drawArraysInstancedANGLE( + this.ctx.TRIANGLES, + 0, + buffers.position.itemSize * buffers.position.universalShapesNumItems, + horz_first_id_after_window_index - horz_first_id_in_window_index + ); + } + } } + this.ctx.flush(); this.renderColumnLabels(model, id_order.slice(horz_first_id_in_window_index, horz_first_id_after_window_index === -1 ? undefined : horz_first_id_after_window_index)); // finally, refresh overlay (highlights) @@ -726,6 +682,18 @@ export default class OncoprintWebGLCellView { return { radius }; } + private ensureSimpleCountBuffer(model:OncoprintModel) { + const numColumns = model.getIdOrder().length; + if (!this.simple_count_buffer || this.simple_count_buffer.numItems !== numColumns) { + const buffer = this.ctx.createBuffer() as OncoprintTrackBuffer; + this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffer); + this.ctx.bufferData(this.ctx.ARRAY_BUFFER, new Float32Array(_.range(0, numColumns)), this.ctx.STATIC_DRAW); + buffer.itemSize = 1; + buffer.numItems = numColumns; + this.simple_count_buffer = buffer; + } + } + private clearTrackPositionAndColorBuffers(model:OncoprintModel, track_id?:TrackId) { let tracks_to_clear; if (typeof track_id === 'undefined') { @@ -734,17 +702,10 @@ export default class OncoprintWebGLCellView { tracks_to_clear = [track_id]; } for (let i=0; iid_to_index[o.id]); // Compute vertex array - const vertex_pos_array:number[] = []; - const vertex_col_array:number[] = []; - const id_to_first_vertex_index:{[columnId:string]:number} = {}; + const num_vertexes = _.sumBy(specific_shapes, (shapeList:IdentifiedShapeList)=>{ + return _.sumBy(shapeList.shape_list, getNumWebGLVertexes); + }) + (universal_shapes ? _.sumBy(universal_shapes, getNumWebGLVertexes) : 0); + const vertex_pos_array = new Float32Array(num_vertexes); + const vertex_col_array = new Float32Array(num_vertexes); + + // original values dont mean anything but its faster to create an object with all the keys + // already in it than to slowly grow it + const id_to_first_vertex_index:{[columnId:string]:number} = _.clone(id_to_index); const color_vertexes:ColorVertex[] = []; const color_bank_index:{[colorHash:string]:ColorBankIndex} = {}; - function hashVector(colorVertex:number[]) { - return colorVertex.join(","); - } - const position_bit_pack_base = this.position_bit_pack_base; function packPos(posVertex:number[]) { // values must be in [0,255] (integer) @@ -872,41 +896,59 @@ export default class OncoprintWebGLCellView { } const vertexifiedShapes:{[shapeHash:string]:{position:number[], color:number[]}} = {}; + let vertex_array_index = 0; + + function addShapeVertexes(_shape:ComputedShapeParams, zindex:number) { + const hash = Shape.hashComputedShape(_shape, zindex); + if (!(hash in vertexifiedShapes)) { + vertexifiedShapes[hash] = {position:[], color:[]}; + const position = vertexifiedShapes[hash].position; + const color = vertexifiedShapes[hash].color; + shapeToVertexes(_shape, zindex, function(pos:PositionVertex, col:ColorVertex) { + pos = pos.map(Math.round) as PositionVertex; + + position.push(packPos(pos)); + + const col_hash = `${col[0]},${col[1]},${col[2]},${col[3]}`; + let col_index = color_bank_index[col_hash]; + if (typeof col_index === "undefined") { + col_index = color_vertexes.length; + color_vertexes.push(col); + color_bank_index[col_hash] = col_index; + } + color.push(col_index); + }); + } + const positionVertexes = vertexifiedShapes[hash].position; + const colorVertexes = vertexifiedShapes[hash].color; + for (let i=0; i