{"spec_id":"swimmer-clinical-timeline","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nswimmer-clinical-timeline: Swimmer Plot for Clinical Trial Timelines\nLibrary: altair 6.2.1 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-06-08\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove this script's directory from sys.path so 'import altair' finds the installed package\n_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _dir]\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\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# Imprint categorical palette — positions 1 & 2 for treatment arms\nARM_COLORS = [\"#009E73\", \"#C475FD\"]  # Arm A (Combo), Arm B (Mono)\n\n# Event colors with semantic Imprint palette matches\nevent_color_map = {\n    \"Partial Response\": \"#BD8233\",  # Imprint pos 4 — ochre, partial positive\n    \"Complete Response\": \"#2ABCCD\",  # Imprint pos 6 — cyan, strong positive\n    \"Progressive Disease\": \"#AE3030\",  # Imprint pos 5 — matte red, negative outcome\n    \"Ongoing\": INK_MUTED,  # theme-adaptive neutral\n}\nevent_shape_map = {\n    \"Partial Response\": \"triangle-up\",\n    \"Complete Response\": \"cross\",\n    \"Progressive Disease\": \"diamond\",\n    \"Ongoing\": \"triangle-right\",\n}\n\n# Data — simulated Phase II oncology trial, 25 patients, two 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.uniform(4, 48, n_patients), 1)\ndurations = np.sort(durations)[::-1]\nongoing_mask = np.random.choice([True, False], n_patients, p=[0.3, 0.7])\n\nevents_list = []\nfor i, pid in enumerate(patient_ids):\n    dur = durations[i]\n    if dur > 8:\n        pr_time = np.round(np.random.uniform(4, min(dur * 0.5, 16)), 1)\n        events_list.append({\"patient_id\": pid, \"time\": pr_time, \"event_type\": \"Partial Response\"})\n    if dur > 20 and np.random.random() > 0.5:\n        cr_time = np.round(np.random.uniform(12, min(dur * 0.7, 30)), 1)\n        events_list.append({\"patient_id\": pid, \"time\": cr_time, \"event_type\": \"Complete Response\"})\n    if not ongoing_mask[i] and dur > 6:\n        pd_time = np.round(dur - np.random.uniform(0, 3), 1)\n        events_list.append({\"patient_id\": pid, \"time\": pd_time, \"event_type\": \"Progressive Disease\"})\n    if ongoing_mask[i]:\n        events_list.append({\"patient_id\": pid, \"time\": dur, \"event_type\": \"Ongoing\"})\n\nbars_df = pd.DataFrame({\"patient_id\": patient_ids, \"duration\": durations, \"arm\": arms, \"ongoing\": ongoing_mask})\nsort_order = bars_df.sort_values(\"duration\", ascending=True)[\"patient_id\"].tolist()\n\nevents_df = pd.DataFrame(events_list)\nevents_df = events_df.merge(bars_df[[\"patient_id\", \"arm\"]], on=\"patient_id\")\n\n# Interactive arm highlight — click treatment arm in legend to focus\narm_selection = alt.selection_point(fields=[\"arm\"], bind=\"legend\")\n\n# Bars — use fill (not color) so the arm legend stays separate from event color legend\nbars = (\n    alt.Chart(bars_df)\n    .mark_bar(height=12, cornerRadiusEnd=3)\n    .encode(\n        x=alt.X(\n            \"duration:Q\",\n            title=\"Time on Study (Weeks)\",\n            axis=alt.Axis(titleFontSize=12, labelFontSize=10, tickSize=0, grid=True, gridOpacity=0.15, gridColor=INK),\n        ),\n        y=alt.Y(\"patient_id:N\", title=None, sort=sort_order, axis=alt.Axis(labelFontSize=10, tickSize=0)),\n        fill=alt.Fill(\n            \"arm:N\",\n            title=\"Treatment Arm\",\n            scale=alt.Scale(domain=[\"Arm A (Combo)\", \"Arm B (Mono)\"], range=ARM_COLORS),\n            legend=alt.Legend(orient=\"right\", symbolSize=120, symbolStrokeWidth=0),\n        ),\n        opacity=alt.condition(arm_selection, alt.value(1.0), alt.value(0.25)),\n        tooltip=[\"patient_id:N\", \"arm:N\", alt.Tooltip(\"duration:Q\", title=\"Weeks on Study\")],\n    )\n    .add_params(arm_selection)\n)\n\n# Median reference line for population context\nmedian_dur = float(np.median(durations))\nrule_df = pd.DataFrame({\"median\": [median_dur]})\nmedian_rule = alt.Chart(rule_df).mark_rule(strokeDash=[6, 4], strokeWidth=1.2, color=INK_SOFT).encode(x=\"median:Q\")\nmedian_label = (\n    alt.Chart(rule_df)\n    .mark_text(align=\"left\", dx=4, dy=-8, fontSize=9, color=INK_MUTED, fontStyle=\"italic\")\n    .encode(x=\"median:Q\", y=alt.value(0), text=alt.value(f\"Median: {median_dur:.0f} wk\"))\n)\n\n# Event markers — shape + color both encode event_type; Vega-Lite merges into one legend\nmarkers = (\n    alt.Chart(events_df)\n    .mark_point(filled=True, size=180, stroke=PAGE_BG, strokeWidth=1.0)\n    .encode(\n        x=alt.X(\"time:Q\"),\n        y=alt.Y(\"patient_id:N\", sort=sort_order),\n        shape=alt.Shape(\n            \"event_type:N\",\n            title=\"Clinical Event\",\n            scale=alt.Scale(domain=list(event_shape_map.keys()), range=list(event_shape_map.values())),\n            legend=alt.Legend(orient=\"right\", symbolSize=120, symbolStrokeWidth=0),\n        ),\n        color=alt.Color(\n            \"event_type:N\",\n            title=\"Clinical Event\",\n            scale=alt.Scale(domain=list(event_color_map.keys()), range=list(event_color_map.values())),\n            legend=alt.Legend(orient=\"right\", symbolSize=120, symbolStrokeWidth=0),\n        ),\n        tooltip=[\"patient_id:N\", \"event_type:N\", alt.Tooltip(\"time:Q\", title=\"Week\")],\n        opacity=alt.condition(arm_selection, alt.value(1.0), alt.value(0.25)),\n    )\n)\n\n# Title — 56 chars, below 67-char baseline, fontsize=16\ntitle_text = \"swimmer-clinical-timeline · python · altair · anyplot.ai\"\n\nchart = (\n    (bars + median_rule + median_label + markers)\n    .properties(\n        width=480,\n        height=320,\n        background=PAGE_BG,\n        padding={\"left\": 0, \"right\": 0, \"top\": 0, \"bottom\": 0},\n        title=alt.Title(title_text, fontSize=16, fontWeight=\"normal\", color=INK, anchor=\"start\", offset=12),\n    )\n    .configure_view(fill=PAGE_BG, stroke=None, strokeWidth=0, continuousWidth=480, continuousHeight=320)\n    .configure_axis(domainColor=INK_SOFT, tickColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=10,\n        padding=6,\n        cornerRadius=3,\n    )\n    .configure_title(color=INK)\n)\n\n# Save PNG with scale_factor=4.0, then pad to exact 3200×1800\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}