{"spec_id":"gantt-dependencies","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\ngantt-dependencies: Gantt Chart with Dependencies\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\nfrom datetime import datetime, timedelta\n\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_point,\n    geom_rect,\n    geom_segment,\n    ggplot,\n    labs,\n    scale_fill_manual,\n    scale_x_datetime,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\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 palette — 4 project phases (canonical order, positions 1–4)\ngroup_colors = {\"Requirements\": \"#009E73\", \"Design\": \"#C475FD\", \"Development\": \"#4467A3\", \"Testing\": \"#BD8233\"}\n\n# Data: software development project with finish-to-start dependencies\ntasks_data = [\n    {\n        \"task\": \"Gather Requirements\",\n        \"start\": datetime(2024, 1, 1),\n        \"end\": datetime(2024, 1, 10),\n        \"group\": \"Requirements\",\n        \"depends_on\": [],\n    },\n    {\n        \"task\": \"Document Specs\",\n        \"start\": datetime(2024, 1, 10),\n        \"end\": datetime(2024, 1, 17),\n        \"group\": \"Requirements\",\n        \"depends_on\": [\"Gather Requirements\"],\n    },\n    {\n        \"task\": \"Review & Approve\",\n        \"start\": datetime(2024, 1, 17),\n        \"end\": datetime(2024, 1, 20),\n        \"group\": \"Requirements\",\n        \"depends_on\": [\"Document Specs\"],\n    },\n    {\n        \"task\": \"System Architecture\",\n        \"start\": datetime(2024, 1, 20),\n        \"end\": datetime(2024, 1, 30),\n        \"group\": \"Design\",\n        \"depends_on\": [\"Review & Approve\"],\n    },\n    {\n        \"task\": \"Database Design\",\n        \"start\": datetime(2024, 1, 30),\n        \"end\": datetime(2024, 2, 7),\n        \"group\": \"Design\",\n        \"depends_on\": [\"System Architecture\"],\n    },\n    {\n        \"task\": \"UI/UX Mockups\",\n        \"start\": datetime(2024, 1, 20),\n        \"end\": datetime(2024, 2, 3),\n        \"group\": \"Design\",\n        \"depends_on\": [\"Review & Approve\"],\n    },\n    {\n        \"task\": \"Backend API\",\n        \"start\": datetime(2024, 2, 7),\n        \"end\": datetime(2024, 2, 28),\n        \"group\": \"Development\",\n        \"depends_on\": [\"Database Design\"],\n    },\n    {\n        \"task\": \"Frontend Components\",\n        \"start\": datetime(2024, 2, 3),\n        \"end\": datetime(2024, 2, 25),\n        \"group\": \"Development\",\n        \"depends_on\": [\"UI/UX Mockups\"],\n    },\n    {\n        \"task\": \"Integration\",\n        \"start\": datetime(2024, 2, 28),\n        \"end\": datetime(2024, 3, 8),\n        \"group\": \"Development\",\n        \"depends_on\": [\"Backend API\", \"Frontend Components\"],\n    },\n    {\n        \"task\": \"Unit Testing\",\n        \"start\": datetime(2024, 2, 28),\n        \"end\": datetime(2024, 3, 8),\n        \"group\": \"Testing\",\n        \"depends_on\": [\"Backend API\"],\n    },\n    {\n        \"task\": \"Integration Testing\",\n        \"start\": datetime(2024, 3, 8),\n        \"end\": datetime(2024, 3, 15),\n        \"group\": \"Testing\",\n        \"depends_on\": [\"Integration\"],\n    },\n    {\n        \"task\": \"User Acceptance\",\n        \"start\": datetime(2024, 3, 15),\n        \"end\": datetime(2024, 3, 21),\n        \"group\": \"Testing\",\n        \"depends_on\": [\"Integration Testing\"],\n    },\n]\n\ndf = pd.DataFrame(tasks_data)\n\n# Critical path: longest finish-to-start chain through the project\ncritical_path_tasks = {\n    \"Gather Requirements\",\n    \"Document Specs\",\n    \"Review & Approve\",\n    \"System Architecture\",\n    \"Database Design\",\n    \"Backend API\",\n    \"Integration\",\n    \"Integration Testing\",\n    \"User Acceptance\",\n}\n\n# Build y-axis positions — group header above each group's tasks\ngroup_order = [\"Requirements\", \"Design\", \"Development\", \"Testing\"]\ntask_labels = {}\ny_pos = 0\ngroup_positions = {}\ntask_positions = {}\n\nfor grp in group_order:\n    group_positions[grp] = y_pos\n    task_labels[y_pos] = f\"▸ {grp}\"\n    y_pos += 1\n    for _, row in df[df[\"group\"] == grp].iterrows():\n        task_positions[row[\"task\"]] = y_pos\n        task_labels[y_pos] = f\"   {row['task']}\"\n        y_pos += 1\n\n# Group aggregate bars spanning each group's full date range\ngroup_data = [\n    {\n        \"y\": group_positions[grp],\n        \"start\": df[df[\"group\"] == grp][\"start\"].min(),\n        \"end\": df[df[\"group\"] == grp][\"end\"].max(),\n        \"group\": grp,\n        \"ymin\": group_positions[grp] - 0.42,\n        \"ymax\": group_positions[grp] + 0.42,\n    }\n    for grp in group_order\n]\n\n# Task bars split by critical path membership (geom_rect requires ymin/ymax)\ncritical_task_data = []\nnormal_task_data = []\nfor _, row in df.iterrows():\n    y = task_positions[row[\"task\"]]\n    hw = 0.34 if row[\"task\"] in critical_path_tasks else 0.26\n    entry = {\"y\": y, \"start\": row[\"start\"], \"end\": row[\"end\"], \"group\": row[\"group\"], \"ymin\": y - hw, \"ymax\": y + hw}\n    (critical_task_data if row[\"task\"] in critical_path_tasks else normal_task_data).append(entry)\n\nphase_order = pd.CategoricalDtype(categories=group_order, ordered=True)\ngroups_df = pd.DataFrame(group_data)\ngroups_df[\"group\"] = groups_df[\"group\"].astype(phase_order)\ncritical_df = pd.DataFrame(critical_task_data)\ncritical_df[\"group\"] = critical_df[\"group\"].astype(phase_order)\nnormal_df = pd.DataFrame(normal_task_data)\nnormal_df[\"group\"] = normal_df[\"group\"].astype(phase_order)\n\n# L-shaped dependency connectors: vertical leg + horizontal leg + V-arrowhead\narrow_size_days = 1.8\narrow_wing = 0.22\n\nvert_segs = []\nhoriz_segs = []\narrowhead_segs = []\n\nfor _, row in df.iterrows():\n    for dep_name in row[\"depends_on\"]:\n        if dep_name not in task_positions:\n            continue\n        dep_row = df[df[\"task\"] == dep_name].iloc[0]\n        x_bend = dep_row[\"end\"]\n        x_target = row[\"start\"]\n        y_dep = task_positions[dep_name]\n        y_task = task_positions[row[\"task\"]]\n        is_crit = dep_name in critical_path_tasks and row[\"task\"] in critical_path_tasks\n\n        vert_segs.append({\"x_start\": x_bend, \"x_end\": x_bend, \"y_start\": y_dep, \"y_end\": y_task, \"critical\": is_crit})\n        horiz_segs.append(\n            {\n                \"x_start\": x_bend,\n                \"x_end\": x_target - timedelta(days=arrow_size_days * 0.3),\n                \"y_start\": y_task,\n                \"y_end\": y_task,\n                \"critical\": is_crit,\n            }\n        )\n        # V-arrowhead: two lines converging at x_target\n        arrowhead_segs.append(\n            {\n                \"x_start\": x_target - timedelta(days=arrow_size_days),\n                \"x_end\": x_target,\n                \"y_start\": y_task - arrow_wing,\n                \"y_end\": y_task,\n                \"critical\": is_crit,\n            }\n        )\n        arrowhead_segs.append(\n            {\n                \"x_start\": x_target - timedelta(days=arrow_size_days),\n                \"x_end\": x_target,\n                \"y_start\": y_task + arrow_wing,\n                \"y_end\": y_task,\n                \"critical\": is_crit,\n            }\n        )\n\nvert_df = pd.DataFrame(vert_segs)\nhoriz_df = pd.DataFrame(horiz_segs)\narrowhead_df = pd.DataFrame(arrowhead_segs)\n\ncrit_vert = vert_df[vert_df[\"critical\"]]\nnorm_vert = vert_df[~vert_df[\"critical\"]]\ncrit_horiz = horiz_df[horiz_df[\"critical\"]]\nnorm_horiz = horiz_df[~horiz_df[\"critical\"]]\ncrit_heads = arrowhead_df[arrowhead_df[\"critical\"]]\nnorm_heads = arrowhead_df[~arrowhead_df[\"critical\"]]\n\n# Milestone diamonds at key phase-handoff points\nmilestones = pd.DataFrame(\n    [\n        {\"x\": datetime(2024, 1, 20), \"y\": task_positions[\"Review & Approve\"]},\n        {\"x\": datetime(2024, 2, 7), \"y\": task_positions[\"Database Design\"]},\n        {\"x\": datetime(2024, 2, 28), \"y\": task_positions[\"Backend API\"]},\n        {\"x\": datetime(2024, 3, 21), \"y\": task_positions[\"User Acceptance\"]},\n    ]\n)\n\ny_breaks = list(task_labels.keys())\ny_labels_list = [task_labels[y] for y in y_breaks]\n\ntitle = \"gantt-dependencies · python · plotnine · anyplot.ai\"\nn = len(title)\ntitle_fontsize = max(round(12 * 67 / n) if n > 67 else 12, 8)\n\nplot = (\n    ggplot()\n    # Non-critical task bars (subdued)\n    + geom_rect(\n        data=normal_df,\n        mapping=aes(xmin=\"start\", xmax=\"end\", ymin=\"ymin\", ymax=\"ymax\", fill=\"group\"),\n        color=None,\n        alpha=0.5,\n    )\n    # Critical path task bars (prominent, slightly taller)\n    + geom_rect(\n        data=critical_df,\n        mapping=aes(xmin=\"start\", xmax=\"end\", ymin=\"ymin\", ymax=\"ymax\", fill=\"group\"),\n        color=None,\n        alpha=1.0,\n    )\n    # Group aggregate bars (tallest — span each phase)\n    + geom_rect(\n        data=groups_df,\n        mapping=aes(xmin=\"start\", xmax=\"end\", ymin=\"ymin\", ymax=\"ymax\", fill=\"group\"),\n        color=None,\n        alpha=0.85,\n    )\n    # Non-critical connectors\n    + geom_segment(\n        data=norm_vert,\n        mapping=aes(x=\"x_start\", xend=\"x_end\", y=\"y_start\", yend=\"y_end\"),\n        color=INK_SOFT,\n        size=0.4,\n        alpha=0.7,\n    )\n    + geom_segment(\n        data=norm_horiz,\n        mapping=aes(x=\"x_start\", xend=\"x_end\", y=\"y_start\", yend=\"y_end\"),\n        color=INK_SOFT,\n        size=0.4,\n        alpha=0.7,\n    )\n    + geom_segment(\n        data=norm_heads,\n        mapping=aes(x=\"x_start\", xend=\"x_end\", y=\"y_start\", yend=\"y_end\"),\n        color=INK_SOFT,\n        size=0.6,\n        alpha=0.75,\n    )\n    # Critical path connectors (bolder, theme-ink color)\n    + geom_segment(\n        data=crit_vert,\n        mapping=aes(x=\"x_start\", xend=\"x_end\", y=\"y_start\", yend=\"y_end\"),\n        color=INK,\n        size=0.6,\n        alpha=0.9,\n    )\n    + geom_segment(\n        data=crit_horiz,\n        mapping=aes(x=\"x_start\", xend=\"x_end\", y=\"y_start\", yend=\"y_end\"),\n        color=INK,\n        size=0.6,\n        alpha=0.9,\n    )\n    + geom_segment(\n        data=crit_heads,\n        mapping=aes(x=\"x_start\", xend=\"x_end\", y=\"y_start\", yend=\"y_end\"),\n        color=INK,\n        size=0.9,\n        alpha=0.95,\n    )\n    # Milestone diamonds at phase-handoff points\n    + geom_point(\n        data=milestones, mapping=aes(x=\"x\", y=\"y\"), shape=\"D\", size=2, color=\"#AE3030\", fill=\"#AE3030\", alpha=0.9\n    )\n    # Scales\n    + scale_fill_manual(values=group_colors, name=\"Project Phase\", limits=group_order)\n    + scale_y_continuous(breaks=y_breaks, labels=y_labels_list, trans=\"reverse\")\n    + scale_x_datetime(\n        date_breaks=\"1 week\", date_labels=\"%b %d\", limits=(datetime(2023, 12, 29), datetime(2024, 3, 22))\n    )\n    # Labels\n    + labs(\n        title=title,\n        subtitle=\"Bold bars & dark arrows = Critical Path  |  Subdued bars & muted arrows = Non-critical\",\n        x=\"Date (2024)\",\n        y=\"\",\n    )\n    # Theme — Imprint palette chrome, theme-adaptive backgrounds and text\n    + theme_minimal()\n    + theme(\n        figure_size=(8, 4.5),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        plot_title=element_text(size=title_fontsize, weight=\"bold\", color=INK, margin={\"b\": 3}),\n        plot_subtitle=element_text(size=8, color=INK_SOFT, margin={\"b\": 5}),\n        axis_title_x=element_text(size=10, color=INK),\n        axis_text_x=element_text(size=8, color=INK_SOFT, rotation=45, ha=\"right\"),\n        axis_text_y=element_text(size=8, color=INK_SOFT, ha=\"right\"),\n        legend_title=element_text(size=8, weight=\"bold\", color=INK),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_position=\"right\",\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_key_size=8,\n        panel_grid_major_y=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_grid_major_x=element_line(color=INK, size=0.2, alpha=0.15),\n        panel_border=element_rect(color=INK_SOFT, fill=None),\n        axis_line=element_line(color=INK_SOFT),\n    )\n)\n\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\", verbose=False)\n"}