Skip to content

Commit 91a1b1b

Browse files
authored
Merge branch 'main' into feat/scalebar
2 parents 6f9cc96 + de8b126 commit 91a1b1b

File tree

11 files changed

+332
-29
lines changed

11 files changed

+332
-29
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
# BioNGFF Viewer
1+
# 🚧 BioNGFF Viewer 🚧
22

3-
Monorepo while testing around
3+
Web viewer for bioimaging data.
4+
Built to support [NGFF specifications](https://ngff.openmicroscopy.org/latest/).
5+
Leverages [Vizarr](https://github.com/hms-dbmi/vizarr).
46

57
## Demo
68

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sites/app/index.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5-
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
65
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>Vite + React</title>
6+
<title>BioNGFF - Viewer</title>
87
</head>
98
<body>
109
<div id="root"></div>

sites/app/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"dependencies": {
1515
"@biongff/viewer": "workspace:*",
1616
"react": "^18.2.0",
17-
"react-dom": "^18.2.0"
17+
"react-dom": "^18.2.0",
18+
"@mui/material": "^7.2.0"
1819
},
1920
"devDependencies": {
2021
"@types/react": "^18.2.15",

sites/app/src/App.jsx

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
import React from 'react';
2+
3+
import CssBaseline from '@mui/material/CssBaseline';
4+
import { ThemeProvider, createTheme } from '@mui/material/styles';
5+
26
import './App.css';
37
import { Viewer, parseMatrix } from '@biongff/viewer';
48

9+
const darkTheme = createTheme({
10+
palette: {
11+
mode: 'dark',
12+
},
13+
typography: {
14+
fontSize: 12,
15+
},
16+
});
17+
518
function App() {
619
const url = new URL(window.location.href);
720

@@ -15,14 +28,17 @@ function App() {
1528
.map((v) => parseMatrix(v));
1629

1730
return (
18-
<>
19-
<Viewer
20-
sources={sources}
21-
channelAxis={channelAxis}
22-
isLabel={isLabel}
23-
modelMatrices={modelMatrices}
24-
/>
25-
</>
31+
<ThemeProvider theme={darkTheme}>
32+
<CssBaseline />
33+
<div className="App">
34+
<Viewer
35+
sources={sources}
36+
channelAxis={channelAxis}
37+
isLabel={isLabel}
38+
modelMatrices={modelMatrices}
39+
/>
40+
</div>
41+
</ThemeProvider>
2642
);
2743
}
2844

sites/app/src/index.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ body {
2828
place-items: center;
2929
min-width: 320px;
3030
min-height: 100vh;
31+
overflow: hidden;
3132
}
3233

3334
h1 {
@@ -71,8 +72,14 @@ button:focus-visible {
7172
position: absolute;
7273
top: 1rem;
7374
left: 1rem;
75+
bottom: 1rem;
7476
z-index: 1;
7577
padding: 0.5rem;
78+
overflow-y: auto;
79+
overflow-x: hidden;
80+
width: 200px;
81+
padding-right: 15px;
82+
box-sizing: border-box;
7683
}
7784

7885
.viewer-matrix {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React, { useEffect, useState } from 'react';
2+
3+
import Grid from '@mui/material/Grid';
4+
import Input from '@mui/material/Input';
5+
import Slider from '@mui/material/Slider';
6+
import Typography from '@mui/material/Typography';
7+
8+
const AxisSlider = ({ axis_labels, axisIndex, selections, max, onChange }) => {
9+
const [value, setValue] = useState(0);
10+
const axisLabel = axis_labels[axisIndex];
11+
12+
useEffect(() => {
13+
setValue(selections[0] ? selections[0][axisIndex] : 1);
14+
}, [selections, axisIndex]);
15+
16+
const setSelections = (v = value) => {
17+
onChange(
18+
selections.map((s) => {
19+
s[axisIndex] = v;
20+
return s;
21+
}),
22+
);
23+
};
24+
25+
return (
26+
<>
27+
<Grid container justifyContent="space-between">
28+
<Typography>{axisLabel}</Typography>
29+
<Input
30+
value={value}
31+
size="small"
32+
onChange={(e) => {
33+
setSelections(Number(e.target.value));
34+
}}
35+
inputProps={{
36+
step: 1,
37+
min: 0,
38+
max: max,
39+
type: 'number',
40+
}}
41+
/>
42+
</Grid>
43+
<Slider
44+
size="small"
45+
min={0}
46+
max={max}
47+
value={value}
48+
onChange={(_e, v) => setValue(v)}
49+
onChangeCommitted={() => setSelections()}
50+
/>
51+
</>
52+
);
53+
};
54+
55+
export const AxisSliders = ({
56+
axis_labels,
57+
channel_axis,
58+
loader,
59+
selections,
60+
onChange,
61+
}) => {
62+
// from vizarr AxisSliders
63+
const sliders = axis_labels
64+
.slice(0, -2) // ignore last two axes, [y,x]
65+
.map((name, i) => [name, i, loader[0].shape[i]]) // capture the name, index, and size of non-yx dims
66+
.filter((d) => {
67+
if (d[1] === channel_axis) return false; // ignore channel_axis (for OME-Zarr channel_axis === 1)
68+
if (d[2] > 1) return true; // keep if size > 1
69+
return false; // otherwise ignore as well
70+
})
71+
.map(([name, i, size]) => (
72+
<AxisSlider
73+
selections={selections}
74+
key={name}
75+
axis_labels={axis_labels}
76+
axisIndex={i}
77+
max={size - 1}
78+
step={1}
79+
onChange={onChange}
80+
/>
81+
));
82+
83+
return <>{sliders}</>;
84+
};
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import React from 'react';
2+
3+
import { RadioButtonChecked, RadioButtonUnchecked } from '@mui/icons-material';
4+
import Grid from '@mui/material/Grid';
5+
import IconButton from '@mui/material/IconButton';
6+
import Slider from '@mui/material/Slider';
7+
import Typography from '@mui/material/Typography';
8+
9+
const ChannelController = ({
10+
channel_axis,
11+
names,
12+
selections,
13+
contrastLimits,
14+
contrastLimitsRange,
15+
channelVisible,
16+
colormap,
17+
colors,
18+
toggleChannelVisibility,
19+
setChannelContrast,
20+
}) => {
21+
// from vizarr ChannelController
22+
const value = [...contrastLimits];
23+
const color = `rgb(${colormap ? [255, 255, 255] : colors})`;
24+
const on = channelVisible;
25+
const [min, max] = contrastLimitsRange;
26+
27+
const nameIndex = Number.isInteger(channel_axis)
28+
? selections[channel_axis]
29+
: 0;
30+
const label = names[nameIndex];
31+
32+
return (
33+
<>
34+
<Grid container justifyContent="space-between">
35+
<Typography noWrap title={label}>
36+
{label}
37+
</Typography>
38+
</Grid>
39+
<Grid container spacing={2}>
40+
<Grid display="flex" justifyContent="center" alignItems="center">
41+
<IconButton
42+
style={{
43+
color,
44+
padding: 0,
45+
backgroundColor: 'transparent',
46+
}}
47+
onClick={toggleChannelVisibility}
48+
>
49+
{on ? <RadioButtonChecked /> : <RadioButtonUnchecked />}
50+
</IconButton>
51+
</Grid>
52+
<Grid
53+
size="grow"
54+
display="flex"
55+
justifyContent="center"
56+
alignItems="center"
57+
>
58+
<Slider
59+
size="small"
60+
value={value}
61+
min={min}
62+
max={max}
63+
step={0.01}
64+
style={{
65+
padding: 0,
66+
color,
67+
}}
68+
onChange={(_e, v) => setChannelContrast(v)}
69+
/>
70+
</Grid>
71+
</Grid>
72+
</>
73+
);
74+
};
75+
76+
export const ChannelControllers = ({
77+
channel_axis,
78+
names,
79+
layerProps,
80+
toggleChannelVisibility,
81+
setChannelContrast,
82+
}) => {
83+
const nChannels = layerProps.selections.length;
84+
return (
85+
<Grid sx={{ width: '100%' }}>
86+
{[...Array(nChannels).keys()].map((i) => (
87+
<ChannelController
88+
key={i}
89+
channel_axis={channel_axis}
90+
names={names}
91+
selections={layerProps.selections[i]}
92+
contrastLimits={layerProps.contrastLimits[i]}
93+
contrastLimitsRange={layerProps.contrastLimitsRange[i]}
94+
channelVisible={layerProps.channelsVisible[i]}
95+
colors={layerProps.colors[i]}
96+
colormap={layerProps.colormap}
97+
toggleChannelVisibility={() => toggleChannelVisibility(i)}
98+
setChannelContrast={(cl) => setChannelContrast(i, cl)}
99+
/>
100+
))}
101+
</Grid>
102+
);
103+
};

viewer/src/components/Controller.jsx renamed to viewer/src/components/Controller/Controller.jsx

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,25 @@ import React from 'react';
33
import VisibilityIcon from '@mui/icons-material/Visibility';
44
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
55
import Checkbox from '@mui/material/Checkbox';
6+
import Divider from '@mui/material/Divider';
67
import FormControlLabel from '@mui/material/FormControlLabel';
78
import FormGroup from '@mui/material/FormGroup';
8-
import Slider from '@mui/material/Slider';
9+
import Grid from '@mui/material/Grid';
910
import Stack from '@mui/material/Stack';
1011

11-
const OpactiySlider = ({ value, onChange }) => (
12-
<Slider
13-
size="small"
14-
min={0}
15-
max={1}
16-
step={0.01}
17-
value={value}
18-
onChange={onChange}
19-
/>
20-
);
12+
import { AxisSliders } from './AxisSliders';
13+
import { ChannelControllers } from './ChannelControllers';
14+
import { OpacitySlider } from './OpacitySlider';
2115

2216
export const Controller = ({
17+
sourceData,
2318
layerStates,
2419
resetViewState,
2520
toggleVisibility,
2621
setLayerOpacity,
22+
setLayerSelections,
23+
toggleChannelVisibility,
24+
setChannelContrast,
2725
}) => {
2826
const controls = layerStates.map((layerState, index) => {
2927
if (!layerState) {
@@ -45,12 +43,28 @@ export const Controller = ({
4543
/>
4644
}
4745
/>
48-
<OpactiySlider
46+
<AxisSliders
47+
{...sourceData[index]}
48+
selections={layerState.layerProps.selections}
49+
onChange={(selections) => setLayerSelections(index, selections)}
50+
/>
51+
<OpacitySlider
4952
value={layerState.layerProps.opacity}
5053
onChange={(e, value) => setLayerOpacity(index, null, value)}
5154
/>
52-
{layerState.labels?.map((label) => (
55+
<Divider>Channels</Divider>
56+
<ChannelControllers
57+
{...sourceData[index]}
58+
{...layerState}
59+
toggleChannelVisibility={(i) => toggleChannelVisibility(index, i)}
60+
setChannelContrast={(i, contrast) =>
61+
setChannelContrast(index, i, contrast)
62+
}
63+
/>
64+
{layerState.labels?.length && <Divider>Labels</Divider>}
65+
{layerState.labels?.map((label, i) => (
5366
<React.Fragment key={label.layerProps.id}>
67+
{i > 0 && <Divider />}
5468
<FormControlLabel
5569
key={label.layerProps.id}
5670
label={`${label.layerProps.id} (label)`}
@@ -64,7 +78,7 @@ export const Controller = ({
6478
/>
6579
}
6680
/>
67-
<OpactiySlider
81+
<OpacitySlider
6882
value={label.layerProps.opacity}
6983
onChange={(e, value) =>
7084
setLayerOpacity(index, label.layerProps.id, value)
@@ -79,7 +93,6 @@ export const Controller = ({
7993
return (
8094
<div className="viewer-controller">
8195
<Stack spacing={2}>
82-
<p>Layers</p>
8396
<FormGroup>{controls}</FormGroup>
8497
<button type="button" className="btn" onClick={resetViewState}>
8598
Reset view

0 commit comments

Comments
 (0)