{"spec_id":"gantt-dependencies","library":"echarts","language":"javascript","code":"// anyplot.ai\n// gantt-dependencies: Gantt Chart with Dependencies\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-25\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Software release plan: four sequential phases, each a group of dependent tasks.\nconst day = (year, month, date) => Date.UTC(year, month - 1, date);\n\nconst rows = [\n  { name: \"Requirements\", isGroup: true },\n  { name: \"Gather Requirements\", group: \"Requirements\", start: day(2026, 1, 5), end: day(2026, 1, 9), deps: [] },\n  { name: \"Stakeholder Review\", group: \"Requirements\", start: day(2026, 1, 9), end: day(2026, 1, 12), deps: [\"Gather Requirements\"] },\n  { name: \"Design\", isGroup: true },\n  { name: \"System Architecture\", group: \"Design\", start: day(2026, 1, 12), end: day(2026, 1, 19), deps: [\"Stakeholder Review\"] },\n  { name: \"UI/UX Design\", group: \"Design\", start: day(2026, 1, 12), end: day(2026, 1, 21), deps: [\"Stakeholder Review\"] },\n  { name: \"Development\", isGroup: true },\n  { name: \"Backend API\", group: \"Development\", start: day(2026, 1, 19), end: day(2026, 2, 2), deps: [\"System Architecture\"] },\n  { name: \"Database Setup\", group: \"Development\", start: day(2026, 1, 19), end: day(2026, 1, 26), deps: [\"System Architecture\"] },\n  { name: \"Frontend Implementation\", group: \"Development\", start: day(2026, 1, 21), end: day(2026, 2, 4), deps: [\"UI/UX Design\"] },\n  { name: \"Testing\", isGroup: true },\n  { name: \"Integration Testing\", group: \"Testing\", start: day(2026, 2, 4), end: day(2026, 2, 9), deps: [\"Backend API\", \"Database Setup\", \"Frontend Implementation\"] },\n  { name: \"QA Review\", group: \"Testing\", start: day(2026, 2, 9), end: day(2026, 2, 12), deps: [\"Integration Testing\"] },\n  { name: \"Deployment\", group: \"Testing\", start: day(2026, 2, 12), end: day(2026, 2, 13), deps: [\"QA Review\"] },\n];\n\n// Group headers show the aggregate span of their child tasks.\nrows.forEach((row) => {\n  if (!row.isGroup) return;\n  const children = rows.filter((r) => r.group === row.name);\n  row.start = Math.min(...children.map((r) => r.start));\n  row.end = Math.max(...children.map((r) => r.end));\n});\n\nconst rowIndex = new Map(rows.map((row, i) => [row.name, i]));\nconst groupColor = {\n  Requirements: t.palette[0],\n  Design: t.palette[1],\n  Development: t.palette[2],\n  Testing: t.palette[3],\n};\n\nfunction hexToRgba(hex, alpha) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\n// One bar per row — group headers render as thin outlined spans, tasks as solid bars.\nconst barData = rows.map((row, i) => {\n  const color = groupColor[row.group || row.name];\n  return {\n    value: [i, row.start, row.end, row.isGroup ? 1 : 0],\n    itemStyle: row.isGroup\n      ? { color: hexToRgba(color, 0.28), borderColor: color, borderWidth: 2 }\n      : { color },\n  };\n});\n\n// Critical path (CPM forward/backward pass): a dependency edge is on the\n// critical path when both ends have zero slack against the fixed schedule\n// and the edge itself is the tight one (no idle gap) — this is what\n// distinguishes the true scheduling bottleneck (e.g. Frontend Implementation\n// feeding Integration Testing) from a converging predecessor with slack\n// (e.g. Database Setup, which finishes days early).\nconst taskRows = rows.filter((r) => !r.isGroup);\nconst successorsOf = new Map(taskRows.map((r) => [r.name, []]));\ntaskRows.forEach((r) => (r.deps || []).forEach((dep) => successorsOf.get(dep).push(r.name)));\nconst taskByName = new Map(taskRows.map((r) => [r.name, r]));\nconst projectFinish = Math.max(...taskRows.map((r) => r.end));\n\nconst lateFinish = new Map();\nfor (let i = taskRows.length - 1; i >= 0; i--) {\n  const r = taskRows[i];\n  const succs = successorsOf.get(r.name);\n  lateFinish.set(\n    r.name,\n    succs.length\n      ? Math.min(...succs.map((s) => lateFinish.get(s) - (taskByName.get(s).end - taskByName.get(s).start)))\n      : projectFinish\n  );\n}\nconst isCritical = (name) => {\n  const r = taskByName.get(name);\n  return lateFinish.get(name) - (r.end - r.start) === r.start;\n};\n\n// Dependency edges: predecessor's end (finish) -> successor's start.\nconst depData = [];\nrows.forEach((row) => {\n  if (!row.deps) return;\n  row.deps.forEach((predName) => {\n    const pred = rows[rowIndex.get(predName)];\n    const critical = pred.end === row.start && isCritical(predName) && isCritical(row.name);\n    depData.push({ value: [rowIndex.get(predName), pred.end, rowIndex.get(row.name), row.start, critical ? 1 : 0] });\n  });\n});\ndepData.sort((a, b) => a.value[4] - b.value[4]); // critical-path arrows paint last, on top\n\nfunction renderTask(params, api) {\n  const idx = api.value(0);\n  const isGroup = api.value(3) === 1;\n  const start = api.coord([api.value(1), idx]);\n  const end = api.coord([api.value(2), idx]);\n  const rowHeight = api.size([0, 1])[1];\n  const barHeight = isGroup ? rowHeight * 0.32 : rowHeight * 0.56;\n  const shape = echarts.graphic.clipRectByRect(\n    { x: start[0], y: start[1] - barHeight / 2, width: Math.max(end[0] - start[0], 2), height: barHeight },\n    { x: params.coordSys.x, y: params.coordSys.y, width: params.coordSys.width, height: params.coordSys.height }\n  );\n  if (shape) shape.r = isGroup ? 2 : 4;\n  return shape && { type: \"rect\", shape, style: api.style() };\n}\n\n// Elbow connector (horizontal-vertical-horizontal) with an arrowhead landing on\n// the successor's left edge — keeps the line out of the row band between the\n// two tasks rather than cutting diagonally across intermediate bars. Edges on\n// the critical path render bolder and in accent amber so the scheduling\n// bottleneck reads at a glance.\nfunction renderDependency(params, api) {\n  const p1 = api.coord([api.value(1), api.value(0)]);\n  const p2 = api.coord([api.value(3), api.value(2)]);\n  const critical = api.value(4) === 1;\n  const stroke = critical ? t.amber : t.inkSoft;\n  const lineWidth = critical ? 3 : 2;\n  const arrow = critical ? 11 : 9;\n  const gap = Math.min(28, Math.max((p2[0] - p1[0]) / 2, 10));\n  const midX = p1[0] + gap;\n  return {\n    type: \"group\",\n    children: [\n      {\n        type: \"polyline\",\n        shape: {\n          points: [\n            [p1[0], p1[1]],\n            [midX, p1[1]],\n            [midX, p2[1]],\n            [p2[0] - arrow, p2[1]],\n          ],\n        },\n        style: { stroke, lineWidth, fill: \"none\" },\n      },\n      {\n        type: \"polygon\",\n        shape: {\n          points: [\n            [p2[0], p2[1]],\n            [p2[0] - arrow, p2[1] - arrow / 2],\n            [p2[0] - arrow, p2[1] + arrow / 2],\n          ],\n        },\n        style: { fill: stroke },\n      },\n    ],\n  };\n}\n\nfunction formatDate(ts) {\n  const months = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\n  const dt = new Date(ts);\n  return `${months[dt.getUTCMonth()]} ${dt.getUTCDate()}`;\n}\n\nfunction formatRowLabel(name, index) {\n  return rows[index].isGroup ? `{group|${name}}` : `{task|${name}}`;\n}\n\nconst title = \"Software Release Plan · gantt-dependencies · javascript · echarts · anyplot.ai\";\nconst titleFontSize = title.length > 67 ? Math.round(24 * (67 / title.length)) : 24;\n\nconst minStart = Math.min(...rows.map((r) => r.start));\nconst maxEnd = Math.max(...rows.map((r) => r.end));\nconst pad = 2 * 24 * 60 * 60 * 1000; // 2-day breathing room on each side\n\n// --- Init --------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option --------------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: title,\n    subtext: \"Amber connectors trace the critical path (zero slack) through Integration Testing to Deployment\",\n    left: \"center\",\n    top: 18,\n    textStyle: { color: t.ink, fontSize: titleFontSize, fontWeight: 500 },\n    subtextStyle: { color: t.inkSoft, fontSize: 13 },\n  },\n  grid: { left: 300, right: 70, top: 116, bottom: 76 },\n  xAxis: {\n    // A plain value axis (timestamps in ms) rather than type \"time\" — the\n    // \"time\" axis's automatic \"nice\" tick picker always adds a tick at each\n    // month boundary in addition to its regular interval, which can land the\n    // two ticks a single day apart and overlap. A value axis honors a fixed\n    // \"interval\" exactly, so a 4-day cadence is guaranteed to space out labels.\n    type: \"value\",\n    min: minStart - pad,\n    max: maxEnd + pad,\n    interval: 4 * 24 * 60 * 60 * 1000,\n    name: \"Timeline\",\n    nameLocation: \"middle\",\n    nameGap: 34,\n    nameTextStyle: { color: t.inkSoft, fontSize: 13 },\n    axisLabel: { color: t.inkSoft, fontSize: 14, formatter: formatDate },\n    axisLine: { lineStyle: { color: t.grid, width: 1 } },\n    axisTick: { show: false },\n    splitLine: { show: true, lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"category\",\n    inverse: true,\n    data: rows.map((r) => r.name),\n    axisLabel: {\n      fontSize: 15,\n      formatter: formatRowLabel,\n      rich: {\n        group: { color: t.ink, fontWeight: 700, fontSize: 16 },\n        task: { color: t.inkSoft, fontSize: 14, padding: [0, 0, 0, 22] },\n      },\n    },\n    axisLine: { lineStyle: { color: t.grid, width: 1 } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  series: [\n    { type: \"custom\", name: \"Tasks\", renderItem: renderTask, encode: { x: [1, 2], y: 0 }, data: barData, z: 2 },\n    { type: \"custom\", name: \"Dependencies\", renderItem: renderDependency, data: depData, silent: true, z: 3 },\n  ],\n});\nchart.on(\"finished\", () => {\n  window.__anyplotReady = true;\n});\n"}