{"spec_id":"gantt-dependencies","library":"muix","language":"javascript","code":"// anyplot.ai\n// gantt-dependencies: Gantt Chart with Dependencies\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-08-25\nimport { BarChart } from \"@mui/x-charts/BarChart\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Software release plan: four phases, each a group of dependent tasks.\n// start/end are day offsets from 2026-01-01 (Day 0 = Jan 1).\nconst PHASES = [\n  {\n    name: \"Requirements\",\n    tasks: [\n      { task: \"Gather requirements\", start: 0, end: 4, dependsOn: [] },\n      { task: \"Stakeholder review\", start: 4, end: 7, dependsOn: [\"Gather requirements\"] },\n      { task: \"Finalize scope\", start: 7, end: 9, dependsOn: [\"Stakeholder review\"] },\n    ],\n  },\n  {\n    name: \"Design\",\n    tasks: [\n      { task: \"UI wireframes\", start: 9, end: 14, dependsOn: [\"Finalize scope\"] },\n      { task: \"Database schema\", start: 9, end: 15, dependsOn: [\"Finalize scope\"] },\n      { task: \"Architecture review\", start: 15, end: 17, dependsOn: [\"UI wireframes\", \"Database schema\"] },\n    ],\n  },\n  {\n    name: \"Development\",\n    tasks: [\n      { task: \"Backend API\", start: 17, end: 27, dependsOn: [\"Architecture review\"] },\n      { task: \"Frontend UI\", start: 17, end: 29, dependsOn: [\"Architecture review\"] },\n      { task: \"Integration\", start: 29, end: 33, dependsOn: [\"Backend API\", \"Frontend UI\"] },\n    ],\n  },\n  {\n    name: \"Testing\",\n    tasks: [\n      { task: \"Unit testing\", start: 33, end: 36, dependsOn: [\"Integration\"] },\n      { task: \"QA testing\", start: 36, end: 40, dependsOn: [\"Unit testing\"] },\n      { task: \"UAT sign-off\", start: 40, end: 43, dependsOn: [\"QA testing\"] },\n      { task: \"Deployment\", start: 43, end: 44, dependsOn: [\"UAT sign-off\"] },\n    ],\n  },\n];\n\nconst MONTH_LENGTHS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; // 2026 (non-leap)\nconst MONTH_NAMES = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\n\nfunction formatDay(dayOffset) {\n  let day = Math.round(dayOffset) + 1;\n  let month = 0;\n  while (day > MONTH_LENGTHS[month]) {\n    day -= MONTH_LENGTHS[month];\n    month += 1;\n  }\n  return `${MONTH_NAMES[month]} ${day}`;\n}\n\n// Flatten into rows (group header, then its child tasks) — top-to-bottom order.\n// The band y-axis places array index 0 at the top row, so this array order is\n// already the desired reading order (earliest phase first).\nconst rows = [];\nconst rowLabelByTask = {};\nPHASES.forEach((phase, phaseIndex) => {\n  const groupStart = Math.min(...phase.tasks.map((task) => task.start));\n  const groupEnd = Math.max(...phase.tasks.map((task) => task.end));\n  rows.push({ label: phase.name, kind: \"group\", start: groupStart, end: groupEnd, phaseIndex });\n  phase.tasks.forEach((task) => {\n    const label = `↳ ${task.task}`;\n    rowLabelByTask[task.task] = label;\n    rows.push({ label, kind: \"task\", start: task.start, end: task.end, phaseIndex, dependsOn: task.dependsOn });\n  });\n});\n\nconst rowLabels = rows.map((row) => row.label);\nconst maxDay = Math.max(...rows.map((row) => row.end));\n\nconst offsetData = rows.map((row) => row.start);\nconst groupSpanData = rows.map((row) => (row.kind === \"group\" ? row.end - row.start : null));\nconst phaseTaskData = PHASES.map((_, phaseIndex) =>\n  rows.map((row) => (row.kind === \"task\" && row.phaseIndex === phaseIndex ? row.end - row.start : null)),\n);\n\n// Dependency edges — predecessor's end (right edge) to successor's start (left edge).\nconst edges = [];\nrows.forEach((row) => {\n  if (row.kind !== \"task\") return;\n  row.dependsOn.forEach((depName) => {\n    const fromLabel = rowLabelByTask[depName];\n    const fromRow = rows.find((candidate) => candidate.label === fromLabel);\n    if (!fromRow) return;\n    edges.push({ fromLabel, fromDay: fromRow.end, toLabel: row.label, toDay: row.start });\n  });\n});\n\nconst series = [\n  { id: \"offset\", data: offsetData, stack: \"total\", color: \"transparent\" },\n  { id: \"group-span\", label: \"Phase span\", data: groupSpanData, stack: \"total\", color: t.ink },\n  ...PHASES.map((phase, phaseIndex) => ({\n    id: `phase-${phaseIndex}`,\n    label: `${phase.name} tasks`,\n    data: phaseTaskData[phaseIndex],\n    stack: \"total\",\n    color: t.palette[phaseIndex],\n  })),\n];\n\nconst TITLE = \"gantt-dependencies · javascript · muix · anyplot.ai\";\nconst TITLE_HEIGHT = 44;\nconst CAPTION_HEIGHT = 26;\n\n// --- Custom overlay: dependency connector arrows -----------------------------\n// Reads the chart's real x/y scales so arrows land exactly on bar edges — not a\n// drawn approximation. Rendered as a child of <BarChart>, inside its context.\nfunction DependencyArrows() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const bandwidth = typeof yScale.bandwidth === \"function\" ? yScale.bandwidth() : 0;\n\n  return (\n    <g>\n      <defs>\n        <marker id=\"dep-arrowhead\" markerWidth=\"10\" markerHeight=\"10\" refX=\"7\" refY=\"3\" orient=\"auto\">\n          <path d=\"M0,0 L8,3 L0,6 Z\" fill={t.inkSoft} />\n        </marker>\n      </defs>\n      {edges.map((edge) => {\n        const fromX = xScale(edge.fromDay);\n        const fromY = yScale(edge.fromLabel) + bandwidth / 2;\n        const toX = xScale(edge.toDay);\n        const toY = yScale(edge.toLabel) + bandwidth / 2;\n        const midX = fromX + 14;\n        const d = `M ${fromX} ${fromY} L ${midX} ${fromY} L ${midX} ${toY} L ${toX - 6} ${toY}`;\n        return (\n          <path\n            key={`${edge.fromLabel}->${edge.toLabel}`}\n            d={d}\n            stroke={t.inkSoft}\n            strokeWidth={1.5}\n            fill=\"none\"\n            markerEnd=\"url(#dep-arrowhead)\"\n          />\n        );\n      })}\n    </g>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  const chartHeight = window.ANYPLOT_SIZE.height - TITLE_HEIGHT - CAPTION_HEIGHT;\n\n  return (\n    <Box\n      sx={{\n        width: window.ANYPLOT_SIZE.width,\n        height: window.ANYPLOT_SIZE.height,\n        display: \"flex\",\n        flexDirection: \"column\",\n        boxSizing: \"border-box\",\n        px: 2,\n      }}\n    >\n      <Typography sx={{ fontSize: 22, fontWeight: 500, color: t.ink, lineHeight: `${TITLE_HEIGHT}px` }}>\n        {TITLE}\n      </Typography>\n      <BarChart\n        width={window.ANYPLOT_SIZE.width - 32}\n        height={chartHeight}\n        layout=\"horizontal\"\n        skipAnimation\n        series={series}\n        xAxis={[\n          {\n            scaleType: \"linear\",\n            min: 0,\n            max: maxDay + 2,\n            valueFormatter: (value) => formatDay(value),\n            tickLabelStyle: { fontSize: 13, fill: t.inkSoft },\n            label: \"Project timeline (2026)\",\n            labelStyle: { fontSize: 14, fill: t.inkSoft },\n          },\n        ]}\n        yAxis={[\n          {\n            scaleType: \"band\",\n            data: rowLabels,\n            tickLabelStyle: { fontSize: 14, fill: t.ink },\n            categoryGapRatio: 0.3,\n          },\n        ]}\n        grid={{ vertical: true }}\n        margin={{ left: 235, right: 30, top: 70, bottom: 55 }}\n        slotProps={{\n          legend: {\n            position: { vertical: \"top\", horizontal: \"right\" },\n            itemMarkWidth: 12,\n            itemMarkHeight: 12,\n            labelStyle: { fontSize: 13, fill: t.inkSoft },\n            padding: { top: 0, bottom: 20 },\n          },\n        }}\n      >\n        <DependencyArrows />\n      </BarChart>\n      <Typography sx={{ fontSize: 12, color: t.inkSoft, lineHeight: `${CAPTION_HEIGHT}px` }}>\n        {\"→\"} arrow: finish-to-start dependency (predecessor end {\"→\"} successor start)\n      </Typography>\n    </Box>\n  );\n}\n"}