labdata 0.0.24__tar.gz → 0.0.25__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {labdata-0.0.24 → labdata-0.0.25}/PKG-INFO +2 -2
- {labdata-0.0.24 → labdata-0.0.25}/labdata/__init__.py +1 -1
- {labdata-0.0.24 → labdata-0.0.25}/labdata/dashboard/compute.py +2 -2
- {labdata-0.0.24 → labdata-0.0.25}/labdata/dashboard/decisiontask.py +15 -7
- labdata-0.0.25/labdata/dashboard/index.py +225 -0
- labdata-0.0.25/labdata/dashboard/notes.py +184 -0
- labdata-0.0.25/labdata/dashboard/procedures.py +284 -0
- labdata-0.0.25/labdata/dashboard/segmentation.py +414 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/dashboard/sorting.py +1 -1
- {labdata-0.0.24 → labdata-0.0.25}/labdata/dashboard/video.py +1 -1
- {labdata-0.0.24 → labdata-0.0.25}/labdata/stacks.py +18 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata.egg-info/PKG-INFO +2 -2
- {labdata-0.0.24 → labdata-0.0.25}/labdata.egg-info/SOURCES.txt +2 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata.egg-info/requires.txt +1 -1
- {labdata-0.0.24 → labdata-0.0.25}/pyproject.toml +1 -1
- labdata-0.0.24/labdata/dashboard/index.py +0 -408
- labdata-0.0.24/labdata/dashboard/segmentation.py +0 -49
- {labdata-0.0.24 → labdata-0.0.25}/LICENSE +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/README.md +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/cli.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/compute/__init__.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/compute/caiman.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/compute/ec2.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/compute/ephys.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/compute/pose.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/compute/schedulers.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/compute/singularity.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/compute/suite2p.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/compute/utils.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/copy.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/dashboard/__init__.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/dashboard/explorer/__init__.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/dashboard/explorer/cellsegmentation.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/dashboard/explorer/spikesorting.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/export.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/rules/__init__.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/rules/ephys.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/rules/imaging.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/rules/utils.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/s3.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/schema/__init__.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/schema/ephys.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/schema/general.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/schema/histology.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/schema/onephoton.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/schema/procedures.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/schema/tasks.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/schema/twophoton.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/schema/utils.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/schema/video.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/utils.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata/widgets.py +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata.egg-info/dependency_links.txt +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata.egg-info/entry_points.txt +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/labdata.egg-info/top_level.txt +0 -0
- {labdata-0.0.24 → labdata-0.0.25}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: labdata
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.25
|
|
4
4
|
Summary: Package to manage data in experimental neuroscience labs
|
|
5
5
|
Author-email: Joao Couto <jpcouto@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/jcouto/labdata
|
|
@@ -23,7 +23,7 @@ Requires-Dist: dash; extra == "dashboard"
|
|
|
23
23
|
Requires-Dist: dash_bootstrap_components; extra == "dashboard"
|
|
24
24
|
Requires-Dist: dash-extensions; extra == "dashboard"
|
|
25
25
|
Requires-Dist: plotly; extra == "dashboard"
|
|
26
|
-
Requires-Dist: streamlit>1.
|
|
26
|
+
Requires-Dist: streamlit>1.54; extra == "dashboard"
|
|
27
27
|
Requires-Dist: streamlit-bokeh; extra == "dashboard"
|
|
28
28
|
Requires-Dist: scipy; extra == "dashboard"
|
|
29
29
|
Requires-Dist: scikit-image; extra == "dashboard"
|
|
@@ -44,7 +44,7 @@ def _jobs_panel(table, status_field, target_field, cols, delete_key):
|
|
|
44
44
|
x='Jobs:Q',
|
|
45
45
|
color=alt.Color('color:N').scale(None)
|
|
46
46
|
).configure_axis(labelLimit=3000))
|
|
47
|
-
st.altair_chart(ch,
|
|
47
|
+
st.altair_chart(ch, width="stretch")
|
|
48
48
|
|
|
49
49
|
# filtered table
|
|
50
50
|
selected_states = st.multiselect('Select status:', STATES, default=STATES[1:], key=f'{delete_key}_filter')
|
|
@@ -58,7 +58,7 @@ def _jobs_panel(table, status_field, target_field, cols, delete_key):
|
|
|
58
58
|
display_df,
|
|
59
59
|
column_config={'Select': st.column_config.CheckboxColumn(required=True)},
|
|
60
60
|
disabled=[c for c in display_df.columns if c != 'Select'],
|
|
61
|
-
|
|
61
|
+
width="stretch",
|
|
62
62
|
key=f'{delete_key}_editor',
|
|
63
63
|
)
|
|
64
64
|
selected_jobs = edited[edited['Select'] == True].index.tolist()
|
|
@@ -106,11 +106,15 @@ def decisionmaking_tab(schema = None):
|
|
|
106
106
|
lambda x: x.strftime("%y-%m-%d"))
|
|
107
107
|
return summarysource
|
|
108
108
|
|
|
109
|
+
if st.button('Refresh', icon=':material/refresh:'):
|
|
110
|
+
get_subjects_last_ran.clear()
|
|
111
|
+
get_summary_data.clear()
|
|
112
|
+
|
|
109
113
|
all_subjects,subjects,max_date = get_subjects_last_ran()
|
|
110
114
|
if not len(subjects):
|
|
111
115
|
st.write('There are no sessions.')
|
|
112
116
|
return
|
|
113
|
-
|
|
117
|
+
|
|
114
118
|
selected_indices = st.multiselect('Select subjects:',all_subjects,default = [s['subject_name'] for s in subjects])
|
|
115
119
|
if len(selected_indices):
|
|
116
120
|
# cache data to run faster
|
|
@@ -124,7 +128,7 @@ def decisionmaking_tab(schema = None):
|
|
|
124
128
|
scale=alt.Scale(nice={'interval': 'day', 'step': 1})).axis(labelAngle=45,format='%Y-%m-%d').title('Date'),
|
|
125
129
|
y=alt.Y('subject_name:O').title('Subject name'),
|
|
126
130
|
color=alt.Color('performance_easy:Q',
|
|
127
|
-
scale=alt.Scale(scheme='spectral', reverse=True,domain
|
|
131
|
+
scale=alt.Scale(scheme='spectral', reverse=True, domain=[0.5,1], clamp=True)).title('Perf easy'),
|
|
128
132
|
tooltip = ['subject_name','session_name','session_datetime','performance_easy','performance','n_trials']
|
|
129
133
|
|
|
130
134
|
).properties(width=1000, title ='Behavioral performance').add_params(point_selector)
|
|
@@ -160,6 +164,10 @@ def decisionmaking_tab(schema = None):
|
|
|
160
164
|
& dict(subject_name=subject_name, session_name=session_name)
|
|
161
165
|
).fetch(*cols, as_dict=True)
|
|
162
166
|
|
|
167
|
+
if st.button('Refresh sessions', icon=':material/refresh:'):
|
|
168
|
+
get_subject_data.clear()
|
|
169
|
+
get_session_trials.clear()
|
|
170
|
+
|
|
163
171
|
trialset, weights, watering = get_subject_data(subject_name)
|
|
164
172
|
sessions = sorted(trialset['session_datetime'].unique())
|
|
165
173
|
|
|
@@ -192,7 +200,7 @@ def decisionmaking_tab(schema = None):
|
|
|
192
200
|
|
|
193
201
|
with col_summary:
|
|
194
202
|
black, orange = '#000000','#ff7f0e'
|
|
195
|
-
perf_val = trialset.dropna(subset=['performance_easy','performance']).sort_values('session_datetime').drop_duplicates('session_datetime').iloc[[-
|
|
203
|
+
perf_val = trialset.dropna(subset=['performance_easy','performance']).sort_values('session_datetime').drop_duplicates('session_datetime').iloc[[-1]]
|
|
196
204
|
x_enc = alt.X('session_datetime:T', axis=axis).title('Session date')
|
|
197
205
|
perf_chart = (
|
|
198
206
|
alt.Chart(trialset).mark_line(color=black).encode(x=x_enc, y=alt.Y('performance_easy:Q', axis=axis).title('Performance')).properties(width=1000) +
|
|
@@ -224,7 +232,7 @@ def decisionmaking_tab(schema = None):
|
|
|
224
232
|
chartw + make_ruler('weighing_datetime'),
|
|
225
233
|
charth2o + make_ruler('watering_datetime'),
|
|
226
234
|
).resolve_scale(x='shared')
|
|
227
|
-
st.altair_chart(linked.interactive(),
|
|
235
|
+
st.altair_chart(linked.interactive(), width="stretch")
|
|
228
236
|
|
|
229
237
|
with col_psych:
|
|
230
238
|
|
|
@@ -251,7 +259,7 @@ def decisionmaking_tab(schema = None):
|
|
|
251
259
|
lines = base.mark_line().encode(y=alt.Y('proportion:Q'))
|
|
252
260
|
st.altair_chart((lines + errorbars + pts).properties(
|
|
253
261
|
width=400, height=500, title=f'{pd.Timestamp(selected_dt).strftime("%Y-%m-%d %H:%M")}'
|
|
254
|
-
).interactive(),
|
|
262
|
+
).interactive(), width="stretch")
|
|
255
263
|
|
|
256
264
|
# reaction time per condition
|
|
257
265
|
rt_rows = []
|
|
@@ -291,9 +299,9 @@ def decisionmaking_tab(schema = None):
|
|
|
291
299
|
rt_lines = rt_base.mark_line().encode(y=alt.Y('median_rt:Q'))
|
|
292
300
|
st.altair_chart((rt_lines + rt_err + rt_pts).properties(
|
|
293
301
|
width=400, height=300, title='Reaction time per condition'
|
|
294
|
-
).interactive(),
|
|
302
|
+
).interactive(), width="stretch")
|
|
295
303
|
else:
|
|
296
|
-
st.
|
|
304
|
+
st.error('No data available for this session.')
|
|
297
305
|
|
|
298
306
|
if len(snames) > 1:
|
|
299
307
|
st.slider('Session', min_value=0, max_value=len(snames)-1,
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import streamlit as st
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
from functools import partial
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
def intro_tab(user_name = None, schema = None):
|
|
9
|
+
@st.cache_data
|
|
10
|
+
def get_subjects():
|
|
11
|
+
if user_name is not None:
|
|
12
|
+
df = pd.DataFrame((schema.Subject() & f'user_name = "{user_name}"').fetch())
|
|
13
|
+
else:
|
|
14
|
+
df = pd.DataFrame(schema.Subject().fetch())
|
|
15
|
+
return df.sort_values("subject_name").reset_index(drop=True)
|
|
16
|
+
|
|
17
|
+
@st.cache_data
|
|
18
|
+
def get_sessions(subject_names):
|
|
19
|
+
dfs = [pd.DataFrame((schema.Session() * schema.Dataset() &
|
|
20
|
+
f'subject_name = "{s}"').fetch()) for s in subject_names]
|
|
21
|
+
dfs = [d for d in dfs if len(d)]
|
|
22
|
+
if not dfs:
|
|
23
|
+
return None
|
|
24
|
+
return pd.concat(dfs).sort_values("session_datetime").reset_index(drop=True)
|
|
25
|
+
|
|
26
|
+
@st.cache_data
|
|
27
|
+
def get_files(dataset_keys_frozen):
|
|
28
|
+
keys = [dict(k) for k in dataset_keys_frozen]
|
|
29
|
+
rows = (schema.Dataset.DataFiles * schema.File & keys).fetch(
|
|
30
|
+
'subject_name', 'session_name', 'dataset_name',
|
|
31
|
+
'file_path', 'file_size', 'file_datetime', 'storage', as_dict=True)
|
|
32
|
+
if not rows:
|
|
33
|
+
return pd.DataFrame()
|
|
34
|
+
df = pd.DataFrame(rows)
|
|
35
|
+
df['size'] = df['file_size'].apply(
|
|
36
|
+
lambda b: f'{b/1e9:.2f} GB' if b >= 1e9 else f'{b/1e6:.1f} MB' if b >= 1e6 else f'{b/1e3:.0f} KB')
|
|
37
|
+
return df.drop(columns='file_size')
|
|
38
|
+
|
|
39
|
+
@st.cache_data
|
|
40
|
+
def get_users():
|
|
41
|
+
return list(schema.LabMember.fetch('user_name'))
|
|
42
|
+
|
|
43
|
+
@st.cache_data
|
|
44
|
+
def get_strains():
|
|
45
|
+
return list(schema.Strain().fetch('strain_name'))
|
|
46
|
+
|
|
47
|
+
list_tab, add_tab, edit_tab = st.tabs(['Subjects/Sessions', 'New', 'Update'])
|
|
48
|
+
|
|
49
|
+
with list_tab:
|
|
50
|
+
subjects = get_subjects()
|
|
51
|
+
event_subjects = st.dataframe(
|
|
52
|
+
subjects,
|
|
53
|
+
hide_index=True,
|
|
54
|
+
width="stretch",
|
|
55
|
+
on_select='rerun',
|
|
56
|
+
selection_mode='multi-row',
|
|
57
|
+
key='subjects_table',
|
|
58
|
+
)
|
|
59
|
+
sel_subj_idx = (event_subjects.selection or {}).get('rows', [])
|
|
60
|
+
selected_names = list(subjects.iloc[sel_subj_idx]['subject_name']) if sel_subj_idx else []
|
|
61
|
+
if selected_names:
|
|
62
|
+
st.session_state['subjects_selected'] = selected_names
|
|
63
|
+
|
|
64
|
+
sessions = get_sessions(selected_names) if selected_names else None
|
|
65
|
+
if sessions is None:
|
|
66
|
+
st.write('No subjects selected.')
|
|
67
|
+
else:
|
|
68
|
+
dtype_vals = [s if s is not None else 'None' for s in sessions.dataset_type.values]
|
|
69
|
+
uniqueds = np.unique(dtype_vals)
|
|
70
|
+
metric_cols = st.columns(max(1, len(uniqueds)))
|
|
71
|
+
for col, d in zip(metric_cols, uniqueds):
|
|
72
|
+
col.metric(d, sum(1 for v in dtype_vals if v == d))
|
|
73
|
+
|
|
74
|
+
# color palette for dataset types (light backgrounds, readable text)
|
|
75
|
+
type_palette = ['#d0e8ff', '#ffd6d6', '#d4f5d4', '#fff0cc',
|
|
76
|
+
'#e8d4f5', '#ffd9b3', '#d4f0f5', '#f5d4e8',
|
|
77
|
+
'#e8f5d4', '#f5e8d4']
|
|
78
|
+
dtype_list = [s if s is not None else 'None' for s in sessions['dataset_type'].values]
|
|
79
|
+
unique_types = list(dict.fromkeys(dtype_list)) # preserve order, deduplicate
|
|
80
|
+
type_color = {t: type_palette[i % len(type_palette)] for i, t in enumerate(unique_types)}
|
|
81
|
+
|
|
82
|
+
def _color_rows(row):
|
|
83
|
+
dt = row.get('dataset_type')
|
|
84
|
+
dt = dt if dt is not None else 'None'
|
|
85
|
+
bg = type_color.get(dt, '')
|
|
86
|
+
return [f'background-color: {bg}'] * len(row)
|
|
87
|
+
|
|
88
|
+
sess_styled = sessions.style.apply(_color_rows, axis=1)
|
|
89
|
+
event_sessions = st.dataframe(
|
|
90
|
+
sess_styled,
|
|
91
|
+
hide_index=True,
|
|
92
|
+
width="stretch",
|
|
93
|
+
on_select='rerun',
|
|
94
|
+
selection_mode='multi-row',
|
|
95
|
+
key='sessions_table',
|
|
96
|
+
)
|
|
97
|
+
sel_indices = (event_sessions.selection or {}).get('rows', [])
|
|
98
|
+
sel_datasets = sessions.iloc[sel_indices] if sel_indices else pd.DataFrame()
|
|
99
|
+
if len(sel_datasets):
|
|
100
|
+
dataset_keys = sel_datasets[['subject_name', 'session_name', 'dataset_name']].to_dict('records')
|
|
101
|
+
file_stats = get_files(tuple(frozenset(d.items()) for d in dataset_keys))
|
|
102
|
+
if len(file_stats):
|
|
103
|
+
st.write('### Files')
|
|
104
|
+
ds_order = list(dict.fromkeys(file_stats['dataset_name'].values))
|
|
105
|
+
even_color, odd_color = '#dceeff', '#fff3dc'
|
|
106
|
+
ds_color = {ds: (even_color if i % 2 == 0 else odd_color)
|
|
107
|
+
for i, ds in enumerate(ds_order)}
|
|
108
|
+
|
|
109
|
+
def _color_file_rows(row):
|
|
110
|
+
bg = ds_color.get(row['dataset_name'], '')
|
|
111
|
+
return [f'background-color: {bg}'] * len(row)
|
|
112
|
+
|
|
113
|
+
file_styled = file_stats.style.apply(_color_file_rows, axis=1)
|
|
114
|
+
event_files = st.dataframe(
|
|
115
|
+
file_styled,
|
|
116
|
+
hide_index=True,
|
|
117
|
+
width="stretch",
|
|
118
|
+
on_select='rerun',
|
|
119
|
+
selection_mode='multi-row',
|
|
120
|
+
key='files_table',
|
|
121
|
+
)
|
|
122
|
+
sel_file_idx = (event_files.selection or {}).get('rows', [])
|
|
123
|
+
if sel_file_idx:
|
|
124
|
+
sel_files = file_stats.iloc[sel_file_idx]
|
|
125
|
+
st.caption(f'{len(sel_file_idx)} file(s) selected')
|
|
126
|
+
if st.button('Download files', key='download_files_btn'):
|
|
127
|
+
keys = sel_files[['file_path', 'storage']].to_dict('records')
|
|
128
|
+
with st.spinner(f'Downloading {len(sel_file_idx)} file(s)…'):
|
|
129
|
+
local = (schema.File & keys).get()
|
|
130
|
+
if local:
|
|
131
|
+
st.success(f'Downloaded {len(local)} file(s).')
|
|
132
|
+
for p in local:
|
|
133
|
+
st.code(str(p))
|
|
134
|
+
else:
|
|
135
|
+
st.write('No files found for the selected datasets.')
|
|
136
|
+
|
|
137
|
+
with add_tab:
|
|
138
|
+
users = get_users()
|
|
139
|
+
available_strains = get_strains()
|
|
140
|
+
project_users = list((schema.Project.User() & f'project_name = "{schema.schema_project}"').fetch('user_name'))
|
|
141
|
+
userindex = users.index(project_users[0]) if project_users and project_users[0] in users else None
|
|
142
|
+
with st.form('add_subject'):
|
|
143
|
+
insert_dict = dict()
|
|
144
|
+
insert_dict['user_name'] = st.selectbox('User Name', users, index=userindex)
|
|
145
|
+
insert_dict['subject_name'] = st.text_input('Subject ID', value=None)
|
|
146
|
+
insert_dict['subject_dob'] = st.date_input('Date of Birth')
|
|
147
|
+
sex = st.selectbox('Sex', ['M', 'F', 'Unknown'])
|
|
148
|
+
insert_dict['subject_sex'] = 'U' if sex == 'Unknown' else sex
|
|
149
|
+
insert_dict['strain_name'] = st.selectbox('Strain', available_strains)
|
|
150
|
+
if st.form_submit_button('Add Subject', type='primary'):
|
|
151
|
+
schema.Subject().insert1(insert_dict)
|
|
152
|
+
get_subjects.clear()
|
|
153
|
+
st.success(f'Added subject {insert_dict["subject_name"]}.')
|
|
154
|
+
st.rerun()
|
|
155
|
+
|
|
156
|
+
with edit_tab:
|
|
157
|
+
users = get_users()
|
|
158
|
+
available_strains = get_strains()
|
|
159
|
+
selected_names = st.session_state.get('subjects_selected', [])
|
|
160
|
+
if not selected_names:
|
|
161
|
+
st.info('Select a subject in the Subjects/Sessions tab first.')
|
|
162
|
+
else:
|
|
163
|
+
selected_subject = selected_names[0]
|
|
164
|
+
st.write(f'### Edit subject: {selected_subject}')
|
|
165
|
+
with st.form('edit_subject'):
|
|
166
|
+
edit_dict = (schema.Subject & dict(subject_name=selected_subject)).fetch1()
|
|
167
|
+
edit_dict['subject_dob'] = st.date_input('Date of Birth', value=edit_dict['subject_dob'])
|
|
168
|
+
edit_dict['subject_sex'] = st.selectbox('Sex', ['M', 'F', 'U'],
|
|
169
|
+
index=['M', 'F', 'U'].index(edit_dict['subject_sex']))
|
|
170
|
+
edit_dict['strain_name'] = st.selectbox('Strain', available_strains,
|
|
171
|
+
index=available_strains.index(edit_dict['strain_name']))
|
|
172
|
+
if st.form_submit_button('Save changes', type='primary'):
|
|
173
|
+
schema.Subject().update1(edit_dict)
|
|
174
|
+
get_subjects.clear()
|
|
175
|
+
st.success(f'Updated {selected_subject}.')
|
|
176
|
+
st.rerun()
|
|
177
|
+
st.set_page_config(
|
|
178
|
+
page_title="labdata dashboard",
|
|
179
|
+
page_icon="🧪",
|
|
180
|
+
layout="wide",
|
|
181
|
+
initial_sidebar_state="auto")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
from procedures import procedures_tab
|
|
185
|
+
from notes import notes_tab
|
|
186
|
+
from sorting import sorting_tab
|
|
187
|
+
from segmentation import segmentation_tab
|
|
188
|
+
from video import video_tab
|
|
189
|
+
from decisiontask import decisionmaking_tab
|
|
190
|
+
from compute import compute_tab
|
|
191
|
+
|
|
192
|
+
@st.cache_resource
|
|
193
|
+
def load_schema(project):
|
|
194
|
+
# load project
|
|
195
|
+
from labdata import load_project_schema
|
|
196
|
+
return load_project_schema(project)
|
|
197
|
+
|
|
198
|
+
project = None
|
|
199
|
+
for k in sys.argv:
|
|
200
|
+
if 'project=' in k:
|
|
201
|
+
project = k.replace('project=','')
|
|
202
|
+
with st.spinner(f'Connecting to {"project " + project if project else "database"}…'):
|
|
203
|
+
schema = load_schema(project)
|
|
204
|
+
|
|
205
|
+
page_names_to_funcs = {
|
|
206
|
+
"**Subjects**": partial(intro_tab, schema = schema),
|
|
207
|
+
"**Procedures**": partial(procedures_tab, schema = schema),
|
|
208
|
+
"**Notes**": partial(notes_tab, schema = schema),
|
|
209
|
+
"**Spike sorting**": partial(sorting_tab, schema = schema),
|
|
210
|
+
"**Cell Segmentation**": partial(segmentation_tab, schema = schema),
|
|
211
|
+
"**Video**": partial(video_tab, schema = schema),
|
|
212
|
+
"**Decision-making behavior**": partial(decisionmaking_tab, schema = schema),
|
|
213
|
+
"**Compute/Upload**": partial(compute_tab, schema = schema),
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
from labdata import plugins
|
|
217
|
+
for p in plugins.keys():
|
|
218
|
+
if hasattr(plugins[p],'dashboard_function'):
|
|
219
|
+
page_names_to_funcs[plugins[p].dashboard_name] = partial(plugins[p].dashboard_function, schema = schema)
|
|
220
|
+
|
|
221
|
+
tab_name = st.sidebar.radio(
|
|
222
|
+
"``labdata dashboard``",
|
|
223
|
+
page_names_to_funcs.keys())
|
|
224
|
+
|
|
225
|
+
page_names_to_funcs[tab_name]()
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import streamlit as st
|
|
3
|
+
from datetime import datetime, date, time as dtime
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def notes_tab(schema=None):
|
|
7
|
+
|
|
8
|
+
@st.cache_data
|
|
9
|
+
def get_users():
|
|
10
|
+
return list(schema.LabMember.fetch('user_name'))
|
|
11
|
+
|
|
12
|
+
@st.cache_data
|
|
13
|
+
def get_subjects():
|
|
14
|
+
return list(schema.Subject.fetch('subject_name'))
|
|
15
|
+
|
|
16
|
+
@st.cache_data
|
|
17
|
+
def get_sessions(subject_name):
|
|
18
|
+
return list((schema.Session & dict(subject_name=subject_name)).fetch('session_name'))
|
|
19
|
+
|
|
20
|
+
@st.cache_data
|
|
21
|
+
def get_datasets(subject_name, session_name):
|
|
22
|
+
return list((schema.Dataset & dict(subject_name=subject_name,
|
|
23
|
+
session_name=session_name)).fetch('dataset_name'))
|
|
24
|
+
|
|
25
|
+
@st.cache_data
|
|
26
|
+
def get_note_counts():
|
|
27
|
+
rows = (schema.Subject.aggr(
|
|
28
|
+
schema.Dataset * schema.Note,
|
|
29
|
+
note_count='count(*)'
|
|
30
|
+
)).fetch(as_dict=True)
|
|
31
|
+
df = pd.DataFrame(rows)
|
|
32
|
+
if len(df):
|
|
33
|
+
return df[df['note_count'] > 0].sort_values('note_count', ascending=False).reset_index(drop=True)
|
|
34
|
+
return []
|
|
35
|
+
|
|
36
|
+
@st.cache_data
|
|
37
|
+
def get_subject_notes(subject_name):
|
|
38
|
+
query = schema.Dataset * schema.Session * schema.Note & dict(subject_name=subject_name)
|
|
39
|
+
rows = query.fetch(as_dict=True)
|
|
40
|
+
if not rows:
|
|
41
|
+
return pd.DataFrame()
|
|
42
|
+
df = pd.DataFrame(rows)
|
|
43
|
+
cols = ['subject_name', 'session_name', 'dataset_name', 'session_datetime',
|
|
44
|
+
'notes', 'notetaker', 'note_datetime']
|
|
45
|
+
cols = [c for c in cols if c in df.columns]
|
|
46
|
+
return df[cols].sort_values('note_datetime', ascending=False).reset_index(drop=True)
|
|
47
|
+
|
|
48
|
+
users = get_users()
|
|
49
|
+
subjects = get_subjects()
|
|
50
|
+
|
|
51
|
+
browse_tab, update_tab, add_tab = st.tabs(['Browse', 'Update', 'Create'])
|
|
52
|
+
|
|
53
|
+
def _render_notes_table(notes_df, editor_key):
|
|
54
|
+
display_df = notes_df.copy()
|
|
55
|
+
display_df.insert(0, 'Select', False)
|
|
56
|
+
edited = st.data_editor(
|
|
57
|
+
display_df,
|
|
58
|
+
column_config={'Select': st.column_config.CheckboxColumn(required=True)},
|
|
59
|
+
disabled=[c for c in display_df.columns if c != 'Select'],
|
|
60
|
+
width="stretch",
|
|
61
|
+
key=editor_key,
|
|
62
|
+
)
|
|
63
|
+
selected_rows = edited[edited['Select'] == True]
|
|
64
|
+
if len(selected_rows) > 1:
|
|
65
|
+
st.warning('Multiple notes selected — selecting the first one.')
|
|
66
|
+
if len(selected_rows):
|
|
67
|
+
st.session_state['selected_note'] = selected_rows.iloc[0].to_dict()
|
|
68
|
+
|
|
69
|
+
with browse_tab:
|
|
70
|
+
import altair as alt
|
|
71
|
+
counts_df = get_note_counts()
|
|
72
|
+
subject_filter = None
|
|
73
|
+
if len(counts_df):
|
|
74
|
+
sel = alt.selection_point(fields=['subject_name'], name='subject_sel')
|
|
75
|
+
chart = (alt.Chart(counts_df).mark_bar().encode(
|
|
76
|
+
x=alt.X('subject_name:N', sort='ascending', title='Subject'),
|
|
77
|
+
y=alt.Y('note_count:Q', title='# notes'),
|
|
78
|
+
color=alt.condition(sel, alt.value('black'), alt.value('#aaaaaa')),
|
|
79
|
+
tooltip=['subject_name:N', 'note_count:Q'],
|
|
80
|
+
).add_params(sel).properties(height=200))
|
|
81
|
+
event = st.altair_chart(chart, width="stretch", on_select='rerun',
|
|
82
|
+
key='notes_count_chart')
|
|
83
|
+
if st.session_state.pop('notes_clear_pending', False):
|
|
84
|
+
st.session_state.pop('notes_selected_subject', None)
|
|
85
|
+
else:
|
|
86
|
+
pts = (event.selection or {}).get('subject_sel', [])
|
|
87
|
+
if pts and pts[0].get('subject_name'):
|
|
88
|
+
st.session_state['notes_selected_subject'] = pts[0]['subject_name']
|
|
89
|
+
subject_filter = st.session_state.get('notes_selected_subject')
|
|
90
|
+
if subject_filter:
|
|
91
|
+
st.write(f'**Selected:** {subject_filter}')
|
|
92
|
+
if st.button('Clear selection', key='notes_clear_subject'):
|
|
93
|
+
st.session_state['notes_clear_pending'] = True
|
|
94
|
+
st.rerun()
|
|
95
|
+
else:
|
|
96
|
+
st.write('No notes in the database.')
|
|
97
|
+
|
|
98
|
+
if subject_filter:
|
|
99
|
+
notes_df = get_subject_notes(subject_filter)
|
|
100
|
+
if len(notes_df):
|
|
101
|
+
if len(notes_df) > 1:
|
|
102
|
+
idx = st.slider('Note', min_value=1, max_value=len(notes_df), value=1,
|
|
103
|
+
format='%d', key='notes_slider',
|
|
104
|
+
help='Cycle through notes (sorted by date, newest first)')
|
|
105
|
+
row = notes_df.iloc[idx - 1]
|
|
106
|
+
st.write(f"**{row.get('session_datetime', '')}** — {row.get('dataset_name', '')} — *{row.get('notetaker', '')}*")
|
|
107
|
+
st.write(row.get('notes', ''))
|
|
108
|
+
st.session_state['selected_note'] = row.to_dict()
|
|
109
|
+
with st.expander('Show all notes as table'):
|
|
110
|
+
_render_notes_table(notes_df, 'notes_browse_editor')
|
|
111
|
+
else:
|
|
112
|
+
_render_notes_table(notes_df, 'notes_browse_editor')
|
|
113
|
+
else:
|
|
114
|
+
st.write('No notes found for this subject.')
|
|
115
|
+
|
|
116
|
+
with add_tab:
|
|
117
|
+
st.write('### Add a note to a dataset')
|
|
118
|
+
subject_name = st.selectbox('Subject', subjects, index=None, key='notes_add_subject')
|
|
119
|
+
session_name = None
|
|
120
|
+
dataset_name = None
|
|
121
|
+
if subject_name:
|
|
122
|
+
sessions = get_sessions(subject_name)
|
|
123
|
+
session_name = st.selectbox('Session', sessions, index=None, key='notes_add_session')
|
|
124
|
+
if session_name:
|
|
125
|
+
datasets = get_datasets(subject_name, session_name)
|
|
126
|
+
dataset_name = st.selectbox('Dataset', datasets, index=None, key='notes_add_dataset')
|
|
127
|
+
with st.form('add_note'):
|
|
128
|
+
project_users = (schema.Project.User() & f'project_name = "{schema.schema_project}"').fetch('user_name')
|
|
129
|
+
user_index = None
|
|
130
|
+
if len(project_users) and project_users[0] in users:
|
|
131
|
+
user_index = users.index(project_users[0])
|
|
132
|
+
notetaker = st.selectbox('Notetaker', users, index=user_index)
|
|
133
|
+
note_date = st.date_input('Date', value=date.today())
|
|
134
|
+
note_time = st.time_input('Time', value=dtime(datetime.now().hour, datetime.now().minute))
|
|
135
|
+
note_text = st.text_area('Note (max 4000 characters)', max_chars=4000)
|
|
136
|
+
submitted = st.form_submit_button('Add Note', type='primary')
|
|
137
|
+
if submitted:
|
|
138
|
+
if not subject_name:
|
|
139
|
+
st.error('Select a subject.')
|
|
140
|
+
elif not session_name:
|
|
141
|
+
st.error('Select a session.')
|
|
142
|
+
elif not dataset_name:
|
|
143
|
+
st.error('Select a dataset.')
|
|
144
|
+
elif not notetaker:
|
|
145
|
+
st.error('Select a notetaker.')
|
|
146
|
+
elif not note_text.strip():
|
|
147
|
+
st.error('Note text cannot be empty.')
|
|
148
|
+
else:
|
|
149
|
+
note_dt = datetime.combine(note_date, note_time)
|
|
150
|
+
dataset_key = dict(subject_name=subject_name, session_name=session_name,
|
|
151
|
+
dataset_name=dataset_name)
|
|
152
|
+
schema.Note.insert1(dict(notetaker=notetaker, note_datetime=note_dt, notes=note_text))
|
|
153
|
+
schema.Dataset.update1({**dataset_key, 'notetaker': notetaker, 'note_datetime': note_dt})
|
|
154
|
+
get_note_counts.clear()
|
|
155
|
+
get_subject_notes.clear()
|
|
156
|
+
st.success(f'Note {dataset_key} added successfully.')
|
|
157
|
+
|
|
158
|
+
with update_tab:
|
|
159
|
+
selected = st.session_state.get('selected_note', {})
|
|
160
|
+
if not selected:
|
|
161
|
+
st.info('Select a note in the Browse tab first.')
|
|
162
|
+
else:
|
|
163
|
+
notetaker_upd = selected.get('notetaker')
|
|
164
|
+
note_dt_upd = selected.get('note_datetime')
|
|
165
|
+
pre_text = selected.get('notes', '')
|
|
166
|
+
subject_name_upd = selected.get('subject_name')
|
|
167
|
+
session_name_upd = selected.get('session_name')
|
|
168
|
+
dataset_name_upd = selected.get('dataset_name')
|
|
169
|
+
if subject_name_upd and session_name_upd and dataset_name_upd:
|
|
170
|
+
st.write(f"**Subject:** {subject_name_upd} **Session:** {session_name_upd} **Dataset:** {dataset_name_upd}")
|
|
171
|
+
st.write(f"**Notetaker:** {notetaker_upd} **Date/time:** {note_dt_upd}")
|
|
172
|
+
with st.form('update_note'):
|
|
173
|
+
new_text = st.text_area('Note', value=pre_text, max_chars=4000)
|
|
174
|
+
submitted_upd = st.form_submit_button('Update Note', type='primary')
|
|
175
|
+
if submitted_upd:
|
|
176
|
+
if not new_text.strip():
|
|
177
|
+
st.error('Note text cannot be empty.')
|
|
178
|
+
else:
|
|
179
|
+
schema.Note.update1(dict(notetaker=notetaker_upd, note_datetime=note_dt_upd,
|
|
180
|
+
notes=new_text))
|
|
181
|
+
get_note_counts.clear()
|
|
182
|
+
get_subject_notes.clear()
|
|
183
|
+
st.session_state.pop('selected_note', None)
|
|
184
|
+
st.success(f'Note {dict(notetaker=notetaker_upd, note_datetime=note_dt_upd)} updated.')
|