From b56f4add2666256cc806c5748f98e350ad9d6040 Mon Sep 17 00:00:00 2001 From: Trevor Manz Date: Tue, 14 Nov 2023 13:45:03 -0500 Subject: [PATCH] fix(avivator/ome-zarr): handle missiing selectable dimension (#713) --- CHANGELOG.md | 1 + sites/avivator/src/constants.js | 2 +- sites/avivator/src/hooks.js | 2 +- sites/avivator/src/utils.js | 82 ++++++++++++++++++++++++--------- 4 files changed, 62 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c8efbc5c..1f6491f76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added ### Changed +- Fix Avivator demo for OME-Zarr with only spatial axes ## 0.14.1 diff --git a/sites/avivator/src/constants.js b/sites/avivator/src/constants.js index 03cad34fd..5a4816d43 100644 --- a/sites/avivator/src/constants.js +++ b/sites/avivator/src/constants.js @@ -26,6 +26,6 @@ export const COLOR_PALLETE = [ [255, 255, 255], [255, 0, 0] ]; -export const GLOBAL_SLIDER_DIMENSION_FIELDS = ['z', 't']; +export const GLOBAL_SLIDER_DIMENSION_FIELDS = /** @type {const} */ (['z', 't']); export const INITIAL_SLIDER_VALUE = [1500, 20000]; export const FILL_PIXEL_VALUE = '----'; diff --git a/sites/avivator/src/hooks.js b/sites/avivator/src/hooks.js index 527449f00..edd59172c 100644 --- a/sites/avivator/src/hooks.js +++ b/sites/avivator/src/hooks.js @@ -136,7 +136,7 @@ export const useImage = (source, history) => { ? [[255, 255, 255]] : newDomains.map( (_, i) => - (Channels[i].Color && Channels[i].Color.slice(0, -1)) ?? + (Channels[i]?.Color && Channels[i].Color.slice(0, -1)) ?? COLOR_PALLETE[i] ); useViewerStore.setState({ diff --git a/sites/avivator/src/utils.js b/sites/avivator/src/utils.js index 78c7a1fd2..cec9b5d13 100644 --- a/sites/avivator/src/utils.js +++ b/sites/avivator/src/utils.js @@ -246,24 +246,33 @@ export function getNameFromUrl(url) { /** * Return the midpoint of the global dimensions as a default selection. * - * @param { import('../../src/types').PixelSource<['t', 'z', 'c']> } pixelSource + * @param {{ name: string, size: number }[]} dimensions + * @returns {{ [Key in typeof GLOBAL_SLIDER_DIMENSION_FIELDS[number]]?: number } */ -function getDefaultGlobalSelection({ labels, shape }) { - const dims = labels - .map((name, i) => [name, i]) - .filter(d => GLOBAL_SLIDER_DIMENSION_FIELDS.includes(d[0])); - - /** - * @type { { t: number, z: number, c: number } } - */ +function getDefaultGlobalSelection(dimensions) { + const globalSelectableDimensions = dimensions.filter(d => + GLOBAL_SLIDER_DIMENSION_FIELDS.includes(d.name.toLowerCase()) + ); + + /** @type {{ [Key in typeof GLOBAL_SLIDER_DIMENSION_FIELDS[number]]?: number } */ const selection = {}; - dims.forEach(([name, index]) => { - selection[name] = Math.floor((shape[index] || 0) / 2); - }); + for (const dim of globalSelectableDimensions) { + selection[dim.name] = Math.floor(dim.size / 2); + } return selection; } +function isGlobalOrXYDimension(name) { + // normalize name to lowercase + name = name.toLowerCase(); + return ( + name === 'x' || + name === 'y' || + GLOBAL_SLIDER_DIMENSION_FIELDS.includes(name) + ); +} + /** * @param {Array.} shape loader shape */ @@ -272,31 +281,58 @@ export function isInterleaved(shape) { return lastDimSize === 3 || lastDimSize === 4; } +/** + * @template A + * @template B + * @param {Array} a + * @param {Array} b + * @returns {Array<[A, B]>} + */ +function zip(a, b) { + if (a.length !== b.length) { + throw new Error('Array lengths must be equal'); + } + return a.map((val, i) => [val, b[i]]); +} + // Create a default selection using the midpoint of the available global dimensions, // and then the first four available selections from the first selectable channel. /** * - * @param { import('../../src/types').PixelSource<['t', 'z', 'c']> } pixelSource + * @param {{ labels: string[], shape: number[] }} pixelSource */ -export function buildDefaultSelection(pixelSource) { +export function buildDefaultSelection({ labels, shape }) { let selection = []; - const globalSelection = getDefaultGlobalSelection(pixelSource); + + const dimensions = zip(labels, shape).map(([name, size]) => ({ name, size })); + + const globalSelection = getDefaultGlobalSelection(dimensions); + // First non-global dimension with some sort of selectable values. + const firstNonGlobalSelectableDimension = dimensions.find( + dim => !isGlobalOrXYDimension(dim.name) + ); - const firstNonGlobalDimension = pixelSource.labels - .map((name, i) => ({ name, size: pixelSource.shape[i] })) - .find(d => !GLOBAL_SLIDER_DIMENSION_FIELDS.includes(d.name) && d.size); + // If there are no additional selectable dimensions, return the global selection. + if (!firstNonGlobalSelectableDimension) { + return [globalSelection]; + } - for (let i = 0; i < Math.min(4, firstNonGlobalDimension.size); i += 1) { + for ( + let i = 0; + i < Math.min(4, firstNonGlobalSelectableDimension.size); + i += 1 + ) { selection.push({ - [firstNonGlobalDimension.name]: i, + [firstNonGlobalSelectableDimension.name]: i, ...globalSelection }); } - selection = isInterleaved(pixelSource.shape) - ? [{ ...selection[0], c: 0 }] - : selection; + if (isInterleaved(shape)) { + return [{ ...selection[0], c: 0 }]; + } + return selection; }