{"spec_id":"gantt-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// gantt-basic: Basic Gantt Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// A software-release schedule. Chart.js ships no date-scale adapter in this\n// runtime, so dates are converted to day offsets from PROJECT_START and the\n// linear x-axis ticks are formatted back into calendar dates.\nconst PROJECT_START = new Date(Date.UTC(2026, 0, 5)); // 2026-01-05\nconst REFERENCE_DATE = new Date(Date.UTC(2026, 2, 2)); // \"current date\" marker\n\nconst dayOffset = (date) => Math.round((date - PROJECT_START) / 86400000);\n\nconst CATEGORIES = [\"Planning\", \"Design\", \"Development\", \"Testing\", \"Launch\"];\n\nconst tasks = [\n  { task: \"Requirements Gathering\", start: Date.UTC(2026, 0, 5), end: Date.UTC(2026, 0, 16), category: \"Planning\" },\n  { task: \"Stakeholder Review\", start: Date.UTC(2026, 0, 12), end: Date.UTC(2026, 0, 19), category: \"Planning\" },\n  { task: \"Wireframes\", start: Date.UTC(2026, 0, 19), end: Date.UTC(2026, 1, 2), category: \"Design\" },\n  { task: \"UI Design\", start: Date.UTC(2026, 0, 26), end: Date.UTC(2026, 1, 13), category: \"Design\" },\n  { task: \"Design Review\", start: Date.UTC(2026, 1, 9), end: Date.UTC(2026, 1, 16), category: \"Design\" },\n  { task: \"Backend API\", start: Date.UTC(2026, 1, 16), end: Date.UTC(2026, 2, 20), category: \"Development\" },\n  { task: \"Frontend Build\", start: Date.UTC(2026, 1, 23), end: Date.UTC(2026, 2, 27), category: \"Development\" },\n  { task: \"Integration\", start: Date.UTC(2026, 2, 20), end: Date.UTC(2026, 3, 3), category: \"Development\" },\n  { task: \"QA Testing\", start: Date.UTC(2026, 3, 3), end: Date.UTC(2026, 3, 17), category: \"Testing\" },\n  { task: \"Bug Fixes\", start: Date.UTC(2026, 3, 10), end: Date.UTC(2026, 3, 21), category: \"Testing\" },\n  { task: \"Launch Prep\", start: Date.UTC(2026, 3, 21), end: Date.UTC(2026, 3, 28), category: \"Launch\" },\n  { task: \"Go Live\", start: Date.UTC(2026, 3, 28), end: Date.UTC(2026, 4, 1), category: \"Launch\" },\n].map((row) => ({ ...row, start: dayOffset(new Date(row.start)), end: dayOffset(new Date(row.end)) }));\n\nconst totalDays = Math.max(...tasks.map((row) => row.end)) + 5;\nconst todayOffset = dayOffset(REFERENCE_DATE);\nconst isActive = (row) => row.start <= todayOffset && todayOffset <= row.end;\n\nconst MONTHS = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\nfunction formatDayOffset(offset) {\n  const d = new Date(PROJECT_START.getTime() + Math.round(offset) * 86400000);\n  return `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`;\n}\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart -------------------------------------------------------------------\n// Subtle alternating row bands (drawn behind the grid/bars) to make it easier\n// to trace a task row across the full timeline width.\nconst rowBandPlugin = {\n  id: \"rowBands\",\n  beforeDraw(chart) {\n    const { ctx, chartArea, scales } = chart;\n    const rowHeight = chartArea.height / tasks.length;\n    ctx.save();\n    ctx.fillStyle = t.elevatedBg;\n    tasks.forEach((_, i) => {\n      if (i % 2 !== 0) return;\n      const center = scales.y.getPixelForValue(i);\n      ctx.fillRect(chartArea.left, center - rowHeight / 2, chartArea.width, rowHeight);\n    });\n    ctx.restore();\n  },\n};\n\n// Compact \"Nd\" duration labels next to each bar, with in-progress tasks\n// (those straddling REFERENCE_DATE) called out as the chart's focal point.\nconst durationLabelPlugin = {\n  id: \"durationLabels\",\n  afterDatasetsDraw(chart) {\n    const { ctx, chartArea } = chart;\n    const meta = chart.getDatasetMeta(0);\n    ctx.save();\n    ctx.textBaseline = \"middle\";\n    meta.data.forEach((bar, i) => {\n      const row = tasks[i];\n      const active = isActive(row);\n      const label = `${row.end - row.start}d${active ? \" · in progress\" : \"\"}`;\n      ctx.font = `${active ? \"bold \" : \"\"}11px -apple-system, BlinkMacSystemFont, sans-serif`;\n      ctx.fillStyle = active ? t.ink : t.inkSoft;\n      const labelWidth = ctx.measureText(label).width;\n      if (bar.x + 6 + labelWidth <= chartArea.right) {\n        ctx.textAlign = \"left\";\n        ctx.fillText(label, bar.x + 6, bar.y);\n      } else {\n        ctx.textAlign = \"right\";\n        ctx.fillText(label, bar.base - 6, bar.y);\n      }\n    });\n    ctx.restore();\n  },\n};\n\n// A dashed reference line marks REFERENCE_DATE using the theme's ink color\n// (the \"neutral\" semantic anchor — reference lines share the text/grid hue).\nconst referenceLinePlugin = {\n  id: \"referenceLine\",\n  afterDraw(chart) {\n    const { x: xScale, y: yScale } = chart.scales;\n    const refX = xScale.getPixelForValue(dayOffset(REFERENCE_DATE));\n    if (refX < xScale.left || refX > xScale.right) return;\n    const { ctx } = chart;\n    ctx.save();\n    ctx.strokeStyle = t.ink;\n    ctx.setLineDash([6, 4]);\n    ctx.lineWidth = 2;\n    ctx.beginPath();\n    ctx.moveTo(refX, yScale.top);\n    ctx.lineTo(refX, yScale.bottom);\n    ctx.stroke();\n    ctx.setLineDash([]);\n    ctx.fillStyle = t.ink;\n    ctx.font = \"14px -apple-system, BlinkMacSystemFont, sans-serif\";\n    ctx.textAlign = \"left\";\n    ctx.textBaseline = \"bottom\";\n    ctx.fillText(\"Today\", refX + 8, yScale.bottom - 6);\n    ctx.restore();\n  },\n};\n\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels: tasks.map((row) => row.task),\n    datasets: [\n      {\n        label: \"Tasks\",\n        data: tasks.map((row) => [row.start, row.end]),\n        backgroundColor: tasks.map((row) => t.palette[CATEGORIES.indexOf(row.category) % t.palette.length]),\n        borderColor: tasks.map(() => t.ink),\n        borderWidth: tasks.map((row) => (isActive(row) ? 3 : 0)),\n        borderRadius: 4,\n        borderSkipped: false,\n        barPercentage: 0.6,\n        categoryPercentage: 0.7,\n      },\n    ],\n  },\n  options: {\n    indexAxis: \"y\",\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 24 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"gantt-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: {\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          generateLabels: () =>\n            CATEGORIES.map((category, i) => ({\n              text: category,\n              fillStyle: t.palette[i % t.palette.length],\n              strokeStyle: t.palette[i % t.palette.length],\n              lineWidth: 0,\n            })),\n        },\n        onClick: () => {},\n      },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: 0,\n        max: totalDays,\n        ticks: {\n          stepSize: 14,\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: formatDayOffset,\n        },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Timeline (2026)\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        ticks: { color: t.inkSoft, font: { size: 15 } },\n        grid: { display: false },\n      },\n    },\n  },\n  plugins: [rowBandPlugin, durationLabelPlugin, referenceLinePlugin],\n});\n"}