{"spec_id":"gantt-dependencies","library":"altair","language":"python","code":"\"\"\" anyplot.ai\ngantt-dependencies: Gantt Chart with Dependencies\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove script dir from sys.path to prevent self-import (file is named altair.py)\nsys.path = [p for p in sys.path if os.path.abspath(p) != os.path.dirname(os.path.abspath(__file__))]\n\nimport altair as alt\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme tokens\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint categorical palette — positions 1-5 for the 5 project phases\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n# Amber semantic anchor: \"caution\" role suits dependency lines (delays propagate)\nDEP_COLOR = \"#DDCC77\"\n\n# Data — Software Development Project with Dependencies\ntasks_data = [\n    # Requirements Phase\n    {\n        \"task\": \"Requirements Gathering\",\n        \"start\": \"2024-01-01\",\n        \"end\": \"2024-01-15\",\n        \"group\": \"Requirements\",\n        \"depends_on\": None,\n        \"task_id\": \"REQ1\",\n    },\n    {\n        \"task\": \"Requirements Review\",\n        \"start\": \"2024-01-16\",\n        \"end\": \"2024-01-22\",\n        \"group\": \"Requirements\",\n        \"depends_on\": \"REQ1\",\n        \"task_id\": \"REQ2\",\n    },\n    # Design Phase\n    {\n        \"task\": \"System Architecture\",\n        \"start\": \"2024-01-23\",\n        \"end\": \"2024-02-05\",\n        \"group\": \"Design\",\n        \"depends_on\": \"REQ2\",\n        \"task_id\": \"DES1\",\n    },\n    {\n        \"task\": \"Database Design\",\n        \"start\": \"2024-02-06\",\n        \"end\": \"2024-02-15\",\n        \"group\": \"Design\",\n        \"depends_on\": \"DES1\",\n        \"task_id\": \"DES2\",\n    },\n    {\n        \"task\": \"UI/UX Design\",\n        \"start\": \"2024-02-06\",\n        \"end\": \"2024-02-20\",\n        \"group\": \"Design\",\n        \"depends_on\": \"DES1\",\n        \"task_id\": \"DES3\",\n    },\n    # Development Phase\n    {\n        \"task\": \"Backend Development\",\n        \"start\": \"2024-02-16\",\n        \"end\": \"2024-03-20\",\n        \"group\": \"Development\",\n        \"depends_on\": \"DES2\",\n        \"task_id\": \"DEV1\",\n    },\n    {\n        \"task\": \"Frontend Development\",\n        \"start\": \"2024-02-21\",\n        \"end\": \"2024-03-25\",\n        \"group\": \"Development\",\n        \"depends_on\": \"DES3\",\n        \"task_id\": \"DEV2\",\n    },\n    {\n        \"task\": \"API Integration\",\n        \"start\": \"2024-03-21\",\n        \"end\": \"2024-04-05\",\n        \"group\": \"Development\",\n        \"depends_on\": \"DEV1\",\n        \"task_id\": \"DEV3\",\n    },\n    # Testing Phase\n    {\n        \"task\": \"Unit Testing\",\n        \"start\": \"2024-03-26\",\n        \"end\": \"2024-04-10\",\n        \"group\": \"Testing\",\n        \"depends_on\": \"DEV2\",\n        \"task_id\": \"TST1\",\n    },\n    {\n        \"task\": \"Integration Testing\",\n        \"start\": \"2024-04-06\",\n        \"end\": \"2024-04-20\",\n        \"group\": \"Testing\",\n        \"depends_on\": \"DEV3\",\n        \"task_id\": \"TST2\",\n    },\n    {\n        \"task\": \"User Acceptance Testing\",\n        \"start\": \"2024-04-21\",\n        \"end\": \"2024-05-05\",\n        \"group\": \"Testing\",\n        \"depends_on\": \"TST2\",\n        \"task_id\": \"TST3\",\n    },\n    # Deployment Phase\n    {\n        \"task\": \"Deployment Prep\",\n        \"start\": \"2024-05-06\",\n        \"end\": \"2024-05-12\",\n        \"group\": \"Deployment\",\n        \"depends_on\": \"TST3\",\n        \"task_id\": \"DPL1\",\n    },\n    {\n        \"task\": \"Production Deploy\",\n        \"start\": \"2024-05-13\",\n        \"end\": \"2024-05-15\",\n        \"group\": \"Deployment\",\n        \"depends_on\": \"DPL1\",\n        \"task_id\": \"DPL2\",\n    },\n]\n\ndf = pd.DataFrame(tasks_data)\ndf[\"start\"] = pd.to_datetime(df[\"start\"])\ndf[\"end\"] = pd.to_datetime(df[\"end\"])\ntask_lookup = {r[\"task_id\"]: r for _, r in df.iterrows()}\n\ngroup_order = [\"Requirements\", \"Design\", \"Development\", \"Testing\", \"Deployment\"]\n# Imprint positions 1→5 in canonical order (green, lavender, blue, ochre, matte-red)\ngroup_colors = {g: IMPRINT_PALETTE[i] for i, g in enumerate(group_order)}\n\n# Build display rows with ordinal task ordering (group headers + indented tasks)\ntask_order = []\nchart_rows = []\nfor grp in group_order:\n    grp_tasks = df[df[\"group\"] == grp]\n    label = f\"▸ {grp}\"\n    task_order.append(label)\n    chart_rows.append(\n        {\n            \"task\": label,\n            \"start\": grp_tasks[\"start\"].min(),\n            \"end\": grp_tasks[\"end\"].max(),\n            \"group\": grp,\n            \"is_group\": True,\n        }\n    )\n    for _, r in grp_tasks.iterrows():\n        display = f\"  {r['task']}\"\n        task_order.append(display)\n        chart_rows.append({\"task\": display, \"start\": r[\"start\"], \"end\": r[\"end\"], \"group\": grp, \"is_group\": False})\n\nchart_df = pd.DataFrame(chart_rows)\n\n# Dependency data: 2 points per arrow using shared ordinal Y scale\ndep_line_rows = []\ndep_arrow_rows = []\ndep_id = 0\nfor _, r in df.iterrows():\n    if r[\"depends_on\"] and r[\"depends_on\"] in task_lookup:\n        pred = task_lookup[r[\"depends_on\"]]\n        dep_line_rows.append({\"x\": pred[\"end\"], \"task\": f\"  {pred['task']}\", \"dep_id\": dep_id})\n        dep_line_rows.append({\"x\": r[\"start\"], \"task\": f\"  {r['task']}\", \"dep_id\": dep_id})\n        dep_arrow_rows.append({\"x\": r[\"start\"], \"task\": f\"  {r['task']}\"})\n        dep_id += 1\ndep_line_df = pd.DataFrame(dep_line_rows)\ndep_arrow_df = pd.DataFrame(dep_arrow_rows)\n\n# Title — 49 chars, under 67 baseline → use default 16px\ntitle_str = \"gantt-dependencies · python · altair · anyplot.ai\"\ntitle_fs = round(16 * min(1.0, 67 / len(title_str)))\n\n# Y-axis: conditional bold + larger font for group headers\ny_axis = alt.Axis(\n    labelFontSize=alt.ExprRef(\"indexof(datum.value, '▸') >= 0 ? 18 : 16\"),\n    labelFontWeight=alt.ExprRef(\"indexof(datum.value, '▸') >= 0 ? 'bold' : 'normal'\"),\n    labelColor=INK_SOFT,\n    labelLimit=270,\n    ticks=False,\n    domain=False,\n    labelPadding=8,\n)\n\n# Task bars — colored by group, native bottom legend for phases\ntask_bars = (\n    alt.Chart(chart_df[~chart_df[\"is_group\"]])\n    .mark_bar(cornerRadius=4)\n    .encode(\n        x=alt.X(\n            \"start:T\",\n            title=\"Project Timeline (2024)\",\n            axis=alt.Axis(format=\"%b %d\", labelFontSize=16, titleFontSize=18, labelColor=INK_SOFT, titleColor=INK),\n        ),\n        x2=\"end:T\",\n        y=alt.Y(\"task:N\", sort=task_order, title=None, axis=y_axis),\n        color=alt.Color(\n            \"group:N\",\n            scale=alt.Scale(domain=group_order, range=[group_colors[g] for g in group_order]),\n            legend=alt.Legend(\n                orient=\"bottom\",\n                direction=\"horizontal\",\n                title=None,\n                labelFontSize=16,\n                symbolSize=280,\n                symbolType=\"square\",\n                fillColor=ELEVATED_BG,\n                strokeColor=INK_SOFT,\n                padding=8,\n                labelColor=INK_SOFT,\n                cornerRadius=4,\n            ),\n        ),\n        tooltip=[\n            \"task:N\",\n            alt.Tooltip(\"start:T\", format=\"%Y-%m-%d\"),\n            alt.Tooltip(\"end:T\", format=\"%Y-%m-%d\"),\n            \"group:N\",\n        ],\n    )\n)\n\n# Group summary bars — thin, INK-colored to sit on the structural layer\ngroup_bars = (\n    alt.Chart(chart_df[chart_df[\"is_group\"]])\n    .mark_bar(cornerRadius=3, size=12, opacity=0.9)\n    .encode(\n        x=\"start:T\",\n        x2=\"end:T\",\n        y=alt.Y(\"task:N\", sort=task_order),\n        color=alt.value(INK),\n        tooltip=[\"task:N\", alt.Tooltip(\"start:T\", format=\"%Y-%m-%d\"), alt.Tooltip(\"end:T\", format=\"%Y-%m-%d\")],\n    )\n)\n\n# Dependency lines — amber dashed (caution: delays propagate)\ndep_lines = (\n    alt.Chart(dep_line_df)\n    .mark_line(strokeWidth=3.5, opacity=0.9, color=DEP_COLOR, strokeDash=[8, 4])\n    .encode(x=\"x:T\", y=alt.Y(\"task:N\", sort=task_order), detail=\"dep_id:N\")\n)\n\n# Arrowheads at successor start — size=300 for clear visibility\narrow_heads = (\n    alt.Chart(dep_arrow_df)\n    .mark_point(shape=\"triangle-right\", size=300, filled=True, color=DEP_COLOR, opacity=0.95)\n    .encode(x=\"x:T\", y=alt.Y(\"task:N\", sort=task_order))\n)\n\n# Dependency label — positioned above the chart area as a subtitle line\ndep_label_df = pd.DataFrame([{\"x\": \"2024-04-01\", \"label\": \"── ── ▶  Finish-to-start dependency\"}])\ndep_label = (\n    alt.Chart(dep_label_df)\n    .mark_text(fontSize=14, color=DEP_COLOR, align=\"right\", fontStyle=\"italic\")\n    .encode(x=alt.X(\"x:T\", axis=None), text=\"label:N\")\n    .properties(width=500, height=22)\n)\n\n# Main chart — inner view sized to land within 3200×1800 after vl-convert padding\nmain = alt.layer(group_bars, task_bars, dep_lines, arrow_heads).properties(\n    width=500, height=280, title=alt.Title(title_str, fontSize=title_fs, color=INK, anchor=\"middle\")\n)\n\nchart = (\n    alt.vconcat(main, dep_label, spacing=2)\n    .configure_axisX(grid=True, gridOpacity=0.15, gridDash=[3, 3], gridColor=INK)\n    .configure_axisY(grid=False)\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        labelFontSize=16,\n        symbolSize=280,\n        padding=8,\n        cornerRadius=4,\n    )\n    .properties(background=PAGE_BG)\n)\n\n# Save PNG then pad to exact 3200×1800\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        \"Shrink chart .properties(width=, height=) and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}