Grouped Box Plot — Pygal

A grouped box plot displays multiple box plots side-by-side within each category, enabling comparison of distributions across subgroups. Each group contains boxes representing different subcategories or conditions, making it ideal for multi-factor comparisons and A/B testing scenarios with multiple metrics.

Grouped Box Plot rendered with Pygal

Python source (Pygal)

""" anyplot.ai
box-grouped: Grouped Box Plot
Library: pygal 3.1.0 | Python 3.13.13
Quality: 85/100 | Updated: 2026-05-08
"""

import os

import numpy as np
import pygal
from pygal.style import Style


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

IMPRINT = ("#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477")

# Data - Employee performance scores across departments and experience levels
np.random.seed(42)

departments = ["Engineering", "Marketing", "Sales"]
experience_levels = ["Junior", "Senior", "Lead"]

# Generate performance distributions with realistic differences
data = {}
# Engineering: Higher scores overall, tight distributions
data[("Engineering", "Junior")] = np.random.normal(72, 8, 50)
data[("Engineering", "Senior")] = np.random.normal(82, 6, 50)
data[("Engineering", "Lead")] = np.random.normal(88, 5, 50)

# Marketing: More variability
data[("Marketing", "Junior")] = np.random.normal(68, 12, 50)
data[("Marketing", "Senior")] = np.random.normal(76, 10, 50)
data[("Marketing", "Lead")] = np.random.normal(84, 8, 50)

# Sales: Widest distributions with outliers
data[("Sales", "Junior")] = np.random.normal(65, 15, 50)
data[("Sales", "Senior")] = np.random.normal(78, 12, 50)
data[("Sales", "Lead")] = np.random.normal(85, 10, 50)

# Add outliers to show box plot features
data[("Engineering", "Junior")] = np.append(data[("Engineering", "Junior")], [45, 95])
data[("Sales", "Senior")] = np.append(data[("Sales", "Senior")], [40, 105])
data[("Marketing", "Lead")] = np.append(data[("Marketing", "Lead")], [60, 100])

# Subcategory colors: use Okabe-Ito palette starting with brand green
subcategory_colors = IMPRINT[:3]

# Build full color tuple: repeat pattern for each department group
all_colors = subcategory_colors * len(departments)

custom_style = Style(
    background=PAGE_BG,
    plot_background=PAGE_BG,
    foreground=INK,
    foreground_strong=INK,
    foreground_subtle=INK_MUTED,
    colors=all_colors,
    title_font_size=28,
    label_font_size=22,
    major_label_font_size=18,
    legend_font_size=16,
    value_font_size=14,
    opacity=0.85,
    opacity_hover=1.0,
)

# Create box chart with legend showing only 3 experience levels
chart = pygal.Box(
    width=4800,
    height=2700,
    style=custom_style,
    title="box-grouped · pygal · anyplot.ai",
    x_title="Department",
    y_title="Performance Score",
    show_legend=True,
    legend_at_bottom=True,
    legend_at_bottom_columns=3,
    legend_box_size=36,
    truncate_legend=-1,
    show_y_guides=True,
    show_x_guides=False,
    margin=80,
    box_mode="tukey",
    x_label_rotation=0,
    yrange=(35, 110),
    range=(35, 110),
    y_labels=[40, 50, 60, 70, 80, 90, 100, 110],
    dots_size=8,
)

# Track which experience levels have been labeled in legend
labeled_levels = set()

# Add the 9 boxes grouped by department
for dept in departments:
    for level in experience_levels:
        values = data[(dept, level)].tolist()
        # Only first occurrence of each experience level gets a legend entry
        if level not in labeled_levels:
            chart.add(level, values)
            labeled_levels.add(level)
        else:
            # Suppress legend entry with None label
            chart.add(None, values)

# X-axis labels: show department name in center of each group
x_labels = ["", "Engineering", "", "", "Marketing", "", "", "Sales", ""]
chart.x_labels = x_labels

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

Part of Grouped Box Plot on anyplot.ai.

Other implementations