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.

""" 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.