Skip to content

Commit

Permalink
feat: added niivue visualizer and updated meta analysis page
Browse files Browse the repository at this point in the history
  • Loading branch information
nicoalee committed Nov 11, 2024
1 parent 8f234c1 commit f7f598f
Show file tree
Hide file tree
Showing 7 changed files with 374 additions and 77 deletions.
231 changes: 229 additions & 2 deletions compose/neurosynth-frontend/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions compose/neurosynth-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@mui/styles": "^5.5.1",
"@mui/system": "^5.5.1",
"@mui/x-data-grid": "^5.10.0",
"@niivue/niivue": "^0.46.0",
"@reactour/tour": "^2.10.3",
"@sentry/react": "^7.48.0",
"@tanstack/react-table": "^8.20.5",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Box, Slider, Typography } from '@mui/material';
import { useEffect, useRef, useState } from 'react';
import { Niivue, SHOW_RENDER } from '@niivue/niivue';

let niivue: Niivue;

const NiiVueVisualizer: React.FC<{ imageURL: string }> = ({ imageURL }) => {
const canvasRef = useRef(null);
const [thresholdPositive, setThresholdPositive] = useState(0);
const [thresholdNegative, setThresholdNegative] = useState(0);

const handleUpdateThresholdPositive = (event: Event, newValue: number | number[]) => {
if (!niivue) return;
setThresholdPositive(newValue as number);
niivue.volumes[1].cal_min = newValue as number;
niivue.updateGLVolume();
};

const handkleUpdateThresholdNegative = (event: Event, newValue: number | number[]) => {
if (!niivue) return;
setThresholdNegative(newValue as number);
niivue.volumes[1].cal_minNeg = -2;
niivue.volumes[1].cal_maxNeg = newValue as number;
niivue.updateGLVolume();
};

useEffect(() => {
if (!canvasRef.current) return;

const volumes = [
{
url: 'https://neurovault.org/static/images/GenericMNI.nii.gz',
colormap: 'gray',
opacity: 1,
},
{
url: imageURL,
colorMap: 'warm',
colormapNegative: 'winter',
cal_min: 0,
cal_max: 2,
cal_minNeg: -1,
cal_maxNeg: -2,
opacity: 1,
},
];

niivue = new Niivue({
show3Dcrosshair: true,
});

niivue.opts.isColorbar = true;
niivue.setSliceMM(false);

niivue.attachToCanvas(canvasRef.current);
niivue.addVolumesFromUrl(volumes).then(() => {
niivue.volumes[1].alphaThreshold = 0;
niivue.volumes[0].colorbarVisible = false;
niivue.volumes[1].cal_minNeg = -1.0;
niivue.volumes[1].cal_maxNeg = -2.0;
niivue.opts.multiplanarShowRender = SHOW_RENDER.ALWAYS;
niivue.setInterpolation(true);
niivue.updateGLVolume();
});
}, [imageURL]);

return (
<Box>
<Box sx={{ marginBottom: '10px', display: 'flex', justifyContent: 'space-between' }}>
<Box width="200px">
<Typography variant="body2" gutterBottom>
-Threshold
</Typography>
<Slider
valueLabelDisplay="auto"
min={-2}
step={0.01}
max={0}
onChange={handkleUpdateThresholdNegative}
value={thresholdNegative}
></Slider>
</Box>
<Box width="200px">
<Typography variant="body2" gutterBottom>
+Threshold
</Typography>
<Slider
valueLabelDisplay="auto"
min={0}
step={0.01}
max={2}
onChange={handleUpdateThresholdPositive}
value={thresholdPositive}
></Slider>
</Box>
</Box>
<Box sx={{ height: '300px' }}>
<canvas ref={canvasRef} />
</Box>
</Box>
);
};

export default NiiVueVisualizer;
20 changes: 6 additions & 14 deletions compose/neurosynth-frontend/src/helpers/MetaAnalysis.helpers.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
MetaAnalysisReturn,
NeurovaultFile,
ResultReturn,
} from 'neurosynth-compose-typescript-sdk';
import { MetaAnalysisReturn, NeurovaultFile, ResultReturn } from 'neurosynth-compose-typescript-sdk';

export const getResultStatus = (
metaAnalysisObj: MetaAnalysisReturn | undefined,
Expand All @@ -29,21 +25,17 @@ export const getResultStatus = (
severity: 'error',
};

if (
metaAnalysisResult.neurovault_collection?.files &&
metaAnalysisResult.neurovault_collection.files.length === 0
)
if (metaAnalysisResult.neurovault_collection?.files && metaAnalysisResult.neurovault_collection.files.length === 0)
return {
statusText: 'Detected run but no result found',
color: 'warning',
severity: 'warning',
};

const allFilesAreValid = (
metaAnalysisResult.neurovault_collection.files as Array<NeurovaultFile>
).every((file) => !!file.image_id);
if (!allFilesAreValid)
return { statusText: 'Latest Run Failed', color: 'error', severity: 'error' };
const allFilesAreValid = (metaAnalysisResult.neurovault_collection.files as Array<NeurovaultFile>).every(
(file) => !!file.image_id
);
if (!allFilesAreValid) return { statusText: 'Latest Run Failed', color: 'error', severity: 'error' };

if (!metaAnalysisObj?.neurostore_analysis?.neurostore_id) {
return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,68 +1,51 @@
import { Box, Link, Paper, Typography } from "@mui/material";
import {
MetaAnalysisReturn,
ResultReturn,
} from "neurosynth-compose-typescript-sdk";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import useGetAnalysisById from "hooks/analyses/useGetAnalysisById";
import StudyPoints from "pages/Study/components/StudyPoints";
import { PointReturn } from "neurostore-typescript-sdk";
import StateHandlerComponent from "components/StateHandlerComponent/StateHandlerComponent";
import { studyPointsToStorePoints } from "pages/Study/store/StudyStore.helpers";
import { getResultStatus } from "helpers/MetaAnalysis.helpers";
import { Box, Link, Typography } from '@mui/material';
import StateHandlerComponent from 'components/StateHandlerComponent/StateHandlerComponent';
import NiiVueVisualizer from 'components/Visualizer/NiiVueVisualizer';
import useGetAnalysisById from 'hooks/analyses/useGetAnalysisById';
import { PointReturn } from 'neurostore-typescript-sdk';
import { MetaAnalysisReturn, ResultReturn } from 'neurosynth-compose-typescript-sdk';
import StudyPoints from 'pages/Study/components/StudyPoints';
import { studyPointsToStorePoints } from 'pages/Study/store/StudyStore.helpers';

const DisplayMetaAnalysisResult: React.FC<{
metaAnalysis: MetaAnalysisReturn | undefined;
metaAnalysisResult: ResultReturn | undefined;
}> = (props) => {
const resultStatus = getResultStatus(
props.metaAnalysis,
props.metaAnalysisResult
);

const { data, isLoading, isError } = useGetAnalysisById(
props.metaAnalysis?.neurostore_analysis?.neurostore_id || undefined
);

const { points, analysisSpace, analysisMap } = studyPointsToStorePoints(
(data?.points || []) as PointReturn[]
);
const { points, analysisSpace, analysisMap } = studyPointsToStorePoints((data?.points || []) as PointReturn[]);

const neurovaultLink =
props.metaAnalysisResult?.neurovault_collection?.url || "";
const neurovaultLink = props.metaAnalysisResult?.neurovault_collection?.url || '';

return (
<Paper sx={{ padding: "1rem", margin: "1rem 0" }}>
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<Typography sx={{ marginBottom: "1rem" }} variant="h5">
Result
</Typography>
{resultStatus.color === "error" && (
<Box sx={{ display: "flex", alignItems: "center" }}>
<ErrorOutlineIcon
color="error"
sx={{ marginRight: "10px" }}
/>
<Typography sx={{ color: "error.main" }}>
{resultStatus.statusText}
</Typography>
</Box>
)}
<Box>
<Box display="flex">
<Box width="300px">
<Box>item 1</Box>
<Box>item 2</Box>
<Box>item 3</Box>
<Box>item 3</Box>
<Box>item 3</Box>
<Box>item 3</Box>
<Box>item 3</Box>
<Box>item 3</Box>
</Box>
<Box sx={{ width: '100%' }}>
<NiiVueVisualizer imageURL="https://neurovault.org/media/images/18479/z_corr-FDR_method-indep.nii.gz" />
</Box>
</Box>
<Box sx={{ marginBottom: "1rem" }}>
<Typography sx={{ fontWeight: "bold" }} gutterBottom>
<Box sx={{ marginBottom: '1rem' }}>
<Typography sx={{ fontWeight: 'bold' }} gutterBottom>
Neurovault
</Typography>
<Link
sx={{ fontWeight: "normal" }}
sx={{ fontWeight: 'normal' }}
underline="hover"
target="_blank"
rel="noreferrer"
href={
neurovaultLink.includes("/api")
? neurovaultLink.replace(/\/api/, "")
: neurovaultLink
}
href={neurovaultLink.includes('/api') ? neurovaultLink.replace(/\/api/, '') : neurovaultLink}
>
Neurovault Collection Link
</Link>
Expand All @@ -76,7 +59,7 @@ const DisplayMetaAnalysisResult: React.FC<{
points={points}
/>
</StateHandlerComponent>
</Paper>
</Box>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { ResultReturn } from 'neurosynth-compose-typescript-sdk';
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import DisplayMetaAnalysisResult from './DisplayMetaAnalysisResult';
import DisplayMetaAnalysisSpecification from './MetaAnalysisSpecification';
import MetaAnalysisResultStatusAlert from './MetaAnalysisResultStatusAlert';
import DisplayMetaAnalysisSpecification from './MetaAnalysisSpecification';

const MetaAnalysisResult: React.FC = (props) => {
const MetaAnalysisResult: React.FC = () => {
const { projectId, metaAnalysisId } = useParams<{
projectId: string;
metaAnalysisId: string;
Expand Down
14 changes: 2 additions & 12 deletions compose/neurosynth-frontend/src/pages/Project/ProjectPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,7 @@ const ProjectPage: React.FC = (props) => {
const userCanEdit = useUserCanEdit(projectUser || undefined);
const getProjectIsError = useProjectIsError();

useGuard(
'/',
'No project found with id: ' + projectId,
!getProjectIsLoading && getProjectIsError
);
useGuard('/', 'No project found with id: ' + projectId, !getProjectIsLoading && getProjectIsError);

const tab = useMemo(() => {
if (!metaAnalysesTabEnabled) return 0;
Expand Down Expand Up @@ -108,9 +104,7 @@ const ProjectPage: React.FC = (props) => {
<ProjectComponentsEditPrivacyToggle />
</Box>
<TextEdit
onSave={(updatedDescription, label) =>
updateProjectDescription(updatedDescription)
}
onSave={(updatedDescription, label) => updateProjectDescription(updatedDescription)}
textFieldSx={{ input: { fontSize: '1.25rem' } }}
textToEdit={projectDescription || ''}
editIconIsVisible={userCanEdit}
Expand Down Expand Up @@ -186,8 +180,6 @@ const ProjectPage: React.FC = (props) => {
<Tab
onClick={() => navigate(`/projects/${projectId}/project`)}
sx={{
padding: '1rem',
fontSize: '1.2rem',
color: tab === 0 ? '#ef8a24 !important' : 'primary.main',
fontWeight: tab === 0 ? 'bold' : 'normal',
}}
Expand All @@ -197,8 +189,6 @@ const ProjectPage: React.FC = (props) => {
<Tab
onClick={() => navigate(`/projects/${projectId}/meta-analyses`)}
sx={{
padding: '1rem',
fontSize: '1.2rem',
color: tab === 1 ? '#ef8a24 !important' : 'primary.main',
fontWeight: tab === 1 ? 'bold' : 'normal',
}}
Expand Down

0 comments on commit f7f598f

Please sign in to comment.