Skip to content

Commit 613c0c0

Browse files
sambra95enryHclaude
authored
updates on create_visualisations page in microgrowth to make the sele… (#51)
* updates on create_visualisations page in microgrowth to make the selected plotting parameters persist between pages * update explanation of use of js for checking preselected boxes in aggrid when reloading the table * 🎨 format * Replace JS grid-restore hack with an explicit save-selection button Drop the JsCode-based re-check of preselected AgGrid rows on mount (flagged in review as odd to see JS for this). Instead, only persist the checked rows into session state when the user clicks a "Save selection" button, so navigating away and back no longer wipes the saved selection just because the grid remounts unchecked. * Replace AgGrid selection with a native data_editor checkbox column The previous "Save selection" button still read live state off an AgGrid component, which remounts unchecked on page navigation - so clicking Save after navigating back silently wrote an empty selection over the previously saved one. st.data_editor is a first-party Streamlit widget whose checked state is rebuilt from session state on every render, so it reflects the saved selection correctly right after navigating back, and Save now reliably captures what's shown. Also drops the now-unused streamlit-aggrid dependency. * update streamlit to 1.59 so sample selection table can st.dataframe with multiselect. remove aggrid dependency * add claude skills to git ignore --------- Co-authored-by: Henry Webel <heweb@dtu.dk> Co-authored-by: enryh <noreply@anthropic.com>
1 parent 0a73c1c commit 613c0c0

6 files changed

Lines changed: 92 additions & 180 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
22
.pytest_cache
33
*.egg-info
44
*.pyc
5+
6+
# Streamlit agent skills (environment-specific symlinks)
7+
.agents/skills/developing-with-streamlit
8+
.claude/skills/developing-with-streamlit

MicroGrowth/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,6 @@ The threshold is configurable and affects how conservatively phase transitions a
273273
- xlrd (for .xls file support)
274274
- growthcurves (core analysis package)
275275
- streamlit_sortables
276-
- streamlit-aggrid
277276

278277
See `requirements.txt` or `environment.yaml` for specific versions.
279278

MicroGrowth/environment.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@ dependencies:
88
- pandas=2.3.3
99
- scipy=1.16.3
1010
- plotly=6.3.1
11-
- streamlit=1.52.0
11+
- streamlit=1.59.2
1212
- openpyxl=3.1.5 # .xlsx support
1313
- xlrd=2.0.2 # .xls support (legacy)
1414
- pip:
1515
- kaleido==0.2.1
1616
- streamlit-plotly-events
1717
- streamlit-sortables==0.3.1
18-
- streamlit-aggrid==1.2.1
1918
- st_selectable_grid==1.0.0
2019
- pyod==2.0.7
2120
- growthcurves>=0.7.1

MicroGrowth/requirements.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ pandas==2.3.3
33
scipy==1.16.3
44
plotly==6.3.1
55
kaleido==0.2.1
6-
streamlit==1.52.0
6+
streamlit==1.59.2
77
openpyxl==3.1.5
88
xlrd==2.0.2
99
growthcurves>=0.7.1
1010
pyod==2.0.7
1111
streamlit_sortables==0.3.1
12-
streamlit-aggrid==1.2.1
1312
st_selectable_grid==1.0.0

MicroGrowth/src/ui_functions/create_visualizations_ui.py

Lines changed: 83 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,43 @@
44
import streamlit as st
55
from src.functions.visualization_functions import _unique_preserve_order
66
from src.styling import data_grid_style
7-
from st_aggrid import AgGrid, GridOptionsBuilder, GridUpdateMode
87
from streamlit_sortables import sort_items
98

109

10+
def _persistent_selectbox(container, label, options, store_key):
11+
"""Selectbox whose selection survives page navigation via a plain state key."""
12+
wkey = f"{store_key}_widget"
13+
stored = st.session_state.get(store_key)
14+
st.session_state.setdefault(wkey, stored if stored in options else options[0])
15+
value = container.selectbox(label, options, key=wkey)
16+
st.session_state[store_key] = value
17+
return value
18+
19+
20+
def _persistent_sortable(order_key, label, values, sig_extra=None):
21+
"""Drag-sortable list whose order survives navigation; empty values keep the order."""
22+
sig_key = f"{order_key}_sig"
23+
ver_key = f"{order_key}_ver"
24+
st.session_state.setdefault(order_key, [])
25+
st.session_state.setdefault(ver_key, 0)
26+
if values:
27+
order = [v for v in st.session_state[order_key] if v in values]
28+
order += [v for v in values if v not in order]
29+
st.session_state[order_key] = order
30+
31+
sig = (sig_extra, tuple(values))
32+
if st.session_state.get(sig_key) != sig:
33+
st.session_state[sig_key] = sig
34+
st.session_state[ver_key] += 1
35+
36+
st.markdown(f"**{label}**")
37+
st.session_state[order_key] = sort_items(
38+
st.session_state[order_key],
39+
key=f"{order_key}_sortable_{st.session_state[ver_key]}",
40+
)
41+
return [v for v in st.session_state[order_key] if v in values]
42+
43+
1144
def ui_growth_selection_container(plates: dict) -> dict:
1245
"""Render the sample selection container and return selection context."""
1346
# Build options once per rerun.
@@ -43,87 +76,44 @@ def ui_growth_selection_container(plates: dict) -> dict:
4376
# -----------------------------
4477
sel_key = "growth_combined_sel"
4578
sel = st.session_state.setdefault(sel_key, {})
46-
st.session_state[sel_key] = {sid: bool(sel.get(sid, False)) for sid in ids}
47-
sel = st.session_state[sel_key]
48-
grid_ver_key = "sample_selection_grid_ver"
49-
st.session_state.setdefault(grid_ver_key, 0)
50-
51-
def _selected_ids():
52-
return [sid for sid in ids if sel.get(sid, False)]
53-
54-
def _selected_opt_rows(sel_ids: list[str]) -> pd.DataFrame:
55-
if not sel_ids:
56-
return opt.iloc[0:0].copy()
57-
return opt[opt["_id"].isin(sel_ids)].copy()
5879

5980
# -----------------------------
6081
# UI: Step 1 selection (outside form so grid changes rerun)
6182
# -----------------------------
6283
with st.container(border=True):
6384
st.header("Step 1. Select Samples for Visualization")
6485

65-
# Apply styling for data grid (moved to styling.py)
6686
data_grid_style()
6787

68-
# Prepare dataframe for display with selection column
6988
if has_split:
7089
display_cols = ["Plate", "Sample Name", "Strain", "Condition", "Wells"]
7190
else:
7291
display_cols = ["Plate", "Sample Name", "Wells"]
7392

74-
display_df = opt[display_cols + ["_id"]].copy()
75-
76-
gb = GridOptionsBuilder.from_dataframe(display_df)
77-
gb.configure_selection(
78-
"multiple",
79-
use_checkbox=True,
80-
rowMultiSelectWithClick=True,
81-
)
82-
gb.configure_column("_id", hide=True)
83-
gb.configure_columns(display_cols, editable=False)
84-
if display_cols:
85-
gb.configure_column(
86-
display_cols[0],
87-
headerCheckboxSelection=True,
88-
checkboxSelection=True,
89-
)
90-
grid_options = gb.build()
91-
pre_selected_rows = [idx for idx, sid in enumerate(ids) if sel.get(sid, False)]
92-
grid_response = AgGrid(
93-
display_df,
94-
gridOptions=grid_options,
95-
update_mode=GridUpdateMode.SELECTION_CHANGED,
96-
pre_selected_rows=pre_selected_rows,
97-
fit_columns_on_grid_load=True,
93+
# selection_default only applies when the widget's keyed state is absent,
94+
# so it restores the saved ticks on returning from another page.
95+
event = st.dataframe(
96+
opt[display_cols],
97+
hide_index=True,
98+
width="stretch",
9899
height=400,
99-
width="100%",
100-
key=f"sample_selection_grid_{st.session_state[grid_ver_key]}",
101-
)
102-
selected_rows = grid_response.get("selected_rows")
103-
if selected_rows is None:
104-
selected_ids = set()
105-
elif isinstance(selected_rows, pd.DataFrame):
106-
selected_ids = set(
107-
selected_rows.get("_id", pd.Series([], dtype=str)).tolist()
108-
)
109-
elif isinstance(selected_rows, list):
110-
if selected_rows and isinstance(selected_rows[0], dict):
111-
selected_ids = {
112-
row.get("_id") for row in selected_rows if row.get("_id")
100+
on_select="rerun",
101+
selection_mode="multi-row",
102+
selection_default={
103+
"selection": {
104+
"rows": [i for i, sid in enumerate(ids) if sel.get(sid, False)]
113105
}
114-
else:
115-
selected_ids = {row for row in selected_rows if isinstance(row, str)}
116-
else:
117-
selected_ids = set()
118-
for sid in ids:
119-
sel[sid] = sid in selected_ids
120-
121-
sel_ids = _selected_ids()
122-
sel_opt = _selected_opt_rows(sel_ids)
123-
sel_sample_names = (
124-
_unique_preserve_order(sel_opt["Sample Name"].astype(str).tolist())
125-
if not sel_opt.empty
126-
else []
106+
},
107+
key="sample_selection_grid",
108+
)
109+
110+
chosen = set(event.selection.rows)
111+
st.session_state[sel_key] = {sid: i in chosen for i, sid in enumerate(ids)}
112+
113+
sel_ids = [sid for i, sid in enumerate(ids) if i in chosen]
114+
sel_opt = opt[opt["_id"].isin(sel_ids)].copy()
115+
sel_sample_names = _unique_preserve_order(
116+
sel_opt["Sample Name"].astype(str).tolist()
127117
)
128118

129119
return {
@@ -139,22 +129,6 @@ def _selected_opt_rows(sel_ids: list[str]) -> pd.DataFrame:
139129
@st.fragment
140130
def ui_growth_stats_controls_container(has_split: bool, sel_opt: pd.DataFrame) -> dict:
141131
"""Render growth stats controls and return form selections."""
142-
# -----------------------------
143-
# Order state (stats x-axis + legend)
144-
# -----------------------------
145-
x_order_key = "growth_stats_x_order"
146-
x_order_sig_key = "growth_stats_x_order_sig"
147-
x_order_ver_key = "growth_stats_x_order_ver"
148-
149-
leg_order_key = "growth_stats_legend_order"
150-
leg_order_sig_key = "growth_stats_legend_order_sig"
151-
leg_order_ver_key = "growth_stats_legend_order_ver"
152-
153-
st.session_state.setdefault(x_order_key, [])
154-
st.session_state.setdefault(x_order_ver_key, 0)
155-
st.session_state.setdefault(leg_order_key, [])
156-
st.session_state.setdefault(leg_order_ver_key, 0)
157-
158132
with st.container(border=True):
159133
st.header("Step 2. option a) Plot Growth Statistics")
160134

@@ -165,17 +139,11 @@ def ui_growth_stats_controls_container(has_split: bool, sel_opt: pd.DataFrame) -
165139
group_choices += ["Strain", "Condition"]
166140

167141
cA, cB = st.columns([1, 1])
168-
x_col = cA.selectbox(
169-
"X-axis column",
170-
options=x_choices,
171-
index=0,
172-
key="growth_stats_x_col",
142+
x_col = _persistent_selectbox(
143+
cA, "X-axis column", x_choices, "growth_stats_x_col"
173144
)
174-
legend_group = cB.selectbox(
175-
"Legend grouping",
176-
options=group_choices,
177-
index=0,
178-
key="growth_stats_legend_group",
145+
legend_group = _persistent_selectbox(
146+
cB, "Legend grouping", group_choices, "growth_stats_legend_group"
179147
)
180148
legend_col = None if legend_group == "None" else legend_group
181149

@@ -190,53 +158,15 @@ def ui_growth_stats_controls_container(has_split: bool, sel_opt: pd.DataFrame) -
190158
else []
191159
)
192160

193-
# drag ordering: x-axis
194-
cur_x_order = [v for v in st.session_state[x_order_key] if v in x_vals]
195-
for v in x_vals:
196-
if v not in cur_x_order:
197-
cur_x_order.append(v)
198-
st.session_state[x_order_key] = cur_x_order
199-
200-
x_sig = (x_col, tuple(x_vals))
201-
if st.session_state.get(x_order_sig_key) != x_sig:
202-
st.session_state[x_order_sig_key] = x_sig
203-
st.session_state[x_order_ver_key] += 1
204-
205-
if x_vals:
206-
st.markdown("**Drag to set x-axis order:**")
207-
st.session_state[x_order_key] = sort_items(
208-
st.session_state[x_order_key],
209-
key=f"growth_stats_x_sortable_{st.session_state[x_order_ver_key]}",
210-
)
211-
212-
# drag ordering: legend
213-
if legend_col:
214-
cur_leg_order = [
215-
v for v in st.session_state[leg_order_key] if v in legend_vals
216-
]
217-
for v in legend_vals:
218-
if v not in cur_leg_order:
219-
cur_leg_order.append(v)
220-
st.session_state[leg_order_key] = cur_leg_order
221-
222-
leg_sig = (legend_col, tuple(legend_vals))
223-
if st.session_state.get(leg_order_sig_key) != leg_sig:
224-
st.session_state[leg_order_sig_key] = leg_sig
225-
st.session_state[leg_order_ver_key] += 1
226-
227-
if legend_vals:
228-
st.markdown("**Drag to set legend order:**")
229-
st.session_state[leg_order_key] = sort_items(
230-
st.session_state[leg_order_key],
231-
key=f"growth_stats_leg_sortable_{st.session_state[leg_order_ver_key]}",
232-
)
233-
234-
x_ordered = [v for v in st.session_state[x_order_key] if v in x_vals]
235-
legend_ordered = (
236-
[v for v in st.session_state[leg_order_key] if v in legend_vals]
237-
if legend_col
238-
else []
239-
)
161+
x_ordered = _persistent_sortable(
162+
"growth_stats_x_order", "Drag to set x-axis order:", x_vals, x_col
163+
)
164+
legend_ordered = _persistent_sortable(
165+
"growth_stats_legend_order",
166+
"Drag to set legend order:",
167+
legend_vals,
168+
legend_col,
169+
)
240170

241171
return {
242172
"x_col": x_col,
@@ -251,51 +181,30 @@ def ui_growth_curves_controls_container(
251181
max_t: float, sel_sample_names: list[str]
252182
) -> dict:
253183
"""Render growth curves controls and return form selections."""
254-
# -----------------------------
255-
# Order state (curves sample order: mean+reps)
256-
# -----------------------------
257-
curves_order_key = "growth_curves_sample_order"
258-
curves_order_sig_key = "growth_curves_sample_order_sig"
259-
curves_order_ver_key = "growth_curves_sample_order_ver"
260-
st.session_state.setdefault(curves_order_key, [])
261-
st.session_state.setdefault(curves_order_ver_key, 0)
262-
263184
with st.container(border=True):
264185
st.header("Step 2. option b) Plot Growth Curves")
265186

187+
# Persist across navigation via a plain key; re-seed widget when absent.
188+
tw_key = "growth_curves_time_window"
189+
tw_wkey = f"{tw_key}_widget"
190+
lo0, hi0 = st.session_state.get(tw_key, (0.0, min(72.0, max_t)))
191+
lo0 = min(max(float(lo0), 0.0), max_t)
192+
hi0 = min(max(float(hi0), lo0), max_t)
193+
st.session_state.setdefault(tw_wkey, (lo0, hi0))
266194
curves_t0, curves_t1 = st.slider(
267195
"Mean/replicates plot time window (hours)",
268196
0.0,
269197
max_t,
270-
(0.0, min(72.0, max_t)),
271198
step=0.5,
272-
key="growth_curves_time_window",
199+
key=tw_wkey,
273200
)
201+
st.session_state[tw_key] = (curves_t0, curves_t1)
274202

275-
# drag ordering: sample names (specific to mean+reps)
276-
cur_curves_order = [
277-
v for v in st.session_state[curves_order_key] if v in sel_sample_names
278-
]
279-
for v in sel_sample_names:
280-
if v not in cur_curves_order:
281-
cur_curves_order.append(v)
282-
st.session_state[curves_order_key] = cur_curves_order
283-
284-
curves_sig = tuple(sel_sample_names)
285-
if st.session_state.get(curves_order_sig_key) != curves_sig:
286-
st.session_state[curves_order_sig_key] = curves_sig
287-
st.session_state[curves_order_ver_key] += 1
288-
289-
if sel_sample_names:
290-
st.markdown("**Drag to set Sample Name order (mean/replicates):**")
291-
st.session_state[curves_order_key] = sort_items(
292-
st.session_state[curves_order_key],
293-
key=f"growth_curves_sortable_{st.session_state[curves_order_ver_key]}",
294-
)
295-
296-
curves_ordered = [
297-
v for v in st.session_state[curves_order_key] if v in sel_sample_names
298-
]
203+
curves_ordered = _persistent_sortable(
204+
"growth_curves_sample_order",
205+
"Drag to set Sample Name order (mean/replicates):",
206+
sel_sample_names,
207+
)
299208

300209
return {
301210
"curves_t0": curves_t0,

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ dependencies = [
2323
"scipy",
2424
"matplotlib",
2525
"seaborn",
26-
"streamlit>=1.52.0",
26+
# 1.56.0 added st.dataframe(selection_default=...), which Step 1 sample
27+
# selection needs to restore ticked rows after page navigation.
28+
"streamlit>=1.56.0",
2729
"growthcurves",
2830
]
2931
# use requirements.txt instead of pyproject.toml for dependencies

0 commit comments

Comments
 (0)