{"spec_id":"swimmer-clinical-timeline","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nswimmer-clinical-timeline: Swimmer Plot for Clinical Trial Timelines\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-06-08\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\n\n\nLetsPlot.setup_html()\n\n# Theme tokens — see prompts/default-style-guide.md\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# Imprint palette — hybrid-v3 sort order\nIMPRINT_PALETTE = [\n    \"#009E73\",  # 1 brand green\n    \"#C475FD\",  # 2 lavender\n    \"#4467A3\",  # 3 blue\n    \"#BD8233\",  # 4 ochre\n    \"#AE3030\",  # 5 matte red (semantic: bad/loss/progression)\n    \"#2ABCCD\",  # 6 cyan\n    \"#954477\",  # 7 rose\n    \"#99B314\",  # 8 lime\n]\n\n# Data — Simulated Phase II Oncology Trial (25 patients, 2 treatment 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)\"], n_patients, p=[0.52, 0.48])\ndurations = np.round(np.random.gamma(shape=4, scale=5, size=n_patients), 1)\ndurations = np.clip(durations, 3, 48)\n\nevents_list = []\nfor i in range(n_patients):\n    pid = patient_ids[i]\n    dur = durations[i]\n\n    if np.random.rand() < 0.65:\n        t = np.round(np.random.uniform(2, min(dur * 0.5, 12)), 1)\n        events_list.append({\"patient_id\": pid, \"time\": t, \"event_type\": \"Partial Response\"})\n\n        cr_upper = min(dur * 0.8, dur - 1)\n        if np.random.rand() < 0.35 and cr_upper > t + 2:\n            t_cr = np.round(np.random.uniform(t + 2, cr_upper), 1)\n            events_list.append({\"patient_id\": pid, \"time\": t_cr, \"event_type\": \"Complete Response\"})\n\n    if np.random.rand() < 0.4:\n        t_pd = np.round(np.random.uniform(dur * 0.5, dur), 1)\n        events_list.append({\"patient_id\": pid, \"time\": t_pd, \"event_type\": \"Progressive Disease\"})\n\n    if np.random.rand() < 0.3:\n        events_list.append({\"patient_id\": pid, \"time\": dur, \"event_type\": \"Ongoing\"})\n\nevents_df = pd.DataFrame(events_list)\n\n# Sort patients by duration (shortest at bottom, longest at top)\nbar_df = pd.DataFrame({\"patient_id\": patient_ids, \"duration\": durations, \"arm\": arms})\nbar_df = bar_df.sort_values(\"duration\", ascending=True).reset_index(drop=True)\nbar_df[\"y_pos\"] = range(len(bar_df))\n\n# Map y positions to events\ny_map = dict(zip(bar_df[\"patient_id\"], bar_df[\"y_pos\"]))\nevents_df[\"y_pos\"] = events_df[\"patient_id\"].map(y_map)\n\n# Median reference line\nmedian_duration = float(np.median(durations))\n\n# Bar geometry helpers\nbar_df[\"y_min\"] = bar_df[\"y_pos\"] - 0.35\nbar_df[\"y_max\"] = bar_df[\"y_pos\"] + 0.35\nbar_df[\"x_min\"] = 0.0\n\n# Complete responders — for highlight bands and best-CR annotation\ncr_patients = set(events_df[events_df[\"event_type\"] == \"Complete Response\"][\"patient_id\"])\nbar_df[\"has_cr\"] = bar_df[\"patient_id\"].isin(cr_patients)\ncr_bar = bar_df[bar_df[\"has_cr\"]].sort_values(\"duration\", ascending=False)\nbest_responder = cr_bar.iloc[0] if len(cr_bar) > 0 else None\n\n# Ongoing patients — separate arrow segments (spec: arrow = still on study)\nongoing_pids = set(events_df[events_df[\"event_type\"] == \"Ongoing\"][\"patient_id\"])\nongoing_df = bar_df[bar_df[\"patient_id\"].isin(ongoing_pids)][[\"y_pos\", \"duration\"]].copy()\nongoing_df[\"x_end\"] = ongoing_df[\"duration\"] + 2.5\nongoing_df[\"event_type\"] = \"Ongoing\"\n\n# Non-ongoing events for point markers\npoint_events_df = events_df[events_df[\"event_type\"] != \"Ongoing\"].copy()\n\n# Median annotation\nmedian_label_df = pd.DataFrame(\n    {\"x\": [median_duration + 0.5], \"y\": [-0.65], \"label\": [f\"Median: {median_duration:.0f}w\"]}\n)\n\ntitle = \"swimmer-clinical-timeline · python · letsplot · anyplot.ai\"\n\n# Plot\nplot = (\n    ggplot()\n    # Subtle CR highlight bands using Imprint green at low alpha\n    + geom_rect(\n        aes(xmin=\"x_min\", xmax=\"duration\", ymin=\"y_min\", ymax=\"y_max\"),\n        data=bar_df[bar_df[\"has_cr\"]],\n        fill=IMPRINT_PALETTE[0],\n        alpha=0.12,\n    )\n    # Treatment duration bars — fill by arm with interactive tooltips (lets-plot feature)\n    + geom_rect(\n        aes(xmin=\"x_min\", xmax=\"duration\", ymin=\"y_min\", ymax=\"y_max\", fill=\"arm\"),\n        data=bar_df,\n        alpha=0.8,\n        tooltips=layer_tooltips().line(\"@patient_id\").line(\"Arm: @arm\").line(\"Duration: @duration wks\"),\n    )\n    # Median reference line\n    + geom_vline(xintercept=median_duration, color=INK_MUTED, linetype=\"dashed\", size=0.6)\n    # Clinical event markers with interactive tooltips (lets-plot feature)\n    + geom_point(\n        aes(x=\"time\", y=\"y_pos\", color=\"event_type\", shape=\"event_type\"),\n        data=point_events_df,\n        size=4,\n        stroke=1.0,\n        tooltips=layer_tooltips().line(\"@patient_id\").line(\"@event_type\").line(\"Week @time\"),\n    )\n    # Median label annotation\n    + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=median_label_df, size=3.5, hjust=0, color=INK_MUTED)\n    # Arm fill scale — Imprint positions 1 & 2 (first series always #009E73)\n    + scale_fill_manual(\n        name=\"Treatment Arm\", values={\"Arm A (Combo)\": IMPRINT_PALETTE[0], \"Arm B (Mono)\": IMPRINT_PALETTE[1]}\n    )\n    # Event color scale — semantic mapping: red for progression (bad outcome), cyan for ongoing\n    + scale_color_manual(\n        name=\"Clinical Event\",\n        values={\n            \"Partial Response\": IMPRINT_PALETTE[3],\n            \"Complete Response\": IMPRINT_PALETTE[2],\n            \"Progressive Disease\": IMPRINT_PALETTE[4],\n            \"Ongoing\": IMPRINT_PALETTE[5],\n        },\n    )\n    + scale_shape_manual(\n        name=\"Clinical Event\", values={\"Partial Response\": 17, \"Complete Response\": 8, \"Progressive Disease\": 18}\n    )\n    + scale_y_continuous(breaks=list(bar_df[\"y_pos\"]), labels=list(bar_df[\"patient_id\"]), expand=[0.03, 0.05])\n    + scale_x_continuous(name=\"Time on Study (Weeks)\", expand=[0.01, 0.08])\n    + labs(title=title, y=\"Patient\")\n    + theme_minimal()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        plot_title=element_text(size=16, face=\"bold\", color=INK),\n        axis_title=element_text(size=12, color=INK),\n        axis_text_x=element_text(size=10, color=INK_SOFT),\n        axis_text_y=element_text(size=8, color=INK_SOFT),\n        legend_title=element_text(size=11, face=\"bold\", color=INK),\n        legend_text=element_text(size=10, color=INK_SOFT),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_position=\"right\",\n        panel_grid_major_x=element_line(color=INK_SOFT, size=0.25),\n        panel_grid_major_y=element_blank(),\n        panel_grid_minor=element_blank(),\n        axis_line=element_line(color=INK_SOFT),\n        panel_border=element_rect(color=INK_SOFT, size=0.4),\n    )\n    + ggsize(800, 450)\n)\n\n# Ongoing patient arrows — mapped via color=\"event_type\" so \"Ongoing\" appears in the legend\nif len(ongoing_df) > 0:\n    plot = plot + geom_segment(\n        aes(x=\"duration\", xend=\"x_end\", y=\"y_pos\", yend=\"y_pos\", color=\"event_type\"),\n        data=ongoing_df,\n        size=1.5,\n        arrow=arrow(type=\"closed\", angle=20, length=6),\n    )\n\n# Best CR annotation — geom_label gives a boxed callout for visual prominence\nif best_responder is not None:\n    plot = plot + geom_label(\n        aes(x=\"x\", y=\"y\", label=\"label\"),\n        data=pd.DataFrame(\n            {\n                \"x\": [float(best_responder[\"duration\"]) * 0.5],\n                \"y\": [float(best_responder[\"y_pos\"]) + 0.6],\n                \"label\": [\"Best CR\"],\n            }\n        ),\n        size=4,\n        hjust=0.5,\n        color=IMPRINT_PALETTE[2],\n        fill=ELEVATED_BG,\n        fontface=\"bold\",\n        label_size=0.5,\n    )\n\n# Save — theme-suffixed as required by pipeline; path=\".\" writes to current dir\nggsave(plot, f\"plot-{THEME}.png\", scale=4, path=\".\")\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n\nif os.path.exists(\"lets-plot-images\"):\n    import shutil\n\n    shutil.rmtree(\"lets-plot-images\")\n"}