{"spec_id":"box-grouped","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nbox-grouped: Grouped Box Plot\nLibrary: altair 6.2.2 | Python 3.13.15\nQuality: 86/100 | Updated: 2026-08-18\n\"\"\"\n\nimport os\n\nimport altair\nimport numpy as np\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\"\n\n# Imprint palette (positions 1, 2, 3 for Junior, Mid, Senior)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Data - Employee performance scores across departments and experience levels\nnp.random.seed(42)\n\ndepartments = [\"Engineering\", \"Sales\", \"Marketing\", \"Support\"]\nexperience_levels = [\"Junior\", \"Mid\", \"Senior\"]\n\ndata = []\n# Create varied distributions for each combination\ndistributions = {\n    (\"Engineering\", \"Junior\"): (65, 12),\n    (\"Engineering\", \"Mid\"): (75, 10),\n    (\"Engineering\", \"Senior\"): (85, 8),\n    (\"Sales\", \"Junior\"): (55, 15),\n    (\"Sales\", \"Mid\"): (70, 12),\n    (\"Sales\", \"Senior\"): (80, 10),\n    (\"Marketing\", \"Junior\"): (60, 14),\n    (\"Marketing\", \"Mid\"): (72, 11),\n    (\"Marketing\", \"Senior\"): (82, 9),\n    (\"Support\", \"Junior\"): (58, 13),\n    (\"Support\", \"Mid\"): (68, 12),\n    (\"Support\", \"Senior\"): (78, 10),\n}\n\nfor dept in departments:\n    for exp in experience_levels:\n        mean, std = distributions[(dept, exp)]\n        n_samples = 50\n        values = np.random.normal(mean, std, n_samples)\n        # Add some outliers\n        if np.random.random() > 0.5:\n            values = np.append(values, [mean + 3.5 * std, mean - 3 * std])\n        # Clip to realistic range\n        values = np.clip(values, 0, 100)\n        for v in values:\n            data.append({\"Department\": dept, \"Experience\": exp, \"Performance Score\": v})\n\ndf = pd.DataFrame(data)\n\n# Order departments by descending Senior-level median score, giving the\n# grouped comparison a clear takeaway (best- to worst-performing department).\ndept_order = (\n    df[df[\"Experience\"] == \"Senior\"]\n    .groupby(\"Department\")[\"Performance Score\"]\n    .median()\n    .sort_values(ascending=False)\n    .index.tolist()\n)\n\n# Create grouped box plot with theme-adaptive styling\nchart = (\n    altair.Chart(df)\n    .mark_boxplot(size=22, median={\"stroke\": INK, \"strokeWidth\": 1.5}, outliers={\"size\": 28, \"strokeOpacity\": 0.7})\n    .encode(\n        x=altair.X(\n            \"Department:N\",\n            title=\"Department\",\n            sort=dept_order,\n            axis=altair.Axis(labelFontSize=11, titleFontSize=12, labelAngle=0),\n        ),\n        y=altair.Y(\n            \"Performance Score:Q\",\n            title=\"Performance Score (%)\",\n            scale=altair.Scale(domain=[0, 105]),\n            axis=altair.Axis(labelFontSize=10, titleFontSize=12),\n        ),\n        color=altair.Color(\n            \"Experience:N\",\n            title=\"Experience Level\",\n            scale=altair.Scale(domain=[\"Junior\", \"Mid\", \"Senior\"], range=IMPRINT),\n            legend=altair.Legend(titleFontSize=10, labelFontSize=10, symbolSize=100, orient=\"top-left\"),\n        ),\n        xOffset=\"Experience:N\",\n        tooltip=[\"Department:N\", \"Experience:N\", \"Performance Score:Q\"],\n    )\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=altair.Title(text=\"box-grouped · python · altair · anyplot.ai\", fontSize=16, anchor=\"middle\"),\n    )\n    .configure_view(fill=PAGE_BG, stroke=None)\n    .configure_axis(\n        domainColor=INK_SOFT, tickColor=INK_SOFT, gridColor=INK, gridOpacity=0.10, labelColor=INK_SOFT, titleColor=INK\n    )\n    .configure_title(color=INK)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=None, labelColor=INK_SOFT, titleColor=INK)\n)\n\n# Save as PNG and HTML with theme-suffixed filenames\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\nchart.save(f\"plot-{THEME}.html\")\n\n# Canvas contract: pad (never crop) the exported PNG up to the exact target.\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}x{_h}, exceeds target {TW}x{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values 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"}