Skip to content

Commit fcf4608

Browse files
authored
Merge pull request #31 from BioNGFF/feat/anndata-zarr
add anndata-zarr package
2 parents 48c70af + 6edf776 commit fcf4608

26 files changed

+1245
-50
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ yarn-error.log*
2727
.vscode
2828
app.yaml
2929

30-
.vscode
30+
.vscode
31+
*.tgz

anndata-zarr/.eslintrc.json

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
{
2+
"extends": [
3+
"react-app",
4+
"prettier",
5+
"plugin:import/errors",
6+
"plugin:import/warnings",
7+
"plugin:prettier/recommended"
8+
],
9+
"settings": {
10+
"import/resolver": {
11+
"node": {
12+
"extensions": [
13+
".js",
14+
".jsx",
15+
".ts",
16+
".tsx"
17+
]
18+
},
19+
"alias": {
20+
"map": [
21+
[
22+
"@app",
23+
"./src"
24+
]
25+
],
26+
"extensions": [
27+
".js",
28+
".jsx",
29+
".ts",
30+
".tsx"
31+
]
32+
}
33+
}
34+
},
35+
"rules": {
36+
"import/order": [
37+
"error",
38+
{
39+
"groups": [
40+
"builtin",
41+
"external",
42+
"internal",
43+
[
44+
"parent",
45+
"sibling"
46+
],
47+
"index"
48+
],
49+
"pathGroups": [
50+
{
51+
"pattern": "react",
52+
"group": "external",
53+
"position": "before"
54+
}
55+
],
56+
"pathGroupsExcludedImportTypes": [
57+
"react"
58+
],
59+
"newlines-between": "always",
60+
"alphabetize": {
61+
"order": "asc",
62+
"caseInsensitive": true
63+
}
64+
}
65+
],
66+
"prettier/prettier": [
67+
"error",
68+
{
69+
"singleQuote": true,
70+
"tabWidth": 2,
71+
"useTabs": false
72+
}
73+
]
74+
}
75+
}

anndata-zarr/.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

anndata-zarr/package.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"name": "@biongff/anndata-zarr",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"main": "dist/biongff-anndata-zarr.cjs.js",
7+
"module": "dist/biongff-anndata-zarr.es.js",
8+
"files": [
9+
"dist"
10+
],
11+
"exports": {
12+
".": {
13+
"import": "./dist/biongff-anndata-zarr.es.js",
14+
"require": "./dist/biongff-anndata-zarr.cjs.js"
15+
}
16+
},
17+
"scripts": {
18+
"dev": "vite",
19+
"build": "vite build",
20+
"lint": "eslint .",
21+
"lint:fix": "eslint . --fix",
22+
"preview": "vite preview",
23+
"test": "vitest"
24+
},
25+
"dependencies": {
26+
"@mui/icons-material": "^7.2.0",
27+
"@mui/material": "^7.2.0",
28+
"@tanstack/react-query": "^5.85.3",
29+
"lodash": "^4.17.21",
30+
"react-window": "^2.0.2",
31+
"zarrita": "0.5.0"
32+
},
33+
"peerDependencies": {
34+
"react": "^18.2.0",
35+
"react-dom": "^18.2.0"
36+
},
37+
"devDependencies": {
38+
"@vitejs/plugin-react": "^4.3.3",
39+
"vite": "^6.2.3",
40+
"vitest": "^3.0.8"
41+
}
42+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React, { useState } from 'react';
2+
3+
import Box from '@mui/material/Box';
4+
import Stack from '@mui/material/Stack';
5+
6+
import { FeatureSelect } from './FeatureSelect';
7+
import { ObsSelect } from './ObsSelect';
8+
9+
export const AnndataController = ({ adata, callback = () => {} }) => {
10+
const [feature, setFeature] = useState(null);
11+
const [obsCol, setObsCol] = useState(null);
12+
13+
const handleFeatureSelect = (f) => {
14+
setFeature(f);
15+
setObsCol(null);
16+
};
17+
18+
const handleObsSelect = (o) => {
19+
setObsCol(o);
20+
setFeature(null);
21+
};
22+
23+
return (
24+
<Stack sx={{ height: '100%' }}>
25+
<Box sx={{ height: '50%' }}>
26+
<FeatureSelect
27+
adata={adata}
28+
callback={callback}
29+
feature={feature}
30+
onSelect={handleFeatureSelect}
31+
/>
32+
</Box>
33+
<Box sx={{ height: '50%' }}>
34+
<ObsSelect
35+
adata={adata}
36+
callback={callback}
37+
obsCol={obsCol}
38+
onSelect={handleObsSelect}
39+
/>
40+
</Box>
41+
</Stack>
42+
);
43+
};
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import React, { useEffect, useMemo, useState } from 'react';
2+
3+
import Box from '@mui/material/Box';
4+
import ListItem from '@mui/material/ListItem';
5+
import ListItemButton from '@mui/material/ListItemButton';
6+
import ListItemText from '@mui/material/ListItemText';
7+
import Stack from '@mui/material/Stack';
8+
import TextField from '@mui/material/TextField';
9+
import { List } from 'react-window';
10+
11+
import { useAnndataColors, useAnndataFeatures } from '../hooks';
12+
import { Legend } from './Legend';
13+
14+
const RowComponent = ({ index, items, style, onSelect, selectedIndex }) => {
15+
return (
16+
<ListItem style={style} key={index} component="div" disablePadding>
17+
<ListItemButton
18+
style={{ height: '100%' }}
19+
onClick={() => onSelect({ index: items[index].matrixIndex })}
20+
selected={items[index].matrixIndex === selectedIndex}
21+
>
22+
<ListItemText primary={items[index].name} />
23+
</ListItemButton>
24+
</ListItem>
25+
);
26+
};
27+
28+
export const FeatureSelect = ({
29+
adata,
30+
feature,
31+
onSelect,
32+
callback = () => {},
33+
}) => {
34+
const [searchTerm, setSearchTerm] = useState('');
35+
36+
const { data, isLoading, serverError } = useAnndataFeatures(adata);
37+
const colorData = useAnndataColors(
38+
{
39+
...adata,
40+
matrixProps: {
41+
feature: feature,
42+
},
43+
},
44+
{ enabled: !!feature },
45+
);
46+
47+
useEffect(() => {
48+
if (colorData?.serverError) {
49+
callback(null);
50+
return;
51+
}
52+
if (!colorData?.isLoading && colorData?.data) {
53+
callback(colorData.data.colors);
54+
}
55+
}, [colorData, callback]);
56+
57+
const items = useMemo(() => {
58+
if (!data) return [];
59+
const allItems = data.map((name, index) => ({
60+
name,
61+
matrixIndex: index,
62+
}));
63+
if (!searchTerm) return allItems;
64+
return allItems.filter((item) =>
65+
item.name.toLowerCase().includes(searchTerm.toLowerCase()),
66+
);
67+
}, [data, searchTerm]);
68+
69+
const legend = useMemo(() => {
70+
if (colorData?.serverError || colorData?.isLoading || !colorData?.data) {
71+
return null;
72+
}
73+
const { min, max, colorscale } = colorData.data;
74+
return <Legend min={min} max={max} colorscale={colorscale} />;
75+
}, [colorData.data, colorData?.isLoading, colorData?.serverError]);
76+
77+
if (isLoading) {
78+
return <></>;
79+
}
80+
if (serverError) {
81+
return <div>Error loading features</div>;
82+
}
83+
return (
84+
<Box
85+
sx={{
86+
width: 250,
87+
height: '100%',
88+
minHeight: 250,
89+
zIndex: 1,
90+
}}
91+
>
92+
<Stack sx={{ height: '100%' }}>
93+
<TextField
94+
label="Search features"
95+
type="search"
96+
variant="filled"
97+
fullWidth
98+
value={searchTerm}
99+
onChange={(e) => setSearchTerm(e.target.value)}
100+
/>
101+
<List
102+
rowComponent={RowComponent}
103+
rowCount={items.length}
104+
rowHeight={25}
105+
rowProps={{
106+
items,
107+
onSelect,
108+
selectedIndex: feature?.index,
109+
}}
110+
/>
111+
{!!feature && legend}
112+
</Stack>
113+
</Box>
114+
);
115+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React, { useMemo } from 'react';
2+
3+
import _ from 'lodash';
4+
5+
import { getColor } from '../utils';
6+
import '../index.css';
7+
8+
export const Legend = ({ min, max, colorscale }) => {
9+
const spanList = useMemo(() => {
10+
return _.range(100).map((i) => {
11+
const color = getColor({ value: i / 100, colorscale });
12+
return (
13+
<span
14+
key={i}
15+
className="grad-step"
16+
style={{ backgroundColor: `rgba(${color})` }}
17+
></span>
18+
);
19+
});
20+
}, [colorscale]);
21+
22+
return (
23+
<div className="legend">
24+
<div className="gradient">
25+
{spanList}
26+
<span className="domain-min">{min.toFixed(2)}</span>
27+
<span className="domain-mid">{((min + max) / 2).toFixed(2)}</span>
28+
<span className="domain-max">{max.toFixed(2)}</span>
29+
</div>
30+
</div>
31+
);
32+
};

0 commit comments

Comments
 (0)