{"spec_id":"gantt-dependencies","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\ngantt-dependencies: Gantt Chart with Dependencies\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Prevent this file from shadowing the installed bokeh package\n_this = str(Path(__file__).parent.resolve())\nsys.path = [p for p in sys.path if p not in (\"\", _this)]\n\nimport pandas as pd\nfrom bokeh.io import output_file, save\nfrom bokeh.models import BoxAnnotation, ColumnDataSource, HoverTool, LabelSet, Legend, LegendItem\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens (Imprint palette + theme-adaptive chrome)\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\n# Imprint categorical palette — positions 1-4 for 4 project phases\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data — software development project with task dependencies\ntasks_data = [\n    # Requirements Phase\n    {\n        \"task\": \"Requirements Gathering\",\n        \"start\": \"2026-01-06\",\n        \"end\": \"2026-01-10\",\n        \"group\": \"Requirements\",\n        \"depends_on\": [],\n    },\n    {\n        \"task\": \"Stakeholder Review\",\n        \"start\": \"2026-01-13\",\n        \"end\": \"2026-01-15\",\n        \"group\": \"Requirements\",\n        \"depends_on\": [\"Requirements Gathering\"],\n    },\n    {\n        \"task\": \"Requirements Sign-off\",\n        \"start\": \"2026-01-16\",\n        \"end\": \"2026-01-17\",\n        \"group\": \"Requirements\",\n        \"depends_on\": [\"Stakeholder Review\"],\n    },\n    # Design Phase\n    {\n        \"task\": \"System Architecture\",\n        \"start\": \"2026-01-20\",\n        \"end\": \"2026-01-24\",\n        \"group\": \"Design\",\n        \"depends_on\": [\"Requirements Sign-off\"],\n    },\n    {\n        \"task\": \"Database Design\",\n        \"start\": \"2026-01-27\",\n        \"end\": \"2026-01-30\",\n        \"group\": \"Design\",\n        \"depends_on\": [\"System Architecture\"],\n    },\n    {\n        \"task\": \"UI/UX Design\",\n        \"start\": \"2026-01-27\",\n        \"end\": \"2026-01-31\",\n        \"group\": \"Design\",\n        \"depends_on\": [\"System Architecture\"],\n    },\n    # Development Phase\n    {\n        \"task\": \"Backend Core\",\n        \"start\": \"2026-02-03\",\n        \"end\": \"2026-02-14\",\n        \"group\": \"Development\",\n        \"depends_on\": [\"Database Design\"],\n    },\n    {\n        \"task\": \"Frontend Core\",\n        \"start\": \"2026-02-03\",\n        \"end\": \"2026-02-12\",\n        \"group\": \"Development\",\n        \"depends_on\": [\"UI/UX Design\"],\n    },\n    {\n        \"task\": \"API Integration\",\n        \"start\": \"2026-02-17\",\n        \"end\": \"2026-02-21\",\n        \"group\": \"Development\",\n        \"depends_on\": [\"Backend Core\", \"Frontend Core\"],\n    },\n    # Testing Phase\n    {\n        \"task\": \"Unit Testing\",\n        \"start\": \"2026-02-17\",\n        \"end\": \"2026-02-21\",\n        \"group\": \"Testing\",\n        \"depends_on\": [\"Backend Core\"],\n    },\n    {\n        \"task\": \"Integration Testing\",\n        \"start\": \"2026-02-24\",\n        \"end\": \"2026-02-28\",\n        \"group\": \"Testing\",\n        \"depends_on\": [\"API Integration\", \"Unit Testing\"],\n    },\n    {\n        \"task\": \"User Acceptance\",\n        \"start\": \"2026-03-03\",\n        \"end\": \"2026-03-07\",\n        \"group\": \"Testing\",\n        \"depends_on\": [\"Integration Testing\"],\n    },\n]\n\ndf = pd.DataFrame(tasks_data)\ndf[\"start\"] = pd.to_datetime(df[\"start\"])\ndf[\"end\"] = pd.to_datetime(df[\"end\"])\ndf[\"duration\"] = (df[\"end\"] - df[\"start\"]).dt.days\n\ntask_lookup = {row[\"task\"]: idx for idx, row in df.iterrows()}\n\n# Critical path — forward/backward pass through dependency graph\nsuccessors = {row[\"task\"]: [] for _, row in df.iterrows()}\nfor _, row in df.iterrows():\n    for dep in row[\"depends_on\"]:\n        if dep in successors:\n            successors[dep].append(row[\"task\"])\n\nlp_to = {}\nfor _, row in df.iterrows():\n    task = row[\"task\"]\n    dur = row[\"duration\"]\n    if not row[\"depends_on\"]:\n        lp_to[task] = dur\n    else:\n        lp_to[task] = max(lp_to[dep] for dep in row[\"depends_on\"] if dep in lp_to) + dur\n\nlp_from = {}\nfor _, row in df.sort_values(\"end\", ascending=False).iterrows():\n    task = row[\"task\"]\n    dur = row[\"duration\"]\n    if not successors[task]:\n        lp_from[task] = dur\n    else:\n        lp_from[task] = dur + max(lp_from[s] for s in successors[task])\n\nmax_lp = max(lp_to.values())\ncritical_tasks = {t for t in lp_to if lp_to[t] + lp_from[t] - df.iloc[task_lookup[t]][\"duration\"] == max_lp}\n\n# Imprint palette positions 1–4 for project phases\ngroups = [\"Requirements\", \"Design\", \"Development\", \"Testing\"]\ngroup_colors = dict(zip(groups, IMPRINT_PALETTE[:4], strict=False))\n\n# Group aggregate span bars\ngroup_spans = {}\nfor group in groups:\n    gdf = df[df[\"group\"] == group]\n    group_spans[group] = {\"start\": gdf[\"start\"].min(), \"end\": gdf[\"end\"].max()}\n\n# Y-positions: group header row then indented task rows\ny_positions = {}\ny_labels = []\ny_is_group = []\ncurrent_y = 0\n\nfor group in groups:\n    y_positions[f\"__group__{group}\"] = current_y\n    y_labels.append((current_y, group))\n    y_is_group.append(True)\n    current_y += 1\n    for task in df[df[\"group\"] == group][\"task\"].tolist():\n        y_positions[task] = current_y\n        y_labels.append((current_y, f\"   {task}\"))\n        y_is_group.append(False)\n        current_y += 1\n\nmax_y = current_y\n\n# Title — 48 chars < 67 char baseline, no scaling needed → 50pt\ntitle_text = \"gantt-dependencies · python · bokeh · anyplot.ai\"\nn_title = len(title_text)\ntitle_fs = f\"{max(34, round(50 * 67 / n_title))}pt\" if n_title > 67 else \"50pt\"\n\n# Figure — 3200×1800 landscape canvas (hard rule)\np = figure(\n    width=3200,\n    height=1800,\n    x_axis_type=\"datetime\",\n    y_range=(max_y + 0.5, -0.5),\n    title=title_text,\n    x_axis_label=\"Timeline (Weeks)\",\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=50,\n    min_border_top=110,\n    min_border_right=60,\n)\n\n# Theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\np.title.text_font_size = title_fs\np.title.text_color = INK\np.title.text_font_style = \"bold\"\n\np.xaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.axis_label_text_color = INK\np.xaxis.major_label_text_font_size = \"34pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.xaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.visible = False\n\np.xgrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.12\np.xgrid.grid_line_dash = [6, 4]\np.ygrid.grid_line_alpha = 0.0\n\n# Alternating row bands (theme-adaptive subtle tint)\nfor i in range(max_y):\n    if i % 2 == 0:\n        p.add_layout(\n            BoxAnnotation(\n                bottom=i - 0.5, top=i + 0.5, fill_color=INK, fill_alpha=0.04, level=\"underlay\", line_color=None\n            )\n        )\n\n# Group aggregate span bars (semi-transparent)\ngroup_renderers = {}\nfor group in groups:\n    span = group_spans[group]\n    y = y_positions[f\"__group__{group}\"]\n    src = ColumnDataSource(data={\"y\": [y], \"left\": [span[\"start\"]], \"right\": [span[\"end\"]]})\n    r = p.hbar(y=\"y\", left=\"left\", right=\"right\", height=0.7, color=group_colors[group], alpha=0.2, source=src)\n    group_renderers[group] = r\n\n# Task bars — per-group renderers so legend swatches show full-saturation phase colors\ngroup_task_renderers = {}\nfor group in groups:\n    gdf = df[df[\"group\"] == group]\n    gys, glefts, grights = [], [], []\n    gnames, ggroups, gstarts, gends, gdurations, gdeps, gcrit = [], [], [], [], [], [], []\n    for _, row in gdf.iterrows():\n        gys.append(y_positions[row[\"task\"]])\n        glefts.append(row[\"start\"])\n        grights.append(row[\"end\"])\n        gnames.append(row[\"task\"])\n        ggroups.append(row[\"group\"])\n        gstarts.append(row[\"start\"].strftime(\"%b %d, %Y\"))\n        gends.append(row[\"end\"].strftime(\"%b %d, %Y\"))\n        gdurations.append(f\"{row['duration']} days\")\n        deps = row[\"depends_on\"]\n        gdeps.append(\", \".join(deps) if deps else \"None\")\n        gcrit.append(\"★ Critical Path\" if row[\"task\"] in critical_tasks else \"\")\n    gsrc = ColumnDataSource(\n        data={\n            \"y\": gys,\n            \"left\": glefts,\n            \"right\": grights,\n            \"task_name\": gnames,\n            \"group_name\": ggroups,\n            \"start_str\": gstarts,\n            \"end_str\": gends,\n            \"duration\": gdurations,\n            \"dependencies\": gdeps,\n            \"critical\": gcrit,\n        }\n    )\n    gr = p.hbar(\n        y=\"y\",\n        left=\"left\",\n        right=\"right\",\n        height=0.5,\n        fill_color=group_colors[group],\n        fill_alpha=0.9,\n        line_color=PAGE_BG,\n        line_width=1,\n        source=gsrc,\n    )\n    group_task_renderers[group] = gr\n\n# Critical path border overlay (dark outline on critical tasks)\nfor _, row in df.iterrows():\n    if row[\"task\"] in critical_tasks:\n        y = y_positions[row[\"task\"]]\n        src = ColumnDataSource(data={\"y\": [y], \"left\": [row[\"start\"]], \"right\": [row[\"end\"]]})\n        p.hbar(y=\"y\", left=\"left\", right=\"right\", height=0.54, fill_alpha=0, line_color=INK, line_width=5, source=src)\n\n# Dependency arrows — improved visibility for non-critical (theme-adaptive INK_SOFT)\ncrit_arrow_xs, crit_arrow_ys = [], []\nnorm_arrow_xs, norm_arrow_ys = [], []\ncrit_head_xs, crit_head_ys = [], []\nnorm_head_xs, norm_head_ys = [], []\n\nfor _, row in df.iterrows():\n    task_name = row[\"task\"]\n    task_y = y_positions[task_name]\n    task_start_ms = row[\"start\"].value / 1e6\n    is_task_critical = task_name in critical_tasks\n\n    for dep_name in row[\"depends_on\"]:\n        if dep_name in task_lookup:\n            dep_row = df.iloc[task_lookup[dep_name]]\n            dep_end_ms = dep_row[\"end\"].value / 1e6\n            dep_y = y_positions[dep_name]\n            is_crit_dep = is_task_critical and dep_name in critical_tasks\n\n            h_offset = 1.0 * 24 * 60 * 60 * 1000\n            if task_y != dep_y:\n                mid_x = dep_end_ms + h_offset\n                xs = [dep_end_ms, mid_x, mid_x, task_start_ms]\n                ys = [dep_y, dep_y, task_y, task_y]\n            else:\n                xs = [dep_end_ms, task_start_ms]\n                ys = [dep_y, task_y]\n\n            arrow_size = 3.5 * 24 * 60 * 60 * 1000\n            hxs = [task_start_ms - arrow_size, task_start_ms, task_start_ms - arrow_size]\n            hys = [task_y - 0.18, task_y, task_y + 0.18]\n\n            if is_crit_dep:\n                crit_arrow_xs.append(xs)\n                crit_arrow_ys.append(ys)\n                crit_head_xs.append(hxs)\n                crit_head_ys.append(hys)\n            else:\n                norm_arrow_xs.append(xs)\n                norm_arrow_ys.append(ys)\n                norm_head_xs.append(hxs)\n                norm_head_ys.append(hys)\n\ndep_renderer = None\nif norm_arrow_xs:\n    dep_renderer = p.multi_line(\n        xs=norm_arrow_xs, ys=norm_arrow_ys, line_color=INK_SOFT, line_width=2.5, line_alpha=0.65\n    )\n    p.patches(xs=norm_head_xs, ys=norm_head_ys, fill_color=INK_SOFT, fill_alpha=0.65, line_color=INK_SOFT, line_width=1)\n\ncrit_dep_renderer = None\nif crit_arrow_xs:\n    crit_dep_renderer = p.multi_line(xs=crit_arrow_xs, ys=crit_arrow_ys, line_color=INK, line_width=4, line_alpha=0.9)\n    p.patches(xs=crit_head_xs, ys=crit_head_ys, fill_color=INK, fill_alpha=0.9, line_color=INK, line_width=1)\n\n# Custom y-axis labels via LabelSet (reduced left padding: 12 days vs previous 14)\ngroup_label_ys, group_label_texts = [], []\ntask_label_ys, task_label_texts = [], []\n\nfor (y, label), is_group in zip(y_labels, y_is_group, strict=True):\n    if is_group:\n        group_label_ys.append(y)\n        group_label_texts.append(label)\n    else:\n        task_label_ys.append(y)\n        task_label_texts.append(label)\n\nlabel_x = df[\"start\"].min() - pd.Timedelta(days=1)\n\np.add_layout(\n    LabelSet(\n        x=\"x\",\n        y=\"y\",\n        text=\"text\",\n        source=ColumnDataSource(\n            data={\"y\": group_label_ys, \"text\": group_label_texts, \"x\": [label_x] * len(group_label_ys)}\n        ),\n        text_font_size=\"30pt\",\n        text_font_style=\"bold\",\n        text_align=\"right\",\n        x_offset=-10,\n        text_baseline=\"middle\",\n        text_color=INK,\n    )\n)\n\np.add_layout(\n    LabelSet(\n        x=\"x\",\n        y=\"y\",\n        text=\"text\",\n        source=ColumnDataSource(\n            data={\"y\": task_label_ys, \"text\": task_label_texts, \"x\": [label_x] * len(task_label_ys)}\n        ),\n        text_font_size=\"22pt\",\n        text_align=\"right\",\n        x_offset=-10,\n        text_baseline=\"middle\",\n        text_color=INK_SOFT,\n    )\n)\n\n# X range — 12-day left padding (reduced from 14) to minimise unused canvas space\nx_min = df[\"start\"].min() - pd.Timedelta(days=12)\nx_max = df[\"end\"].max() + pd.Timedelta(days=2)\np.x_range.start = x_min\np.x_range.end = x_max\n\n# Legend — use full-opacity task-bar renderers so swatches show full-saturation phase colors\nlegend_items = []\nfor group in groups:\n    legend_items.append(LegendItem(label=group, renderers=[group_task_renderers[group]]))\nif dep_renderer:\n    legend_items.append(LegendItem(label=\"Dependency\", renderers=[dep_renderer]))\nif crit_dep_renderer:\n    legend_items.append(LegendItem(label=\"Critical Path\", renderers=[crit_dep_renderer]))\n\nlegend = Legend(\n    items=legend_items,\n    location=\"top_right\",\n    label_text_font_size=\"28pt\",\n    label_text_color=INK_SOFT,\n    spacing=12,\n    padding=20,\n    background_fill_color=ELEVATED_BG,\n    background_fill_alpha=0.9,\n    border_line_color=INK_SOFT,\n    border_line_width=1,\n)\np.add_layout(legend)\n\n# Hover tool for interactive HTML\nhover = HoverTool(\n    renderers=list(group_task_renderers.values()),\n    tooltips=[\n        (\"Task\", \"@task_name\"),\n        (\"Phase\", \"@group_name\"),\n        (\"Start\", \"@start_str\"),\n        (\"End\", \"@end_str\"),\n        (\"Duration\", \"@duration\"),\n        (\"Dependencies\", \"@dependencies\"),\n        (\"Status\", \"@critical\"),\n    ],\n)\np.add_tools(hover)\n\n# Save interactive HTML\noutput_file(f\"plot-{THEME}.html\", title=title_text)\nsave(p)\n\n# Save PNG via headless Chrome (Selenium + CDP for exact 3200×1800 viewport)\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\"--headless=new\", \"--no-sandbox\", \"--disable-dev-shm-usage\", \"--disable-gpu\", \"--hide-scrollbars\"):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}