UNPKG

oncoprintjs

Version:

A data visualization for cancer genomic data.

1,320 lines (1,198 loc) 79.3 kB
import gl_matrix from 'gl-matrix'; import svgfactory from './svgfactory'; import makeSvgElement from './makesvgelement'; import shapeToVertexes, { getNumWebGLVertexes, } from './oncoprintshapetovertexes'; import CachedProperty from './CachedProperty'; import { ComputedShapeParams, Shape } from './oncoprintshape'; import $ from 'jquery'; import OncoprintModel, { ColumnId, ColumnLabel, ColumnProp, GAP_MODE_ENUM, IdentifiedShapeList, TrackId, TrackOverlappingCells, TrackProp, } from './oncoprintmodel'; import OncoprintToolTip from './oncoprinttooltip'; import { ifndef, sgndiff } from './utils'; import { CellClickCallback, CellMouseOverCallback } from './oncoprint'; import { getFragmentShaderSource, getVertexShaderSource } from './shaders'; import _ from 'lodash'; import MouseUpEvent = JQuery.MouseUpEvent; import MouseMoveEvent = JQuery.MouseMoveEvent; 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,...] type ColumnIdIndex = number; type PositionVertex = [number, number, number]; type ColorVertex = [number, number, number, number]; type OncoprintGap = { origin_x: number; origin_y: number; data: OncoprintGapConfig; }; export type OncoprintGapConfig = { labelFormatter: () => string; tooltipFormatter: () => string; }; export type OncoprintWebGLContext = WebGLRenderingContext & { viewportWidth: number; viewportHeight: number; }; // TODO: handle this differently, considered an anti-pattern https://webglfundamentals.org/webgl/lessons/webgl-anti-patterns.html export type OncoprintShaderProgram = WebGLProgram & { vertexPositionAttribute: any; vertexColorAttribute: any; vertexOncoprintColumnAttribute: any; gapSizeUniform: WebGLUniformLocation; columnsRightAfterGapsUniform: WebGLUniformLocation; samplerUniform: WebGLUniformLocation; pMatrixUniform: WebGLUniformLocation; mvMatrixUniform: WebGLUniformLocation; columnWidthUniform: WebGLUniformLocation; scrollXUniform: WebGLUniformLocation; scrollYUniform: WebGLUniformLocation; zoomXUniform: WebGLUniformLocation; zoomYUniform: WebGLUniformLocation; offsetYUniform: WebGLUniformLocation; supersamplingRatioUniform: WebGLUniformLocation; positionBitPackBaseUniform: WebGLUniformLocation; texSizeUniform: WebGLUniformLocation; }; // TODO: handle this differently, considered an anti-pattern https://webglfundamentals.org/webgl/lessons/webgl-anti-patterns.html export type OncoprintTrackBuffer = WebGLBuffer & { itemSize: number; 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; const CELL_HIGHLIGHT_STROKE = 'rgba(0,0,0,0.5)'; export default class OncoprintWebGLCellView { public readonly position_bit_pack_base = 128; private readonly supersampling_ratio = 2; private readonly antialias_on_cell_width_thresh = 5; private antialias = true; private dummy_scroll_div_client_size: CachedProperty<{ width: number; height: number; }>; public visible_area_width: number; private mouseMoveHandler: (evt: MouseMoveEvent) => void; private ctx: OncoprintWebGLContext | null; private gap_ctx: CanvasRenderingContext2D | null; private ext: ANGLE_instanced_arrays | null; private overlay_ctx: CanvasRenderingContext2D | null; private column_label_ctx: CanvasRenderingContext2D | null; private mvMatrix: any; private pMatrix: any; private shader_program: OncoprintShaderProgram; private scroll_x: number = 0; private scroll_y: number = 0; private maximum_column_label_width = 0; private maximum_column_label_height = 0; private rendering_suppressed = false; private specific_shapes: TrackProp<IdentifiedShapeList[]> = {}; private universal_shapes: TrackProp<ComputedShapeParams[]> = {}; public vertex_data: TrackProp<{ pos_array: Float32Array; col_array: Float32Array; //ColorBankIndex[], col_bank: ColorBank; universal_shapes_start_index: number; }> = {}; public vertex_column_array: TrackProp<Float32Array> = {}; // ColumnIdIndex[] private vertex_position_buffer: TrackProp<OncoprintVertexTrackBuffer> = {}; private vertex_color_buffer: TrackProp<OncoprintVertexTrackBuffer> = {}; private vertex_column_buffer: TrackProp<OncoprintTrackBuffer> = {}; 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<ColumnProp<number>> = {}; // index of first vertex corresponding to given id for given track, e.g. 0, 3, 6, ... constructor( private $container: JQuery, private $canvas: JQuery<HTMLCanvasElement>, private $overlay_canvas: JQuery<HTMLCanvasElement>, private $gap_canvas: JQuery<HTMLCanvasElement>, private $column_label_canvas: JQuery<HTMLCanvasElement>, private $dummy_scroll_div_contents: JQuery, model: OncoprintModel, private tooltip: OncoprintToolTip, private highlight_area_callback: | undefined | ((left: number, right: number) => void), cell_over_callback: CellMouseOverCallback, cell_click_callback: CellClickCallback ) { this.getWebGLContextAndSetUpMatrices(); this.setUpShaders(model); this.getOverlayContextAndClear(); this.visible_area_width = $canvas[0].width; const self = this; this.tooltip.center = true; this.scroll_x = 0; this.scroll_y = 0; this.dummy_scroll_div_client_size = new CachedProperty( { width: $dummy_scroll_div_contents.parent()[0].clientWidth, height: $dummy_scroll_div_contents.parent()[0].clientHeight, }, function() { return { width: $dummy_scroll_div_contents.parent()[0].clientWidth, height: $dummy_scroll_div_contents.parent()[0].clientHeight, }; } ); this.highlight_area_callback = typeof highlight_area_callback === 'undefined' ? function() {} : highlight_area_callback; // function(left, right) { ... } (function initializeOverlayEvents(self) { let dragging = false; let drag_diff_minimum = 10; let drag_start_x: number; let drag_end_x: number; let last_cell_over: TrackOverlappingCells | null = null; function dragIsValid(drag_start_x: number, drag_end_x: number) { return Math.abs(drag_start_x - drag_end_x) >= drag_diff_minimum; } function executeDragOrClick(mouse_up_evt?: MouseUpEvent) { if (!dragging) { return; } dragging = false; if (!dragIsValid(drag_start_x, drag_end_x)) { if (mouse_up_evt) { // its a click const offset = self.$overlay_canvas.offset(); const mouseX = mouse_up_evt.pageX - offset.left; const mouseY = mouse_up_evt.pageY - offset.top; const overlapping_cells = model.getOverlappingCells( mouseX + self.scroll_x, mouseY + self.scroll_y ); if (overlapping_cells === null) { cell_click_callback(null); } else { cell_click_callback( overlapping_cells.ids[0], overlapping_cells.track ); } } return; } const left = Math.min(drag_start_x, drag_end_x); const right = Math.max(drag_start_x, drag_end_x); self.highlight_area_callback( left + self.scroll_x, right + self.scroll_x ); } function mouseInOverlayCanvas(mouse_x: number, mouse_y: number) { const offset = self.$overlay_canvas.offset(); const width = self.$overlay_canvas.width(); const height = self.$overlay_canvas.height(); return ( mouse_x >= offset.left && mouse_x < width + offset.left && mouse_y >= offset.top && mouse_y < height + offset.top ); } self.mouseMoveHandler = function(evt) { if (!mouseInOverlayCanvas(evt.pageX, evt.pageY)) { self.clearOverlay(); self.highlightHighlightedIds(model); self.highlightHighlightedTracks(model); tooltip.hide(); if (last_cell_over !== null) { last_cell_over = null; cell_over_callback(null); } } }; $(document).on('mousemove', self.mouseMoveHandler); self.$overlay_canvas.on('mousemove', function(evt) { if (self.rendering_suppressed) { return; } self.clearOverlay(); const offset = self.$overlay_canvas.offset(); const mouseX = evt.pageX - offset.left; const mouseY = evt.pageY - offset.top; let overlapping_cells = model.getOverlappingCells( mouseX + self.scroll_x, mouseY + self.scroll_y ); if (!dragging) { const overlapping_data = overlapping_cells === null ? null : overlapping_cells.ids.map(function(id) { return model.getTrackDatum( overlapping_cells.track, id ); }); if (overlapping_data !== null) { last_cell_over = overlapping_cells; cell_over_callback( overlapping_cells.ids[0], overlapping_cells.track ); self.highlightCell( model, overlapping_cells.track, overlapping_cells.ids[0] ); self.highlightColumn(model, overlapping_cells.ids[0]); const clientRect = self.$overlay_canvas[0].getBoundingClientRect(); tooltip.show( 250, model.getZoomedColumnLeft( overlapping_cells.ids[0] ) + model.getCellWidth() / 2 + clientRect.left - self.scroll_x, model.getCellTops(overlapping_cells.track) + clientRect.top - self.scroll_y, model.getTrackTooltipFn(overlapping_cells.track)( overlapping_data ) ); } else { overlapping_cells = null; } // find a gap which is in range of mouse position const overlappingGap = self.gapTooltipTargets.find( (t: any) => { return ( _.inRange(mouseX - t.origin_x, 0, 20) && _.inRange(t.origin_y - mouseY, -10, 15) ); } ); // if there is no gap, turn if (overlappingGap === undefined) { self.hoveredGap = undefined; } else if (self.hoveredGap === overlappingGap) { // tooltip should already be showing, so do nothing } else { // we have a new hovered gap, so show a tooltip const clientRect = self.$overlay_canvas[0].getBoundingClientRect(); self.hoveredGap = overlappingGap; tooltip.center = false; tooltip.show( 250, clientRect.left + overlappingGap.origin_x, clientRect.top + overlappingGap.origin_y - 20, $( `<span>${overlappingGap.data.tooltipFormatter()}</span>` ), false ); } if (!overlapping_data && !overlappingGap) { tooltip.hideIfNotAlreadyGoingTo(150); } } else { overlapping_cells = null; drag_end_x = mouseX; const left = Math.min(mouseX, drag_start_x); const right = Math.max(mouseX, drag_start_x); const drag_rect_fill = dragIsValid(drag_start_x, drag_end_x) ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.2)'; self.overlayFillRect( left, 0, right - left, self.getVisibleAreaHeight(model), drag_rect_fill ); } if (overlapping_cells === null) { last_cell_over = null; cell_over_callback(null); } self.highlightHighlightedIds( model, overlapping_cells ? overlapping_cells.ids : [] ); self.highlightHighlightedTracks(model); }); self.$overlay_canvas.on('mousedown', function(evt) { if (!mouseInOverlayCanvas(evt.pageX, evt.pageY)) { return; } dragging = true; drag_start_x = evt.pageX - self.$overlay_canvas.offset().left; drag_end_x = drag_start_x; tooltip.hide(); }); self.$overlay_canvas.on('mouseup', function(evt) { if (!mouseInOverlayCanvas(evt.pageX, evt.pageY)) { return; } executeDragOrClick(evt); }); self.$overlay_canvas.on('mouseleave', function(evt) { executeDragOrClick(); }); })(this); $dummy_scroll_div_contents.parent().scroll(function() { self.clearOverlay(); self.highlightHighlightedIds(model); self.highlightHighlightedTracks(model); }); } private drawGapLabel(txt: string, x: number, y: number) { this.gap_ctx.font = '15pt Arial'; this.gap_ctx.textAlign = 'right'; const origin_x = x * this.supersampling_ratio + 52; const origin_y = y * this.supersampling_ratio + 4; this.gap_ctx.fillText(txt, origin_x, origin_y); return { origin_x: x, origin_y: y, }; } private getNewCanvas() { const old_canvas = this.$canvas[0]; const new_canvas = old_canvas.cloneNode() as HTMLCanvasElement; const parent_node = old_canvas.parentNode; parent_node.removeChild(old_canvas); 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 getGapContext() { try { return this.$gap_canvas[0].getContext('2d'); } catch (e) { return null; } } private getWebGLCanvasContext() { try { const canvas = this.$canvas[0]; const ctx = this.ctx || (canvas.getContext('experimental-webgl', { alpha: false, antialias: this.antialias, }) as OncoprintWebGLContext); ctx.clearColor(1.0, 1.0, 1.0, 1.0); ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT); ctx.viewportWidth = canvas.width; ctx.viewportHeight = canvas.height; ctx.viewport(0, 0, ctx.viewportWidth, ctx.viewportHeight); ctx.enable(ctx.DEPTH_TEST); ctx.enable(ctx.BLEND); ctx.blendEquation(ctx.FUNC_ADD); ctx.blendFunc(ctx.SRC_ALPHA, ctx.ONE_MINUS_SRC_ALPHA); ctx.depthMask(false); return ctx; } catch (e) { return null; } } private createShaderProgram( vertex_shader: WebGLShader, fragment_shader: WebGLShader ) { const program = this.ctx.createProgram(); this.ctx.attachShader(program, vertex_shader); this.ctx.attachShader(program, fragment_shader); this.ctx.linkProgram(program); const success = this.ctx.getProgramParameter( program, this.ctx.LINK_STATUS ); if (!success) { const msg = this.ctx.getProgramInfoLog(program); this.ctx.deleteProgram(program); throw 'Unable to link shader program: ' + msg; } return program; } private createShader( source: string, type: 'VERTEX_SHADER' | 'FRAGMENT_SHADER' ) { const shader = this.ctx.createShader(this.ctx[type]); this.ctx.shaderSource(shader, source); this.ctx.compileShader(shader); const success = this.ctx.getShaderParameter( shader, this.ctx.COMPILE_STATUS ); if (!success) { const msg = this.ctx.getShaderInfoLog(shader); this.ctx.deleteShader(shader); throw 'Unable to compile shader: ' + msg; } return shader; } private overlayStrokeRect( x: number, y: number, width: number, height: number, color: string ) { const ctx = this.overlay_ctx; ctx.strokeStyle = color; (ctx as any).strokeWidth = 10; ctx.strokeRect( this.supersampling_ratio * x, this.supersampling_ratio * y, this.supersampling_ratio * width, this.supersampling_ratio * height ); } private overlayFillRect( x: number, y: number, width: number, height: number, color: string ) { const ctx = this.overlay_ctx; ctx.fillStyle = color; ctx.fillRect( this.supersampling_ratio * x, this.supersampling_ratio * y, this.supersampling_ratio * width, this.supersampling_ratio * height ); } public clearOverlay() { this.overlay_ctx.fillStyle = 'rgba(0,0,0,0)'; this.overlay_ctx.clearRect( 0, 0, this.$overlay_canvas[0].width, this.$overlay_canvas[0].height ); } private getOverlayContextAndClear() { this.overlay_ctx = this.$overlay_canvas[0].getContext('2d'); this.clearOverlay(); } private getColumnLabelsContext() { this.column_label_ctx = this.$column_label_canvas[0].getContext('2d'); } private getColumnLabelY(model: OncoprintModel) { return ( (model.getOncoprintHeight() + 10 - this.scroll_y) * this.supersampling_ratio ); } private overlayColumnLabelHighlight(model: OncoprintModel, id: ColumnId) { const label = model.getColumnLabels()[id]; if (label) { this.prepareContextForColumnLabelText(model, this.overlay_ctx); const cell_width = model.getCellWidth(); const left_padding = ((label.left_padding_percent || 0) * cell_width) / 100; let highlightHeight = cell_width * this.supersampling_ratio; let highlightWidth = this.overlay_ctx.measureText(label.text).width + left_padding * this.supersampling_ratio + 20; let dx = left_padding * this.supersampling_ratio; if (label.circle_color) { const circleDiameter = 2 * this.getColumnLabelCircleSpec(model).radius * this.supersampling_ratio; highlightHeight = Math.max(highlightHeight, circleDiameter); highlightWidth = Math.max(highlightWidth, circleDiameter); dx = Math.min(dx, -circleDiameter / 2); } const origin_x = (model.getZoomedColumnLeft(id) + cell_width / 2 - this.scroll_x) * this.supersampling_ratio; const origin_y = this.getColumnLabelY(model); this.overlay_ctx.save(); this.overlay_ctx.translate(origin_x, origin_y); const angle = this.getColumnLabelAngleRadians(label); this.overlay_ctx.rotate(angle); this.overlay_ctx.fillStyle = 'rgba(255,255,0,0.4)'; this.overlay_ctx.fillRect( dx, -highlightHeight / 2, highlightWidth, highlightHeight ); this.overlay_ctx.restore(); } } private getWebGLContextAndSetUpMatrices() { this.gap_ctx = this.getGapContext(); 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]); self.mvMatrix = mvMatrix; const pMatrix = gl_matrix.mat4.create(); gl_matrix.mat4.ortho( pMatrix, 0, self.ctx.viewportWidth, self.ctx.viewportHeight, 0, -5, 1000 ); // y axis inverted so that y increases down like SVG self.pMatrix = pMatrix; })(this); } private getColumnIndexesAfterAGap(model: OncoprintModel) { // uniform length is minimum 1 return model .getColumnIndexesAfterAGap() .concat([Number.POSITIVE_INFINITY]); } private setUpShaders(model: OncoprintModel) { const columnsRightAfterGapsSize = this.getColumnIndexesAfterAGap(model) .length; 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' ); this.ctx.enableVertexAttribArray( shader_program.vertexPositionAttribute ); shader_program.vertexColorAttribute = this.ctx.getAttribLocation( shader_program, 'aColVertex' ); this.ctx.enableVertexAttribArray(shader_program.vertexColorAttribute); shader_program.vertexOncoprintColumnAttribute = this.ctx.getAttribLocation( shader_program, 'aVertexOncoprintColumn' ); this.ctx.enableVertexAttribArray( shader_program.vertexOncoprintColumnAttribute ); shader_program.gapSizeUniform = this.ctx.getUniformLocation( shader_program, 'gapSize' ); shader_program.columnsRightAfterGapsUniform = this.ctx.getUniformLocation( shader_program, 'columnsRightAfterGaps' ); shader_program.samplerUniform = this.ctx.getUniformLocation( shader_program, 'uSampler' ); shader_program.pMatrixUniform = this.ctx.getUniformLocation( shader_program, 'uPMatrix' ); shader_program.mvMatrixUniform = this.ctx.getUniformLocation( shader_program, 'uMVMatrix' ); shader_program.columnWidthUniform = this.ctx.getUniformLocation( shader_program, 'columnWidth' ); shader_program.scrollXUniform = this.ctx.getUniformLocation( shader_program, 'scrollX' ); shader_program.scrollYUniform = this.ctx.getUniformLocation( shader_program, 'scrollY' ); shader_program.zoomXUniform = this.ctx.getUniformLocation( shader_program, 'zoomX' ); shader_program.zoomYUniform = this.ctx.getUniformLocation( shader_program, 'zoomY' ); shader_program.offsetYUniform = this.ctx.getUniformLocation( shader_program, 'offsetY' ); shader_program.supersamplingRatioUniform = this.ctx.getUniformLocation( shader_program, 'supersamplingRatio' ); shader_program.positionBitPackBaseUniform = this.ctx.getUniformLocation( shader_program, 'positionBitPackBase' ); shader_program.texSizeUniform = this.ctx.getUniformLocation( shader_program, 'texSize' ); this.shader_program = shader_program; } private resizeAndClear(model: OncoprintModel) { const height = this.getVisibleAreaHeight(model); const total_width = this.getTotalWidth(model); const visible_area_width = this.visible_area_width; const scrollbar_slack = 20; this.$dummy_scroll_div_contents.css({ 'min-width': total_width, 'min-height': model.getOncoprintHeight(), }); this.$dummy_scroll_div_contents.parent().css({ height: height + scrollbar_slack, width: visible_area_width + scrollbar_slack, }); // add space for scrollbars this.dummy_scroll_div_client_size.update(); this.$canvas[0].height = this.supersampling_ratio * height; this.$canvas[0].style.height = height + 'px'; this.$gap_canvas[0].height = this.supersampling_ratio * height; this.$gap_canvas[0].style.height = height + 'px'; this.$gap_canvas[0].width = this.supersampling_ratio * visible_area_width; this.$gap_canvas[0].style.width = visible_area_width + 'px'; this.$overlay_canvas[0].height = this.supersampling_ratio * height; this.$overlay_canvas[0].style.height = height + 'px'; this.$column_label_canvas[0].height = this.supersampling_ratio * height; this.$column_label_canvas[0].style.height = height + 'px'; this.$canvas[0].width = this.supersampling_ratio * visible_area_width; this.$canvas[0].style.width = visible_area_width + 'px'; this.$overlay_canvas[0].width = this.supersampling_ratio * visible_area_width; this.$overlay_canvas[0].style.width = visible_area_width + 'px'; this.$column_label_canvas[0].width = this.supersampling_ratio * visible_area_width; this.$column_label_canvas[0].style.width = visible_area_width + 'px'; this.$container.css('height', height); this.$container.css('width', visible_area_width); this.getWebGLContextAndSetUpMatrices(); this.setUpShaders(model); this.getOverlayContextAndClear(); this.getColumnLabelsContext(); } public gapTooltipTargets: OncoprintGap[] = []; public hoveredGap: OncoprintGap; private renderAllTracks(model: OncoprintModel, dont_resize?: boolean) { if (this.rendering_suppressed) { return; } const scroll_x = this.scroll_x; const scroll_y = this.scroll_y; const zoom_x = model.getHorzZoom(); const zoom_y = model.getVertZoom(); const viewport = this.getViewportOncoprintSpace(model); const window_left = viewport.left; const window_right = viewport.right; const window_top = viewport.top; const window_bottom = viewport.bottom; const id_order = model.getIdOrder(); let horz_first_id_in_window_index = model.getClosestColumnIndexToLeft( window_left ); const horz_first_id_after_window_index = model.getClosestColumnIndexToLeft( window_right, false, true ); horz_first_id_in_window_index = horz_first_id_in_window_index < 1 ? 0 : horz_first_id_in_window_index - 1; const horz_first_id_in_window = id_order[horz_first_id_in_window_index]; const horz_first_id_after_window = horz_first_id_after_window_index === -1 || horz_first_id_after_window_index === id_order.length ? null : id_order[horz_first_id_after_window_index]; if (!dont_resize) { this.resizeAndClear(model); } this.ctx.clearColor(1.0, 1.0, 1.0, 1.0); this.ctx.clear(this.ctx.COLOR_BUFFER_BIT | this.ctx.DEPTH_BUFFER_BIT); this.gap_ctx.clearRect( 0, 0, this.$gap_canvas[0].width, this.$gap_canvas[0].height ); const gapOffsets = model.getGapOffsets(); const tracks = model.getTracks(); this.gapTooltipTargets = []; for (let i = 0; i < tracks.length; i++) { const track_id = tracks[i]; const cell_top = model.getCellTops(track_id); const cell_height = model.getCellHeight(track_id); if ( model.showGaps() && model.gapMode() === GAP_MODE_ENUM.SHOW_GAPS_PERCENT ) { const gaps = this.getGaps(model, track_id); if (gaps) { gaps.forEach((gap: OncoprintGapConfig, i: number) => { const x = gapOffsets[i] - scroll_x - model.getGapSize(); const y = model.getZoomedTrackTops()[track_id] + cell_height - scroll_y - 4; this.drawGapLabel(gap.labelFormatter(), x, y); this.gapTooltipTargets.push({ origin_x: x, origin_y: y, data: gap, }); }); } } if ( cell_top / zoom_y >= window_bottom || (cell_top + cell_height) / zoom_y < window_top ) { // vertical clipping continue; } const buffers = this.getTrackBuffers(track_id); if (buffers.position.numItems === 0) { continue; } 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) this.clearOverlay(); this.highlightHighlightedIds(model); this.highlightHighlightedTracks(model); } private static getColumnLabelsFontSize(model: OncoprintModel) { return model.getCellWidth() / 2 + 2; } private prepareContextForColumnLabelText( model: OncoprintModel, ctx: CanvasRenderingContext2D ) { const font_size = OncoprintWebGLCellView.getColumnLabelsFontSize(model); const font_family = 'Arial'; ctx.font = this.supersampling_ratio * font_size + 'px ' + font_family; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; } private renderColumnLabels(model: OncoprintModel, ids: ColumnId[]) { // first clear this.column_label_ctx.fillStyle = 'rgba(0,0,0,0)'; this.column_label_ctx.clearRect( 0, 0, this.$column_label_canvas[0].width, this.$column_label_canvas[0].height ); this.maximum_column_label_width = 0; this.maximum_column_label_height = 0; // continue to rendering const labels = model.getColumnLabels(); // dont do anything if theres no labels if (Object.keys(labels).length === 0) { return; } const origin_y = this.getColumnLabelY(model); const x_map = model.getZoomedColumnLeft(); const scroll_x = this.scroll_x; const cell_width = model.getCellWidth(); this.prepareContextForColumnLabelText(model, this.column_label_ctx); for (let i = 0; i < ids.length; i++) { const label = labels[ids[i]]; let label_height = this.column_label_ctx.measureText('m').width / this.supersampling_ratio; if (label) { const origin_x = (x_map[ids[i]] + cell_width / 2 - scroll_x) * this.supersampling_ratio; if (label.circle_color) { // draw circle if specified this.column_label_ctx.save(); const circleSpec = this.getColumnLabelCircleSpec(model); label_height = Math.max( 2 * circleSpec.radius, label_height ); this.column_label_ctx.translate(origin_x, origin_y); this.column_label_ctx.fillStyle = label.circle_color; this.column_label_ctx.beginPath(); this.column_label_ctx.arc( 0, 0, this.supersampling_ratio * circleSpec.radius, 0, 2 * Math.PI ); this.column_label_ctx.fill(); this.column_label_ctx.restore(); } this.column_label_ctx.save(); const text_angle = this.getColumnLabelAngleRadians(label); const left_padding = ((label.left_padding_percent || 0) * cell_width) / 100; this.column_label_ctx.translate(origin_x, origin_y); this.column_label_ctx.rotate(text_angle); this.column_label_ctx.fillStyle = label.text_color || 'rgba(0,0,0,1)'; this.column_label_ctx.fillText( label.text, left_padding * this.supersampling_ratio, 0 ); const text_width = this.column_label_ctx.measureText(label.text).width / this.supersampling_ratio; this.maximum_column_label_width = Math.max( this.maximum_column_label_width, Math.cos(text_angle) * text_width ); this.maximum_column_label_height = Math.max( this.maximum_column_label_height, Math.sin(text_angle) * text_width, label_height ); this.column_label_ctx.restore(); } } } private getColumnLabelAngleRadians(label: ColumnLabel) { return ( (ifndef(label.angle_in_degrees, COLUMN_LABEL_ANGLE) * Math.PI) / 180 ); } private getColumnLabelCircleSpec(model: OncoprintModel) { const radius = 0.9 * (model.getCellWidth() / 2); 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') { tracks_to_clear = model.getTracks(); } else { tracks_to_clear = [track_id]; } for (let i = 0; i < tracks_to_clear.length; i++) { if (this.is_buffer_empty[tracks_to_clear[i]]) { this.is_buffer_empty[tracks_to_clear[i]].position = true; this.is_buffer_empty[tracks_to_clear[i]].color = true; this.is_buffer_empty[tracks_to_clear[i]].color_texture = true; } } } private clearTrackColumnBuffers(model: OncoprintModel, track_id?: TrackId) { let tracks_to_clear; if (typeof track_id === 'undefined') { tracks_to_clear = model.getTracks(); } else { tracks_to_clear = [track_id]; } for (let i = 0; i < tracks_to_clear.length; i++) { if (this.is_buffer_empty[tracks_to_clear[i]]) { this.is_buffer_empty[tracks_to_clear[i]].column = true; } } } private deleteBuffers(model: OncoprintModel, track_id?: TrackId) { let tracks_to_clear; if (typeof track_id === 'undefined') { tracks_to_clear = model.getTracks(); } else { tracks_to_clear = [track_id]; } for (let i = 0; i < tracks_to_clear.length; i++) { const track_id = tracks_to_clear[i]; if (this.vertex_position_buffer[track_id]) { this.ctx.deleteBuffer(this.vertex_position_buffer[track_id]); delete this.vertex_position_buffer[track_id]; } if (this.vertex_color_buffer[track_id]) { this.ctx.deleteBuffer(this.vertex_color_buffer[track_id]); delete this.vertex_color_buffer[track_id]; } if (this.vertex_column_buffer[track_id]) { this.ctx.deleteBuffer(this.vertex_column_buffer[track_id]); delete this.vertex_column_buffer[track_id]; } if (this.color_texture[track_id]) { this.ctx.deleteTexture(this.color_texture[track_id].texture); delete this.color_texture[track_id]; } this.is_buffer_empty[track_id] = { position: true, color: true, color_texture: true, column: true, }; } } private deleteSimpleCountBuffer(model: OncoprintModel) { if (this.simple_count_buffer) { this.ctx.deleteBuffer(this.simple_count_buffer); this.simple_count_buffer = null; } } private getTrackBuffers(track_id: TrackId) { this.is_buffer_empty[track_id] = this.is_buffer_empty[track_id] || { position: true, color: true, color_texture: true, column: true, }; if (this.is_buffer_empty[track_id].position) { const pos_buffer = this.vertex_position_buffer[track_id] || (this.ctx.createBuffer() as OncoprintVertexTrackBuffer); const pos_array = this.vertex_data[track_id].pos_array; const universal_shapes_start_index = this.vertex_data[track_id] .universal_shapes_start_index; this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, pos_buffer); this.ctx.bufferData( this.ctx.ARRAY_BUFFER, pos_array, this.ctx.STATIC_DRAW ); pos_buffer.itemSize = 1; pos_buffer.specificShapesNumItems = universal_shapes_start_index / pos_buffer.itemSize; pos_buffer.universalShapesNumItems = (pos_array.length - universal_shapes_start_index) / pos_buffer.itemSize; this.vertex_position_buffer[track_id] = pos_buffer; } if (this.is_buffer_empty[track_id].color) { const col_buffer = this.vertex_color_buffer[track_id] || (this.ctx.createBuffer() as OncoprintVertexTrackBuffer); const col_array = this.vertex_data[track_id].col_array; const universal_shapes_start_index = this.vertex_data[track_id] .universal_shapes_start_index; this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, col_buffer); this.ctx.bufferData( this.ctx.ARRAY_BUFFER, col_array, this.ctx.STATIC_DRAW ); col_buffer.itemSize = 1;