UNPKG

vitessce

Version:

Vitessce app and React component library

393 lines (371 loc) 14.2 kB
import React, { useEffect, useMemo, useCallback } from 'react'; import TitleInfo from '../TitleInfo'; import { capitalize } from '../../utils'; import { useDeckCanvasSize, useReady, useUrls, useExpressionValueGetter, } from '../hooks'; import { setCellSelection, mergeCellSets, canLoadResolution } from '../utils'; import { useCellsData, useCellSetsData, useGeneSelection, useMoleculesData, useNeighborhoodsData, useRasterData, useExpressionAttrs, } from '../data-hooks'; import { getCellColors } from '../interpolate-colors'; import Spatial from './Spatial'; import SpatialOptions from './SpatialOptions'; import SpatialTooltipSubscriber from './SpatialTooltipSubscriber'; import { makeSpatialSubtitle, getInitialSpatialTargets } from './utils'; import { useCoordination, useLoaders, useSetComponentHover, useSetComponentViewInfo, useAuxiliaryCoordination, } from '../../app/state/hooks'; import { COMPONENT_COORDINATION_TYPES } from '../../app/state/coordination'; const SPATIAL_DATA_TYPES = [ 'cells', 'molecules', 'raster', 'cell-sets', 'expression-matrix', ]; /** * A subscriber component for the spatial plot. * @param {object} props * @param {string} props.theme The current theme name. * @param {object} props.coordinationScopes The mapping from coordination types to coordination * scopes. * @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 The component title. */ export default function SpatialSubscriber(props) { const { uuid, coordinationScopes, removeGridComponent, observationsLabelOverride: observationsLabel = 'cell', observationsPluralLabelOverride: observationsPluralLabel = `${observationsLabel}s`, subobservationsLabelOverride: subobservationsLabel = 'molecule', subobservationsPluralLabelOverride: subobservationsPluralLabel = `${subobservationsLabel}s`, theme, disableTooltip = false, title = 'Spatial', disable3d, globalDisable3d, } = props; const loaders = useLoaders(); const setComponentHover = useSetComponentHover(); const setComponentViewInfo = useSetComponentViewInfo(uuid); // Get "props" from the coordination space. const [{ dataset, spatialZoom: zoom, spatialTargetX: targetX, spatialTargetY: targetY, spatialTargetZ: targetZ, spatialRotationX: rotationX, spatialRotationY: rotationY, spatialRotationZ: rotationZ, spatialRotationOrbit: rotationOrbit, spatialOrbitAxis: orbitAxis, spatialImageLayer: rasterLayers, spatialSegmentationLayer: cellsLayer, spatialPointLayer: moleculesLayer, spatialNeighborhoodLayer: neighborhoodsLayer, obsFilter: cellFilter, obsHighlight: cellHighlight, featureSelection: geneSelection, obsSetSelection: cellSetSelection, obsSetColor: cellSetColor, obsColorEncoding: cellColorEncoding, additionalObsSets: additionalCellSets, spatialAxisFixed, featureValueColormap: geneExpressionColormap, featureValueColormapRange: geneExpressionColormapRange, }, { setSpatialZoom: setZoom, setSpatialTargetX: setTargetX, setSpatialTargetY: setTargetY, setSpatialTargetZ: setTargetZ, setSpatialRotationX: setRotationX, setSpatialRotationOrbit: setRotationOrbit, setSpatialOrbitAxis: setOrbitAxis, setSpatialImageLayer: setRasterLayers, setSpatialSegmentationLayer: setCellsLayer, setSpatialPointLayer: setMoleculesLayer, setSpatialNeighborhoodLayer: setNeighborhoodsLayer, setObsFilter: setCellFilter, setObsSetSelection: setCellSetSelection, setObsHighlight: setCellHighlight, setObsSetColor: setCellSetColor, setObsColorEncoding: setCellColorEncoding, setAdditionalObsSets: setAdditionalCellSets, setMoleculeHighlight, setSpatialAxisFixed, setFeatureValueColormap: setGeneExpressionColormap, setFeatureValueColormapRange: setGeneExpressionColormapRange, }] = useCoordination(COMPONENT_COORDINATION_TYPES.spatial, coordinationScopes); const [ { rasterLayersCallbacks, }, ] = useAuxiliaryCoordination( COMPONENT_COORDINATION_TYPES.layerController, coordinationScopes, ); const use3d = rasterLayers?.some(l => l.use3d); const [urls, addUrl, resetUrls] = useUrls(); const [ isReady, setItemIsReady, setItemIsNotReady, resetReadyItems, ] = useReady( SPATIAL_DATA_TYPES, ); const [width, height, deckRef] = useDeckCanvasSize(); // Reset file URLs and loader progress when the dataset has changed. // Also clear the array of automatically-initialized layers. 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, false, { setSpatialSegmentationLayer: setCellsLayer }, { spatialSegmentationLayer: cellsLayer }, ); const [molecules, moleculesCount, locationsCount] = useMoleculesData( loaders, dataset, setItemIsReady, addUrl, false, { setSpatialPointLayer: setMoleculesLayer }, { spatialPointLayer: moleculesLayer }, ); const [neighborhoods] = useNeighborhoodsData( loaders, dataset, setItemIsReady, addUrl, false, { setSpatialNeighborhoodLayer: setNeighborhoodsLayer }, { spatialNeighborhoodLayer: neighborhoodsLayer }, ); const [cellSets] = useCellSetsData( loaders, dataset, setItemIsReady, addUrl, false, { setObsSetSelection: setCellSetSelection, setObsSetColor: setCellSetColor }, { obsSetSelection: cellSetSelection, obsSetColor: cellSetColor }, ); const [expressionData] = useGeneSelection( loaders, dataset, setItemIsReady, false, geneSelection, setItemIsNotReady, ); const [attrs] = useExpressionAttrs( loaders, dataset, setItemIsReady, addUrl, false, ); // eslint-disable-next-line no-unused-vars const [raster, imageLayerLoaders, imageLayerMeta] = useRasterData( loaders, dataset, setItemIsReady, addUrl, false, { setSpatialImageLayer: setRasterLayers }, { spatialImageLayer: rasterLayers }, ); const layers = useMemo(() => { // Only want to pass in cells layer once if there is not `bitmask`. // We pass in the cells data regardless because it is needed for selection, // but the rendering layer itself is not needed. const canPassInCellsLayer = !imageLayerMeta.some(l => l?.metadata?.isBitmask); return [ ...(moleculesLayer ? [{ ...moleculesLayer, type: 'molecules' }] : []), ...((cellsLayer && canPassInCellsLayer) ? [{ ...cellsLayer, type: 'cells' }] : []), ...(neighborhoodsLayer ? [{ ...neighborhoodsLayer, type: 'neighborhoods' }] : []), ...(rasterLayers ? rasterLayers.map(l => ({ ...l, type: (l.type && ['raster', 'bitmask'].includes(l.type) ? l.type : 'raster') })) : []), ]; }, [cellsLayer, moleculesLayer, neighborhoodsLayer, rasterLayers, imageLayerMeta]); useEffect(() => { if ((typeof targetX !== 'number' || typeof targetY !== 'number')) { const { initialTargetX, initialTargetY, initialTargetZ, initialZoom, } = getInitialSpatialTargets({ width, height, cells, imageLayerLoaders, useRaster: Boolean(loaders[dataset].loaders.raster), use3d, }); setTargetX(initialTargetX); setTargetY(initialTargetY); setTargetZ(initialTargetZ); setZoom(initialZoom); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [imageLayerLoaders, cells, targetX, targetY, setTargetX, setTargetY, setZoom, use3d]); 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, cellSetColor, cellSetSelection, expressionData, attrs]); // The bitmask layer needs access to a array (i.e a texture) lookup of cell -> expression value // where each cell id indexes into the array. // Cell ids in `attrs.rows` do not necessaryily correspond to indices in that array, though, // so we create a "shifted" array where this is the case. const shiftedExpressionDataForBitmask = useMemo(() => { const hasBitmask = imageLayerMeta.some(l => l?.metadata?.isBitmask); if (attrs?.rows && expressionData && hasBitmask) { const maxId = attrs.rows.reduce((max, curr) => Math.max(max, Number(curr))); const result = new Uint8Array(maxId + 1); // eslint-disable-next-line no-plusplus for (let i = 0; i < attrs.rows.length; i++) { const id = attrs.rows[i]; result.set(expressionData[0].slice(i, i + 1), Number(id)); } return [result]; } return [new Uint8Array()]; }, [attrs, expressionData, imageLayerMeta]); const cellSelection = useMemo(() => Array.from(cellColors.keys()), [cellColors]); const getCellInfo = (cellId) => { const cell = cells[cellId]; if (cell) { return { [`${capitalize(observationsLabel)} ID`]: cellId, ...cell.factors, }; } return null; }; const setViewState = ({ zoom: newZoom, target, rotationX: newRotationX, rotationOrbit: newRotationOrbit, orbitAxis: newOrbitAxis, }) => { setZoom(newZoom); setTargetX(target[0]); setTargetY(target[1]); setTargetZ(target[2] || null); setRotationX(newRotationX); setRotationOrbit(newRotationOrbit); setOrbitAxis(newOrbitAxis || null); }; const subtitle = makeSpatialSubtitle({ observationsCount: cellsCount, observationsLabel, observationsPluralLabel, subobservationsCount: moleculesCount, subobservationsLabel, subobservationsPluralLabel, locationsCount, }); // 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 }); const hasExpressionData = loaders[dataset].loaders['expression-matrix']; const hasCellsData = loaders[dataset].loaders.cells || imageLayerMeta.some(l => l?.metadata?.isBitmask); const canLoad3DLayers = imageLayerLoaders.some(loader => Boolean( Array.from({ length: loader.data.length, }).filter((_, res) => canLoadResolution(loader.data, res)).length, )); // Only show 3D options if we can theoretically load the data and it is allowed to be loaded. const canShow3DOptions = canLoad3DLayers && !(disable3d?.length === imageLayerLoaders.length) && !globalDisable3d; return ( <TitleInfo title={title} info={subtitle} isSpatial urls={urls} theme={theme} removeGridComponent={removeGridComponent} isReady={isReady} options={ // Only show button if there is expression or 3D data because only cells data // does not have any options (i.e for color encoding, you need to switch to expression data) canShow3DOptions || hasExpressionData ? ( <SpatialOptions observationsLabel={observationsLabel} cellColorEncoding={cellColorEncoding} setCellColorEncoding={setCellColorEncoding} setSpatialAxisFixed={setSpatialAxisFixed} spatialAxisFixed={spatialAxisFixed} use3d={use3d} geneExpressionColormap={geneExpressionColormap} setGeneExpressionColormap={setGeneExpressionColormap} geneExpressionColormapRange={geneExpressionColormapRange} setGeneExpressionColormapRange={setGeneExpressionColormapRange} canShowExpressionOptions={hasExpressionData} canShowColorEncodingOption={hasCellsData && hasExpressionData} canShow3DOptions={canShow3DOptions} /> ) : null } > <Spatial ref={deckRef} uuid={uuid} width={width} height={height} viewState={{ zoom, target: [targetX, targetY, targetZ], rotationX, rotationY, rotationZ, rotationOrbit, orbitAxis, }} setViewState={setViewState} layers={layers} cells={cells} cellFilter={cellFilter} cellSelection={cellSelection} cellHighlight={cellHighlight} cellColors={cellColors} molecules={molecules} neighborhoods={neighborhoods} imageLayerLoaders={imageLayerLoaders} setCellFilter={setCellFilter} setCellSelection={setCellSelectionProp} setCellHighlight={setCellHighlight} setMoleculeHighlight={setMoleculeHighlight} setComponentHover={() => { setComponentHover(uuid); }} updateViewInfo={setComponentViewInfo} rasterLayersCallbacks={rasterLayersCallbacks} spatialAxisFixed={spatialAxisFixed} geneExpressionColormap={geneExpressionColormap} geneExpressionColormapRange={geneExpressionColormapRange} expressionData={shiftedExpressionDataForBitmask} cellColorEncoding={cellColorEncoding} getExpressionValue={getExpressionValue} theme={theme} /> {!disableTooltip && ( <SpatialTooltipSubscriber parentUuid={uuid} cellHighlight={cellHighlight} width={width} height={height} getCellInfo={getCellInfo} /> )} </TitleInfo> ); }