Skip to content

Commit 50a1663

Browse files
committed
included a video
1 parent 5177d6a commit 50a1663

2 files changed

Lines changed: 185 additions & 63 deletions

File tree

behavior_data_visualizer/main.py

Lines changed: 99 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,56 @@
99
from lecilab_behavior_analysis import utils as lbaut
1010
from behavior_data_visualizer import utils
1111
import 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
193236
if __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)

behavior_data_visualizer/utils.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from lecilab_behavior_analysis import df_transforms as dft
55
import plotly.express as px
66
import socket
7+
import pandas as pd
8+
from pathlib import Path
79

810
def set_mouse_data_dict(data_dict):
911
global mouse_data_dict
@@ -57,7 +59,19 @@ def update_performance_figure(clickData, mouse_name):
5759
# select the dataset
5860
sdf = df[df['year_month_day'] == date]
5961
sdf = dft.get_performance_through_trials(sdf, window=50)
60-
fig = px.line(sdf, x='total_trial', y='performance_w', color='current_training_stage')
62+
fig = px.line(
63+
sdf,
64+
x='total_trial',
65+
y='performance_w',
66+
color='current_training_stage',
67+
hover_data={
68+
'total_trial': True,
69+
'performance_w': True,
70+
'subject': False,
71+
'task': False,
72+
'date': True,
73+
'trial': False,
74+
})
6175
# put legend inside the plot
6276
fig.update_layout(legend=dict(
6377
orientation='h',
@@ -92,3 +106,74 @@ def get_data_path():
92106
"tectum": "/mnt/c/Users/HMARTINEZ/LeCiLab/data/behavioral_data/",
93107
}
94108
return paths.get(hostname, None)
109+
110+
111+
def display_video(clickData):
112+
try:
113+
total_trial = clickData['points'][0]['x']
114+
print(clickData)
115+
116+
except:
117+
return {}
118+
119+
return total_trial
120+
121+
122+
def get_video_path(project_name, mouse_name, task, date, trial):
123+
# get the path to the video
124+
data_path = get_data_path()
125+
if data_path is None:
126+
return None
127+
# format date to YYYYMMDD_HHMMSS
128+
date = date.replace('-', '')
129+
date = date.replace(':', '')
130+
date = date.replace(' ', '_')
131+
# video_path = f"{data_path}/{mouse_name}/videos/{mouse_name}_{total_trial}.mp4"
132+
# return video_path
133+
return f"{data_path}/{project_name}/videos/{mouse_name}/{mouse_name}_{task}_{date}.mp4"
134+
135+
136+
def get_mouse_data_dict(project_name):
137+
# Load the data
138+
outpath = get_data_path() + project_name + "/sessions/"
139+
# go through the tree and get the data
140+
mouse_data_dict = {}
141+
142+
# get the animals from the path
143+
for path in Path(outpath).iterdir():
144+
# check if the path is a directory
145+
if path.is_dir():
146+
# check if the path has a csv file
147+
if any(path.glob(f'{path.name}.csv')):
148+
# read that csv file if the animal is 008 # REMOVE ME
149+
if path.name == 'ACV008':
150+
data = pd.read_csv(path / f'{path.name}.csv', sep=';')
151+
# get the first 5000 rows
152+
data = data.head(5000)
153+
# add columns
154+
data = dft.add_day_column_to_df(data)
155+
# add it to the dictionary
156+
mouse_data_dict[path.name] = data
157+
# sort the dictionary
158+
mouse_data_dict = dict(sorted(mouse_data_dict.items()))
159+
# pass it to utils to make it global
160+
set_mouse_data_dict(mouse_data_dict)
161+
# return the data
162+
return mouse_data_dict
163+
164+
165+
def get_seconds_of_trial(subject, date, trial_number):
166+
try:
167+
df = mouse_data_dict[subject]
168+
session_df = df[df.date == date]
169+
except:
170+
return {}
171+
172+
# get the timestamp of the first trial
173+
start_of_first_trial = session_df.iloc[0].TRIAL_START
174+
# get the timestamp of the trial
175+
trial_start = session_df[session_df['trial'] == trial_number].TRIAL_START.values[0]
176+
# calculate the difference in seconds
177+
trial_start_seconds = (trial_start - start_of_first_trial)
178+
179+
return trial_start_seconds

0 commit comments

Comments
 (0)