{"spec_id":"gantt-dependencies","library":"d3","language":"javascript","code":"// anyplot.ai\n// gantt-dependencies: Gantt Chart with Dependencies\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 94/100 | Updated: 2026-08-25\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 110, right: 60, bottom: 60, left: 230 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Offsets are workdays from the project kickoff date. Database Schema, Unit\n// Testing, and Staging Deployment each start a few days after their\n// predecessor's end so the schedule shows real slack, not one uniformly\n// tight critical-path chain.\nconst PROJECT_START = new Date(2026, 0, 5);\nconst dayToDate = (d) => new Date(PROJECT_START.getTime() + d * 86400000);\n\nconst GROUPS = [\n  {\n    name: \"Requirements\",\n    tasks: [\n      { task: \"Gather Requirements\", start: 0, end: 5 },\n      { task: \"Stakeholder Review\", start: 5, end: 8, dependsOn: [\"Gather Requirements\"] },\n    ],\n  },\n  {\n    name: \"Design\",\n    tasks: [\n      { task: \"System Architecture\", start: 8, end: 14, dependsOn: [\"Stakeholder Review\"] },\n      { task: \"UI/UX Design\", start: 8, end: 16, dependsOn: [\"Stakeholder Review\"] },\n      { task: \"Database Schema\", start: 17, end: 21, dependsOn: [\"System Architecture\"] },\n    ],\n  },\n  {\n    name: \"Development\",\n    tasks: [\n      { task: \"Backend API\", start: 21, end: 35, dependsOn: [\"Database Schema\"] },\n      { task: \"Frontend Components\", start: 16, end: 30, dependsOn: [\"UI/UX Design\"] },\n      { task: \"Auth Module\", start: 21, end: 29, dependsOn: [\"Database Schema\"] },\n      { task: \"Integration\", start: 35, end: 39, dependsOn: [\"Backend API\", \"Frontend Components\"] },\n    ],\n  },\n  {\n    name: \"Testing\",\n    tasks: [\n      { task: \"Unit Testing\", start: 32, end: 40, dependsOn: [\"Auth Module\"] },\n      { task: \"Integration Testing\", start: 39, end: 45, dependsOn: [\"Integration\"] },\n      { task: \"User Acceptance Testing\", start: 45, end: 49, dependsOn: [\"Integration Testing\"] },\n    ],\n  },\n  {\n    name: \"Deployment\",\n    tasks: [\n      { task: \"Staging Deployment\", start: 52, end: 55, dependsOn: [\"User Acceptance Testing\"] },\n      { task: \"Production Release\", start: 55, end: 58, dependsOn: [\"Staging Deployment\"] },\n    ],\n  },\n];\n\n// --- Flatten into rows (group header row, then its indented task rows) ------\nconst rows = [];\nGROUPS.forEach((group, groupIndex) => {\n  const groupStart = d3.min(group.tasks, (d) => d.start);\n  const groupEnd = d3.max(group.tasks, (d) => d.end);\n  rows.push({ kind: \"group\", name: group.name, start: groupStart, end: groupEnd, groupIndex });\n  group.tasks.forEach((d) => rows.push({ kind: \"task\", ...d, groupIndex }));\n});\n\nconst allStart = d3.min(rows, (d) => d.start);\nconst allEnd = d3.max(rows, (d) => d.end);\n\n// --- Scales -------------------------------------------------------------------\nconst x = d3.scaleTime()\n  .domain([dayToDate(allStart), dayToDate(allEnd)])\n  .nice(d3.timeWeek)\n  .range([0, iw]);\n\nconst rowHeight = ih / rows.length;\nconst y = (i) => i * rowHeight + rowHeight / 2;\n\n// Pixel extent of every row's bar (group aggregate or task), keyed by row\n// index — used below to route dependency arrows around bars they would\n// otherwise cross.\nconst rowExtent = rows.map((row) => ({ xStart: x(dayToDate(row.start)), xEnd: x(dayToDate(row.end)) }));\nconst taskRowIndex = new Map();\nrows.forEach((row, i) => {\n  if (row.kind === \"task\") taskRowIndex.set(row.task, i);\n});\n\n// --- SVG mount ----------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\nsvg.append(\"defs\").append(\"marker\")\n  .attr(\"id\", \"arrowhead\")\n  .attr(\"viewBox\", \"0 0 10 10\")\n  .attr(\"refX\", 9)\n  .attr(\"refY\", 5)\n  .attr(\"markerWidth\", 7)\n  .attr(\"markerHeight\", 7)\n  .attr(\"orient\", \"auto-start-reverse\")\n  .append(\"path\")\n  .attr(\"d\", \"M0,0 L10,5 L0,10 Z\")\n  .attr(\"fill\", t.inkSoft);\n\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Axes -----------------------------------------------------------------\nconst xAxis = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(d3.timeWeek.every(1)).tickFormat(d3.timeFormat(\"%b %d\")));\nxAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\");\nxAxis.selectAll(\"line\").attr(\"stroke\", t.grid);\nxAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\ng.append(\"g\")\n  .attr(\"class\", \"grid\")\n  .selectAll(\"line\")\n  .data(x.ticks(d3.timeWeek.every(1)))\n  .join(\"line\")\n  .attr(\"x1\", (d) => x(d))\n  .attr(\"x2\", (d) => x(d))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// --- Row labels (group names bold, task names indented) -----------------------\ng.selectAll(\".row-label\")\n  .data(rows)\n  .join(\"text\")\n  .attr(\"class\", \"row-label\")\n  .attr(\"x\", (d) => (d.kind === \"group\" ? -30 : -14))\n  .attr(\"y\", (d, i) => y(i))\n  .attr(\"dy\", \"0.32em\")\n  .attr(\"text-anchor\", \"end\")\n  .style(\"font-size\", (d) => (d.kind === \"group\" ? \"14px\" : \"13px\"))\n  .style(\"font-weight\", (d) => (d.kind === \"group\" ? \"600\" : \"400\"))\n  .attr(\"fill\", (d) => (d.kind === \"group\" ? t.ink : t.inkSoft))\n  .text((d) => d.name || d.task);\n\n// --- Group aggregate span bars (thin bracket behind the group's tasks) --------\nconst groupRows = rows.map((d, i) => ({ ...d, rowIndex: i })).filter((d) => d.kind === \"group\");\ng.selectAll(\".group-span\")\n  .data(groupRows)\n  .join(\"rect\")\n  .attr(\"class\", \"group-span\")\n  .attr(\"x\", (d) => rowExtent[d.rowIndex].xStart)\n  .attr(\"y\", (d) => y(d.rowIndex) - rowHeight * 0.16)\n  .attr(\"width\", (d) => rowExtent[d.rowIndex].xEnd - rowExtent[d.rowIndex].xStart)\n  .attr(\"height\", rowHeight * 0.32)\n  .attr(\"rx\", 3)\n  .attr(\"fill\", (d) => t.palette[d.groupIndex % t.palette.length])\n  .attr(\"fill-opacity\", 0.35);\n\n// --- Task bars ------------------------------------------------------------\nconst taskRows = rows.map((d, i) => ({ ...d, rowIndex: i })).filter((d) => d.kind === \"task\");\nconst barHeight = rowHeight * 0.6;\ng.selectAll(\".task-bar\")\n  .data(taskRows)\n  .join(\"rect\")\n  .attr(\"class\", \"task-bar\")\n  .attr(\"x\", (d) => rowExtent[d.rowIndex].xStart)\n  .attr(\"y\", (d) => y(d.rowIndex) - barHeight / 2)\n  .attr(\"width\", (d) => Math.max(rowExtent[d.rowIndex].xEnd - rowExtent[d.rowIndex].xStart, 2))\n  .attr(\"height\", barHeight)\n  .attr(\"rx\", 4)\n  .attr(\"fill\", (d) => t.palette[d.groupIndex % t.palette.length]);\n\n// --- Dependency arrows: predecessor's right edge -> successor's left edge ----\n// The vertical trunk is routed around any bar (task or group-aggregate) that\n// occupies a row strictly between predecessor and successor, so arrows skirt\n// around unrelated bars instead of cutting through them.\nconst edges = [];\ntaskRows.forEach((row) => {\n  (row.dependsOn || []).forEach((depName) => {\n    const fromRow = taskRowIndex.get(depName);\n    if (fromRow === undefined) return;\n    edges.push({ id: `${depName}->${row.task}`, fromRow, toRow: row.rowIndex });\n  });\n});\n\nconst laneX = (fromXEnd, toXStart, intervening) => {\n  const PAD = 12;\n  const naturalMid = fromXEnd + Math.max((toXStart - fromXEnd) / 2, 12);\n  const collides = (cand) => intervening.some((e) => cand >= e.xStart - PAD && cand <= e.xEnd + PAD);\n  if (!intervening.length || !collides(naturalMid)) return naturalMid;\n  const rightDetour = d3.max(intervening, (e) => e.xEnd) + PAD;\n  const leftDetour = d3.min(intervening, (e) => e.xStart) - PAD;\n  const leftValid = leftDetour >= fromXEnd - PAD;\n  if (leftValid && Math.abs(leftDetour - naturalMid) <= Math.abs(rightDetour - naturalMid)) {\n    return leftDetour;\n  }\n  return rightDetour;\n};\n\ng.selectAll(\".dependency-arrow\")\n  .data(edges)\n  .join(\"path\")\n  .attr(\"class\", \"dependency-arrow\")\n  .attr(\"d\", (d) => {\n    const lo = Math.min(d.fromRow, d.toRow);\n    const hi = Math.max(d.fromRow, d.toRow);\n    const intervening = rowExtent.slice(lo + 1, hi);\n    const fromXEnd = rowExtent[d.fromRow].xEnd;\n    const toXStart = rowExtent[d.toRow].xStart;\n    const trunkX = laneX(fromXEnd, toXStart, intervening);\n    const fromY = y(d.fromRow);\n    const toY = y(d.toRow);\n    return d3.line()([\n      [fromXEnd, fromY],\n      [trunkX, fromY],\n      [trunkX, toY],\n      [toXStart - 6, toY],\n    ]);\n  })\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"marker-end\", \"url(#arrowhead)\");\n\n// --- Legend: group colors ------------------------------------------------\nconst legend = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top - 46})`);\nlet legendX = 0;\nGROUPS.forEach((group, i) => {\n  const item = legend.append(\"g\").attr(\"transform\", `translate(${legendX},0)`);\n  item.append(\"rect\")\n    .attr(\"width\", 14)\n    .attr(\"height\", 14)\n    .attr(\"rx\", 3)\n    .attr(\"fill\", t.palette[i % t.palette.length]);\n  const label = item.append(\"text\")\n    .attr(\"x\", 20)\n    .attr(\"y\", 11)\n    .style(\"font-size\", \"13px\")\n    .attr(\"fill\", t.inkSoft)\n    .text(group.name);\n  legendX += 20 + label.node().getComputedTextLength() + 28;\n});\n\n// --- Title ------------------------------------------------------------------\nsvg.append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"gantt-dependencies · javascript · d3 · anyplot.ai\");\n"}