{"spec_id":"gantt-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\ngantt-basic: Basic Gantt Chart\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-10\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport pandas as pd\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, Legend, LegendItem\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\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\"\n\n# Data - Software Development Project\ntasks = [\n    {\"task\": \"Requirements Analysis\", \"start\": \"2025-01-06\", \"end\": \"2025-01-17\", \"category\": \"Planning\"},\n    {\"task\": \"System Design\", \"start\": \"2025-01-13\", \"end\": \"2025-01-31\", \"category\": \"Planning\"},\n    {\"task\": \"Database Schema\", \"start\": \"2025-01-27\", \"end\": \"2025-02-07\", \"category\": \"Development\"},\n    {\"task\": \"Backend API\", \"start\": \"2025-02-03\", \"end\": \"2025-02-28\", \"category\": \"Development\"},\n    {\"task\": \"Frontend UI\", \"start\": \"2025-02-10\", \"end\": \"2025-03-14\", \"category\": \"Development\"},\n    {\"task\": \"Integration\", \"start\": \"2025-03-03\", \"end\": \"2025-03-21\", \"category\": \"Development\"},\n    {\"task\": \"Unit Testing\", \"start\": \"2025-02-17\", \"end\": \"2025-03-14\", \"category\": \"Testing\"},\n    {\"task\": \"System Testing\", \"start\": \"2025-03-17\", \"end\": \"2025-03-28\", \"category\": \"Testing\"},\n    {\"task\": \"User Acceptance\", \"start\": \"2025-03-24\", \"end\": \"2025-04-04\", \"category\": \"Testing\"},\n    {\"task\": \"Documentation\", \"start\": \"2025-03-10\", \"end\": \"2025-04-04\", \"category\": \"Deployment\"},\n    {\"task\": \"Deployment\", \"start\": \"2025-04-01\", \"end\": \"2025-04-11\", \"category\": \"Deployment\"},\n    {\"task\": \"Training\", \"start\": \"2025-04-07\", \"end\": \"2025-04-18\", \"category\": \"Deployment\"},\n]\n\ndf = pd.DataFrame(tasks)\ndf[\"start\"] = pd.to_datetime(df[\"start\"])\ndf[\"end\"] = pd.to_datetime(df[\"end\"])\n\n# Sort by start date for chronological order\ndf = df.sort_values([\"start\", \"category\"], ascending=[True, True]).reset_index(drop=True)\n\n# Convert dates to numeric for plotting (milliseconds since epoch)\ndf[\"start_ms\"] = df[\"start\"].astype(\"int64\") // 10**6\ndf[\"end_ms\"] = df[\"end\"].astype(\"int64\") // 10**6\n\n# Assign y positions (inverted so first task is at top)\ndf[\"y\"] = list(range(len(df) - 1, -1, -1))\n\n# Color mapping by category using Okabe-Ito palette\nokabe_ito = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\ncategories = df[\"category\"].unique().tolist()\ncolor_map = {cat: okabe_ito[i % len(okabe_ito)] for i, cat in enumerate(categories)}\ndf[\"color\"] = df[\"category\"].map(color_map)\n\n# Create ColumnDataSource\nsource = ColumnDataSource(\n    data={\n        \"task\": df[\"task\"],\n        \"y\": df[\"y\"],\n        \"left\": df[\"start_ms\"],\n        \"right\": df[\"end_ms\"],\n        \"color\": df[\"color\"],\n        \"category\": df[\"category\"],\n    }\n)\n\n# Create figure\np = figure(\n    width=4800,\n    height=2700,\n    title=\"gantt-basic · bokeh · anyplot.ai\",\n    x_axis_type=\"datetime\",\n    y_range=(-0.5, len(df) - 0.5),\n    tools=\"\",\n    toolbar_location=None,\n)\n\n# Bar height\nbar_height = 0.65\n\n# Draw Gantt bars\np.hbar(\n    y=\"y\",\n    left=\"left\",\n    right=\"right\",\n    height=bar_height,\n    color=\"color\",\n    alpha=0.9,\n    source=source,\n    line_color=INK_SOFT,\n    line_width=2,\n)\n\n# Add task labels on the left side\nx_range_span = df[\"end_ms\"].max() - df[\"start_ms\"].min()\nfor i, row in df.iterrows():\n    y_pos = df.loc[i, \"y\"]\n    task_name = row[\"task\"]\n    p.text(\n        x=[df[\"start_ms\"].min() - x_range_span * 0.015],\n        y=[y_pos],\n        text=[task_name],\n        text_font_size=\"28pt\",\n        text_align=\"right\",\n        text_baseline=\"middle\",\n        text_color=INK,\n    )\n\n# Title styling\np.title.text_font_size = \"28pt\"\np.title.text_color = INK\n\n# X-axis styling\np.xaxis.axis_label = \"Timeline\"\np.xaxis.axis_label_text_font_size = \"22pt\"\np.xaxis.major_label_text_font_size = \"18pt\"\np.xaxis.axis_label_text_color = INK\np.xaxis.major_label_text_color = INK_SOFT\np.xaxis.axis_line_color = INK_SOFT\np.xaxis.axis_line_width = 2\np.xaxis.major_tick_line_color = INK_SOFT\np.xaxis.major_tick_line_width = 2\np.xaxis.minor_tick_line_color = None\n\n# Hide y-axis\np.yaxis.visible = False\n\n# Grid styling\np.xgrid.grid_line_color = INK_SOFT\np.xgrid.grid_line_alpha = 0.10\np.xgrid.grid_line_width = 1\np.ygrid.grid_line_color = None\n\n# Background\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\n# Extend x-range to accommodate task labels\nx_min = df[\"start_ms\"].min()\nx_max = df[\"end_ms\"].max()\nx_padding = (x_max - x_min) * 0.22\np.x_range.start = x_min - x_padding\np.x_range.end = x_max + (x_max - x_min) * 0.03\n\n# Add legend\nlegend_items = []\nfor cat in categories:\n    dummy = p.hbar(y=[-100], left=[0], right=[1], height=0.1, color=color_map[cat], visible=False)\n    legend_items.append(LegendItem(label=cat, renderers=[dummy]))\n\nlegend = Legend(items=legend_items, location=\"top_right\")\nlegend.label_text_font_size = \"18pt\"\nlegend.label_text_color = INK_SOFT\nlegend.glyph_height = 40\nlegend.glyph_width = 50\nlegend.spacing = 20\nlegend.padding = 25\nlegend.background_fill_color = ELEVATED_BG\nlegend.background_fill_alpha = 0.9\nlegend.border_line_color = INK_SOFT\nlegend.border_line_width = 1\np.add_layout(legend, \"right\")\n\n# Save as HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with Selenium\nW, H = 4800, 2700\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}