From ad1b7effb1a4aca10bb08b0dd025455b89a5310a Mon Sep 17 00:00:00 2001 From: Nicholas Lee Date: Wed, 4 Dec 2024 21:29:42 -0500 Subject: [PATCH] feat: added results and dynamic image display --- .../StateHandlerComponent.tsx | 6 +- .../Visualizer/NiiVueVisualizer.tsx | 135 +++++++++++++----- .../metaAnalyses/useGetMetaAnalysisById.tsx | 6 +- .../useGetMetaAnalysisResultById.tsx | 7 +- .../hooks/metaAnalyses/useGetNeurovault.tsx | 74 ++++++++++ .../components/DisplayMetaAnalysisResult.tsx | 66 --------- .../components/DisplayMetaAnalysisResults.tsx | 87 +++++++++++ .../components/MetaAnalysisResult.tsx | 21 +-- .../MetaAnalysisResultStatusAlert.tsx | 36 +++-- compose/neurosynth-frontend/src/utils/api.ts | 5 +- 10 files changed, 294 insertions(+), 149 deletions(-) create mode 100644 compose/neurosynth-frontend/src/hooks/metaAnalyses/useGetNeurovault.tsx delete mode 100644 compose/neurosynth-frontend/src/pages/MetaAnalysis/components/DisplayMetaAnalysisResult.tsx create mode 100644 compose/neurosynth-frontend/src/pages/MetaAnalysis/components/DisplayMetaAnalysisResults.tsx diff --git a/compose/neurosynth-frontend/src/components/StateHandlerComponent/StateHandlerComponent.tsx b/compose/neurosynth-frontend/src/components/StateHandlerComponent/StateHandlerComponent.tsx index 3cfb71a8c..c8572488e 100644 --- a/compose/neurosynth-frontend/src/components/StateHandlerComponent/StateHandlerComponent.tsx +++ b/compose/neurosynth-frontend/src/components/StateHandlerComponent/StateHandlerComponent.tsx @@ -13,11 +13,7 @@ export interface IStateHandlerComponent { const StateHandlerComponent: React.FC = (props) => { if (props.isError) { - return ( - - {props.errorMessage || 'There was an error'} - - ); + return {props.errorMessage || 'There was an error'}; } if (props.isLoading) { diff --git a/compose/neurosynth-frontend/src/components/Visualizer/NiiVueVisualizer.tsx b/compose/neurosynth-frontend/src/components/Visualizer/NiiVueVisualizer.tsx index 70dec5588..c2f1ecf3b 100644 --- a/compose/neurosynth-frontend/src/components/Visualizer/NiiVueVisualizer.tsx +++ b/compose/neurosynth-frontend/src/components/Visualizer/NiiVueVisualizer.tsx @@ -1,26 +1,74 @@ -import { Box, Slider, Typography } from '@mui/material'; -import { useEffect, useRef, useState } from 'react'; +import { Box, Checkbox, Slider, Typography } from '@mui/material'; +import { ChangeEvent, 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(3); - const [thresholdNegative, setThresholdNegative] = useState(-3); + const [softThreshold, setSoftThresold] = useState(false); + const [showNegatives, setShowNegatives] = useState(false); + const [showCrosshairs, setShowCrosshairs] = useState(true); + const [threshold, setThreshold] = useState<{ + min: number; + max: number; + value: number; + }>({ + min: 0, + max: 6, + value: 3, + }); - const handleUpdateThresholdPositive = (event: Event, newValue: number | number[]) => { + const handleUpdateThreshold = (event: Event, newValue: number | number[]) => { if (!niivue) return; - setThresholdPositive(newValue as number); - niivue.volumes[1].cal_min = newValue as number; + const typedVal = newValue as number; + setThreshold((prev) => ({ + ...prev, + value: typedVal, + })); + + // update threshold positive + niivue.volumes[1].cal_min = typedVal; + + // update threshold negative + niivue.volumes[1].cal_maxNeg = -1 * typedVal; + + niivue.updateGLVolume(); + }; + + const handleToggleSoftThreshold = (event: ChangeEvent, checked: boolean) => { + if (!niivue) return; + + setSoftThresold(checked); + if (checked) { + niivue.overlayOutlineWidth = 2; + niivue.volumes[1].alphaThreshold = 5; + } else { + niivue.overlayOutlineWidth = 0; + niivue.volumes[1].alphaThreshold = 0; + } + niivue.updateGLVolume(); + }; + + const handleToggleShowCrosshairs = (event: ChangeEvent, checked: boolean) => { + if (!niivue) return; + setShowCrosshairs(checked); + if (checked) { + niivue.setCrosshairWidth(1); + } else { + niivue.setCrosshairWidth(0); + } niivue.updateGLVolume(); }; - const handleUpdateThresholdNegative = (event: Event, newValue: number | number[]) => { + const handleToggleNegatives = (event: ChangeEvent, checked: boolean) => { if (!niivue) return; - setThresholdNegative(newValue as number); - niivue.volumes[1].cal_minNeg = -6; - niivue.volumes[1].cal_maxNeg = newValue as number; + setShowNegatives(checked); + if (checked) { + niivue.volumes[1].colormapNegative = 'winter'; + } else { + niivue.volumes[1].colormapNegative = ''; + } niivue.updateGLVolume(); }; @@ -39,11 +87,10 @@ const NiiVueVisualizer: React.FC<{ imageURL: string }> = ({ imageURL }) => { url: imageURL, // url: 'https://niivue.github.io/niivue/images/fslt.nii.gz', colorMap: 'warm', - colormapNegative: 'winter', - cal_min: 3, - cal_max: 6, - cal_minNeg: -6, - cal_maxNeg: -3, + cal_min: 0, // default + cal_max: 6, // default + cal_minNeg: -6, // default + cal_maxNeg: 0, // default opacity: 1, }, ]; @@ -58,42 +105,56 @@ const NiiVueVisualizer: React.FC<{ imageURL: string }> = ({ imageURL }) => { niivue.attachToCanvas(canvasRef.current); niivue.addVolumesFromUrl(volumes).then(() => { niivue.volumes[1].alphaThreshold = 0; + niivue.overlayOutlineWidth = 0; + niivue.volumes[0].colorbarVisible = false; + niivue.volumes[1].colormapNegative = ''; + niivue.opts.multiplanarShowRender = SHOW_RENDER.ALWAYS; + + const globalMax = niivue.volumes[1].global_max || 6; + const globalMin = niivue.volumes[1].global_min || 0; + const largestAbsoluteValue = Math.max(Math.abs(globalMin), globalMax); + const startingValue = largestAbsoluteValue < 2.58 ? largestAbsoluteValue : 2.58; + + setThreshold({ + min: 0, + max: largestAbsoluteValue + 0.1, + value: startingValue, + }); + niivue.volumes[1].cal_min = startingValue; + niivue.volumes[1].cal_max = largestAbsoluteValue + 0.1; + niivue.setInterpolation(true); niivue.updateGLVolume(); - console.log(niivue); }); }, [imageURL]); return ( - - - -Threshold - + + Threshold - - - +Threshold - - + + Soft Threshold + + + + Show Negatives + + + + Show Crosshairs + diff --git a/compose/neurosynth-frontend/src/hooks/metaAnalyses/useGetMetaAnalysisById.tsx b/compose/neurosynth-frontend/src/hooks/metaAnalyses/useGetMetaAnalysisById.tsx index d4234a739..7cea8e67a 100644 --- a/compose/neurosynth-frontend/src/hooks/metaAnalyses/useGetMetaAnalysisById.tsx +++ b/compose/neurosynth-frontend/src/hooks/metaAnalyses/useGetMetaAnalysisById.tsx @@ -4,11 +4,7 @@ import API from 'utils/api'; const useGetMetaAnalysisById = (metaAnalysisId: string | undefined) => { const query = useQuery( ['meta-analyses', metaAnalysisId], - () => - API.NeurosynthServices.MetaAnalysisService.metaAnalysesIdGet( - metaAnalysisId || '', - true - ), + () => API.NeurosynthServices.MetaAnalysisService.metaAnalysesIdGet(metaAnalysisId || '', true), { enabled: !!metaAnalysisId, select: (data) => data.data, diff --git a/compose/neurosynth-frontend/src/hooks/metaAnalyses/useGetMetaAnalysisResultById.tsx b/compose/neurosynth-frontend/src/hooks/metaAnalyses/useGetMetaAnalysisResultById.tsx index e9779ce6f..18aad3a87 100644 --- a/compose/neurosynth-frontend/src/hooks/metaAnalyses/useGetMetaAnalysisResultById.tsx +++ b/compose/neurosynth-frontend/src/hooks/metaAnalyses/useGetMetaAnalysisResultById.tsx @@ -5,11 +5,8 @@ import API from 'utils/api'; const useGetMetaAnalysisResultById = (metaAnalysisResultId: string | undefined | null) => { return useQuery( - ['meta-analyses-results', metaAnalysisResultId], - () => - API.NeurosynthServices.MetaAnalysisService.metaAnalysisResultsIdGet( - metaAnalysisResultId || '' - ), + ['meta-analyses-result', metaAnalysisResultId], + () => API.NeurosynthServices.MetaAnalysisService.metaAnalysisResultsIdGet(metaAnalysisResultId || ''), { select: (res: AxiosResponse) => res.data, enabled: !!metaAnalysisResultId, diff --git a/compose/neurosynth-frontend/src/hooks/metaAnalyses/useGetNeurovault.tsx b/compose/neurosynth-frontend/src/hooks/metaAnalyses/useGetNeurovault.tsx new file mode 100644 index 000000000..ab49dd159 --- /dev/null +++ b/compose/neurosynth-frontend/src/hooks/metaAnalyses/useGetNeurovault.tsx @@ -0,0 +1,74 @@ +import axios, { AxiosResponse } from 'axios'; +import { useQuery } from 'react-query'; + +export interface INeurovault { + url: string; + id: number; + file: string; + collection: string; + collection_id: string; + file_size: string; + cognitive_paradigm_cogatlas: string; + cognitive_paradigm_cogatlas_id: string; + cognitive_contrast_cogatlas: string; + cognitive_contrast_cogatlas_id: string; + map_type: string; + analysis_level: string; + name: string; + description: string; + add_date: string; + modify_date: string; + is_valid: boolean; + surface_left_file: string; + surface_right_file: string; + data_origin: string; + target_template_image: string; + subject_species: string; + figure: string; + handedness: string; + age: string; + gender: string; + race: string; + ethnicity: string; + BMI: string; + fat_percentage: string; + waist_hip_ratio: string; + mean_PDS_score: string; + tanner_stage: string; + days_since_menstruation: string; + hours_since_last_meal: string; + bis_bas_score: string; + spsrq_score: string; + bis11_score: string; + thumbnail: string; + reduced_representation: string; + is_thresholded: boolean; + perc_bad_voxels: number; + not_mni: boolean; + brain_coverage: number; + perc_voxels_outside: number; + number_of_subjects: string; + modality: string; + statistic_parameters: string; + smoothness_fwhm: string; + contrast_definition: string; + contrast_definition_cogatlas: string; + cognitive_paradigm_description_url: string; + image_type: string; +} + +function useGetNeurovaultImages(neurovaultImages: string[]) { + return useQuery({ + queryKey: ['neurovault-images', ...neurovaultImages], + queryFn: async () => { + const res = await Promise.all>(neurovaultImages.map((url) => axios.get(url))); + return res.map((x) => ({ + ...x.data, + file: x.data.file.replace(/http/, 'https'), + })); + }, + enabled: neurovaultImages.length > 0, + }); +} + +export default useGetNeurovaultImages; diff --git a/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/DisplayMetaAnalysisResult.tsx b/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/DisplayMetaAnalysisResult.tsx deleted file mode 100644 index e3902430d..000000000 --- a/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/DisplayMetaAnalysisResult.tsx +++ /dev/null @@ -1,66 +0,0 @@ -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 { data, isLoading, isError } = useGetAnalysisById( - props.metaAnalysis?.neurostore_analysis?.neurostore_id || undefined - ); - - const { points, analysisSpace, analysisMap } = studyPointsToStorePoints((data?.points || []) as PointReturn[]); - - const neurovaultLink = props.metaAnalysisResult?.neurovault_collection?.url || ''; - - return ( - - - - item 1 - item 2 - item 3 - item 3 - item 3 - item 3 - item 3 - item 3 - - - - - - - - Neurovault - - - Neurovault Collection Link - - - - - - - ); -}; - -export default DisplayMetaAnalysisResult; diff --git a/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/DisplayMetaAnalysisResults.tsx b/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/DisplayMetaAnalysisResults.tsx new file mode 100644 index 000000000..255834272 --- /dev/null +++ b/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/DisplayMetaAnalysisResults.tsx @@ -0,0 +1,87 @@ +import { OpenInNew } from '@mui/icons-material'; +import { Box, Button, Link, List, ListItemButton, ListItemText, Typography } from '@mui/material'; +import NiiVueVisualizer from 'components/Visualizer/NiiVueVisualizer'; +import { MetaAnalysisReturn, NeurovaultFile, ResultReturn } from 'neurosynth-compose-typescript-sdk'; +import MetaAnalysisResultStatusAlert from './MetaAnalysisResultStatusAlert'; +import useGetMetaAnalysisResultById from 'hooks/metaAnalyses/useGetMetaAnalysisResultById'; +import { useState } from 'react'; +import StateHandlerComponent from 'components/StateHandlerComponent/StateHandlerComponent'; +import useGetNeurovaultImages, { INeurovault } from 'hooks/metaAnalyses/useGetNeurovault'; + +const DisplayMetaAnalysisResults: React.FC<{ + metaAnalysis: MetaAnalysisReturn | undefined; +}> = (props) => { + // const [] = useState(); + + // Each result represents a run. We just need to get the last item to get the latest run + const metaAnalysisResults = (props.metaAnalysis?.results || []) as ResultReturn[]; + const { data, isLoading, isError } = useGetMetaAnalysisResultById( + metaAnalysisResults[metaAnalysisResults.length - 1]?.id + ); + const neurovaultLink = data?.neurovault_collection?.url || ''; + + const neurovaultFileURLs = ((data?.neurovault_collection?.files || []) as NeurovaultFile[]).map( + (file) => file.url || '' + ); + const { + data: neurovaultFiles, + isLoading: neurovaultFilesIsLoading, + isError: neurovaultFilesIsError, + } = useGetNeurovaultImages(neurovaultFileURLs); + console.log({ neurovaultFiles }); + const [selectedNeurovaultImage, setSelectedNeurovaultImage] = useState(); + + return ( + + + + + + {(neurovaultFiles || []).map((neurovaultFile) => ( + setSelectedNeurovaultImage(neurovaultFile)} + selected={selectedNeurovaultImage?.id === neurovaultFile.id} + > + + + ))} + + + + {selectedNeurovaultImage?.file ? ( + + ) : ( + No image selected + )} + + + + + ); +}; + +export default DisplayMetaAnalysisResults; diff --git a/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/MetaAnalysisResult.tsx b/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/MetaAnalysisResult.tsx index c488c31cf..c8d4582b3 100644 --- a/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/MetaAnalysisResult.tsx +++ b/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/MetaAnalysisResult.tsx @@ -1,11 +1,8 @@ import { Box, Tab, Tabs, Typography } from '@mui/material'; import { useGetMetaAnalysisById } from 'hooks'; -import useGetMetaAnalysisResultById from 'hooks/metaAnalyses/useGetMetaAnalysisResultById'; -import { ResultReturn } from 'neurosynth-compose-typescript-sdk'; import { useState } from 'react'; import { useParams } from 'react-router-dom'; -import DisplayMetaAnalysisResult from './DisplayMetaAnalysisResult'; -import MetaAnalysisResultStatusAlert from './MetaAnalysisResultStatusAlert'; +import DisplayMetaAnalysisResults from './DisplayMetaAnalysisResults'; import DisplayMetaAnalysisSpecification from './MetaAnalysisSpecification'; const MetaAnalysisResult: React.FC = () => { @@ -14,12 +11,6 @@ const MetaAnalysisResult: React.FC = () => { metaAnalysisId: string; }>(); const { data: metaAnalysis } = useGetMetaAnalysisById(metaAnalysisId); - const { data: metaAnalysisResult } = useGetMetaAnalysisResultById( - metaAnalysis?.results && metaAnalysis.results.length - ? (metaAnalysis.results[metaAnalysis.results.length - 1] as ResultReturn).id - : undefined - ); - const [tab, setTab] = useState(0); return ( @@ -54,13 +45,9 @@ const MetaAnalysisResult: React.FC = () => { {tab === 0 ? ( - <> - - - + + + ) : ( diff --git a/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/MetaAnalysisResultStatusAlert.tsx b/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/MetaAnalysisResultStatusAlert.tsx index 47ab45d4c..a323e4b54 100644 --- a/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/MetaAnalysisResultStatusAlert.tsx +++ b/compose/neurosynth-frontend/src/pages/MetaAnalysis/components/MetaAnalysisResultStatusAlert.tsx @@ -1,27 +1,41 @@ import { Alert } from '@mui/material'; import { getResultStatus } from 'helpers/MetaAnalysis.helpers'; import { MetaAnalysisReturn, ResultReturn } from 'neurosynth-compose-typescript-sdk'; -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; + +const localStorageResultAlertKey = 'hide-meta-analysis-result-alert'; const MetaAnalysisResultStatusAlert: React.FC<{ metaAnalysis?: MetaAnalysisReturn; metaAnalysisResult?: ResultReturn; }> = ({ metaAnalysis, metaAnalysisResult }) => { + const shouldHide = !!localStorage.getItem(`${localStorageResultAlertKey}-${metaAnalysis?.id}`); + const [hideAlert, setHideAlert] = useState(shouldHide); + const resultStatus = useMemo(() => { return getResultStatus(metaAnalysis, metaAnalysisResult); }, [metaAnalysis, metaAnalysisResult]); return ( - - {resultStatus.statusText} - + <> + {!hideAlert && ( + { + setHideAlert(true); + localStorage.setItem(`${localStorageResultAlertKey}-${metaAnalysis?.id}`, 'true'); + }} + sx={{ + padding: '4px 10px', + marginBottom: '1rem', + alignItems: 'center', + }} + > + {resultStatus.statusText} + + )} + ); }; diff --git a/compose/neurosynth-frontend/src/utils/api.ts b/compose/neurosynth-frontend/src/utils/api.ts index 669504811..1dc3ae8a4 100644 --- a/compose/neurosynth-frontend/src/utils/api.ts +++ b/compose/neurosynth-frontend/src/utils/api.ts @@ -34,14 +34,13 @@ const env = import.meta.env.VITE_APP_ENV as 'DEV' | 'STAGING' | 'PROD'; const NEUROSTORE_API_DOMAIN = import.meta.env.VITE_APP_NEUROSTORE_API_DOMAIN as string; const NEUROSYNTH_API_DOMAIN = import.meta.env.VITE_APP_NEUROSYNTH_API_DOMAIN as string; -let TOKEN = ''; const neurostoreConfig: Configuration = new Configuration({ basePath: NEUROSTORE_API_DOMAIN, - accessToken: TOKEN, + accessToken: '', }); const neurosynthConfig: Configuration = new Configuration({ basePath: NEUROSYNTH_API_DOMAIN, - accessToken: TOKEN, + accessToken: '', }); const NeurostoreServices = {