{"spec_id":"timeline-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// timeline-basic: Event Timeline\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ---------------------------------------\n// Cloud platform product roadmap, chronological, alternating above/below the\n// spine to avoid label overlap.\nconst roadmap = [\n  { date: \"2024-01-15\", event: \"Public API v2 Launch\", category: \"API\" },\n  { date: \"2024-03-10\", event: \"Multi-Region Deployment\", category: \"Platform\" },\n  { date: \"2024-04-25\", event: \"Android Widget Support\", category: \"Mobile\" },\n  { date: \"2024-06-05\", event: \"GraphQL Endpoint\", category: \"API\" },\n  { date: \"2024-07-18\", event: \"Zero-Downtime Migrations\", category: \"Platform\" },\n  { date: \"2024-09-02\", event: \"Offline Mode\", category: \"Mobile\" },\n  { date: \"2024-10-14\", event: \"Rate Limiting v2\", category: \"API\" },\n  { date: \"2024-12-01\", event: \"Auto-Scaling Clusters\", category: \"Platform\" },\n  { date: \"2025-01-20\", event: \"Dark Mode Rollout\", category: \"Mobile\" },\n  { date: \"2025-03-08\", event: \"Webhooks GA\", category: \"API\" },\n  { date: \"2025-05-15\", event: \"Edge Caching Layer\", category: \"Platform\" },\n  { date: \"2025-07-10\", event: \"Cross-Platform Sync\", category: \"Mobile\" },\n];\n\nconst MS_PER_DAY = 86_400_000;\nconst baseDate = new Date(roadmap[0].date);\nconst dayOffset = (isoDate) => Math.round((new Date(isoDate) - baseDate) / MS_PER_DAY);\nconst formatDate = (days) => {\n  const d = new Date(baseDate.getTime() + days * MS_PER_DAY);\n  return d.toLocaleDateString(\"en-US\", { month: \"short\", year: \"numeric\" });\n};\n\nconst categories = [\"API\", \"Platform\", \"Mobile\"];\nconst points = roadmap.map((row, i) => ({\n  x: dayOffset(row.date),\n  y: i % 2 === 0 ? 1 : -1,\n  event: row.event,\n  dateLabel: formatDate(dayOffset(row.date)),\n  category: row.category,\n}));\n\nconst lastDay = points[points.length - 1].x;\nconst xPadding = Math.round(lastDay * 0.08);\n\nconst datasets = categories.map((category, i) => ({\n  label: category,\n  data: points.filter((p) => p.category === category),\n  backgroundColor: t.palette[i],\n  borderColor: t.pageBg,\n  borderWidth: 2,\n  pointRadius: 9,\n  pointHoverRadius: 9,\n  showLine: false,\n}));\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Custom plugin: spine + stems (behind markers), labels (above markers) --\nconst timelineDecorations = {\n  id: \"timelineDecorations\",\n  beforeDatasetsDraw(chart) {\n    const { ctx, chartArea, scales } = chart;\n    const spineY = scales.y.getPixelForValue(0);\n\n    ctx.save();\n    ctx.strokeStyle = t.grid;\n    ctx.lineWidth = 2;\n    ctx.beginPath();\n    ctx.moveTo(chartArea.left, spineY);\n    ctx.lineTo(chartArea.right, spineY);\n    ctx.stroke();\n\n    ctx.strokeStyle = t.inkSoft;\n    ctx.lineWidth = 1.5;\n    for (const dataset of chart.data.datasets) {\n      for (const point of dataset.data) {\n        const px = scales.x.getPixelForValue(point.x);\n        const py = scales.y.getPixelForValue(point.y);\n        ctx.beginPath();\n        ctx.moveTo(px, spineY);\n        ctx.lineTo(px, py);\n        ctx.stroke();\n      }\n    }\n    ctx.restore();\n  },\n  afterDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n\n    ctx.save();\n    for (const dataset of chart.data.datasets) {\n      for (const point of dataset.data) {\n        const px = scales.x.getPixelForValue(point.x);\n        const py = scales.y.getPixelForValue(point.y);\n        const above = point.y > 0;\n\n        ctx.textAlign = \"center\";\n        ctx.textBaseline = above ? \"bottom\" : \"top\";\n\n        const labelY = above ? py - 14 : py + 14;\n        ctx.fillStyle = t.ink;\n        ctx.font = \"600 15px sans-serif\";\n        ctx.fillText(point.event, px, labelY);\n\n        const dateY = above ? labelY - 19 : labelY + 19;\n        ctx.fillStyle = t.inkSoft;\n        ctx.font = \"13px sans-serif\";\n        ctx.fillText(point.dateLabel, px, dateY);\n      }\n    }\n    ctx.restore();\n  },\n};\n\n// --- Chart --------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: { datasets },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 60, bottom: 10, left: 20, right: 20 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"timeline-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { bottom: 24 },\n      },\n      legend: {\n        position: \"bottom\",\n        labels: { color: t.ink, font: { size: 16 }, usePointStyle: true, boxWidth: 10 },\n      },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: -xPadding,\n        max: lastDay + xPadding,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          maxTicksLimit: 8,\n          callback: (value) => formatDate(value),\n        },\n        grid: { display: false },\n        border: { color: t.inkSoft },\n        title: { display: true, text: \"Date\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        display: false,\n        min: -1.8,\n        max: 1.8,\n      },\n    },\n  },\n  plugins: [timelineDecorations],\n});\n"}