{"spec_id":"swimmer-clinical-timeline","library":"d3","language":"javascript","code":"// anyplot.ai\n// swimmer-clinical-timeline: Swimmer Plot for Clinical Trial Timelines\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 89/100 | Created: 2026-06-08\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data: 20 patients, Phase II oncology trial, 2 treatment arms ---\nconst patients = [\n  { id: \"PT-001\", arm: \"Arm A\", duration: 28, ongoing: true,  events: [{ t: 3, type: \"pr\" }, { t: 10, type: \"cr\" }] },\n  { id: \"PT-002\", arm: \"Arm B\", duration: 26, ongoing: true,  events: [{ t: 4, type: \"pr\" }, { t: 14, type: \"cr\" }] },\n  { id: \"PT-003\", arm: \"Arm A\", duration: 24, ongoing: false, events: [{ t: 5, type: \"pr\" }, { t: 20, type: \"pd\" }] },\n  { id: \"PT-004\", arm: \"Arm B\", duration: 22, ongoing: true,  events: [{ t: 6, type: \"pr\" }] },\n  { id: \"PT-005\", arm: \"Arm A\", duration: 20, ongoing: false, events: [{ t: 4, type: \"ae\" }, { t: 18, type: \"pd\" }] },\n  { id: \"PT-006\", arm: \"Arm B\", duration: 20, ongoing: true,  events: [{ t: 5, type: \"pr\" }, { t: 12, type: \"cr\" }] },\n  { id: \"PT-007\", arm: \"Arm A\", duration: 18, ongoing: false, events: [{ t: 3, type: \"pr\" }, { t: 14, type: \"cr\" }, { t: 17, type: \"ae\" }] },\n  { id: \"PT-008\", arm: \"Arm B\", duration: 16, ongoing: false, events: [{ t: 5, type: \"pr\" }, { t: 14, type: \"pd\" }] },\n  { id: \"PT-009\", arm: \"Arm A\", duration: 16, ongoing: true,  events: [{ t: 4, type: \"pr\" }] },\n  { id: \"PT-010\", arm: \"Arm B\", duration: 14, ongoing: false, events: [{ t: 4, type: \"ae\" }, { t: 12, type: \"pd\" }] },\n  { id: \"PT-011\", arm: \"Arm A\", duration: 13, ongoing: false, events: [{ t: 3, type: \"pr\" }, { t: 10, type: \"cr\" }] },\n  { id: \"PT-012\", arm: \"Arm B\", duration: 12, ongoing: false, events: [{ t: 3, type: \"pr\" }, { t: 10, type: \"pd\" }] },\n  { id: \"PT-013\", arm: \"Arm A\", duration: 10, ongoing: false, events: [{ t: 4, type: \"ae\" }, { t: 8,  type: \"pd\" }] },\n  { id: \"PT-014\", arm: \"Arm B\", duration: 10, ongoing: true,  events: [{ t: 3, type: \"pr\" }] },\n  { id: \"PT-015\", arm: \"Arm A\", duration: 8,  ongoing: false, events: [{ t: 2, type: \"ae\" }, { t: 7,  type: \"pd\" }] },\n  { id: \"PT-016\", arm: \"Arm B\", duration: 8,  ongoing: false, events: [{ t: 3, type: \"cr\" }] },\n  { id: \"PT-017\", arm: \"Arm A\", duration: 6,  ongoing: false, events: [{ t: 2, type: \"ae\" }, { t: 5,  type: \"pd\" }] },\n  { id: \"PT-018\", arm: \"Arm B\", duration: 5,  ongoing: false, events: [{ t: 2, type: \"pr\" }, { t: 4,  type: \"ae\" }] },\n  { id: \"PT-019\", arm: \"Arm A\", duration: 4,  ongoing: false, events: [{ t: 2, type: \"pd\" }] },\n  { id: \"PT-020\", arm: \"Arm B\", duration: 3,  ongoing: false, events: [{ t: 2, type: \"ae\" }] },\n];\n\n// Sorted longest-bar-first (top of chart)\npatients.sort((a, b) => b.duration - a.duration);\n\n// Event display config — Imprint palette positions + amber semantic anchor\nconst eventColors = {\n  pr: t.palette[2],  // #4467A3 blue   — Partial Response\n  cr: t.palette[3],  // #BD8233 ochre  — Complete Response (next ordinal after PR)\n  pd: t.palette[4],  // #AE3030 red    — Progressive Disease (semantic: bad)\n  ae: t.amber,       // #DDCC77 amber  — Adverse Event (semantic: warning)\n};\nconst eventLabels = {\n  pr: \"Partial Response\",\n  cr: \"Complete Response\",\n  pd: \"Progressive Disease\",\n  ae: \"Adverse Event\",\n};\nconst eventSymbolTypes = {\n  pr: d3.symbolTriangle,  // up-triangle = improvement\n  cr: d3.symbolStar,      // star = best outcome\n  pd: d3.symbolDiamond,   // diamond = deterioration marker\n  ae: d3.symbolCircle,    // circle = safety event\n};\n\n// --- Layout ---\nconst margin = { top: 88, right: 250, bottom: 78, left: 100 };\nconst iw = width  - margin.left - margin.right;\nconst ih = height - margin.top  - margin.bottom;\n\n// --- SVG mount ---\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\n\nconst g = svg.append(\"g\")\n  .attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales ---\nconst x = d3.scaleLinear().domain([0, 32]).range([0, iw]);\n\nconst y = d3.scaleBand()\n  .domain(patients.map(d => d.id))\n  .range([0, ih])\n  .padding(0.35);\n\nconst barH = y.bandwidth();\nconst symSize = Math.max(60, barH * barH * 0.12); // scale symbols with bar height\n\n// --- Vertical gridlines (behind bars) ---\nx.ticks(8).forEach(v => {\n  g.append(\"line\")\n    .attr(\"x1\", x(v)).attr(\"x2\", x(v))\n    .attr(\"y1\", 0).attr(\"y2\", ih)\n    .attr(\"stroke\", t.grid)\n    .attr(\"stroke-width\", 1);\n});\n\n// --- Median reference line ---\n// Durations sorted: 28,26,24,22,20,20,18,16,16,14,13,12,10,10,8,8,6,5,4,3 → median=(14+13)/2=13.5\nconst medianDuration = 13.5;\ng.append(\"line\")\n  .attr(\"x1\", x(medianDuration)).attr(\"x2\", x(medianDuration))\n  .attr(\"y1\", -18).attr(\"y2\", ih)\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"stroke-dasharray\", \"6,4\")\n  .attr(\"opacity\", 0.55);\ng.append(\"text\")\n  .attr(\"x\", x(medianDuration) + 6)\n  .attr(\"y\", -4)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"12px\")\n  .style(\"font-style\", \"italic\")\n  .text(\"Median: 13.5 mo\");\n\n// --- Horizontal bars (treatment duration) ---\ng.selectAll(\".bar\")\n  .data(patients)\n  .join(\"rect\")\n  .attr(\"x\", 0)\n  .attr(\"y\", d => y(d.id) + barH * 0.05)\n  .attr(\"width\", d => Math.max(x(d.duration), 3))\n  .attr(\"height\", barH * 0.9)\n  .attr(\"fill\", d => d.arm === \"Arm A\" ? t.palette[0] : t.palette[1])\n  .attr(\"fill-opacity\", 0.72)\n  .attr(\"rx\", 3);\n\n// --- Ongoing arrows (right-pointing triangle at bar end) ---\npatients.filter(d => d.ongoing).forEach(d => {\n  const cx = x(d.duration);\n  const cy = y(d.id) + barH / 2;\n  const ah = barH * 0.45;\n  const aw = barH * 0.60;\n  g.append(\"path\")\n    .attr(\"d\", `M ${cx} ${cy - ah} L ${cx + aw} ${cy} L ${cx} ${cy + ah} Z`)\n    .attr(\"fill\", d.arm === \"Arm A\" ? t.palette[0] : t.palette[1]);\n});\n\n// --- Event markers (on top of bars) ---\npatients.forEach(p => {\n  p.events.forEach(ev => {\n    g.append(\"path\")\n      .attr(\"transform\", `translate(${x(ev.t)},${y(p.id) + barH / 2})`)\n      .attr(\"d\", d3.symbol().type(eventSymbolTypes[ev.type]).size(symSize)())\n      .attr(\"fill\", eventColors[ev.type])\n      .attr(\"stroke\", t.pageBg)\n      .attr(\"stroke-width\", 1.5);\n  });\n});\n\n// --- X-axis ---\nconst xAxis = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(8).tickSize(5));\n\nxAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\");\nxAxis.selectAll(\".tick line\").attr(\"stroke\", t.inkSoft);\nxAxis.select(\".domain\").attr(\"stroke\", \"none\");\n\n// --- Y-axis (patient IDs) ---\nconst yAxis = g.append(\"g\")\n  .call(d3.axisLeft(y).tickSize(0));\n\nyAxis.selectAll(\"text\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"12px\")\n  .attr(\"dx\", \"-5px\");\nyAxis.select(\".domain\").attr(\"stroke\", \"none\");\n\n// --- Axis label ---\nsvg.append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", height - 20)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"Months on Treatment\");\n\n// --- Legend (right side) ---\nconst lx = margin.left + iw + 28;\nlet ly = margin.top + 12;\n\n// helper: draw a filled callout pill behind a legend section header\nfunction legendHeader(svgEl, x, y, label) {\n  svgEl.append(\"rect\")\n    .attr(\"x\", x - 4).attr(\"y\", y - 13)\n    .attr(\"width\", 110).attr(\"height\", 18).attr(\"rx\", 3)\n    .attr(\"fill\", t.elevatedBg || (window.ANYPLOT_THEME === \"dark\" ? \"#242420\" : \"#FFFDF6\"))\n    .attr(\"opacity\", 0.9);\n  svgEl.append(\"text\").attr(\"x\", x).attr(\"y\", y)\n    .attr(\"fill\", t.ink).style(\"font-size\", \"13px\").style(\"font-weight\", \"600\")\n    .text(label);\n}\n\n// Treatment arms\nlegendHeader(svg, lx, ly, \"Treatment Arm\");\n\n[{ label: \"Arm A\", c: t.palette[0] }, { label: \"Arm B\", c: t.palette[1] }].forEach(arm => {\n  ly += 22;\n  svg.append(\"rect\")\n    .attr(\"x\", lx).attr(\"y\", ly - 8)\n    .attr(\"width\", 22).attr(\"height\", 13).attr(\"rx\", 2)\n    .attr(\"fill\", arm.c).attr(\"fill-opacity\", 0.72);\n  svg.append(\"text\").attr(\"x\", lx + 28).attr(\"y\", ly + 3)\n    .attr(\"fill\", t.inkSoft).style(\"font-size\", \"12px\").text(arm.label);\n});\n\n// Status (ongoing indicator)\nly += 30;\nlegendHeader(svg, lx, ly, \"Status\");\n\nly += 22;\nconst arH = 8;\nsvg.append(\"path\")\n  .attr(\"d\", `M ${lx} ${ly - arH} L ${lx + 16} ${ly} L ${lx} ${ly + arH} Z`)\n  .attr(\"fill\", t.inkSoft);\nsvg.append(\"text\").attr(\"x\", lx + 22).attr(\"y\", ly + 4)\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"12px\").text(\"Ongoing\");\n\n// Events\nly += 30;\nlegendHeader(svg, lx, ly, \"Events\");\n\n[\"pr\", \"cr\", \"pd\", \"ae\"].forEach(type => {\n  ly += 22;\n  svg.append(\"path\")\n    .attr(\"transform\", `translate(${lx + 10},${ly})`)\n    .attr(\"d\", d3.symbol().type(eventSymbolTypes[type]).size(70)())\n    .attr(\"fill\", eventColors[type])\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", 1.5);\n  svg.append(\"text\").attr(\"x\", lx + 24).attr(\"y\", ly + 4)\n    .attr(\"fill\", t.inkSoft).style(\"font-size\", \"12px\").text(eventLabels[type]);\n});\n\n// --- Title ---\nconst title = \"swimmer-clinical-timeline · javascript · d3 · anyplot.ai\";\nsvg.append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", 52)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(title);\n"}