{"spec_id":"histogram-kde","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nhistogram-kde: Histogram with KDE Overlay\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 87/100 | Updated: 2026-08-05\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Theme-adaptive Chrome\")\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint categorical palette (canonical order, first series always #009E73)\nIMPRINT_PALETTE = (\n    \"#009E73\",  # brand green — histogram bars\n    \"#C475FD\",  # lavender — KDE curve\n    \"#4467A3\",  # blue\n    \"#BD8233\",  # ochre\n    \"#AE3030\",  # matte red\n    \"#2ABCCD\",  # cyan\n    \"#954477\",  # rose\n    \"#99B314\",  # lime\n)\n\n# Data - injection molding barrel temperature readings from a quality-control\n# monitoring line, target set-point 205C, with occasional cold-start and\n# overheat excursions that give the distribution its tail behavior\nnp.random.seed(42)\ntemperatures = np.concatenate(\n    [\n        np.random.normal(205, 3.5, 550),  # steady-state process operation\n        np.random.normal(196, 2.0, 30),  # cold-start under-temp events\n        np.random.normal(216, 2.5, 20),  # overheat spikes\n    ]\n)\n\n# Compute histogram bins with density normalization\nn_bins = 28\ncounts, bin_edges = np.histogram(temperatures, bins=n_bins, density=True)\n\n# Compute KDE using a Gaussian kernel (Scott's rule for bandwidth)\nx_range = np.linspace(temperatures.min() - 1, temperatures.max() + 1, 200)\nn = len(temperatures)\nbandwidth = n ** (-1 / 5) * np.std(temperatures)\nkde = np.zeros_like(x_range)\nfor xi in temperatures:\n    kde += np.exp(-0.5 * ((x_range - xi) / bandwidth) ** 2)\nkde /= n * bandwidth * np.sqrt(2 * np.pi)\n\n# Histogram bars as a filled step path so pygal's XY chart can draw them\nhist_xy = [(float(bin_edges[0]), 0.0)]\nfor i, count in enumerate(counts):\n    left = float(bin_edges[i])\n    right = float(bin_edges[i + 1])\n    height = float(count)\n    hist_xy.append((left, height))\n    hist_xy.append((right, height))\nhist_xy.append((float(bin_edges[-1]), 0.0))\n\n# Reference line at the 205C target set-point, spanning from the baseline up\n# to the tallest peak so it reads as a data-storytelling cue without\n# stretching the y-axis beyond what the histogram/KDE already require\ntarget_temp = 205.0\ntarget_height = float(max(counts.max(), kde.max()))\ntarget_xy = [(target_temp, 0.0), (target_temp, target_height)]\n\n# Title fontsize scales with the mandated title length (see\n# prompts/plot-generator.md \"Title fontsize must scale with title length\")\ntitle = \"Injection Molding Barrel Temperature · histogram-kde · python · pygal · anyplot.ai\"\ntitle_font_size = max(round(66 * min(1.0, 67 / len(title))), 44)\n\n# Style for the 3200x1800 px canvas with theme-adaptive tokens\n# (see prompts/library/pygal.md \"Sizing + Theme for 3200x1800 px\")\n# Third series color is the \"neutral\" semantic anchor (same hex as INK) so the\n# 205C reference line reads as chart structure rather than a fourth data category\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[0], IMPRINT_PALETTE[1], INK),\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=0.5,\n    opacity_hover=0.7,\n    stroke_opacity=1,\n)\n\n# Create XY chart. Pygal-distinctive tooltip formatters give the interactive\n# HTML export readable hover values (temperature in C, density to 4 decimals)\n# without touching the static PNG.\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=title,\n    x_title=\"Barrel Temperature (C)\",\n    y_title=\"Probability Density\",\n    show_dots=False,\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_box_size=28,\n    show_y_guides=True,\n    show_x_guides=False,\n    x_value_formatter=lambda x: f\"{x:.1f}C\",\n    value_formatter=lambda y: f\"{y:.4f}\",\n)\n\n# Semi-transparent histogram fill (first series, brand green) so the KDE\n# curve remains visible through the bars\nchart.add(\"Histogram\", hist_xy, fill=True, stroke_style={\"width\": 2.5})\n\n# KDE curve drawn thick and fully opaque so it stays prominent over the\n# tallest green peaks in both themes (second series, lavender)\nkde_data = [(float(x), float(y)) for x, y in zip(x_range, kde, strict=True)]\nchart.add(\"KDE Curve\", kde_data, fill=False, stroke_style={\"width\": 8})\n\n# Dashed neutral-tone reference line calling out the 205C target set-point,\n# turning the plot from a plain distribution into a QC story about how far\n# the cold-start/overheat tails drift from spec (third series)\nchart.add(\n    \"Target Set-Point (205C)\",\n    target_xy,\n    fill=False,\n    stroke_style={\"width\": 3, \"dasharray\": \"16, 12\", \"linecap\": \"round\"},\n)\n\n# Save outputs\nchart.render_to_file(f\"plot-{THEME}.html\")\nchart.render_to_png(f\"plot-{THEME}.png\")\n"}