1- import { Box , Button , Checkbox , Link , Slider , Typography } from '@mui/material' ;
2- import { ChangeEvent , useEffect , useRef , useState } from 'react' ;
1+ import { Box , Button , Checkbox , Link , Slider , TextField , Typography } from '@mui/material' ;
2+ import { ChangeEvent , useCallback , useEffect , useRef , useState } from 'react' ;
33import { Niivue , SHOW_RENDER } from '@niivue/niivue' ;
44import { Download , OpenInNew } from '@mui/icons-material' ;
55import ImageIcon from '@mui/icons-material/Image' ;
6+ import ThresholdSlider from './ThresholdSlider' ;
7+ import StateHandlerComponent from 'components/StateHandlerComponent/StateHandlerComponent' ;
68
7- let niivue : Niivue ;
9+ let thresholdDebounce : NodeJS . Timeout ;
810
911const NiiVueVisualizer : React . FC < { file : string ; filename : string ; neurovaultLink ?: string } > = ( {
1012 file,
1113 filename,
1214 neurovaultLink,
1315} ) => {
1416 const canvasRef = useRef < HTMLCanvasElement > ( null ) ;
15- const [ softThreshold , setSoftThresold ] = useState ( true ) ;
17+ const niivueRef = useRef < Niivue | null > ( null ) ;
18+ const [ softThreshold , setSoftThreshold ] = useState ( true ) ;
1619 const [ showNegatives , setShowNegatives ] = useState ( false ) ;
20+ const [ disableNegatives , setDisableNegatives ] = useState ( false ) ;
1721 const [ showCrosshairs , setShowCrosshairs ] = useState ( true ) ;
22+ const [ brainCoordinateString , setBrainCoordinateString ] = useState ( '' ) ;
23+ const [ isLoading , setIsLoading ] = useState ( false ) ;
24+
1825 const [ threshold , setThreshold ] = useState < {
1926 min : number ;
2027 max : number ;
@@ -25,155 +32,257 @@ const NiiVueVisualizer: React.FC<{ file: string; filename: string; neurovaultLin
2532 value : 3 ,
2633 } ) ;
2734
28- const handleUpdateThreshold = ( event : Event , newValue : number | number [ ] ) => {
29- if ( ! niivue ) return ;
30- const typedVal = newValue as number ;
31- setThreshold ( ( prev ) => ( {
32- ...prev ,
33- value : typedVal ,
34- } ) ) ;
35+ const handleChangeLocation = ( location : unknown ) => {
36+ const typedLocation = location as {
37+ axCorSage : number ;
38+ frac : Float32Array ;
39+ mm : Float32Array ;
40+ string : string ;
41+ values : { id : string ; mm : Float32Array ; name : string ; value : number ; vox : number [ ] } [ ] ;
42+ vox : Float32Array ;
43+ xy : number [ ] ;
44+ } ;
3545
36- // update threshold positive
37- niivue . volumes [ 1 ] . cal_min = typedVal ;
46+ const fileValues = typedLocation ?. values ?. [ 1 ] ;
47+ if ( ! fileValues ) return ;
48+ const [ x , y , z ] = fileValues ?. mm || [ ] ;
49+ const value = fileValues ?. value ;
3850
39- // update threshold negative
40- niivue . volumes [ 1 ] . cal_maxNeg = - 1 * typedVal ;
51+ const str = `X: ${ Math . round ( x ) } | Y: ${ Math . round ( y ) } | Z: ${ Math . round ( z ) } = ${ value . toFixed ( 3 ) } ` ;
52+ setBrainCoordinateString ( str ) ;
53+ } ;
54+
55+ const updateSoftThresholdInNiivue = ( softThresholdEnabled : boolean ) => {
56+ if ( ! niivueRef . current ) return ;
4157
42- niivue . updateGLVolume ( ) ;
58+ if ( softThresholdEnabled ) {
59+ niivueRef . current . overlayOutlineWidth = 2 ;
60+ niivueRef . current . volumes [ 1 ] . alphaThreshold = 5 ;
61+ } else {
62+ niivueRef . current . overlayOutlineWidth = 0 ;
63+ niivueRef . current . volumes [ 1 ] . alphaThreshold = 0 ;
64+ }
65+ niivueRef . current . updateGLVolume ( ) ;
4366 } ;
4467
4568 const handleToggleSoftThreshold = ( event : ChangeEvent < HTMLInputElement > , checked : boolean ) => {
46- if ( ! niivue ) return ;
69+ setSoftThreshold ( checked ) ;
70+ updateSoftThresholdInNiivue ( checked ) ;
71+ } ;
4772
48- setSoftThresold ( checked ) ;
49- if ( checked ) {
50- niivue . overlayOutlineWidth = 2 ;
51- niivue . volumes [ 1 ] . alphaThreshold = 5 ;
73+ const updateCrosshairsInNiivue = ( showCrosshairsEnabled : boolean ) => {
74+ if ( ! niivueRef . current ) return ;
75+ if ( showCrosshairsEnabled ) {
76+ niivueRef . current . setCrosshairWidth ( 1 ) ;
5277 } else {
53- niivue . overlayOutlineWidth = 0 ;
54- niivue . volumes [ 1 ] . alphaThreshold = 0 ;
78+ niivueRef . current . setCrosshairWidth ( 0 ) ;
5579 }
56- niivue . updateGLVolume ( ) ;
80+ niivueRef . current . updateGLVolume ( ) ;
5781 } ;
5882
5983 const handleToggleShowCrosshairs = ( event : ChangeEvent < HTMLInputElement > , checked : boolean ) => {
60- if ( ! niivue ) return ;
6184 setShowCrosshairs ( checked ) ;
62- if ( checked ) {
63- niivue . setCrosshairWidth ( 1 ) ;
85+ updateCrosshairsInNiivue ( checked ) ;
86+ } ;
87+
88+ const updateNegativesInNiivue = ( showNegativesEnabled : boolean ) => {
89+ if ( ! niivueRef . current ) return ;
90+
91+ if ( showNegativesEnabled ) {
92+ niivueRef . current . volumes [ 1 ] . colormapNegative = 'winter' ;
6493 } else {
65- niivue . setCrosshairWidth ( 0 ) ;
94+ niivueRef . current . volumes [ 1 ] . colormapNegative = '' ;
6695 }
67- niivue . updateGLVolume ( ) ;
96+ niivueRef . current . updateGLVolume ( ) ;
6897 } ;
6998
7099 const handleToggleNegatives = ( event : ChangeEvent < HTMLInputElement > , checked : boolean ) => {
71- if ( ! niivue ) return ;
72100 setShowNegatives ( checked ) ;
73- if ( checked ) {
74- niivue . volumes [ 1 ] . colormapNegative = 'winter' ;
75- } else {
76- niivue . volumes [ 1 ] . colormapNegative = '' ;
77- }
78- niivue . updateGLVolume ( ) ;
101+ updateNegativesInNiivue ( checked ) ;
79102 } ;
80103
81104 useEffect ( ( ) => {
82- if ( ! canvasRef . current ) return ;
83-
84- const volumes = [
85- {
86- // TODO: need to check if TAL vs MNI and set accordingly
87- url : 'https://neurovault.org/static/images/GenericMNI.nii.gz' ,
88- // url: 'https://niivue.github.io/niivue/images/fslmean.nii.gz',
89- colormap : 'gray' ,
90- opacity : 1 ,
91- } ,
92- {
105+ const updateNiivue = async ( ) => {
106+ if ( ! canvasRef . current ) return ;
107+
108+ // this should only run once initially to load the niivue instance as well as a base image
109+ if ( niivueRef . current === null ) {
110+ niivueRef . current = new Niivue ( {
111+ show3Dcrosshair : true ,
112+ } ) ;
113+ niivueRef . current . attachToCanvas ( canvasRef . current ) ;
114+ niivueRef . current . overlayOutlineWidth = 2 ;
115+ niivueRef . current . opts . multiplanarShowRender = SHOW_RENDER . ALWAYS ;
116+ niivueRef . current . opts . isColorbar = true ;
117+ niivueRef . current . setSliceMM ( false ) ;
118+ niivueRef . current . onLocationChange = handleChangeLocation ;
119+ await niivueRef . current . addVolumeFromUrl ( {
120+ // we can assume that maps will only be in MNI space
121+ url : 'https://neurovault.org/static/images/GenericMNI.nii.gz' ,
122+ colormap : 'gray' ,
123+ opacity : 1 ,
124+ colorbarVisible : false ,
125+ } ) ;
126+ }
127+
128+ const niivue = niivueRef . current ;
129+ await niivueRef . current . addVolumeFromUrl ( {
93130 url : file ,
94- // url: 'https://niivue.github.io/niivue/images/fslt.nii.gz',
95- colorMap : 'warm' ,
131+ colormap : 'warm' ,
96132 cal_min : 0 , // default
97133 cal_max : 6 , // default
98134 cal_minNeg : - 6 , // default
99135 cal_maxNeg : 0 , // default
100136 opacity : 1 ,
101- } ,
102- ] ;
103-
104- niivue = new Niivue ( {
105- show3Dcrosshair : true ,
106- } ) ;
107-
108- niivue . opts . isColorbar = true ;
109- niivue . setSliceMM ( false ) ;
110-
111- niivue . attachToCanvas ( canvasRef . current ) ;
112- niivue . addVolumesFromUrl ( volumes ) . then ( ( ) => {
113- niivue . overlayOutlineWidth = 2 ;
114- niivue . volumes [ 1 ] . alphaThreshold = 5 ;
115-
116- niivue . volumes [ 0 ] . colorbarVisible = false ;
117- niivue . volumes [ 1 ] . colormapNegative = '' ;
118-
119- niivue . opts . multiplanarShowRender = SHOW_RENDER . ALWAYS ;
137+ } ) ;
120138
121- const globalMax = niivue . volumes [ 1 ] . global_max || 6 ;
139+ const globalMax = niivue . volumes [ 1 ] . global_max || 2.58 ;
122140 const globalMin = niivue . volumes [ 1 ] . global_min || 0 ;
123141 const largestAbsoluteValue = Math . max ( Math . abs ( globalMin ) , globalMax ) ;
124- const startingValue = largestAbsoluteValue < 2.58 ? largestAbsoluteValue : 2.58 ;
142+
143+ updateCrosshairsInNiivue ( showCrosshairs ) ; // update crosshair settings in case they have been updated in other maps
144+ updateSoftThresholdInNiivue ( softThreshold ) ; // update threshold settings in case they have been updated in other maps
145+ // update negative settings in case they have been updated in other maps. If no negatives, disable
146+ if ( globalMin < 0 ) {
147+ setShowNegatives ( false ) ;
148+ setDisableNegatives ( false ) ;
149+ updateNegativesInNiivue ( false ) ;
150+ } else {
151+ setShowNegatives ( false ) ;
152+ setDisableNegatives ( true ) ;
153+ updateNegativesInNiivue ( false ) ;
154+ }
155+
156+ let startingValue ;
157+ let maxOrThreshold ;
158+ if ( filename . startsWith ( 'z_' ) ) {
159+ startingValue = 2.58 ;
160+ maxOrThreshold = largestAbsoluteValue < 2.58 ? 2.58 : largestAbsoluteValue ;
161+ } else {
162+ startingValue = 0 ;
163+ maxOrThreshold = largestAbsoluteValue ;
164+ }
125165
126166 setThreshold ( {
127167 min : 0 ,
128- max : Math . round ( ( largestAbsoluteValue + 0.1 ) * 100 ) / 100 ,
129- value : startingValue ,
168+ max : Math . round ( maxOrThreshold * 100 ) / 100 ,
169+ value : Math . round ( startingValue * 100 ) / 100 ,
130170 } ) ;
171+
131172 niivue . volumes [ 1 ] . cal_min = startingValue ;
132- niivue . volumes [ 1 ] . cal_max = largestAbsoluteValue + 0.1 ;
173+ niivue . volumes [ 1 ] . cal_max = maxOrThreshold ;
133174
134175 niivue . setInterpolation ( true ) ;
135176 niivue . updateGLVolume ( ) ;
136- } ) ;
137- } , [ file ] ) ;
177+ } ;
178+
179+ updateNiivue ( ) ;
180+
181+ return ( ) => {
182+ if ( niivueRef . current && niivueRef . current . volumes [ 1 ] ) {
183+ niivueRef . current . removeVolume ( niivueRef . current . volumes [ 1 ] ) ;
184+ }
185+ } ;
186+ } , [ file , filename ] ) ;
138187
139188 const handleDownloadImage = ( ) => {
140- if ( ! niivue ) return ;
141- niivue . saveScene ( filename + '.png' ) ;
189+ if ( ! niivueRef . current ) return ;
190+ niivueRef . current . saveScene ( filename + '.png' ) ;
191+ } ;
192+
193+ const updateThresholdNiivue = ( update : { thresholdValue : number ; thresholdMax : number ; thresholdMin : number } ) => {
194+ if ( ! niivueRef . current ) return ;
195+
196+ // update threshold positive
197+ niivueRef . current . volumes [ 1 ] . cal_min = update . thresholdValue ;
198+ // update threshold negative
199+ niivueRef . current . volumes [ 1 ] . cal_minNeg = - 1 * update . thresholdValue ;
200+
201+ niivueRef . current . volumes [ 1 ] . cal_max = update . thresholdMax ;
202+ niivueRef . current . volumes [ 1 ] . cal_maxNeg = - 1 * update . thresholdMax ;
203+
204+ niivueRef . current . updateGLVolume ( ) ;
142205 } ;
143206
207+ const handleUpdateThreshold = useCallback (
208+ ( update : { thresholdValue : number ; thresholdMax : number ; thresholdMin : number } ) => {
209+ setThreshold ( {
210+ min : update . thresholdMin ,
211+ max : update . thresholdMax ,
212+ value : update . thresholdValue ,
213+ } ) ;
214+
215+ updateThresholdNiivue ( update ) ;
216+ } ,
217+ [ ]
218+ ) ;
219+
144220 return (
145221 < Box >
222+ { /* <StateHandlerComponent isLoading={isLoading} isError={false}> */ }
146223 < Box sx = { { marginBottom : '10px' , display : 'flex' , justifyContent : 'space-between' } } >
147224 < Box width = "250px" >
148- < Typography gutterBottom = { false } > Threshold</ Typography >
149- < Slider
150- valueLabelDisplay = "auto"
151- min = { threshold . min }
152- step = { 0.01 }
153- max = { threshold . max }
154- onChange = { handleUpdateThreshold }
155- value = { threshold . value }
156- > </ Slider >
225+ < ThresholdSlider
226+ thresholdMin = { threshold . min }
227+ thresholdMax = { threshold . max }
228+ threshold = { threshold . value }
229+ onDebouncedThresholdChange = { handleUpdateThreshold }
230+ />
157231 </ Box >
158- < Box display = "flex" >
159- < Box width = "100px" >
160- < Typography gutterBottom = { false } > Soft Threshold</ Typography >
161- < Checkbox checked = { softThreshold } onChange = { handleToggleSoftThreshold } />
232+ < Box width = "130px" display = "flex" flexDirection = "column" >
233+ < Box display = "flex" justifyContent = "space-between" alignItems = "center" >
234+ < Typography variant = "caption" gutterBottom = { false } >
235+ Soft Threshold
236+ </ Typography >
237+ < Checkbox sx = { { padding : 0 } } checked = { softThreshold } onChange = { handleToggleSoftThreshold } />
162238 </ Box >
163- < Box width = "100px" >
164- < Typography gutterBottom = { false } > Show Negatives</ Typography >
165- < Checkbox checked = { showNegatives } onChange = { handleToggleNegatives } />
166- </ Box >
167- < Box width = "100px" >
168- < Typography gutterBottom = { false } > Show Crosshairs</ Typography >
239+ < Box display = "flex" justifyContent = "space-between" alignItems = "center" >
240+ < Typography variant = "caption" gutterBottom = { false } >
241+ Show Crosshairs
242+ </ Typography >
169243 < Checkbox
244+ sx = { { padding : 0 } }
170245 value = { showCrosshairs }
171246 checked = { showCrosshairs }
172247 onChange = { handleToggleShowCrosshairs }
173248 />
174249 </ Box >
250+ < Box display = "flex" justifyContent = "space-between" alignItems = "center" >
251+ < Typography
252+ variant = "caption"
253+ color = { disableNegatives ? 'muted.main' : 'inherit' }
254+ gutterBottom = { false }
255+ >
256+ { disableNegatives ? 'No Negatives' : 'Show Negatives' }
257+ { /* Show Negatives */ }
258+ </ Typography >
259+ < Checkbox
260+ sx = { { padding : 0 } }
261+ disabled = { disableNegatives }
262+ checked = { showNegatives }
263+ onChange = { handleToggleNegatives }
264+ />
265+ </ Box >
175266 </ Box >
176267 </ Box >
268+ { /* </StateHandlerComponent> */ }
269+ < Box sx = { { height : '32px' } } >
270+ { brainCoordinateString && (
271+ < Box
272+ sx = { {
273+ width : '260px' ,
274+ backgroundColor : 'black' ,
275+ textAlign : 'center' ,
276+ borderTopLeftRadius : '4px' ,
277+ borderTopRightRadius : '4px' ,
278+ } }
279+ >
280+ < Typography padding = "4px 8px" display = "inline-block" color = "white" >
281+ { brainCoordinateString }
282+ </ Typography >
283+ </ Box >
284+ ) }
285+ </ Box >
177286 < Box sx = { { height : '300px' } } >
178287 < canvas ref = { canvasRef } />
179288 </ Box >
0 commit comments