{"spec_id":"violin-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nviolin-basic: Basic Violin Plot\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-29\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens\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 palette — positions 1-4 for 4 category violins\nVIOLIN_COLORS = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n# Darker variants for IQR boxes (depth inside the violin)\nIQR_COLORS = [\"#006B4E\", \"#8B3DB8\", \"#2E4878\", \"#8A5A1A\"]\n# Warm white median line — high contrast against all dark IQR boxes in both themes\nMEDIAN_COLOR = \"#FAF8F1\"\n\n# Interleaved palette: 3 tokens per violin (fill, IQR fill, median line)\npalette = []\nfor vc, ic in zip(VIOLIN_COLORS, IQR_COLORS, strict=True):\n    palette.extend([vc, ic, MEDIAN_COLOR])\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=tuple(palette),\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    opacity=0.78,\n    opacity_hover=0.92,\n    transition=\"200ms ease-in\",\n)\n\n# Data — test scores across 4 class groups with distinct distribution shapes\nnp.random.seed(42)\ndata = {\n    \"Honors\": np.clip(np.random.normal(88, 6, 200), 50, 100),\n    \"Standard\": np.clip(60 + np.random.gamma(3.5, 4, 200), 40, 100),\n    \"Remedial\": np.clip(np.random.normal(62, 8, 200), 30, 100),\n    \"Advanced\": np.clip(np.concatenate([np.random.normal(75, 6, 120), np.random.normal(93, 4, 80)]), 45, 100),\n}\n\n# Y range computed from data to minimise unused canvas space\nall_values = np.concatenate(list(data.values()))\ny_pad = 3.0\ny_min = float(all_values.min()) - y_pad\ny_max = float(all_values.max()) + y_pad\n\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=\"violin-basic · python · pygal · anyplot.ai\",\n    x_title=\"Class Group\",\n    y_title=\"Test Score (%)\",\n    show_legend=False,\n    stroke=True,\n    fill=True,\n    dots_size=0,\n    show_x_guides=False,\n    show_y_guides=True,\n    range=(y_min, y_max),\n    xrange=(0, 5.0),\n    margin=50,\n    value_formatter=lambda x: f\"{x:.0f}%\",\n    x_value_formatter=lambda x: \"\",\n    tooltip_border_radius=10,\n    tooltip_fancy_mode=True,\n    human_readable=True,\n    pretty_print=True,\n)\n\n# Violin widths — Advanced wider to highlight bimodal shape\nbase_width = 0.38\nwidths = {\"Honors\": base_width, \"Standard\": base_width, \"Remedial\": base_width, \"Advanced\": 0.46}\nn_points = 100\n\nfor i, (category, values) in enumerate(data.items()):\n    center_x = i + 1.0\n    violin_width = widths[category]\n\n    # KDE with Silverman's rule\n    n = len(values)\n    std = np.std(values)\n    iqr_val = np.percentile(values, 75) - np.percentile(values, 25)\n    bandwidth = 0.9 * min(std, iqr_val / 1.34) * n ** (-0.2)\n\n    y_grid = np.linspace(y_min, y_max, n_points)\n    density = np.zeros_like(y_grid)\n    for v in values:\n        density += np.exp(-0.5 * ((y_grid - v) / bandwidth) ** 2)\n    density /= n * bandwidth * np.sqrt(2 * np.pi)\n    density = density / density.max() * violin_width\n\n    median_val = float(np.median(values))\n    q1 = float(np.percentile(values, 25))\n    q3 = float(np.percentile(values, 75))\n    tooltip = f\"{category} — Median: {median_val:.1f}%, Q1: {q1:.1f}%, Q3: {q3:.1f}%\"\n    if category == \"Advanced\":\n        tooltip += \" · Bimodal: two sub-populations (75% & 93%)\"\n\n    # Mirrored violin shape\n    left_pts = [(center_x - d, y) for y, d in zip(y_grid, density, strict=True)]\n    right_pts = [(center_x + d, y) for y, d in zip(y_grid[::-1], density[::-1], strict=True)]\n    violin_pts = left_pts + right_pts + [left_pts[0]]\n    chart.add(category, violin_pts, formatter=lambda x, t=tooltip: t, stroke_style={\"width\": 2})\n\n    # IQR box — darker Imprint shade, drawn with next palette slot\n    box_w = 0.16\n    iqr_box = [\n        (center_x - box_w, q1),\n        (center_x - box_w, q3),\n        (center_x + box_w, q3),\n        (center_x + box_w, q1),\n        (center_x - box_w, q1),\n    ]\n    chart.add(None, iqr_box, stroke=True, fill=True, show_dots=False, stroke_style={\"width\": 4, \"color\": INK_MUTED})\n\n    # Median line — warm white against dark IQR box (third palette slot per violin)\n    median_line = [(center_x - box_w * 1.1, median_val), (center_x + box_w * 1.1, median_val)]\n    chart.add(None, median_line, stroke=True, fill=False, show_dots=False, stroke_style={\"width\": 18})\n\nchart.x_labels = [\"\", \"Honors\", \"Standard\", \"Remedial\", \"Advanced\", \"\"]\nchart.x_labels_major_count = 4\n\n# Save\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\nchart.render_to_png(f\"plot-{THEME}.png\")\n"}