├── .gitignore ├── .gitignore~ ├── Procfile ├── README.md ├── ZOMBIE_BRAIN.png ├── aal_atlas.txt ├── app.py ├── app.py~ ├── car.obj ├── gray_scale.txt ├── mouse_brain_outline.obj ├── mouse_map.txt ├── mouse_surf.obj ├── realct.obj ├── realct.txt ├── requirements.txt ├── requirements.txt~ └── surf_reg_model_both.obj /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | *.pyc 3 | .DS_Store 4 | .env 5 | -------------------------------------------------------------------------------- /.gitignore~: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:server 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Dash Brain Surface Viewer 2 | 3 | View the app: http://brain-surface-viewer.herokuapp.com/ 4 | 5 | ![brain-app-screenshot](ZOMBIE_BRAIN.png) 6 | 7 | ### Credit 8 | 9 | - [ACE Lab](https://www.mcgill.ca/bic/research/ace-lab-evans) at McGill for the brain data and inspiration from their excellent brain [Surface Viewer](https://brainbrowser.cbrain.mcgill.ca/surface-viewer#ct) 10 | - [Julia Huntenburg](https://github.com/juhuntenburg) for figuring out how to [read MNI objects in Python](https://github.com/juhuntenburg/laminar_python/blob/master/io_mesh.py) 11 | - [E. Petrisor](https://github.com/empet) for her extensive [exploration in Python with Plotly.js meshes](https://plot.ly/~empet/14767/mesh3d-from-a-stl-file/) 12 | 13 | -------------------------------------------------------------------------------- /ZOMBIE_BRAIN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/dash-brain-surface-viewer/4cc4051e922a24649a4a4bcde6a9ed0a983f7ab6/ZOMBIE_BRAIN.png -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import dash 3 | import dash_core_components as dcc 4 | import dash_html_components as html 5 | from dash.dependencies import Input, Output, State 6 | import dash_colorscales as dcs 7 | import numpy as np 8 | import json 9 | from textwrap import dedent as d 10 | 11 | app = dash.Dash(__name__) 12 | server = app.server 13 | 14 | DEFAULT_COLORSCALE = [[0, 'rgb(12,51,131)'], [0.25, 'rgb(10,136,186)'],\ 15 | [0.5, 'rgb(242,211,56)'], [0.75, 'rgb(242,143,56)'], \ 16 | [1, 'rgb(217,30,30)']] 17 | 18 | DEFAULT_COLORSCALE_NO_INDEX = [ea[1] for ea in DEFAULT_COLORSCALE] 19 | 20 | def read_mniobj(file): 21 | ''' function to read a MNI obj file ''' 22 | 23 | def triangulate_polygons(list_vertex_indices): 24 | ''' triangulate a list of n indices n=len(list_vertex_indices) ''' 25 | 26 | for k in range(0, len(list_vertex_indices), 3): 27 | yield list_vertex_indices[k: k+3] 28 | 29 | fp=open(file,'r') 30 | n_vert=[] 31 | n_poly=[] 32 | k=0 33 | list_indices=[] 34 | # Find number of vertices and number of polygons, stored in .obj file. 35 | # Then extract list of all vertices in polygons 36 | for i, line in enumerate(fp): 37 | if i==0: 38 | #Number of vertices 39 | n_vert=int(line.split()[6]) 40 | vertices=np.zeros([n_vert,3]) 41 | elif i<=n_vert: 42 | vertices[i-1]=list(map(float, line.split())) 43 | elif i>2*n_vert+5: 44 | if not line.strip(): 45 | k=1 46 | elif k==1: 47 | list_indices.extend(line.split()) 48 | #at this point list_indices is a list of strings, and each string is a vertex index, like this '23' 49 | #maps in Python 3.6 returns a generator, hence we convert it to a list 50 | list_indices=list(map(int, list_indices))#conver the list of string indices to int indices 51 | faces=np.array(list(triangulate_polygons(np.array(list_indices)))) 52 | return vertices, faces 53 | 54 | def standard_intensity(x,y,z): 55 | ''' color the mesh with a colorscale according to the values 56 | of the vertices z-coordinates ''' 57 | return z 58 | 59 | def plotly_triangular_mesh(vertices, faces, intensities=None, colorscale="Viridis", 60 | flatshading=False, showscale=False, reversescale=False, plot_edges=False): 61 | ''' vertices = a numpy array of shape (n_vertices, 3) 62 | faces = a numpy array of shape (n_faces, 3) 63 | intensities can be either a function of (x,y,z) or a list of values ''' 64 | 65 | x,y,z=vertices.T 66 | I,J,K=faces.T 67 | 68 | if intensities is None: 69 | intensities = standard_intensity(x,y,z) 70 | 71 | if hasattr(intensities, '__call__'): 72 | intensity=intensities(x,y,z)#the intensities are computed via a function, 73 | #that returns the list of vertices intensities 74 | elif isinstance(intensities, (list, np.ndarray)): 75 | intensity=intensities#intensities are given in a list 76 | else: 77 | raise ValueError("intensities can be either a function or a list, np.array") 78 | 79 | mesh=dict( 80 | type='mesh3d', 81 | x=x, y=y, z=z, 82 | colorscale=colorscale, 83 | intensity= intensities, 84 | flatshading=flatshading, 85 | i=I, j=J, k=K, 86 | name='', 87 | showscale=showscale 88 | ) 89 | 90 | mesh.update(lighting=dict( ambient= 0.18, 91 | diffuse= 1, 92 | fresnel= 0.1, 93 | specular= 1, 94 | roughness= 0.1, 95 | facenormalsepsilon=1e-6, 96 | vertexnormalsepsilon= 1e-12)) 97 | 98 | mesh.update(lightposition=dict(x=100, 99 | y=200, 100 | z= 0)) 101 | 102 | if showscale is True: 103 | mesh.update(colorbar=dict(thickness=20, ticklen=4, len=0.75)) 104 | 105 | if plot_edges is False: # the triangle sides are not plotted 106 | return [mesh] 107 | else:#plot edges 108 | #define the lists Xe, Ye, Ze, of x, y, resp z coordinates of edge end points for each triangle 109 | #None separates data corresponding to two consecutive triangles 110 | tri_vertices= vertices[faces] 111 | Xe=[] 112 | Ye=[] 113 | Ze=[] 114 | for T in tri_vertices: 115 | Xe+=[T[k%3][0] for k in range(4)]+[ None] 116 | Ye+=[T[k%3][1] for k in range(4)]+[ None] 117 | Ze+=[T[k%3][2] for k in range(4)]+[ None] 118 | #define the lines to be plotted 119 | lines=dict(type='scatter3d', 120 | x=Xe, 121 | y=Ye, 122 | z=Ze, 123 | mode='lines', 124 | name='', 125 | line=dict(color= 'rgb(70,70,70)', width=1) 126 | ) 127 | return [mesh, lines] 128 | 129 | pts, tri=read_mniobj("surf_reg_model_both.obj") 130 | intensities=np.loadtxt('aal_atlas.txt') 131 | data=plotly_triangular_mesh(pts, tri, intensities, 132 | colorscale=DEFAULT_COLORSCALE, flatshading=False, 133 | showscale=False, reversescale=False, plot_edges=False) 134 | data[0]['name'] = 'human_atlas' 135 | 136 | axis_template = dict( 137 | showbackground=True, 138 | backgroundcolor="rgb(10, 10,10)", 139 | gridcolor="rgb(255, 255, 255)", 140 | zerolinecolor="rgb(255, 255, 255)") 141 | 142 | plot_layout = dict( 143 | title = '', 144 | margin=dict(t=0,b=0,l=0,r=0), 145 | font=dict(size=12, color='white'), 146 | width=700, 147 | height=700, 148 | showlegend=False, 149 | plot_bgcolor='black', 150 | paper_bgcolor='black', 151 | scene=dict(xaxis=axis_template, 152 | yaxis=axis_template, 153 | zaxis=axis_template, 154 | aspectratio=dict(x=1, y=1.2, z=1), 155 | camera=dict(eye=dict(x=1.25, y=1.25, z=1.25)), 156 | annotations=[] 157 | ) 158 | ) 159 | 160 | styles = { 161 | 'pre': { 162 | 'border': 'thin lightgrey solid', 163 | 'padding': '10px', 164 | 'marginBottom': '20px' 165 | }, 166 | 'graph': { 167 | 'userSelect': 'none', 168 | 'margin': 'auto' 169 | } 170 | } 171 | 172 | ''' 173 | ~~~~~~~~~~~~~~~~ 174 | ~~ APP LAYOUT ~~ 175 | ~~~~~~~~~~~~~~~~ 176 | ''' 177 | 178 | app.layout = html.Div(children=[ 179 | html.P( 180 | children=[''' 181 | Click on the brain to add an annotation. \ 182 | Drag the black corners of the graph to rotate. ''', 183 | html.A( 184 | children='GitHub', 185 | target= '_blank', 186 | href='https://github.com/plotly/dash-brain-surface-viewer', 187 | style={'color': '#F012BE'} 188 | ), 189 | '.' 190 | ] 191 | ), 192 | html.Div([ 193 | html.P( 194 | children='Click colorscale to change:', 195 | style={'display':'inline-block', 'fontSize':'12px'} 196 | ), 197 | html.Div([ 198 | dcs.DashColorscales( 199 | id='colorscale-picker', 200 | colorscale=DEFAULT_COLORSCALE_NO_INDEX 201 | )], style={'marginTop':'-15px', 'marginLeft':'-30px'} 202 | ), 203 | html.Div([ 204 | dcc.RadioItems( 205 | options=[ 206 | {'label': 'Cortical Thickness', 'value': 'human'}, 207 | {'label': 'Mouse Brain', 'value': 'mouse'}, 208 | {'label': 'Brain Atlas', 'value': 'human_atlas'}, 209 | ], 210 | value='human_atlas', 211 | id='radio-options', 212 | labelStyle={'display': 'inline-block'} 213 | ) 214 | ]) 215 | ]), 216 | dcc.Graph( 217 | id='brain-graph', 218 | figure={ 219 | 'data': data, 220 | 'layout': plot_layout, 221 | }, 222 | config={'editable': True, 'scrollZoom': False}, 223 | style=styles['graph'] 224 | ), 225 | html.Div([ 226 | dcc.Markdown(d(""" 227 | **Click Data** 228 | 229 | Click on points in the graph. 230 | """)), 231 | html.Pre(id='click-data', style=styles['pre']), 232 | ]), 233 | html.Div([ 234 | dcc.Markdown(d(""" 235 | **Relayout Data** 236 | 237 | Drag the graph corners to rotate it. 238 | """)), 239 | html.Pre(id='relayout-data', style=styles['pre']), 240 | ]), 241 | html.P( 242 | children=[ 243 | 'Dash/Python code on ', 244 | html.A( 245 | children='GitHub', 246 | target= '_blank', 247 | href='https://github.com/plotly/dash-brain-surface-viewer', 248 | style={'color': '#F012BE'} 249 | ), 250 | '. Brain data from Mcgill\'s ACE Lab ', 251 | html.A( 252 | children='Surface Viewer', 253 | target= '_blank', 254 | href='https://brainbrowser.cbrain.mcgill.ca/surface-viewer#ct', 255 | style={'color': '#F012BE'} 256 | ), 257 | '.' 258 | ] 259 | ) 260 | ], style={'margin': '0 auto'}) 261 | 262 | app.css.append_css({'external_url': 'https://codepen.io/plotly/pen/YeqjLb.css'}) 263 | 264 | @app.callback( 265 | Output('brain-graph', 'figure'), 266 | [Input('brain-graph', 'clickData'), 267 | Input('radio-options', 'value'), 268 | Input('colorscale-picker', 'colorscale')], 269 | [State('brain-graph', 'figure')]) 270 | def add_marker(clickData, val, colorscale, figure): 271 | 272 | if figure['data'][0]['name'] != val: 273 | if val == 'human': 274 | pts, tri=read_mniobj("realct.obj") 275 | intensities=np.loadtxt('realct.txt') 276 | figure['data']=plotly_triangular_mesh(pts, tri, intensities, 277 | colorscale=DEFAULT_COLORSCALE, flatshading=False, 278 | showscale=False, reversescale=False, plot_edges=False) 279 | elif val == 'human_atlas': 280 | pts, tri=read_mniobj("surf_reg_model_both.obj") 281 | intensities=np.loadtxt('aal_atlas.txt') 282 | figure['data']=plotly_triangular_mesh(pts, tri, intensities, 283 | colorscale=DEFAULT_COLORSCALE, flatshading=False, 284 | showscale=False, reversescale=False, plot_edges=False) 285 | elif val == 'mouse': 286 | pts, tri=read_mniobj("mouse_surf.obj") 287 | intensities=np.loadtxt('mouse_map.txt') 288 | figure['data']=plotly_triangular_mesh(pts, tri, intensities, 289 | colorscale=DEFAULT_COLORSCALE, flatshading=False, 290 | showscale=False, reversescale=False, plot_edges=False) 291 | pts, tri=read_mniobj("mouse_brain_outline.obj") 292 | outer_mesh = plotly_triangular_mesh(pts, tri)[0] 293 | outer_mesh['opacity'] = 0.5 294 | outer_mesh['colorscale'] = 'Greys' 295 | figure['data'].append(outer_mesh) 296 | figure['data'][0]['name'] = val 297 | 298 | elif clickData != None: 299 | if 'points' in clickData: 300 | marker = dict( 301 | x = [clickData['points'][0]['x']], 302 | y = [clickData['points'][0]['y']], 303 | z = [clickData['points'][0]['z']], 304 | mode = 'markers', 305 | marker = dict(size=15, line=dict(width=3)), 306 | name = 'Marker', 307 | type = 'scatter3d', 308 | text = ['Click point to remove annotation'] 309 | ) 310 | anno = dict( 311 | x = clickData['points'][0]['x'], 312 | y = clickData['points'][0]['y'], 313 | z = clickData['points'][0]['z'], 314 | font = dict(color = 'black'), 315 | bgcolor = 'white', 316 | borderpad = 5, 317 | bordercolor = 'black', 318 | borderwidth = 1, 319 | captureevents = True, 320 | ay = -50, 321 | arrowcolor = 'white', 322 | arrowwidth = 2, 323 | arrowhead = 0, 324 | text = 'Click here to annotate
(Click point to remove)', 325 | ) 326 | if len(figure['data']) > 1: 327 | same_point_found = False 328 | for i, pt in enumerate(figure['data']): 329 | if pt['x'] == marker['x'] and pt['y'] == marker['y'] and pt['z'] == marker['z']: 330 | ANNO_TRACE_INDEX_OFFSET = 1 331 | if val == 'mouse': 332 | ANNO_TRACE_INDEX_OFFSET = 2 333 | figure['data'].pop(i) 334 | print('DEL. MARKER', i, figure['layout']['scene']['annotations']) 335 | if len(figure['layout']['scene']['annotations']) >= (i-ANNO_TRACE_INDEX_OFFSET): 336 | try: 337 | figure['layout']['scene']['annotations'].pop(i-ANNO_TRACE_INDEX_OFFSET) 338 | except: 339 | pass 340 | same_point_found = True 341 | break 342 | if same_point_found == False: 343 | figure['data'].append(marker) 344 | figure['layout']['scene']['annotations'].append(anno) 345 | else: 346 | figure['data'].append(marker) 347 | figure['layout']['scene']['annotations'].append(anno) 348 | 349 | cs = [] 350 | for i, rgb in enumerate(colorscale): 351 | cs.append([i/(len(colorscale)-1), rgb]) 352 | figure['data'][0]['colorscale'] = cs 353 | 354 | return figure 355 | 356 | @app.callback( 357 | Output('click-data', 'children'), 358 | [Input('brain-graph', 'clickData')]) 359 | def display_click_data(clickData): 360 | return json.dumps(clickData, indent=4) 361 | 362 | @app.callback( 363 | Output('relayout-data', 'children'), 364 | [Input('brain-graph', 'relayoutData')]) 365 | def display_click_data(relayoutData): 366 | return json.dumps(relayoutData, indent=4) 367 | 368 | if __name__ == '__main__': 369 | app.run_server(debug=True) 370 | -------------------------------------------------------------------------------- /app.py~: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import dash 3 | import dash_core_components as dcc 4 | import dash_html_components as html 5 | from dash.dependencies import Input, Output, State 6 | import dash_colorscales as dcs 7 | import numpy as np 8 | import json 9 | from textwrap import dedent as d 10 | 11 | app = dash.Dash() 12 | 13 | DEFAULT_COLORSCALE = [[0, 'rgb(12,51,131)'], [0.25, 'rgb(10,136,186)'],\ 14 | [0.5, 'rgb(242,211,56)'], [0.75, 'rgb(242,143,56)'], \ 15 | [1, 'rgb(217,30,30)']] 16 | 17 | DEFAULT_COLORSCALE_NO_INDEX = [ea[1] for ea in DEFAULT_COLORSCALE] 18 | 19 | def read_mniobj(file): 20 | ''' function to read a MNI obj file ''' 21 | 22 | def triangulate_polygons(list_vertex_indices): 23 | ''' triangulate a list of n indices n=len(list_vertex_indices) ''' 24 | 25 | for k in range(0, len(list_vertex_indices), 3): 26 | yield list_vertex_indices[k: k+3] 27 | 28 | fp=open(file,'r') 29 | n_vert=[] 30 | n_poly=[] 31 | k=0 32 | list_indices=[] 33 | # Find number of vertices and number of polygons, stored in .obj file. 34 | # Then extract list of all vertices in polygons 35 | for i, line in enumerate(fp): 36 | if i==0: 37 | #Number of vertices 38 | n_vert=int(line.split()[6]) 39 | vertices=np.zeros([n_vert,3]) 40 | elif i<=n_vert: 41 | vertices[i-1]=list(map(float, line.split())) 42 | elif i>2*n_vert+5: 43 | if not line.strip(): 44 | k=1 45 | elif k==1: 46 | list_indices.extend(line.split()) 47 | #at this point list_indices is a list of strings, and each string is a vertex index, like this '23' 48 | #maps in Python 3.6 returns a generator, hence we convert it to a list 49 | list_indices=list(map(int, list_indices))#conver the list of string indices to int indices 50 | faces=np.array(list(triangulate_polygons(np.array(list_indices)))) 51 | return vertices, faces 52 | 53 | def standard_intensity(x,y,z): 54 | ''' color the mesh with a colorscale according to the values 55 | of the vertices z-coordinates ''' 56 | return z 57 | 58 | def plotly_triangular_mesh(vertices, faces, intensities=None, colorscale="Viridis", 59 | flatshading=False, showscale=False, reversescale=False, plot_edges=False): 60 | ''' vertices = a numpy array of shape (n_vertices, 3) 61 | faces = a numpy array of shape (n_faces, 3) 62 | intensities can be either a function of (x,y,z) or a list of values ''' 63 | 64 | x,y,z=vertices.T 65 | I,J,K=faces.T 66 | 67 | if intensities is None: 68 | intensities = standard_intensity(x,y,z) 69 | 70 | if hasattr(intensities, '__call__'): 71 | intensity=intensities(x,y,z)#the intensities are computed via a function, 72 | #that returns the list of vertices intensities 73 | elif isinstance(intensities, (list, np.ndarray)): 74 | intensity=intensities#intensities are given in a list 75 | else: 76 | raise ValueError("intensities can be either a function or a list, np.array") 77 | 78 | mesh=dict( 79 | type='mesh3d', 80 | x=x, y=y, z=z, 81 | colorscale=colorscale, 82 | intensity= intensities, 83 | flatshading=flatshading, 84 | i=I, j=J, k=K, 85 | name='', 86 | showscale=showscale 87 | ) 88 | 89 | mesh.update(lighting=dict( ambient= 0.18, 90 | diffuse= 1, 91 | fresnel= 0.1, 92 | specular= 1, 93 | roughness= 0.1, 94 | facenormalsepsilon=1e-6, 95 | vertexnormalsepsilon= 1e-12)) 96 | 97 | mesh.update(lightposition=dict(x=100, 98 | y=200, 99 | z= 0)) 100 | 101 | if showscale is True: 102 | mesh.update(colorbar=dict(thickness=20, ticklen=4, len=0.75)) 103 | 104 | if plot_edges is False: # the triangle sides are not plotted 105 | return [mesh] 106 | else:#plot edges 107 | #define the lists Xe, Ye, Ze, of x, y, resp z coordinates of edge end points for each triangle 108 | #None separates data corresponding to two consecutive triangles 109 | tri_vertices= vertices[faces] 110 | Xe=[] 111 | Ye=[] 112 | Ze=[] 113 | for T in tri_vertices: 114 | Xe+=[T[k%3][0] for k in range(4)]+[ None] 115 | Ye+=[T[k%3][1] for k in range(4)]+[ None] 116 | Ze+=[T[k%3][2] for k in range(4)]+[ None] 117 | #define the lines to be plotted 118 | lines=dict(type='scatter3d', 119 | x=Xe, 120 | y=Ye, 121 | z=Ze, 122 | mode='lines', 123 | name='', 124 | line=dict(color= 'rgb(70,70,70)', width=1) 125 | ) 126 | return [mesh, lines] 127 | 128 | pts, tri=read_mniobj("surf_reg_model_both.obj") 129 | intensities=np.loadtxt('aal_atlas.txt') 130 | data=plotly_triangular_mesh(pts, tri, intensities, 131 | colorscale=DEFAULT_COLORSCALE, flatshading=False, 132 | showscale=False, reversescale=False, plot_edges=False) 133 | data[0]['name'] = 'human_atlas' 134 | 135 | axis_template = dict( 136 | showbackground=True, 137 | backgroundcolor="rgb(10, 10,10)", 138 | gridcolor="rgb(255, 255, 255)", 139 | zerolinecolor="rgb(255, 255, 255)") 140 | 141 | plot_layout = dict( 142 | title = '', 143 | margin=dict(t=0,b=0,l=0,r=0), 144 | font=dict(size=12, color='white'), 145 | width=700, 146 | height=700, 147 | showlegend=False, 148 | plot_bgcolor='black', 149 | paper_bgcolor='black', 150 | scene=dict(xaxis=axis_template, 151 | yaxis=axis_template, 152 | zaxis=axis_template, 153 | aspectratio=dict(x=1, y=1.2, z=1), 154 | camera=dict(eye=dict(x=1.25, y=1.25, z=1.25)), 155 | annotations=[] 156 | ) 157 | ) 158 | 159 | styles = { 160 | 'pre': { 161 | 'border': 'thin lightgrey solid', 162 | 'padding': '10px', 163 | 'marginBottom': '20px' 164 | }, 165 | 'graph': { 166 | 'userSelect': 'none', 167 | 'margin': 'auto' 168 | } 169 | } 170 | 171 | ''' 172 | ~~~~~~~~~~~~~~~~ 173 | ~~ APP LAYOUT ~~ 174 | ~~~~~~~~~~~~~~~~ 175 | ''' 176 | 177 | app.layout = html.Div(children=[ 178 | html.P( 179 | children=[''' 180 | Click on the brain to add an annotation. \ 181 | Drag the black corners of the graph to rotate. ''', 182 | html.A( 183 | children='GitHub', 184 | target= '_blank', 185 | href='https://github.com/plotly/dash-brain-surface-viewer', 186 | style={'color': '#F012BE'} 187 | ), 188 | '.' 189 | ] 190 | ), 191 | html.Div([ 192 | html.P( 193 | children='Click colorscale to change:', 194 | style={'display':'inline-block', 'fontSize':'12px'} 195 | ), 196 | html.Div([ 197 | dcs.DashColorscales( 198 | id='colorscale-picker', 199 | colorscale=DEFAULT_COLORSCALE_NO_INDEX 200 | )], style={'marginTop':'-15px', 'marginLeft':'-30px'} 201 | ), 202 | html.Div([ 203 | dcc.RadioItems( 204 | options=[ 205 | {'label': 'Cortical Thickness', 'value': 'human'}, 206 | {'label': 'Mouse Brain', 'value': 'mouse'}, 207 | {'label': 'Brain Atlas', 'value': 'human_atlas'}, 208 | ], 209 | value='human_atlas', 210 | id='radio-options', 211 | labelStyle={'display': 'inline-block'} 212 | ) 213 | ]) 214 | ]), 215 | dcc.Graph( 216 | id='brain-graph', 217 | figure={ 218 | 'data': data, 219 | 'layout': plot_layout, 220 | }, 221 | config={'editable': True, 'scrollZoom': False}, 222 | style=styles['graph'] 223 | ), 224 | html.Div([ 225 | dcc.Markdown(d(""" 226 | **Click Data** 227 | 228 | Click on points in the graph. 229 | """)), 230 | html.Pre(id='click-data', style=styles['pre']), 231 | ]), 232 | html.Div([ 233 | dcc.Markdown(d(""" 234 | **Relayout Data** 235 | 236 | Drag the graph corners to rotate it. 237 | """)), 238 | html.Pre(id='relayout-data', style=styles['pre']), 239 | ]), 240 | html.P( 241 | children=[ 242 | 'Dash/Python code on ', 243 | html.A( 244 | children='GitHub', 245 | target= '_blank', 246 | href='https://github.com/plotly/dash-brain-surface-viewer', 247 | style={'color': '#F012BE'} 248 | ), 249 | '. Brain data from Mcgill\'s ACE Lab ', 250 | html.A( 251 | children='Surface Viewer', 252 | target= '_blank', 253 | href='https://brainbrowser.cbrain.mcgill.ca/surface-viewer#ct', 254 | style={'color': '#F012BE'} 255 | ), 256 | '.' 257 | ] 258 | ) 259 | ], style={'margin': '0 auto'}) 260 | 261 | app.css.append_css({'external_url': 'https://codepen.io/plotly/pen/YeqjLb.css'}) 262 | 263 | @app.callback( 264 | Output('brain-graph', 'figure'), 265 | [Input('brain-graph', 'clickData'), 266 | Input('radio-options', 'value'), 267 | Input('colorscale-picker', 'colorscale')], 268 | [State('brain-graph', 'figure')]) 269 | def add_marker(clickData, val, colorscale, figure): 270 | 271 | if figure['data'][0]['name'] != val: 272 | if val == 'human': 273 | pts, tri=read_mniobj("realct.obj") 274 | intensities=np.loadtxt('realct.txt') 275 | figure['data']=plotly_triangular_mesh(pts, tri, intensities, 276 | colorscale=DEFAULT_COLORSCALE, flatshading=False, 277 | showscale=False, reversescale=False, plot_edges=False) 278 | elif val == 'human_atlas': 279 | pts, tri=read_mniobj("surf_reg_model_both.obj") 280 | intensities=np.loadtxt('aal_atlas.txt') 281 | figure['data']=plotly_triangular_mesh(pts, tri, intensities, 282 | colorscale=DEFAULT_COLORSCALE, flatshading=False, 283 | showscale=False, reversescale=False, plot_edges=False) 284 | elif val == 'mouse': 285 | pts, tri=read_mniobj("mouse_surf.obj") 286 | intensities=np.loadtxt('mouse_map.txt') 287 | figure['data']=plotly_triangular_mesh(pts, tri, intensities, 288 | colorscale=DEFAULT_COLORSCALE, flatshading=False, 289 | showscale=False, reversescale=False, plot_edges=False) 290 | pts, tri=read_mniobj("mouse_brain_outline.obj") 291 | outer_mesh = plotly_triangular_mesh(pts, tri)[0] 292 | outer_mesh['opacity'] = 0.5 293 | outer_mesh['colorscale'] = 'Greys' 294 | figure['data'].append(outer_mesh) 295 | figure['data'][0]['name'] = val 296 | 297 | elif clickData != None: 298 | if 'points' in clickData: 299 | marker = dict( 300 | x = [clickData['points'][0]['x']], 301 | y = [clickData['points'][0]['y']], 302 | z = [clickData['points'][0]['z']], 303 | mode = 'markers', 304 | marker = dict(size=15, line=dict(width=3)), 305 | name = 'Marker', 306 | type = 'scatter3d', 307 | text = ['Click point to remove annotation'] 308 | ) 309 | anno = dict( 310 | x = clickData['points'][0]['x'], 311 | y = clickData['points'][0]['y'], 312 | z = clickData['points'][0]['z'], 313 | font = dict(color = 'black'), 314 | bgcolor = 'white', 315 | borderpad = 5, 316 | bordercolor = 'black', 317 | borderwidth = 1, 318 | captureevents = True, 319 | ay = -50, 320 | arrowcolor = 'white', 321 | arrowwidth = 2, 322 | arrowhead = 0, 323 | text = 'Click here to annotate
(Click point to remove)', 324 | ) 325 | if len(figure['data']) > 1: 326 | same_point_found = False 327 | for i, pt in enumerate(figure['data']): 328 | if pt['x'] == marker['x'] and pt['y'] == marker['y'] and pt['z'] == marker['z']: 329 | ANNO_TRACE_INDEX_OFFSET = 1 330 | if val == 'mouse': 331 | ANNO_TRACE_INDEX_OFFSET = 2 332 | figure['data'].pop(i) 333 | print('DEL. MARKER', i, figure['layout']['scene']['annotations']) 334 | if len(figure['layout']['scene']['annotations']) >= (i-ANNO_TRACE_INDEX_OFFSET): 335 | try: 336 | figure['layout']['scene']['annotations'].pop(i-ANNO_TRACE_INDEX_OFFSET) 337 | except: 338 | pass 339 | same_point_found = True 340 | break 341 | if same_point_found == False: 342 | figure['data'].append(marker) 343 | figure['layout']['scene']['annotations'].append(anno) 344 | else: 345 | figure['data'].append(marker) 346 | figure['layout']['scene']['annotations'].append(anno) 347 | 348 | cs = [] 349 | for i, rgb in enumerate(colorscale): 350 | cs.append([i/(len(colorscale)-1), rgb]) 351 | figure['data'][0]['colorscale'] = cs 352 | 353 | return figure 354 | 355 | @app.callback( 356 | Output('click-data', 'children'), 357 | [Input('brain-graph', 'clickData')]) 358 | def display_click_data(clickData): 359 | return json.dumps(clickData, indent=4) 360 | 361 | @app.callback( 362 | Output('relayout-data', 'children'), 363 | [Input('brain-graph', 'relayoutData')]) 364 | def display_click_data(relayoutData): 365 | return json.dumps(relayoutData, indent=4) 366 | 367 | if __name__ == '__main__': 368 | app.run_server(debug=True) 369 | -------------------------------------------------------------------------------- /gray_scale.txt: -------------------------------------------------------------------------------- 1 | 0.00 0.00 0.00 1.0 2 | 0.01 0.01 0.01 1.0 3 | 0.02 0.02 0.02 1.0 4 | 0.03 0.03 0.03 1.0 5 | 0.04 0.04 0.04 1.0 6 | 0.05 0.05 0.05 1.0 7 | 0.06 0.06 0.06 1.0 8 | 0.07 0.07 0.07 1.0 9 | 0.08 0.08 0.08 1.0 10 | 0.09 0.09 0.09 1.0 11 | 0.10 0.10 0.10 1.0 12 | 0.11 0.11 0.11 1.0 13 | 0.12 0.12 0.12 1.0 14 | 0.13 0.13 0.13 1.0 15 | 0.14 0.14 0.14 1.0 16 | 0.15 0.15 0.15 1.0 17 | 0.16 0.16 0.16 1.0 18 | 0.17 0.17 0.17 1.0 19 | 0.18 0.18 0.18 1.0 20 | 0.19 0.19 0.19 1.0 21 | 0.20 0.20 0.20 1.0 22 | 0.21 0.21 0.21 1.0 23 | 0.22 0.22 0.22 1.0 24 | 0.23 0.23 0.23 1.0 25 | 0.24 0.24 0.24 1.0 26 | 0.25 0.25 0.25 1.0 27 | 0.26 0.26 0.26 1.0 28 | 0.27 0.27 0.27 1.0 29 | 0.28 0.28 0.28 1.0 30 | 0.29 0.29 0.29 1.0 31 | 0.30 0.30 0.30 1.0 32 | 0.31 0.31 0.31 1.0 33 | 0.32 0.32 0.32 1.0 34 | 0.33 0.33 0.33 1.0 35 | 0.34 0.34 0.34 1.0 36 | 0.35 0.35 0.35 1.0 37 | 0.36 0.36 0.36 1.0 38 | 0.37 0.37 0.37 1.0 39 | 0.38 0.38 0.38 1.0 40 | 0.39 0.39 0.39 1.0 41 | 0.40 0.40 0.40 1.0 42 | 0.41 0.41 0.41 1.0 43 | 0.42 0.42 0.42 1.0 44 | 0.43 0.43 0.43 1.0 45 | 0.44 0.44 0.44 1.0 46 | 0.45 0.45 0.45 1.0 47 | 0.46 0.46 0.46 1.0 48 | 0.47 0.47 0.47 1.0 49 | 0.48 0.48 0.48 1.0 50 | 0.49 0.49 0.49 1.0 51 | 0.50 0.50 0.50 1.0 52 | 0.51 0.51 0.51 1.0 53 | 0.52 0.52 0.52 1.0 54 | 0.53 0.53 0.53 1.0 55 | 0.54 0.54 0.54 1.0 56 | 0.55 0.55 0.55 1.0 57 | 0.56 0.56 0.56 1.0 58 | 0.57 0.57 0.57 1.0 59 | 0.58 0.58 0.58 1.0 60 | 0.59 0.59 0.59 1.0 61 | 0.60 0.60 0.60 1.0 62 | 0.61 0.61 0.61 1.0 63 | 0.62 0.62 0.62 1.0 64 | 0.63 0.63 0.63 1.0 65 | 0.64 0.64 0.64 1.0 66 | 0.65 0.65 0.65 1.0 67 | 0.66 0.66 0.66 1.0 68 | 0.67 0.67 0.67 1.0 69 | 0.68 0.68 0.68 1.0 70 | 0.69 0.69 0.69 1.0 71 | 0.70 0.70 0.70 1.0 72 | 0.71 0.71 0.71 1.0 73 | 0.72 0.72 0.72 1.0 74 | 0.73 0.73 0.73 1.0 75 | 0.74 0.74 0.74 1.0 76 | 0.75 0.75 0.75 1.0 77 | 0.76 0.76 0.76 1.0 78 | 0.77 0.77 0.77 1.0 79 | 0.78 0.78 0.78 1.0 80 | 0.79 0.79 0.79 1.0 81 | 0.80 0.80 0.80 1.0 82 | 0.81 0.81 0.81 1.0 83 | 0.82 0.82 0.82 1.0 84 | 0.83 0.83 0.83 1.0 85 | 0.84 0.84 0.84 1.0 86 | 0.85 0.85 0.85 1.0 87 | 0.86 0.86 0.86 1.0 88 | 0.87 0.87 0.87 1.0 89 | 0.88 0.88 0.88 1.0 90 | 0.89 0.89 0.89 1.0 91 | 0.90 0.90 0.90 1.0 92 | 0.91 0.91 0.91 1.0 93 | 0.92 0.92 0.92 1.0 94 | 0.93 0.93 0.93 1.0 95 | 0.94 0.94 0.94 1.0 96 | 0.95 0.95 0.95 1.0 97 | 0.96 0.96 0.96 1.0 98 | 0.97 0.97 0.97 1.0 99 | 0.98 0.98 0.98 1.0 100 | 0.99 0.99 0.99 1.0 101 | 1.00 1.00 1.00 1.0 102 | 103 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2018.1.18 2 | chardet==3.0.4 3 | click==6.7 4 | dash==0.20.0 5 | dash-colorscales==0.0.4 6 | dash-core-components==0.18.1 7 | dash-html-components==0.8.0 8 | dash-renderer==0.11.3 9 | decorator==4.2.1 10 | enum34==1.1.6 11 | Flask==0.12.2 12 | Flask-Compress==1.4.0 13 | gunicorn==19.7.1 14 | idna==2.6 15 | ipython-genutils==0.2.0 16 | itsdangerous==0.24 17 | Jinja2==2.10 18 | jsonschema==2.6.0 19 | jupyter-core==4.4.0 20 | MarkupSafe==1.0 21 | nbformat==4.4.0 22 | numpy==1.14.0 23 | plotly==2.3.0 24 | pytz==2017.3 25 | requests==2.18.4 26 | six==1.11.0 27 | traitlets==4.3.2 28 | urllib3==1.22 29 | Werkzeug==0.14.1 30 | wheel==0.24.0 31 | -------------------------------------------------------------------------------- /requirements.txt~: -------------------------------------------------------------------------------- 1 | certifi==2018.1.18 2 | chardet==3.0.4 3 | click==6.7 4 | dash==0.20.0 5 | dash-colorscales==0.0.4 6 | dash-core-components==0.18.1 7 | dash-html-components==0.8.0 8 | dash-renderer==0.11.3 9 | decorator==4.2.1 10 | enum34==1.1.6 11 | Flask==0.12.2 12 | Flask-Compress==1.4.0 13 | functools32==3.2.3.post2 14 | gunicorn==19.7.1 15 | idna==2.6 16 | ipython-genutils==0.2.0 17 | itsdangerous==0.24 18 | Jinja2==2.10 19 | jsonschema==2.6.0 20 | jupyter-core==4.4.0 21 | MarkupSafe==1.0 22 | nbformat==4.4.0 23 | numpy==1.14.0 24 | plotly==2.3.0 25 | pytz==2017.3 26 | requests==2.18.4 27 | six==1.11.0 28 | traitlets==4.3.2 29 | urllib3==1.22 30 | Werkzeug==0.14.1 31 | wheel==0.24.0 32 | --------------------------------------------------------------------------------