# (c) 2026 William J. Dwyer. Free and open-source software under the MIT License (see LICENSE).
# If you use or build upon this software, please retain this notice and cite the accompanying
# methodology paper together with the archived software (Zenodo DOI: 10.5281/zenodo.20709963).
"""CV-screen decision flowchart. Emits a color PNG, a grayscale PNG, and a color SVG from one
layout. Updated to include the three-way eq-7 model check (log-normal vs gamma vs neither), which
routes to the log-scale t (geometric-mean ratio), a gamma GLM with a log link (arithmetic mean,
no back-transform bias), or a distribution-free backstop."""
import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch, Polygon, FancyArrowPatch
import matplotlib.patches as mp

INK = "#222222"

def palette(mode):
    if mode == "color":
        return dict(START="#e9eef3", PROC="#eef2f6", DEC="#e3ebf2", DEC_EC="#27496d",
                    RAW="#dbe7f3", RAW_EC="#27496d", PAR="#d9ead3", PAR_EC="#2e6b2e",
                    NP="#fce5cd", NP_EC="#9a6a2a", GAM="#e7dcf2", GAM_EC="#6a3d9a")
    # grayscale
    g = lambda v: (v, v, v)
    return dict(START="#e8e8e8", PROC="#f1f1f1", DEC="#e6e6e6", DEC_EC="#555555",
                RAW="#ededed", RAW_EC="#444444", PAR="#e3e3e3", PAR_EC="#444444",
                NP="#f0f0f0", NP_EC="#444444", GAM="#dcdcdc", GAM_EC="#444444")

def build(mode):
    P = palette(mode)
    fig, ax = plt.subplots(figsize=(10.2, 13.8))
    ax.set_xlim(0, 102); ax.set_ylim(0, 146); ax.axis("off")

    def box(x, y, w, h, text, fc, ec=INK, fs=10.5, bold=False):
        ax.add_patch(FancyBboxPatch((x-w/2, y-h/2), w, h,
            boxstyle="round,pad=0.3,rounding_size=1.2", fc=fc, ec=ec, lw=1.4, zorder=2))
        ax.text(x, y, text, ha="center", va="center", fontsize=fs, color=INK,
                fontweight="bold" if bold else "normal", zorder=3, linespacing=1.32)
    def diamond(x, y, w, h, text, fs=10):
        ax.add_patch(Polygon([(x, y+h/2), (x+w/2, y), (x, y-h/2), (x-w/2, y)],
            closed=True, fc=P["DEC"], ec=P["DEC_EC"], lw=1.5, zorder=2))
        ax.text(x, y, text, ha="center", va="center", fontsize=fs, color=INK, zorder=3, linespacing=1.28)
    def arrow(x1, y1, x2, y2, label=None, lx=None, ly=None, fs=9.3, rad=0.0):
        ax.add_patch(FancyArrowPatch((x1, y1), (x2, y2), arrowstyle="-|>", mutation_scale=15,
            lw=1.4, color=INK, connectionstyle=f"arc3,rad={rad}", zorder=1))
        if label:
            ax.text(lx if lx is not None else (x1+x2)/2, ly if ly is not None else (y1+y2)/2,
                    label, ha="center", va="center", fontsize=fs, color=INK, fontweight="bold",
                    bbox=dict(fc="white", ec="none", pad=1.0), zorder=4)

    # ---- nodes (centre spine at x=50) ----
    box(50, 138, 60, 8, "Two groups, positive continuous outcome\n(concentrations, costs, durations, biomarkers)\nfrom raw data or published summary statistics", P["START"], fs=10, bold=True)
    box(50, 127, 52, 6.2, "Compute  CV = SD / mean  for each group", P["PROC"], fs=11, bold=True)
    diamond(50, 114.5, 46, 16, "Any non-positive\nvalues?\n(differences, change scores)", fs=9.2)
    box(89, 114.5, 22, 11, "Cannot take logs:\nPermutation t-test\nor Mann–Whitney", P["NP"], ec=P["NP_EC"], fs=9.0)
    diamond(50, 96, 46, 15, "Either group\nCV ≥ ~0.5?", fs=10.5)
    box(12, 96, 20, 12, "Raw-scale\n(Welch) t-test\n\nReport difference\nin means", P["RAW"], ec=P["RAW_EC"], fs=9.0)
    box(50, 80, 60, 8.5, "Skew matters (by CV ≈ 1.0 the raw t-test is clearly deficient).\nLog-transform both groups.", P["PROC"], fs=9.7)

    # three-way eq-7 model check (enlarged diamond; gamma branch pushed clear to the right)
    diamond(50, 63, 50, 18, "Model check (eq-7):\nis  s$_y^2$  nearer\nln(1+CV$^2$)  or  ψ′(1/CV$^2$)?\n(outside both → bootstrap)", fs=9.0)
    box(90, 63, 22, 16, "Gamma GLM\n(log link):\nthe arithmetic mean\ndirectly — no\nback-transform bias", P["GAM"], ec=P["GAM_EC"], fs=8.4)

    diamond(50, 42, 54, 17, "Log-t and permutation-t\nagree?\n(logs adequately normal;\nShapiro–Wilk underpowered at n<30)", fs=8.8)
    box(20, 22, 31, 15, "Log-scale t-test (exact, UMPU)\nvariance model from the CV ratio:\n≈ 1 → pooled (Student); else Welch\n\nReport geometric-mean ratio;\npermutation-t confirms", P["PAR"], ec=P["PAR_EC"], fs=8.4)
    box(80, 22, 31, 15, "Parametric log-t unreliable:\nPermutation t-test and/or\nMann–Whitney (scale-invariant)\n\nReport Hodges–Lehmann shift\nor probability of dominance", P["NP"], ec=P["NP_EC"], fs=8.6)

    # ---- arrows ----
    arrow(50, 134, 50, 130.2)                                   # start -> compute
    arrow(50, 123.8, 50, 122.6)                                 # compute -> D1
    arrow(73, 114.5, 78, 114.5, "Yes", lx=75.5, ly=117.2)      # D1 -> cannot logs (clear of box)
    arrow(50, 106.5, 50, 103.6, "No", lx=53.4, ly=105.0)       # D1 -> D2
    arrow(27, 96, 22, 96, "No  (CV < 0.5)", lx=34, ly=99)      # D2 -> raw
    arrow(50, 88.5, 50, 84.3, "Yes  (CV ≥ 0.5)", lx=59.5, ly=86.4)  # D2 -> log-transform
    arrow(50, 75.7, 50, 72.1)                                   # log-transform -> model check
    arrow(75, 63, 79, 63, "gamma", lx=76.8, ly=65.6)           # model check -> gamma GLM (4-unit gap)
    arrow(50, 54, 50, 50.6, "log-normal", lx=59.5, ly=52.4)    # model check -> agree?
    arrow(34, 36, 26, 30, "Yes", lx=27, ly=34)                 # agree? -> log-scale t
    arrow(66, 36, 74, 30, "No", lx=73, ly=34)                  # agree? -> non-parametric

    ax.text(51, 9, "At the smallest samples the permutation / parametric log-t attains a resolution the rank test cannot:\n"
                   "a discrete rank statistic (e.g., 3 vs 3) cannot reach p < 0.05, whereas the log-scale t and its exact\n"
                   "permutation reference distribution can.  The eq-7 model check compares the observed log-variance against\n"
                   "the log-normal and gamma values implied by the CV; ‘outside both’ routes to a BCa bootstrap.",
            ha="center", va="center", fontsize=8.6, color="#444", style="italic", linespacing=1.4,
            bbox=dict(boxstyle="round,pad=0.5", fc="#fafafa", ec="#cccccc"))
    ax.text(51, 144, "Choosing and confirming a two-group test with the CV screen, ratio, and model check",
            ha="center", va="center", fontsize=13, fontweight="bold", color=INK)

    leg = [mp.Patch(fc=P["RAW"], ec=P["RAW_EC"], label="Raw-scale parametric"),
           mp.Patch(fc=P["PAR"], ec=P["PAR_EC"], label="Log-scale parametric"),
           mp.Patch(fc=P["GAM"], ec=P["GAM_EC"], label="Model-based (gamma GLM)"),
           mp.Patch(fc=P["NP"],  ec=P["NP_EC"],  label="Non-parametric / assumption-light")]
    ax.legend(handles=leg, loc="lower center", bbox_to_anchor=(0.5, -0.004), ncol=4,
              fontsize=8.3, frameon=False, handlelength=1.2)
    return fig

META = {"Author": "William J. Dwyer (ORCID 0009-0004-0855-7222)",
        "Subject": "Generated by fig_flowchart.py for the Comparable-Strata Designer; MIT License; cite Zenodo DOI 10.5281/zenodo.20709963"}
for mode, names in [("color", ["fig_flowchart_color.png", "fig_flowchart_color.svg"]),
                    ("gray",  ["fig_flowchart_gray.png"])]:
    fig = build(mode)
    for n in names:
        if n.endswith(".svg"):
            fig.savefig(n, bbox_inches="tight", metadata={"Title": n[:-4],
                        "Creator": "fig_flowchart.py (W. J. Dwyer; MIT; Zenodo 10.5281/zenodo.20709963)"})
        else:
            fig.savefig(n, dpi=200, bbox_inches="tight", metadata={**META, "Title": n[:-4]})
        print("saved", n)
    plt.close(fig)
