UNPKG

higlass

Version:

HiGlass Hi-C / genomic / large data viewer

409 lines (328 loc) 11.2 kB
// @ts-nocheck import { scaleLinear } from 'd3-scale'; import TiledPixiTrack from './TiledPixiTrack'; import { tileProxy } from './services'; const BINS_PER_TILE = 1024; class Tiled1DPixiTrack extends TiledPixiTrack { constructor(context, options) { super(context, options); const { onMouseMoveZoom, isValueScaleLocked, getLockGroupExtrema } = context; this.onMouseMoveZoom = onMouseMoveZoom; this.isValueScaleLocked = isValueScaleLocked; this.getLockGroupExtrema = getLockGroupExtrema; if (this.onMouseMoveZoom) { this.pubSubs.push( this.pubSub.subscribe( 'app.mouseMove', this.mouseMoveHandler.bind(this), ), ); } } initTile(tile) { /** * We don't need to do anything but draw the tile. * * Child classes that rely on transforming tiles when zooming * and panning can override this function to draw all the elements * that will later be transformed. */ // this.drawTile(tile); super.initTile(tile); } tileToLocalId(tile) { /* * The local tile identifier */ // tile contains [zoomLevel, xPos] return `${tile.join('.')}`; } tileToRemoteId(tile) { /** * The tile identifier used on the server */ // tile contains [zoomLevel, xPos] return `${tile.join('.')}`; } relevantScale() { /** * Which scale should we use for calculating tile positions? * * Horizontal tracks should use the xScale and vertical tracks * should use the yScale * * This function should be overwritten by HorizontalTiled1DPixiTrack.js * and VerticalTiled1DPixiTrack.js */ return null; } setVisibleTiles(tilePositions) { /** * Set which tiles are visible right now. * * @param tiles: A set of tiles which will be considered the currently visible * tile positions. */ this.visibleTiles = tilePositions.map((x) => ({ tileId: this.tileToLocalId(x), remoteId: this.tileToRemoteId(x), })); this.visibleTileIds = new Set(this.visibleTiles.map((x) => x.tileId)); } calculateVisibleTiles() { // if we don't know anything about this dataset, no point // in trying to get tiles if (!this.tilesetInfo) { return; } // calculate the zoom level given the scales and the data bounds this.zoomLevel = this.calculateZoomLevel(); if (this.tilesetInfo.resolutions) { const sortedResolutions = this.tilesetInfo.resolutions .map((x) => +x) .sort((a, b) => b - a); const xTiles = tileProxy.calculateTilesFromResolution( sortedResolutions[this.zoomLevel], this._xScale, this.tilesetInfo.min_pos[0], this.tilesetInfo.max_pos[0], ); const tiles = xTiles.map((x) => [this.zoomLevel, x]); this.setVisibleTiles(tiles); return; } // x doesn't necessary mean 'x' axis, it just refers to the relevant axis // (x if horizontal, y if vertical) const xTiles = tileProxy.calculateTiles( this.zoomLevel, this.relevantScale(), this.tilesetInfo.min_pos[0], this.tilesetInfo.max_pos[0], this.tilesetInfo.max_zoom, this.tilesetInfo.max_width, ); const tiles = xTiles.map((x) => [this.zoomLevel, x]); this.setVisibleTiles(tiles); } getTilePosAndDimensions(zoomLevel, tilePos, binsPerTileIn) { /** * Get the tile's position in its coordinate system. */ const xTilePos = tilePos[0]; const yTilePos = tilePos[0]; if (this.tilesetInfo.resolutions) { // the default bins per tile which should // not be used because the right value should be in the tileset info const binsPerTile = binsPerTileIn || BINS_PER_TILE; const sortedResolutions = this.tilesetInfo.resolutions .map((x) => +x) .sort((a, b) => b - a); const chosenResolution = sortedResolutions[zoomLevel]; const tileWidth = chosenResolution * binsPerTile; const tileHeight = tileWidth; const tileX = chosenResolution * binsPerTile * tilePos[0]; const tileY = chosenResolution * binsPerTile * tilePos[1]; return { tileX, tileY, tileWidth, tileHeight, }; } // max_width should be substitutable with 2 ** tilesetInfo.max_zoom const totalWidth = this.tilesetInfo.max_width; const totalHeight = this.tilesetInfo.max_width; const minX = this.tilesetInfo.min_pos[0]; const minY = this.tilesetInfo.min_pos[1]; const tileWidth = totalWidth / 2 ** zoomLevel; const tileHeight = totalHeight / 2 ** zoomLevel; const tileX = minX + xTilePos * tileWidth; const tileY = minY + yTilePos * tileHeight; return { tileX, tileY, tileWidth, tileHeight, }; } updateTile(tile) { // no need to redraw this tile, usually // unless the data scale changes or something like that } scheduleRerender() { this.backgroundTaskScheduler.enqueueTask( this.handleRerender.bind(this), null, this.uuid, ); } handleRerender() { this.rerender(this.options, false); } getIndicesOfVisibleDataInTile(tile) { const visible = this._xScale.range(); if (!this.tilesetInfo) return [null, null]; if (!tile.tileData.dense) { console.warn(`No dense data in tile: ${tile.tileData.tilePos}`); return [null, null]; } const { tileX, tileWidth } = this.getTilePosAndDimensions( tile.tileData.zoomLevel, tile.tileData.tilePos, this.tilesetInfo.bins_per_dimension || this.tilesetInfo.tile_size, ); const tileXScale = scaleLinear() .domain([ 0, this.tilesetInfo.tile_size || this.tilesetInfo.bins_per_dimension, ]) .range([tileX, tileX + tileWidth]); const start = Math.max( 0, Math.round(tileXScale.invert(this._xScale.invert(visible[0]))), ); const end = Math.min( tile.tileData.dense.length, Math.round(tileXScale.invert(this._xScale.invert(visible[1]))), ); return [start, end]; } /** * Returns the minimum in the visible area (not visible tiles) */ minVisibleValue(ignoreFixedScale = false) { let visibleAndFetchedIds = this.visibleAndFetchedIds(); if (visibleAndFetchedIds.length === 0) { visibleAndFetchedIds = Object.keys(this.fetchedTiles); } const minimumsPerTile = visibleAndFetchedIds .map((x) => this.fetchedTiles[x]) .filter((tile) => tile.tileData.dense) .map((tile) => { const ind = this.getIndicesOfVisibleDataInTile(tile); return tile.tileData.denseDataExtrema.getMinNonZeroInSubset(ind); }); const min = Math.min(...minimumsPerTile); if (ignoreFixedScale) return min; return this.valueScaleMin !== null ? this.valueScaleMin : min; } /** * Returns the maximum in the visible area (not visible tiles) */ maxVisibleValue(ignoreFixedScale = false) { let visibleAndFetchedIds = this.visibleAndFetchedIds(); if (visibleAndFetchedIds.length === 0) { visibleAndFetchedIds = Object.keys(this.fetchedTiles); } const maximumsPerTile = visibleAndFetchedIds .map((x) => this.fetchedTiles[x]) .filter((tile) => tile.tileData.dense) .map((tile) => { const ind = this.getIndicesOfVisibleDataInTile(tile); return tile.tileData.denseDataExtrema.getMaxNonZeroInSubset(ind); }); const max = Math.max(...maximumsPerTile); if (ignoreFixedScale) return max; return this.valueScaleMax !== null ? this.valueScaleMax : max; } /** * Return an aggregated visible value. For example, the minimum or maximum. * * @description * The difference to `minVisibleValueInTiles` * is that the truly visible min or max value is returned instead of the * min or max value of the tile. The latter is not necessarily visible. * * For 'min' and 'max' this is identical to minVisibleValue and maxVisibleValue * * @param {string} aggregator Aggregation method. Currently supports `min` * and `max` only. * @return {number} The aggregated value. */ getAggregatedVisibleValue(aggregator = 'max') { const aggregate = aggregator === 'min' ? Math.min : Math.max; const limit = aggregator === 'min' ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY; let visibleAndFetchedIds = this.visibleAndFetchedIds(); if (visibleAndFetchedIds.length === 0) { visibleAndFetchedIds = Object.keys(this.fetchedTiles); } const visible = this._xScale.range(); return visibleAndFetchedIds .map((x) => this.fetchedTiles[x]) .map((tile) => { if (!tile.tileData.tilePos) { return aggregator === 'min' ? this.minVisibleValue() : this.maxVisibleValue(); } const { tileX, tileWidth } = this.getTilePosAndDimensions( tile.tileData.zoomLevel, tile.tileData.tilePos, this.tilesetInfo.bins_per_dimension || this.tilesetInfo.tile_size, ); const tileXScale = scaleLinear() .domain([ 0, this.tilesetInfo.tile_size || this.tilesetInfo.bins_per_dimension, ]) .range([tileX, tileX + tileWidth]); const start = Math.max( 0, Math.round(tileXScale.invert(this._xScale.invert(visible[0]))), ); const end = Math.min( tile.tileData.dense.length, Math.round(tileXScale.invert(this._xScale.invert(visible[1]))), ); return tile.tileData.dense.slice(start, end); }) .reduce((smallest, current) => aggregate(smallest, ...current), limit); } /** * Get the data value at a relative pixel position * @param {number} relPos Relative pixel position, where 0 indicates the * start of the track * @return {number} The data value at `relPos` */ getDataAtPos(relPos) { let value; if (!this.tilesetInfo) return value; const zoomLevel = this.calculateZoomLevel(); const tileWidth = tileProxy.calculateTileWidth( this.tilesetInfo, zoomLevel, this.tilesetInfo.tile_size, ); // console.log('dataPos:', this._xScale.invert(relPos)); const tilePos = this._xScale.invert(relPos) / tileWidth; const tileId = this.tileToLocalId([zoomLevel, Math.floor(tilePos)]); const fetchedTile = this.fetchedTiles[tileId]; if (!fetchedTile) return value; const posInTileX = this.tilesetInfo.tile_size * (tilePos - Math.floor(tilePos)); if (fetchedTile.tileData.dense) { // gene annotation tracks, for example, don't have dense // data return fetchedTile.tileData.dense[Math.floor(posInTileX)]; } return null; } mouseMoveHandler({ x, y } = {}) { if (!this.isWithin(x, y)) return; this.mouseX = x; this.mouseY = y; this.mouseMoveZoomHandler(); } mouseMoveZoomHandler() { // Implemented in the horizontal and vertical sub-classes } zoomed(...args) { super.zoomed(...args); this.mouseMoveZoomHandler(); } } export default Tiled1DPixiTrack;