├── .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 | 
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 |
--------------------------------------------------------------------------------