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 ;
})}