UNPKG

higlass

Version:

HiGlass Hi-C / genomic / large data viewer

440 lines (381 loc) 13.7 kB
// @ts-nocheck import { format } from 'd3-format'; import HeatmapTiledPixiTrack from './HeatmapTiledPixiTrack'; import { tileProxy } from './services'; import getAggregationFunction from './utils/get-aggregation-function'; import selectedItemsToCumWeights from './utils/selected-items-to-cum-weights'; import selectedItemsToSize from './utils/selected-items-to-size'; export default class HorizontalMultivecTrack extends HeatmapTiledPixiTrack { constructor(context, options) { super(context, options); this.pMain = this.pMobile; // Continuous scaling is currently not supported this.continuousScaling = false; this.updateDataFetcher(options); } updateDataFetcher(options) { if ( options?.selectRows && options.selectRowsAggregationMethod === 'server' ) { const { pubSub, dataFetcher: prevDataFetcher } = this; const prevDataConfigOptions = prevDataFetcher.dataConfig.options; const nextDataConfigOptions = { aggGroups: options.selectRows, aggFunc: options.selectRowsAggregationMode, }; if ( JSON.stringify(prevDataConfigOptions) !== JSON.stringify(nextDataConfigOptions) ) { // Override the dataFetcher object with a new dataConfig, // containing the .options property. // This would otherwise be set in the call to super() // in the TiledPixiTrack ancestor constructor. const newDataConfig = { ...prevDataFetcher.dataConfig, options: nextDataConfigOptions, }; this.dataFetcher = new prevDataFetcher.constructor( newDataConfig, pubSub, ); // Only fetch new tiles if the tileset has been registered // and has a tilesetUid (for example, due to file url-based tracks). if (this.dataFetcher.dataConfig.tilesetUid) { this.fetchNewTiles( Object.keys(this.fetchedTiles).map((x) => ({ tileId: x, remoteId: x, })), ); } } } } rerender(options, force) { this.updateDataFetcher(options); super.rerender(options, force); if (options.selectRows) { // The weights for selectRows groups must be computed // any time options.selectRows changes. this.selectRowsCumWeights = selectedItemsToCumWeights( options.selectRows, options.selectRowsAggregationWithRelativeHeight, ); } } tileDataToCanvas(pixData) { const canvas = document.createElement('canvas'); if (this.options.selectRows && this.tilesetInfo.shape) { canvas.width = this.tilesetInfo.shape[0]; canvas.height = selectedItemsToSize( this.options.selectRows, this.options.selectRowsAggregationWithRelativeHeight, ); } else if (this.tilesetInfo.shape) { canvas.width = this.tilesetInfo.shape[0]; canvas.height = this.tilesetInfo.shape[1]; } else { canvas.width = this.tilesetInfo.tile_size; // , pixData.length / 4); canvas.height = 1; } const ctx = canvas.getContext('2d'); ctx.fillStyle = 'transparent'; ctx.fillRect(0, 0, canvas.width, canvas.height); if ( pixData.length !== 0 && pixData.length === 4 * canvas.width * canvas.height ) { const pix = new ImageData(pixData, canvas.width, canvas.height); ctx.putImageData(pix, 0, 0); } else { console.warn('HorizontalMultivecTrack: pixData has an incorrect length.'); } return canvas; } setSpriteProperties(sprite, zoomLevel, tilePos) { const { tileX, tileWidth } = this.getTilePosAndDimensions( zoomLevel, tilePos, this.tilesetInfo.tile_size, ); const tileEndX = tileX + tileWidth; sprite.width = this._refXScale(tileEndX) - this._refXScale(tileX); sprite.height = this.dimensions[1]; sprite.x = this._refXScale(tileX); sprite.y = 0; } leftTrackZoomed(newXScale, newYScale, k, tx, ty) { // a separate zoom function if the track is drawn on // the left const offset = this._xScale(0) - k * this._refXScale(0); this.pMobile.position.x = offset + this.position[0]; this.pMobile.position.y = this.position[1]; this.pMobile.scale.x = k; this.pMobile.scale.y = 1; } zoomed(newXScale, newYScale, k, tx) { super.zoomed(newXScale, newYScale); this.pMain.position.x = tx; // translateX; this.pMain.position.y = this.position[1]; // translateY; this.pMain.scale.x = k; // scaleX; this.pMain.scale.y = 1; // scaleY; this.drawColorbar(); } calculateVisibleTiles() { // if we don't know anything about this dataset, no point // in trying to get tiles if (!this.tilesetInfo) { return; } this.zoomLevel = this.calculateZoomLevel(); if (this.tilesetInfo.resolutions) { const sortedResolutions = this.tilesetInfo.resolutions .map((x) => +x) .sort((a, b) => b - a); this.xTiles = tileProxy.calculateTilesFromResolution( sortedResolutions[this.zoomLevel], this._xScale, this.tilesetInfo.min_pos[0], null, this.tilesetInfo.tile_size, ); } else { this.xTiles = tileProxy.calculateTiles( this.zoomLevel, this._xScale, this.tilesetInfo.min_pos[0], this.tilesetInfo.max_pos[0], this.tilesetInfo.max_zoom, this.tilesetInfo.max_width, ); } const tiles = this.xTiles.map((x) => [this.zoomLevel, x]); this.setVisibleTiles(tiles); } calculateZoomLevel() { if (!this.tilesetInfo) return undefined; const minX = this.tilesetInfo.min_pos[0]; let zoomIndexX = null; if (this.tilesetInfo.resolutions) { zoomIndexX = tileProxy.calculateZoomLevelFromResolutions( this.tilesetInfo.resolutions, this._xScale, minX, ); } else { zoomIndexX = tileProxy.calculateZoomLevel( this._xScale, this.tilesetInfo.min_pos[0], this.tilesetInfo.max_pos[0], ); } return zoomIndexX; } /** * Create the local tile identifier, which be used with the * tile stores in TiledPixiTrack * * @param {array} tile: [zoomLevel, xPos] */ tileToLocalId(tile) { return tile.join('.'); } /** * Create the remote tile identifier, which will be used to identify the * tile on the server * * @param {array} tile: [zoomLevel, xPos] */ tileToRemoteId(tile) { return tile.join('.'); } /** * Calculate the tile position at the given track position * * @param {Number} trackX: The track's X position * @param {Number} trackY: The track's Y position * * @return {array} [zoomLevel, tilePos] */ getTilePosAtPosition(trackX, trackY) { if (!this.tilesetInfo) return undefined; const zoomLevel = this.calculateZoomLevel(); // the width of the tile in base pairs const tileWidth = tileProxy.calculateTileWidth( this.tilesetInfo, zoomLevel, this.tilesetInfo.tile_size, ); // the position of the tile containing the query position const tilePos = this._xScale.invert(trackX) / tileWidth; return [zoomLevel, Math.floor(tilePos)]; } /** * Return the data currently visible at position X and Y * * @param {Number} trackX: The x position relative to the track's start and end * @param {Number} trakcY: The y position relative to the track's start and end */ getVisibleData(trackX, trackY) { const zoomLevel = this.calculateZoomLevel(); // the width of the tile in base pairs const tileWidth = tileProxy.calculateTileWidth( this.tilesetInfo, zoomLevel, this.tilesetInfo.tile_size, ); // the position of the tile containing the query position const tilePos = this._xScale.invert(trackX) / tileWidth; let numRows = this.tilesetInfo.shape ? this.tilesetInfo.shape[1] : 1; if (this.options.selectRows) { numRows = selectedItemsToSize( this.options.selectRows, this.options.selectRowsAggregationWithRelativeHeight, ); } // the position of query within the tile let posInTileX = this.tilesetInfo.tile_size * (tilePos - Math.floor(tilePos)); const posInTileYNormalized = trackY / this.dimensions[1]; const posInTileY = posInTileYNormalized * numRows; let selectedRowIndex = Math.floor(posInTileY); let selectedRowItem; if (this.options.selectRows) { // The `posInTileY` may not directly correspond to data indices if rows are filtered/reordered, // the `selectRows` array must be checked to convert the y-position to a data index/indices first. if (this.options.selectRowsAggregationWithRelativeHeight) { // Height must take into account the size of sub-arrays, so use the cumulative weight array. selectedRowIndex = this.selectRowsCumWeights.findIndex( (weight, i) => posInTileYNormalized <= weight && (i === this.selectRowsCumWeights.length - 1 || this.selectRowsCumWeights[i + 1] >= posInTileYNormalized), ); } selectedRowItem = this.options.selectRows[selectedRowIndex]; } const tileId = this.tileToLocalId([zoomLevel, Math.floor(tilePos)]); const fetchedTile = this.fetchedTiles[tileId]; let value = ''; if (fetchedTile) { if (!this.tilesetInfo.shape) { posInTileX = fetchedTile.tileData.dense.length * (tilePos - Math.floor(tilePos)); } /* const a = rangeQuery2d(fetchedTile.tileData.dense, this.tilesetInfo.shape[0], this.tilesetInfo.shape[1], [Math.floor(posInTileX), Math.floor(posInTileX)], [posInTileY, posInTileY], */ let index = null; if (this.tilesetInfo.shape) { // Accomodate data from vector sources if ( Array.isArray(selectedRowItem) && this.options.selectRowsAggregationMethod === 'client' ) { // Need to aggregate, so `index` will actually be an array. index = selectedRowItem.map( (rowI) => this.tilesetInfo.shape[0] * rowI + Math.floor(posInTileX), ); } else if ( selectedRowItem && this.options.selectRowsAggregationMethod === 'client' ) { index = this.tilesetInfo.shape[0] * selectedRowItem + Math.floor(posInTileX); } else { // No need to aggregate, `index` will contain a single item. index = this.tilesetInfo.shape[0] * selectedRowIndex + Math.floor(posInTileX); } } else { index = fetchedTile.tileData.dense.length * selectedRowIndex + Math.floor(posInTileX); } if (Array.isArray(index)) { // Need to aggregate to compute `value`. const aggFunc = getAggregationFunction( this.options.selectRowsAggregationMode, ); const values = index.map((i) => fetchedTile.tileData.dense[i]); value = format('.3f')(aggFunc(values)); value += '<br/>'; value += `${index.length}-item ${this.options.selectRowsAggregationMode}`; } else { value = format('.3f')(fetchedTile.tileData.dense[index]); if (Array.isArray(selectedRowItem)) { value += '<br/>'; value += `${selectedRowItem.length}-item ${this.options.selectRowsAggregationMode}`; } } } // add information about the row if (this.tilesetInfo.row_infos) { value += '<br/>'; let rowInfo = ''; if (this.options.selectRows && !Array.isArray(selectedRowItem)) { rowInfo = this.tilesetInfo.row_infos[selectedRowItem]; } else if ( selectedRowIndex >= 0 && selectedRowIndex < this.tilesetInfo.row_infos.length ) { rowInfo = this.tilesetInfo.row_infos[selectedRowIndex]; } if (typeof rowInfo === 'object') { // The simplest thing to do here is conform to the tab-separated values convention. value += Object.values(rowInfo).join('\t'); } else { // Probably a tab-separated string since not an object. value += rowInfo; } } return `${value}`; } /** * Get some information to display when the mouse is over this * track * * @param {Number} trackX: the x position of the mouse over the track * @param {Number} trackY: the y position of the mouse over the track * * @return {string}: A HTML string containing the information to display * */ getMouseOverHtml(trackX, trackY) { if (!this.tilesetInfo) return ''; const tilePos = this.getTilePosAtPosition(trackX, trackY); let output = ''; if ( this.options?.heatmapValueScaling && this.options.heatmapValueScaling === 'categorical' && this.options.colorRange ) { const visibleData = this.getVisibleData(trackX, trackY); const elements = visibleData.split('<br/>'); const color = this.options.colorRange[Number.parseInt(elements[0], 10) - 1]; const label = elements[1]; if ( Number.isNaN(color) || color === 'NaN' || typeof color === 'undefined' || color === 'undefined' ) return ''; output = `<svg width="10" height="10" style="position:relative;bottom:1px"><rect width="10" height="10" rx="2" ry="2" style="fill:${color};stroke:black;stroke-width:2;"></svg> ${label}`; } else { output += `Data value: ${this.getVisibleData(trackX, trackY)}</br>`; output += `Zoom level: ${tilePos[0]} tile position: ${tilePos[1]}`; } return output; } }