Kaynağa Gözat

Updating app

Martin Horvat 1 gün önce
ebeveyn
işleme
9f9f61be66
1 değiştirilmiş dosya ile 133 ekleme ve 15 silme
  1. 133 15
      shiny_app/app.py

+ 133 - 15
shiny_app/app.py

@@ -23,6 +23,7 @@ if str(PROJECT_ROOT) not in sys.path:
 DATA_RAW = PROJECT_ROOT / "data" / "raw"
 DATA_GEN = PROJECT_ROOT / "data" / "gen"
 DEFAULT_METADATA_PATH = DATA_GEN / "metadata.pkl"
+WWW_DIR = APP_DIR / "www"
 
 # -----------------------------------------------------------------------------
 # Local package imports
@@ -67,7 +68,11 @@ def _empty_figure(message: str = "No plot available") -> go.Figure:
 
 
 def _load_metadata() -> pd.DataFrame:
-    """Load metadata table from disk or build it from DATA_RAW."""
+    """Load metadata table from disk or build it from DATA_RAW.
+
+    If data/gen/metadata.pkl does not exist, the metadata table is recreated
+    from data/raw and saved to data/gen/metadata.pkl.
+    """
     if DEFAULT_METADATA_PATH.exists():
         df_meta = pd.read_pickle(DEFAULT_METADATA_PATH)
     else:
@@ -100,10 +105,18 @@ DF_META = _load_metadata()
 
 
 def _metadata_for_display(df_meta: pd.DataFrame) -> pd.DataFrame:
-    """Return metadata with index columns exposed and an internal row_id."""
+    """Return metadata for UI display.
+
+    Path-like columns are hidden from the table, but they remain available in
+    DF_META for loading images.
+    """
     df = df_meta.reset_index().copy()
     df.insert(0, "row_id", range(len(df)))
 
+    # Hide all path-like columns from the UI table.
+    df = df.loc[:, ~df.columns.str.contains("path", case=False)]
+    df = df.loc[:, ~df.columns.str.contains("filename", case=False)]
+
     preferred = [
         "row_id",
         "patient_id",
@@ -113,8 +126,6 @@ def _metadata_for_display(df_meta: pd.DataFrame) -> pd.DataFrame:
         "is_corrupted",
         "PET_filename",
         "SEG_filename",
-        "PET_path",
-        "SEG_path",
     ]
     cols = [c for c in preferred if c in df.columns] + [
         c for c in df.columns if c not in preferred
@@ -124,6 +135,9 @@ def _metadata_for_display(df_meta: pd.DataFrame) -> pd.DataFrame:
 
 DF_DISPLAY = _metadata_for_display(DF_META)
 
+ORGAN_CHOICES = ["All"] + sorted(DF_DISPLAY["organ"].dropna().astype(str).unique().tolist())
+VISIT_CHOICES = ["All"] + [str(v) for v in sorted(DF_DISPLAY["visit"].dropna().unique().tolist())]
+
 
 def _format_image_id(row: pd.Series) -> str:
     """Create a readable image identifier from a selected metadata row."""
@@ -166,11 +180,67 @@ def _safe_error_message(exc: BaseException) -> str:
 
 app_ui = ui.page_fluid(
     ui.head_content(
-        ui.tags.link(rel="shortcut icon", href="favicon.ico", type="image/x-icon")
+        ui.tags.link(
+            rel="shortcut icon",
+            href="/favicon.ico?v=3",
+            type="image/x-icon",
+        )
     ),
     ui.h2("Spatial SUV tail-feature explorer"),
     ui.layout_sidebar(
         ui.sidebar(
+            ui.div(
+                ui.h5("Run analysis"),
+                ui.p("Select one row in the metadata table, then compute."),
+                ui.input_action_button(
+                    "run",
+                    "Compute selected row",
+                    class_="btn-success btn-lg",
+                    width="100%",
+                ),
+                style="""
+                    position: sticky;
+                    top: 0;
+                    z-index: 1000;
+                    background: white;
+                    padding: 1rem 0 1rem 0;
+                    border-bottom: 1px solid #ddd;
+                    margin-bottom: 1rem;
+                """,
+            ),
+            ui.h4("Metadata filters"),
+            ui.input_text(
+                "filter_patient",
+                "Patient contains",
+                value="",
+                placeholder="e.g. A001",
+            ),
+            ui.input_select(
+                "filter_organ",
+                "Organ",
+                choices=ORGAN_CHOICES,
+                selected="All",
+            ),
+            ui.input_select(
+                "filter_visit",
+                "Visit",
+                choices=VISIT_CHOICES,
+                selected="All",
+            ),
+            ui.input_select(
+                "filter_ae",
+                "AE patient",
+                choices=["All", "True", "False"],
+                selected="All",
+            ),
+            ui.input_select(
+                "filter_corrupted",
+                "Corrupted",
+                choices=["All", "True", "False"],
+                selected="All",
+            ),
+            ui.hr(),
+            ui.h4("Analysis settings"),
             ui.input_text(
                 "probs",
                 "Percentiles",
@@ -204,14 +274,13 @@ app_ui = ui.page_fluid(
             ui.input_checkbox("compute_local_contrast", "Compute local contrast", value=True),
             ui.input_checkbox("compute_sphericity", "Compute sphericity", value=True),
             ui.input_checkbox("crop_to_roi", "Crop to ROI", value=True),
-            ui.hr(),
-            ui.input_action_button("run", "Compute selected row", class_="btn-primary"),
-            width=330,
+            width=340,
         ),
         ui.navset_tab(
             ui.nav_panel(
                 "Metadata table",
-                ui.p("Select one row from the table, then click 'Compute selected row'."),
+                ui.p("Filter and select one row, then click 'Compute selected row'."),
+                ui.output_ui("filter_summary"),
                 ui.output_data_frame("metadata_table"),
             ),
             ui.nav_panel(
@@ -246,12 +315,54 @@ app_ui = ui.page_fluid(
 
 
 def server(input: Inputs, output: Outputs, session: Session):
+    @reactive.calc
+    def filtered_metadata_display() -> pd.DataFrame:
+        """Apply sidebar filters to the displayed metadata table."""
+        df = DF_DISPLAY.copy()
+
+        patient_text = input.filter_patient().strip()
+        organ = input.filter_organ()
+        visit = input.filter_visit()
+        ae = input.filter_ae()
+        corrupted = input.filter_corrupted()
+
+        if patient_text:
+            df = df[
+                df["patient_id"]
+                .astype(str)
+                .str.contains(patient_text, case=False, na=False)
+            ]
+
+        if organ != "All":
+            df = df[df["organ"].astype(str) == organ]
+
+        if visit != "All":
+            df = df[df["visit"].astype(str) == visit]
+
+        if ae != "All" and "is_AE_patient" in df.columns:
+            df = df[df["is_AE_patient"].astype(bool) == (ae == "True")]
+
+        if corrupted != "All" and "is_corrupted" in df.columns:
+            df = df[df["is_corrupted"].astype(bool) == (corrupted == "True")]
+
+        return df.reset_index(drop=True)
+
+    @render.ui
+    def filter_summary():
+        n_shown = len(filtered_metadata_display())
+        n_total = len(DF_DISPLAY)
+        return ui.div(
+            ui.strong("Rows shown: "),
+            f"{n_shown} / {n_total}",
+        )
+
     @render.data_frame
     def metadata_table():
         return render.DataGrid(
-            DF_DISPLAY,
+            filtered_metadata_display(),
             selection_mode="row",
-            filters=True,
+            filters=False,
+            width="100%",
             height="650px",
         )
 
@@ -260,7 +371,16 @@ def server(input: Inputs, output: Outputs, session: Session):
         selected = metadata_table.cell_selection()["rows"]
         if not selected:
             return None
-        return DF_DISPLAY.iloc[int(selected[0])]
+
+        df = filtered_metadata_display()
+        if df.empty:
+            return None
+
+        row_pos = int(selected[0])
+        if row_pos >= len(df):
+            return None
+
+        return df.iloc[row_pos]
 
     @reactive.calc
     @reactive.event(input.run)
@@ -464,7 +584,7 @@ def server(input: Inputs, output: Outputs, session: Session):
 app = App(
     app_ui,
     server,
-    static_assets=Path(__file__).resolve().parent / "www",
+    static_assets=WWW_DIR,
 )
 
 
@@ -475,5 +595,3 @@ if __name__ == "__main__":
     #     shiny run --reload shiny_app/app.py
     # Direct execution via `python shiny_app/app.py` works without auto-reload.
     run_app(app, host="127.0.0.1", port=8000, reload=False)
-
-