{"spec_id":"box-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nbox-basic: Basic Box Plot\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-28\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove this script's directory from sys.path so 'import pygal' finds the\n# installed package rather than this file itself (same-name collision).\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p or \".\") != _here]\n\nimport re\n\nimport cairosvg\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nIMPRINT_PALETTE = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# Data\nnp.random.seed(42)\ncategories = [\"Engineering\", \"Marketing\", \"Sales\", \"Operations\", \"HR\"]\ndata = {\n    \"Engineering\": np.random.normal(85000, 15000, 100),\n    \"Marketing\": np.random.normal(72000, 12000, 100),\n    \"Sales\": np.random.normal(68000, 18000, 100),\n    \"Operations\": np.random.normal(62000, 10000, 100),\n    \"HR\": np.random.normal(58000, 8000, 100),\n}\n\ndata[\"Engineering\"] = np.append(data[\"Engineering\"], [130000, 135000, 40000])\ndata[\"Sales\"] = np.append(data[\"Sales\"], [120000, 25000])\n\nmedians = {cat: float(np.median(data[cat])) for cat in categories}\nhighest_dept = max(medians, key=medians.get)\nlowest_dept = min(medians, key=medians.get)\n\ntitle = \"box-basic · python · pygal · anyplot.ai\"\ntitle_len = len(title)\ntitle_font_size = round(66 * 67 / title_len) if title_len > 67 else 66\n\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=IMPRINT_PALETTE,\n    title_font_size=title_font_size,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    opacity=1.0,\n    opacity_hover=1.0,\n)\n\n# Plot\nchart = pygal.Box(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=title,\n    x_title=\"Department\",\n    y_title=\"Salary ($)\",\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=5,\n    legend_box_size=36,\n    truncate_legend=-1,\n    truncate_label=-1,\n    show_y_guides=True,\n    show_x_guides=False,\n    margin=80,\n    spacing=40,\n    box_mode=\"tukey\",\n    range=(20000, 145000),\n    y_labels=[20000, 40000, 60000, 80000, 100000, 120000, 140000],\n)\n\nfor category in categories:\n    chart.add(category, data[category].tolist())\n\nsvg_string = chart.render().decode(\"utf-8\")\n\n# Increase box fill opacity (pygal hardcodes subtle-fill at 0.2)\n# Also add ink-colored border stroke to each box for publication-ready borders\nsvg_string = svg_string.replace(\n    \".subtle-fill{fill-opacity:.2}\", f\".subtle-fill{{fill-opacity:.7;stroke:{INK_SOFT};stroke-width:4}}\"\n)\n\n# Thicken series lines (whiskers + median) and remove heavy chart border\nextra_css = \".series line{stroke-width:5}.background{stroke:none}\"\nsvg_string = svg_string.replace(\"</style>\", extra_css + \"</style>\", 1)\n\n# Enlarge outlier dots (pygal hardcodes r=3 for box outliers)\nsvg_string = re.sub(r'(<circle[^>]*) r=\"3\" (class=\"subtle-fill)', r'\\1 r=\"20\" \\2', svg_string)\n\n# Format y-axis tick labels with comma separators (e.g. 80000 → 80,000)\nsvg_string = re.sub(r\">(\\d{5,})<\", lambda m: f\">{int(m.group(1)):,}<\", svg_string)\n\n# Add storytelling subtitle annotation\nannotation_svg = (\n    f'<text x=\"1600\" y=\"185\" text-anchor=\"middle\" '\n    f'font-size=\"34\" fill=\"{INK_MUTED}\" font-family=\"sans-serif\" font-style=\"italic\">'\n    f\"Highest median: {highest_dept} (${medians[highest_dept]:,.0f})\"\n    f\"  ·  Lowest median: {lowest_dept} (${medians[lowest_dept]:,.0f})\"\n    f\"  ·  Gap: ${medians[highest_dept] - medians[lowest_dept]:,.0f}\"\n    f\"</text>\"\n)\nsvg_string = svg_string.replace(\"</svg>\", f\"{annotation_svg}</svg>\")\n\n# Save\nwith open(f\"plot-{THEME}.html\", \"w\") as f:\n    f.write(svg_string)\n\ncairosvg.svg2png(bytestring=svg_string.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\")\n"}