{"spec_id":"swimmer-clinical-timeline","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// swimmer-clinical-timeline: Swimmer Plot for Clinical Trial Timelines\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 88/100 | Created: 2026-06-08\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data ---\nconst rawPatients = [\n  { id: \"PT-01\", duration: 52, group: \"A\", ongoing: true,  events: [{t:8,type:\"PR\"},{t:18,type:\"CR\"}] },\n  { id: \"PT-02\", duration: 48, group: \"A\", ongoing: false, events: [{t:6,type:\"PR\"},{t:36,type:\"PD\"}] },\n  { id: \"PT-03\", duration: 45, group: \"A\", ongoing: true,  events: [{t:10,type:\"PR\"}] },\n  { id: \"PT-04\", duration: 41, group: \"A\", ongoing: false, events: [{t:8,type:\"PR\"},{t:16,type:\"CR\"},{t:38,type:\"AE\"}] },\n  { id: \"PT-05\", duration: 38, group: \"A\", ongoing: false, events: [{t:12,type:\"PR\"},{t:30,type:\"PD\"}] },\n  { id: \"PT-06\", duration: 35, group: \"A\", ongoing: false, events: [{t:6,type:\"AE\"},{t:20,type:\"PR\"}] },\n  { id: \"PT-07\", duration: 28, group: \"A\", ongoing: false, events: [{t:10,type:\"PD\"}] },\n  { id: \"PT-08\", duration: 24, group: \"A\", ongoing: false, events: [{t:8,type:\"PR\"},{t:20,type:\"PD\"}] },\n  { id: \"PT-09\", duration: 20, group: \"A\", ongoing: false, events: [{t:6,type:\"AE\"},{t:14,type:\"PD\"}] },\n  { id: \"PT-10\", duration: 18, group: \"A\", ongoing: false, events: [{t:4,type:\"PD\"}] },\n  { id: \"PT-11\", duration: 14, group: \"A\", ongoing: false, events: [{t:8,type:\"AE\"},{t:12,type:\"PD\"}] },\n  { id: \"PT-12\", duration: 10, group: \"A\", ongoing: false, events: [{t:6,type:\"PD\"}] },\n  { id: \"PT-13\", duration: 50, group: \"B\", ongoing: true,  events: [{t:6,type:\"PR\"},{t:14,type:\"CR\"}] },\n  { id: \"PT-14\", duration: 46, group: \"B\", ongoing: true,  events: [{t:8,type:\"PR\"},{t:22,type:\"CR\"}] },\n  { id: \"PT-15\", duration: 43, group: \"B\", ongoing: false, events: [{t:10,type:\"PR\"},{t:20,type:\"CR\"},{t:40,type:\"PD\"}] },\n  { id: \"PT-16\", duration: 39, group: \"B\", ongoing: false, events: [{t:8,type:\"PR\"},{t:32,type:\"PD\"}] },\n  { id: \"PT-17\", duration: 36, group: \"B\", ongoing: false, events: [{t:6,type:\"AE\"},{t:16,type:\"PR\"}] },\n  { id: \"PT-18\", duration: 32, group: \"B\", ongoing: false, events: [{t:10,type:\"PR\"},{t:26,type:\"PD\"}] },\n  { id: \"PT-19\", duration: 26, group: \"B\", ongoing: false, events: [{t:6,type:\"AE\"}] },\n  { id: \"PT-20\", duration: 22, group: \"B\", ongoing: false, events: [{t:8,type:\"PR\"},{t:18,type:\"PD\"}] },\n  { id: \"PT-21\", duration: 18, group: \"B\", ongoing: false, events: [{t:4,type:\"PD\"}] },\n  { id: \"PT-22\", duration: 16, group: \"B\", ongoing: false, events: [{t:6,type:\"AE\"},{t:12,type:\"PD\"}] },\n  { id: \"PT-23\", duration: 12, group: \"B\", ongoing: false, events: [{t:4,type:\"PR\"},{t:10,type:\"PD\"}] },\n  { id: \"PT-24\", duration: 8,  group: \"B\", ongoing: false, events: [{t:4,type:\"PD\"}] },\n  { id: \"PT-25\", duration: 6,  group: \"B\", ongoing: false, events: [{t:4,type:\"AE\"}] },\n];\n\n// Sort descending by duration — longest patient at top of chart\nconst patients = rawPatients.sort((a, b) => b.duration - a.duration);\nconst labels = patients.map(p => p.id);\nconst n = patients.length;\n\n// Bar data per arm (null for patients in the other arm — Chart.js skips nulls)\nconst armAData = patients.map(p => p.group === \"A\" ? p.duration : null);\nconst armBData = patients.map(p => p.group === \"B\" ? p.duration : null);\n\n// Scatter event data — y2 is a hidden linear axis aligned to the categorical y-axis.\n// Alignment formula: for categorical index i (0=top), y2 = n - 1 - i\n// so index 0 (top bar) → y2 = n-1 (near y2 max = top), index n-1 (bottom) → y2 = 0.\nconst prPts = [], crPts = [], pdPts = [], aePts = [], ongoingPts = [];\npatients.forEach((p, i) => {\n  const y = n - 1 - i;\n  if (p.ongoing) ongoingPts.push({ x: p.duration, y });\n  p.events.forEach(e => {\n    const pt = { x: e.t, y };\n    if (e.type === \"PR\") prPts.push(pt);\n    else if (e.type === \"CR\") crPts.push(pt);\n    else if (e.type === \"PD\") pdPts.push(pt);\n    else if (e.type === \"AE\") aePts.push(pt);\n  });\n});\n\n// --- Mount ---\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels,\n    datasets: [\n      {\n        type: \"bar\",\n        label: \"Arm A — Experimental\",\n        data: armAData,\n        backgroundColor: t.palette[0] + \"99\",\n        borderColor: t.palette[0],\n        borderWidth: 1.5,\n        borderRadius: 4,\n        yAxisID: \"y\",\n        xAxisID: \"x\",\n        barPercentage: 0.55,\n        categoryPercentage: 0.9,\n      },\n      {\n        type: \"bar\",\n        label: \"Arm B — Control\",\n        data: armBData,\n        backgroundColor: t.palette[1] + \"99\",\n        borderColor: t.palette[1],\n        borderWidth: 1.5,\n        borderRadius: 4,\n        yAxisID: \"y\",\n        xAxisID: \"x\",\n        barPercentage: 0.55,\n        categoryPercentage: 0.9,\n      },\n      {\n        type: \"scatter\",\n        label: \"Partial Response (▲)\",\n        data: prPts,\n        yAxisID: \"y2\",\n        xAxisID: \"x\",\n        backgroundColor: t.palette[2],\n        borderColor: t.pageBg,\n        borderWidth: 1.5,\n        pointStyle: \"triangle\",\n        pointRadius: 7,\n        pointHoverRadius: 9,\n      },\n      {\n        type: \"scatter\",\n        label: \"Complete Response (★)\",\n        data: crPts,\n        yAxisID: \"y2\",\n        xAxisID: \"x\",\n        backgroundColor: t.palette[3],\n        borderColor: t.pageBg,\n        borderWidth: 1.5,\n        pointStyle: \"star\",\n        pointRadius: 12,\n        pointHoverRadius: 14,\n      },\n      {\n        type: \"scatter\",\n        label: \"Progressive Disease (◆)\",\n        data: pdPts,\n        yAxisID: \"y2\",\n        xAxisID: \"x\",\n        backgroundColor: t.palette[4],\n        borderColor: t.pageBg,\n        borderWidth: 1.5,\n        pointStyle: \"rectRot\",\n        pointRadius: 7,\n        pointHoverRadius: 9,\n      },\n      {\n        type: \"scatter\",\n        label: \"Adverse Event (●)\",\n        data: aePts,\n        yAxisID: \"y2\",\n        xAxisID: \"x\",\n        backgroundColor: t.palette[5],\n        borderColor: t.pageBg,\n        borderWidth: 1.5,\n        pointStyle: \"circle\",\n        pointRadius: 6,\n        pointHoverRadius: 8,\n      },\n      {\n        type: \"scatter\",\n        label: \"Ongoing at Cutoff (→)\",\n        data: ongoingPts,\n        yAxisID: \"y2\",\n        xAxisID: \"x\",\n        backgroundColor: t.ink,\n        borderColor: t.pageBg,\n        borderWidth: 1,\n        pointStyle: \"triangle\",\n        rotation: 90,\n        pointRadius: 8,\n        pointHoverRadius: 10,\n      },\n    ],\n  },\n  options: {\n    indexAxis: \"y\",\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"swimmer-clinical-timeline · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"600\" },\n        padding: { top: 12, bottom: 20 },\n      },\n      legend: {\n        display: true,\n        position: \"bottom\",\n        labels: {\n          color: t.inkSoft,\n          font: { size: 12 },\n          padding: 22,\n          usePointStyle: true,\n          pointStyleWidth: 16,\n        },\n      },\n    },\n    layout: {\n      padding: { top: 10, right: 50, bottom: 10, left: 10 },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        position: \"bottom\",\n        min: 0,\n        max: 60,\n        title: {\n          display: true,\n          text: \"Time on Study (Weeks)\",\n          color: t.ink,\n          font: { size: 15, weight: \"500\" },\n          padding: { top: 10 },\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 13 },\n          stepSize: 10,\n        },\n        grid: { color: t.grid },\n        border: { color: t.grid },\n      },\n      y: {\n        type: \"category\",\n        position: \"left\",\n        title: {\n          display: true,\n          text: \"Patient ID\",\n          color: t.ink,\n          font: { size: 15, weight: \"500\" },\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 13 },\n        },\n        grid: { display: false },\n        border: { display: false },\n      },\n      // Hidden linear axis aligned to the categorical y-axis for scatter event markers.\n      // Range [-0.5, n-0.5] maps cleanly: index 0 (top bar) → y2 = n-1, index n-1 → y2 = 0.\n      y2: {\n        type: \"linear\",\n        display: false,\n        min: -0.5,\n        max: n - 0.5,\n      },\n    },\n  },\n  plugins: [\n    {\n      id: \"bgfill\",\n      beforeDraw(chart) {\n        const ctx = chart.canvas.getContext(\"2d\");\n        ctx.save();\n        ctx.fillStyle = t.pageBg;\n        ctx.fillRect(0, 0, chart.width, chart.height);\n        ctx.restore();\n      },\n    },\n  ],\n});\n"}