{"spec_id":"swimmer-clinical-timeline","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// swimmer-clinical-timeline: Swimmer Plot for Clinical Trial Timelines\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-08-24\n\nconst t = window.ANYPLOT_TOKENS;\n// ANYPLOT_TOKENS has no \"muted\" anchor — derive it the same way the style\n// guide's INK_MUTED reference snippet does (tertiary text / disabled tone).\nconst inkMuted = t.theme === \"dark\" ? \"#A8A79F\" : \"#6B6A63\";\n\n// --- Custom markers (core Highcharts has no star/arrow symbol) -------------\nHighcharts.SVGRenderer.prototype.symbols.star = function (x, y, w, h) {\n  const cx = x + w / 2;\n  const cy = y + h / 2;\n  const outerR = w / 2;\n  const innerR = outerR * 0.45;\n  const path = [];\n  for (let i = 0; i < 10; i += 1) {\n    const r = i % 2 === 0 ? outerR : innerR;\n    const angle = (Math.PI / 5) * i - Math.PI / 2;\n    const px = cx + r * Math.cos(angle);\n    const py = cy + r * Math.sin(angle);\n    path.push(i === 0 ? [\"M\", px, py] : [\"L\", px, py]);\n  }\n  path.push([\"Z\"]);\n  return path;\n};\n\nHighcharts.SVGRenderer.prototype.symbols.arrow = function (x, y, w, h) {\n  const midY = y + h / 2;\n  return [\n    [\"M\", x, midY - h * 0.28],\n    [\"L\", x + w * 0.55, midY - h * 0.28],\n    [\"L\", x + w * 0.55, y],\n    [\"L\", x + w, midY],\n    [\"L\", x + w * 0.55, y + h],\n    [\"L\", x + w * 0.55, midY + h * 0.28],\n    [\"L\", x, midY + h * 0.28],\n    [\"Z\"],\n  ];\n};\n\n// --- Data (in-memory, deterministic, tiny fixed-seed LCG) -------------------\nlet seed = 42;\nfunction nextRandom() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nconst patientCount = 25;\nconst patients = [];\nfor (let i = 0; i < patientCount; i += 1) {\n  const group = nextRandom() < 0.52 ? \"Arm A\" : \"Arm B\";\n  const durationWeeks = Math.round(6 + nextRandom() * 52);\n  const ongoing = nextRandom() < 0.28;\n  const events = [];\n\n  if (nextRandom() < 0.7) {\n    const time = Math.round(durationWeeks * (0.15 + nextRandom() * 0.2));\n    events.push({ time, type: \"partial_response\" });\n  }\n\n  const outcomeRoll = nextRandom();\n  if (!ongoing && outcomeRoll < 0.35) {\n    const time = Math.round(durationWeeks * (0.55 + nextRandom() * 0.35));\n    events.push({ time: Math.min(time, durationWeeks), type: \"complete_response\" });\n  } else if (!ongoing && outcomeRoll < 0.65) {\n    events.push({ time: durationWeeks, type: \"progressive_disease\" });\n  }\n\n  if (nextRandom() < 0.3) {\n    const time = Math.round(durationWeeks * (0.1 + nextRandom() * 0.7));\n    events.push({ time, type: \"adverse_event\" });\n  }\n\n  patients.push({\n    id: `PT-${String(i + 1).padStart(3, \"0\")}`,\n    group,\n    durationWeeks,\n    ongoing,\n    events,\n  });\n}\n\n// Sorted so the longest-running patient lands at the top of the inverted axis\npatients.sort((a, b) => a.durationWeeks - b.durationWeeks);\n\n// Cohort median duration — a data-derived focal point for the \"story\" of the\n// cohort (patients array is already duration-sorted, so the middle entry is\n// the median), not an arbitrary annotation.\nconst medianDurationWeeks = patients[Math.floor(patients.length / 2)].durationWeeks;\n\nconst categories = patients.map((p) => p.id);\nconst armAData = [];\nconst armBData = [];\nconst partialResponseData = [];\nconst completeResponseData = [];\nconst progressiveDiseaseData = [];\nconst adverseEventData = [];\nconst ongoingData = [];\n\npatients.forEach((p, index) => {\n  const bar = { x: index, y: p.durationWeeks };\n  if (p.group === \"Arm A\") {\n    armAData.push(bar);\n  } else {\n    armBData.push(bar);\n  }\n  if (p.ongoing) {\n    ongoingData.push({ x: index, y: p.durationWeeks });\n  }\n  p.events.forEach((event) => {\n    const point = { x: index, y: event.time };\n    if (event.type === \"partial_response\") partialResponseData.push(point);\n    if (event.type === \"complete_response\") completeResponseData.push(point);\n    if (event.type === \"progressive_disease\") progressiveDiseaseData.push(point);\n    if (event.type === \"adverse_event\") adverseEventData.push(point);\n  });\n});\n\n// Subtle zebra banding across the 25 patient rows aids scanning without\n// competing with the data (kept within the grid-opacity range).\nconst rowBandColor = t.theme === \"dark\" ? \"rgba(255,255,255,0.035)\" : \"rgba(0,0,0,0.035)\";\nconst rowBands = categories\n  .map((_, i) => i)\n  .filter((i) => i % 2 === 1)\n  .map((i) => ({ from: i - 0.5, to: i + 0.5, color: rowBandColor }));\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"column\",\n    inverted: true,\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"swimmer-clinical-timeline · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"23px\", fontWeight: \"700\", letterSpacing: \"-0.2px\" },\n  },\n  subtitle: {\n    text: \"Phase II oncology trial · 25 patients · two treatment arms\",\n    style: { color: t.inkSoft, fontSize: \"14px\", fontWeight: \"400\", letterSpacing: \"0.3px\" },\n  },\n  xAxis: {\n    categories,\n    plotBands: rowBands,\n    title: { text: \"Patient ID\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: { style: { color: t.inkSoft, fontSize: \"12px\", fontWeight: \"500\" } },\n  },\n  yAxis: {\n    title: { text: \"Time on Study (weeks)\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    gridLineColor: t.grid,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    min: 0,\n    plotLines: [\n      {\n        value: medianDurationWeeks,\n        color: t.inkSoft,\n        width: 1.5,\n        dashStyle: \"Dash\",\n        zIndex: 5,\n        label: {\n          text: `Cohort median: ${medianDurationWeeks} wk`,\n          rotation: 0,\n          align: \"left\",\n          verticalAlign: \"top\",\n          x: 6,\n          y: 16,\n          style: { color: t.inkSoft, fontSize: \"12px\", fontStyle: \"italic\" },\n        },\n      },\n    ],\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"13px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  plotOptions: {\n    series: { animation: false },\n    column: { borderWidth: 0, pointWidth: 12, pointPadding: 0.15, groupPadding: 0.05 },\n    scatter: { marker: { lineColor: t.pageBg, lineWidth: 1.5 } },\n  },\n  tooltip: {\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    style: { color: t.ink, fontSize: \"13px\" },\n    headerFormat: \"<b>{point.key}</b><br/>\",\n    pointFormat: \"{series.name}: {point.y} wk\",\n  },\n  series: [\n    { type: \"column\", name: \"Arm A\", data: armAData, color: t.palette[0] },\n    { type: \"column\", name: \"Arm B\", data: armBData, color: t.palette[1] },\n    {\n      type: \"scatter\",\n      name: \"Partial response\",\n      data: partialResponseData,\n      color: t.amber,\n      marker: { symbol: \"triangle\", radius: 7 },\n    },\n    {\n      type: \"scatter\",\n      name: \"Complete response\",\n      data: completeResponseData,\n      color: t.palette[0],\n      marker: { symbol: \"star\", radius: 8 },\n    },\n    {\n      type: \"scatter\",\n      name: \"Progressive disease\",\n      data: progressiveDiseaseData,\n      color: t.palette[4],\n      marker: { symbol: \"diamond\", radius: 7 },\n    },\n    {\n      type: \"scatter\",\n      name: \"Adverse event\",\n      data: adverseEventData,\n      color: inkMuted,\n      marker: { symbol: \"square\", radius: 6 },\n    },\n    {\n      type: \"scatter\",\n      name: \"Ongoing (censored)\",\n      data: ongoingData,\n      color: t.ink,\n      marker: { symbol: \"arrow\", radius: 9 },\n    },\n  ],\n});\n"}