UNPKG

vitessce

Version:

Vitessce app and React component library

467 lines (454 loc) 15 kB
import React from 'react'; import range from 'lodash/range'; import { Matrix4 } from 'math.gl'; import Grid from '@material-ui/core/Grid'; import Slider from '@material-ui/core/Slider'; import InputLabel from '@material-ui/core/InputLabel'; import Select from '@material-ui/core/Select'; import Checkbox from '@material-ui/core/Checkbox'; import { getDefaultInitialViewState } from '@hms-dbmi/viv'; import { getBoundingCube, getMultiSelectionStats, } from './utils'; import { COLORMAP_OPTIONS, canLoadResolution, formatBytes, getStatsForResolution, } from '../utils'; import { DEFAULT_RASTER_DOMAIN_TYPE } from '../spatial/constants'; import { StyledSelectionSlider, useSelectStyles } from './styles'; const DOMAIN_OPTIONS = ['Full', 'Min/Max']; /** * Wrapper for the dropdown that selects a colormap (None, viridis, magma, etc.). * @prop {Object} loader Loader object with metadata. * @prop {function} handleMultiPropertyChange Function to propgate multiple layer changes at once. * This prevents updates from overridding each other. * @prop {number} resolution Current 3D resolution. * @prop {boolean} disable3d Whether or not to enable 3D selection * @prop {function} setRasterLayerCallback Setter for callbacks that fire after raster/volume loads. * @prop {function} setAreAllChannelsLoading Setter for whether or not a given channel is loading. * @prop {Object} setViewState Setter for the current view state. * @prop {number} spatialHeight Height of the spatial component. * @prop {number} spatialWidth Width of the spatial component. * @prop {object} channels Channels object. * @prop {boolean} use3d Whether or not 3D is enabled for this layer. */ function VolumeDropdown({ loader: loaderWithMeta, handleMultiPropertyChange, resolution: currResolution, disable3d, setRasterLayerCallback, setAreAllChannelsLoading, setViewState, spatialHeight, spatialWidth, channels, use3d, modelMatrix, }) { const classes = useSelectStyles(); const selections = channels.map(i => i.selection); const { data: loader } = loaderWithMeta; const handleChange = async (val) => { // val is the resolution - null indicates 2D const shouldUse3D = typeof val === 'number'; setAreAllChannelsLoading(true); setRasterLayerCallback(() => { setAreAllChannelsLoading(false); setRasterLayerCallback(null); }); if (shouldUse3D) { const [xSlice, ySlice, zSlice] = getBoundingCube(loader); const propertiesChanged = { resolution: val, xSlice, ySlice, zSlice, use3d: shouldUse3D, }; // Only make the fetch if needed i.e if the 3d was just being turned on. if (!use3d) { const { sliders } = await getMultiSelectionStats({ loader, selections, use3d: shouldUse3D, }); propertiesChanged.channels = [...channels]; propertiesChanged.channels.forEach((ch, i) => { // eslint-disable-next-line no-param-reassign ch.slider = sliders[i]; }); } // Update all properties at once to avoid overriding calls. handleMultiPropertyChange(propertiesChanged); const defaultViewState = getDefaultInitialViewState(loader, { height: spatialHeight, width: spatialWidth }, 1.5, true, new Matrix4(modelMatrix)); setViewState({ ...defaultViewState, rotationX: 0, rotationOrbit: 0, }); } else { const { sliders } = await getMultiSelectionStats({ loader, selections, use3d: shouldUse3D, }); const newChannels = [...channels]; newChannels.forEach((ch, i) => { // eslint-disable-next-line no-param-reassign ch.slider = sliders[i]; }); // Update all properties at once to avoid overriding calls. handleMultiPropertyChange({ resolution: val, use3d: shouldUse3D, spatialAxisFixed: false, channels: newChannels, }); const defaultViewState = getDefaultInitialViewState(loader, { height: spatialHeight, width: spatialWidth }, 0.5, false, new Matrix4(modelMatrix)); setViewState({ ...defaultViewState, rotationX: null, rotationOrbit: null, orbitAxis: null, }); } }; const { labels, shape } = Array.isArray(loader) ? loader[0] : loader; const hasZStack = shape[labels.indexOf('z')] > 1; return ( <> <Select native value={currResolution} onChange={e => handleChange( e.target.value === '2D' ? e.target.value : Number(e.target.value), ) } classes={{ root: classes.selectRoot }} > { <option key="2D" value="2D"> 2D Visualization </option> } {Array.from({ length: loader.length }) .fill(0) // eslint-disable-next-line no-unused-vars .map((_, resolution) => { if (loader) { if (canLoadResolution(loader, resolution)) { const { height, width, depthDownsampled, totalBytes, } = getStatsForResolution(loader, resolution); return ( <option key={`(${height}, ${width}, ${depthDownsampled})`} value={resolution} disabled={ disable3d || !hasZStack } > {`3D: ${resolution}x Downsampled, ~${formatBytes( totalBytes, )} per channel, (${height}, ${width}, ${depthDownsampled})`} </option> ); } } return null; })} </Select> </> ); } /** * Wrapper for the dropdown that selects a colormap (None, viridis, magma, etc.). * @prop {string} value Currently selected value for the colormap. * @prop {string} inputId Css id. * @prop {function} handleChange Callback for every change in colormap. */ function ColormapSelect({ value, inputId, handleChange }) { const classes = useSelectStyles(); return ( <Select native onChange={e => handleChange(e.target.value === '' ? null : e.target.value)} value={value} inputProps={{ name: 'colormap', id: inputId }} style={{ width: '100%' }} classes={{ root: classes.selectRoot }} > <option aria-label="None" value="">None</option> {COLORMAP_OPTIONS.map(name => ( <option key={name} value={name}> {name} </option> ))} </Select> ); } function TransparentColorCheckbox({ value, handleChange }) { return ( <Checkbox style={{ float: 'left', padding: 0 }} color="default" onChange={() => { if (value) { handleChange(null); } else { handleChange([0, 0, 0]); } }} checked={Boolean(value)} /> ); } /** * Wrapper for the slider that updates opacity. * @prop {string} value Currently selected value between 0 and 1. * @prop {function} handleChange Callback for every change in opacity. */ function OpacitySlider({ value, handleChange }) { return ( <Slider value={value} onChange={(e, v) => handleChange(v)} valueLabelDisplay="auto" getAriaLabel={() => 'opacity slider'} min={0} max={1} step={0.01} orientation="horizontal" style={{ marginTop: '7px' }} /> ); } /** * Wrapper for the dropdown that chooses the domain type. * @prop {string} value Currently selected value (i.e 'Max/Min'). * @prop {string} inputId Css id. * @prop {function} handleChange Callback for every change in domain. */ function SliderDomainSelector({ value, inputId, handleChange }) { const classes = useSelectStyles(); return ( <Select native onChange={e => handleChange(e.target.value)} value={value} inputProps={{ name: 'domain-selector', id: inputId }} style={{ width: '100%' }} classes={{ root: classes.selectRoot }} > {DOMAIN_OPTIONS.map(name => ( <option key={name} value={name}> {name} </option> ))} </Select> ); } /** * Wrapper for the slider that chooses global selections (z, t etc.). * @prop {string} field The dimension this selects for (z, t etc.). * @prop {number} value Currently selected index (1, 4, etc.). * @prop {function} handleChange Callback for every change in selection. * @prop {function} possibleValues All available values for the field. */ function GlobalSelectionSlider({ field, value, handleChange, possibleValues, }) { return ( <StyledSelectionSlider value={value} // See https://github.com/hms-dbmi/viv/issues/176 for why // we have the two handlers. onChange={ (event, newValue) => { handleChange({ selection: { [field]: newValue }, event }); } } onChangeCommitted={ (event, newValue) => { handleChange({ selection: { [field]: newValue }, event }); } } valueLabelDisplay="auto" getAriaLabel={() => `${field} slider`} marks={possibleValues.map(val => ({ value: val }))} min={Number(possibleValues[0])} max={Number(possibleValues.slice(-1))} orientation="horizontal" step={null} /> ); } /** * Wrapper for each of the options to show its name and then its UI component. * @prop {string} name Display name for option. * @prop {number} opacity Current opacity value. * @prop {string} inputId An id for css. * @prop {object} children Components to be rendered next to the name (slider, dropdown etc.). */ function LayerOption({ name, inputId, children }) { return ( <Grid container direction="row" alignItems="center" justifyContent="center"> <Grid item xs={6}> <InputLabel htmlFor={inputId}>{name}:</InputLabel> </Grid> <Grid item xs={6}> {children} </Grid> </Grid> ); } /** * Gloabl options for all channels (opacity, colormap, etc.). * @prop {string} colormap What colormap is currently selected (None, viridis etc.). * @prop {number} opacity Current opacity value. * @prop {function} handleColormapChange Callback for when colormap changes. * @prop {function} handleOpacityChange Callback for when opacity changes. * @prop {object} globalControlLabels All available options for global control (z and t). * @prop {function} handleGlobalChannelsSelectionChange Callback for global selection changes. * @prop {function} handleDomainChange Callback for domain type changes (full or min/max). * @prop {array} channels Current channel object for inferring the current global selection. * @prop {array} dimensions Currently available dimensions (channel, z, t etc.). * @prop {string} domainType One of Max/Min or Full (soon presets as well). * @prop {boolean} disableChannelsIfRgbDetected Whether or not we need colormap controllers if RGB. */ function LayerOptions({ colormap, opacity, handleColormapChange, handleOpacityChange, handleTransparentColorChange, globalControlLabels, globalLabelValues, handleGlobalChannelsSelectionChange, handleSliderChange, handleDomainChange, transparentColor, channels, domainType, disableChannelsIfRgbDetected, shouldShowTransparentColor, shouldShowDomain, shouldShowColormap, use3d, loader, handleMultiPropertyChange, resolution, disable3d, setRasterLayerCallback, setAreAllChannelsLoading, setViewState, spatialHeight, spatialWidth, modelMatrix, }) { const { labels, shape } = Array.isArray(loader.data) ? loader.data[0] : loader.data; const hasDimensionsAndChannels = labels.length > 0 && channels.length > 0; const hasZStack = shape[labels.indexOf('z')] > 1; // Only show volume button if we can actually view resolutions. const hasViewableResolutions = Boolean(Array.from({ length: loader.data.length, }).filter((_, res) => canLoadResolution(loader.data, res)).length); return ( <Grid container direction="column" style={{ width: '100%' }}> {hasZStack && !disable3d && hasViewableResolutions && ( <VolumeDropdown loader={loader} handleSliderChange={handleSliderChange} handleDomainChange={handleDomainChange} channels={channels} handleMultiPropertyChange={handleMultiPropertyChange} resolution={resolution} disable3d={disable3d} setRasterLayerCallback={setRasterLayerCallback} setAreAllChannelsLoading={setAreAllChannelsLoading} setViewState={setViewState} spatialHeight={spatialHeight} spatialWidth={spatialWidth} use3d={use3d} modelMatrix={modelMatrix} /> ) } {hasDimensionsAndChannels && !use3d && globalControlLabels.map( field => shape[labels.indexOf(field)] > 1 && ( <LayerOption name={field} inputId={`${field}-slider`} key={field}> <GlobalSelectionSlider field={field} value={globalLabelValues[field]} handleChange={handleGlobalChannelsSelectionChange} possibleValues={range(shape[labels.indexOf(field)])} /> </LayerOption> ), )} {!disableChannelsIfRgbDetected ? ( <> {shouldShowColormap && ( <Grid item> <LayerOption name="Colormap" inputId="colormap-select"> <ColormapSelect value={colormap || ''} inputId="colormap-select" handleChange={handleColormapChange} /> </LayerOption> </Grid> )} {shouldShowDomain && ( <Grid item> <LayerOption name="Domain" inputId="domain-selector"> <SliderDomainSelector value={domainType || DEFAULT_RASTER_DOMAIN_TYPE} handleChange={(value) => { handleDomainChange(value); }} /> </LayerOption> </Grid> )} </> ) : null} {!use3d && ( <Grid item> <LayerOption name="Opacity" inputId="opacity-slider"> <OpacitySlider value={opacity} handleChange={handleOpacityChange} /> </LayerOption> </Grid> )} {shouldShowTransparentColor && !use3d && ( <Grid item> <LayerOption name="Zero Transparent" inputId="transparent-color-selector" > <TransparentColorCheckbox value={transparentColor} handleChange={handleTransparentColorChange} /> </LayerOption> </Grid> )} </Grid> ); } export default LayerOptions;