{"spec_id":"gantt-dependencies","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// gantt-dependencies: Gantt Chart with Dependencies\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-08-24\n\nconst t = window.ANYPLOT_TOKENS;\nconst MUTED = window.ANYPLOT_THEME === \"light\" ? \"#6B6A63\" : \"#A8A79F\";\n\n// --- Data: a 4-phase software project, 12 tasks with finish-to-start deps --\nconst PROJECT_START = Date.UTC(2026, 0, 5); // Mon, Jan 5 2026\nconst DAY = 86400000;\nconst dateAt = (day) => PROJECT_START + day * DAY;\n\nconst TASKS = [\n  { id: \"reqs\", task: \"Gather Requirements\", group: \"Requirements\", start: 0, end: 4, dependsOn: [] },\n  { id: \"stake\", task: \"Stakeholder Review\", group: \"Requirements\", start: 4, end: 6, dependsOn: [\"reqs\"] },\n  { id: \"wire\", task: \"Wireframes\", group: \"Design\", start: 6, end: 10, dependsOn: [\"stake\"] },\n  { id: \"arch\", task: \"Architecture Design\", group: \"Design\", start: 6, end: 12, dependsOn: [\"stake\"] },\n  { id: \"dreview\", task: \"Design Review\", group: \"Design\", start: 12, end: 14, dependsOn: [\"wire\", \"arch\"] },\n  { id: \"backend\", task: \"Backend API\", group: \"Development\", start: 14, end: 24, dependsOn: [\"dreview\"] },\n  { id: \"frontend\", task: \"Frontend UI\", group: \"Development\", start: 14, end: 22, dependsOn: [\"dreview\"] },\n  { id: \"schema\", task: \"Database Schema\", group: \"Development\", start: 14, end: 18, dependsOn: [\"dreview\"] },\n  { id: \"integ\", task: \"Integration\", group: \"Development\", start: 24, end: 28, dependsOn: [\"backend\", \"frontend\", \"schema\"] },\n  { id: \"unit\", task: \"Unit Testing\", group: \"Testing\", start: 28, end: 32, dependsOn: [\"integ\"] },\n  { id: \"systest\", task: \"Integration Testing\", group: \"Testing\", start: 32, end: 36, dependsOn: [\"unit\"] },\n  { id: \"uat\", task: \"UAT Sign-off\", group: \"Testing\", start: 36, end: 39, dependsOn: [\"systest\"] },\n];\n\nconst GROUP_ORDER = [...new Set(TASKS.map((task) => task.group))];\nconst GROUP_COLOR = Object.fromEntries(GROUP_ORDER.map((group, i) => [group, t.palette[i]]));\n\n// Display rows: an aggregate header per group followed by its own tasks, so\n// groups always sit above their children (spec's hierarchy requirement).\nconst ROWS = [];\nGROUP_ORDER.forEach((group) => {\n  const groupTasks = TASKS.filter((task) => task.group === group);\n  ROWS.push({\n    type: \"group\",\n    label: group,\n    start: Math.min(...groupTasks.map((task) => task.start)),\n    end: Math.max(...groupTasks.map((task) => task.end)),\n  });\n  groupTasks.forEach((task) => {\n    ROWS.push({ type: \"task\", label: task.task, id: task.id, start: task.start, end: task.end, group });\n  });\n});\n\nconst categories = ROWS.map((row) => row.label);\nconst rowIndexById = Object.fromEntries(ROWS.map((row, i) => [row.id, i]).filter(([id]) => id !== undefined));\n\n// Dependency arrows connect a predecessor's end (right edge) to its\n// successor's start (left edge) — finish-to-start, the only relation used.\nconst DEPENDENCIES = TASKS.flatMap((task) =>\n  task.dependsOn.map((depId) => {\n    const predecessor = TASKS.find((candidate) => candidate.id === depId);\n    return {\n      fromRow: rowIndexById[depId],\n      toRow: rowIndexById[task.id],\n      fromDay: predecessor.end,\n      toDay: task.start,\n    };\n  })\n);\n\n// --- Stacked \"floating bar\" technique ---------------------------------------\n// The core bundle has no xrange/gantt series, so every row is built as a\n// stacked bar: an invisible \"offset\" series pushes the bar to its start day,\n// and a visible duration series draws the actual span on top of it. Stacking\n// in small day-offset integers (not raw epoch-ms timestamps) keeps every\n// series in the stack the same order of magnitude — mixing a ~1.7e12 offset\n// with a ~1e8 duration on the same stack corrupts Highcharts' internal\n// stack geometry. The axis stays a plain linear day count; labels/tooltips\n// convert back to real dates via dateAt() + Highcharts.dateFormat.\nconst offsetData = ROWS.map((row) => row.start);\nconst phaseTotalData = ROWS.map((row) =>\n  row.type === \"group\" ? { y: row.end - row.start, start: dateAt(row.start), end: dateAt(row.end) } : null\n);\nconst taskSeriesByGroup = GROUP_ORDER.map((group) => ({\n  name: group,\n  color: GROUP_COLOR[group],\n  data: ROWS.map((row) =>\n    row.type === \"task\" && row.group === group\n      ? { y: row.end - row.start, start: dateAt(row.start), end: dateAt(row.end) }\n      : null\n  ),\n}));\n\nconst lastEndDay = Math.max(...TASKS.map((task) => task.end));\n\n// Reusable rightward arrowhead — every dependency flows left to right in time.\nHighcharts.SVGRenderer.prototype.symbols.ganttArrow = (x, y, w, h) => [\n  \"M\", x, y,\n  \"L\", x + w, y + h / 2,\n  \"L\", x, y + h,\n  \"Z\",\n];\n\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"bar\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    marginLeft: 210,\n    style: { fontFamily: \"inherit\" },\n    events: {\n      // The core bundle has no annotations module either, so dependency\n      // arrows are drawn as native SVG paths through chart.renderer — each\n      // predecessor→successor pair converted from data to screen space via\n      // toPixels() on every render pass, exactly like the quiver-basic\n      // vector-field technique.\n      render() {\n        if (this.depGroup) this.depGroup.destroy();\n        this.depGroup = this.renderer.g(\"dependencies\").add();\n        const rowAxis = this.xAxis[0];\n        const timeAxis = this.yAxis[0];\n\n        DEPENDENCIES.forEach(({ fromRow, toRow, fromDay, toDay }) => {\n          const y1 = rowAxis.toPixels(fromRow, false);\n          const y2 = rowAxis.toPixels(toRow, false);\n          const x1 = timeAxis.toPixels(fromDay, false);\n          const x2 = timeAxis.toPixels(toDay, false);\n          const jog = Math.max(6, Math.min(16, (x2 - x1) / 2));\n          const midX = x1 + jog;\n\n          this.renderer\n            .path([\"M\", x1, y1, \"L\", midX, y1, \"L\", midX, y2, \"L\", x2 - 7, y2])\n            .attr({ stroke: MUTED, \"stroke-width\": 1.5, fill: \"none\", opacity: 0.8, zIndex: 6 })\n            .add(this.depGroup);\n          this.renderer\n            .symbol(\"ganttArrow\", x2 - 8, y2 - 4, 8, 8)\n            .attr({ fill: MUTED, opacity: 0.85, zIndex: 6 })\n            .add(this.depGroup);\n        });\n      },\n    },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"gantt-dependencies · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Arrows mark finish-to-start dependencies between tasks\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    categories,\n    reversed: true,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineWidth: 0,\n    labels: {\n      useHTML: true,\n      style: { fontSize: \"14px\" },\n      formatter() {\n        const row = ROWS[this.pos];\n        return row.type === \"group\"\n          ? `<span style=\"color:${t.ink};font-weight:700;font-size:15px\">${row.label}</span>`\n          : `<span style=\"display:inline-block;padding-left:18px;color:${t.inkSoft}\">${row.label}</span>`;\n      },\n    },\n  },\n  yAxis: {\n    min: -3,\n    max: lastEndDay + 3,\n    startOnTick: false,\n    endOnTick: false,\n    // Highcharts defaults reversedStacks:true, which (combined with an\n    // inverted \"bar\" chart) silently anchors every stacked series back to 0\n    // instead of the previous series' cumulative — breaking the invisible\n    // offset + visible duration floating-bar technique used here.\n    reversedStacks: false,\n    tickInterval: 7,\n    title: { text: \"Project Timeline\", style: { color: t.inkSoft, fontSize: \"16px\" } },\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: {\n      style: { color: t.inkSoft, fontSize: \"14px\" },\n      formatter() {\n        return Highcharts.dateFormat(\"%b %e\", dateAt(this.value));\n      },\n    },\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  tooltip: {\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    style: { color: t.ink, fontSize: \"13px\" },\n    formatter() {\n      const point = this.point;\n      if (point.start === undefined) return false;\n      const fmt = (ms) => Highcharts.dateFormat(\"%b %e, %Y\", ms);\n      return `<b>${this.x}</b><br/>${fmt(point.start)} – ${fmt(point.end)}`;\n    },\n  },\n  plotOptions: {\n    series: { animation: false },\n    bar: { stacking: \"normal\", pointPadding: 0.08, groupPadding: 0.12, borderWidth: 0 },\n  },\n  series: [\n    {\n      name: \"offset\",\n      data: offsetData,\n      color: \"transparent\",\n      enableMouseTracking: false,\n      showInLegend: false,\n    },\n    {\n      name: \"Phase total\",\n      data: phaseTotalData,\n      color: t.ink,\n      opacity: 0.16,\n    },\n    ...taskSeriesByGroup,\n  ],\n});\n"}