99from lecilab_behavior_analysis import utils as lbaut
1010from behavior_data_visualizer import utils
1111import fire
12+ import os
13+ from flask import send_from_directory
14+ from dash .exceptions import PreventUpdate
1215
16+ # Serve static files (e.g., videos)
17+ STATIC_PATH = os .path .join (os .getcwd (), 'static' )
18+ if not os .path .exists (STATIC_PATH ):
19+ os .makedirs (STATIC_PATH )
1320
14- # Create the app
15- def app_builder (mouse_data_dict ):
16- # create an empty dictionary for the session data
21+ def app_builder (project_name ):
22+ mouse_data_dict = utils .get_mouse_data_dict (project_name )
1723 session_data_dict = {}
1824
1925 app = dash .Dash (__name__ )
2026
21- # Create the layout
27+ # Serve static files route
28+ @app .server .route ('/videos/<path:filename>' )
29+ def serve_video (filename ):
30+ return send_from_directory (STATIC_PATH , filename )
31+
32+ # Clientside callback to set video start time
33+ app .clientside_callback (
34+ """
35+ function(startData) {
36+ const video = document.getElementById("video-player");
37+ if (video && startData && startData.time !== undefined) {
38+ const setTime = () => {
39+ if (video.readyState >= 1) {
40+ video.currentTime = startData.time;
41+ video.play();
42+ } else {
43+ video.addEventListener('loadedmetadata', () => {
44+ video.currentTime = startData.time;
45+ video.play();
46+ });
47+ }
48+ };
49+ setTimeout(setTime, 500);
50+ }
51+ return window.dash_clientside.no_update;
52+ }
53+ """ ,
54+ dash .Output ("video-start-time" , "data" ),
55+ dash .Input ("video-start-time" , "data" )
56+ )
57+
58+ # Layout
2259 app .layout = dash .html .Div ([
60+ dash .dcc .Store (id = "video-start-time" ), # Declare globally in layout
61+
2362 dash .dcc .Tabs ([
2463 dash .dcc .Tab (label = 'Compare mice' , children = [
2564 dash .dcc .Checklist (
@@ -32,21 +71,23 @@ def app_builder(mouse_data_dict):
3271 ]),
3372
3473 dash .dcc .Tab (label = 'Single mouse reactive' , children = [
35- dash .dcc .Dropdown (
36- id = 'single-mouse-dropdown' ,
37- options = [{'label' : key , 'value' : key } for key in mouse_data_dict .keys ()],
38- value = None ,
39- multi = False ,
40- style = {'width' : '30%' }
41- ),
42- dash .dcc .Graph (id = 'reactive-calendar' , style = {'width' : '100%' }),
4374 dash .html .Div ([
44- dash .html .Pre (id = 'single-mouse-text' , style = {'flex' : '1' }),
45- dash .dcc .Graph (id = 'single-mouse-performance' , style = {'flex' : '1' , 'height' : '15%' }),
46- dash .dcc .Graph (id = "single-mouse-psychometric" , style = {'flex' : '1' , 'height' : '15%' }),
75+ dash .dcc .Dropdown (
76+ id = 'single-mouse-dropdown' ,
77+ options = [{'label' : key , 'value' : key } for key in mouse_data_dict .keys ()],
78+ value = None ,
79+ multi = False ,
80+ style = {'width' : '5%' }
81+ ),
82+ dash .dcc .Graph (id = 'reactive-calendar' , style = {'width' : '55%' }),
83+ dash .html .Pre (id = 'single-mouse-text' , style = {'flex' : '1' , 'width' : '40%' }),
84+ ], style = {'display' : 'flex' , 'flex-direction' : 'row' }),
85+ dash .html .Div ([
86+ dash .dcc .Graph (id = 'single-mouse-performance' , style = {'flex' : '1' , 'height' : '15%' , 'width' : '35%' }),
87+ dash .dcc .Graph (id = "single-mouse-psychometric" , style = {'flex' : '1' , 'height' : '15%' , 'width' : '20%' }),
88+ dash .html .Pre (id = 'single-mouse-video' , style = {'display' : 'flex' , 'flex-direction' : 'row' , 'flex' : '1' , 'width' : '45%' }),
4789 ], style = {'display' : 'flex' , 'flex-direction' : 'row' }),
4890 ]),
49-
5091 dash .dcc .Tab (label = 'Reports' , children = [
5192 dash .html .H3 ('Subject progress' ),
5293 dash .dcc .Dropdown (
@@ -70,15 +111,13 @@ def app_builder(mouse_data_dict):
70111 ])
71112 ])
72113
73- # Callback for the mouse comparison
74114 @app .callback (
75115 dash .dependencies .Output ('graph' , 'figure' ),
76116 [dash .dependencies .Input ('mice-checklist' , 'value' )],
77117 )
78118 def update_figure (selected_items ):
79119 if len (selected_items ) == 0 :
80120 return {}
81- # merge the datasets of the selected mice
82121 tdfs = []
83122 for key in selected_items :
84123 df = mouse_data_dict [key ]
@@ -88,7 +127,6 @@ def update_figure(selected_items):
88127 fig = px .line (tdf , x = 'total_trial' , y = 'performance_w' , color = 'mouse_name' )
89128 return fig
90129
91- # Callbacks for the single mouse reactive
92130 @app .callback (
93131 dash .dependencies .Output ('reactive-calendar' , 'figure' ),
94132 [dash .dependencies .Input ('single-mouse-dropdown' , 'value' )],
@@ -97,34 +135,67 @@ def update_calendar(mouse_name):
97135 if mouse_name is None :
98136 return {}
99137 df = mouse_data_dict [mouse_name ]
100-
101138 dates_df = df .groupby (["year_month_day" ]).count ().reset_index ()
102-
103139 fig = calplot (
104140 dates_df ,
105141 x = 'year_month_day' ,
106142 y = 'trial' ,
107- # text="year_month_day",
108143 )
109144 return fig
110145
111- # Update the figures with the click data
112146 @app .callback (
113147 dash .dependencies .Output ('single-mouse-text' , 'children' ),
114148 dash .dependencies .Output ('single-mouse-performance' , 'figure' ),
115149 dash .dependencies .Output ('single-mouse-psychometric' , 'figure' ),
116150 [dash .dependencies .Input ('reactive-calendar' , 'clickData' ),
117- dash .dependencies .Input ('single-mouse-dropdown' , 'value' )],
151+ dash .dependencies .Input ('single-mouse-dropdown' , 'value' )],
118152 )
119153 def update_single_mouse_reactive (clickData , mouse_name ):
120154 text = utils .display_click_data (clickData , mouse_name )
121155 perf_fig = utils .update_performance_figure (clickData , mouse_name )
122156 psych_fig = utils .update_psychometric_figure (clickData , mouse_name )
123157 return text , perf_fig , psych_fig
124158
159+ @app .callback (
160+ dash .dependencies .Output ('single-mouse-video' , 'children' ),
161+ dash .dependencies .Output ('video-start-time' , 'data' , allow_duplicate = True ),
162+ [dash .dependencies .Input ('single-mouse-performance' , 'clickData' )],
163+ prevent_initial_call = True
164+ )
165+ def update_single_mouse_video (clickData ):
166+ if clickData is None :
167+ raise PreventUpdate
168+
169+ try :
170+ subject , task , date , trial = clickData ['points' ][0 ]['customdata' ]
171+ video_path = utils .get_video_path (project_name , subject , task , date , trial )
172+ except (KeyError , TypeError ):
173+ return dash .html .Div ("Invalid click data" ), dash .no_update
174+
175+ if not os .path .exists (video_path ):
176+ return dash .html .Div (f"Video file not found: { video_path } " ), dash .no_update
177+
178+ video_filename = os .path .basename (video_path )
179+ static_video_path = os .path .join (STATIC_PATH , video_filename )
180+ if not os .path .exists (static_video_path ):
181+ try :
182+ os .symlink (video_path , static_video_path )
183+ except OSError as e :
184+ return dash .html .Div (f"Error linking video: { e } " ), dash .no_update
185+
186+ # convert trial to seconds
187+ start_time = utils .get_seconds_of_trial (subject , date , trial )
188+ video_component = dash .html .Video (
189+ id = "video-player" ,
190+ src = f"/videos/{ video_filename } " ,
191+ controls = True ,
192+ autoPlay = True ,
193+ muted = True ,
194+ style = {"width" : "100%" },
195+ )
196+
197+ return video_component , {"time" : start_time }
125198
126- # Callback for the reports
127- # Figure for the subject progress
128199 @app .callback (
129200 dash .dependencies .Output ('subject-progress' , component_property = 'src' ),
130201 [dash .dependencies .Input ('reports-mice-dropdown' , 'value' )],
@@ -136,7 +207,6 @@ def update_subject_progress(selected_value):
136207 fig = fm .subject_progress_figure (df )
137208 return utils .fig_to_uri (fig )
138209
139- # Dropdown for the sessions
140210 @app .callback (
141211 dash .dependencies .Output ('reports-session-dropdown' , 'options' ),
142212 [dash .dependencies .Input ('reports-mice-dropdown' , 'value' )],
@@ -148,7 +218,6 @@ def update_session_dropdown(selected_value):
148218 session_data_dict = utils .get_diccionary_of_dates (df )
149219 return [{'label' : key , 'value' : session_data_dict [key ]} for key in session_data_dict .keys ()]
150220
151- # Figure for the session summary
152221 @app .callback (
153222 dash .dependencies .Output ('session-summary' , component_property = 'src' ),
154223 [dash .dependencies .Input ('reports-mice-dropdown' , 'value' )],
@@ -164,38 +233,6 @@ def update_session_summary(mouse, session):
164233
165234 return app
166235
167- def get_mouse_data_dict (project_name ):
168- # Load the data
169- outpath = utils .get_data_path () + project_name + "/sessions/"
170- # go through the tree and get the data
171- mouse_data_dict = {}
172-
173- # get the animals from the path
174- for path in Path (outpath ).iterdir ():
175- # check if the path is a directory
176- if path .is_dir ():
177- # check if the path has a csv file
178- if any (path .glob (f'{ path .name } .csv' )):
179- # read that csv file
180- data = pd .read_csv (path / f'{ path .name } .csv' , sep = ';' )
181- # add columns
182- data = dft .add_day_column_to_df (data )
183- # add it to the dictionary
184- mouse_data_dict [path .name ] = data
185- # sort the dictionary
186- mouse_data_dict = dict (sorted (mouse_data_dict .items ()))
187- # pass it to utils to make it global
188- utils .set_mouse_data_dict (mouse_data_dict )
189- # return the data
190- return mouse_data_dict
191-
192- # Run the app
193236if __name__ == '__main__' :
194- # Get the data
195- # Fire(get_mouse_data_dict)
196- mouse_data_dict = get_mouse_data_dict ("visual_and_COT_data" )
197- # Build the app
198- app = app_builder (mouse_data_dict )
199- # Run the app
200- app .run (debug = True )
201-
237+ app = app_builder ('visual_and_COT_data' )
238+ app .run (debug = True , port = 8051 )
0 commit comments