{"spec_id":"violin-box","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nviolin-box: Violin Plot with Embedded Box Plot\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-05-12\n\"\"\"\n\nimport os\nimport sys\n\nimport numpy as np\n\n\nsys.path = [p for p in sys.path if not p.endswith(\"/python\")]\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_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Okabe-Ito palette - use first 4 colors for violin categories\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\")\n\n# Data - Generate distributions for different categories with scores constrained to 0-100\nnp.random.seed(42)\nraw_data = {\n    \"Engineering\": np.random.normal(75, 10, 200),\n    \"Marketing\": np.random.normal(62, 12, 200),\n    \"Sales\": np.random.normal(68, 14, 200),\n    \"Operations\": np.random.normal(55, 8, 200),\n}\n# Clip all values to 0-100 range\ndata = {k: np.clip(v, 0, 100) for k, v in raw_data.items()}\n\n# Custom style for 4800x2700 px canvas\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,\n    title_font_size=28,\n    label_font_size=22,\n    major_label_font_size=18,\n    legend_font_size=16,\n    value_font_size=14,\n    stroke_width=3,\n)\n\n# Create XY chart for violin plot with embedded box\nchart = pygal.XY(\n    width=4800,\n    height=2700,\n    style=custom_style,\n    title=\"violin-box · pygal · anyplot.ai\",\n    x_title=\"Department\",\n    y_title=\"Performance Score (0-100)\",\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=(0, 105),\n    xrange=(0, 6),\n    margin=80,\n)\n\n# Parameters for violin shapes\nviolin_width = 0.35\nn_points = 100\n\n# Box plot styling\nbox_stroke_style = {\"width\": 4, \"dasharray\": \"\"}\nmedian_stroke_style = {\"width\": 6, \"dasharray\": \"\"}\nwhisker_stroke_style = {\"width\": 3, \"dasharray\": \"\"}\n\n# Add violins with embedded box plots for each category\nfor i, (category, values) in enumerate(data.items()):\n    center_x = i + 1.5\n    violin_color = IMPRINT[i]\n\n    # Compute KDE using Silverman's rule\n    n = len(values)\n    std = np.std(values)\n    iqr = np.percentile(values, 75) - np.percentile(values, 25)\n    bandwidth = 0.9 * min(std, iqr / 1.34) * n ** (-0.2)\n\n    # Create range of y values for density\n    y_min, y_max = values.min(), values.max()\n    y_range = np.linspace(max(0, y_min - 5), min(100, y_max + 5), n_points)\n\n    # Gaussian kernel density estimation\n    density = np.zeros_like(y_range)\n    for v in values:\n        density += np.exp(-0.5 * ((y_range - v) / bandwidth) ** 2)\n    density /= n * bandwidth * np.sqrt(2 * np.pi)\n\n    # Normalize density to desired width\n    density = density / density.max() * violin_width\n\n    # Create violin shape (mirrored density)\n    left_points = [(center_x - d, y) for y, d in zip(y_range, density, strict=True)]\n    right_points = [(center_x + d, y) for y, d in zip(y_range[::-1], density[::-1], strict=True)]\n    violin_points = left_points + right_points + [left_points[0]]\n\n    chart.add(category, violin_points)\n\n    # Calculate box plot statistics\n    median = float(np.median(values))\n    q1 = float(np.percentile(values, 25))\n    q3 = float(np.percentile(values, 75))\n    iqr_val = q3 - q1\n\n    # Whiskers: 1.5 * IQR or data min/max\n    lower_whisker = max(values.min(), q1 - 1.5 * iqr_val)\n    upper_whisker = min(values.max(), q3 + 1.5 * iqr_val)\n\n    # Identify outliers\n    outliers = values[(values < lower_whisker) | (values > upper_whisker)]\n\n    box_width = 0.10\n\n    # Quartile box - use elevated background color for visibility\n    elevated_bg = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\n    quartile_box = [\n        (center_x - box_width, q1),\n        (center_x - box_width, q3),\n        (center_x + box_width, q3),\n        (center_x + box_width, q1),\n        (center_x - box_width, q1),\n    ]\n    chart.add(None, quartile_box, stroke=True, fill=True, show_dots=False, stroke_style=box_stroke_style)\n\n    # Whisker lines (vertical lines from box to whisker ends)\n    lower_whisker_line = [(center_x, q1), (center_x, lower_whisker)]\n    upper_whisker_line = [(center_x, q3), (center_x, upper_whisker)]\n    chart.add(None, lower_whisker_line, stroke=True, fill=False, show_dots=False, stroke_style=whisker_stroke_style)\n    chart.add(None, upper_whisker_line, stroke=True, fill=False, show_dots=False, stroke_style=whisker_stroke_style)\n\n    # Whisker caps (horizontal lines at ends)\n    cap_width = box_width * 0.8\n    lower_cap = [(center_x - cap_width, lower_whisker), (center_x + cap_width, lower_whisker)]\n    upper_cap = [(center_x - cap_width, upper_whisker), (center_x + cap_width, upper_whisker)]\n    chart.add(None, lower_cap, stroke=True, fill=False, show_dots=False, stroke_style=whisker_stroke_style)\n    chart.add(None, upper_cap, stroke=True, fill=False, show_dots=False, stroke_style=whisker_stroke_style)\n\n    # Median line (thicker, contrasting)\n    median_line = [(center_x - box_width * 1.2, median), (center_x + box_width * 1.2, median)]\n    chart.add(None, median_line, stroke=True, fill=False, show_dots=False, stroke_style=median_stroke_style)\n\n    # Outliers as points\n    if len(outliers) > 0:\n        outlier_points = [(center_x, float(o)) for o in outliers]\n        chart.add(None, outlier_points, stroke=False, fill=False, show_dots=True, dots_size=18)\n\n# X-axis labels at violin positions\nchart.x_labels = [\"\", \"Engineering\", \"Marketing\", \"Sales\", \"Operations\", \"\"]\nchart.x_labels_major_count = 4\n\n# Save outputs\nchart.render_to_file(f\"plot-{THEME}.html\")\nchart.render_to_png(f\"plot-{THEME}.png\")\n"}