Violin Plot with Embedded Box Plot — Pygal

A violin plot with an embedded box plot inside, combining the distribution shape visualization (KDE) with traditional quartile statistics. Shows both the probability density and summary statistics in one plot.

Violin Plot with Embedded Box Plot rendered with Pygal

Python source (Pygal)

""" anyplot.ai
violin-box: Violin Plot with Embedded Box Plot
Library: pygal 3.1.0 | Python 3.13.13
Quality: 86/100 | Updated: 2026-05-12
"""

import os
import sys

import numpy as np


sys.path = [p for p in sys.path if not p.endswith("/python")]
import pygal
from pygal.style import Style


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Okabe-Ito palette - use first 4 colors for violin categories
IMPRINT = ("#009E73", "#C475FD", "#4467A3", "#BD8233")

# Data - Generate distributions for different categories with scores constrained to 0-100
np.random.seed(42)
raw_data = {
    "Engineering": np.random.normal(75, 10, 200),
    "Marketing": np.random.normal(62, 12, 200),
    "Sales": np.random.normal(68, 14, 200),
    "Operations": np.random.normal(55, 8, 200),
}
# Clip all values to 0-100 range
data = {k: np.clip(v, 0, 100) for k, v in raw_data.items()}

# Custom style for 4800x2700 px canvas
custom_style = Style(
    background=PAGE_BG,
    plot_background=PAGE_BG,
    foreground=INK,
    foreground_strong=INK,
    foreground_subtle=INK_MUTED,
    colors=IMPRINT,
    title_font_size=28,
    label_font_size=22,
    major_label_font_size=18,
    legend_font_size=16,
    value_font_size=14,
    stroke_width=3,
)

# Create XY chart for violin plot with embedded box
chart = pygal.XY(
    width=4800,
    height=2700,
    style=custom_style,
    title="violin-box · pygal · anyplot.ai",
    x_title="Department",
    y_title="Performance Score (0-100)",
    show_legend=False,
    stroke=True,
    fill=True,
    dots_size=0,
    show_x_guides=False,
    show_y_guides=True,
    range=(0, 105),
    xrange=(0, 6),
    margin=80,
)

# Parameters for violin shapes
violin_width = 0.35
n_points = 100

# Box plot styling
box_stroke_style = {"width": 4, "dasharray": ""}
median_stroke_style = {"width": 6, "dasharray": ""}
whisker_stroke_style = {"width": 3, "dasharray": ""}

# Add violins with embedded box plots for each category
for i, (category, values) in enumerate(data.items()):
    center_x = i + 1.5
    violin_color = IMPRINT[i]

    # Compute KDE using Silverman's rule
    n = len(values)
    std = np.std(values)
    iqr = np.percentile(values, 75) - np.percentile(values, 25)
    bandwidth = 0.9 * min(std, iqr / 1.34) * n ** (-0.2)

    # Create range of y values for density
    y_min, y_max = values.min(), values.max()
    y_range = np.linspace(max(0, y_min - 5), min(100, y_max + 5), n_points)

    # Gaussian kernel density estimation
    density = np.zeros_like(y_range)
    for v in values:
        density += np.exp(-0.5 * ((y_range - v) / bandwidth) ** 2)
    density /= n * bandwidth * np.sqrt(2 * np.pi)

    # Normalize density to desired width
    density = density / density.max() * violin_width

    # Create violin shape (mirrored density)
    left_points = [(center_x - d, y) for y, d in zip(y_range, density, strict=True)]
    right_points = [(center_x + d, y) for y, d in zip(y_range[::-1], density[::-1], strict=True)]
    violin_points = left_points + right_points + [left_points[0]]

    chart.add(category, violin_points)

    # Calculate box plot statistics
    median = float(np.median(values))
    q1 = float(np.percentile(values, 25))
    q3 = float(np.percentile(values, 75))
    iqr_val = q3 - q1

    # Whiskers: 1.5 * IQR or data min/max
    lower_whisker = max(values.min(), q1 - 1.5 * iqr_val)
    upper_whisker = min(values.max(), q3 + 1.5 * iqr_val)

    # Identify outliers
    outliers = values[(values < lower_whisker) | (values > upper_whisker)]

    box_width = 0.10

    # Quartile box - use elevated background color for visibility
    elevated_bg = "#FFFDF6" if THEME == "light" else "#242420"
    quartile_box = [
        (center_x - box_width, q1),
        (center_x - box_width, q3),
        (center_x + box_width, q3),
        (center_x + box_width, q1),
        (center_x - box_width, q1),
    ]
    chart.add(None, quartile_box, stroke=True, fill=True, show_dots=False, stroke_style=box_stroke_style)

    # Whisker lines (vertical lines from box to whisker ends)
    lower_whisker_line = [(center_x, q1), (center_x, lower_whisker)]
    upper_whisker_line = [(center_x, q3), (center_x, upper_whisker)]
    chart.add(None, lower_whisker_line, stroke=True, fill=False, show_dots=False, stroke_style=whisker_stroke_style)
    chart.add(None, upper_whisker_line, stroke=True, fill=False, show_dots=False, stroke_style=whisker_stroke_style)

    # Whisker caps (horizontal lines at ends)
    cap_width = box_width * 0.8
    lower_cap = [(center_x - cap_width, lower_whisker), (center_x + cap_width, lower_whisker)]
    upper_cap = [(center_x - cap_width, upper_whisker), (center_x + cap_width, upper_whisker)]
    chart.add(None, lower_cap, stroke=True, fill=False, show_dots=False, stroke_style=whisker_stroke_style)
    chart.add(None, upper_cap, stroke=True, fill=False, show_dots=False, stroke_style=whisker_stroke_style)

    # Median line (thicker, contrasting)
    median_line = [(center_x - box_width * 1.2, median), (center_x + box_width * 1.2, median)]
    chart.add(None, median_line, stroke=True, fill=False, show_dots=False, stroke_style=median_stroke_style)

    # Outliers as points
    if len(outliers) > 0:
        outlier_points = [(center_x, float(o)) for o in outliers]
        chart.add(None, outlier_points, stroke=False, fill=False, show_dots=True, dots_size=18)

# X-axis labels at violin positions
chart.x_labels = ["", "Engineering", "Marketing", "Sales", "Operations", ""]
chart.x_labels_major_count = 4

# Save outputs
chart.render_to_file(f"plot-{THEME}.html")
chart.render_to_png(f"plot-{THEME}.png")

Part of Violin Plot with Embedded Box Plot on anyplot.ai.

Other implementations