"""enhanced_funnel_plot.py – Funnel Plot single arm et Al. ‑plot tab
--------------------------------------------------------------------------
This single file bundles:
  • **EnhancedFunnelPlotTab** – upgraded funnel plot with variance‑stabilising
    transformations, Egger, Peters, Trim‑and‑Fill, fixed/random pooling and
    subgroup colouring + data table.
  • **ReadmeTab** – static README shown inside the GUI.
  • **CombinedApp** – minimal Tk interface wiring everything together.
Copy/rename the file as you wish and run it with Python ≥ 3.9.
"""

from __future__ import annotations

# ── std / typing ────────────────────────────────────────────────────────────
import itertools
import os
from typing import Dict, List, Literal, Sequence, Tuple

# ── numeric & plotting ──────────────────────────────────────────────────────
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.axes import Axes
from matplotlib.figure import Figure

# ── scientific stats (optional fall‑backs) ──────────────────────────────────
try:
    from scipy.stats import t  # Egger / Peters       
except ImportError:  # pragma: no cover
    t = None
    print("⚠️  scipy non installato – Egger/Peters disabilitati.")
try:
    import statsmodels.stats.meta_analysis as ma  # type: ignore
    HAS_STM = True
    _fit_tau_iterative = getattr(ma, "_fit_tau_iterative", getattr(ma, "fit_tau_iterative", None))  # type: ignore
    _trimfill = getattr(ma, "_trimfill", getattr(ma, "trimfill", None))  # type: ignore

    if _fit_tau_iterative is None:
        print("ℹ️  statsmodels presente ma REML non disponibile – userò DerSimonian‑Laird.")
    if _trimfill is None:
        print("ℹ️  statsmodels presente ma Trim‑and‑Fill non disponibile in questa versione.")
except ImportError:  # pragma: no cover
    HAS_STM = False
    _fit_tau_iterative = None
    _trimfill = None
    print("ℹ️  statsmodels non installato – Trim‑and‑Fill & REML non disponibili.")
# ── GUI ─────────────────────────────────────────────────────────────────────
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from tkinter.scrolledtext import ScrolledText
# ── auto-install statsmodels se mancante ─────────────────────────
try:
    import statsmodels.stats.meta_analysis as ma  # noqa: F401
except ImportError:
    import subprocess, sys
    print("ℹ️  statsmodels non trovato, provo a installarlo…")
    subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "statsmodels"])
    import statsmodels.stats.meta_analysis as ma  # noqa: F401
    print("✅  statsmodels installato con successo")
##############################################################################
# Helper functions                                                           #
##############################################################################

def _calc_effect(events: int, total: int, tx: Literal["raw", "logit", "ft"] = "logit") -> Tuple[float, float]:
    """Return *effect* and *se* for a single‑arm proportion under the chosen
    transformation (raw proportion, logit, Freeman–Tukey)."""
    if total <= 0:
        raise ValueError("Total must be > 0")

    if tx == "raw":
        p = events / total
        se = np.sqrt(p * (1 - p) / total)
        return p, se

    if tx == "logit":
        # continuity correction 0.5
        p_adj = (events + 0.5) / (total + 1)
        logit = np.log(p_adj / (1 - p_adj))
        se = np.sqrt(1 / (events + .5) + 1 / (total - events + .5))
        return logit, se

    if tx == "ft":            # Freeman–Tukey double-arcsine
        # ── termini complementari (successi e insuccessi) ───────────────
        denom = total + 1
        eff = (
        np.arcsin(np.sqrt((events + 0.5) / denom)) +
        np.arcsin(np.sqrt((total - events + 0.5) / denom))
        )
        # var(FT) ≈ 1 / (n + 0.5)  ⇒  se = 1 / √(n + 0.5)
        se = 1 / np.sqrt(total + 0.5)
        return eff, se

    raise ValueError(f"Unknown transformation {tx}")


def _dl_tau2(effects: np.ndarray, variances: np.ndarray) -> float:
    q = np.sum((effects - np.average(effects, weights=1/variances))**2 / variances)
    df = len(effects) - 1
    c = np.sum(1/variances) - np.sum(1/variances**2) / np.sum(1/variances)
    return max(0., (q - df) / c)


def _pool(effects: np.ndarray, ses: np.ndarray, model: Literal["fixed", "random"] = "random",
          tau2_method: Literal["DL", "REML"] = "DL") -> Tuple[float, float, float]:
    variances = ses ** 2

    if model == "fixed":
        w = 1 / variances
        pooled = np.sum(w * effects) / w.sum()
        se_pool = np.sqrt(1 / w.sum())
        return pooled, se_pool, 0.0

    # random‑effects ------------------------------------------------------
    if tau2_method == "REML" and HAS_STM:
        tau2, _ = _fit_tau_iterative(effects, variances, method="reml")  # type: ignore
    else:
        tau2 = _dl_tau2(effects, variances)

    w = 1 / (variances + tau2)
    pooled = np.sum(w * effects) / w.sum()
    se_pool = np.sqrt(1 / w.sum())
    return pooled, se_pool, tau2


def egger_test(effects: np.ndarray, ses: np.ndarray) -> Dict[str, float]:
    if t is None:
        raise RuntimeError("scipy non installato – Egger non disponibile")
    precision = 1 / ses
    std_eff = effects / ses
    X = np.vstack([precision, np.ones_like(precision)]).T
    beta, _, _, _ = np.linalg.lstsq(X, std_eff, rcond=None)
    intercept = beta[1]
    n = len(effects)
    res = std_eff - X @ beta
    df = n - 2
    mse = (res**2).sum() / df
    mean_prec = precision.mean()
    sxx = ((precision - mean_prec)**2).sum()
    se_int = np.sqrt(mse * (1/n + mean_prec**2 / sxx))
    t_stat = intercept / se_int
    p_val = 2 * (1 - t.cdf(abs(t_stat), df))
    return {"intercept": intercept, "se": se_int, "t": t_stat, "df": df, "p": p_val}


def peters_test(events: Sequence[int], totals: Sequence[int]) -> Dict[str, float]:
    if t is None:
        raise RuntimeError("scipy non installato – Peters non disponibile")
    effects, ses = zip(*[_calc_effect(e, n, "logit") for e, n in zip(events, totals)])
    effects = np.asarray(effects)
    iv = 1 / np.asarray(totals)
    w = np.asarray(totals)
    X = np.vstack([iv, np.ones_like(iv)]).T
    WX = X * np.sqrt(w)[:, None]
    Wy = effects * np.sqrt(w)
    beta, _, _, _ = np.linalg.lstsq(WX, Wy, rcond=None)
    intercept = beta[1]
    res = Wy - WX @ beta
    df = len(effects) - 2
    mse = (res**2).sum() / df
    mean_x = (iv * w).sum() / w.sum()
    sxx = (w * (iv - mean_x)**2).sum()
    se_int = np.sqrt(mse * (1/w.sum() + mean_x**2 / sxx))
    t_stat = intercept / se_int
    p_val = 2 * (1 - t.cdf(abs(t_stat), df))
    return {"intercept": intercept, "se": se_int, "t": t_stat, "df": df, "p": p_val}


def trim_and_fill(effects: np.ndarray, ses: np.ndarray, side: str = "auto") -> Dict[str, np.ndarray]:
    if not HAS_STM:
        raise RuntimeError("statsmodels non installato – Trim‑and‑Fill non disponibile")
    return _trimfill(effects, ses, side=side)  # type: ignore

##############################################################################
# Enhanced Funnel Plot tab                                                   #
##############################################################################

class EnhancedFunnelPlotTab:
    """Tkinter tab that draws an advanced funnel plot with bias diagnostics."""

    _TX_OPTIONS = {"Proportion (raw)": "raw", "Logit": "logit", "Freeman–Tukey": "ft"}
    _PALETTE = itertools.cycle(plt.get_cmap("tab10").colors)

    def __init__(self, notebook: ttk.Notebook):
        self.frame = ttk.Frame(notebook)
        notebook.add(self.frame, text="Funnel Plot (adv.)")
        self._build_gui()
        self._edit_iid: str | None = None

    # ── GUI layout ────────────────────────────────────────────────────────
    def _build_gui(self) -> None:
        f = self.frame
        # data grid -------------------------------------------------------
        cols = ("✔", "Study", "Events", "Total", "Group")
        self.tree = ttk.Treeview(f, columns=cols, show="headings", height=12)
        for c in cols:
            self.tree.heading(c, text=c)
            self.tree.column(c, width=80, anchor="center")
        self.tree.grid(row=0, column=0, columnspan=8, padx=5, pady=5, sticky="nsew")
        self.tree.bind("<Double-1>", self._toggle_include)

        # entry fields ----------------------------------------------------
        self.entries: Dict[str, tk.Entry] = {}
        for i, lab in enumerate(["Study", "Events", "Total", "Group"], start=1):
            ttk.Label(f, text=lab).grid(row=i, column=0, sticky="e")
            e = ttk.Entry(f, width=14)
            e.grid(row=i, column=1, sticky="w")
            self.entries[lab.lower()] = e

        # buttons ---------------------------------------------------------
        btn_specs: List[Tuple[str, callable]] = [
            ("Add", self._add), ("Edit", self._edit), ("Update", self._update),
            ("Delete", self._delete), ("Import CSV", self._import),
            ("Export CSV", self._export), ("Show Funnel", self._funnel),
        ]
        for i, (txt, cmd) in enumerate(btn_specs):
            ttk.Button(f, text=txt, command=cmd, width=12).grid(row=5, column=i, pady=4)

        # options ---------------------------------------------------------
        ttk.Label(f, text="Scale:").grid(row=1, column=3, sticky="e")
        self.tx_var = tk.StringVar(value="Logit")
        ttk.OptionMenu(f, self.tx_var, "Logit", *self._TX_OPTIONS.keys()).grid(row=1, column=4, sticky="w")

        ttk.Label(f, text="Model:").grid(row=2, column=3, sticky="e")
        self.model_var = tk.StringVar(value="random")
        ttk.OptionMenu(f, self.model_var, "random", "fixed", "random").grid(row=2, column=4, sticky="w")
        ttk.Label(f, text="Trim‑and‑Fill:").grid(row=3, column=3, sticky="e")
        self.tf_var = tk.StringVar(value="auto")
        ttk.OptionMenu(f, self.tf_var, "auto", "none", "auto", "left", "right").grid(row=3, column=4, sticky="w")

        # grid weights ----------------------------------------------------
        for col in range(8):
            f.grid_columnconfigure(col, weight=1)
        f.grid_rowconfigure(0, weight=1)

    # ── CRUD ops ──────────────────────────────────────────────────────────
    def _toggle_include(self, event):
        iid = self.tree.identify_row(event.y)
        if not iid:
            return
        cur = self.tree.set(iid, "✔")
        self.tree.set(iid, "✔", "" if cur == "✔" else "✔")

    def _data_rows(self, *, included_only: bool = False):
        for iid in self.tree.get_children():
            vals = self.tree.item(iid)["values"]
            if included_only and vals[0] != "✔":
                continue
            yield iid, vals

    def _add(self):
        try:
            study = self.entries["study"].get().strip()
            events = int(self.entries["events"].get())
            total = int(self.entries["total"].get())
            group = self.entries["group"].get().strip() or "All"
        except Exception:
            messagebox.showerror("Input error", "Please enter valid numeric values")
            return
        self.tree.insert("", tk.END, values=("✔", study, events, total, group))
        for e in self.entries.values():
            e.delete(0, tk.END)

    def _edit(self):
        iid = self.tree.focus()
        if not iid:
            return
        vals = self.tree.item(iid)["values"][1:]
        for key, val in zip(["study", "events", "total", "group"], vals):
            self.entries[key].delete(0, tk.END)
            self.entries[key].insert(0, val)
        self._edit_iid = iid

    def _update(self):
        if not self._edit_iid:
            return
        try:
            study = self.entries["study"].get().strip()
            events = int(self.entries["events"].get())
            total = int(self.entries["total"].get())
            group = self.entries["group"].get().strip() or "All"
        except Exception:
            messagebox.showerror("Input error", "Please enter valid numeric values")
            return
        self.tree.item(self._edit_iid, values=("✔", study, events, total, group))
        self._edit_iid = None
        for e in self.entries.values():
            e.delete(0, tk.END)

    def _delete(self):
        iid = self.tree.focus()
        if iid:
            self.tree.delete(iid)

    def _import(self):
        path = filedialog.askopenfilename(filetypes=[("CSV", "*.csv")])
        if not path:
            return
        df = pd.read_csv(path)
        for _, r in df.iterrows():
            self.tree.insert("", tk.END, values=("✔", r["Study"], int(r["Events"]), int(r["Total"]), r.get("Group", "All")))

    def _export(self):
        path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV", "*.csv")])
        if not path:
            return
        rows = [
            {"Include": v[0], "Study": v[1], "Events": v[2], "Total": v[3], "Group": v[4]}
            for _, v in self._data_rows()
        ]
        pd.DataFrame(rows).to_csv(path, index=False)
        messagebox.showinfo("Export", f"Data saved to {os.path.basename(path)}")

    # ── analysis helpers ─────────────────────────────────────────────────
    def _prepare(self):
        rows = [v for _, v in self._data_rows(included_only=True)]
        if len(rows) < 3:
            raise ValueError("Need ≥ 3 studies for the funnel plot")
        events, totals, labels, groups = zip(*[(r[2], r[3], r[1], r[4]) for r in rows])
        tx_key = self.tx_var.get()
        tx = self._TX_OPTIONS[tx_key]
        eff, se = zip(*[_calc_effect(e, n, tx) for e, n in zip(events, totals)])
        return np.array(eff), np.array(se), np.array(events), np.array(totals), list(labels), list(groups), tx_key

    # ── main routine ─────────────────────────────────────────────────────
    def _funnel(self):
        """
        Show funnel plot in a dedicated window and display the data table in a separate
        figure so the main plot is not squashed. Provides a user‑controlled option for
        Trim‑and‑Fill ("none", "auto", "left", "right").
        """
        try:
            eff, se, events, totals, labels, groups, tx_label = self._prepare()
        except Exception as exc:
            messagebox.showerror("Error", str(exc))
            return

        pooled, se_pool, _ = _pool(eff, se, model=self.model_var.get())

        # --- main funnel figure (plot + diagnostics text) ---------------
        fig: Figure = plt.figure(figsize=(7, 7))
        gs = fig.add_gridspec(nrows=2, ncols=1, height_ratios=[3, .4])
        ax_plot: Axes = fig.add_subplot(gs[0])
        ax_info: Axes = fig.add_subplot(gs[1]); ax_info.axis("off")

        # confidence region ---------------------------------------------
        se_line = np.linspace(0, se.max()*1.05, 200)[1:]
        for z, col, lab in ((2.58, "#FFCCCC", "99%"),
                            (1.96, "#FFDD99", "95%"),
                            (1.64, "#FFFFCC", "90%")):
            ax_plot.fill_betweenx(se_line,
                                  pooled - z*se_line,
                                  pooled + z*se_line,
                                  color=col, alpha=.6,
                                  label=lab)

        # scatter points, coloured by subgroup --------------------------
        group2col: Dict[str, str] = {}
        for g in groups:
            if g not in group2col:
                group2col[g] = next(self._PALETTE)
        colors = [group2col[g] for g in groups]
        ax_plot.scatter(eff, se, c=colors, edgecolor="black", zorder=10)

        # labels
        for x, y, lab in zip(eff, se, labels):
            ax_plot.annotate(lab, (x, y), xytext=(4, 0),
                             textcoords="offset points", fontsize=7)

        # pooled vertical line
        ax_plot.axvline(pooled, color="black", lw=1.2,
                        label=f"Pooled ({self.model_var.get()})")
        ax_plot.set_xlabel(f"Effect size ({tx_label})")
        ax_plot.set_ylabel("Standard error")
        ax_plot.invert_yaxis()
        ax_plot.legend(fontsize=8, loc="upper right")
        ax_plot.set_title("Funnel plot with subgroup colours")

        # --- bias diagnostics ------------------------------------------
        lines = []
        try:
            eg = egger_test(eff, se)
            lines.append(f"Egger b₀ = {eg['intercept']:.3f} (p = {eg['p']:.3f})")
        except Exception as e:
            lines.append(f"Egger unavailable: {e}")

        try:
            pt = peters_test(events, totals)
            lines.append(f"Peters b₀ = {pt['intercept']:.3f} (p = {pt['p']:.3f})")
        except Exception as e:
            lines.append(f"Peters unavailable: {e}")

        # controlled Trim‑and‑Fill --------------------------------------
        tf_side = getattr(self, "tf_var", tk.StringVar(value="auto")).get()
        if HAS_STM and tf_side != "none":
            try:
                side = "auto" if tf_side == "auto" else tf_side
                tf = trim_and_fill(eff, se, side=side)
                lines.append(f"Trim‑and‑Fill: {int(tf['n_filled'])} imputed study(ies)")
            except Exception as e:
                lines.append(f"Trim‑and‑Fill error: {e}")

        ax_info.text(.01, .5, "\n".join(lines),
                     va="center", ha="left", fontsize=9,
                     transform=ax_info.transAxes)
        # --- Egger's test results table -----------------------------------
        if 'eg' in locals():
            ci_low = eg['intercept'] - 1.96 * eg['se']
            ci_high = eg['intercept'] + 1.96 * eg['se']
            egger_df = pd.DataFrame({
                "Egger's Test": ["Intercept (β₀)", "Standard Error", "t-value",
                                 "Degrees of Freedom", "p-value", "95% CI"],
                "Value": [round(eg['intercept'], 4),
                          round(eg['se'], 4),
                          round(eg['t'], 4),
                          int(eg['df']),
                          f"{eg['p']:.4f}",
                          f"[{ci_low:.4f}, {ci_high:.4f}]"]
            })
            fig_eg, ax_eg = plt.subplots(figsize=(4, 1.5))
            ax_eg.axis("off")
            tbl_eg = ax_eg.table(cellText=egger_df.values.tolist(),
                                 colLabels=egger_df.columns,
                                 loc="center", cellLoc="center")
            tbl_eg.auto_set_font_size(False)
            tbl_eg.set_fontsize(8)
            tbl_eg.scale(1, 1.2)
            fig_eg.tight_layout()
            plt.show()

        plt.tight_layout()
        plt.show()

        # --- separate figure for data table ----------------------------
        df = pd.DataFrame({
            "Study": labels,
            "Group": groups,
            "Events": events,
            "Total": totals,
            "Effect": eff.round(4),
            "SE": se.round(4),
        })
        fig_tbl, ax_tbl = plt.subplots(figsize=(6, 0.4*len(df) + 1))
        ax_tbl.axis("off")
        tbl = ax_tbl.table(cellText=df.values.tolist(),
                           colLabels=df.columns,
                           loc="center", cellLoc="center")
        tbl.auto_set_font_size(False)
        tbl.set_fontsize(8)
        tbl.scale(1, 1.2)
        fig_tbl.tight_layout()
        plt.show()
import tkinter as tk
from tkinter import ttk
import tkinter as tk
from tkinter import ttk
from datetime import datetime

from datetime import datetime
import tkinter as tk
from tkinter import ttk

from datetime import datetime
import tkinter as tk
from tkinter import ttk

from datetime import datetime
import tkinter as tk
from tkinter import ttk

class ReadmeTab:
    """
    Static README tab for the Funnel Plot single arm et Al. application.
    """
    def __init__(self, master: ttk.Notebook):
        self.frame = ttk.Frame(master)
        master.add(self.frame, text="README")
        self._build_gui()

    def _build_gui(self):
        today = datetime.now().strftime("%Y-%m-%d")

        # Motto agostiniano e nome applicazione
        motto_label = tk.Label(self.frame, text="In Illo uno unum", font=("Segoe UI", 12, "italic"), fg="#2266AA")
        motto_label.pack(anchor="n", pady=(10, 2))
        app_label = tk.Label(self.frame, text="Funnel Plot single arm et Al.", font=("Segoe UI", 13, "bold"), fg="#1A5276")
        app_label.pack(anchor="n", pady=(2, 2))

        # Autore e data
        author_label = tk.Label(self.frame, text=f"Author: Mario Dioguardi", font=("Segoe UI", 10), fg="#444444")
        author_label.pack(anchor="n")
        date_label = tk.Label(self.frame, text=f"Last update: {today}", font=("Segoe UI", 10), fg="#888888")
        date_label.pack(anchor="n", pady=(0, 10))

        readme_text = (
            "README: Application Tabs and Statistical Methods\n"
            "===============================================\n\n"
            "This application implements several analysis tabs for meta-analysis of continuous and binary data, both single-arm and double-arm.\n"
            "Each tab supports data import/export via CSV files and provides fixed-effects meta-analysis with Egger's test for publication bias.\n\n"

            "Tab Overview and Calculations\n"
            "-----------------------------\n\n"

            "1. ProportionSingleArmTab\n"
            "   • Purpose: Meta-analysis of single-arm binary (proportion) data (e.g. prevalence, incidence).\n"
            "   • Calculations:\n"
            "     - Effect size per study: logit(p), where p = Events / Total\n"
            "     - Continuity correction: logit((Events+0.5)/(Total+1))\n"
            "     - Standard Error: sqrt(1/(Events+0.5) + 1/(Total-Events+0.5))\n"
            "     - Fixed effects pooling (inverse variance)\n"
            "     - Egger's test (intercept, SE, t, p-value, 95% CI)\n"
            "     - Funnel plot: X=logit(p), Y=SE, subgroups coloured, 99/95/90% CI bands in greyscale\n"
            "   • CSV columns: Study, Events, Total, Group (optional)\n\n"

            "2. SMDSingleArmTab\n"
            "   • Purpose: Meta-analysis of single-arm binary data, effect as SMD (z-score logit(p)) between studies\n"
            "   • Calculations:\n"
            "     - Effect size: z-score of logit(p): (logit(p)_i – mean(logit(p))) / SD(logit(p))\n"
            "     - SE: as above\n"
            "     - Fixed effects pooling\n"
            "     - Egger's test on SMD\n"
            "     - Funnel plot: X=SMD (z-score logit(p)), Y=SE, subgroups coloured, CI bands\n"
            "   • CSV columns: Study, Events, Total, Group (optional)\n\n"

            "3. MDContinuousSingleArmTab\n"
            "   • Purpose: Meta-analysis of continuous single-arm data (e.g. single-group mean)\n"
            "   • Calculations:\n"
            "     - Effect size: Mean\n"
            "     - SE: SD / sqrt(N)\n"
            "     - Fixed effects pooling\n"
            "     - Egger's test on mean\n"
            "     - Funnel plot: X=Mean, Y=SE, subgroups coloured, CI bands\n"
            "   • CSV columns: Study, Mean, SD, N, Group (optional)\n\n"

            "4. SMDSingleArmContTab\n"
            "   • Purpose: Meta-analysis of continuous single-arm data using SMD (mean/SD per study)\n"
            "   • Calculations:\n"
            "     - Effect size: SMD = Mean / SD\n"
            "     - SE: sqrt(1/N)\n"
            "     - Fixed effects pooling\n"
            "     - Egger's test on SMD\n"
            "     - Funnel plot: X=SMD, Y=SE, subgroups coloured, CI bands\n"
            "   • CSV columns: Study, Mean, SD, N, Group (optional)\n\n"

            "5. DoubleArmContTab\n"
            "   • Purpose: Meta-analysis of continuous two-arm data (treatment vs control).\n"
            "   • The user can select between MD (mean difference) or SMD (Hedges' g, standardised mean difference).\n"
            "   • Calculations:\n"
            "     - MD: Mean Difference = MeanA – MeanB\n"
            "       SE_MD = sqrt(SD_A^2/N_A + SD_B^2/N_B)\n"
            "     - SMD (Hedges' g):\n"
            "         SD_pooled = sqrt(((N_A-1)SD_A^2 + (N_B-1)SD_B^2)/(N_A+N_B-2))\n"
            "         SMD = (MeanA – MeanB) / SD_pooled\n"
            "         Hedges' correction: J = 1 – 3/[4(N_A+N_B-2)-1], SMD_adj = SMD * J\n"
            "         SE_SMD = sqrt((N_A+N_B)/(N_A*N_B) + (SMD_adj^2)/(2(N_A+N_B)))\n"
            "     - Fixed effects pooling\n"
            "     - Egger's test on chosen metric\n"
            "     - Funnel plot: X=MD or SMD, Y=SE, subgroups coloured, CI bands\n"
            "   • CSV columns: Study, Mean A, SD A, N A, Mean B, SD B, N B, Group (optional)\n\n"

            "6. ReadmeTab\n"
            "   • Displays this documentation, update date, instructions and references.\n\n"

            "CSV Import/Export for Each Tab\n"
            "------------------------------\n"
            "• All tabs support import and export to CSV files.\n"
            "• Required columns for each tab are listed above. The Group column is optional but allows visual subgroup distinction in plots.\n"
            "• The 'Include' column is used internally to track active studies in the GUI.\n\n"

            "Graphical Features\n"
            "------------------\n"
            "• Each plot supports subgroup colouring (legend automatically generated).\n"
            "• 99%, 95%, and 90% confidence intervals are drawn in greyscale, legend labels display only the percent value (IC 99%, IC 95%, IC 90%).\n"
            "• Each point in the plot is labelled with the study name.\n"
            "• Plots can be saved as PNG images from the GUI.\n\n"

            "Meta-analytic Methods\n"
            "---------------------\n"
            "• All meta-analyses use the fixed-effects (inverse-variance) model.\n"
            "• Egger's regression test is used to assess publication bias for each metric.\n"
            "• For more information or formula details, see the code or contact the author.\n\n"

            "Requirements\n"
            "-----------\n"
            "- Python 3.9 or later\n"
            "- Libraries: numpy, pandas, matplotlib, scipy, statsmodels\n"
            "\n"
            "Disclaimer\n"
            "----------\n"
            "This application is a BETA version provided for research and academic use only.\n"
            "It is NOT intended for clinical decision-making, diagnosis, or treatment.\n"
            "No medical claims are made regarding the results.\n"
            "Use at your own risk.\n"
            "For feedback or bug reports, contact: mario.dioguardi@unifg.it\n"
        )

        text_widget = tk.Text(self.frame, wrap="word", height=48, width=110, font=("Consolas", 10))
        text_widget.insert("1.0", readme_text)
        text_widget.config(state="disabled")
        text_widget.pack(expand=True, fill="both", padx=12, pady=8)



# ── Continuous outcomes support ─────────────────────────────────────────────
from typing import Literal, Tuple

def _calc_effect_cont(mean_t: float, sd_t: float, n_t: int,
                      mean_c: float, sd_c: float, n_c: int,
                      metric: Literal["md", "smd"] = "md") -> Tuple[float, float]:
    """
    Return (effect, se) for continuous outcomes.

    * md  – Mean Difference
    * smd – Hedges g (standardised)
    """
    diff = mean_t - mean_c
    if metric == "md":
        se = np.sqrt(sd_t**2 / n_t + sd_c**2 / n_c)
        return diff, se

    # Hedges g
    df = n_t + n_c - 2
    s_pool = np.sqrt(((n_t - 1) * sd_t**2 + (n_c - 1) * sd_c**2) / df)
    g = diff / s_pool
    J = 1 - 3 / (4 * df - 1)
    g *= J
    var = (n_t + n_c) / (n_t * n_c) + g**2 / (2 * df)
    se = np.sqrt(var)
    return g, se




import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from scipy.stats import norm, linregress
import csv

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import t




def logit_cont(events, total):
    """Logit transformation with continuity correction (for 0 events or 0 non-events)."""
    p_adj = (events + 0.5) / (total + 1)
    logit_val = np.log(p_adj / (1 - p_adj))
    se = np.sqrt(1 / (events + 0.5) + 1 / (total - events + 0.5))
    return logit_val, se

def egger_test(effects, ses):
    """Egger's regression test for funnel plot asymmetry."""
    precision = 1 / ses
    std_eff = effects / ses
    X = np.vstack([precision, np.ones_like(precision)]).T
    beta, _, _, _ = np.linalg.lstsq(X, std_eff, rcond=None)
    intercept = beta[1]
    n = len(effects)
    res = std_eff - X @ beta
    df = n - 2
    mse = (res**2).sum() / df
    mean_prec = precision.mean()
    sxx = ((precision - mean_prec) ** 2).sum()
    se_int = np.sqrt(mse * (1/n + mean_prec**2 / sxx))
    t_stat = intercept / se_int
    p_val = 2 * (1 - t.cdf(abs(t_stat), df))
    return {"intercept": intercept, "se": se_int, "t": t_stat, "df": df, "p": p_val}

class LogitSingleArmTab:
    GREYS = ["#D0D0D0", "#AAAAAA", "#787878"]  # 99%, 95%, 90%
    GREY_LABELS = ["IC 99%", "IC 95%", "IC 90%"]

    def __init__(self, notebook: ttk.Notebook):
        self.frame = ttk.Frame(notebook)
        notebook.add(self.frame, text="Logit(p) Single Arm")
        self._build_gui()
        self._edit_iid = None
        self.last_fig = None

    def _build_gui(self):
        f = self.frame
        cols = ("✔", "Study", "Events", "Total", "Group")
        self.tree = ttk.Treeview(f, columns=cols, show="headings", height=12)
        for c in cols:
            self.tree.heading(c, text=c)
            self.tree.column(c, width=80, anchor="center")
        self.tree.grid(row=0, column=0, columnspan=8, padx=5, pady=5, sticky="nsew")
        self.tree.bind("<Double-1>", self._toggle_include)

        self.entries = {}
        for i, lab in enumerate(["Study", "Events", "Total", "Group"], start=1):
            ttk.Label(f, text=lab).grid(row=i, column=0, sticky="e")
            e = ttk.Entry(f, width=14)
            e.grid(row=i, column=1, sticky="w")
            self.entries[lab.lower()] = e

        btn_specs = [
            ("Add", self._add), ("Edit", self._edit), ("Update", self._update),
            ("Delete", self._delete), ("Import CSV", self._import),
            ("Export CSV", self._export), ("Show Funnel", self._funnel),
            ("Save Plot", self._save_plot)
        ]
        for i, (txt, cmd) in enumerate(btn_specs):
            ttk.Button(f, text=txt, command=cmd, width=12).grid(row=5, column=i, pady=4)

        for col in range(8):
            f.grid_columnconfigure(col, weight=1)
        f.grid_rowconfigure(0, weight=1)

    def _toggle_include(self, event):
        iid = self.tree.identify_row(event.y)
        if not iid:
            return
        cur = self.tree.set(iid, "✔")
        self.tree.set(iid, "✔", "" if cur == "✔" else "✔")

    def _data_rows(self, included_only=False):
        for iid in self.tree.get_children():
            vals = self.tree.item(iid)["values"]
            if included_only and vals[0] != "✔":
                continue
            yield iid, vals

    def _add(self):
        try:
            study = self.entries["study"].get().strip()
            events = int(self.entries["events"].get())
            total = int(self.entries["total"].get())
            group = self.entries["group"].get().strip() or "All"
        except Exception:
            messagebox.showerror("Input error", "Please enter valid numeric values")
            return
        self.tree.insert("", tk.END, values=("✔", study, events, total, group))
        for e in self.entries.values():
            e.delete(0, tk.END)

    def _edit(self):
        iid = self.tree.focus()
        if not iid:
            return
        vals = self.tree.item(iid)["values"][1:]
        for key, val in zip(["study", "events", "total", "group"], vals):
            self.entries[key].delete(0, tk.END)
            self.entries[key].insert(0, val)
        self._edit_iid = iid

    def _update(self):
        if not self._edit_iid:
            return
        try:
            study = self.entries["study"].get().strip()
            events = int(self.entries["events"].get())
            total = int(self.entries["total"].get())
            group = self.entries["group"].get().strip() or "All"
        except Exception:
            messagebox.showerror("Input error", "Please enter valid numeric values")
            return
        self.tree.item(self._edit_iid, values=("✔", study, events, total, group))
        self._edit_iid = None
        for e in self.entries.values():
            e.delete(0, tk.END)

    def _delete(self):
        iid = self.tree.focus()
        if iid:
            self.tree.delete(iid)

    def _import(self):
        path = filedialog.askopenfilename(filetypes=[("CSV", "*.csv")])
        if not path:
            return
        df = pd.read_csv(path)
        for _, r in df.iterrows():
            self.tree.insert("", tk.END, values=("✔", r["Study"], int(r["Events"]), int(r["Total"]), r.get("Group", "All")))

    def _export(self):
        path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV", "*.csv")])
        if not path:
            return
        rows = [
            {"Include": v[0], "Study": v[1], "Events": v[2], "Total": v[3], "Group": v[4]}
            for _, v in self._data_rows()
        ]
        pd.DataFrame(rows).to_csv(path, index=False)
        messagebox.showinfo("Export", f"Data saved to {path}")

    def _prepare(self):
        rows = [v for _, v in self._data_rows(included_only=True)]
        if len(rows) < 3:
            raise ValueError("Need ≥ 3 studies for the funnel plot")
        events, totals, labels, groups = zip(*[(r[2], r[3], r[1], r[4]) for r in rows])
        logit_vals, se_vals = zip(*[logit_cont(e, n) for e, n in zip(events, totals)])
        return np.array(logit_vals), np.array(se_vals), np.array(events), np.array(totals), list(labels), list(groups)

    def _funnel(self):
        try:
            eff, se, events, totals, labels, groups = self._prepare()
        except Exception as exc:
            messagebox.showerror("Error", str(exc))
            return

        # Fixed effects pooling for logit(p)
        w = 1 / se**2
        pooled = np.sum(w * eff) / np.sum(w)

        fig, ax = plt.subplots(figsize=(7, 7))
        se_line = np.linspace(0.01, se.max()*1.05, 200)
        zvals = [2.58, 1.96, 1.64]  # 99, 95, 90%
        for i, (z, grey, label) in enumerate(zip(zvals, self.GREYS, self.GREY_LABELS)):
            ax.fill_betweenx(se_line, pooled - z*se_line, pooled + z*se_line, color=grey, alpha=0.65, label=label)

        # Subgroup colouring
        groups_unique = list(dict.fromkeys(groups))
        group2col = {g: plt.cm.tab10(i % 10) for i, g in enumerate(groups_unique)}
        colors = [group2col[g] for g in groups]
        ax.scatter(eff, se, c=colors, edgecolor="black", zorder=10)
        for x, y, lab in zip(eff, se, labels):
            ax.annotate(lab, (x, y), xytext=(4, 0), textcoords="offset points", fontsize=7)

        ax.axvline(pooled, color="black", lw=1.2, label="Pooled (Fixed)")
        ax.set_xlabel("Effect size (logit(p))")
        ax.set_ylabel("Standard Error")
        ax.invert_yaxis()

        legend_elems = [
            plt.Line2D([0], [0], color=grey, lw=8, label=lab)
            for grey, lab in zip(self.GREYS, self.GREY_LABELS)
        ] + [
            plt.Line2D([0], [0], marker="o", color="w", markerfacecolor=group2col[g], label=g, markeredgecolor="k")
            for g in groups_unique
        ]
        ax.legend(handles=legend_elems, fontsize=8, loc="upper right", title="Legend")
        ax.set_title("Funnel plot (logit(p), single arm, fixed effects, subgroups)")
        plt.tight_layout()
        plt.show()
        self.last_fig = fig

        # Egger table
        try:
            eg = egger_test(eff, se)
            ci_low = eg['intercept'] - 1.96 * eg['se']
            ci_high = eg['intercept'] + 1.96 * eg['se']
            egger_df = pd.DataFrame({
                "Egger's Test": ["Intercept (β₀)", "Std Error", "t-value", "DF", "p-value", "95% CI"],
                "Value": [round(eg['intercept'], 4), round(eg['se'], 4), round(eg['t'], 4), int(eg['df']), f"{eg['p']:.4f}", f"[{ci_low:.4f}, {ci_high:.4f}]"]
            })
            self._show_table_popup(egger_df, title="Egger's Test Results")
        except Exception as e:
            messagebox.showerror("Egger Test", str(e))

        # Table of studies
        df = pd.DataFrame({
            "Study": labels, "Group": groups, "Events": events,
            "Total": totals, "logit(p)": np.round(eff, 4), "SE": np.round(se, 4),
        })
        self._show_table_popup(df, title="Included Studies")

    def _save_plot(self):
        if self.last_fig is None:
            messagebox.showerror("No Plot", "You must generate a funnel plot first.")
            return
        path = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[("PNG", "*.png")])
        if not path:
            return
        self.last_fig.savefig(path, dpi=300)
        messagebox.showinfo("Saved", f"Plot saved to {path}")

    def _show_table_popup(self, df, title="Table"):
        win = tk.Toplevel(self.frame)
        win.title(title)
        frame = ttk.Frame(win)
        frame.pack(fill="both", expand=True)
        tree = ttk.Treeview(frame, columns=list(df.columns), show="headings", height=min(15, len(df) + 1))
        for c in df.columns:
            tree.heading(c, text=c)
            tree.column(c, width=100, anchor="center")
        for row in df.values.tolist():
            tree.insert("", tk.END, values=row)
        tree.pack(fill="both", expand=True)
        ttk.Button(win, text="Close", command=win.destroy).pack(pady=5)
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import t

def logit_z(events, total):
    """Logit transformation with continuity correction (for 0 events or 0 non-events)."""
    p_adj = (events + 0.5) / (total + 1)
    logit_val = np.log(p_adj / (1 - p_adj))
    se = np.sqrt(1 / (events + 0.5) + 1 / (total - events + 0.5))
    return logit_val, se

def zscore(arr):
    mean = np.mean(arr)
    std = np.std(arr, ddof=1)
    if std == 0:
        return np.zeros_like(arr)
    return (arr - mean) / std

def egger_test(effects, ses):
    precision = 1 / ses
    std_eff = effects / ses
    X = np.vstack([precision, np.ones_like(precision)]).T
    beta, _, _, _ = np.linalg.lstsq(X, std_eff, rcond=None)
    intercept = beta[1]
    n = len(effects)
    res = std_eff - X @ beta
    df = n - 2
    mse = (res**2).sum() / df
    mean_prec = precision.mean()
    sxx = ((precision - mean_prec) ** 2).sum()
    se_int = np.sqrt(mse * (1/n + mean_prec**2 / sxx))
    t_stat = intercept / se_int
    p_val = 2 * (1 - t.cdf(abs(t_stat), df))
    return {"intercept": intercept, "se": se_int, "t": t_stat, "df": df, "p": p_val}

class SMDSingleArmZTab:
    GREYS = ["#D0D0D0", "#AAAAAA", "#787878"]  # 99%, 95%, 90%
    GREY_LABELS = ["IC 99%", "IC 95%", "IC 90%"]

    def __init__(self, notebook: ttk.Notebook):
        self.frame = ttk.Frame(notebook)
        notebook.add(self.frame, text="SMD Single Arm (z‑score logit(p))")
        self._build_gui()
        self._edit_iid = None
        self.last_fig = None

    def _build_gui(self):
        f = self.frame
        cols = ("✔", "Study", "Events", "Total", "Group")
        self.tree = ttk.Treeview(f, columns=cols, show="headings", height=12)
        for c in cols:
            self.tree.heading(c, text=c)
            self.tree.column(c, width=80, anchor="center")
        self.tree.grid(row=0, column=0, columnspan=8, padx=5, pady=5, sticky="nsew")
        self.tree.bind("<Double-1>", self._toggle_include)

        self.entries = {}
        for i, lab in enumerate(["Study", "Events", "Total", "Group"], start=1):
            ttk.Label(f, text=lab).grid(row=i, column=0, sticky="e")
            e = ttk.Entry(f, width=14)
            e.grid(row=i, column=1, sticky="w")
            self.entries[lab.lower()] = e

        btn_specs = [
            ("Add", self._add), ("Edit", self._edit), ("Update", self._update),
            ("Delete", self._delete), ("Import CSV", self._import),
            ("Export CSV", self._export), ("Show Funnel", self._funnel),
            ("Save Plot", self._save_plot)
        ]
        for i, (txt, cmd) in enumerate(btn_specs):
            ttk.Button(f, text=txt, command=cmd, width=12).grid(row=5, column=i, pady=4)

        for col in range(8):
            f.grid_columnconfigure(col, weight=1)
        f.grid_rowconfigure(0, weight=1)

    def _toggle_include(self, event):
        iid = self.tree.identify_row(event.y)
        if not iid:
            return
        cur = self.tree.set(iid, "✔")
        self.tree.set(iid, "✔", "" if cur == "✔" else "✔")

    def _data_rows(self, included_only=False):
        for iid in self.tree.get_children():
            vals = self.tree.item(iid)["values"]
            if included_only and vals[0] != "✔":
                continue
            yield iid, vals

    def _add(self):
        try:
            study = self.entries["study"].get().strip()
            events = int(self.entries["events"].get())
            total = int(self.entries["total"].get())
            group = self.entries["group"].get().strip() or "All"
        except Exception:
            messagebox.showerror("Input error", "Please enter valid numeric values")
            return
        self.tree.insert("", tk.END, values=("✔", study, events, total, group))
        for e in self.entries.values():
            e.delete(0, tk.END)

    def _edit(self):
        iid = self.tree.focus()
        if not iid:
            return
        vals = self.tree.item(iid)["values"][1:]
        for key, val in zip(["study", "events", "total", "group"], vals):
            self.entries[key].delete(0, tk.END)
            self.entries[key].insert(0, val)
        self._edit_iid = iid

    def _update(self):
        if not self._edit_iid:
            return
        try:
            study = self.entries["study"].get().strip()
            events = int(self.entries["events"].get())
            total = int(self.entries["total"].get())
            group = self.entries["group"].get().strip() or "All"
        except Exception:
            messagebox.showerror("Input error", "Please enter valid numeric values")
            return
        self.tree.item(self._edit_iid, values=("✔", study, events, total, group))
        self._edit_iid = None
        for e in self.entries.values():
            e.delete(0, tk.END)

    def _delete(self):
        iid = self.tree.focus()
        if iid:
            self.tree.delete(iid)

    def _import(self):
        path = filedialog.askopenfilename(filetypes=[("CSV", "*.csv")])
        if not path:
            return
        df = pd.read_csv(path)
        for _, r in df.iterrows():
            self.tree.insert("", tk.END, values=("✔", r["Study"], int(r["Events"]), int(r["Total"]), r.get("Group", "All")))

    def _export(self):
        path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV", "*.csv")])
        if not path:
            return
        rows = [
            {"Include": v[0], "Study": v[1], "Events": v[2], "Total": v[3], "Group": v[4]}
            for _, v in self._data_rows()
        ]
        pd.DataFrame(rows).to_csv(path, index=False)
        messagebox.showinfo("Export", f"Data saved to {path}")

    def _prepare(self):
        rows = [v for _, v in self._data_rows(included_only=True)]
        if len(rows) < 3:
            raise ValueError("Need ≥ 3 studies for the funnel plot")
        events, totals, labels, groups = zip(*[(r[2], r[3], r[1], r[4]) for r in rows])
        logit_vals, se_vals = zip(*[logit_z(e, n) for e, n in zip(events, totals)])
        smd_vals = zscore(np.array(logit_vals))
        return np.array(smd_vals), np.array(se_vals), np.array(events), np.array(totals), list(labels), list(groups), np.array(logit_vals)

    def _funnel(self):
        try:
            smd, se, events, totals, labels, groups, logit_vals = self._prepare()
        except Exception as exc:
            messagebox.showerror("Error", str(exc))
            return

        # Fixed effects pooling for SMD (weighted mean)
        w = 1 / se**2
        pooled = np.sum(w * smd) / np.sum(w)

        fig, ax = plt.subplots(figsize=(7, 7))
        se_line = np.linspace(0.01, se.max()*1.05, 200)
        zvals = [2.58, 1.96, 1.64]  # 99, 95, 90%
        for i, (z, grey, label) in enumerate(zip(zvals, self.GREYS, self.GREY_LABELS)):
            ax.fill_betweenx(se_line, pooled - z*se_line, pooled + z*se_line, color=grey, alpha=0.65, label=label)

        # Subgroup colouring
        groups_unique = list(dict.fromkeys(groups))
        group2col = {g: plt.cm.tab10(i % 10) for i, g in enumerate(groups_unique)}
        colors = [group2col[g] for g in groups]
        ax.scatter(smd, se, c=colors, edgecolor="black", zorder=10)
        for x, y, lab in zip(smd, se, labels):
            ax.annotate(lab, (x, y), xytext=(4, 0), textcoords="offset points", fontsize=7)

        ax.axvline(pooled, color="black", lw=1.2, label="Pooled (Fixed)")
        ax.set_xlabel("SMD (z-score logit(p))")
        ax.set_ylabel("Standard Error")
        ax.invert_yaxis()

        legend_elems = [
            plt.Line2D([0], [0], color=grey, lw=8, label=lab)
            for grey, lab in zip(self.GREYS, self.GREY_LABELS)
        ] + [
            plt.Line2D([0], [0], marker="o", color="w", markerfacecolor=group2col[g], label=g, markeredgecolor="k")
            for g in groups_unique
        ]
        ax.legend(handles=legend_elems, fontsize=8, loc="upper right", title="Legend")
        ax.set_title("Funnel plot (SMD, z-score logit(p), single arm, fixed effects, subgroups)")
        plt.tight_layout()
        plt.show()
        self.last_fig = fig

        # Egger table
        try:
            eg = egger_test(smd, se)
            ci_low = eg['intercept'] - 1.96 * eg['se']
            ci_high = eg['intercept'] + 1.96 * eg['se']
            egger_df = pd.DataFrame({
                "Egger's Test": ["Intercept (β₀)", "Std Error", "t-value", "DF", "p-value", "95% CI"],
                "Value": [round(eg['intercept'], 4), round(eg['se'], 4), round(eg['t'], 4), int(eg['df']), f"{eg['p']:.4f}", f"[{ci_low:.4f}, {ci_high:.4f}]"]
            })
            self._show_table_popup(egger_df, title="Egger's Test Results")
        except Exception as e:
            messagebox.showerror("Egger Test", str(e))

        # Table of studies
        df = pd.DataFrame({
            "Study": labels, "Group": groups, "Events": events,
            "Total": totals, "logit(p)": np.round(logit_vals, 4), "SMD (z-score)": np.round(smd, 4), "SE": np.round(se, 4),
        })
        self._show_table_popup(df, title="Included Studies")

    def _save_plot(self):
        if self.last_fig is None:
            messagebox.showerror("No Plot", "You must generate a funnel plot first.")
            return
        path = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[("PNG", "*.png")])
        if not path:
            return
        self.last_fig.savefig(path, dpi=300)
        messagebox.showinfo("Saved", f"Plot saved to {path}")

    def _show_table_popup(self, df, title="Table"):
        win = tk.Toplevel(self.frame)
        win.title(title)
        frame = ttk.Frame(win)
        frame.pack(fill="both", expand=True)
        tree = ttk.Treeview(frame, columns=list(df.columns), show="headings", height=min(15, len(df) + 1))
        for c in df.columns:
            tree.heading(c, text=c)
            tree.column(c, width=100, anchor="center")
        for row in df.values.tolist():
            tree.insert("", tk.END, values=row)
        tree.pack(fill="both", expand=True)
        ttk.Button(win, text="Close", command=win.destroy).pack(pady=5)

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import t

def egger_test(effects, ses):
    precision = 1 / ses
    std_eff = effects / ses
    X = np.vstack([precision, np.ones_like(precision)]).T
    beta, _, _, _ = np.linalg.lstsq(X, std_eff, rcond=None)
    intercept = beta[1]
    n = len(effects)
    res = std_eff - X @ beta
    df = n - 2
    mse = (res**2).sum() / df
    mean_prec = precision.mean()
    sxx = ((precision - mean_prec) ** 2).sum()
    se_int = np.sqrt(mse * (1/n + mean_prec**2 / sxx))
    t_stat = intercept / se_int
    p_val = 2 * (1 - t.cdf(abs(t_stat), df))
    return {"intercept": intercept, "se": se_int, "t": t_stat, "df": df, "p": p_val}

class MDContinuousSingleArmTab:
    GREYS = ["#D0D0D0", "#AAAAAA", "#787878"]  # 99%, 95%, 90%
    GREY_LABELS = ["IC 99%", "IC 95%", "IC 90%"]

    def __init__(self, notebook: ttk.Notebook):
        self.frame = ttk.Frame(notebook)
        notebook.add(self.frame, text="MD Single Arm (Continuous)")
        self._build_gui()
        self._edit_iid = None
        self.last_fig = None

    def _build_gui(self):
        f = self.frame
        cols = ("✔", "Study", "Mean", "SD", "N", "Group")
        self.tree = ttk.Treeview(f, columns=cols, show="headings", height=12)
        for c in cols:
            self.tree.heading(c, text=c)
            self.tree.column(c, width=90 if c=="Study" else 75, anchor="center")
        self.tree.grid(row=0, column=0, columnspan=8, padx=5, pady=5, sticky="nsew")
        self.tree.bind("<Double-1>", self._toggle_include)

        self.entries = {}
        for i, lab in enumerate(["Study", "Mean", "SD", "N", "Group"], start=1):
            ttk.Label(f, text=lab).grid(row=i, column=0, sticky="e")
            e = ttk.Entry(f, width=14)
            e.grid(row=i, column=1, sticky="w")
            self.entries[lab.lower()] = e

        btn_specs = [
            ("Add", self._add), ("Edit", self._edit), ("Update", self._update),
            ("Delete", self._delete), ("Import CSV", self._import),
            ("Export CSV", self._export), ("Show Funnel", self._funnel),
            ("Save Plot", self._save_plot)
        ]
        for i, (txt, cmd) in enumerate(btn_specs):
            ttk.Button(f, text=txt, command=cmd, width=12).grid(row=6, column=i, pady=4)

        for col in range(8):
            f.grid_columnconfigure(col, weight=1)
        f.grid_rowconfigure(0, weight=1)

    def _toggle_include(self, event):
        iid = self.tree.identify_row(event.y)
        if not iid:
            return
        cur = self.tree.set(iid, "✔")
        self.tree.set(iid, "✔", "" if cur == "✔" else "✔")

    def _data_rows(self, included_only=False):
        for iid in self.tree.get_children():
            vals = self.tree.item(iid)["values"]
            if included_only and vals[0] != "✔":
                continue
            yield iid, vals

    def _add(self):
        try:
            study = self.entries["study"].get().strip()
            mean = float(self.entries["mean"].get())
            sd = float(self.entries["sd"].get())
            n = int(self.entries["n"].get())
            group = self.entries["group"].get().strip() or "All"
        except Exception:
            messagebox.showerror("Input error", "Please enter valid numeric values")
            return
        self.tree.insert("", tk.END, values=("✔", study, mean, sd, n, group))
        for e in self.entries.values():
            e.delete(0, tk.END)

    def _edit(self):
        iid = self.tree.focus()
        if not iid:
            return
        vals = self.tree.item(iid)["values"][1:]
        for key, val in zip(["study", "mean", "sd", "n", "group"], vals):
            self.entries[key].delete(0, tk.END)
            self.entries[key].insert(0, val)
        self._edit_iid = iid

    def _update(self):
        if not self._edit_iid:
            return
        try:
            study = self.entries["study"].get().strip()
            mean = float(self.entries["mean"].get())
            sd = float(self.entries["sd"].get())
            n = int(self.entries["n"].get())
            group = self.entries["group"].get().strip() or "All"
        except Exception:
            messagebox.showerror("Input error", "Please enter valid numeric values")
            return
        self.tree.item(self._edit_iid, values=("✔", study, mean, sd, n, group))
        self._edit_iid = None
        for e in self.entries.values():
            e.delete(0, tk.END)

    def _delete(self):
        iid = self.tree.focus()
        if iid:
            self.tree.delete(iid)

    def _import(self):
        path = filedialog.askopenfilename(filetypes=[("CSV", "*.csv")])
        if not path:
            return
        df = pd.read_csv(path)
        for _, r in df.iterrows():
            self.tree.insert(
                "", tk.END,
                values=("✔", r["Study"], float(r["Mean"]), float(r["SD"]), int(r["N"]), r.get("Group", "All"))
            )

    def _export(self):
        path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV", "*.csv")])
        if not path:
            return
        rows = [
            {"Include": v[0], "Study": v[1], "Mean": v[2], "SD": v[3], "N": v[4], "Group": v[5]}
            for _, v in self._data_rows()
        ]
        pd.DataFrame(rows).to_csv(path, index=False)
        messagebox.showinfo("Export", f"Data saved to {path}")

    def _prepare(self):
        rows = [v for _, v in self._data_rows(included_only=True)]
        if len(rows) < 3:
            raise ValueError("Need ≥ 3 studies for the funnel plot")
        mean, sd, n, labels, groups = zip(*[(r[2], r[3], r[4], r[1], r[5]) for r in rows])
        mean = np.array(mean, dtype=float)
        sd = np.array(sd, dtype=float)
        n = np.array(n, dtype=float)
        se = sd / np.sqrt(n)
        return mean, se, sd, n, list(labels), list(groups)

    def _funnel(self):
        try:
            md, se, sd, n, labels, groups = self._prepare()
        except Exception as exc:
            messagebox.showerror("Error", str(exc))
            return

        # Fixed effects pooling for MD
        w = 1 / se**2
        pooled = np.sum(w * md) / np.sum(w)

        fig, ax = plt.subplots(figsize=(7, 7))
        se_line = np.linspace(0.01, se.max()*1.05, 200)
        zvals = [2.58, 1.96, 1.64]  # 99, 95, 90%
        for i, (z, grey, label) in enumerate(zip(self.GREYS, zvals, self.GREY_LABELS)):
            ax.fill_betweenx(se_line, pooled - z*se_line, pooled + z*se_line, color=grey, alpha=0.65, label=label)

        # Subgroup colouring
        groups_unique = list(dict.fromkeys(groups))
        group2col = {g: plt.cm.tab10(i % 10) for i, g in enumerate(groups_unique)}
        colors = [group2col[g] for g in groups]
        ax.scatter(md, se, c=colors, edgecolor="black", zorder=10)
        for x, y, lab in zip(md, se, labels):
            ax.annotate(lab, (x, y), xytext=(4, 0), textcoords="offset points", fontsize=7)

        ax.axvline(pooled, color="black", lw=1.2, label="Pooled (Fixed)")
        ax.set_xlabel("Mean (MD)")
        ax.set_ylabel("Standard Error")
        ax.invert_yaxis()

        legend_elems = [
            plt.Line2D([0], [0], color=grey, lw=8, label=lab)
            for grey, lab in zip(self.GREYS, self.GREY_LABELS)
        ] + [
            plt.Line2D([0], [0], marker="o", color="w", markerfacecolor=group2col[g], label=g, markeredgecolor="k")
            for g in groups_unique
        ]
        ax.legend(handles=legend_elems, fontsize=8, loc="upper right", title="Legend")
        ax.set_title("Funnel plot (mean, single arm, fixed effects, subgroups)")
        plt.tight_layout()
        plt.show()
        self.last_fig = fig

        # Egger table
        try:
            eg = egger_test(md, se)
            ci_low = eg['intercept'] - 1.96 * eg['se']
            ci_high = eg['intercept'] + 1.96 * eg['se']
            egger_df = pd.DataFrame({
                "Egger's Test": ["Intercept (β₀)", "Std Error", "t-value", "DF", "p-value", "95% CI"],
                "Value": [round(eg['intercept'], 4), round(eg['se'], 4), round(eg['t'], 4), int(eg['df']), f"{eg['p']:.4f}", f"[{ci_low:.4f}, {ci_high:.4f}]"]
            })
            self._show_table_popup(egger_df, title="Egger's Test Results")
        except Exception as e:
            messagebox.showerror("Egger Test", str(e))

        # Table of studies
        df = pd.DataFrame({
            "Study": labels, "Group": groups, "Mean": np.round(md, 4), "SD": np.round(sd, 4),
            "N": n.astype(int), "SE": np.round(se, 4),
        })
        self._show_table_popup(df, title="Included Studies")

    def _save_plot(self):
        if self.last_fig is None:
            messagebox.showerror("No Plot", "You must generate a funnel plot first.")
            return
        path = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[("PNG", "*.png")])
        if not path:
            return
        self.last_fig.savefig(path, dpi=300)
        messagebox.showinfo("Saved", f"Plot saved to {path}")

    def _show_table_popup(self, df, title="Table"):
        win = tk.Toplevel(self.frame)
        win.title(title)
        frame = ttk.Frame(win)
        frame.pack(fill="both", expand=True)
        tree = ttk.Treeview(frame, columns=list(df.columns), show="headings", height=min(15, len(df) + 1))
        for c in df.columns:
            tree.heading(c, text=c)
            tree.column(c, width=100, anchor="center")
        for row in df.values.tolist():
            tree.insert("", tk.END, values=row)
        tree.pack(fill="both", expand=True)
        ttk.Button(win, text="Close", command=win.destroy).pack(pady=5)

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import t

def single_arm_smd(mean, sd):
    """Calcola SMD per singolo braccio (mean/sd)."""
    if np.any(sd == 0):
        smd = np.where(sd == 0, 0, mean/sd)  # evitiamo div/0
    else:
        smd = mean / sd
    return smd

def single_arm_smd_se(n):
    """Errore standard approssimato per SMD single-arm (mean/sd)."""
    # Formula approssimativa (v. Wan 2014): se = sqrt((1/n) + (smd^2/(2n)))
    # Ma di solito per SMD single-arm, se = sqrt(1/n) (approssimazione robusta)
    return np.sqrt(1 / n)

def egger_test(effects, ses):
    precision = 1 / ses
    std_eff = effects / ses
    X = np.vstack([precision, np.ones_like(precision)]).T
    beta, _, _, _ = np.linalg.lstsq(X, std_eff, rcond=None)
    intercept = beta[1]
    n = len(effects)
    res = std_eff - X @ beta
    df = n - 2
    mse = (res**2).sum() / df
    mean_prec = precision.mean()
    sxx = ((precision - mean_prec) ** 2).sum()
    se_int = np.sqrt(mse * (1/n + mean_prec**2 / sxx))
    t_stat = intercept / se_int
    p_val = 2 * (1 - t.cdf(abs(t_stat), df))
    return {"intercept": intercept, "se": se_int, "t": t_stat, "df": df, "p": p_val}

class SMDSingleArmContTab:
    GREYS = ["#D0D0D0", "#AAAAAA", "#787878"]  # 99%, 95%, 90%
    GREY_LABELS = ["IC 99%", "IC 95%", "IC 90%"]

    def __init__(self, notebook: ttk.Notebook):
        self.frame = ttk.Frame(notebook)
        notebook.add(self.frame, text="SMD Single Arm (Continuous)")
        self._build_gui()
        self._edit_iid = None
        self.last_fig = None

    def _build_gui(self):
        f = self.frame
        cols = ("✔", "Study", "Mean", "SD", "N", "Group")
        self.tree = ttk.Treeview(f, columns=cols, show="headings", height=12)
        for c in cols:
            self.tree.heading(c, text=c)
            self.tree.column(c, width=90 if c=="Study" else 75, anchor="center")
        self.tree.grid(row=0, column=0, columnspan=8, padx=5, pady=5, sticky="nsew")
        self.tree.bind("<Double-1>", self._toggle_include)

        self.entries = {}
        for i, lab in enumerate(["Study", "Mean", "SD", "N", "Group"], start=1):
            ttk.Label(f, text=lab).grid(row=i, column=0, sticky="e")
            e = ttk.Entry(f, width=14)
            e.grid(row=i, column=1, sticky="w")
            self.entries[lab.lower()] = e

        btn_specs = [
            ("Add", self._add), ("Edit", self._edit), ("Update", self._update),
            ("Delete", self._delete), ("Import CSV", self._import),
            ("Export CSV", self._export), ("Show Funnel", self._funnel),
            ("Save Plot", self._save_plot)
        ]
        for i, (txt, cmd) in enumerate(btn_specs):
            ttk.Button(f, text=txt, command=cmd, width=12).grid(row=6, column=i, pady=4)

        for col in range(8):
            f.grid_columnconfigure(col, weight=1)
        f.grid_rowconfigure(0, weight=1)

    def _toggle_include(self, event):
        iid = self.tree.identify_row(event.y)
        if not iid:
            return
        cur = self.tree.set(iid, "✔")
        self.tree.set(iid, "✔", "" if cur == "✔" else "✔")

    def _data_rows(self, included_only=False):
        for iid in self.tree.get_children():
            vals = self.tree.item(iid)["values"]
            if included_only and vals[0] != "✔":
                continue
            yield iid, vals

    def _add(self):
        try:
            study = self.entries["study"].get().strip()
            mean = float(self.entries["mean"].get())
            sd = float(self.entries["sd"].get())
            n = int(self.entries["n"].get())
            group = self.entries["group"].get().strip() or "All"
        except Exception:
            messagebox.showerror("Input error", "Please enter valid numeric values")
            return
        self.tree.insert("", tk.END, values=("✔", study, mean, sd, n, group))
        for e in self.entries.values():
            e.delete(0, tk.END)

    def _edit(self):
        iid = self.tree.focus()
        if not iid:
            return
        vals = self.tree.item(iid)["values"][1:]
        for key, val in zip(["study", "mean", "sd", "n", "group"], vals):
            self.entries[key].delete(0, tk.END)
            self.entries[key].insert(0, val)
        self._edit_iid = iid

    def _update(self):
        if not self._edit_iid:
            return
        try:
            study = self.entries["study"].get().strip()
            mean = float(self.entries["mean"].get())
            sd = float(self.entries["sd"].get())
            n = int(self.entries["n"].get())
            group = self.entries["group"].get().strip() or "All"
        except Exception:
            messagebox.showerror("Input error", "Please enter valid numeric values")
            return
        self.tree.item(self._edit_iid, values=("✔", study, mean, sd, n, group))
        self._edit_iid = None
        for e in self.entries.values():
            e.delete(0, tk.END)

    def _delete(self):
        iid = self.tree.focus()
        if iid:
            self.tree.delete(iid)

    def _import(self):
        path = filedialog.askopenfilename(filetypes=[("CSV", "*.csv")])
        if not path:
            return
        df = pd.read_csv(path)
        for _, r in df.iterrows():
            self.tree.insert(
                "", tk.END,
                values=("✔", r["Study"], float(r["Mean"]), float(r["SD"]), int(r["N"]), r.get("Group", "All"))
            )

    def _export(self):
        path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV", "*.csv")])
        if not path:
            return
        rows = [
            {"Include": v[0], "Study": v[1], "Mean": v[2], "SD": v[3], "N": v[4], "Group": v[5]}
            for _, v in self._data_rows()
        ]
        pd.DataFrame(rows).to_csv(path, index=False)
        messagebox.showinfo("Export", f"Data saved to {path}")

    def _prepare(self):
        rows = [v for _, v in self._data_rows(included_only=True)]
        if len(rows) < 3:
            raise ValueError("Need ≥ 3 studies for the funnel plot")
        mean, sd, n, labels, groups = zip(*[(r[2], r[3], r[4], r[1], r[5]) for r in rows])
        mean = np.array(mean, dtype=float)
        sd = np.array(sd, dtype=float)
        n = np.array(n, dtype=float)
        smd = single_arm_smd(mean, sd)
        se = single_arm_smd_se(n)
        return smd, se, mean, sd, n, list(labels), list(groups)

    def _funnel(self):
        try:
            smd, se, mean, sd, n, labels, groups = self._prepare()
        except Exception as exc:
            messagebox.showerror("Error", str(exc))
            return

        # Fixed effects pooling for SMD
        w = 1 / se**2
        pooled = np.sum(w * smd) / np.sum(w)

        fig, ax = plt.subplots(figsize=(7, 7))
        se_line = np.linspace(0.01, se.max()*1.05, 200)
        zvals = [2.58, 1.96, 1.64]  # 99, 95, 90%
        for i, (z, grey, label) in enumerate(zip(zvals, self.GREYS, self.GREY_LABELS)):
            ax.fill_betweenx(se_line, pooled - z*se_line, pooled + z*se_line, color=grey, alpha=0.65, label=label)

        # Subgroup colouring
        groups_unique = list(dict.fromkeys(groups))
        group2col = {g: plt.cm.tab10(i % 10) for i, g in enumerate(groups_unique)}
        colors = [group2col[g] for g in groups]
        ax.scatter(smd, se, c=colors, edgecolor="black", zorder=10)
        for x, y, lab in zip(smd, se, labels):
            ax.annotate(lab, (x, y), xytext=(4, 0), textcoords="offset points", fontsize=7)

        ax.axvline(pooled, color="black", lw=1.2, label="Pooled (Fixed)")
        ax.set_xlabel("SMD (Mean/SD, single arm)")
        ax.set_ylabel("Standard Error")
        ax.invert_yaxis()

        legend_elems = [
            plt.Line2D([0], [0], color=grey, lw=8, label=lab)
            for grey, lab in zip(self.GREYS, self.GREY_LABELS)
        ] + [
            plt.Line2D([0], [0], marker="o", color="w", markerfacecolor=group2col[g], label=g, markeredgecolor="k")
            for g in groups_unique
        ]
        ax.legend(handles=legend_elems, fontsize=8, loc="upper right", title="Legend")
        ax.set_title("Funnel plot (SMD, single arm, fixed effects, subgroups)")
        plt.tight_layout()
        plt.show()
        self.last_fig = fig

        # Egger table
        try:
            eg = egger_test(smd, se)
            ci_low = eg['intercept'] - 1.96 * eg['se']
            ci_high = eg['intercept'] + 1.96 * eg['se']
            egger_df = pd.DataFrame({
                "Egger's Test": ["Intercept (β₀)", "Std Error", "t-value", "DF", "p-value", "95% CI"],
                "Value": [round(eg['intercept'], 4), round(eg['se'], 4), round(eg['t'], 4), int(eg['df']), f"{eg['p']:.4f}", f"[{ci_low:.4f}, {ci_high:.4f}]"]
            })
            self._show_table_popup(egger_df, title="Egger's Test Results")
        except Exception as e:
            messagebox.showerror("Egger Test", str(e))

        # Table of studies
        df = pd.DataFrame({
            "Study": labels, "Group": groups, "Mean": np.round(mean, 4), "SD": np.round(sd, 4),
            "N": n.astype(int), "SMD": np.round(smd, 4), "SE": np.round(se, 4),
        })
        self._show_table_popup(df, title="Included Studies")

    def _save_plot(self):
        if self.last_fig is None:
            messagebox.showerror("No Plot", "You must generate a funnel plot first.")
            return
        path = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[("PNG", "*.png")])
        if not path:
            return
        self.last_fig.savefig(path, dpi=300)
        messagebox.showinfo("Saved", f"Plot saved to {path}")

    def _show_table_popup(self, df, title="Table"):
        win = tk.Toplevel(self.frame)
        win.title(title)
        frame = ttk.Frame(win)
        frame.pack(fill="both", expand=True)
        tree = ttk.Treeview(frame, columns=list(df.columns), show="headings", height=min(15, len(df) + 1))
        for c in df.columns:
            tree.heading(c, text=c)
            tree.column(c, width=100, anchor="center")
        for row in df.values.tolist():
            tree.insert("", tk.END, values=row)
        tree.pack(fill="both", expand=True)
        ttk.Button(win, text="Close", command=win.destroy).pack(pady=5)
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import t

def calc_md(mean_a, sd_a, n_a, mean_b, sd_b, n_b):
    md = mean_a - mean_b
    se = np.sqrt(sd_a**2/n_a + sd_b**2/n_b)
    return md, se


def calc_smd(mean_a, sd_a, n_a, mean_b, sd_b, n_b):
    sd_pooled = np.sqrt(((n_a-1)*sd_a**2 + (n_b-1)*sd_b**2) / (n_a+n_b-2))
    smd = np.where(sd_pooled > 0, (mean_a - mean_b) / sd_pooled, 0)
    # Hedges' correction for SMD
    J = 1 - (3/(4*(n_a+n_b-2)-1))
    smd = smd * J
    se = np.sqrt((n_a + n_b)/(n_a*n_b) + (smd**2)/(2*(n_a+n_b)))
    return smd, se


def egger_test(effects, ses):
    precision = 1 / ses
    std_eff = effects / ses
    X = np.vstack([precision, np.ones_like(precision)]).T
    beta, _, _, _ = np.linalg.lstsq(X, std_eff, rcond=None)
    intercept = beta[1]
    n = len(effects)
    res = std_eff - X @ beta
    df = n - 2
    mse = (res**2).sum() / df
    mean_prec = precision.mean()
    sxx = ((precision - mean_prec) ** 2).sum()
    se_int = np.sqrt(mse * (1/n + mean_prec**2 / sxx))
    t_stat = intercept / se_int
    p_val = 2 * (1 - t.cdf(abs(t_stat), df))
    return {"intercept": intercept, "se": se_int, "t": t_stat, "df": df, "p": p_val}

class DoubleArmContTab:
    GREYS = ["#D0D0D0", "#AAAAAA", "#787878"]  # 99%, 95%, 90%
    GREY_LABELS = ["IC 99%", "IC 95%", "IC 90%"]

    def __init__(self, notebook: ttk.Notebook):
        self.frame = ttk.Frame(notebook)
        notebook.add(self.frame, text="MD/SMD Double Arm")
        self._build_gui()
        self._edit_iid = None
        self.last_fig = None

    def _build_gui(self):
        f = self.frame
        # Setup metric selector
        self.metric_var = tk.StringVar(value="MD")
        ttk.Label(f, text="Metric:").grid(row=0, column=0, sticky="e")
        ttk.OptionMenu(f, self.metric_var, "MD", "MD", "SMD").grid(row=0, column=1, sticky="w", pady=2)

        cols = ("✔", "Study", "Mean A", "SD A", "N A", "Mean B", "SD B", "N B", "Group")
        self.tree = ttk.Treeview(f, columns=cols, show="headings", height=12)
        for i, c in enumerate(cols):
            self.tree.heading(c, text=c)
            self.tree.column(c, width=80, anchor="center")
        self.tree.grid(row=1, column=0, columnspan=10, padx=5, pady=5, sticky="nsew")
        self.tree.bind("<Double-1>", self._toggle_include)

        self.entries = {}
        entry_labels = ["Study", "Mean A", "SD A", "N A", "Mean B", "SD B", "N B", "Group"]
        for i, lab in enumerate(entry_labels, start=2):
            ttk.Label(f, text=lab).grid(row=i, column=0, sticky="e")
            e = ttk.Entry(f, width=12)
            e.grid(row=i, column=1, sticky="w")
            self.entries[lab.lower().replace(" ", "_")] = e

        btn_specs = [
            ("Add", self._add), ("Edit", self._edit), ("Update", self._update),
            ("Delete", self._delete), ("Import CSV", self._import),
            ("Export CSV", self._export), ("Show Funnel", self._funnel),
            ("Save Plot", self._save_plot)
        ]
        for i, (txt, cmd) in enumerate(btn_specs):
            ttk.Button(f, text=txt, command=cmd, width=12).grid(row=10, column=i, pady=4)

        for col in range(10):
            f.grid_columnconfigure(col, weight=1)
        f.grid_rowconfigure(1, weight=1)

    def _toggle_include(self, event):
        iid = self.tree.identify_row(event.y)
        if not iid:
            return
        cur = self.tree.set(iid, "✔")
        self.tree.set(iid, "✔", "" if cur == "✔" else "✔")

    def _data_rows(self, included_only=False):
        for iid in self.tree.get_children():
            vals = self.tree.item(iid)["values"]
            if included_only and vals[0] != "✔":
                continue
            yield iid, vals

    def _add(self):
        try:
            study = self.entries["study"].get().strip()
            mean_a = float(self.entries["mean_a"].get())
            sd_a = float(self.entries["sd_a"].get())
            n_a = int(self.entries["n_a"].get())
            mean_b = float(self.entries["mean_b"].get())
            sd_b = float(self.entries["sd_b"].get())
            n_b = int(self.entries["n_b"].get())
            group = self.entries["group"].get().strip() or "All"
        except Exception:
            messagebox.showerror("Input error", "Please enter valid numeric values")
            return
        self.tree.insert("", tk.END, values=("✔", study, mean_a, sd_a, n_a, mean_b, sd_b, n_b, group))
        for e in self.entries.values():
            e.delete(0, tk.END)

    def _edit(self):
        iid = self.tree.focus()
        if not iid:
            return
        vals = self.tree.item(iid)["values"][1:]
        keys = ["study", "mean_a", "sd_a", "n_a", "mean_b", "sd_b", "n_b", "group"]
        for key, val in zip(keys, vals):
            self.entries[key].delete(0, tk.END)
            self.entries[key].insert(0, val)
        self._edit_iid = iid

    def _update(self):
        if not self._edit_iid:
            return
        try:
            study = self.entries["study"].get().strip()
            mean_a = float(self.entries["mean_a"].get())
            sd_a = float(self.entries["sd_a"].get())
            n_a = int(self.entries["n_a"].get())
            mean_b = float(self.entries["mean_b"].get())
            sd_b = float(self.entries["sd_b"].get())
            n_b = int(self.entries["n_b"].get())
            group = self.entries["group"].get().strip() or "All"
        except Exception:
            messagebox.showerror("Input error", "Please enter valid numeric values")
            return
        self.tree.item(self._edit_iid, values=("✔", study, mean_a, sd_a, n_a, mean_b, sd_b, n_b, group))
        self._edit_iid = None
        for e in self.entries.values():
            e.delete(0, tk.END)

    def _delete(self):
        iid = self.tree.focus()
        if iid:
            self.tree.delete(iid)

    def _import(self):
        path = filedialog.askopenfilename(filetypes=[("CSV", "*.csv")])
        if not path:
            return
        df = pd.read_csv(path)
        for _, r in df.iterrows():
            self.tree.insert(
                "", tk.END,
                values=("✔", r["Study"], float(r["Mean A"]), float(r["SD A"]), int(r["N A"]),
                        float(r["Mean B"]), float(r["SD B"]), int(r["N B"]), r.get("Group", "All"))
            )

    def _export(self):
        path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV", "*.csv")])
        if not path:
            return
        rows = [
            {"Include": v[0], "Study": v[1], "Mean A": v[2], "SD A": v[3], "N A": v[4],
             "Mean B": v[5], "SD B": v[6], "N B": v[7], "Group": v[8]}
            for _, v in self._data_rows()
        ]
        pd.DataFrame(rows).to_csv(path, index=False)
        messagebox.showinfo("Export", f"Data saved to {path}")

    def _prepare(self):
        rows = [v for _, v in self._data_rows(included_only=True)]
        if len(rows) < 3:
            raise ValueError("Need ≥ 3 studies for the funnel plot")
        mean_a, sd_a, n_a, mean_b, sd_b, n_b, labels, groups = zip(*[
            (r[2], r[3], r[4], r[5], r[6], r[7], r[1], r[8]) for r in rows])
        mean_a = np.array(mean_a, dtype=float)
        sd_a = np.array(sd_a, dtype=float)
        n_a = np.array(n_a, dtype=float)
        mean_b = np.array(mean_b, dtype=float)
        sd_b = np.array(sd_b, dtype=float)
        n_b = np.array(n_b, dtype=float)
        return mean_a, sd_a, n_a, mean_b, sd_b, n_b, list(labels), list(groups)

    def _funnel(self):
        try:
            mean_a, sd_a, n_a, mean_b, sd_b, n_b, labels, groups = self._prepare()
        except Exception as exc:
            messagebox.showerror("Error", str(exc))
            return

        metric = self.metric_var.get()
        if metric == "MD":
            effects, ses = calc_md(mean_a, sd_a, n_a, mean_b, sd_b, n_b)
            xlabel = "Mean Difference (A - B)"
        else:
            effects, ses = calc_smd(mean_a, sd_a, n_a, mean_b, sd_b, n_b)
            xlabel = "Standardised Mean Difference (SMD, Hedges’ g)"

        # Fixed effects pooled effect
        w = 1 / ses**2
        pooled = np.sum(w * effects) / np.sum(w)

        fig, ax = plt.subplots(figsize=(7, 7))
        se_line = np.linspace(0.01, ses.max()*1.05, 200)
        zvals = [2.58, 1.96, 1.64]  # 99, 95, 90%
        for i, (z, grey, label) in enumerate(zip(zvals, self.GREYS, self.GREY_LABELS)):
            ax.fill_betweenx(se_line, pooled - z*se_line, pooled + z*se_line, color=grey, alpha=0.65, label=label)

        # Subgroup colouring
        groups_unique = list(dict.fromkeys(groups))
        group2col = {g: plt.cm.tab10(i % 10) for i, g in enumerate(groups_unique)}
        colors = [group2col[g] for g in groups]
        ax.scatter(effects, ses, c=colors, edgecolor="black", zorder=10)
        for x, y, lab in zip(effects, ses, labels):
            ax.annotate(lab, (x, y), xytext=(4, 0), textcoords="offset points", fontsize=7)

        ax.axvline(pooled, color="black", lw=1.2, label="Pooled (Fixed)")
        ax.set_xlabel(xlabel)
        ax.set_ylabel("Standard Error")
        ax.invert_yaxis()

        legend_elems = [
            plt.Line2D([0], [0], color=grey, lw=8, label=lab)
            for grey, lab in zip(self.GREYS, self.GREY_LABELS)
        ] + [
            plt.Line2D([0], [0], marker="o", color="w", markerfacecolor=group2col[g], label=g, markeredgecolor="k")
            for g in groups_unique
        ]
        ax.legend(handles=legend_elems, fontsize=8, loc="upper right", title="Legend")
        ax.set_title(f"Funnel plot ({metric}, double arm, fixed effects, subgroups)")
        plt.tight_layout()
        plt.show()
        self.last_fig = fig

        # Egger table
        try:
            eg = egger_test(effects, ses)
            ci_low = eg['intercept'] - 1.96 * eg['se']
            ci_high = eg['intercept'] + 1.96 * eg['se']
            egger_df = pd.DataFrame({
                "Egger's Test": ["Intercept (β₀)", "Std Error", "t-value", "DF", "p-value", "95% CI"],
                "Value": [round(eg['intercept'], 4), round(eg['se'], 4), round(eg['t'], 4), int(eg['df']), f"{eg['p']:.4f}", f"[{ci_low:.4f}, {ci_high:.4f}]"]
            })
            self._show_table_popup(egger_df, title="Egger's Test Results")
        except Exception as e:
            messagebox.showerror("Egger Test", str(e))

        # Table of studies
        df = pd.DataFrame({
            "Study": labels, "Group": groups,
            "Mean A": mean_a, "SD A": sd_a, "N A": n_a.astype(int),
            "Mean B": mean_b, "SD B": sd_b, "N B": n_b.astype(int),
            metric: np.round(effects, 4), "SE": np.round(ses, 4)
        })
        self._show_table_popup(df, title="Included Studies")

    def _save_plot(self):
        if self.last_fig is None:
            messagebox.showerror("No Plot", "You must generate a funnel plot first.")
            return
        path = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[("PNG", "*.png")])
        if not path:
            return
        self.last_fig.savefig(path, dpi=300)
        messagebox.showinfo("Saved", f"Plot saved to {path}")

    def _show_table_popup(self, df, title="Table"):
        win = tk.Toplevel(self.frame)
        win.title(title)
        frame = ttk.Frame(win)
        frame.pack(fill="both", expand=True)
        tree = ttk.Treeview(frame, columns=list(df.columns), show="headings", height=min(15, len(df) + 1))
        for c in df.columns:
            tree.heading(c, text=c)
            tree.column(c, width=100, anchor="center")
        for row in df.values.tolist():
            tree.insert("", tk.END, values=row)
        tree.pack(fill="both", expand=True)
        ttk.Button(win, text="Close", command=win.destroy).pack(pady=5)

class CombinedApp:
    """Main container that wires together all requested tabs."""
    def __init__(self, root: tk.Tk):
        root.title("Funnel Plot single arm et Al.")
        root.geometry("850x450") 
        nb = ttk.Notebook(root)
        nb.pack(fill="both", expand=True)
        EnhancedFunnelPlotTab(nb)
        LogitSingleArmTab (nb)
        SMDSingleArmZTab (nb)
        MDContinuousSingleArmTab (nb)
        SMDSingleArmContTab (nb)
        DoubleArmContTab(nb)
        ReadmeTab(nb)

        # Status bar
        ttk.Label(root,
                  text="Mario Dioguardi – build 1.3 (03/06/2025)",
                  anchor="e", relief="sunken").pack(fill="x", side="bottom")


# ── Entrypoint ─────────────────────────────────────────────────────────────
if __name__ == "__main__":
    root = tk.Tk()
    CombinedApp(root)
    root.mainloop()


