From e867a6ac4ccbcb00175ed48f0493e1a40d60eb58 Mon Sep 17 00:00:00 2001 From: Alex Surtees Date: Mon, 16 Mar 2026 15:07:56 +0000 Subject: [PATCH 1/9] Add plugin --- anndata-zarr/.eslintrc.json | 75 ++++++++ anndata-zarr/.gitignore | 24 +++ anndata-zarr/package.json | 45 +++++ .../src/components/AnndataController.jsx | 43 +++++ anndata-zarr/src/components/FeatureSelect.jsx | 115 +++++++++++++ anndata-zarr/src/components/Legend.jsx | 32 ++++ anndata-zarr/src/components/ObsSelect.jsx | 160 +++++++++++++++++ anndata-zarr/src/constants/colorscales.js | 138 +++++++++++++++ anndata-zarr/src/hooks.js | 113 ++++++++++++ anndata-zarr/src/index.css | 37 ++++ anndata-zarr/src/index.js | 6 + anndata-zarr/src/provider.jsx | 11 ++ anndata-zarr/src/utils.js | 161 ++++++++++++++++++ anndata-zarr/vite.config.js | 34 ++++ viewer/src/components/Viewer.tsx | 1 - 15 files changed, 994 insertions(+), 1 deletion(-) create mode 100644 anndata-zarr/.eslintrc.json create mode 100644 anndata-zarr/.gitignore create mode 100644 anndata-zarr/package.json create mode 100644 anndata-zarr/src/components/AnndataController.jsx create mode 100644 anndata-zarr/src/components/FeatureSelect.jsx create mode 100644 anndata-zarr/src/components/Legend.jsx create mode 100644 anndata-zarr/src/components/ObsSelect.jsx create mode 100644 anndata-zarr/src/constants/colorscales.js create mode 100644 anndata-zarr/src/hooks.js create mode 100644 anndata-zarr/src/index.css create mode 100644 anndata-zarr/src/index.js create mode 100644 anndata-zarr/src/provider.jsx create mode 100644 anndata-zarr/src/utils.js create mode 100644 anndata-zarr/vite.config.js diff --git a/anndata-zarr/.eslintrc.json b/anndata-zarr/.eslintrc.json new file mode 100644 index 00000000..b672f57c --- /dev/null +++ b/anndata-zarr/.eslintrc.json @@ -0,0 +1,75 @@ +{ + "extends": [ + "react-app", + "prettier", + "plugin:import/errors", + "plugin:import/warnings", + "plugin:prettier/recommended" + ], + "settings": { + "import/resolver": { + "node": { + "extensions": [ + ".js", + ".jsx", + ".ts", + ".tsx" + ] + }, + "alias": { + "map": [ + [ + "@app", + "./src" + ] + ], + "extensions": [ + ".js", + ".jsx", + ".ts", + ".tsx" + ] + } + } + }, + "rules": { + "import/order": [ + "error", + { + "groups": [ + "builtin", + "external", + "internal", + [ + "parent", + "sibling" + ], + "index" + ], + "pathGroups": [ + { + "pattern": "react", + "group": "external", + "position": "before" + } + ], + "pathGroupsExcludedImportTypes": [ + "react" + ], + "newlines-between": "always", + "alphabetize": { + "order": "asc", + "caseInsensitive": true + } + } + ], + "prettier/prettier": [ + "error", + { + "singleQuote": true, + "tabWidth": 2, + "useTabs": false + } + ] + } +} \ No newline at end of file diff --git a/anndata-zarr/.gitignore b/anndata-zarr/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/anndata-zarr/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/anndata-zarr/package.json b/anndata-zarr/package.json new file mode 100644 index 00000000..8adb6011 --- /dev/null +++ b/anndata-zarr/package.json @@ -0,0 +1,45 @@ +{ + "name": "@biongff/anndata-zarr", + "private": true, + "version": "0.0.0", + "type": "module", + "main": "dist/biongff-anndata-zarr.cjs.js", + "module": "dist/biongff-anndata-zarr.es.js", + "files": [ + "dist" + ], + "exports": { + ".": { + "import": "./dist/biongff-anndata-zarr.es.js", + "require": "./dist/biongff-anndata-zarr.cjs.js" + }, + "./dist/anndata-zarr.css": "./dist/anndata-zarr.css" + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "preview": "vite preview", + "test": "vitest" + }, + "dependencies": { + "@tanstack/react-query": "^5.85.3", + "lodash": "^4.17.21", + "react-window": "^2.0.2", + "zarrita": "0.5.0" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "@mui/icons-material": "^7.2.0", + "@mui/material": "^7.2.0", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.3", + "vite": "^6.2.3", + "vitest": "^3.0.8" + } +} diff --git a/anndata-zarr/src/components/AnndataController.jsx b/anndata-zarr/src/components/AnndataController.jsx new file mode 100644 index 00000000..131ae6c7 --- /dev/null +++ b/anndata-zarr/src/components/AnndataController.jsx @@ -0,0 +1,43 @@ +import React, { useState } from 'react'; + +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; + +import { FeatureSelect } from './FeatureSelect'; +import { ObsSelect } from './ObsSelect'; + +export const AnndataController = ({ adata, callback = () => {} }) => { + const [feature, setFeature] = useState(null); + const [obsCol, setObsCol] = useState(null); + + const handleFeatureSelect = (f) => { + setFeature(f); + setObsCol(null); + }; + + const handleObsSelect = (o) => { + setObsCol(o); + setFeature(null); + }; + + return ( + + + + + + + + + ); +}; diff --git a/anndata-zarr/src/components/FeatureSelect.jsx b/anndata-zarr/src/components/FeatureSelect.jsx new file mode 100644 index 00000000..23b4732b --- /dev/null +++ b/anndata-zarr/src/components/FeatureSelect.jsx @@ -0,0 +1,115 @@ +import React, { useEffect, useMemo, useState } from 'react'; + +import Box from '@mui/material/Box'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemText from '@mui/material/ListItemText'; +import Stack from '@mui/material/Stack'; +import TextField from '@mui/material/TextField'; +import { List } from 'react-window'; + +import { useAnndataColors, useAnndataFeatures } from '../hooks'; +import { Legend } from './Legend'; + +const RowComponent = ({ index, items, style, onSelect, selectedIndex }) => { + return ( + + onSelect({ index: items[index].matrixIndex })} + selected={items[index].matrixIndex === selectedIndex} + > + + + + ); +}; + +export const FeatureSelect = ({ + adata, + feature, + onSelect, + callback = () => {}, +}) => { + const [searchTerm, setSearchTerm] = useState(''); + + const { data, isLoading, serverError } = useAnndataFeatures(adata); + const colorData = useAnndataColors( + { + ...adata, + matrixProps: { + feature: feature, + }, + }, + { enabled: !!feature }, + ); + + useEffect(() => { + if (colorData?.serverError) { + callback(null); + return; + } + if (!colorData?.isLoading && colorData?.data) { + callback(colorData.data.colors); + } + }, [colorData, callback]); + + const items = useMemo(() => { + if (!data) return []; + const allItems = data.map((name, index) => ({ + name, + matrixIndex: index, + })); + if (!searchTerm) return allItems; + return allItems.filter((item) => + item.name.toLowerCase().includes(searchTerm.toLowerCase()), + ); + }, [data, searchTerm]); + + const legend = useMemo(() => { + if (colorData?.serverError || colorData?.isLoading || !colorData?.data) { + return null; + } + const { min, max, colorscale } = colorData.data; + return ; + }, [colorData.data, colorData?.isLoading, colorData?.serverError]); + + if (isLoading) { + return <>; + } + if (serverError) { + return
Error loading features
; + } + return ( + + + setSearchTerm(e.target.value)} + /> + + {!!feature && legend} + + + ); +}; diff --git a/anndata-zarr/src/components/Legend.jsx b/anndata-zarr/src/components/Legend.jsx new file mode 100644 index 00000000..786bb58e --- /dev/null +++ b/anndata-zarr/src/components/Legend.jsx @@ -0,0 +1,32 @@ +import React, { useMemo } from 'react'; + +import _ from 'lodash'; + +import { getColor } from '../utils'; +import '../index.css'; + +export const Legend = ({ min, max, colorscale }) => { + const spanList = useMemo(() => { + return _.range(100).map((i) => { + const color = getColor({ value: i / 100, colorscale }); + return ( + + ); + }); + }, [colorscale]); + + return ( +
+
+ {spanList} + {min.toFixed(2)} + {((min + max) / 2).toFixed(2)} + {max.toFixed(2)} +
+
+ ); +}; diff --git a/anndata-zarr/src/components/ObsSelect.jsx b/anndata-zarr/src/components/ObsSelect.jsx new file mode 100644 index 00000000..84506a55 --- /dev/null +++ b/anndata-zarr/src/components/ObsSelect.jsx @@ -0,0 +1,160 @@ +import React, { useEffect, useMemo, useState } from 'react'; + +import ExpandLess from '@mui/icons-material/ExpandLess'; +import ExpandMore from '@mui/icons-material/ExpandMore'; +import Alert from '@mui/material/Alert'; +import Box from '@mui/material/Box'; +import Collapse from '@mui/material/Collapse'; +import Divider from '@mui/material/Divider'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import Stack from '@mui/material/Stack'; + +import { COLORSCALES } from '../constants/colorscales'; +import { useAnndataColors, useAnndataObs } from '../hooks'; +import { getColor } from '../utils'; +import { Legend } from './Legend'; + +// @TODO: fix styling (width) +const CategoricalCol = ({ col, showColor = false }) => { + const [open, setOpen] = useState(false); + const { categories } = col; + + return ( + + setOpen(!open)} + sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} + > + e.stopPropagation()} />} + label={col.name} + key={col.name} + value={col.name} + /> + {open ? : } + + + {categories.length > 100 && ( + + Truncated to 100 categories + + )} + + {categories.slice(0, 100).map((cat, i) => ( + + {showColor && ( + + + + )} + + + ))} + + + + ); +}; + +const NumericalCol = ({ col }) => { + return ( + } + label={col.name} + key={col.name} + value={col.name} + /> + ); +}; + +export const ObsSelect = ({ adata, obsCol, onSelect, callback = () => {} }) => { + const { data, isLoading, serverError } = useAnndataObs(adata); + const colorData = useAnndataColors( + { + ...adata, + matrixProps: { + obs: { col: obsCol }, + }, + }, + { + enabled: !!obsCol, + }, + ); + + useEffect(() => { + if (colorData?.serverError) { + callback(null); + return; + } + if (!colorData?.isLoading && colorData?.data) { + callback(colorData.data.colors); + } + }, [colorData, callback]); + + const legend = useMemo(() => { + if (colorData?.serverError || colorData?.isLoading || !colorData?.data) { + return null; + } + const { min, max, colorscale, categories } = colorData.data; + if (categories) { + return null; + } + return ; + }, [colorData.data, colorData?.isLoading, colorData?.serverError]); + + if (isLoading) { + return <>; + } + if (serverError) { + return
Error loading obs
; + } + return ( + + + Observations + + + onSelect(e.target.value)} + > + Categorical + {data.categorical.map((col) => ( + + ))} + Numerical + {data.numerical.map((col) => ( + + ))} + + + + {!!obsCol && legend} + + + ); +}; diff --git a/anndata-zarr/src/constants/colorscales.js b/anndata-zarr/src/constants/colorscales.js new file mode 100644 index 00000000..3834d121 --- /dev/null +++ b/anndata-zarr/src/constants/colorscales.js @@ -0,0 +1,138 @@ +// From plotly https://github.com/plotly/plotly.js/blob/5bc25b490702e5ed61265207833dbd58e8ab27f1/src/components/colorscale/scales.js +export const COLORSCALES = { + Greys: ['#000000', '#ffffff'], + + YlGnBu: [ + '#081d58', + '#253494', + '#225ea8', + '#1d91c0', + '#41b6c4', + '#7fcdbb', + '#c7e9b4', + '#edf8d9', + '#ffffd9', + ], + + Greens: [ + '#00441b', + '#006d2c', + '#238b45', + '#41ab5d', + '#74c476', + '#a1d9a5', + '#c7e9c0', + '#e5f5e0', + '#f7fcf5', + ], + + YlOrRd: [ + '#800026', + '#bd0026', + '#e31a1c', + '#fc4e2a', + '#fd8d3c', + '#feb24c', + '#fed976', + '#ffed9f', + '#ffffcc', + ], + + Bluered: ['#0000ff', '#ff0000'], + + RdBu: ['#050aac', '#6a89f7', '#bebebe', '#dcaa84', '#e6915a', '#b20a1c'], + + Reds: ['#dcdcdc', '#f5c39d', '#f5a069', '#b20a1c'], + + Blues: ['#050aac', '#283cba', '#4664f5', '#5a78f5', '#6a89f7', '#dcdcdc'], + + Picnic: [ + '#0000ff', + '#3399ff', + '#66ccff', + '#99ccff', + '#ccccff', + '#ffffff', + '#ffccff', + '#ff99ff', + '#ff66cc', + '#ff6666', + '#ff0000', + ], + + Rainbow: [ + '#96005a', + '#0000c8', + '#0019ff', + '#0098ff', + '#2cff96', + '#97ff00', + '#ffe600', + '#ff6f00', + '#ff0000', + ], + + Portland: ['#0c3383', '#0a88ba', '#f2d338', '#f28f38', '#d91e1e'], + + Jet: ['#000083', '#003caa', '#05ffff', '#ffff00', '#fa0000', '#800000'], + + Hot: ['#000000', '#e60000', '#ffd200', '#ffffff'], + + Blackbody: ['#000000', '#e60000', '#e6d200', '#ffffff', '#a0c8ff'], + + Earth: ['#000082', '#00b4b4', '#28d228', '#e6e632', '#784614', '#ffffff'], + + Electric: ['#000000', '#1e0064', '#780064', '#a05a00', '#e6c800', '#fffadc'], + + Viridis: [ + '#440154', + '#48186a', + '#472d7b', + '#424086', + '#3b528b', + '#33638d', + '#2c728e', + '#26828e', + '#21918c', + '#1fa088', + '#28ae80', + '#3fbc73', + '#5ec962', + '#84d44b', + '#addc30', + '#d8e219', + '#fde725', + ], + + Cividis: [ + '#00204c', + '#002a66', + '#00346e', + '#273f6c', + '#3c4a6c', + '#4c556b', + '#5b5f6d', + '#686a70', + '#757575', + '#838178', + '#929c78', + '#a19676', + '#b0a572', + '#c0af6d', + '#d1ba65', + '#e1c75c', + '#f3db4f', + '#ffe945', + ], + + Accent: [ + '#7fc97f', + '#beaed4', + '#fdc086', + '#ffff99', + '#386cb0', + '#f0027f', + '#bf5b17', + '#666666', + ], +}; diff --git a/anndata-zarr/src/hooks.js b/anndata-zarr/src/hooks.js new file mode 100644 index 00000000..76662c7c --- /dev/null +++ b/anndata-zarr/src/hooks.js @@ -0,0 +1,113 @@ +import { useCallback } from 'react'; + +import { useQueries, useQuery } from '@tanstack/react-query'; +import _ from 'lodash'; + +import { COLORSCALES } from './constants/colorscales'; +import { + fetchDataFromZarr, + getZarrPath, + getColors, + getVarNames, + getObs, +} from './utils'; + +const getAnndataColors = async (url, matrixProps, colorProps) => { + let zarrData; + try { + const zarrPath = await getZarrPath(url, matrixProps); + zarrData = await fetchDataFromZarr(zarrPath.url, zarrPath.path, zarrPath.s); + } catch (error) { + console.error(error); + return null; + } + if (!zarrData) return null; + + const { categories } = zarrData; + + const max = categories + ? categories.length - 1 + : colorProps?.max || _.max(zarrData.data); + const min = categories ? 0 : colorProps?.min || _.min(zarrData.data); + const colorscale = categories ? COLORSCALES.Accent : colorProps?.colorscale; + + return { + colors: getColors({ + data: zarrData.data, + max, + min, + colorProps: { ...colorProps, colorscale }, + categories, + }), + max, + min, + ...(categories ? { categories } : {}), + colorscale, + }; +}; + +export const useAnndataColors = (adata = { url: null }, opts = {}) => { + const { + data = null, + isLoading = false, + serverError = null, + } = useQuery({ + queryKey: ['anndataColor', adata.url, adata.matrixProps, adata.colorProps], + queryFn: () => + getAnndataColors(adata.url, adata.matrixProps, adata.colorProps), + ...opts, + }); + + return { data, isLoading, serverError }; +}; + +export const useAnndatasColors = (adatas = [], opts = {}) => { + const combine = useCallback((results) => { + return { + data: results.map((result) => result.data), + isLoading: results.some((result) => result.isLoading), + serverError: results.find((result) => result.error), + }; + }, []); + + const { + data = null, + isLoading = false, + serverError = null, + } = useQueries({ + queries: adatas.map(({ url, matrixProps, colorProps }) => ({ + queryKey: ['anndataColor', url, matrixProps, colorProps], + queryFn: () => getAnndataColors(url, matrixProps, colorProps), + })), + ...opts, + combine, + }); + + return { data, isLoading, serverError }; +}; + +export const useAnndataFeatures = (adata = { url: null, namesCol: null }) => { + const { + data = null, + isLoading = false, + serverError = null, + } = useQuery({ + queryKey: ['anndataFeatures', adata.url, adata.namesCol], + queryFn: () => getVarNames(adata.url, adata.namesCol), + }); + + return { data, isLoading, serverError }; +}; + +export const useAnndataObs = (adata = { url: null }) => { + const { + data = null, + isLoading = false, + serverError = null, + } = useQuery({ + queryKey: ['anndataObs', adata.url], + queryFn: () => getObs(adata.url), + }); + + return { data, isLoading, serverError }; +}; diff --git a/anndata-zarr/src/index.css b/anndata-zarr/src/index.css new file mode 100644 index 00000000..48548679 --- /dev/null +++ b/anndata-zarr/src/index.css @@ -0,0 +1,37 @@ +.grad-step { + display: inline-block; + height: 20px; + width: 1%; +} + +.gradient { + width: 100%; + white-space: nowrap; + position: relative; + display: inline-block; + top: 4px; + padding-bottom: 15px; +} + +.gradient .domain-min { + position: absolute; + left: 0; + font-size: 11px; + bottom: 3px; +} + +.gradient .domain-mid { + position: absolute; + right: 25%; + left: 25%; + text-align: center; + font-size: 11px; + bottom: 3px; +} + +.gradient .domain-max { + position: absolute; + right: 0; + font-size: 11px; + bottom: 3px; +} \ No newline at end of file diff --git a/anndata-zarr/src/index.js b/anndata-zarr/src/index.js new file mode 100644 index 00000000..ea88d0ef --- /dev/null +++ b/anndata-zarr/src/index.js @@ -0,0 +1,6 @@ +export { useAnndataColors } from './hooks'; +export { AnndataProvider } from './provider'; +export { COLORSCALES } from './constants/colorscales'; +export { FeatureSelect } from './components/FeatureSelect'; +export { ObsSelect } from './components/ObsSelect'; +export { AnndataController } from './components/AnndataController'; diff --git a/anndata-zarr/src/provider.jsx b/anndata-zarr/src/provider.jsx new file mode 100644 index 00000000..33d57582 --- /dev/null +++ b/anndata-zarr/src/provider.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +const queryClient = new QueryClient(); + +export function AnndataProvider({ children }) { + return ( + {children} + ); +} diff --git a/anndata-zarr/src/utils.js b/anndata-zarr/src/utils.js new file mode 100644 index 00000000..d8932cd7 --- /dev/null +++ b/anndata-zarr/src/utils.js @@ -0,0 +1,161 @@ +import _ from 'lodash'; +import { FetchStore, open, get } from 'zarrita'; + +import { COLORSCALES } from './constants/colorscales'; + +export const fetchDataFromZarr = async (url, path, s) => { + try { + const store = new FetchStore(url); + const node = await open(store, { kind: 'group' }); + + const dataNode = await open(node.resolve(path)); + let result; + if ( + dataNode.attrs?.['encoding-type'] === 'array' || + dataNode.attrs?.['encoding-type'] === 'string-array' + ) { + result = await get(dataNode, s); + } else if (dataNode.attrs?.['encoding-type'] === 'categorical') { + const categoriesArr = await open(dataNode.resolve('categories'), { + kind: 'array', + }); + const codesArr = await open(dataNode.resolve('codes'), { kind: 'array' }); + const { data: categories } = await get(categoriesArr); + const { data } = await get(codesArr, s); + result = { data, categories }; + } else { + throw new Error('Unsupported encoding-type'); + } + + return result; + } catch (error) { + throw error; + } +}; + +export const getVarNames = async (url, namesCol = '_index') => { + try { + const store = new FetchStore(url); + const node = await open(store, { kind: 'group' }); + + const arr = await open(node.resolve(`var/${namesCol}`, { kind: 'array' })); + const varNames = (await get(arr)).data; + return varNames; + } catch (error) { + console.error(error); + return []; + } +}; + +export const getObs = async (url) => { + try { + const store = new FetchStore(url); + const node = await open(store, { kind: 'group' }); + + const cols = (await open(node.resolve('obs', { kind: 'group' }))).attrs?.[ + 'column-order' + ]; + const obs = { categorical: [], numerical: [] }; + for (const col of cols) { + const dataNode = await open(node.resolve(`obs/${col}`)); + const { 'encoding-type': encodingType } = dataNode.attrs || {}; + if (encodingType === 'categorical') { + const categoriesArr = await open(dataNode.resolve('categories'), { + kind: 'array', + }); + const { data: categories } = await get(categoriesArr); + obs.categorical.push({ name: col, categories }); + } else if (encodingType === 'array') { + obs.numerical.push({ name: col }); + } + } + return obs; + } catch (error) { + console.error(error); + return []; + } +}; + +export const getVarIndex = async (url, varId, namesCol = '_index') => { + try { + const store = new FetchStore(url); + const node = await open(store, { kind: 'group' }); + + const arr = await open(node.resolve(`var/${namesCol}`, { kind: 'array' })); + const varNames = (await get(arr)).data; + const varIndex = varNames.findIndex((name) => name === varId); + return varIndex; + } catch (error) { + return -1; + } +}; + +export const getZarrPath = async (url, matrixProps) => { + const { feature, obs } = matrixProps; + if (feature) { + if (feature.index !== undefined && feature.index !== null) { + return { url, path: 'X', s: [null, feature.index] }; + } else if (feature.name) { + return { + url, + path: 'X', + s: [null, await getVarIndex(url, feature.name, feature.namesCol)], + }; + } + } + + if (obs) { + return { + url, + path: `obs/${obs.col}`, + s: null, + }; + } + + throw new Error('No feature or obs in matrixProps'); +}; + +const parseHexColor = (color) => { + const r = parseInt(color?.substring(1, 3), 16); + const g = parseInt(color?.substring(3, 5), 16); + const b = parseInt(color?.substring(5, 7), 16); + + return [r, g, b]; +}; + +const interpolateColor = (color1, color2, factor) => { + const [r1, g1, b1] = parseHexColor(color1); + const [r2, g2, b2] = parseHexColor(color2); + + const r = Math.round(r1 + factor * (r2 - r1)); + const g = Math.round(g1 + factor * (g2 - g1)); + const b = Math.round(b1 + factor * (b2 - b1)); + + return [r, g, b]; +}; + +const computeColor = (colormap, value) => { + if (!colormap || isNaN(value)) { + return [0, 0, 0, 255]; + } else if (value <= 0) { + return parseHexColor(colormap[0]); + } else if (value >= 1) { + return parseHexColor(colormap[colormap.length - 1]); + } + const index1 = Math.floor(value * (colormap.length - 1)); + const index2 = Math.ceil(value * (colormap.length - 1)); + const factor = (value * (colormap.length - 1)) % 1; + return interpolateColor(colormap[index1], colormap[index2], factor); +}; + +export const getColor = ({ value, colorscale = COLORSCALES.Viridis }) => { + return [...computeColor(colorscale, value), 255]; +}; + +export const getColors = ({ data, max, min, colorProps, categories }) => { + return _.map(data, (v, i) => ({ + labelValue: i + 1, + rgba: getColor({ value: (v - min) / (max - min), ...colorProps }), + value: categories ? categories[v] : v, + })); +}; diff --git a/anndata-zarr/vite.config.js b/anndata-zarr/vite.config.js new file mode 100644 index 00000000..9dc91aff --- /dev/null +++ b/anndata-zarr/vite.config.js @@ -0,0 +1,34 @@ +import path from 'path'; + +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + build: { + // outDir: path.resolve(__dirname, '../dist'), + lib: { + entry: path.resolve(__dirname, 'src/index.js'), + name: 'BiongffAnndataZarr', + formats: ['es', 'cjs'], + fileName: (format) => `biongff-anndata-zarr.${format}.js`, + }, + rollupOptions: { + external: [ + 'react', + 'react-dom', + '@mui/material', + '@mui/icons-material', + '@emotion/react', + '@emotion/styled', + ], + output: { + globals: { + react: 'React', + 'react-dom': 'ReactDOM', + }, + }, + }, + }, +}); diff --git a/viewer/src/components/Viewer.tsx b/viewer/src/components/Viewer.tsx index 39e1e3ae..f1b67161 100644 --- a/viewer/src/components/Viewer.tsx +++ b/viewer/src/components/Viewer.tsx @@ -20,7 +20,6 @@ export default function Viewer() { const firstLayer = layers[0] as VizarrLayer; const axisNavigationSnackbar = useAxisNavigation(deckRef, viewport); - const resetViewState = React.useCallback( (layer: VizarrLayer) => { const { deck } = deckRef.current || {}; From 4e574429d584ef93caf4f636d67b0c60756bfc77 Mon Sep 17 00:00:00 2001 From: Alex Surtees Date: Thu, 19 Mar 2026 13:06:21 +0000 Subject: [PATCH 2/9] Working prototype --- .../src/components/AnndataController.jsx | 4 +- anndata-zarr/src/components/FeatureSelect.jsx | 2 +- pnpm-lock.yaml | 327 ++++++++++++++++++ pnpm-workspace.yaml | 3 +- sites/app/package.json | 1 + sites/app/src/App.tsx | 49 ++- sites/app/src/index.css | 9 + sites/app/src/main.tsx | 1 + sites/app/vite.config.js | 5 +- viewer/src/components/VizarrViewer.tsx | 15 +- 10 files changed, 401 insertions(+), 15 deletions(-) create mode 100644 sites/app/src/index.css diff --git a/anndata-zarr/src/components/AnndataController.jsx b/anndata-zarr/src/components/AnndataController.jsx index 131ae6c7..ac2d2dfe 100644 --- a/anndata-zarr/src/components/AnndataController.jsx +++ b/anndata-zarr/src/components/AnndataController.jsx @@ -6,7 +6,7 @@ import Stack from '@mui/material/Stack'; import { FeatureSelect } from './FeatureSelect'; import { ObsSelect } from './ObsSelect'; -export const AnndataController = ({ adata, callback = () => {} }) => { +export const AnndataController = ({ adata, callback = () => { } }) => { const [feature, setFeature] = useState(null); const [obsCol, setObsCol] = useState(null); @@ -40,4 +40,4 @@ export const AnndataController = ({ adata, callback = () => {} }) => { ); -}; +} diff --git a/anndata-zarr/src/components/FeatureSelect.jsx b/anndata-zarr/src/components/FeatureSelect.jsx index 23b4732b..43b987af 100644 --- a/anndata-zarr/src/components/FeatureSelect.jsx +++ b/anndata-zarr/src/components/FeatureSelect.jsx @@ -29,7 +29,7 @@ export const FeatureSelect = ({ adata, feature, onSelect, - callback = () => {}, + callback = () => { }, }) => { const [searchTerm, setSearchTerm] = useState(''); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f2009ba..6d24b83e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,8 +62,54 @@ importers: specifier: ^4.0.15 version: 4.0.15(@types/node@24.3.0) + anndata-zarr: + dependencies: + '@emotion/react': + specifier: ^11.14.0 + version: 11.14.0(@types/react@18.3.18)(react@18.3.1) + '@emotion/styled': + specifier: ^11.14.1 + version: 11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1) + '@mui/icons-material': + specifier: ^7.2.0 + version: 7.3.4(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.18)(react@18.3.1) + '@mui/material': + specifier: ^7.2.0 + version: 7.3.4(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react@18.3.1))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-query': + specifier: ^5.85.3 + version: 5.90.21(react@18.3.1) + lodash: + specifier: ^4.17.21 + version: 4.17.21 + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + react-window: + specifier: ^2.0.2 + version: 2.2.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + zarrita: + specifier: 0.5.0 + version: 0.5.0 + devDependencies: + '@vitejs/plugin-react': + specifier: ^4.3.3 + version: 4.3.4(vite@6.2.7(@types/node@24.3.0)) + vite: + specifier: ^6.2.3 + version: 6.2.7(@types/node@24.3.0) + vitest: + specifier: ^3.0.8 + version: 3.2.4(@types/node@24.3.0) + sites/app: dependencies: + '@biongff/anndata-zarr': + specifier: workspace:* + version: link:../../anndata-zarr '@biongff/vizarr': specifier: workspace:* version: link:../../viewer @@ -1293,6 +1339,14 @@ packages: '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@tanstack/query-core@5.90.20': + resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==} + + '@tanstack/react-query@5.90.21': + resolution: {integrity: sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==} + peerDependencies: + react: ^18 || ^19 + '@turf/boolean-clockwise@5.1.5': resolution: {integrity: sha512-FqbmEEOJ4rU4/2t7FKx0HUWmjFEVqR+NJrFP7ymGSjja2SQ7Q91nnBihGuT+yuHHl6ElMjQ3ttsB/eTmyCycxA==} @@ -1454,9 +1508,23 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + '@vitest/expect@4.0.15': resolution: {integrity: sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w==} + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/mocker@4.0.15': resolution: {integrity: sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ==} peerDependencies: @@ -1468,18 +1536,33 @@ packages: vite: optional: true + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/pretty-format@4.0.15': resolution: {integrity: sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==} + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + '@vitest/runner@4.0.15': resolution: {integrity: sha512-+A+yMY8dGixUhHmNdPUxOh0la6uVzun86vAbuMT3hIDxMrAOmn5ILBHm8ajrqHE0t8R9T1dGnde1A5DTnmi3qw==} + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + '@vitest/snapshot@4.0.15': resolution: {integrity: sha512-A7Ob8EdFZJIBjLjeO0DZF4lqR6U7Ydi5/5LIZ0xcI+23lYlsYJAfGn8PrIWTYdZQRNnSRlzhg0zyGu37mVdy5g==} + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + '@vitest/spy@4.0.15': resolution: {integrity: sha512-+EIjOJmnY6mIfdXtE/bnozKEvTC4Uczg19yeZ2vtCz5Yyb0QQ31QWVQ8hswJ3Ysx/K2EqaNsVanjr//2+P3FHw==} + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@vitest/utils@4.0.15': resolution: {integrity: sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==} @@ -1680,6 +1763,10 @@ packages: resolution: {integrity: sha512-Bvx4xH00qweepGc43xFvMs5BKASXTbHaHm6+kDYIK9p/4iFwjATQkmPKHQSgJZzKbAymhztRbXUf1Nqhzl73/Q==} engines: {node: '>=0.10.0'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1690,6 +1777,10 @@ packages: cartocolor@5.0.2: resolution: {integrity: sha512-Ihb/wU5V6BVbHwapd8l/zg7bnhZ4YPFVfa7quSpL86lfkPJSf4YuNBT+EvesPRP5vSqhl6vZVsQJwCR8alBooQ==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chai@6.2.1: resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==} engines: {node: '>=18'} @@ -1713,6 +1804,10 @@ packages: charenc@0.0.2: resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -1911,6 +2006,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + deck.gl@9.0.41: resolution: {integrity: sha512-Exj/E18ZYWXdkZjz+gS+kSxll7kIpAO0dfOsI7jgMjdn6g0NWEv7W4GJIkPbGkr6E/GurAJRRC9gdjtTS/LKYA==} peerDependencies: @@ -1925,6 +2029,10 @@ packages: react-dom: optional: true + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -2409,6 +2517,9 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -2513,6 +2624,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2883,6 +2997,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + pbf@3.3.0: resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==} hasBin: true @@ -2985,6 +3103,12 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' + react-window@2.2.7: + resolution: {integrity: sha512-SH5nvfUQwGHYyriDUAOt7wfPsfG9Qxd6OdzQxl5oQ4dsSsUicqQvjV7dR+NqZ4coY0fUn3w1jnC5PwzIUWEg5w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -3202,6 +3326,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + strnum@1.1.2: resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} @@ -3280,6 +3407,9 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -3288,10 +3418,22 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + tinyrainbow@3.0.3: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3396,6 +3538,11 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite-plugin-dts@4.5.4: resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} peerDependencies: @@ -3445,6 +3592,34 @@ packages: yaml: optional: true + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vitest@4.0.15: resolution: {integrity: sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -4912,6 +5087,13 @@ snapshots: '@standard-schema/spec@1.0.0': {} + '@tanstack/query-core@5.90.20': {} + + '@tanstack/react-query@5.90.21(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.90.20 + react: 18.3.1 + '@turf/boolean-clockwise@5.1.5': dependencies: '@turf/helpers': 5.1.5 @@ -5147,6 +5329,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + '@vitest/expect@4.0.15': dependencies: '@standard-schema/spec': 1.0.0 @@ -5156,6 +5346,14 @@ snapshots: chai: 6.2.1 tinyrainbow: 3.0.3 + '@vitest/mocker@3.2.4(vite@6.2.7(@types/node@24.3.0))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.2.7(@types/node@24.3.0) + '@vitest/mocker@4.0.15(vite@6.2.7(@types/node@24.3.0))': dependencies: '@vitest/spy': 4.0.15 @@ -5164,23 +5362,49 @@ snapshots: optionalDependencies: vite: 6.2.7(@types/node@24.3.0) + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + '@vitest/pretty-format@4.0.15': dependencies: tinyrainbow: 3.0.3 + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + '@vitest/runner@4.0.15': dependencies: '@vitest/utils': 4.0.15 pathe: 2.0.3 + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + '@vitest/snapshot@4.0.15': dependencies: '@vitest/pretty-format': 4.0.15 magic-string: 0.30.21 pathe: 2.0.3 + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + '@vitest/spy@4.0.15': {} + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + '@vitest/utils@4.0.15': dependencies: '@vitest/pretty-format': 4.0.15 @@ -5421,6 +5645,8 @@ snapshots: buf-compare@1.0.1: {} + cac@6.7.14: {} + callsites@3.1.0: {} caniuse-lite@1.0.30001703: {} @@ -5429,6 +5655,14 @@ snapshots: dependencies: colorbrewer: 1.5.6 + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chai@6.2.1: {} chalk@2.4.2: @@ -5448,6 +5682,8 @@ snapshots: charenc@0.0.2: {} + check-error@2.1.3: {} + clean-stack@2.2.0: {} clean-stack@5.3.0: @@ -5637,6 +5873,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.3: + dependencies: + ms: 2.1.3 + deck.gl@9.0.41(@arcgis/core@4.32.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@deck.gl/aggregation-layers': 9.0.41(@deck.gl/core@9.0.41)(@deck.gl/layers@9.0.41(@deck.gl/core@9.0.41)(@loaders.gl/core@4.3.3)(@luma.gl/core@9.0.28)(@luma.gl/engine@9.0.28(@luma.gl/core@9.0.28)))(@luma.gl/core@9.0.28)(@luma.gl/engine@9.0.28(@luma.gl/core@9.0.28)) @@ -5660,6 +5900,8 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + deep-eql@5.0.2: {} + deep-extend@0.6.0: {} deep-strict-equal@0.2.0: @@ -6080,6 +6322,8 @@ snapshots: js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + js-yaml@4.1.0: dependencies: argparse: 2.0.1 @@ -6179,6 +6423,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.2.1: {} + lru-cache@10.4.3: {} lru-cache@11.2.2: {} @@ -6449,6 +6695,8 @@ snapshots: pathe@2.0.3: {} + pathval@2.0.1: {} + pbf@3.3.0: dependencies: ieee754: 1.2.1 @@ -6547,6 +6795,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-window@2.2.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -6797,6 +7050,10 @@ snapshots: strip-json-comments@3.1.1: {} + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + strnum@1.1.2: {} stylis@4.2.0: {} @@ -6869,6 +7126,8 @@ snapshots: tinybench@2.9.0: {} + tinyexec@0.3.2: {} + tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -6876,8 +7135,14 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + tinyrainbow@3.0.3: {} + tinyspy@4.0.4: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -6952,6 +7217,27 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + vite-node@3.2.4(@types/node@24.3.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.2.7(@types/node@24.3.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-plugin-dts@4.5.4(@types/node@24.3.0)(rollup@4.40.1)(typescript@5.8.2)(vite@6.2.7(@types/node@24.3.0)): dependencies: '@microsoft/api-extractor': 7.55.2(@types/node@24.3.0) @@ -6980,6 +7266,47 @@ snapshots: '@types/node': 24.3.0 fsevents: 2.3.3 + vitest@3.2.4(@types/node@24.3.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@6.2.7(@types/node@24.3.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 6.2.7(@types/node@24.3.0) + vite-node: 3.2.4(@types/node@24.3.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.3.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vitest@4.0.15(@types/node@24.3.0): dependencies: '@vitest/expect': 4.0.15 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 88f2d98a..b35b5f08 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ packages: - 'viewer' - - 'sites/*' \ No newline at end of file + - 'sites/*' + - 'anndata-zarr' diff --git a/sites/app/package.json b/sites/app/package.json index 73ff1897..8012e1db 100644 --- a/sites/app/package.json +++ b/sites/app/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@biongff/vizarr": "workspace:*", + "@biongff/anndata-zarr": "workspace:*", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@mui/icons-material": "^7.2.0", diff --git a/sites/app/src/App.tsx b/sites/app/src/App.tsx index 88695323..d8bc7c3c 100644 --- a/sites/app/src/App.tsx +++ b/sites/app/src/App.tsx @@ -1,6 +1,19 @@ import { type ViewState, Vizarr } from "@biongff/vizarr"; +import { AnndataProvider, AnndataController } from "@biongff/anndata-zarr" import debounce from "just-debounce-it"; import * as React from "react"; +import { ThemeProvider, createTheme } from '@mui/material/styles' +import CssBaseline from '@mui/material/CssBaseline' + + +const darkTheme = createTheme({ + palette: { + mode: 'dark', + }, + typography: { + fontSize: 12, + }, +}); function parseViewStateFromUrl(): ViewState | undefined { const url = new URL(window.location.href); @@ -17,18 +30,24 @@ function parseViewStateFromUrl(): ViewState | undefined { return undefined; } + export default function App() { const urlString = window.location.href; - const { sources, viewState } = React.useMemo(() => { + + const { sources, viewState, anndatas } = React.useMemo(() => { const url = new URL(urlString); const { searchParams } = url; return { sources: searchParams.getAll("source"), viewState: parseViewStateFromUrl(), + anndatas: searchParams + .getAll('anndata') + .map((v) => (v ? { url: v } : null)), }; }, [urlString]); + const [colors, setColors] = React.useState(() => Array(sources.length).fill(null)); // Debounced viewState change handler const handleViewStateChange = React.useMemo( () => @@ -45,10 +64,30 @@ export default function App() { }, 200), [], ); - + const selectCallback = (colorData: any, i: any) => { + setColors((prev) => { + return prev.map((c, ci) => (ci === i ? colorData : c)); + }); + }; + const anndataControllers = React.useMemo(() => { + return sources.map((_s, i) => { + if (!anndatas?.[i]?.url) return null; + return ( + selectCallback(colorData, i)} + /> + ); + }); + }, [anndatas, sources]); return ( -
- -
+ + + +
{anndataControllers}
+ +
+
); } diff --git a/sites/app/src/index.css b/sites/app/src/index.css new file mode 100644 index 00000000..308a6312 --- /dev/null +++ b/sites/app/src/index.css @@ -0,0 +1,9 @@ +.container-right { + position: absolute; + top: 1rem; + right: 1rem; + bottom: 1rem; + z-index: 1; + padding: 0.5rem; + overflow-y: auto; +} diff --git a/sites/app/src/main.tsx b/sites/app/src/main.tsx index dc0390d2..df8831ae 100644 --- a/sites/app/src/main.tsx +++ b/sites/app/src/main.tsx @@ -2,6 +2,7 @@ import { version } from "@biongff/vizarr"; import React, { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import App from "./App"; +import './index.css' console.log(`vizarr v${version}: https://github.com/BioNGFF/vizarr`); diff --git a/sites/app/vite.config.js b/sites/app/vite.config.js index b176e62e..6b88a7d9 100644 --- a/sites/app/vite.config.js +++ b/sites/app/vite.config.js @@ -9,8 +9,9 @@ export default defineConfig(({ mode }) => ({ alias: { ...(mode === "development" ? { - "@biongff/vizarr": path.resolve(__dirname, "../../viewer/src/index.tsx"), - } + "@biongff/vizarr": path.resolve(__dirname, "../../viewer/src/index.tsx"), + "@biongff/anndata-zarr": path.resolve(__dirname, "../../anndata-zarr/src/index.js") + } : {}), }, }, diff --git a/viewer/src/components/VizarrViewer.tsx b/viewer/src/components/VizarrViewer.tsx index 91dbfb96..0f1d7e73 100644 --- a/viewer/src/components/VizarrViewer.tsx +++ b/viewer/src/components/VizarrViewer.tsx @@ -20,10 +20,12 @@ export interface VizarrViewerProps { sources?: string[]; viewState?: ViewState; onViewStateChange?: (viewState: ViewState) => void; + colours: []; } -function VizarrViewerComponent({ sources = [], viewState: initialViewState, onViewStateChange }: VizarrViewerProps) { +function VizarrViewerComponent({ sources = [], viewState: initialViewState, onViewStateChange, colours }: VizarrViewerProps) { const setSourceInfo = useSetAtom(sourceInfoAtom); + const sourceInfo = useAtomValue(sourceInfoAtom) const setViewStateAtom = useSetAtom(viewStateAtom); const sourceError = useAtomValue(sourceErrorAtom); const redirectObj = useAtomValue(redirectObjAtom); @@ -58,7 +60,7 @@ function VizarrViewerComponent({ sources = [], viewState: initialViewState, onVi ); React.useEffect(() => { - async function loadSources() { + async function loadSources(colours) { const results = await Promise.allSettled( configs.map(async (config, index) => { const sourceData = await createSourceData(config); @@ -72,6 +74,10 @@ function VizarrViewerComponent({ sources = [], viewState: initialViewState, onVi let sourceDatas = []; for (const res of results) { if (res.status === "fulfilled") { + + if (colours[0] !== null) { + res.value.labels[0].colors = colours[0] + } sourceDatas.push(res.value); } else { console.error(res.reason); @@ -81,9 +87,10 @@ function VizarrViewerComponent({ sources = [], viewState: initialViewState, onVi setSourceInfo(sourceDatas); } - loadSources(); - }, [configs, setSourceInfo]); + + loadSources(colours); + }, [configs, setSourceInfo, colours]); return ( <> {sourceError === null && redirectObj === null && ( From 1ce7c08302c3fbdaa702731c678caa24d309ea7a Mon Sep 17 00:00:00 2001 From: Alex Surtees Date: Mon, 23 Mar 2026 10:32:59 +0000 Subject: [PATCH 3/9] Inject colours into viewer --- pnpm-lock.yaml | 4 +- sites/app/package.json | 4 +- sites/app/src/App.tsx | 10 ++--- viewer/src/api.tsx | 7 ++++ viewer/src/components/VizarrViewer.tsx | 57 ++++---------------------- viewer/src/index.tsx | 2 +- viewer/src/io.ts | 34 ++++++++++++++- 7 files changed, 59 insertions(+), 59 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d24b83e..e55c50a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,10 +108,10 @@ importers: sites/app: dependencies: '@biongff/anndata-zarr': - specifier: workspace:* + specifier: workspace:anndata-zarr version: link:../../anndata-zarr '@biongff/vizarr': - specifier: workspace:* + specifier: workspace:viewer version: link:../../viewer '@emotion/react': specifier: ^11.14.0 diff --git a/sites/app/package.json b/sites/app/package.json index 8012e1db..506f786a 100644 --- a/sites/app/package.json +++ b/sites/app/package.json @@ -9,8 +9,8 @@ "check": "tsc" }, "dependencies": { - "@biongff/vizarr": "workspace:*", - "@biongff/anndata-zarr": "workspace:*", + "@biongff/vizarr": "workspace:viewer", + "@biongff/anndata-zarr": "workspace:anndata-zarr", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@mui/icons-material": "^7.2.0", diff --git a/sites/app/src/App.tsx b/sites/app/src/App.tsx index d8bc7c3c..02812aff 100644 --- a/sites/app/src/App.tsx +++ b/sites/app/src/App.tsx @@ -1,4 +1,4 @@ -import { type ViewState, Vizarr } from "@biongff/vizarr"; +import { type ViewState, type labelColor, Vizarr } from "@biongff/vizarr"; import { AnndataProvider, AnndataController } from "@biongff/anndata-zarr" import debounce from "just-debounce-it"; import * as React from "react"; @@ -30,11 +30,9 @@ function parseViewStateFromUrl(): ViewState | undefined { return undefined; } - export default function App() { const urlString = window.location.href; - const { sources, viewState, anndatas } = React.useMemo(() => { const url = new URL(urlString); const { searchParams } = url; @@ -47,7 +45,9 @@ export default function App() { }; }, [urlString]); - const [colors, setColors] = React.useState(() => Array(sources.length).fill(null)); + + + const [colors, setColors] = React.useState((): labelColor[][] => Array(sources.length).fill([])); // Debounced viewState change handler const handleViewStateChange = React.useMemo( () => @@ -86,7 +86,7 @@ export default function App() {
{anndataControllers}
- +
); diff --git a/viewer/src/api.tsx b/viewer/src/api.tsx index d6284bab..5afa231e 100644 --- a/viewer/src/api.tsx +++ b/viewer/src/api.tsx @@ -27,6 +27,13 @@ type Events = { export type { ViewState, ImageLayerConfig }; +export type labelColor = { + labelValue: number, + rgba: [r: number, g: number, b: number, a: number], + value?: string | number | null +} + + export interface VizarrViewer { addImage(config: ImageLayerConfig): void; setViewState(viewState: ViewState): void; diff --git a/viewer/src/components/VizarrViewer.tsx b/viewer/src/components/VizarrViewer.tsx index 0f1d7e73..b4fbbbcf 100644 --- a/viewer/src/components/VizarrViewer.tsx +++ b/viewer/src/components/VizarrViewer.tsx @@ -3,7 +3,7 @@ import { Box, Link, Typography } from "@mui/material"; import { type PrimitiveAtom, Provider, atom, useAtomValue, useSetAtom } from "jotai"; import React from "react"; import { ViewStateContext } from "../hooks"; -import { createSourceData } from "../io"; +import { loadSources } from "../io" import { type ImageLayerConfig, type ViewState, @@ -15,26 +15,24 @@ import { import theme from "../theme"; import Menu from "./Menu"; import Viewer from "./Viewer"; +import type { OmeColor } from "../layers/label-layer"; export interface VizarrViewerProps { sources?: string[]; viewState?: ViewState; onViewStateChange?: (viewState: ViewState) => void; - colours: []; + labelColours?: OmeColor[][]; } -function VizarrViewerComponent({ sources = [], viewState: initialViewState, onViewStateChange, colours }: VizarrViewerProps) { +function VizarrViewerComponent({ sources = [], viewState: initialViewState, onViewStateChange, labelColours }: VizarrViewerProps) { const setSourceInfo = useSetAtom(sourceInfoAtom); - const sourceInfo = useAtomValue(sourceInfoAtom) const setViewStateAtom = useSetAtom(viewStateAtom); const sourceError = useAtomValue(sourceErrorAtom); const redirectObj = useAtomValue(redirectObjAtom); - React.useEffect(() => { - if (initialViewState) { - setViewStateAtom(initialViewState); - } - }, [initialViewState, setViewStateAtom]); + if (initialViewState) { + setViewStateAtom(initialViewState); + } const viewStateAtomWithEffect: PrimitiveAtom = atom( (get) => get(viewStateAtom), @@ -50,47 +48,10 @@ function VizarrViewerComponent({ sources = [], viewState: initialViewState, onVi }, ); - const [configs] = React.useState( - sources.map((source, index) => { - const config: ImageLayerConfig = { - source: source, - }; - return config; - }), - ); - React.useEffect(() => { - async function loadSources(colours) { - const results = await Promise.allSettled( - configs.map(async (config, index) => { - const sourceData = await createSourceData(config); - const id = Math.random().toString(36).slice(2); - if (!sourceData.name) { - sourceData.name = `image_${index}`; - } - return { id, ...sourceData }; - }), - ); - let sourceDatas = []; - for (const res of results) { - if (res.status === "fulfilled") { - - if (colours[0] !== null) { - res.value.labels[0].colors = colours[0] - } - sourceDatas.push(res.value); - } else { - console.error(res.reason); - } - } - sourceDatas = sourceDatas.filter((s) => s !== null); - setSourceInfo(sourceDatas); - } - - + loadSources(sources, labelColours).then((sourceData) => setSourceInfo(sourceData)) + }, [sources, labelColours, setSourceInfo]); - loadSources(colours); - }, [configs, setSourceInfo, colours]); return ( <> {sourceError === null && redirectObj === null && ( diff --git a/viewer/src/index.tsx b/viewer/src/index.tsx index 5fe6d55e..9a674bfd 100644 --- a/viewer/src/index.tsx +++ b/viewer/src/index.tsx @@ -5,6 +5,6 @@ export { default as Vizarr } from "./components/VizarrViewer"; export type { VizarrViewerProps } from "./components/VizarrViewer"; export { createViewer } from "./api"; -export type { VizarrViewer } from "./api"; +export type { VizarrViewer, labelColor } from "./api"; export type { ViewState, ImageLayerConfig } from "./state"; diff --git a/viewer/src/io.ts b/viewer/src/io.ts index 6102e3e9..0bffdd70 100644 --- a/viewer/src/io.ts +++ b/viewer/src/io.ts @@ -3,7 +3,7 @@ import { ZarrPixelSource } from "./ZarrPixelSource"; import { loadOmeMultiscales, loadPlate, loadWell } from "./ome"; import * as utils from "./utils"; -import { DEFAULT_LABEL_OPACITY } from "./layers/label-layer"; +import { DEFAULT_LABEL_OPACITY, type OmeColor } from "./layers/label-layer"; import type { BaseLayerProps } from "./layers/viv-layers"; import type { ImageLayerConfig, LayerState, MultichannelConfig, SingleChannelConfig, SourceData } from "./state"; @@ -277,3 +277,35 @@ function getSourceSelectionTransform( ); }; } + +export async function loadSources(sources: string[], labelColors?: OmeColor[][]) { + const results = await Promise.allSettled( + sources.map(async (source, index) => { + const sourceData = await createSourceData({ source: source }); + const id = Math.random().toString(36).slice(2); + if (!sourceData.name) { + sourceData.name = `image_${index}`; + } + debugger; + if (labelColors && labelColors[index].length) { + if (!sourceData.labels) { + utils.assert("Feature colours provided but source image has no label!") + } else { + debugger; + sourceData.labels[0].colors = labelColors[index] + } + } + return { id, ...sourceData }; + }), + ); + let sourceDatas = []; + for (const res of results) { + if (res.status === "fulfilled") { + + sourceDatas.push(res.value); + } else { + console.error(res.reason); + } + } + return sourceDatas.filter((s) => s !== null); +} From ab4fc92a0d935eb68126975c46b048fab4599973 Mon Sep 17 00:00:00 2001 From: Alex Surtees Date: Wed, 25 Mar 2026 15:16:14 +0000 Subject: [PATCH 4/9] Show label layer when feature colours provided --- viewer/src/components/VizarrViewer.tsx | 5 ++++- viewer/src/io.ts | 12 +++++++----- viewer/src/state.ts | 9 +++++---- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/viewer/src/components/VizarrViewer.tsx b/viewer/src/components/VizarrViewer.tsx index b4fbbbcf..253e6692 100644 --- a/viewer/src/components/VizarrViewer.tsx +++ b/viewer/src/components/VizarrViewer.tsx @@ -49,7 +49,10 @@ function VizarrViewerComponent({ sources = [], viewState: initialViewState, onVi ); React.useEffect(() => { - loadSources(sources, labelColours).then((sourceData) => setSourceInfo(sourceData)) + loadSources(sources, labelColours).then((sourceData) => { + setSourceInfo(sourceData) + } + ) }, [sources, labelColours, setSourceInfo]); return ( diff --git a/viewer/src/io.ts b/viewer/src/io.ts index 0bffdd70..2e002ce2 100644 --- a/viewer/src/io.ts +++ b/viewer/src/io.ts @@ -225,7 +225,7 @@ export function initLayerStateFromSource(source: SourceData & { id: string }): L let labels = undefined; if (source.labels && source.labels.length > 0) { labels = source.labels.map((label, i) => ({ - on: false, + on: label.on ? label.on : false, transformSourceSelection: getSourceSelectionTransform(label.loader[0], source.loader[0]), layerProps: { id: `${source.id}_${i}`, @@ -286,13 +286,15 @@ export async function loadSources(sources: string[], labelColors?: OmeColor[][]) if (!sourceData.name) { sourceData.name = `image_${index}`; } - debugger; if (labelColors && labelColors[index].length) { - if (!sourceData.labels) { - utils.assert("Feature colours provided but source image has no label!") + if (!sourceData.labels || (!sourceData.labels.length)) { + console.log('Did not find source labels') + throw new Error('Feature colours provided but source image has no label!') } else { - debugger; + //Really not the best way to do this but the layer state is heavily wrapped up in + //being derived directly from the sourceData and would require a fairly large refactor to find sourceData.labels[0].colors = labelColors[index] + sourceData.labels[0].on = true } } return { id, ...sourceData }; diff --git a/viewer/src/state.ts b/viewer/src/state.ts index 9a85c7b4..15e9bc31 100644 --- a/viewer/src/state.ts +++ b/viewer/src/state.ts @@ -62,6 +62,7 @@ export type ImageLabels = Array<{ loader: ZarrPixelSource[]; modelMatrix: Matrix4; colors?: ReadonlyArray; + on?: boolean }>; export type SourceData = { @@ -184,10 +185,10 @@ const imageLabelsIstanceFamily = atomFamily((a: Atom) => return labels.map((label) => label.on ? new LabelLayer({ - ...label.layerProps, - selection: label.transformSourceSelection(layerProps.selections[0]), - pickable: true, - }) + ...label.layerProps, + selection: label.transformSourceSelection(layerProps.selections[0]), + pickable: true, + }) : null, ); }), From 108ccf7ef111d5d0a9e37e3346ec78c3801278c4 Mon Sep 17 00:00:00 2001 From: Alex Surtees Date: Thu, 26 Mar 2026 07:46:10 +0000 Subject: [PATCH 5/9] Linting --- anndata-zarr/.eslintrc.json | 116 +++++------- anndata-zarr/package.json | 4 +- .../src/components/AnndataController.jsx | 34 ++-- anndata-zarr/src/components/FeatureSelect.jsx | 39 ++-- anndata-zarr/src/components/Legend.jsx | 16 +- anndata-zarr/src/components/ObsSelect.jsx | 73 +++----- anndata-zarr/src/constants/colorscales.js | 175 +++++++----------- anndata-zarr/src/hooks.js | 31 ++-- anndata-zarr/src/index.css | 2 +- anndata-zarr/src/index.js | 12 +- anndata-zarr/src/provider.jsx | 8 +- anndata-zarr/src/utils.js | 63 +++---- anndata-zarr/vite.config.js | 25 +-- sites/app/src/App.tsx | 32 ++-- sites/app/src/main.tsx | 2 +- sites/app/vite.config.js | 6 +- viewer/src/api.tsx | 9 +- viewer/src/components/VizarrViewer.tsx | 18 +- viewer/src/io.ts | 13 +- viewer/src/state.ts | 10 +- viewer/tests/features.test.js | 59 +++--- 21 files changed, 296 insertions(+), 451 deletions(-) diff --git a/anndata-zarr/.eslintrc.json b/anndata-zarr/.eslintrc.json index b672f57c..69304d74 100644 --- a/anndata-zarr/.eslintrc.json +++ b/anndata-zarr/.eslintrc.json @@ -1,75 +1,43 @@ { - "extends": [ - "react-app", - "prettier", - "plugin:import/errors", - "plugin:import/warnings", - "plugin:prettier/recommended" - ], - "settings": { - "import/resolver": { - "node": { - "extensions": [ - ".js", - ".jsx", - ".ts", - ".tsx" - ] - }, - "alias": { - "map": [ - [ - "@app", - "./src" - ] - ], - "extensions": [ - ".js", - ".jsx", - ".ts", - ".tsx" - ] - } - } - }, - "rules": { - "import/order": [ - "error", - { - "groups": [ - "builtin", - "external", - "internal", - [ - "parent", - "sibling" - ], - "index" - ], - "pathGroups": [ - { - "pattern": "react", - "group": "external", - "position": "before" - } - ], - "pathGroupsExcludedImportTypes": [ - "react" - ], - "newlines-between": "always", - "alphabetize": { - "order": "asc", - "caseInsensitive": true - } - } - ], - "prettier/prettier": [ - "error", - { - "singleQuote": true, - "tabWidth": 2, - "useTabs": false - } - ] - } -} \ No newline at end of file + "extends": ["react-app", "prettier", "plugin:import/errors", "plugin:import/warnings", "plugin:prettier/recommended"], + "settings": { + "import/resolver": { + "node": { + "extensions": [".js", ".jsx", ".ts", ".tsx"] + }, + "alias": { + "map": [["@app", "./src"]], + "extensions": [".js", ".jsx", ".ts", ".tsx"] + } + } + }, + "rules": { + "import/order": [ + "error", + { + "groups": ["builtin", "external", "internal", ["parent", "sibling"], "index"], + "pathGroups": [ + { + "pattern": "react", + "group": "external", + "position": "before" + } + ], + "pathGroupsExcludedImportTypes": ["react"], + "newlines-between": "always", + "alphabetize": { + "order": "asc", + "caseInsensitive": true + } + } + ], + "prettier/prettier": [ + "error", + { + "singleQuote": true, + "tabWidth": 2, + "useTabs": false + } + ] + } +} diff --git a/anndata-zarr/package.json b/anndata-zarr/package.json index 8adb6011..bdd7650b 100644 --- a/anndata-zarr/package.json +++ b/anndata-zarr/package.json @@ -5,9 +5,7 @@ "type": "module", "main": "dist/biongff-anndata-zarr.cjs.js", "module": "dist/biongff-anndata-zarr.es.js", - "files": [ - "dist" - ], + "files": ["dist"], "exports": { ".": { "import": "./dist/biongff-anndata-zarr.es.js", diff --git a/anndata-zarr/src/components/AnndataController.jsx b/anndata-zarr/src/components/AnndataController.jsx index ac2d2dfe..5ea26f92 100644 --- a/anndata-zarr/src/components/AnndataController.jsx +++ b/anndata-zarr/src/components/AnndataController.jsx @@ -1,12 +1,12 @@ -import React, { useState } from 'react'; +import React, { useState } from "react"; -import Box from '@mui/material/Box'; -import Stack from '@mui/material/Stack'; +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; -import { FeatureSelect } from './FeatureSelect'; -import { ObsSelect } from './ObsSelect'; +import { FeatureSelect } from "./FeatureSelect"; +import { ObsSelect } from "./ObsSelect"; -export const AnndataController = ({ adata, callback = () => { } }) => { +export const AnndataController = ({ adata, callback = () => {} }) => { const [feature, setFeature] = useState(null); const [obsCol, setObsCol] = useState(null); @@ -21,23 +21,13 @@ export const AnndataController = ({ adata, callback = () => { } }) => { }; return ( - - - + + + - - + + ); -} +}; diff --git a/anndata-zarr/src/components/FeatureSelect.jsx b/anndata-zarr/src/components/FeatureSelect.jsx index 43b987af..6614f23e 100644 --- a/anndata-zarr/src/components/FeatureSelect.jsx +++ b/anndata-zarr/src/components/FeatureSelect.jsx @@ -1,21 +1,21 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from "react"; -import Box from '@mui/material/Box'; -import ListItem from '@mui/material/ListItem'; -import ListItemButton from '@mui/material/ListItemButton'; -import ListItemText from '@mui/material/ListItemText'; -import Stack from '@mui/material/Stack'; -import TextField from '@mui/material/TextField'; -import { List } from 'react-window'; +import Box from "@mui/material/Box"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemText from "@mui/material/ListItemText"; +import Stack from "@mui/material/Stack"; +import TextField from "@mui/material/TextField"; +import { List } from "react-window"; -import { useAnndataColors, useAnndataFeatures } from '../hooks'; -import { Legend } from './Legend'; +import { useAnndataColors, useAnndataFeatures } from "../hooks"; +import { Legend } from "./Legend"; const RowComponent = ({ index, items, style, onSelect, selectedIndex }) => { return ( onSelect({ index: items[index].matrixIndex })} selected={items[index].matrixIndex === selectedIndex} > @@ -25,13 +25,8 @@ const RowComponent = ({ index, items, style, onSelect, selectedIndex }) => { ); }; -export const FeatureSelect = ({ - adata, - feature, - onSelect, - callback = () => { }, -}) => { - const [searchTerm, setSearchTerm] = useState(''); +export const FeatureSelect = ({ adata, feature, onSelect, callback = () => {} }) => { + const [searchTerm, setSearchTerm] = useState(""); const { data, isLoading, serverError } = useAnndataFeatures(adata); const colorData = useAnndataColors( @@ -61,9 +56,7 @@ export const FeatureSelect = ({ matrixIndex: index, })); if (!searchTerm) return allItems; - return allItems.filter((item) => - item.name.toLowerCase().includes(searchTerm.toLowerCase()), - ); + return allItems.filter((item) => item.name.toLowerCase().includes(searchTerm.toLowerCase())); }, [data, searchTerm]); const legend = useMemo(() => { @@ -84,12 +77,12 @@ export const FeatureSelect = ({ - + { const spanList = useMemo(() => { return _.range(100).map((i) => { const color = getColor({ value: i / 100, colorscale }); - return ( - - ); + return ; }); }, [colorscale]); diff --git a/anndata-zarr/src/components/ObsSelect.jsx b/anndata-zarr/src/components/ObsSelect.jsx index 84506a55..f7b28012 100644 --- a/anndata-zarr/src/components/ObsSelect.jsx +++ b/anndata-zarr/src/components/ObsSelect.jsx @@ -1,25 +1,25 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from "react"; -import ExpandLess from '@mui/icons-material/ExpandLess'; -import ExpandMore from '@mui/icons-material/ExpandMore'; -import Alert from '@mui/material/Alert'; -import Box from '@mui/material/Box'; -import Collapse from '@mui/material/Collapse'; -import Divider from '@mui/material/Divider'; -import FormControl from '@mui/material/FormControl'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import ListItemText from '@mui/material/ListItemText'; -import Radio from '@mui/material/Radio'; -import RadioGroup from '@mui/material/RadioGroup'; -import Stack from '@mui/material/Stack'; +import ExpandLess from "@mui/icons-material/ExpandLess"; +import ExpandMore from "@mui/icons-material/ExpandMore"; +import Alert from "@mui/material/Alert"; +import Box from "@mui/material/Box"; +import Collapse from "@mui/material/Collapse"; +import Divider from "@mui/material/Divider"; +import FormControl from "@mui/material/FormControl"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import Radio from "@mui/material/Radio"; +import RadioGroup from "@mui/material/RadioGroup"; +import Stack from "@mui/material/Stack"; -import { COLORSCALES } from '../constants/colorscales'; -import { useAnndataColors, useAnndataObs } from '../hooks'; -import { getColor } from '../utils'; -import { Legend } from './Legend'; +import { COLORSCALES } from "../constants/colorscales"; +import { useAnndataColors, useAnndataObs } from "../hooks"; +import { getColor } from "../utils"; +import { Legend } from "./Legend"; // @TODO: fix styling (width) const CategoricalCol = ({ col, showColor = false }) => { @@ -28,10 +28,7 @@ const CategoricalCol = ({ col, showColor = false }) => { return ( - setOpen(!open)} - sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} - > + setOpen(!open)} sx={{ display: "flex", alignItems: "center", cursor: "pointer" }}> e.stopPropagation()} />} label={col.name} @@ -70,14 +67,7 @@ const CategoricalCol = ({ col, showColor = false }) => { }; const NumericalCol = ({ col }) => { - return ( - } - label={col.name} - key={col.name} - value={col.name} - /> - ); + return } label={col.name} key={col.name} value={col.name} />; }; export const ObsSelect = ({ adata, obsCol, onSelect, callback = () => {} }) => { @@ -125,26 +115,19 @@ export const ObsSelect = ({ adata, obsCol, onSelect, callback = () => {} }) => { - + Observations - - - onSelect(e.target.value)} - > + + + onSelect(e.target.value)}> Categorical {data.categorical.map((col) => ( - + ))} Numerical {data.numerical.map((col) => ( diff --git a/anndata-zarr/src/constants/colorscales.js b/anndata-zarr/src/constants/colorscales.js index 3834d121..4d7ec392 100644 --- a/anndata-zarr/src/constants/colorscales.js +++ b/anndata-zarr/src/constants/colorscales.js @@ -1,138 +1,89 @@ // From plotly https://github.com/plotly/plotly.js/blob/5bc25b490702e5ed61265207833dbd58e8ab27f1/src/components/colorscale/scales.js export const COLORSCALES = { - Greys: ['#000000', '#ffffff'], - - YlGnBu: [ - '#081d58', - '#253494', - '#225ea8', - '#1d91c0', - '#41b6c4', - '#7fcdbb', - '#c7e9b4', - '#edf8d9', - '#ffffd9', - ], + Greys: ["#000000", "#ffffff"], - Greens: [ - '#00441b', - '#006d2c', - '#238b45', - '#41ab5d', - '#74c476', - '#a1d9a5', - '#c7e9c0', - '#e5f5e0', - '#f7fcf5', - ], + YlGnBu: ["#081d58", "#253494", "#225ea8", "#1d91c0", "#41b6c4", "#7fcdbb", "#c7e9b4", "#edf8d9", "#ffffd9"], - YlOrRd: [ - '#800026', - '#bd0026', - '#e31a1c', - '#fc4e2a', - '#fd8d3c', - '#feb24c', - '#fed976', - '#ffed9f', - '#ffffcc', - ], + Greens: ["#00441b", "#006d2c", "#238b45", "#41ab5d", "#74c476", "#a1d9a5", "#c7e9c0", "#e5f5e0", "#f7fcf5"], - Bluered: ['#0000ff', '#ff0000'], + YlOrRd: ["#800026", "#bd0026", "#e31a1c", "#fc4e2a", "#fd8d3c", "#feb24c", "#fed976", "#ffed9f", "#ffffcc"], - RdBu: ['#050aac', '#6a89f7', '#bebebe', '#dcaa84', '#e6915a', '#b20a1c'], + Bluered: ["#0000ff", "#ff0000"], - Reds: ['#dcdcdc', '#f5c39d', '#f5a069', '#b20a1c'], + RdBu: ["#050aac", "#6a89f7", "#bebebe", "#dcaa84", "#e6915a", "#b20a1c"], - Blues: ['#050aac', '#283cba', '#4664f5', '#5a78f5', '#6a89f7', '#dcdcdc'], + Reds: ["#dcdcdc", "#f5c39d", "#f5a069", "#b20a1c"], + + Blues: ["#050aac", "#283cba", "#4664f5", "#5a78f5", "#6a89f7", "#dcdcdc"], Picnic: [ - '#0000ff', - '#3399ff', - '#66ccff', - '#99ccff', - '#ccccff', - '#ffffff', - '#ffccff', - '#ff99ff', - '#ff66cc', - '#ff6666', - '#ff0000', + "#0000ff", + "#3399ff", + "#66ccff", + "#99ccff", + "#ccccff", + "#ffffff", + "#ffccff", + "#ff99ff", + "#ff66cc", + "#ff6666", + "#ff0000", ], - Rainbow: [ - '#96005a', - '#0000c8', - '#0019ff', - '#0098ff', - '#2cff96', - '#97ff00', - '#ffe600', - '#ff6f00', - '#ff0000', - ], + Rainbow: ["#96005a", "#0000c8", "#0019ff", "#0098ff", "#2cff96", "#97ff00", "#ffe600", "#ff6f00", "#ff0000"], - Portland: ['#0c3383', '#0a88ba', '#f2d338', '#f28f38', '#d91e1e'], + Portland: ["#0c3383", "#0a88ba", "#f2d338", "#f28f38", "#d91e1e"], - Jet: ['#000083', '#003caa', '#05ffff', '#ffff00', '#fa0000', '#800000'], + Jet: ["#000083", "#003caa", "#05ffff", "#ffff00", "#fa0000", "#800000"], - Hot: ['#000000', '#e60000', '#ffd200', '#ffffff'], + Hot: ["#000000", "#e60000", "#ffd200", "#ffffff"], - Blackbody: ['#000000', '#e60000', '#e6d200', '#ffffff', '#a0c8ff'], + Blackbody: ["#000000", "#e60000", "#e6d200", "#ffffff", "#a0c8ff"], - Earth: ['#000082', '#00b4b4', '#28d228', '#e6e632', '#784614', '#ffffff'], + Earth: ["#000082", "#00b4b4", "#28d228", "#e6e632", "#784614", "#ffffff"], - Electric: ['#000000', '#1e0064', '#780064', '#a05a00', '#e6c800', '#fffadc'], + Electric: ["#000000", "#1e0064", "#780064", "#a05a00", "#e6c800", "#fffadc"], Viridis: [ - '#440154', - '#48186a', - '#472d7b', - '#424086', - '#3b528b', - '#33638d', - '#2c728e', - '#26828e', - '#21918c', - '#1fa088', - '#28ae80', - '#3fbc73', - '#5ec962', - '#84d44b', - '#addc30', - '#d8e219', - '#fde725', + "#440154", + "#48186a", + "#472d7b", + "#424086", + "#3b528b", + "#33638d", + "#2c728e", + "#26828e", + "#21918c", + "#1fa088", + "#28ae80", + "#3fbc73", + "#5ec962", + "#84d44b", + "#addc30", + "#d8e219", + "#fde725", ], Cividis: [ - '#00204c', - '#002a66', - '#00346e', - '#273f6c', - '#3c4a6c', - '#4c556b', - '#5b5f6d', - '#686a70', - '#757575', - '#838178', - '#929c78', - '#a19676', - '#b0a572', - '#c0af6d', - '#d1ba65', - '#e1c75c', - '#f3db4f', - '#ffe945', + "#00204c", + "#002a66", + "#00346e", + "#273f6c", + "#3c4a6c", + "#4c556b", + "#5b5f6d", + "#686a70", + "#757575", + "#838178", + "#929c78", + "#a19676", + "#b0a572", + "#c0af6d", + "#d1ba65", + "#e1c75c", + "#f3db4f", + "#ffe945", ], - Accent: [ - '#7fc97f', - '#beaed4', - '#fdc086', - '#ffff99', - '#386cb0', - '#f0027f', - '#bf5b17', - '#666666', - ], + Accent: ["#7fc97f", "#beaed4", "#fdc086", "#ffff99", "#386cb0", "#f0027f", "#bf5b17", "#666666"], }; diff --git a/anndata-zarr/src/hooks.js b/anndata-zarr/src/hooks.js index 76662c7c..73f9b702 100644 --- a/anndata-zarr/src/hooks.js +++ b/anndata-zarr/src/hooks.js @@ -1,16 +1,10 @@ -import { useCallback } from 'react'; +import { useCallback } from "react"; -import { useQueries, useQuery } from '@tanstack/react-query'; -import _ from 'lodash'; +import { useQueries, useQuery } from "@tanstack/react-query"; +import _ from "lodash"; -import { COLORSCALES } from './constants/colorscales'; -import { - fetchDataFromZarr, - getZarrPath, - getColors, - getVarNames, - getObs, -} from './utils'; +import { COLORSCALES } from "./constants/colorscales"; +import { fetchDataFromZarr, getColors, getObs, getVarNames, getZarrPath } from "./utils"; const getAnndataColors = async (url, matrixProps, colorProps) => { let zarrData; @@ -25,9 +19,7 @@ const getAnndataColors = async (url, matrixProps, colorProps) => { const { categories } = zarrData; - const max = categories - ? categories.length - 1 - : colorProps?.max || _.max(zarrData.data); + const max = categories ? categories.length - 1 : colorProps?.max || _.max(zarrData.data); const min = categories ? 0 : colorProps?.min || _.min(zarrData.data); const colorscale = categories ? COLORSCALES.Accent : colorProps?.colorscale; @@ -52,9 +44,8 @@ export const useAnndataColors = (adata = { url: null }, opts = {}) => { isLoading = false, serverError = null, } = useQuery({ - queryKey: ['anndataColor', adata.url, adata.matrixProps, adata.colorProps], - queryFn: () => - getAnndataColors(adata.url, adata.matrixProps, adata.colorProps), + queryKey: ["anndataColor", adata.url, adata.matrixProps, adata.colorProps], + queryFn: () => getAnndataColors(adata.url, adata.matrixProps, adata.colorProps), ...opts, }); @@ -76,7 +67,7 @@ export const useAnndatasColors = (adatas = [], opts = {}) => { serverError = null, } = useQueries({ queries: adatas.map(({ url, matrixProps, colorProps }) => ({ - queryKey: ['anndataColor', url, matrixProps, colorProps], + queryKey: ["anndataColor", url, matrixProps, colorProps], queryFn: () => getAnndataColors(url, matrixProps, colorProps), })), ...opts, @@ -92,7 +83,7 @@ export const useAnndataFeatures = (adata = { url: null, namesCol: null }) => { isLoading = false, serverError = null, } = useQuery({ - queryKey: ['anndataFeatures', adata.url, adata.namesCol], + queryKey: ["anndataFeatures", adata.url, adata.namesCol], queryFn: () => getVarNames(adata.url, adata.namesCol), }); @@ -105,7 +96,7 @@ export const useAnndataObs = (adata = { url: null }) => { isLoading = false, serverError = null, } = useQuery({ - queryKey: ['anndataObs', adata.url], + queryKey: ["anndataObs", adata.url], queryFn: () => getObs(adata.url), }); diff --git a/anndata-zarr/src/index.css b/anndata-zarr/src/index.css index 48548679..ada27f24 100644 --- a/anndata-zarr/src/index.css +++ b/anndata-zarr/src/index.css @@ -34,4 +34,4 @@ right: 0; font-size: 11px; bottom: 3px; -} \ No newline at end of file +} diff --git a/anndata-zarr/src/index.js b/anndata-zarr/src/index.js index ea88d0ef..1a061206 100644 --- a/anndata-zarr/src/index.js +++ b/anndata-zarr/src/index.js @@ -1,6 +1,6 @@ -export { useAnndataColors } from './hooks'; -export { AnndataProvider } from './provider'; -export { COLORSCALES } from './constants/colorscales'; -export { FeatureSelect } from './components/FeatureSelect'; -export { ObsSelect } from './components/ObsSelect'; -export { AnndataController } from './components/AnndataController'; +export { useAnndataColors } from "./hooks"; +export { AnndataProvider } from "./provider"; +export { COLORSCALES } from "./constants/colorscales"; +export { FeatureSelect } from "./components/FeatureSelect"; +export { ObsSelect } from "./components/ObsSelect"; +export { AnndataController } from "./components/AnndataController"; diff --git a/anndata-zarr/src/provider.jsx b/anndata-zarr/src/provider.jsx index 33d57582..c250d401 100644 --- a/anndata-zarr/src/provider.jsx +++ b/anndata-zarr/src/provider.jsx @@ -1,11 +1,9 @@ -import React from 'react'; +import React from "react"; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const queryClient = new QueryClient(); export function AnndataProvider({ children }) { - return ( - {children} - ); + return {children}; } diff --git a/anndata-zarr/src/utils.js b/anndata-zarr/src/utils.js index d8932cd7..881c32ba 100644 --- a/anndata-zarr/src/utils.js +++ b/anndata-zarr/src/utils.js @@ -1,30 +1,27 @@ -import _ from 'lodash'; -import { FetchStore, open, get } from 'zarrita'; +import _ from "lodash"; +import { FetchStore, get, open } from "zarrita"; -import { COLORSCALES } from './constants/colorscales'; +import { COLORSCALES } from "./constants/colorscales"; export const fetchDataFromZarr = async (url, path, s) => { try { const store = new FetchStore(url); - const node = await open(store, { kind: 'group' }); + const node = await open(store, { kind: "group" }); const dataNode = await open(node.resolve(path)); let result; - if ( - dataNode.attrs?.['encoding-type'] === 'array' || - dataNode.attrs?.['encoding-type'] === 'string-array' - ) { + if (dataNode.attrs?.["encoding-type"] === "array" || dataNode.attrs?.["encoding-type"] === "string-array") { result = await get(dataNode, s); - } else if (dataNode.attrs?.['encoding-type'] === 'categorical') { - const categoriesArr = await open(dataNode.resolve('categories'), { - kind: 'array', + } else if (dataNode.attrs?.["encoding-type"] === "categorical") { + const categoriesArr = await open(dataNode.resolve("categories"), { + kind: "array", }); - const codesArr = await open(dataNode.resolve('codes'), { kind: 'array' }); + const codesArr = await open(dataNode.resolve("codes"), { kind: "array" }); const { data: categories } = await get(categoriesArr); const { data } = await get(codesArr, s); result = { data, categories }; } else { - throw new Error('Unsupported encoding-type'); + throw new Error("Unsupported encoding-type"); } return result; @@ -33,12 +30,12 @@ export const fetchDataFromZarr = async (url, path, s) => { } }; -export const getVarNames = async (url, namesCol = '_index') => { +export const getVarNames = async (url, namesCol = "_index") => { try { const store = new FetchStore(url); - const node = await open(store, { kind: 'group' }); + const node = await open(store, { kind: "group" }); - const arr = await open(node.resolve(`var/${namesCol}`, { kind: 'array' })); + const arr = await open(node.resolve(`var/${namesCol}`, { kind: "array" })); const varNames = (await get(arr)).data; return varNames; } catch (error) { @@ -50,22 +47,20 @@ export const getVarNames = async (url, namesCol = '_index') => { export const getObs = async (url) => { try { const store = new FetchStore(url); - const node = await open(store, { kind: 'group' }); + const node = await open(store, { kind: "group" }); - const cols = (await open(node.resolve('obs', { kind: 'group' }))).attrs?.[ - 'column-order' - ]; + const cols = (await open(node.resolve("obs", { kind: "group" }))).attrs?.["column-order"]; const obs = { categorical: [], numerical: [] }; for (const col of cols) { const dataNode = await open(node.resolve(`obs/${col}`)); - const { 'encoding-type': encodingType } = dataNode.attrs || {}; - if (encodingType === 'categorical') { - const categoriesArr = await open(dataNode.resolve('categories'), { - kind: 'array', + const { "encoding-type": encodingType } = dataNode.attrs || {}; + if (encodingType === "categorical") { + const categoriesArr = await open(dataNode.resolve("categories"), { + kind: "array", }); const { data: categories } = await get(categoriesArr); obs.categorical.push({ name: col, categories }); - } else if (encodingType === 'array') { + } else if (encodingType === "array") { obs.numerical.push({ name: col }); } } @@ -76,12 +71,12 @@ export const getObs = async (url) => { } }; -export const getVarIndex = async (url, varId, namesCol = '_index') => { +export const getVarIndex = async (url, varId, namesCol = "_index") => { try { const store = new FetchStore(url); - const node = await open(store, { kind: 'group' }); + const node = await open(store, { kind: "group" }); - const arr = await open(node.resolve(`var/${namesCol}`, { kind: 'array' })); + const arr = await open(node.resolve(`var/${namesCol}`, { kind: "array" })); const varNames = (await get(arr)).data; const varIndex = varNames.findIndex((name) => name === varId); return varIndex; @@ -94,11 +89,11 @@ export const getZarrPath = async (url, matrixProps) => { const { feature, obs } = matrixProps; if (feature) { if (feature.index !== undefined && feature.index !== null) { - return { url, path: 'X', s: [null, feature.index] }; + return { url, path: "X", s: [null, feature.index] }; } else if (feature.name) { return { url, - path: 'X', + path: "X", s: [null, await getVarIndex(url, feature.name, feature.namesCol)], }; } @@ -112,13 +107,13 @@ export const getZarrPath = async (url, matrixProps) => { }; } - throw new Error('No feature or obs in matrixProps'); + throw new Error("No feature or obs in matrixProps"); }; const parseHexColor = (color) => { - const r = parseInt(color?.substring(1, 3), 16); - const g = parseInt(color?.substring(3, 5), 16); - const b = parseInt(color?.substring(5, 7), 16); + const r = Number.parseInt(color?.substring(1, 3), 16); + const g = Number.parseInt(color?.substring(3, 5), 16); + const b = Number.parseInt(color?.substring(5, 7), 16); return [r, g, b]; }; diff --git a/anndata-zarr/vite.config.js b/anndata-zarr/vite.config.js index 9dc91aff..f5eec469 100644 --- a/anndata-zarr/vite.config.js +++ b/anndata-zarr/vite.config.js @@ -1,7 +1,7 @@ -import path from 'path'; +import path from "path"; -import react from '@vitejs/plugin-react'; -import { defineConfig } from 'vite'; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; // https://vite.dev/config/ export default defineConfig({ @@ -9,24 +9,17 @@ export default defineConfig({ build: { // outDir: path.resolve(__dirname, '../dist'), lib: { - entry: path.resolve(__dirname, 'src/index.js'), - name: 'BiongffAnndataZarr', - formats: ['es', 'cjs'], + entry: path.resolve(__dirname, "src/index.js"), + name: "BiongffAnndataZarr", + formats: ["es", "cjs"], fileName: (format) => `biongff-anndata-zarr.${format}.js`, }, rollupOptions: { - external: [ - 'react', - 'react-dom', - '@mui/material', - '@mui/icons-material', - '@emotion/react', - '@emotion/styled', - ], + external: ["react", "react-dom", "@mui/material", "@mui/icons-material", "@emotion/react", "@emotion/styled"], output: { globals: { - react: 'React', - 'react-dom': 'ReactDOM', + react: "React", + "react-dom": "ReactDOM", }, }, }, diff --git a/sites/app/src/App.tsx b/sites/app/src/App.tsx index 99aea353..1d752c30 100644 --- a/sites/app/src/App.tsx +++ b/sites/app/src/App.tsx @@ -1,17 +1,16 @@ -import { type ViewState, type labelColor, Vizarr } from "@biongff/vizarr"; +import { type ViewState, Vizarr, type labelColor } from "@biongff/vizarr"; -//@ts-ignore +//@ts-ignore //No types provided by anndata-zarr plugin -import { AnndataProvider, AnndataController } from "@biongff/anndata-zarr" +import { AnndataController, AnndataProvider } from "@biongff/anndata-zarr"; +import CssBaseline from "@mui/material/CssBaseline"; +import { ThemeProvider, createTheme } from "@mui/material/styles"; import debounce from "just-debounce-it"; import * as React from "react"; -import { ThemeProvider, createTheme } from '@mui/material/styles' -import CssBaseline from '@mui/material/CssBaseline' - const darkTheme = createTheme({ palette: { - mode: 'dark', + mode: "dark", }, typography: { fontSize: 12, @@ -42,14 +41,10 @@ export default function App() { return { sources: searchParams.getAll("source"), viewState: parseViewStateFromUrl(), - anndatas: searchParams - .getAll('anndata') - .map((v) => (v ? { url: v } : null)), + anndatas: searchParams.getAll("anndata").map((v) => (v ? { url: v } : null)), }; }, [urlString]); - - const [colors, setColors] = React.useState((): labelColor[][] => Array(sources.length).fill([])); // Debounced viewState change handler const handleViewStateChange = React.useMemo( @@ -76,11 +71,7 @@ export default function App() { return sources.map((_s, i) => { if (!anndatas?.[i]?.url) return null; return ( - selectCallback(colorData, i)} - /> + selectCallback(colorData, i)} /> ); }); }, [anndatas, sources]); @@ -89,7 +80,12 @@ export default function App() {
{anndataControllers}
- +
); diff --git a/sites/app/src/main.tsx b/sites/app/src/main.tsx index df8831ae..c4efe3c7 100644 --- a/sites/app/src/main.tsx +++ b/sites/app/src/main.tsx @@ -2,7 +2,7 @@ import { version } from "@biongff/vizarr"; import React, { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import App from "./App"; -import './index.css' +import "./index.css"; console.log(`vizarr v${version}: https://github.com/BioNGFF/vizarr`); diff --git a/sites/app/vite.config.js b/sites/app/vite.config.js index 6b88a7d9..537a9fcf 100644 --- a/sites/app/vite.config.js +++ b/sites/app/vite.config.js @@ -9,9 +9,9 @@ export default defineConfig(({ mode }) => ({ alias: { ...(mode === "development" ? { - "@biongff/vizarr": path.resolve(__dirname, "../../viewer/src/index.tsx"), - "@biongff/anndata-zarr": path.resolve(__dirname, "../../anndata-zarr/src/index.js") - } + "@biongff/vizarr": path.resolve(__dirname, "../../viewer/src/index.tsx"), + "@biongff/anndata-zarr": path.resolve(__dirname, "../../anndata-zarr/src/index.js"), + } : {}), }, }, diff --git a/viewer/src/api.tsx b/viewer/src/api.tsx index 5afa231e..a44c9497 100644 --- a/viewer/src/api.tsx +++ b/viewer/src/api.tsx @@ -28,11 +28,10 @@ type Events = { export type { ViewState, ImageLayerConfig }; export type labelColor = { - labelValue: number, - rgba: [r: number, g: number, b: number, a: number], - value?: string | number | null -} - + labelValue: number; + rgba: [r: number, g: number, b: number, a: number]; + value?: string | number | null; +}; export interface VizarrViewer { addImage(config: ImageLayerConfig): void; diff --git a/viewer/src/components/VizarrViewer.tsx b/viewer/src/components/VizarrViewer.tsx index ac4dd948..04ecdfcf 100644 --- a/viewer/src/components/VizarrViewer.tsx +++ b/viewer/src/components/VizarrViewer.tsx @@ -5,7 +5,8 @@ import { type PrimitiveAtom, Provider, atom, useAtomValue, useSetAtom } from "jo import React, { useId } from "react"; import { getSourceDataError, sourceDataValid, writeUserErrorMessage } from "../error"; import { ViewStateContext } from "../hooks"; -import { loadSources } from "../io" +import { loadSources } from "../io"; +import type { OmeColor } from "../layers/label-layer"; import { type ImageLayerConfig, type ViewState, @@ -19,7 +20,6 @@ import theme from "../theme"; import Menu from "./Menu"; import { InfoSnackbar } from "./Snackbar"; import Viewer from "./Viewer"; -import type { OmeColor } from "../layers/label-layer"; export interface VizarrViewerProps { sources?: string[]; @@ -28,7 +28,12 @@ export interface VizarrViewerProps { labelColours?: OmeColor[][]; } -function VizarrViewerComponent({ sources = [], viewState: initialViewState, onViewStateChange, labelColours }: VizarrViewerProps) { +function VizarrViewerComponent({ + sources = [], + viewState: initialViewState, + onViewStateChange, + labelColours, +}: VizarrViewerProps) { const setSourceInfo = useSetAtom(sourceInfoAtom); const setViewStateAtom = useSetAtom(viewStateAtom); const sourceError = useAtomValue(sourceErrorAtom); @@ -62,19 +67,16 @@ function VizarrViewerComponent({ sources = [], viewState: initialViewState, onVi let sourceDatas = []; for (const res of results) { if (res.status === "fulfilled") { - sourceDatas.push(res.value); } else { console.error(res.reason); } } const sourceData = sourceDatas.filter((s) => s !== null); - setSourceInfo(sourceData) - } - ) + setSourceInfo(sourceData); + }); }, [sources, labelColours, setSourceInfo]); - return ( <> {redirectObj === null && ( diff --git a/viewer/src/io.ts b/viewer/src/io.ts index ddec3c36..c61efafa 100644 --- a/viewer/src/io.ts +++ b/viewer/src/io.ts @@ -283,18 +283,17 @@ export async function loadSources(sources: string[], labelColors?: OmeColor[][]) sourceData.name = `image_${index}`; } if (labelColors && labelColors[index].length) { - if (!sourceData.labels || (!sourceData.labels.length)) { - throw new utils.AssertionError('Feature colours provided but source image has no label.') + if (!sourceData.labels || !sourceData.labels.length) { + throw new utils.AssertionError("Feature colours provided but source image has no label."); } else { - //Really not the best way to do this but the layer state is heavily wrapped up in + //Really not the best way to do this but the layer state is heavily wrapped up in //being derived directly from the sourceData and would require a fairly large refactor to find - sourceData.labels[0].colors = labelColors[index] - sourceData.labels[0].on = true + sourceData.labels[0].colors = labelColors[index]; + sourceData.labels[0].on = true; } } return { id, ...sourceData }; }), ); - return results - + return results; } diff --git a/viewer/src/state.ts b/viewer/src/state.ts index 3b70d3f1..8589b65c 100644 --- a/viewer/src/state.ts +++ b/viewer/src/state.ts @@ -62,7 +62,7 @@ export type ImageLabels = Array<{ loader: ZarrPixelSource[]; modelMatrix: Matrix4; colors?: ReadonlyArray; - on?: boolean + on?: boolean; }>; export type SourceData = { @@ -187,10 +187,10 @@ const imageLabelsIstanceFamily = atomFamily((a: Atom) => return labels.map((label) => label.on ? new LabelLayer({ - ...label.layerProps, - selection: label.transformSourceSelection(layerProps.selections[0]), - pickable: true, - }) + ...label.layerProps, + selection: label.transformSourceSelection(layerProps.selections[0]), + pickable: true, + }) : null, ); }), diff --git a/viewer/tests/features.test.js b/viewer/tests/features.test.js index a70f700d..44667330 100644 --- a/viewer/tests/features.test.js +++ b/viewer/tests/features.test.js @@ -1,42 +1,37 @@ +import { AssertionError } from "node:assert"; +import { expect, test } from "vitest"; import { loadSources } from "../src/io"; -import { test, expect } from "vitest"; import { range } from "../src/utils"; -import { AssertionError } from "node:assert"; -const labelImageURL = 'https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.5/idr0062A/6001240_labels.zarr' -const imageURL = 'https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.5/idr0066/ExpD_chicken_embryo_MIP.ome.zarr' -const labelIds = range(61) +const labelImageURL = "https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.5/idr0062A/6001240_labels.zarr"; +const imageURL = "https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.5/idr0066/ExpD_chicken_embryo_MIP.ome.zarr"; +const labelIds = range(61); function generateLabelColors(labelIds) { return labelIds.map((id) => { - return ( - { - labelValue: id + 1, - rgba: [Math.floor(Math.random() * 250), Math.floor(Math.random() * 250), Math.floor(Math.random() * 250), Math.floor(Math.random() * 250)], - value: Math.random() - } - ) - }) + return { + labelValue: id + 1, + rgba: [ + Math.floor(Math.random() * 250), + Math.floor(Math.random() * 250), + Math.floor(Math.random() * 250), + Math.floor(Math.random() * 250), + ], + value: Math.random(), + }; + }); } -test('Can create source data with externally-defined label colours', async () => { - const labelColours = generateLabelColors(labelIds) - const sources = await loadSources( - [labelImageURL], - [labelColours] - ) - console.log(labelColours[0]) - console.log(sources[0].value.labels[0].colors[0]) - expect(sources[0].value.labels[0].colors).toBe(labelColours) -}) +test("Can create source data with externally-defined label colours", async () => { + const labelColours = generateLabelColors(labelIds); + const sources = await loadSources([labelImageURL], [labelColours]); + expect(sources[0].value.labels[0].colors).toBe(labelColours); +}); -test('Attempting to add externally-defined label colours to image without label leads to an error', async () => { - const labelColours = generateLabelColors(labelIds) - const sources = await loadSources( - [imageURL], - [labelColours] - ) - expect(sources[0].status).toBe('rejected') +test("Attempting to add externally-defined label colours to image without label leads to an error", async () => { + const labelColours = generateLabelColors(labelIds); + const sources = await loadSources([imageURL], [labelColours]); + expect(sources[0].status).toBe("rejected"); //Requires AssertionError to provide correct error message to user - expect(sources[0].reason.name).toBe('AssertionError') -}) + expect(sources[0].reason.name).toBe("AssertionError"); +}); From 94607727352f8fb99969dcccc2d17446aa53c60d Mon Sep 17 00:00:00 2001 From: dannda Date: Thu, 23 Apr 2026 17:14:44 +0100 Subject: [PATCH 6/9] fix: anndata-zarr build and css --- anndata-zarr/src/components/Legend.jsx | 3 +-- package.json | 3 ++- sites/app/src/App.tsx | 12 +++++++++--- sites/app/vite.config.js | 1 + 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/anndata-zarr/src/components/Legend.jsx b/anndata-zarr/src/components/Legend.jsx index a83f1d3e..0320e54a 100644 --- a/anndata-zarr/src/components/Legend.jsx +++ b/anndata-zarr/src/components/Legend.jsx @@ -3,13 +3,12 @@ import React, { useMemo } from "react"; import _ from "lodash"; import { getColor } from "../utils"; -import "../index.css"; export const Legend = ({ min, max, colorscale }) => { const spanList = useMemo(() => { return _.range(100).map((i) => { const color = getColor({ value: i / 100, colorscale }); - return ; + return ; }); }, [colorscale]); diff --git a/package.json b/package.json index ad8821c4..7cd66d39 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "check": "pnpm build:viewer && pnpm -r run check", "build:viewer": "pnpm --filter vizarr build", "build:app": "pnpm --filter app build", - "build": "pnpm build:viewer && pnpm build:app", + "build:anndata-zarr": "pnpm --filter anndata-zarr build", + "build": "pnpm build:viewer && pnpm build:anndata-zarr && pnpm build:app", "test": "vitest" }, "devDependencies": { diff --git a/sites/app/src/App.tsx b/sites/app/src/App.tsx index 1d752c30..b2d0b3e7 100644 --- a/sites/app/src/App.tsx +++ b/sites/app/src/App.tsx @@ -8,6 +8,8 @@ import { ThemeProvider, createTheme } from "@mui/material/styles"; import debounce from "just-debounce-it"; import * as React from "react"; +import "@biongff/anndata-zarr/dist/anndata-zarr.css"; + const darkTheme = createTheme({ palette: { mode: "dark", @@ -46,6 +48,7 @@ export default function App() { }, [urlString]); const [colors, setColors] = React.useState((): labelColor[][] => Array(sources.length).fill([])); + // Debounced viewState change handler const handleViewStateChange = React.useMemo( () => @@ -62,11 +65,13 @@ export default function App() { }, 200), [], ); - const selectCallback = (colorData: any, i: any) => { + + const selectCallback = React.useCallback((colorData: any, i: any) => { setColors((prev) => { return prev.map((c, ci) => (ci === i ? colorData : c)); }); - }; + }, []); + const anndataControllers = React.useMemo(() => { return sources.map((_s, i) => { if (!anndatas?.[i]?.url) return null; @@ -74,7 +79,8 @@ export default function App() { selectCallback(colorData, i)} /> ); }); - }, [anndatas, sources]); + }, [anndatas, sources, selectCallback]); + return ( diff --git a/sites/app/vite.config.js b/sites/app/vite.config.js index 537a9fcf..f7e900f9 100644 --- a/sites/app/vite.config.js +++ b/sites/app/vite.config.js @@ -10,6 +10,7 @@ export default defineConfig(({ mode }) => ({ ...(mode === "development" ? { "@biongff/vizarr": path.resolve(__dirname, "../../viewer/src/index.tsx"), + "@biongff/anndata-zarr/dist/anndata-zarr.css": path.resolve(__dirname, "../../anndata-zarr/src/index.css"), "@biongff/anndata-zarr": path.resolve(__dirname, "../../anndata-zarr/src/index.js"), } : {}), From 0643b7f080296a10ba8b8a7e03e2a06019214a93 Mon Sep 17 00:00:00 2001 From: dannda Date: Fri, 24 Apr 2026 11:48:23 +0100 Subject: [PATCH 7/9] fix: import css lint --- anndata-zarr/src/components/ObsSelect.jsx | 2 +- anndata-zarr/src/index.js | 2 ++ anndata-zarr/src/utils.js | 12 ++++++++---- anndata-zarr/vite.config.js | 2 +- sites/app/src/App.tsx | 8 ++++++-- viewer/src/components/VizarrViewer.tsx | 2 +- viewer/src/io.ts | 11 +++++------ 7 files changed, 24 insertions(+), 15 deletions(-) diff --git a/anndata-zarr/src/components/ObsSelect.jsx b/anndata-zarr/src/components/ObsSelect.jsx index f7b28012..747f3862 100644 --- a/anndata-zarr/src/components/ObsSelect.jsx +++ b/anndata-zarr/src/components/ObsSelect.jsx @@ -54,7 +54,7 @@ const CategoricalCol = ({ col, showColor = false }) => { height: 10, bgcolor: `rgba(${getColor({ value: i / (categories.length - 1), colorscale: COLORSCALES.Accent })})`, }} - >
+ /> )} diff --git a/anndata-zarr/src/index.js b/anndata-zarr/src/index.js index 1a061206..3e49b763 100644 --- a/anndata-zarr/src/index.js +++ b/anndata-zarr/src/index.js @@ -1,3 +1,5 @@ +import "./index.css"; + export { useAnndataColors } from "./hooks"; export { AnndataProvider } from "./provider"; export { COLORSCALES } from "./constants/colorscales"; diff --git a/anndata-zarr/src/utils.js b/anndata-zarr/src/utils.js index 881c32ba..777cddde 100644 --- a/anndata-zarr/src/utils.js +++ b/anndata-zarr/src/utils.js @@ -26,6 +26,7 @@ export const fetchDataFromZarr = async (url, path, s) => { return result; } catch (error) { + // biome-ignore lint/complexity/noUselessCatch: @TODO: better error handling throw error; } }; @@ -90,7 +91,8 @@ export const getZarrPath = async (url, matrixProps) => { if (feature) { if (feature.index !== undefined && feature.index !== null) { return { url, path: "X", s: [null, feature.index] }; - } else if (feature.name) { + } + if (feature.name) { return { url, path: "X", @@ -130,11 +132,13 @@ const interpolateColor = (color1, color2, factor) => { }; const computeColor = (colormap, value) => { - if (!colormap || isNaN(value)) { + if (!colormap || Number.isNaN(value)) { return [0, 0, 0, 255]; - } else if (value <= 0) { + } + if (value <= 0) { return parseHexColor(colormap[0]); - } else if (value >= 1) { + } + if (value >= 1) { return parseHexColor(colormap[colormap.length - 1]); } const index1 = Math.floor(value * (colormap.length - 1)); diff --git a/anndata-zarr/vite.config.js b/anndata-zarr/vite.config.js index f5eec469..deb993fe 100644 --- a/anndata-zarr/vite.config.js +++ b/anndata-zarr/vite.config.js @@ -1,4 +1,4 @@ -import path from "path"; +import path from "node:path"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; diff --git a/sites/app/src/App.tsx b/sites/app/src/App.tsx index b2d0b3e7..43cdbfe9 100644 --- a/sites/app/src/App.tsx +++ b/sites/app/src/App.tsx @@ -66,7 +66,7 @@ export default function App() { [], ); - const selectCallback = React.useCallback((colorData: any, i: any) => { + const selectCallback = React.useCallback((colorData: labelColor[], i: number) => { setColors((prev) => { return prev.map((c, ci) => (ci === i ? colorData : c)); }); @@ -76,7 +76,11 @@ export default function App() { return sources.map((_s, i) => { if (!anndatas?.[i]?.url) return null; return ( - selectCallback(colorData, i)} /> + selectCallback(colorData, i)} + /> ); }); }, [anndatas, sources, selectCallback]); diff --git a/viewer/src/components/VizarrViewer.tsx b/viewer/src/components/VizarrViewer.tsx index 04ecdfcf..ef5c56e2 100644 --- a/viewer/src/components/VizarrViewer.tsx +++ b/viewer/src/components/VizarrViewer.tsx @@ -75,7 +75,7 @@ function VizarrViewerComponent({ const sourceData = sourceDatas.filter((s) => s !== null); setSourceInfo(sourceData); }); - }, [sources, labelColours, setSourceInfo]); + }, [sources, labelColours, setSourceInfo, setSourceError]); return ( <> diff --git a/viewer/src/io.ts b/viewer/src/io.ts index c61efafa..63b63533 100644 --- a/viewer/src/io.ts +++ b/viewer/src/io.ts @@ -282,15 +282,14 @@ export async function loadSources(sources: string[], labelColors?: OmeColor[][]) if (!sourceData.name) { sourceData.name = `image_${index}`; } - if (labelColors && labelColors[index].length) { + if (labelColors?.[index].length) { if (!sourceData.labels || !sourceData.labels.length) { throw new utils.AssertionError("Feature colours provided but source image has no label."); - } else { - //Really not the best way to do this but the layer state is heavily wrapped up in - //being derived directly from the sourceData and would require a fairly large refactor to find - sourceData.labels[0].colors = labelColors[index]; - sourceData.labels[0].on = true; } + //Really not the best way to do this but the layer state is heavily wrapped up in + //being derived directly from the sourceData and would require a fairly large refactor to find + sourceData.labels[0].colors = labelColors[index]; + sourceData.labels[0].on = true; } return { id, ...sourceData }; }), From bbb2082a2a1e55d77ca8f17c77674e5ba4b68e7a Mon Sep 17 00:00:00 2001 From: dannda Date: Thu, 30 Apr 2026 16:24:10 +0100 Subject: [PATCH 8/9] fix: anndata-zarr boolean arrays handle bool arrays as categorical --- anndata-zarr/src/utils.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/anndata-zarr/src/utils.js b/anndata-zarr/src/utils.js index 777cddde..3e206c43 100644 --- a/anndata-zarr/src/utils.js +++ b/anndata-zarr/src/utils.js @@ -10,7 +10,13 @@ export const fetchDataFromZarr = async (url, path, s) => { const dataNode = await open(node.resolve(path)); let result; - if (dataNode.attrs?.["encoding-type"] === "array" || dataNode.attrs?.["encoding-type"] === "string-array") { + if (dataNode.attrs?.["encoding-type"] === "array" && dataNode.dtype === "bool") { + const boolData = await get(dataNode, s); + result = { + data: Array.from(boolData.data), + categories: ["false", "true"], + }; + } else if (dataNode.attrs?.["encoding-type"] === "array" || dataNode.attrs?.["encoding-type"] === "string-array") { result = await get(dataNode, s); } else if (dataNode.attrs?.["encoding-type"] === "categorical") { const categoriesArr = await open(dataNode.resolve("categories"), { @@ -62,7 +68,11 @@ export const getObs = async (url) => { const { data: categories } = await get(categoriesArr); obs.categorical.push({ name: col, categories }); } else if (encodingType === "array") { - obs.numerical.push({ name: col }); + if (dataNode.dtype === "bool") { + obs.categorical.push({ name: col, categories: ["false", "true"] }); + } else { + obs.numerical.push({ name: col }); + } } } return obs; @@ -155,6 +165,6 @@ export const getColors = ({ data, max, min, colorProps, categories }) => { return _.map(data, (v, i) => ({ labelValue: i + 1, rgba: getColor({ value: (v - min) / (max - min), ...colorProps }), - value: categories ? categories[v] : v, + value: categories ? (categories[v] ?? v) : v, })); }; From 6ab5b3fccef56c1f728c87daf9615a31157b19ae Mon Sep 17 00:00:00 2001 From: dannda Date: Fri, 1 May 2026 11:36:34 +0100 Subject: [PATCH 9/9] fix: warning length displayed on screen --- viewer/src/components/VizarrViewer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/viewer/src/components/VizarrViewer.tsx b/viewer/src/components/VizarrViewer.tsx index ef5c56e2..7631fbc7 100644 --- a/viewer/src/components/VizarrViewer.tsx +++ b/viewer/src/components/VizarrViewer.tsx @@ -109,7 +109,7 @@ function VizarrViewerComponent({

)} - {sourceWarning.length && + {!!sourceWarning.length && sourceWarning.map((warning, index) => { return ; })}