{"spec_id":"swimmer-clinical-timeline","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nswimmer-clinical-timeline: Swimmer Plot for Clinical Trial Timelines\nLibrary: plotnine 0.15.5 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-06-08\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    annotate,\n    coord_cartesian,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_point,\n    geom_segment,\n    geom_vline,\n    ggplot,\n    guide_legend,\n    guides,\n    labs,\n    scale_color_manual,\n    scale_fill_manual,\n    scale_shape_manual,\n    scale_x_continuous,\n    scale_y_discrete,\n    theme,\n    theme_minimal,\n)\n\n\n# Theme tokens — Imprint palette, theme-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\n# Data — Simulated Phase II oncology trial with 25 patients across two arms\nnp.random.seed(42)\n\nn_patients = 25\npatient_ids = [f\"PT-{i + 1:03d}\" for i in range(n_patients)]\narms = np.random.choice([\"Arm A (Combo)\", \"Arm B (Mono)\"], size=n_patients, p=[0.52, 0.48])\ndurations = np.round(np.random.uniform(4, 48, size=n_patients), 1)\ndurations = np.sort(durations)[::-1]\nongoing = np.random.choice([True, False], size=n_patients, p=[0.24, 0.76])\n\nbar_df = pd.DataFrame({\"patient_id\": patient_ids, \"duration\": durations, \"arm\": arms, \"ongoing\": ongoing})\nbar_df = bar_df.sort_values(\"duration\", ascending=True).reset_index(drop=True)\nbar_df[\"patient_id\"] = pd.Categorical(bar_df[\"patient_id\"], categories=bar_df[\"patient_id\"].tolist(), ordered=True)\n\nevents = []\nfor _, row in bar_df.iterrows():\n    pid = row[\"patient_id\"]\n    dur = row[\"duration\"]\n    if np.random.random() < 0.60:\n        t = np.round(np.random.uniform(4, min(dur * 0.5, 16)), 1)\n        events.append({\"patient_id\": pid, \"time\": t, \"event_type\": \"Partial Response\"})\n    if np.random.random() < 0.25:\n        t = np.round(np.random.uniform(min(dur * 0.3, 12), min(dur * 0.7, 30)), 1)\n        events.append({\"patient_id\": pid, \"time\": t, \"event_type\": \"Complete Response\"})\n    if np.random.random() < 0.35:\n        t = np.round(np.random.uniform(dur * 0.5, dur * 0.95), 1)\n        events.append({\"patient_id\": pid, \"time\": t, \"event_type\": \"Progressive Disease\"})\n    if row[\"ongoing\"]:\n        events.append({\"patient_id\": pid, \"time\": dur, \"event_type\": \"Ongoing\"})\n\nevents_df = pd.DataFrame(events)\nevents_df[\"patient_id\"] = pd.Categorical(\n    events_df[\"patient_id\"], categories=bar_df[\"patient_id\"].tolist(), ordered=True\n)\n\n# Summary statistics for storytelling annotations\nmedian_a = bar_df.loc[bar_df[\"arm\"] == \"Arm A (Combo)\", \"duration\"].median()\nmedian_b = bar_df.loc[bar_df[\"arm\"] == \"Arm B (Mono)\", \"duration\"].median()\nn_responders = events_df[events_df[\"event_type\"].isin([\"Partial Response\", \"Complete Response\"])][\n    \"patient_id\"\n].nunique()\nresponse_rate = n_responders / n_patients * 100\n\n# Imprint palette — arms use positions 1-2, events use positions 3/4/5 + muted anchor\narm_colors = {\n    \"Arm A (Combo)\": \"#009E73\",  # Imprint position 1 — brand green\n    \"Arm B (Mono)\": \"#C475FD\",  # Imprint position 2 — lavender\n}\nevent_colors = {\n    \"Partial Response\": \"#4467A3\",  # Imprint position 3 — blue\n    \"Complete Response\": \"#BD8233\",  # Imprint position 4 — ochre\n    \"Progressive Disease\": \"#AE3030\",  # Imprint position 5 — matte red (semantic: bad outcome)\n    \"Ongoing\": INK_MUTED,  # semantic anchor — neutral/ongoing\n}\nevent_shapes = {\"Partial Response\": \"^\", \"Complete Response\": \"D\", \"Progressive Disease\": \"s\", \"Ongoing\": \">\"}\n\ntitle = \"swimmer-clinical-timeline · python · plotnine · anyplot.ai\"\n\n# Plot\nplot = (\n    ggplot()\n    # Median reference lines for storytelling\n    + geom_vline(xintercept=median_a, linetype=\"dashed\", color=arm_colors[\"Arm A (Combo)\"], alpha=0.45, size=0.5)\n    + geom_vline(xintercept=median_b, linetype=\"dotted\", color=arm_colors[\"Arm B (Mono)\"], alpha=0.45, size=0.5)\n    # Patient bars colored by treatment arm\n    + geom_segment(\n        data=bar_df,\n        mapping=aes(x=0, xend=\"duration\", y=\"patient_id\", yend=\"patient_id\", color=\"arm\"),\n        size=4,\n        lineend=\"butt\",\n    )\n    # Clinical event markers — white stroke for contrast against bars\n    + geom_point(\n        data=events_df,\n        mapping=aes(x=\"time\", y=\"patient_id\", shape=\"event_type\", fill=\"event_type\"),\n        size=3,\n        color=\"white\",\n        stroke=0.5,\n    )\n    + scale_color_manual(values=arm_colors, name=\"Treatment Arm\")\n    + scale_shape_manual(values=event_shapes, name=\"Clinical Event\")\n    + scale_fill_manual(values=event_colors, name=\"Clinical Event\")\n    + scale_y_discrete(limits=bar_df[\"patient_id\"].tolist())\n    + scale_x_continuous(breaks=range(0, 55, 6))\n    + coord_cartesian(xlim=(0, max(durations) + 2))\n    + guides(\n        color=guide_legend(order=1, override_aes={\"size\": 4}),\n        shape=guide_legend(order=2, override_aes={\"size\": 3, \"stroke\": 0.3}),\n        fill=guide_legend(order=2),\n    )\n    # Median annotations — colored text, theme-adaptive fill\n    + annotate(\n        \"label\",\n        x=median_a + 0.5,\n        y=2,\n        label=f\"Median A: {median_a:.0f}w\",\n        size=2.5,\n        color=arm_colors[\"Arm A (Combo)\"],\n        fill=ELEVATED_BG,\n        fontweight=\"bold\",\n        ha=\"left\",\n        va=\"center\",\n        label_padding=0.3,\n    )\n    + annotate(\n        \"label\",\n        x=median_b + 0.5,\n        y=4,\n        label=f\"Median B: {median_b:.0f}w\",\n        size=2.5,\n        color=arm_colors[\"Arm B (Mono)\"],\n        fill=ELEVATED_BG,\n        fontweight=\"bold\",\n        ha=\"left\",\n        va=\"center\",\n        label_padding=0.3,\n    )\n    + annotate(\n        \"label\",\n        x=max(durations) - 1,\n        y=n_patients - 1,\n        label=f\"ORR: {response_rate:.0f}% ({n_responders}/{n_patients})\",\n        size=2.8,\n        color=arm_colors[\"Arm A (Combo)\"],\n        fill=ELEVATED_BG,\n        ha=\"right\",\n        va=\"top\",\n        alpha=0.95,\n        label_padding=0.5,\n    )\n    + labs(title=title, x=\"Time on Study (weeks)\", y=\"Patient ID\")\n    + theme_minimal()\n    + theme(\n        figure_size=(8, 4.5),\n        plot_title=element_text(size=12, weight=\"bold\", color=INK, margin={\"b\": 8}),\n        axis_title_x=element_text(size=10, color=INK, margin={\"t\": 6}),\n        axis_title_y=element_text(size=10, color=INK, margin={\"r\": 6}),\n        axis_text_x=element_text(size=8, color=INK_SOFT),\n        axis_text_y=element_text(size=9, color=INK_SOFT, family=\"monospace\"),\n        legend_title=element_text(size=8, weight=\"bold\", color=INK),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_position=\"right\",\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT, size=0.3),\n        legend_key=element_rect(fill=PAGE_BG, color=\"none\"),\n        legend_key_size=12,\n        panel_grid_major_y=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_grid_major_x=element_line(color=INK, size=0.2, alpha=0.12),\n        panel_border=element_blank(),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n    )\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\")\n"}