UNPKG

vitessce

Version:

Vitessce app and React component library

355 lines (329 loc) 13.1 kB
import React, { useState, useEffect, useCallback, useMemo, } from 'react'; import { extent } from 'd3-array'; import isEqual from 'lodash/isEqual'; import TitleInfo from '../TitleInfo'; import { pluralize, capitalize } from '../../utils'; import { useDeckCanvasSize, useReady, useUrls, useExpressionValueGetter, } from '../hooks'; import { setCellSelection, mergeCellSets } from '../utils'; import { getCellSetPolygons } from '../sets/cell-set-utils'; import { useCellsData, useCellSetsData, useGeneSelection, useExpressionAttrs, } from '../data-hooks'; import { getCellColors } from '../interpolate-colors'; import Scatterplot from './Scatterplot'; import ScatterplotTooltipSubscriber from './ScatterplotTooltipSubscriber'; import ScatterplotOptions from './ScatterplotOptions'; import { useCoordination, useLoaders, useSetComponentHover, useSetComponentViewInfo, } from '../../app/state/hooks'; import { getPointSizeDevicePixels, getPointOpacity, } from '../shared-spatial-scatterplot/dynamic-opacity'; import { COMPONENT_COORDINATION_TYPES } from '../../app/state/coordination'; const SCATTERPLOT_DATA_TYPES = ['cells', 'expression-matrix', 'cell-sets']; /** * A subscriber component for the scatterplot. * @param {object} props * @param {number} props.uuid The unique identifier for this component. * @param {string} props.theme The current theme name. * @param {object} props.coordinationScopes The mapping from coordination types to coordination * scopes. * @param {boolean} props.disableTooltip Should the tooltip be disabled? * @param {function} props.removeGridComponent The callback function to pass to TitleInfo, * to call when the component has been removed from the grid. * @param {string} props.title An override value for the component title. * @param {number} props.averageFillDensity Override the average fill density calculation * when using dynamic opacity mode. */ export default function ScatterplotSubscriber(props) { const { uuid, coordinationScopes, removeGridComponent, theme, disableTooltip = false, observationsLabelOverride: observationsLabel = 'cell', observationsPluralLabelOverride: observationsPluralLabel = `${observationsLabel}s`, title: titleOverride, // Average fill density for dynamic opacity calculation. averageFillDensity, } = props; const loaders = useLoaders(); const setComponentHover = useSetComponentHover(); const setComponentViewInfo = useSetComponentViewInfo(uuid); // Get "props" from the coordination space. const [{ dataset, embeddingZoom: zoom, embeddingTargetX: targetX, embeddingTargetY: targetY, embeddingTargetZ: targetZ, embeddingType: mapping, cellFilter, cellHighlight, geneSelection, cellSetSelection, cellSetColor, cellColorEncoding, additionalCellSets, embeddingCellSetPolygonsVisible: cellSetPolygonsVisible, embeddingCellSetLabelsVisible: cellSetLabelsVisible, embeddingCellSetLabelSize: cellSetLabelSize, embeddingCellRadius: cellRadiusFixed, embeddingCellRadiusMode: cellRadiusMode, embeddingCellOpacity: cellOpacityFixed, embeddingCellOpacityMode: cellOpacityMode, geneExpressionColormap, geneExpressionColormapRange, }, { setEmbeddingZoom: setZoom, setEmbeddingTargetX: setTargetX, setEmbeddingTargetY: setTargetY, setEmbeddingTargetZ: setTargetZ, setCellFilter, setCellSetSelection, setCellHighlight, setCellSetColor, setCellColorEncoding, setAdditionalCellSets, setEmbeddingCellSetPolygonsVisible: setCellSetPolygonsVisible, setEmbeddingCellSetLabelsVisible: setCellSetLabelsVisible, setEmbeddingCellSetLabelSize: setCellSetLabelSize, setEmbeddingCellRadius: setCellRadiusFixed, setEmbeddingCellRadiusMode: setCellRadiusMode, setEmbeddingCellOpacity: setCellOpacityFixed, setEmbeddingCellOpacityMode: setCellOpacityMode, setGeneExpressionColormap, setGeneExpressionColormapRange, }] = useCoordination(COMPONENT_COORDINATION_TYPES.scatterplot, coordinationScopes); const [urls, addUrl, resetUrls] = useUrls(); const [width, height, deckRef] = useDeckCanvasSize(); const [ isReady, setItemIsReady, setItemIsNotReady, // eslint-disable-line no-unused-vars resetReadyItems, ] = useReady( SCATTERPLOT_DATA_TYPES, ); const title = titleOverride || `Scatterplot (${mapping})`; // Reset file URLs and loader progress when the dataset has changed. useEffect(() => { resetUrls(); resetReadyItems(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [loaders, dataset]); // Get data from loaders using the data hooks. const [cells, cellsCount] = useCellsData(loaders, dataset, setItemIsReady, addUrl, true); const [cellSets] = useCellSetsData( loaders, dataset, setItemIsReady, addUrl, false, { setCellSetSelection, setCellSetColor }, { cellSetSelection, cellSetColor }, ); const [expressionData] = useGeneSelection( loaders, dataset, setItemIsReady, false, geneSelection, setItemIsNotReady, ); const [attrs] = useExpressionAttrs( loaders, dataset, setItemIsReady, addUrl, false, ); const [dynamicCellRadius, setDynamicCellRadius] = useState(cellRadiusFixed); const [dynamicCellOpacity, setDynamicCellOpacity] = useState(cellOpacityFixed); const mergedCellSets = useMemo(() => mergeCellSets( cellSets, additionalCellSets, ), [cellSets, additionalCellSets]); const setCellSelectionProp = useCallback((v) => { setCellSelection( v, additionalCellSets, cellSetColor, setCellSetSelection, setAdditionalCellSets, setCellSetColor, setCellColorEncoding, ); }, [additionalCellSets, cellSetColor, setCellColorEncoding, setAdditionalCellSets, setCellSetColor, setCellSetSelection]); const cellColors = useMemo(() => getCellColors({ cellColorEncoding, expressionData: expressionData && expressionData[0], geneSelection, cellSets: mergedCellSets, cellSetSelection, cellSetColor, expressionDataAttrs: attrs, theme, }), [cellColorEncoding, geneSelection, mergedCellSets, theme, cellSetSelection, cellSetColor, expressionData, attrs]); // cellSetPolygonCache is an array of tuples like [(key0, val0), (key1, val1), ...], // where the keys are cellSetSelection arrays. const [cellSetPolygonCache, setCellSetPolygonCache] = useState([]); const cacheHas = (cache, key) => cache.findIndex(el => isEqual(el[0], key)) !== -1; const cacheGet = (cache, key) => cache.find(el => isEqual(el[0], key))?.[1]; const cellSetPolygons = useMemo(() => { if ((cellSetLabelsVisible || cellSetPolygonsVisible) && !cacheHas(cellSetPolygonCache, cellSetSelection) && mergedCellSets?.tree?.length && Object.values(cells).length && cellSetColor?.length) { const newCellSetPolygons = getCellSetPolygons({ cells, mapping, cellSets: mergedCellSets, cellSetSelection, cellSetColor, theme, }); setCellSetPolygonCache(cache => [...cache, [cellSetSelection, newCellSetPolygons]]); return newCellSetPolygons; } return cacheGet(cellSetPolygonCache, cellSetSelection) || []; }, [cellSetPolygonsVisible, cellSetPolygonCache, cellSetLabelsVisible, theme, cells, mapping, mergedCellSets, cellSetSelection, cellSetColor]); const cellSelection = useMemo(() => Array.from(cellColors.keys()), [cellColors]); const [xRange, yRange, xExtent, yExtent, numCells] = useMemo(() => { const cellValues = cells && Object.values(cells); if (cellValues?.length) { const cellCoordinates = Object.values(cells) .map(c => c.mappings[mapping]); const xE = extent(cellCoordinates, c => c[0]); const yE = extent(cellCoordinates, c => c[1]); const xR = xE[1] - xE[0]; const yR = yE[1] - yE[0]; return [xR, yR, xE, yE, cellValues.length]; } return [null, null, null, null, null]; }, [cells, mapping]); // After cells have loaded or changed, // compute the cell radius scale based on the // extents of the cell coordinates on the x/y axes. useEffect(() => { if (xRange && yRange) { const pointSizeDevicePixels = getPointSizeDevicePixels( window.devicePixelRatio, zoom, xRange, yRange, width, height, ); setDynamicCellRadius(pointSizeDevicePixels); const nextCellOpacityScale = getPointOpacity( zoom, xRange, yRange, width, height, numCells, averageFillDensity, ); setDynamicCellOpacity(nextCellOpacityScale); if (typeof targetX !== 'number' || typeof targetY !== 'number') { const newTargetX = xExtent[0] + xRange / 2; const newTargetY = yExtent[0] + yRange / 2; const newZoom = Math.log2(Math.min(width / xRange, height / yRange)); setTargetX(newTargetX); // Graphics rendering has the y-axis going south so we need to multiply by negative one. setTargetY(-newTargetY); setZoom(newZoom); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [xRange, yRange, xExtent, yExtent, numCells, cells, mapping, width, height, zoom, averageFillDensity]); const getCellInfo = useCallback((cellId) => { const cellInfo = cells[cellId]; return { [`${capitalize(observationsLabel)} ID`]: cellId, ...(cellInfo ? cellInfo.factors : {}), }; }, [cells, observationsLabel]); const cellSelectionSet = useMemo(() => new Set(cellSelection), [cellSelection]); const getCellIsSelected = useCallback(cellEntry => ( (cellSelectionSet || new Set([])).has(cellEntry[0]) ? 1.0 : 0.0), [cellSelectionSet]); const cellRadius = (cellRadiusMode === 'manual' ? cellRadiusFixed : dynamicCellRadius); const cellOpacity = (cellOpacityMode === 'manual' ? cellOpacityFixed : dynamicCellOpacity); // Set up a getter function for gene expression values, to be used // by the DeckGL layer to obtain values for instanced attributes. const getExpressionValue = useExpressionValueGetter({ attrs, expressionData }); return ( <TitleInfo title={title} info={`${cellsCount} ${pluralize(observationsLabel, observationsPluralLabel, cellsCount)}`} removeGridComponent={removeGridComponent} urls={urls} theme={theme} isReady={isReady} options={( <ScatterplotOptions observationsLabel={observationsLabel} cellRadius={cellRadiusFixed} setCellRadius={setCellRadiusFixed} cellRadiusMode={cellRadiusMode} setCellRadiusMode={setCellRadiusMode} cellOpacity={cellOpacityFixed} setCellOpacity={setCellOpacityFixed} cellOpacityMode={cellOpacityMode} setCellOpacityMode={setCellOpacityMode} cellSetLabelsVisible={cellSetLabelsVisible} setCellSetLabelsVisible={setCellSetLabelsVisible} cellSetLabelSize={cellSetLabelSize} setCellSetLabelSize={setCellSetLabelSize} cellSetPolygonsVisible={cellSetPolygonsVisible} setCellSetPolygonsVisible={setCellSetPolygonsVisible} cellColorEncoding={cellColorEncoding} setCellColorEncoding={setCellColorEncoding} geneExpressionColormap={geneExpressionColormap} setGeneExpressionColormap={setGeneExpressionColormap} geneExpressionColormapRange={geneExpressionColormapRange} setGeneExpressionColormapRange={setGeneExpressionColormapRange} /> )} > <Scatterplot ref={deckRef} uuid={uuid} theme={theme} viewState={{ zoom, target: [targetX, targetY, targetZ] }} setViewState={({ zoom: newZoom, target }) => { setZoom(newZoom); setTargetX(target[0]); setTargetY(target[1]); setTargetZ(target[2] || 0); }} cells={cells} mapping={mapping} cellFilter={cellFilter} cellSelection={cellSelection} cellHighlight={cellHighlight} cellColors={cellColors} cellSetPolygons={cellSetPolygons} cellSetLabelSize={cellSetLabelSize} cellSetLabelsVisible={cellSetLabelsVisible} cellSetPolygonsVisible={cellSetPolygonsVisible} setCellFilter={setCellFilter} setCellSelection={setCellSelectionProp} setCellHighlight={setCellHighlight} cellRadius={cellRadius} cellOpacity={cellOpacity} cellColorEncoding={cellColorEncoding} geneExpressionColormap={geneExpressionColormap} geneExpressionColormapRange={geneExpressionColormapRange} setComponentHover={() => { setComponentHover(uuid); }} updateViewInfo={setComponentViewInfo} getExpressionValue={getExpressionValue} getCellIsSelected={getCellIsSelected} /> {!disableTooltip && ( <ScatterplotTooltipSubscriber parentUuid={uuid} cellHighlight={cellHighlight} width={width} height={height} getCellInfo={getCellInfo} /> )} </TitleInfo> ); }