arka2696 commited on
Commit
9ce1198
·
verified ·
1 Parent(s): 9a78097

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +13 -489
app.py CHANGED
@@ -1,489 +1,13 @@
1
- # plate_map_app.py
2
- # Advanced Plate Map Designer — fixed-width concentration bars + per-well colors + compact legends
3
- import io
4
- from typing import List, Optional, Dict
5
-
6
- import numpy as np
7
- import pandas as pd
8
- import streamlit as st
9
- import matplotlib.pyplot as plt
10
- from matplotlib.patches import Circle, Rectangle
11
-
12
- ROW_LETTERS = "ABCDEFGHIJKLMNOP"
13
-
14
- PLATE_SPECS = {
15
- "96-well (8x12)": {"rows": 8, "cols": 12},
16
- "24-well (4x6)": {"rows": 4, "cols": 6},
17
- }
18
-
19
- DEFAULT_COLORMAPS = {"concentration": "magma", "seeding_density": "viridis"}
20
-
21
- # ----------------------------- Helpers -----------------------------
22
-
23
- def well_id(r: int, c: int) -> str:
24
- return f"{ROW_LETTERS[r-1]}{c}"
25
-
26
- def parse_well_token(token: str) -> List[str]:
27
- token = token.strip().upper()
28
- if token in ("ALL", "*"):
29
- return ["ALL"]
30
- token = token.replace("-", ":")
31
- if ":" in token:
32
- s, e = token.split(":")
33
- srow, scol = s[0], int(s[1:])
34
- erow, ecol = e[0], int(e[1:])
35
- r0, r1 = ROW_LETTERS.index(srow)+1, ROW_LETTERS.index(erow)+1
36
- out = []
37
- for r in range(min(r0, r1), max(r0, r1)+1):
38
- for c in range(min(scol, ecol), max(scol, ecol)+1):
39
- out.append(f"{ROW_LETTERS[r-1]}{c}")
40
- return out
41
- return [token]
42
-
43
- def expand_well_ranges(rng: str) -> List[str]:
44
- if not rng: return []
45
- out, seen = [], set()
46
- for t in [t for t in rng.replace(";", ",").split(",") if t.strip()]:
47
- for w in parse_well_token(t):
48
- if w not in seen:
49
- out.append(w); seen.add(w)
50
- return out
51
-
52
- def empty_plate_df(rows: int, cols: int) -> pd.DataFrame:
53
- rows_list = []
54
- for r in range(1, rows+1):
55
- for c in range(1, cols+1):
56
- rows_list.append({
57
- "well": well_id(r, c), "row": ROW_LETTERS[r-1], "col": c,
58
- "cell_line": "", "cell_color": "", "seeding_density": np.nan,
59
- "compound": "", "compound_color": "",
60
- "concentration": np.nan, "conc_units": "",
61
- "conc_bar_color": "", # per-well concentration bar color
62
- "vehicle": "", "control_type": "",
63
- "timepoint": "", "replicate": "", "group_id": "", "notes": ""
64
- })
65
- return pd.DataFrame(rows_list)
66
-
67
- def serial_values(start: float, factor: float, n: int, direction="decreasing"):
68
- vals = [start]
69
- for _ in range(n-1):
70
- vals.append(vals[-1]/factor if direction=="decreasing" else vals[-1]*factor)
71
- return vals
72
-
73
- def load_uploaded_csv(uploaded) -> Optional[pd.DataFrame]:
74
- try: return pd.read_csv(uploaded)
75
- except Exception:
76
- uploaded.seek(0)
77
- try: return pd.read_excel(uploaded)
78
- except Exception: return None
79
-
80
- def build_compound_color_map(df: pd.DataFrame) -> Dict[str, str]:
81
- comps = [s for s in (str(x).strip() for x in df["compound"].dropna()) if s]
82
- comps = list(dict.fromkeys(comps))
83
- palette = plt.get_cmap("tab20").colors
84
- mapping = {}
85
- for _, r in df.iterrows():
86
- comp = str(r["compound"]).strip()
87
- c = str(r.get("compound_color", "")).strip()
88
- if comp and c: mapping[comp] = c
89
- import matplotlib as mpl
90
- k = 0
91
- for comp in comps:
92
- if comp not in mapping:
93
- mapping[comp] = mpl.colors.to_hex(palette[k % len(palette)]); k += 1
94
- return mapping
95
-
96
- def build_celltype_color_map(df: pd.DataFrame) -> Dict[str, str]:
97
- lines = [s for s in (str(x).strip() for x in df["cell_line"].dropna()) if s]
98
- lines = list(dict.fromkeys(lines))
99
- palette = plt.get_cmap("tab10").colors
100
- mapping = {}
101
- for _, r in df.iterrows():
102
- line = str(r["cell_line"]).strip()
103
- c = str(r.get("cell_color", "")).strip()
104
- if line and c and line not in mapping: mapping[line] = c
105
- import matplotlib as mpl
106
- k = 0
107
- for line in lines:
108
- if line not in mapping:
109
- mapping[line] = mpl.colors.to_hex(palette[k % len(palette)]); k += 1
110
- return mapping
111
-
112
- # ----------------------------- Drawing -----------------------------
113
-
114
- def draw_plate_figure(
115
- df: pd.DataFrame,
116
- rows: int,
117
- cols: int,
118
- color_by: str,
119
- second_label: str = "concentration",
120
- show_legend: bool = True,
121
- edge_highlight: bool = True,
122
- figsize_scale: float = 1.0,
123
- dpi: int = 300,
124
- text_fontsize: int = 8,
125
- conc_mode: str = "text", # "text" (above wells) | "bar" | "none"
126
- conc_bar_color_global: str = "#4d4d4d", # fallback for per-well bar color
127
- ):
128
- cmap_name = DEFAULT_COLORMAPS.get(color_by, "viridis")
129
- numeric_fields = {"concentration", "seeding_density"}
130
-
131
- fig_w = max(6.0, cols * 0.55) * figsize_scale
132
- fig_h = max(5.5, rows * 0.7) * figsize_scale
133
- fig, ax = plt.subplots(figsize=(fig_w, fig_h), dpi=dpi)
134
- ax.set_aspect("equal"); ax.axis("off")
135
-
136
- panel = Rectangle((0, 0), cols + 2, rows + 2, linewidth=0.6, edgecolor="#444", facecolor="#FAFAFA")
137
- ax.add_patch(panel)
138
-
139
- x0, y0 = 1, 1
140
- radius = 0.35
141
-
142
- compound_map = build_compound_color_map(df)
143
- celltype_map = build_celltype_color_map(df)
144
-
145
- face_colors, scalar_norm, categorical_palette = {}, None, {}
146
-
147
- series = df[color_by] if color_by in df.columns else pd.Series([""] * len(df))
148
- if color_by in numeric_fields:
149
- vals = pd.to_numeric(series, errors="coerce")
150
- finite = vals.replace([np.inf, -np.inf], np.nan).dropna()
151
- vmin = finite.min() if len(finite) else 0.0
152
- vmax = finite.max() if len(finite) else 1.0
153
- vmin, vmax = (vmin, vmax) if np.isfinite([vmin, vmax]).all() and vmin != vmax else (0.0, 1.0)
154
- import matplotlib as mpl
155
- cmap = plt.get_cmap(cmap_name)
156
- scalar_norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
157
- for i, w in df["well"].items():
158
- val = pd.to_numeric(df.loc[i, color_by], errors="coerce")
159
- face_colors[w] = "#FFFFFF" if pd.isna(val) else mpl.colors.to_hex(cmap(scalar_norm(val)))
160
- else:
161
- if color_by == "compound":
162
- for _, r in df.iterrows():
163
- comp = str(r["compound"]).strip(); w = r["well"]
164
- face_colors[w] = compound_map.get(comp, "#FFFFFF") if comp else "#FFFFFF"
165
- categorical_palette = compound_map.copy()
166
- else:
167
- categories = sorted([c for c in series.astype(str).unique() if c])
168
- if categories:
169
- palette = plt.get_cmap("tab20").colors
170
- for i, cat in enumerate(categories):
171
- import matplotlib as mpl
172
- categorical_palette[cat] = mpl.colors.to_hex(palette[i % len(palette)])
173
- for i, w in df["well"].items():
174
- key = str(df.loc[i, color_by]) if color_by in df.columns else ""
175
- face_colors[w] = categorical_palette.get(key, "#FFFFFF") if key else "#FFFFFF"
176
-
177
- # Units for concentration labels
178
- units = df["conc_units"].dropna()
179
- conc_unit = str(units.iloc[0]) if len(units) else ""
180
-
181
- # ---- Draw wells
182
- for _, row in df.iterrows():
183
- r_idx = ROW_LETTERS.index(row["row"]) + 1
184
- c_idx = int(row["col"])
185
- cx = x0 + c_idx - 0.5
186
- cy = y0 + (rows - r_idx + 1) - 0.5
187
- fill = face_colors.get(row["well"], "#FFFFFF")
188
-
189
- edge_color = "#333333"; lw = 1.0
190
- if isinstance(row.get("control_type", ""), str) and row["control_type"]:
191
- edge_color = "#000000"; lw = 2.0
192
-
193
- if edge_highlight and (r_idx in (1, rows) or c_idx in (1, cols)):
194
- ax.add_patch(Circle((cx, cy), radius*1.15, linewidth=0, facecolor="#EFEFEF", alpha=0.6, zorder=0))
195
-
196
- ax.add_patch(Circle((cx, cy), radius=radius, linewidth=lw, edgecolor=edge_color, facecolor=fill, zorder=3))
197
-
198
- # cell type inner dot
199
- cell_col = str(row.get("cell_color", "")).strip()
200
- if cell_col:
201
- ax.add_patch(Circle((cx, cy), radius=radius*0.28, linewidth=0.6, edgecolor="#333333",
202
- facecolor=cell_col, zorder=4))
203
-
204
- # concentration: text (above) OR fixed-width bar (below; no text)
205
- if conc_mode == "text":
206
- val = row.get("concentration", np.nan)
207
- if pd.notna(val):
208
- try: txt = f"{float(val):.3g}{conc_unit if conc_unit else ''}"
209
- except Exception: txt = str(val)
210
- ax.text(cx, cy + radius + 0.14, txt, ha="center", va="bottom",
211
- fontsize=text_fontsize, zorder=6)
212
- elif conc_mode == "bar":
213
- val = pd.to_numeric(row.get("concentration", np.nan), errors="coerce")
214
- if not pd.isna(val):
215
- # fixed width bar (same for all wells)
216
- bar_w = 0.9
217
- bar_h = 0.09
218
- x_left = cx - bar_w/2
219
- y_bottom = cy - radius - 0.18
220
- bw_color = str(row.get("conc_bar_color", "")).strip() or conc_bar_color_global
221
- ax.add_patch(Rectangle((x_left, y_bottom), bar_w, bar_h, facecolor=bw_color,
222
- edgecolor="#333333", linewidth=0.6, zorder=6))
223
-
224
- # other overlay in center if not using concentration text
225
- if conc_mode != "text" and second_label and second_label not in ("", "concentration"):
226
- val = row.get(second_label, "")
227
- if pd.notna(val) and str(val):
228
- ax.text(cx, cy, str(val), ha="center", va="center", fontsize=text_fontsize, zorder=5)
229
-
230
- # row/col labels
231
- for c in range(1, cols+1):
232
- ax.text(x0 + c - 0.5, y0 + rows + 0.22, str(c), ha="center", va="bottom", fontsize=10, color="#333333")
233
- for r in range(1, rows+1):
234
- ax.text(x0 - 0.2, y0 + (rows - r + 1) - 0.5, ROW_LETTERS[r-1], ha="right", va="center", fontsize=10, color="#333333")
235
-
236
- # ---- Legends (right)
237
- if show_legend:
238
- # Cell type
239
- if celltype_map:
240
- ax_c = fig.add_axes([0.86, 0.72, 0.12, 0.22]); ax_c.axis("off")
241
- ax_c.text(0.0, 1.05, "Cell type", fontsize=11, fontweight="bold", transform=ax_c.transAxes)
242
- y = 0.92; dy = 0.10
243
- for line, col in list(celltype_map.items())[:25]:
244
- ax_c.add_patch(Circle((0.08, y-0.03), radius=0.03, transform=ax_c.transAxes,
245
- facecolor=col, edgecolor="#333", linewidth=0.6, clip_on=False))
246
- ax_c.text(0.18, y-0.03, line, fontsize=9, va="center", transform=ax_c.transAxes)
247
- y -= dy
248
-
249
- # Treatment
250
- if compound_map:
251
- ax_t = fig.add_axes([0.86, 0.44, 0.12, 0.22]); ax_t.axis("off")
252
- ax_t.text(0.0, 1.05, "Treatment", fontsize=11, fontweight="bold", transform=ax_t.transAxes)
253
- y = 0.92; dy = 0.10
254
- for comp, col in list(compound_map.items())[:20]:
255
- ax_t.add_patch(Rectangle((0.0, y-0.05), 0.25, 0.04, transform=ax_t.transAxes,
256
- facecolor=col, edgecolor="#333", linewidth=0.6, clip_on=False))
257
- ax_t.text(0.30, y-0.035, comp, fontsize=9, va="center", transform=ax_t.transAxes)
258
- y -= dy
259
-
260
- # Concentration legend — equal-length bars, distinct colors, label to the right
261
- if conc_mode == "bar":
262
- conc_vals = pd.to_numeric(df["concentration"], errors="coerce").replace([np.inf, -np.inf], np.nan).dropna()
263
- if len(conc_vals):
264
- uniq_vals = sorted({float(v) for v in conc_vals.tolist()}, reverse=True)[:12]
265
- pal = plt.get_cmap("tab20").colors
266
- legend_colors: Dict[float, str] = {}
267
- idx = 0
268
- for v in uniq_vals:
269
- # try to use any per-well bar color used for that exact value
270
- rows_v = df[pd.to_numeric(df["concentration"], errors="coerce")==v]
271
- col = ""
272
- for _, rr in rows_v.iterrows():
273
- cbc = str(rr.get("conc_bar_color","")).strip()
274
- if cbc: col = cbc; break
275
- if not col:
276
- import matplotlib as mpl
277
- col = mpl.colors.to_hex(pal[idx % len(pal)]); idx += 1
278
- legend_colors[v] = col
279
-
280
- ax_b = fig.add_axes([0.86, 0.18, 0.12, 0.22]); ax_b.axis("off")
281
- ax_b.text(0.0, 1.05, "Concentration", fontsize=11, fontweight="bold", transform=ax_b.transAxes)
282
- y = 0.88; dy = 0.11
283
- for v in uniq_vals:
284
- color = legend_colors[v]
285
- bar_w = 0.70 # fixed legend bar width
286
- bar_h = 0.10
287
- ax_b.add_patch(Rectangle((0.0, y-bar_h/2), bar_w, bar_h, transform=ax_b.transAxes,
288
- facecolor=color, edgecolor="#333", linewidth=0.6))
289
- label = f" – {v:.3g}{conc_unit if conc_unit else ''}"
290
- ax_b.text(0.74, y, label, fontsize=9, va="center", ha="left", transform=ax_b.transAxes)
291
- y -= dy
292
-
293
- ax.set_xlim(0, cols + 2); ax.set_ylim(0, rows + 2)
294
- ax.set_title("Plate Map", fontsize=22, pad=14)
295
- fig.tight_layout(rect=[0.05, 0.05, 0.84, 0.95]) # extra right margin
296
- return fig, ax
297
-
298
- # ----------------------------- UI -----------------------------
299
- st.set_page_config(page_title="Advanced Plate Map Designer", layout="wide")
300
- st.title("Advanced Plate Map Designer")
301
- st.caption("Design detailed 24- and 96-well plate maps, encode multiple experimental factors, and export high-quality figures and tables.")
302
-
303
- with st.sidebar:
304
- st.header("Plate Settings")
305
- plate_choice = st.selectbox("Plate type", list(PLATE_SPECS.keys()), index=0)
306
- rows = PLATE_SPECS[plate_choice]["rows"]; cols = PLATE_SPECS[plate_choice]["cols"]
307
- if "plate_df" not in st.session_state or st.session_state.get("shape", (0,0)) != (rows, cols):
308
- st.session_state["plate_df"] = empty_plate_df(rows, cols)
309
- st.session_state["shape"] = (rows, cols)
310
-
311
- st.divider(); st.subheader("Global Encodings")
312
- color_by = st.selectbox("Color wells by", ["compound","cell_line","control_type","concentration","seeding_density"], 0)
313
- second_label = st.selectbox("Text overlay", ["concentration","seeding_density","compound","cell_line","replicate","group_id","notes","none"], 0)
314
- if second_label == "none": second_label = ""
315
- text_fontsize = st.slider("Text overlay font size", 5, 18, 8)
316
- conc_mode = st.selectbox("Concentration display", ["text","bar","none"], 1)
317
- conc_bar_color_global = "#4d4d4d"
318
- if conc_mode == "bar":
319
- conc_bar_color_global = st.color_picker("Default bar color (fallback)", value="#4d4d4d")
320
- show_legend = st.checkbox("Show legends", True)
321
- edge_highlight = st.checkbox("Highlight edge wells", True)
322
-
323
- st.divider(); st.subheader("Export")
324
- export_dpi = st.slider("Figure DPI", 150, 1200, 600, step=50)
325
- fig_scale = st.slider("Figure size scale", 0.6, 2.5, 1.0, step=0.1)
326
-
327
- col_left, col_right = st.columns([0.55, 0.45])
328
-
329
- with col_left:
330
- st.subheader("Plate Map")
331
- fig, ax = draw_plate_figure(
332
- st.session_state["plate_df"], rows, cols,
333
- color_by=color_by, second_label=second_label,
334
- show_legend=show_legend, edge_highlight=edge_highlight,
335
- figsize_scale=fig_scale, dpi=export_dpi,
336
- text_fontsize=text_fontsize,
337
- conc_mode=conc_mode,
338
- conc_bar_color_global=conc_bar_color_global
339
- )
340
- buf = io.BytesIO(); fig.savefig(buf, format="png", dpi=export_dpi, bbox_inches="tight")
341
- st.image(buf.getvalue(), caption="Rendered plate")
342
- st.download_button("⬇️ Download PNG", data=buf.getvalue(), file_name="plate_map.png", mime="image/png")
343
- buf_svg = io.BytesIO(); fig.savefig(buf_svg, format="svg", bbox_inches="tight")
344
- st.download_button("⬇️ Download SVG", data=buf_svg.getvalue(), file_name="plate_map.svg", mime="image/svg+xml")
345
-
346
- with col_right:
347
- st.subheader("Assignments & Tools")
348
-
349
- # ---------- Quick Assign ----------
350
- with st.expander("Quick Assign by Well Range", expanded=True):
351
- rng = st.text_input("Well range(s) (e.g., A1:A6, B1:B3, C5)", value="")
352
- c1, c2 = st.columns(2)
353
- with c1:
354
- cell_line = st.text_input("Cell line / type", value="")
355
- cell_color = st.color_picker("Cell color (inner dot)", value="#FFFFFF")
356
- seeding = st.number_input("Seeding density (cells/well)", min_value=0, value=0, step=100)
357
- replicate = st.text_input("Replicate ID", value="")
358
- with c2:
359
- compound = st.text_input("Compound", value="")
360
- comp_color = st.color_picker("Compound color (used when 'color by' = compound)", value="#1f77b4")
361
- concentration = st.number_input("Concentration", min_value=0.0, value=0.0, step=0.1, format="%.4f")
362
- conc_units = st.text_input("Conc Units (e.g., nM, μM, mg/mL)", value="")
363
- c3, c4 = st.columns(2)
364
- with c3:
365
- control_type = st.selectbox("Control type", ["","Negative","Positive","Vehicle","Blank"], 0)
366
- timepoint = st.text_input("Timepoint (e.g., 24h)", value="")
367
- with c4:
368
- group_id = st.text_input("Group / Condition ID", value="")
369
- vehicle = st.text_input("Vehicle (optional, e.g., DMSO)", value="")
370
- conc_bar_color_assign = st.color_picker("Concentration bar color (for these wells)", value="#4d4d4d")
371
- notes = st.text_area("Notes", value="", height=80)
372
-
373
- if st.button("Apply to selected wells"):
374
- wells = expand_well_ranges(rng)
375
- df = st.session_state["plate_df"]
376
- if "ALL" in wells: wells = df["well"].tolist()
377
- updates = {
378
- "cell_line": cell_line,
379
- "cell_color": cell_color if cell_color != "#FFFFFF" else "",
380
- "seeding_density": np.nan if seeding == 0 else seeding,
381
- "compound": compound,
382
- "compound_color": comp_color if comp_color else "",
383
- "concentration": np.nan if concentration == 0 else concentration,
384
- "conc_units": conc_units,
385
- "conc_bar_color": conc_bar_color_assign if conc_mode=="bar" else "",
386
- "control_type": control_type,
387
- "timepoint": timepoint,
388
- "replicate": replicate,
389
- "group_id": group_id,
390
- "vehicle": vehicle,
391
- "notes": notes,
392
- }
393
- for w in wells:
394
- st.session_state["plate_df"].loc[
395
- st.session_state["plate_df"]["well"] == w, list(updates.keys())
396
- ] = list(updates.values())
397
- st.success(f"Updated {len(wells)} well(s).")
398
-
399
- # ---------- Serial Dilution ----------
400
- with st.expander("Serial Dilution Wizard", expanded=False):
401
- start_well = st.text_input("Start well (e.g., B2)", value="")
402
- steps = st.number_input("Number of steps", 2, 24, 6)
403
- start_conc = st.number_input("Starting concentration", 0.0, value=10.0, step=0.1, format="%.4f")
404
- factor = st.number_input("Dilution factor", 1.0, value=2.0, step=0.1, format="%.3f")
405
- direction = st.selectbox("Direction", ["across row →", "down column ↓"], 0)
406
- units = st.text_input("Units (e.g., μM, mg/mL)", value="μM")
407
- compound_name = st.text_input("Compound (optional)", value="")
408
- comp_color2 = st.color_picker("Compound color", value="#FF7F0E")
409
- conc_bar_color2 = st.color_picker("Concentration bar color (for these steps)", value="#4d4d4d")
410
- replicate2 = st.text_input("Replicate ID (optional)", value="")
411
- if st.button("Fill serial dilution"):
412
- try:
413
- sr = ROW_LETTERS.index(start_well[0].upper()) + 1
414
- sc = int(start_well[1:])
415
- wells = []
416
- for k in range(steps):
417
- r = sr if "row" in direction else sr + k if "↓" in direction or "down" in direction else sr
418
- c = sc + k if "→" in direction or "across" in direction else sc
419
- if 1 <= r <= rows and 1 <= c <= cols: wells.append(f"{ROW_LETTERS[r-1]}{c}")
420
- vals = serial_values(start_conc, factor, len(wells), "decreasing")
421
- for w, val in zip(wells, vals):
422
- st.session_state["plate_df"].loc[
423
- st.session_state["plate_df"]["well"] == w,
424
- ["compound","compound_color","concentration","conc_units","replicate","conc_bar_color"]
425
- ] = [compound_name, comp_color2, val, units, replicate2, conc_bar_color2]
426
- st.success(f"Applied serial dilution to {len(wells)} well(s).")
427
- except Exception as e:
428
- st.error(f"Could not parse start well. Error: {e}")
429
-
430
- # ---------- Seeding Gradient ----------
431
- with st.expander("Cell Seeding Gradient", expanded=False):
432
- start_well2 = st.text_input("Start well (e.g., C3)", key="seed_start")
433
- steps2 = st.number_input("Steps", 2, 24, 8, key="seed_steps")
434
- start_cells = st.number_input("Starting cells/well", min_value=0, value=20000, step=500, key="seed_num")
435
- factor2 = st.number_input("Multiplicative factor", 0.01, value=0.5, step=0.01, format="%.2f", key="seed_factor")
436
- direction2 = st.selectbox("Direction", ["across row →", "down column ↓"], 0, key="seed_dir")
437
- cell_line2 = st.text_input("Cell line", value="HepG2", key="seed_cellline")
438
- cell_color2 = st.color_picker("Cell color", value="#2ca02c", key="seed_color")
439
- if st.button("Fill seeding gradient"):
440
- try:
441
- sr = ROW_LETTERS.index(start_well2[0].upper()) + 1; sc = int(start_well2[1:])
442
- wells = []
443
- for k in range(steps2):
444
- r = sr if "row" in direction2 else sr + k if "↓" in direction2 or "down" in direction2 else sr
445
- c = sc + k if "→" in direction2 or "across" in direction2 else sc
446
- if 1 <= r <= rows and 1 <= c <= cols: wells.append(f"{ROW_LETTERS[r-1]}{c}")
447
- vals = [int(round(start_cells * (factor2 ** i))) for i in range(len(wells))]
448
- for w, cells in zip(wells, vals):
449
- st.session_state["plate_df"].loc[
450
- st.session_state["plate_df"]["well"] == w,
451
- ["cell_line","cell_color","seeding_density"]
452
- ] = [cell_line2, cell_color2, cells]
453
- st.success(f"Applied seeding gradient to {len(wells)} well(s).")
454
- except Exception as e:
455
- st.error(f"Could not parse start well. Error: {e}")
456
-
457
- # ---------- Utilities / I/O ----------
458
- with st.expander("🧰 Utilities", expanded=False):
459
- edge_type = st.selectbox("Mark edge wells as", ["","Buffer","Blank","Vehicle"], 0)
460
- if st.button("Apply to edge wells"):
461
- if edge_type:
462
- df = st.session_state["plate_df"]
463
- mask = (df["row"].isin([ROW_LETTERS[0], ROW_LETTERS[rows-1]])) | (df["col"].isin([1, cols]))
464
- st.session_state["plate_df"].loc[mask, "control_type"] = "Blank" if edge_type in ("Blank","Buffer") else "Vehicle"
465
- st.success("Edge wells updated.")
466
- if st.button("Clear all assignments"):
467
- st.session_state["plate_df"] = empty_plate_df(rows, cols); st.success("Cleared.")
468
-
469
- with st.expander("Import / Export Table", expanded=False):
470
- uploaded = st.file_uploader("Upload CSV/Excel (must include a 'well' column)", type=["csv","xlsx"])
471
- if uploaded is not None:
472
- df_up = load_uploaded_csv(uploaded)
473
- if df_up is None or "well" not in df_up.columns:
474
- st.error("Failed to load file or missing 'well' column.")
475
- else:
476
- df = st.session_state["plate_df"].set_index("well")
477
- for col in df_up.columns:
478
- if col not in df.columns: df[col] = np.nan
479
- # align columns present in the upload only
480
- df.loc[df_up["well"], df_up.columns] = df_up.set_index("well")
481
- st.session_state["plate_df"] = df.reset_index(); st.success(f"Imported {len(df_up)} rows.")
482
-
483
- out_csv = st.session_state["plate_df"].to_csv(index=False).encode("utf-8")
484
- st.download_button("⬇️ Download CSV", data=out_csv, file_name="plate_map.csv", mime="text/csv")
485
- toexcel = io.BytesIO()
486
- with pd.ExcelWriter(toexcel, engine="openpyxl") as writer:
487
- st.session_state["plate_df"].to_excel(writer, index=False, sheet_name="plate")
488
- st.download_button("⬇️ Download Excel", data=toexcel.getvalue(), file_name="plate_map.xlsx",
489
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
 
1
+ # app.py — launcher for Streamlit inside a Gradio Space
2
+ import os
3
+ import subprocess
4
+
5
+ PORT = os.environ.get("PORT", "7860") # HF Spaces provides PORT
6
+ cmd = [
7
+ "streamlit", "run", "plate_map_app.py",
8
+ "--server.port", PORT,
9
+ "--server.address", "0.0.0.0",
10
+ "--theme.base", "light", # optional
11
+ ]
12
+ # Run and block; return code will propagate to container logs on failure
13
+ subprocess.run(cmd, check=True)