{"spec_id":"swimmer-clinical-timeline","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nswimmer-clinical-timeline: Swimmer Plot for Clinical Trial Timelines\nLibrary: plotly 6.8.0 | Python 3.13.13\nQuality: 88/100 | Created: 2026-06-08\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\n\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\"\nGRID = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\n# Imprint palette — first series always #009E73 (brand green)\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nANYPLOT_AMBER = \"#DDCC77\"  # adverse events / warning\n\nARM_COLORS = {\"Arm A\": IMPRINT_PALETTE[0], \"Arm B\": IMPRINT_PALETTE[1]}\n\nEVENT_CONFIG = {\n    \"partial_response\": {\"symbol\": \"triangle-up\", \"color\": IMPRINT_PALETTE[5], \"name\": \"Partial Response\"},\n    \"complete_response\": {\"symbol\": \"star\", \"color\": IMPRINT_PALETTE[2], \"name\": \"Complete Response\"},\n    \"progressive_disease\": {\"symbol\": \"diamond\", \"color\": IMPRINT_PALETTE[4], \"name\": \"Progressive Disease\"},\n    \"adverse_event\": {\"symbol\": \"circle\", \"color\": ANYPLOT_AMBER, \"name\": \"Adverse Event\"},\n}\n\n# Data — simulated Phase II oncology trial, 25 patients across two treatment arms\nnp.random.seed(42)\n\nn_patients = 25\npatient_ids = [f\"PT-{i:03d}\" for i in range(1, n_patients + 1)]\narm_labels = [\"Arm A\"] * 12 + [\"Arm B\"] * 13\n\ndurations = np.concatenate([np.random.uniform(8, 52, 12), np.random.uniform(4, 48, 13)])\nongoing_flags = np.random.choice([True, False], n_patients, p=[0.3, 0.7])\n\nevent_type_keys = list(EVENT_CONFIG.keys())\npatient_events = []\nfor i in range(n_patients):\n    dur = durations[i]\n    n_ev = np.random.randint(1, 4)\n    evs = []\n    for _ in range(n_ev):\n        et = event_type_keys[np.random.randint(len(event_type_keys))]\n        t = np.random.uniform(1.0, max(dur * 0.85, 1.5))\n        evs.append((et, t))\n    patient_events.append(evs)\n\n# Sort ascending by duration — plotly categorical y-axis places first entry at bottom,\n# last at top, so ascending sort puts the longest-duration patient at the top\nsort_idx = np.argsort(durations)\nsorted_ids = [patient_ids[i] for i in sort_idx]\nsorted_arms = [arm_labels[i] for i in sort_idx]\nsorted_durs = [float(durations[i]) for i in sort_idx]\nsorted_ongoing = [bool(ongoing_flags[i]) for i in sort_idx]\nsorted_events = [patient_events[i] for i in sort_idx]\n\nmedian_dur = float(np.median(sorted_durs))\n\n# Plot\nfig = go.Figure()\n\n# Patient duration bars — each bar colored by treatment arm\nbar_colors = [ARM_COLORS[arm] for arm in sorted_arms]\nfig.add_trace(\n    go.Bar(\n        x=sorted_durs,\n        y=sorted_ids,\n        orientation=\"h\",\n        marker={\"color\": bar_colors, \"opacity\": 0.75, \"line\": {\"width\": 0}},\n        width=0.65,\n        showlegend=False,\n        hovertemplate=\"%{y}: %{x:.1f} wk<extra></extra>\",\n    )\n)\n\n# Dummy bar traces for treatment arm legend entries\nfor arm, color in ARM_COLORS.items():\n    fig.add_trace(go.Bar(x=[None], y=[None], orientation=\"h\", name=arm, marker={\"color\": color, \"opacity\": 0.75}))\n\n# Clinical event markers — one scatter trace per event type; size=11 reduces visual clutter\nfor et_key, config in EVENT_CONFIG.items():\n    ex, ey = [], []\n    for j, evs in enumerate(sorted_events):\n        for et, t in evs:\n            if et == et_key:\n                ex.append(t)\n                ey.append(sorted_ids[j])\n    if ex:\n        fig.add_trace(\n            go.Scatter(\n                x=ex,\n                y=ey,\n                mode=\"markers\",\n                name=config[\"name\"],\n                marker={\"symbol\": config[\"symbol\"], \"size\": 11, \"color\": config[\"color\"], \"line\": {\"color\": INK, \"width\": 1.0}},\n                hovertemplate=f\"{config['name']}: %{{x:.1f}} wk<extra></extra>\",\n            )\n        )\n\n# Ongoing patients: right-pointing triangle placed just beyond bar end\nong_x = [sorted_durs[j] + 0.4 for j in range(n_patients) if sorted_ongoing[j]]\nong_y = [sorted_ids[j] for j in range(n_patients) if sorted_ongoing[j]]\nif ong_x:\n    fig.add_trace(\n        go.Scatter(\n            x=ong_x,\n            y=ong_y,\n            mode=\"markers\",\n            name=\"Ongoing\",\n            marker={\"symbol\": \"triangle-right\", \"size\": 12, \"color\": INK, \"line\": {\"color\": INK, \"width\": 0.5}},\n            hovertemplate=\"Still on study<extra></extra>\",\n        )\n    )\n\n# Median duration reference line — data storytelling focal point\nfig.add_vline(\n    x=median_dur,\n    line={\"color\": INK_MUTED, \"width\": 1.5, \"dash\": \"dot\"},\n    annotation_text=f\"Median {median_dur:.1f} wk\",\n    annotation_position=\"top right\",\n    annotation_font={\"size\": 9, \"color\": INK_MUTED},\n    annotation_bgcolor=ELEVATED_BG,\n    annotation_bordercolor=INK_MUTED,\n    annotation_borderwidth=1,\n    annotation_borderpad=3,\n)\n\n# Style\ntitle = \"swimmer-clinical-timeline · python · plotly · anyplot.ai\"\n\nfig.update_layout(\n    autosize=False,\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    barmode=\"overlay\",\n    title={\"text\": title, \"font\": {\"size\": 16, \"color\": INK}, \"x\": 0.5, \"xanchor\": \"center\"},\n    xaxis={\n        \"title\": {\"text\": \"Weeks on Study\", \"font\": {\"size\": 12, \"color\": INK}},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"gridcolor\": GRID,\n        \"linecolor\": INK_SOFT,\n        \"zerolinecolor\": INK_SOFT,\n        \"showgrid\": True,\n        \"showline\": True,\n        \"mirror\": False,\n        \"range\": [0, 57],\n    },\n    yaxis={\n        \"title\": {\"text\": \"Patient ID\", \"font\": {\"size\": 12, \"color\": INK}},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"linecolor\": INK_SOFT,\n        \"showgrid\": False,\n        \"showline\": True,\n        \"mirror\": False,\n        \"tickmode\": \"array\",\n        \"tickvals\": sorted_ids,\n        \"ticktext\": sorted_ids,\n    },\n    legend={\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n        \"font\": {\"size\": 10, \"color\": INK_SOFT},\n        \"x\": 1.02,\n        \"xanchor\": \"left\",\n        \"y\": 1.0,\n        \"yanchor\": \"top\",\n    },\n    margin={\"l\": 90, \"r\": 170, \"t\": 80, \"b\": 60},\n)\n\n# Save\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}