44import streamlit as st
55from src .functions .visualization_functions import _unique_preserve_order
66from src .styling import data_grid_style
7- from st_aggrid import AgGrid , GridOptionsBuilder , GridUpdateMode
87from 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+
1144def 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
140130def 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 ,
0 commit comments