UNPKG

vitessce

Version:

Vitessce app and React component library

857 lines (777 loc) 28.4 kB
import React, { useRef, useState, useCallback, useMemo, useEffect, useReducer, forwardRef, } from 'react'; import uuidv4 from 'uuid/v4'; import DeckGL from 'deck.gl'; // eslint-disable-next-line import/no-extraneous-dependencies import GL from '@luma.gl/constants'; import { Texture2D } from '@luma.gl/core'; import { OrthographicView } from '@deck.gl/core'; // eslint-disable-line import/no-extraneous-dependencies import range from 'lodash/range'; import clamp from 'lodash/clamp'; import isEqual from 'lodash/isEqual'; import { getLongestString } from '../../utils'; import HeatmapCompositeTextLayer from '../../layers/HeatmapCompositeTextLayer'; import PixelatedBitmapLayer from '../../layers/PixelatedBitmapLayer'; import PaddedExpressionHeatmapBitmapLayer from '../../layers/PaddedExpressionHeatmapBitmapLayer'; import HeatmapBitmapLayer from '../../layers/HeatmapBitmapLayer'; import { DEFAULT_GL_OPTIONS, createDefaultUpdateCellsHover, createDefaultUpdateGenesHover, createDefaultUpdateTracksHover, createDefaultUpdateViewInfo, copyUint8Array, getDefaultColor, } from '../utils'; import { layerFilter, getAxisSizes, mouseToHeatmapPosition, heatmapToMousePosition, mouseToCellColorPosition, } from './utils'; import { TILE_SIZE, MAX_ROW_AGG, MIN_ROW_AGG, COLOR_BAR_SIZE, AXIS_MARGIN, DATA_TEXTURE_SIZE, PIXELATED_TEXTURE_PARAMETERS, } from '../../layers/heatmap-constants'; import HeatmapWorkerPool from './HeatmapWorkerPool'; // Only allocate the memory once for the container const paddedExpressionContainer = new Uint8Array(DATA_TEXTURE_SIZE * DATA_TEXTURE_SIZE); /** * Should the "padded" implementation * be used? Only works if the number of heatmap values is * <= 4096^2 = ~16 million. * @param {number|null} dataLength The number of heatmap values. * @returns {boolean} Whether the more efficient implementation should be used. */ function shouldUsePaddedImplementation(dataLength) { return dataLength <= DATA_TEXTURE_SIZE ** 2; } /** * A heatmap component for cell x gene matrices. * @param {object} props * @param {string} props.uuid The uuid of this component, * used by tooltips to determine whether to render a tooltip or * a crosshair. * @param {string} props.theme The current theme name. * @param {object} props.viewState The viewState for * DeckGL. * @param {function} props.setViewState The viewState setter * for DeckGL. * @param {number} props.width The width of the canvas. * @param {number} props.height The height of the canvas. * @param {object} props.expressionMatrix An object { rows, cols, matrix }, * where matrix is a flat Uint8Array, rows is a list of cell ID strings, * and cols is a list of gene ID strings. * @param {Map} props.cellColors Map of cell ID to color. Optional. * If defined, the key ordering is used to order the cell axis of the heatmap. * @param {array} props.cellColorLabels array of labels to place beside cell color * tracks. Only works for transpose=true. * @param {function} props.clearPleaseWait The clear please wait callback, * called when the expression matrix has loaded (is not null). * @param {function} props.setCellHighlight Callback function called on * hover with the cell ID. Optional. * @param {function} props.setGeneHighlight Callback function called on * hover with the gene ID. Optional. * @param {function} props.updateViewInfo Callback function that gets called with an * object { uuid, project() } where project is a function that maps (cellId, geneId) * to canvas (x,y) coordinates. Used to show tooltips. Optional. * @param {boolean} props.transpose By default, false. * @param {string} props.variablesTitle By default, 'Genes'. * @param {string} props.observationsTitle By default, 'Cells'. * @param {number} props.useDevicePixels By default, 1. Higher values * e.g. 2 increase text sharpness. * @param {boolean} props.hideObservationLabels By default false. * @param {boolean} props.hideVariableLabels By default false. * @param {string} props.colormap The name of the colormap function to use. * @param {array} props.colormapRange A tuple [lower, upper] to adjust the color scale. * @param {function} props.setColormapRange The setter function for colormapRange. */ const Heatmap = forwardRef((props, deckRef) => { const { uuid, theme, viewState: rawViewState, setViewState, width: viewWidth, height: viewHeight, expressionMatrix: expression, cellColors, cellColorLabels = [''], colormap, colormapRange, clearPleaseWait, setComponentHover, setCellHighlight = createDefaultUpdateCellsHover('Heatmap'), setGeneHighlight = createDefaultUpdateGenesHover('Heatmap'), setTrackHighlight = createDefaultUpdateTracksHover('Heatmap'), updateViewInfo = createDefaultUpdateViewInfo('Heatmap'), setIsRendering = () => {}, transpose = false, variablesTitle = 'Genes', observationsTitle = 'Cells', variablesDashes = true, observationsDashes = true, useDevicePixels = 1, hideObservationLabels = false, hideVariableLabels = false, } = props; const viewState = { ...rawViewState, target: (transpose ? [rawViewState.target[1], rawViewState.target[0]] : rawViewState.target), minZoom: 0, }; const axisLeftTitle = (transpose ? variablesTitle : observationsTitle); const axisTopTitle = (transpose ? observationsTitle : variablesTitle); const workerPool = useMemo(() => new HeatmapWorkerPool(), []); useEffect(() => { if (clearPleaseWait && expression) { clearPleaseWait('expression-matrix'); } }, [clearPleaseWait, expression]); const tilesRef = useRef(); const dataRef = useRef(); const [axisLeftLabels, setAxisLeftLabels] = useState([]); const [axisTopLabels, setAxisTopLabels] = useState([]); const [numCellColorTracks, setNumCellColorTracks] = useState([]); // Since we are storing the tile data in a ref, // and updating it asynchronously when the worker finishes, // we need to tie it to a piece of state through this iteration value. const [tileIteration, incTileIteration] = useReducer(i => i + 1, 0); // We need to keep a backlog of the tasks for the worker thread, // since the array buffer can only be held by one thread at a time. const [backlog, setBacklog] = useState([]); // Store a reference to the matrix Uint8Array in the dataRef, // since we need to access its array buffer to transfer // it back and forth from the worker thread. useEffect(() => { // Store the expression matrix Uint8Array in the dataRef. if (expression && expression.matrix && !shouldUsePaddedImplementation(expression.matrix.length) ) { dataRef.current = copyUint8Array(expression.matrix); } }, [dataRef, expression]); // Check if the ordering of axis labels needs to be changed, // for example if the cells "selected" (technically just colored) // have changed. useEffect(() => { if (!expression) { return; } const newCellOrdering = (!cellColors || cellColors.size === 0 ? expression.rows : Array.from(cellColors.keys()) ); const oldCellOrdering = (transpose ? axisTopLabels : axisLeftLabels); if (!isEqual(oldCellOrdering, newCellOrdering)) { if (transpose) { setAxisTopLabels(newCellOrdering); } else { setAxisLeftLabels(newCellOrdering); } } }, [expression, cellColors, axisTopLabels, axisLeftLabels, transpose]); // Set the genes ordering. useEffect(() => { if (!expression) { return; } if (transpose) { setAxisLeftLabels(expression.cols); } else { setAxisTopLabels(expression.cols); } }, [expression, transpose]); const [longestCellLabel, longestGeneLabel] = useMemo(() => { if (!expression) { return ['', '']; } return [ getLongestString(expression.rows), getLongestString([...expression.cols, ...cellColorLabels]), ]; }, [expression, cellColorLabels]); // Creating a look up dictionary once is faster than calling indexOf many times // i.e when cell ordering changes. const expressionRowLookUp = useMemo(() => { const lookUp = new Map(); if (expression?.rows) { // eslint-disable-next-line no-return-assign expression.rows.forEach((cell, j) => (lookUp.set(cell, j))); } return lookUp; }, [expression]); const width = axisTopLabels.length; const height = axisLeftLabels.length; const [axisOffsetLeft, axisOffsetTop] = getAxisSizes( transpose, longestGeneLabel, longestCellLabel, hideObservationLabels, hideVariableLabels, ); const [gl, setGlContext] = useState(null); const offsetTop = axisOffsetTop + COLOR_BAR_SIZE * (transpose ? numCellColorTracks : 0); const offsetLeft = axisOffsetLeft + COLOR_BAR_SIZE * (transpose ? 0 : numCellColorTracks); const matrixWidth = viewWidth - offsetLeft; const matrixHeight = viewHeight - offsetTop; const matrixLeft = -matrixWidth / 2; const matrixRight = matrixWidth / 2; const matrixTop = -matrixHeight / 2; const matrixBottom = matrixHeight / 2; const xTiles = Math.ceil(width / TILE_SIZE); const yTiles = Math.ceil(height / TILE_SIZE); const widthRatio = 1 - (TILE_SIZE - (width % TILE_SIZE)) / (xTiles * TILE_SIZE); const heightRatio = 1 - (TILE_SIZE - (height % TILE_SIZE)) / (yTiles * TILE_SIZE); const tileWidth = (matrixWidth / widthRatio) / (xTiles); const tileHeight = (matrixHeight / heightRatio) / (yTiles); const scaleFactor = 2 ** viewState.zoom; const cellHeight = (matrixHeight * scaleFactor) / height; const cellWidth = (matrixWidth * scaleFactor) / width; // Get power of 2 between 1 and 16, // for number of cells to aggregate together in each direction. const aggSizeX = clamp(2 ** Math.ceil(Math.log2(1 / cellWidth)), MIN_ROW_AGG, MAX_ROW_AGG); const aggSizeY = clamp(2 ** Math.ceil(Math.log2(1 / cellHeight)), MIN_ROW_AGG, MAX_ROW_AGG); const [targetX, targetY] = viewState.target; // Emit the viewInfo object on viewState updates // (used by tooltips / crosshair elements). useEffect(() => { updateViewInfo({ uuid, project: (cellId, geneId) => { const colI = transpose ? axisTopLabels.indexOf(cellId) : axisTopLabels.indexOf(geneId); const rowI = transpose ? axisLeftLabels.indexOf(geneId) : axisLeftLabels.indexOf(cellId); return heatmapToMousePosition( colI, rowI, { offsetLeft, offsetTop, targetX: viewState.target[0], targetY: viewState.target[1], scaleFactor, matrixWidth, matrixHeight, numRows: height, numCols: width, }, ); }, }); }, [uuid, updateViewInfo, transpose, axisTopLabels, axisLeftLabels, offsetLeft, offsetTop, viewState, scaleFactor, matrixWidth, matrixHeight, height, width]); // Listen for viewState changes. // Do not allow the user to zoom and pan outside of the initial window. const onViewStateChange = useCallback(({ viewState: nextViewState }) => { const { zoom: nextZoom } = nextViewState; const nextScaleFactor = 2 ** nextZoom; const minTargetX = nextZoom === 0 ? 0 : -(matrixRight - (matrixRight / nextScaleFactor)); const maxTargetX = -1 * minTargetX; const minTargetY = nextZoom === 0 ? 0 : -(matrixBottom - (matrixBottom / nextScaleFactor)); const maxTargetY = -1 * minTargetY; // Manipulate view state if necessary to keep the user in the window. const nextTarget = [ clamp(nextViewState.target[0], minTargetX, maxTargetX), clamp(nextViewState.target[1], minTargetY, maxTargetY), ]; setViewState({ zoom: nextZoom, target: (transpose ? [nextTarget[1], nextTarget[0]] : nextTarget), }); }, [matrixRight, matrixBottom, transpose, setViewState]); // If `expression` or `cellOrdering` have changed, // then new tiles need to be generated, // so add a new task to the backlog. useEffect(() => { if (!expression || !expression.matrix || expression.matrix.length < DATA_TEXTURE_SIZE ** 2) { return; } // Use a uuid to give the task a unique ID, // to help identify where in the list it is located // after the worker thread asynchronously sends the data back // to this thread. if ( axisTopLabels && axisLeftLabels && xTiles && yTiles ) { setBacklog(prev => [...prev, uuidv4()]); } }, [dataRef, expression, axisTopLabels, axisLeftLabels, xTiles, yTiles]); // When the backlog has updated, a new worker job can be submitted if: // - the backlog has length >= 1 (at least one job is waiting), and // - buffer.byteLength is not zero, so the worker does not currently "own" the buffer. useEffect(() => { if (backlog.length < 1 || shouldUsePaddedImplementation(dataRef.current.length)) { return; } const curr = backlog[backlog.length - 1]; if (dataRef.current && dataRef.current.buffer.byteLength && expressionRowLookUp.size > 0 && !shouldUsePaddedImplementation(dataRef.current.length)) { const { cols, matrix } = expression; const promises = range(yTiles).map(i => range(xTiles).map(async j => workerPool.process({ curr, tileI: i, tileJ: j, tileSize: TILE_SIZE, cellOrdering: transpose ? axisTopLabels : axisLeftLabels, cols, transpose, data: matrix.buffer.slice(), expressionRowLookUp, }))); const process = async () => { const tiles = await Promise.all(promises.flat()); tilesRef.current = tiles.map(i => i.tile); incTileIteration(); dataRef.current = new Uint8Array(tiles[0].buffer); const { curr: currWork } = tiles[0]; setBacklog((prev) => { const currIndex = prev.indexOf(currWork); return prev.slice(currIndex + 1, prev.length); }); }; process(); } }, [axisLeftLabels, axisTopLabels, backlog, expression, transpose, xTiles, yTiles, workerPool, expressionRowLookUp]); useEffect(() => { setIsRendering(backlog.length > 0); }, [backlog, setIsRendering]); // Create the padded expression matrix for holding data which can then be bound to the GPU. const paddedExpressions = useMemo(() => { const cellOrdering = transpose ? axisTopLabels : axisLeftLabels; if (expression?.matrix && cellOrdering.length && gl && shouldUsePaddedImplementation(expression.matrix.length)) { let newIndex = 0; for ( let cellOrderingIndex = 0; cellOrderingIndex < cellOrdering.length; cellOrderingIndex += 1 ) { const cell = cellOrdering[cellOrderingIndex]; newIndex = transpose ? cellOrderingIndex : newIndex; const cellIndex = expressionRowLookUp.get(cell); for ( let geneIndex = 0; geneIndex < expression.cols.length; geneIndex += 1 ) { const index = cellIndex * expression.cols.length + geneIndex; paddedExpressionContainer[ newIndex % (DATA_TEXTURE_SIZE * DATA_TEXTURE_SIZE) ] = expression.matrix[index]; newIndex = transpose ? newIndex + cellOrdering.length : newIndex + 1; } } } return gl ? new Texture2D(gl, { data: paddedExpressionContainer, mipmaps: false, parameters: PIXELATED_TEXTURE_PARAMETERS, // Each color contains a single luminance value. // When sampled, rgb are all set to this luminance, alpha is 1.0. // Reference: https://luma.gl/docs/api-reference/webgl/texture#texture-formats format: GL.LUMINANCE, dataFormat: GL.LUMINANCE, type: GL.UNSIGNED_BYTE, width: DATA_TEXTURE_SIZE, height: DATA_TEXTURE_SIZE, }) : paddedExpressionContainer; }, [ transpose, axisTopLabels, axisLeftLabels, expression, expressionRowLookUp, gl, ]); // Update the heatmap tiles if: // - new tiles are available (`tileIteration` has changed), or // - the matrix bounds have changed, or // - the `aggSizeX` or `aggSizeY` have changed, or // - the cell ordering has changed. const heatmapLayers = useMemo(() => { const usePaddedExpressions = expression?.matrix && shouldUsePaddedImplementation(expression?.matrix.length); if ((!tilesRef.current || backlog.length) && !usePaddedExpressions) { return []; } if (usePaddedExpressions) { const cellOrdering = transpose ? axisTopLabels : axisLeftLabels; // eslint-disable-next-line no-inner-declarations, no-shadow function getLayer(i, j) { const { cols } = expression; return new PaddedExpressionHeatmapBitmapLayer({ id: `heatmapLayer-${i}-${j}`, image: paddedExpressions, bounds: [ matrixLeft + j * tileWidth, matrixTop + i * tileHeight, matrixLeft + (j + 1) * tileWidth, matrixTop + (i + 1) * tileHeight, ], tileI: i, tileJ: j, numXTiles: xTiles, numYTiles: yTiles, origDataSize: transpose ? [cols.length, cellOrdering.length] : [cellOrdering.length, cols.length], aggSizeX, aggSizeY, colormap, colorScaleLo: colormapRange[0], colorScaleHi: colormapRange[1], updateTriggers: { image: [axisLeftLabels, axisTopLabels], bounds: [tileHeight, tileWidth], }, }); } const layers = range(yTiles * xTiles).map( index => getLayer(Math.floor(index / xTiles), index % xTiles), ); return layers; } function getLayer(i, j, tile) { return new HeatmapBitmapLayer({ id: `heatmapLayer-${tileIteration}-${i}-${j}`, image: tile, bounds: [ matrixLeft + j * tileWidth, matrixTop + i * tileHeight, matrixLeft + (j + 1) * tileWidth, matrixTop + (i + 1) * tileHeight, ], aggSizeX, aggSizeY, colormap, colorScaleLo: colormapRange[0], colorScaleHi: colormapRange[1], updateTriggers: { image: [axisLeftLabels, axisTopLabels], bounds: [tileHeight, tileWidth], }, }); } const layers = tilesRef.current.map( (tile, index) => getLayer(Math.floor(index / xTiles), index % xTiles, tile), ); return layers; }, [expression, backlog.length, transpose, axisTopLabels, axisLeftLabels, yTiles, xTiles, paddedExpressions, matrixLeft, tileWidth, matrixTop, tileHeight, aggSizeX, aggSizeY, colormap, colormapRange, tileIteration]); const axisLeftDashes = (transpose ? variablesDashes : observationsDashes); const axisTopDashes = (transpose ? observationsDashes : variablesDashes); // Map cell and gene names to arrays with indices, // to prepare to render the names in TextLayers. const axisTopLabelData = useMemo(() => axisTopLabels.map((d, i) => [i, (axisTopDashes ? `- ${d}` : d)]), [axisTopLabels, axisTopDashes]); const axisLeftLabelData = useMemo(() => axisLeftLabels.map((d, i) => [i, (axisLeftDashes ? `${d} -` : d)]), [axisLeftLabels, axisLeftDashes]); const cellColorLabelsData = useMemo(() => cellColorLabels.map((d, i) => [i, d && (transpose ? `${d} -` : `- ${d}`)]), [cellColorLabels, transpose]); const hideTopLabels = (transpose ? hideObservationLabels : hideVariableLabels); const hideLeftLabels = (transpose ? hideVariableLabels : hideObservationLabels); // Generate the axis label, axis title, and loading indicator text layers. const textLayers = [ new HeatmapCompositeTextLayer({ axis: 'left', id: 'axisLeftCompositeTextLayer', targetX, targetY, scaleFactor, axisLeftLabelData, matrixTop, height, matrixHeight, cellHeight, cellWidth, axisTopLabelData, matrixLeft, width, matrixWidth, viewHeight, viewWidth, theme, axisLeftTitle, axisTopTitle, axisOffsetLeft, axisOffsetTop, hideTopLabels, hideLeftLabels, transpose, }), new HeatmapCompositeTextLayer({ axis: 'top', id: 'axisTopCompositeTextLayer', targetX, targetY, scaleFactor, axisLeftLabelData, matrixTop, height, matrixHeight, cellHeight, cellWidth, axisTopLabelData, matrixLeft, width, matrixWidth, viewHeight, viewWidth, theme, axisLeftTitle, axisTopTitle, axisOffsetLeft, axisOffsetTop, cellColorLabelsData, hideTopLabels, hideLeftLabels, transpose, }), new HeatmapCompositeTextLayer({ axis: 'corner', id: 'cellColorLabelCompositeTextLayer', targetX, targetY, scaleFactor, axisLeftLabelData, matrixTop, height, matrixHeight, cellHeight, cellWidth, axisTopLabelData, matrixLeft, width, matrixWidth, viewHeight, viewWidth, theme, axisLeftTitle, axisTopTitle, axisOffsetLeft, axisOffsetTop, cellColorLabelsData, hideTopLabels, hideLeftLabels, transpose, }), ]; useEffect(() => { setNumCellColorTracks(cellColorLabels.length); }, [cellColorLabels]); // Create the left color bar with a BitmapLayer. // TODO: find a way to do aggregation for this as well. const cellColorsTilesList = useMemo(() => { if (!cellColors) { return null; } let cellId; let offset; let color; let rowI; const cellOrdering = (transpose ? axisTopLabels : axisLeftLabels); const colorBarTileWidthPx = (transpose ? TILE_SIZE : 1); const colorBarTileHeightPx = (transpose ? 1 : TILE_SIZE); const result = range(numCellColorTracks).map((track) => { const trackResult = range((transpose ? xTiles : yTiles)).map((i) => { const tileData = new Uint8ClampedArray(TILE_SIZE * 1 * 4); range(TILE_SIZE).forEach((tileY) => { rowI = (i * TILE_SIZE) + tileY; // the row / cell index if (rowI < cellOrdering.length) { cellId = cellOrdering[rowI]; color = cellColors.get(cellId); offset = (transpose ? tileY : (TILE_SIZE - tileY - 1)) * 4; if (color) { // allows color to be [R, G, B] or array of arrays of [R, G, B] if (typeof color[0] !== 'number') color = color[track] ?? getDefaultColor(theme); const [rValue, gValue, bValue] = color; tileData[offset + 0] = rValue; tileData[offset + 1] = gValue; tileData[offset + 2] = bValue; tileData[offset + 3] = 255; } } }); return new ImageData(tileData, colorBarTileWidthPx, colorBarTileHeightPx); }); return trackResult; }); return result; }, [cellColors, transpose, axisTopLabels, axisLeftLabels, numCellColorTracks, xTiles, yTiles, theme]); const cellColorsLayersList = useMemo(() => { if (!cellColorsTilesList) { return []; } const result = cellColorsTilesList.map((cellColorsTiles, track) => (cellColorsTiles ? cellColorsTiles.map((tile, i) => new PixelatedBitmapLayer({ id: `${(transpose ? 'colorsTopLayer' : 'colorsLeftLayer')}-${track}-${i}-${uuidv4()}`, image: tile, bounds: (transpose ? [ matrixLeft + i * tileWidth, -matrixHeight / 2, matrixLeft + (i + 1) * tileWidth, matrixHeight / 2, ] : [ -matrixWidth / 2, matrixTop + i * tileHeight, matrixWidth / 2, matrixTop + (i + 1) * tileHeight, ]), })) : [])); return (result); }, [cellColorsTilesList, matrixTop, matrixLeft, matrixHeight, matrixWidth, tileWidth, tileHeight, transpose]); const layers = heatmapLayers .concat(textLayers) .concat(...cellColorsLayersList); // Set up the onHover function. function onHover(info, event) { if (!expression) { return; } const { x: mouseX, y: mouseY } = event.offsetCenter; const [trackColI, trackI] = mouseToCellColorPosition(mouseX, mouseY, { axisOffsetTop, axisOffsetLeft, offsetTop, offsetLeft, colorBarSize: COLOR_BAR_SIZE, numCellColorTracks, transpose, targetX, targetY, scaleFactor, matrixWidth, matrixHeight, numRows: height, numCols: width, }); if (trackI === null || trackColI === null) { setTrackHighlight(null); } else { const obsI = expression.rows.indexOf(axisTopLabels[trackColI]); const cellIndex = expression.rows[obsI]; setTrackHighlight([cellIndex, trackI, mouseX, mouseY]); } const [colI, rowI] = mouseToHeatmapPosition(mouseX, mouseY, { offsetLeft, offsetTop, targetX, targetY, scaleFactor, matrixWidth, matrixHeight, numRows: height, numCols: width, }); if (colI === null) { if (transpose) { setCellHighlight(null); } else { setGeneHighlight(null); } } if (rowI === null) { if (transpose) { setGeneHighlight(null); } else { setCellHighlight(null); } } const obsI = expression.rows.indexOf(transpose ? axisTopLabels[colI] : axisLeftLabels[rowI]); const varI = expression.cols.indexOf(transpose ? axisLeftLabels[rowI] : axisTopLabels[colI]); const obsId = expression.rows[obsI]; const varId = expression.cols[varI]; if (setComponentHover) { setComponentHover(); } setCellHighlight(obsId || null); setGeneHighlight(varId || null); } const cellColorsViews = useMemo(() => { const result = range(numCellColorTracks).map((track) => { let view; if (transpose) { view = new OrthographicView({ id: `colorsTop-${track}`, controller: true, x: offsetLeft, y: axisOffsetTop + track * COLOR_BAR_SIZE, width: matrixWidth, height: COLOR_BAR_SIZE - AXIS_MARGIN, }); } else { view = new OrthographicView({ id: `colorsLeft-${track}`, controller: true, x: axisOffsetLeft + track * COLOR_BAR_SIZE, y: offsetTop, width: COLOR_BAR_SIZE - AXIS_MARGIN, height: matrixHeight, }); } return view; }); return result; }, [numCellColorTracks, transpose, offsetLeft, axisOffsetTop, matrixWidth, axisOffsetLeft, offsetTop, matrixHeight]); return ( <DeckGL id={`deckgl-overlay-${uuid}`} ref={deckRef} onWebGLInitialized={setGlContext} views={[ // Note that there are multiple views here, // but only one viewState. new OrthographicView({ id: 'heatmap', controller: true, x: offsetLeft, y: offsetTop, width: matrixWidth, height: matrixHeight, }), new OrthographicView({ id: 'axisLeft', controller: false, x: 0, y: offsetTop, width: axisOffsetLeft, height: matrixHeight, }), new OrthographicView({ id: 'axisTop', controller: false, x: offsetLeft, y: 0, width: matrixWidth, height: axisOffsetTop, }), new OrthographicView({ id: 'cellColorLabel', controller: false, x: (transpose ? 0 : axisOffsetLeft), y: (transpose ? axisOffsetTop : 0), width: (transpose ? axisOffsetLeft : COLOR_BAR_SIZE * numCellColorTracks), height: (transpose ? COLOR_BAR_SIZE * numCellColorTracks : axisOffsetTop), }), ...cellColorsViews, ]} layers={layers} layerFilter={layerFilter} getCursor={interactionState => (interactionState.isDragging ? 'grabbing' : 'default')} glOptions={DEFAULT_GL_OPTIONS} onViewStateChange={onViewStateChange} viewState={viewState} onHover={onHover} useDevicePixels={useDevicePixels} /> ); }); export default Heatmap;