{"spec_id":"swimmer-clinical-timeline","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nswimmer-clinical-timeline: Swimmer Plot for Clinical Trial Timelines\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 88/100 | Created: 2026-06-08\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent implementations/python/matplotlib.py from shadowing the installed\n# matplotlib package when the script is run from its own directory.\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if p not in (\"\", _here)]\n\nimport matplotlib.patches as mpatches\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nfrom matplotlib.lines import Line2D\n\n\n# Theme tokens — Imprint palette + adaptive chrome\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nANYPLOT_AMBER = \"#DDCC77\"\n\nARM_COLORS = {\n    \"Arm A (Standard)\": IMPRINT_PALETTE[0],  # brand green — first series\n    \"Arm B (Experimental)\": IMPRINT_PALETTE[1],  # lavender\n}\n\nEVENT_STYLES = {\n    \"partial_response\": {\"marker\": \"^\", \"color\": IMPRINT_PALETTE[3], \"size\": 90, \"label\": \"Partial Response\"},\n    \"complete_response\": {\"marker\": \"*\", \"color\": IMPRINT_PALETTE[2], \"size\": 150, \"label\": \"Complete Response\"},\n    \"progressive_disease\": {\"marker\": \"D\", \"color\": IMPRINT_PALETTE[4], \"size\": 75, \"label\": \"Progression\"},\n    \"adverse_event\": {\"marker\": \"X\", \"color\": ANYPLOT_AMBER, \"size\": 75, \"label\": \"Adverse Event\"},\n}\n\n# Data — Phase II oncology trial, 25 patients across two treatment arms\nnp.random.seed(42)\nn_patients = 25\narms = [\"Arm A (Standard)\"] * 13 + [\"Arm B (Experimental)\"] * 12\ndurations = np.concatenate([np.random.uniform(4, 22, 13), np.random.uniform(3, 18, 12)])\nongoing_mask = np.random.random(n_patients) < 0.28\n\npatient_ids = [f\"PT-{i + 1:03d}\" for i in range(n_patients)]\n\n# Sort longest bar at top (highest y index)\norder = np.argsort(durations)[::-1]\nsorted_ids = [patient_ids[i] for i in order]\nsorted_dur = durations[order]\nsorted_arms = [arms[i] for i in order]\nsorted_ongoing = ongoing_mask[order]\n\n# Generate clinical events for each patient\nevents = []\nevent_types = list(EVENT_STYLES.keys())\nevent_probs = [0.50, 0.30, 0.35, 0.25]\n\nfor idx, (dur, _arm, _ongoing) in enumerate(zip(sorted_dur, sorted_arms, sorted_ongoing, strict=False)):\n    for etype, prob in zip(event_types, event_probs, strict=False):\n        if np.random.random() < prob:\n            t = np.random.uniform(1.0, dur * 0.85)\n            events.append({\"patient_idx\": idx, \"time\": t, \"event_type\": etype})\n\nevents_df = pd.DataFrame(events) if events else pd.DataFrame(columns=[\"patient_idx\", \"time\", \"event_type\"])\n\n# Apply seaborn theme\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.15,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# Plot — landscape 3200×1800 px (8 in × 4.5 in @ 400 dpi)\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Horizontal bars\nbar_height = 0.55\nfor idx, (dur, arm, ongoing) in enumerate(zip(sorted_dur, sorted_arms, sorted_ongoing, strict=False)):\n    color = ARM_COLORS[arm]\n    ax.barh(idx, dur, height=bar_height, color=color, alpha=0.82, left=0, zorder=2)\n    if ongoing:\n        ax.annotate(\n            \"\",\n            xy=(dur + 0.95, idx),\n            xytext=(dur + 0.08, idx),\n            arrowprops={\"arrowstyle\": \"-|>\", \"color\": color, \"lw\": 1.6, \"mutation_scale\": 11},\n            zorder=3,\n        )\n\n# Event markers — sns.scatterplot for seaborn idiomatic usage\nfor etype, style in EVENT_STYLES.items():\n    mask = events_df[\"event_type\"] == etype\n    if mask.sum() > 0:\n        sns.scatterplot(\n            data=events_df[mask],\n            x=\"time\",\n            y=\"patient_idx\",\n            ax=ax,\n            marker=style[\"marker\"],\n            color=style[\"color\"],\n            s=style[\"size\"],\n            zorder=5,\n            edgecolor=PAGE_BG,\n            linewidth=0.5,\n            legend=False,\n        )\n\n# Axes styling\nax.set_yticks(range(n_patients))\nax.set_yticklabels(sorted_ids)\nax.set_xlabel(\"Time on Study (months)\", fontsize=10, color=INK)\nax.set_ylabel(\"\")\nax.set_xlim(0, sorted_dur.max() + 2.8)\nax.set_ylim(-0.65, n_patients - 0.35)\n\nax.tick_params(axis=\"x\", which=\"both\", length=0, labelsize=8, colors=INK_SOFT)\nax.tick_params(axis=\"y\", which=\"both\", length=0, labelsize=7.5, colors=INK_SOFT)\n\n# Title\ntitle = \"swimmer-clinical-timeline · python · seaborn · anyplot.ai\"\nax.set_title(title, fontsize=12, fontweight=\"medium\", color=INK, pad=8)\n\n# Spines\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nax.spines[\"left\"].set_color(INK_SOFT)\nax.spines[\"bottom\"].set_color(INK_SOFT)\n\n# Vertical grid only (x-axis), behind bars\nax.xaxis.grid(True, alpha=0.15, linewidth=0.6, color=INK, zorder=0)\nax.set_axisbelow(True)\n\n# Storytelling annotation — median duration comparison between arms\narm_a_med = np.median([sorted_dur[i] for i, arm in enumerate(sorted_arms) if \"Arm A\" in arm])\narm_b_med = np.median([sorted_dur[i] for i, arm in enumerate(sorted_arms) if \"Arm B\" in arm])\nax.text(\n    0.98,\n    0.97,\n    f\"Median duration — Arm A: {arm_a_med:.1f} mo  |  Arm B: {arm_b_med:.1f} mo\",\n    transform=ax.transAxes,\n    fontsize=7,\n    color=INK_MUTED,\n    ha=\"right\",\n    va=\"top\",\n    style=\"italic\",\n)\n\n# Legend — treatment arms + event types + ongoing indicator\narm_handles = [mpatches.Patch(color=ARM_COLORS[arm], alpha=0.82, label=arm) for arm in ARM_COLORS]\nevent_handles = [\n    Line2D(\n        [0],\n        [0],\n        marker=style[\"marker\"],\n        color=\"none\",\n        markerfacecolor=style[\"color\"],\n        markersize=7,\n        markeredgewidth=0.5,\n        markeredgecolor=PAGE_BG,\n        label=style[\"label\"],\n    )\n    for style in EVENT_STYLES.values()\n]\nongoing_handle = Line2D(\n    [0],\n    [0],\n    color=INK_SOFT,\n    marker=\">\",\n    markerfacecolor=INK_SOFT,\n    markersize=5,\n    linewidth=1.3,\n    label=\"Still on Treatment\",\n)\n\nlegend = ax.legend(\n    handles=arm_handles + event_handles + [ongoing_handle],\n    fontsize=7,\n    loc=\"lower right\",\n    ncol=2,\n    framealpha=0.92,\n    facecolor=ELEVATED_BG,\n    edgecolor=INK_SOFT,\n)\nlegend.get_frame().set_linewidth(0.5)\n\n# Layout — control padding without bbox_inches=\"tight\"\nfig.subplots_adjust(left=0.11, right=0.97, top=0.93, bottom=0.10)\n\n# Save\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\nplt.close(fig)\n"}