{"spec_id":"gantt-dependencies","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// gantt-dependencies: Gantt Chart with Dependencies\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 94/100 | Updated: 2026-08-25\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Software delivery sprint: four phases, each with its own tasks. start/end\n// are day offsets from PROJECT_START. A dependent task's start never precedes\n// the latest end of its predecessors.\nconst PROJECT_START = new Date(2026, 1, 2); // Mon Feb 2, 2026\n\nconst GROUPS = [\"Requirements\", \"Design\", \"Development\", \"Testing\"];\n\nconst tasks = [\n  { task: \"Stakeholder Interviews\", group: \"Requirements\", start: 0, end: 4, dependsOn: [] },\n  { task: \"Gather Requirements\", group: \"Requirements\", start: 0, end: 5, dependsOn: [] },\n  { task: \"Requirements Sign-off\", group: \"Requirements\", start: 5, end: 8, dependsOn: [\"Stakeholder Interviews\", \"Gather Requirements\"] },\n  { task: \"System Architecture\", group: \"Design\", start: 8, end: 14, dependsOn: [\"Requirements Sign-off\"] },\n  { task: \"UI/UX Design\", group: \"Design\", start: 8, end: 16, dependsOn: [\"Requirements Sign-off\"] },\n  { task: \"Database Schema\", group: \"Design\", start: 14, end: 18, dependsOn: [\"System Architecture\"] },\n  { task: \"API Development\", group: \"Development\", start: 18, end: 29, dependsOn: [\"Database Schema\"] },\n  { task: \"Frontend Build\", group: \"Development\", start: 16, end: 30, dependsOn: [\"UI/UX Design\"] },\n  { task: \"Data Pipeline\", group: \"Development\", start: 18, end: 27, dependsOn: [\"Database Schema\"] },\n  { task: \"Auth Integration\", group: \"Development\", start: 29, end: 33, dependsOn: [\"API Development\"] },\n  { task: \"Unit Testing\", group: \"Testing\", start: 27, end: 32, dependsOn: [\"Data Pipeline\"] },\n  { task: \"Integration Testing\", group: \"Testing\", start: 33, end: 38, dependsOn: [\"Auth Integration\", \"Frontend Build\"] },\n  { task: \"User Acceptance Testing\", group: \"Testing\", start: 38, end: 43, dependsOn: [\"Integration Testing\", \"Unit Testing\"] },\n];\n\nconst groupColor = Object.fromEntries(GROUPS.map((g, i) => [g, t.palette[i]]));\n\n// --- Rows: a header row per group (aggregate span) above its indented tasks -\nconst rows = [];\nconst taskRowIndex = new Map();\nfor (const group of GROUPS) {\n  const members = tasks.filter((task) => task.group === group);\n  const groupStart = Math.min(...members.map((task) => task.start));\n  const groupEnd = Math.max(...members.map((task) => task.end));\n  rows.push({ label: group.toUpperCase(), isHeader: true, color: groupColor[group], start: groupStart, end: groupEnd });\n  for (const task of members) {\n    taskRowIndex.set(task.task, rows.length);\n    rows.push({ label: `  ${task.task}`, isHeader: false, color: groupColor[group], start: task.start, end: task.end });\n  }\n}\n\n// --- Critical path: the chain of tasks binding the project finish date -----\n// Walk back from the last-finishing task, at each step following whichever\n// predecessor finishes latest (the one actually constraining the successor's\n// start). Rendered with heavier emphasis so the driving chain stands out.\nconst tasksByName = new Map(tasks.map((task) => [task.task, task]));\nconst criticalEdges = new Set();\n{\n  let current = tasks.reduce((latest, task) => (task.end > latest.end ? task : latest), tasks[0]);\n  while (current.dependsOn.length) {\n    let predName = current.dependsOn[0];\n    let pred = tasksByName.get(predName);\n    for (const depName of current.dependsOn) {\n      const dep = tasksByName.get(depName);\n      if (dep.end > pred.end) {\n        pred = dep;\n        predName = depName;\n      }\n    }\n    criticalEdges.add(`${predName}=>${current.task}`);\n    current = pred;\n  }\n}\n\nconst labels = rows.map((row) => row.label);\nconst barData = rows.map((row) => [row.start, row.end]);\nconst barBackground = rows.map((row) => (row.isHeader ? \"transparent\" : row.color));\nconst barBorder = rows.map((row) => row.color);\nconst barBorderWidth = rows.map((row) => (row.isHeader ? 3 : 0));\n\nfunction formatDay(offset) {\n  const date = new Date(PROJECT_START.getTime() + offset * 86400000);\n  return date.toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" });\n}\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// Find a day offset in [loX, hiX] for the elbow's vertical segment that\n// avoids passing through any of the given task bars (each a [start, end]\n// range), preferring the value closest to the midpoint.\nfunction freeElbowX(loX, hiX, blockingRanges) {\n  const mid = (loX + hiX) / 2;\n  const isBlocked = (x) => blockingRanges.some(([s, e]) => x > s && x < e);\n  if (!isBlocked(mid)) return mid;\n  const step = 0.1;\n  for (let d = step; d <= (hiX - loX) / 2 + step; d += step) {\n    const right = mid + d;\n    if (right <= hiX && !isBlocked(right)) return right;\n    const left = mid - d;\n    if (left >= loX && !isBlocked(left)) return left;\n  }\n  return mid;\n}\n\n// --- Dependency-arrow plugin (finish-to-start connectors) --------------------\nconst dependencyArrows = {\n  id: \"dependencyArrows\",\n  afterDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n    ctx.save();\n    const arrowSize = 11;\n    for (const task of tasks) {\n      if (!task.dependsOn.length) continue;\n      const toRow = taskRowIndex.get(task.task);\n      const toY = scales.y.getPixelForValue(toRow);\n      const toX = scales.x.getPixelForValue(task.start);\n      for (const depName of task.dependsOn) {\n        const pred = tasks.find((candidate) => candidate.task === depName);\n        if (!pred) continue;\n        const fromRow = taskRowIndex.get(pred.task);\n        const fromY = scales.y.getPixelForValue(fromRow);\n        const fromX = scales.x.getPixelForValue(pred.end);\n\n        // Route the elbow's vertical segment through a day offset not\n        // covered by any task bar strictly between the two rows, so the\n        // connector doesn't cut across unrelated tasks.\n        const rowLo = Math.min(fromRow, toRow);\n        const rowHi = Math.max(fromRow, toRow);\n        const blockingRanges = rows\n          .slice(rowLo + 1, rowHi)\n          .filter((row) => !row.isHeader)\n          .map((row) => [row.start, row.end]);\n        const midDay = freeElbowX(pred.end, task.start, blockingRanges);\n        const midX = scales.x.getPixelForValue(midDay);\n\n        const isCritical = criticalEdges.has(`${depName}=>${task.task}`);\n        ctx.strokeStyle = isCritical ? t.ink : t.inkSoft;\n        ctx.fillStyle = isCritical ? t.ink : t.inkSoft;\n        ctx.lineWidth = isCritical ? 3 : 2;\n\n        ctx.beginPath();\n        ctx.moveTo(fromX, fromY);\n        ctx.lineTo(midX, fromY);\n        ctx.lineTo(midX, toY);\n        ctx.lineTo(toX - arrowSize, toY);\n        ctx.stroke();\n\n        // Arrowhead pointing into the successor bar's start edge.\n        ctx.beginPath();\n        ctx.moveTo(toX, toY);\n        ctx.lineTo(toX - arrowSize, toY - arrowSize / 2);\n        ctx.lineTo(toX - arrowSize, toY + arrowSize / 2);\n        ctx.closePath();\n        ctx.fill();\n      }\n    }\n    ctx.restore();\n  },\n};\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels,\n    datasets: [\n      {\n        label: \"Timeline\",\n        data: barData,\n        backgroundColor: barBackground,\n        borderColor: barBorder,\n        borderWidth: barBorderWidth,\n        borderSkipped: false,\n        barPercentage: 0.7,\n        categoryPercentage: 0.8,\n      },\n    ],\n  },\n  options: {\n    indexAxis: \"y\",\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { right: 24 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"Software Delivery Plan · gantt-dependencies · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 19 }, // 67/79 chars × 22px default title size, rounded\n        padding: { bottom: 20 },\n      },\n      legend: { display: false },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        min: 0,\n        max: 45,\n        title: { display: true, text: \"Project Timeline (2026)\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 15 }, stepSize: 5, callback: (value) => formatDay(value) },\n        grid: { color: t.grid },\n      },\n      y: {\n        reverse: false,\n        ticks: {\n          color: (ctx) => (rows[ctx.index] && rows[ctx.index].isHeader ? t.ink : t.inkSoft),\n          font: (ctx) => (rows[ctx.index] && rows[ctx.index].isHeader ? { size: 16, weight: \"bold\" } : { size: 15 }),\n        },\n        grid: { display: false },\n      },\n    },\n  },\n  plugins: [dependencyArrows],\n});\n"}