{"spec_id":"histogram-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nhistogram-basic: Basic Histogram\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-05-28\n\"\"\"\n\nimport sys\n\n\n# This file is named pygal.py which shadows the installed pygal package.\n# Move the script directory to the end so site-packages takes precedence.\n_script_dir = sys.path.pop(0) if sys.path else None\n\nimport io\nimport os\n\nimport numpy as np\nimport pygal\nfrom PIL import Image, ImageDraw, ImageFont\nfrom pygal.style import Style\n\n\nif _script_dir is not None:\n    sys.path.append(_script_dir)\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\nIMPRINT_PALETTE = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\nBRAND = IMPRINT_PALETTE[0]\n\n# Data — exam scores with realistic left-skewed beta distribution\nnp.random.seed(42)\nn_samples = 500\nraw = np.random.beta(a=5, b=2, size=n_samples)\nvalues = raw * 60 + 35  # Scale to ~35-95 range\n\n# Histogram bins\nn_bins = 20\ncounts, bin_edges = np.histogram(values, bins=n_bins)\nhist_data = [(int(count), float(bin_edges[i]), float(bin_edges[i + 1])) for i, count in enumerate(counts)]\n\n# Key statistics\nmean_val = float(np.mean(values))\nmedian_val = float(np.median(values))\nq1, q3 = float(np.percentile(values, 25)), float(np.percentile(values, 75))\npeak_bin = int(np.argmax(counts))\npeak_lo = float(bin_edges[peak_bin])\npeak_hi = float(bin_edges[peak_bin + 1])\n\n# Chart style — anyplot tokens, canonical sizing for 3200×1800\nfont = \"DejaVu Sans, Helvetica, Arial, sans-serif\"\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=(BRAND,),\n    font_family=font,\n    title_font_family=font,\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    stroke_width=0,\n)\n\n# Chart\nchart = pygal.Histogram(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=\"histogram-basic · python · pygal · anyplot.ai\",\n    x_title=\"Exam Score (points)\",\n    y_title=\"Number of Students\",\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_box_size=24,\n    show_y_guides=True,\n    show_x_guides=False,\n    tooltip_fancy_mode=True,\n    print_values=False,\n    margin_bottom=100,\n    margin_left=80,\n    margin_right=60,\n    margin_top=80,\n)\nchart.add(\"Score Distribution (n=500)\", hist_data)\n\n# PIL annotation overlay\nchart_bytes = chart.render_to_png()\nbase_img = Image.open(io.BytesIO(chart_bytes)).convert(\"RGBA\")\n\nimg_w, img_h = base_img.size\nplot_x0 = int(img_w * 0.073)\nplot_x1 = int(img_w * 0.969)\nplot_y_top = int(img_h * 0.065)\nplot_y_bot = int(img_h * 0.83)\ndata_min = float(bin_edges[0])\ndata_max = float(bin_edges[-1])\ndata_range = data_max - data_min\npx_range = plot_x1 - plot_x0\n\n# Semi-transparent stats box — theme-adaptive fill and outline\nbox_x, box_y, box_w, box_h = plot_x0 + 100, plot_y_top + 30, 1020, 380\nbox_fill = (255, 253, 246, 220) if THEME == \"light\" else (36, 36, 32, 220)\nbox_outline = (74, 74, 68, 200) if THEME == \"light\" else (184, 183, 176, 200)\n\noverlay = Image.new(\"RGBA\", base_img.size, (0, 0, 0, 0))\noverlay_draw = ImageDraw.Draw(overlay)\noverlay_draw.rounded_rectangle(\n    [(box_x, box_y), (box_x + box_w, box_y + box_h)], radius=18, fill=box_fill, outline=box_outline, width=2\n)\nimg = Image.alpha_composite(base_img, overlay).convert(\"RGB\")\ndraw = ImageDraw.Draw(img)\n\ntry:\n    ann_font = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf\", 36)\n    ann_bold = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf\", 40)\nexcept OSError:\n    ann_font = ImageFont.load_default()\n    ann_bold = ann_font\n\n# Mean reference line — dashed, anyplot matte red\nmean_px = int(plot_x0 + (mean_val - data_min) / data_range * px_range)\ny_pos = plot_y_top\nwhile y_pos < plot_y_bot:\n    draw.line([(mean_px, y_pos), (mean_px, min(y_pos + 18, plot_y_bot))], fill=\"#AE3030\", width=5)\n    y_pos += 30\n\n# Mean label — bold and prominent (improved from previous)\nmean_tag = f\"Mean ({mean_val:.1f}) →\"\nmt_bbox = draw.textbbox((0, 0), mean_tag, font=ann_bold)\ndraw.text((mean_px - (mt_bbox[2] - mt_bbox[0]) - 16, plot_y_bot + 8), mean_tag, fill=\"#AE3030\", font=ann_bold)\n\n# Stats box text\ndraw.text((box_x + 30, box_y + 22), \"Distribution Summary\", fill=INK, font=ann_bold)\ndraw.line([(box_x + 30, box_y + 72), (box_x + box_w - 30, box_y + 72)], fill=INK_MUTED, width=2)\nstats_lines = [\n    f\"Mean: {mean_val:.1f}  |  Median: {median_val:.1f}\",\n    f\"Spread (IQR): {q1:.0f} – {q3:.0f} pts\",\n    f\"Peak bin: {peak_lo:.0f}–{peak_hi:.0f} pts ({int(counts[peak_bin])} students)\",\n    \"Skew: left-skewed (mean < median)\",\n]\nfor i, line in enumerate(stats_lines):\n    draw.text((box_x + 30, box_y + 88 + i * 62), line, fill=INK_SOFT, font=ann_font)\n\n# Peak callout above tallest bar\npeak_mid = (peak_lo + peak_hi) / 2\npeak_px = int(plot_x0 + (peak_mid - data_min) / data_range * px_range)\nlabel_text = f\"▼ Peak: {peak_lo:.0f}–{peak_hi:.0f} pts\"\nlbl_bbox = draw.textbbox((0, 0), label_text, font=ann_bold)\ndraw.text((peak_px - (lbl_bbox[2] - lbl_bbox[0]) // 2, plot_y_top + 20), label_text, fill=BRAND, font=ann_bold)\n\n# Save\nimg.save(f\"plot-{THEME}.png\", \"PNG\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}