vitessce
Version:
Vitessce app and React component library
260 lines (226 loc) • 8.07 kB
JavaScript
import {
useRef, useState, useEffect, useCallback, useMemo,
} from 'react';
import debounce from 'lodash/debounce';
import { useGridResize, useEmitGridResize } from '../app/state/hooks';
import { VITESSCE_CONTAINER } from './classNames';
function getWindowDimensions() {
const { innerWidth: width, innerHeight: height } = window;
return {
width,
height,
};
}
export function useVitessceContainer(ref) {
return useCallback(() => {
if (ref.current) {
return ref.current.closest(`.${VITESSCE_CONTAINER}`);
}
return null;
}, [ref]);
}
/**
* Custom hook, gets the full window dimensions.
* @returns {array} `[width, height]` where width and height
* are numbers.
*/
export function useWindowDimensions() {
const [windowDimensions, setWindowDimensions] = useState(
getWindowDimensions(),
);
useEffect(() => {
function handleResize() {
setWindowDimensions(getWindowDimensions());
}
const onResizeDebounced = debounce(handleResize, 100, { trailing: true });
window.addEventListener('resize', onResizeDebounced);
return () => window.removeEventListener('resize', onResizeDebounced);
}, []);
return windowDimensions;
}
/**
* Custom hook, subscribes to GRID_RESIZE and window resize events.
* @returns {array} `[width, height, containerRef]` where width and height
* are numbers and containerRef is a React ref.
*/
export function useGridItemSize() {
const containerRef = useRef();
const [height, setHeight] = useState();
const [width, setWidth] = useState();
const resizeCount = useGridResize();
const incrementResizeCount = useEmitGridResize();
// On window resize events, increment the grid resize count.
useEffect(() => {
function onWindowResize() {
incrementResizeCount();
}
const onResizeDebounced = debounce(onWindowResize, 100, { trailing: true });
window.addEventListener('resize', onResizeDebounced);
onWindowResize();
return () => {
window.removeEventListener('resize', onResizeDebounced);
};
}, [incrementResizeCount]);
// On new grid resize counts, re-compute the component
// width/height.
useEffect(() => {
if (!containerRef.current) return;
const container = containerRef.current;
const containerRect = container.getBoundingClientRect();
setHeight(containerRect.height);
setWidth(containerRect.width);
}, [resizeCount]);
return [width, height, containerRef];
}
/**
* Custom hook, subscribes to GRID_RESIZE and window resize events.
* @returns {array} `[width, height, deckRef]` where width and height
* are numbers and deckRef is a React ref to be used with
* a <DeckGL/> element (or a forwardRef to one).
*/
export function useDeckCanvasSize() {
const deckRef = useRef();
const [height, setHeight] = useState();
const [width, setWidth] = useState();
const resizeCount = useGridResize();
const incrementResizeCount = useEmitGridResize();
// On window resize events, increment the grid resize count.
useEffect(() => {
function onWindowResize() {
incrementResizeCount();
}
const onResizeDebounced = debounce(onWindowResize, 100, { trailing: true });
window.addEventListener('resize', onResizeDebounced);
onWindowResize();
return () => {
window.removeEventListener('resize', onResizeDebounced);
};
}, [incrementResizeCount]);
// On new grid resize counts, re-compute the DeckGL canvas
// width/height.
useEffect(() => {
if (!deckRef.current) return;
const { canvas } = deckRef.current.deck;
const canvasRect = canvas.getBoundingClientRect();
setHeight(canvasRect.height);
setWidth(canvasRect.width);
}, [resizeCount]);
return [width, height, deckRef];
}
/**
* This hook handles a boolean isReady value,
* which only returns true once every item in the
* input list has been marked as "ready".
* @param {string[]} items The items to wait on.
* Should be defined as a constant
* (outside a function component / render function),
* otherwise strange bugs may occur.
* @returns {array} An array
* [isReady, setItemIsReady, setItemIsNotReady, resetReadyItems]
* where isReady is the boolean value,
* setItemIsReady marks one item as ready,
* setItemIsNotReady marks one item as not ready,
* and resetReadyItem marks all items as waiting.
*/
export function useReady(supportedItems) {
const items = supportedItems;
const [waiting, setWaiting] = useState(items);
const setItemIsReady = useCallback((readyItem) => {
setWaiting((waitingItems) => {
const nextWaitingItems = waitingItems.filter(item => item !== readyItem);
// eslint-disable-next-line no-console
console.log(`cleared ${readyItem}; waiting on ${nextWaitingItems.length}: ${JSON.stringify(nextWaitingItems)}`);
return nextWaitingItems;
});
}, [setWaiting]);
const setItemIsNotReady = useCallback((notReadyItem) => {
setWaiting((waitingItems) => {
const nextWaitingItems = [...waitingItems, notReadyItem];
// eslint-disable-next-line no-console
console.log(`waiting on ${nextWaitingItems.length}: ${JSON.stringify(nextWaitingItems)}`);
return nextWaitingItems;
});
}, [setWaiting]);
const resetReadyItems = useCallback(() => {
setWaiting(items);
// eslint-disable-next-line no-console
console.log(`waiting on ${items.length}: ${JSON.stringify(items)}`);
}, [setWaiting, items]);
const isReady = waiting.length === 0;
return [isReady, setItemIsReady, setItemIsNotReady, resetReadyItems];
}
/**
* This hook manages a list of URLs,
* with adding and resetting helpers.
* @returns {array} An array
* [urls, addUrl, resetUrls]
* where urls is the array of URL objects,
* addUrl is a function for adding a URL to the array,
* resetUrls is a function that clears the array.
*/
export function useUrls() {
const [urls, setUrls] = useState([]);
const addUrl = useCallback((url, name) => {
if (url) {
setUrls(prev => ([...prev, { url, name }]));
}
}, [setUrls]);
const resetUrls = useCallback(() => {
setUrls([]);
}, [setUrls]);
return [urls, addUrl, resetUrls];
}
/**
* Custom hook, subscribes to the width and height of the closest .vitessce-container
* element and updates upon window resize events.
* @param {Ref} ref A React ref object within the `.vitessce-container`.
* @returns {array} `[width, height]` where width and height
* are numbers.
*/
export function useClosestVitessceContainerSize(ref) {
const [height, setHeight] = useState();
const [width, setWidth] = useState();
useEffect(() => {
function onWindowResize() {
if (ref.current) {
const {
clientHeight: componentHeight, clientWidth: componentWidth,
} = ref.current.closest('.vitessce-container');
setWidth(componentWidth);
setHeight(componentHeight);
}
}
const onResizeDebounced = debounce(onWindowResize, 100, { trailing: true });
window.addEventListener('resize', onResizeDebounced);
onWindowResize();
return () => {
window.removeEventListener('resize', onResizeDebounced);
};
}, [ref]);
return [width, height];
}
export function useExpressionValueGetter({ attrs, expressionData }) {
// Get a mapping from cell ID to row index in the gene expression matrix.
const cellIdMap = useMemo(() => {
const result = {};
if (attrs && attrs.rows) {
// eslint-disable-next-line no-plusplus
for (let i = 0; i < attrs.rows.length; i++) {
result[attrs.rows[i]] = i;
}
}
return result;
}, [attrs]);
// Set up a getter function for gene expression values, to be used
// by the DeckGL layer to obtain values for instanced attributes.
const getExpressionValue = useCallback((entry) => {
const cellId = entry[0];
if (cellIdMap && expressionData && expressionData[0]) {
const cellIndex = cellIdMap[cellId];
const val = expressionData[0][cellIndex];
return val;
}
return 0;
}, [cellIdMap, expressionData]);
return getExpressionValue;
}