Basic Box Plot — plotnine

A box plot (box-and-whisker plot) showing the distribution of numerical data through quartiles. Displays the median, first and third quartiles as a box, with whiskers extending to show the data range. Essential for comparing distributions across categories and identifying outliers.

Basic Box Plot rendered with plotnine

Python source (plotnine)

""" anyplot.ai
box-basic: Basic Box Plot
Library: plotnine 0.15.4 | Python 3.13.13
Quality: 87/100 | Updated: 2026-05-28
"""

import os
import sys


# Prevent this file from shadowing the plotnine package (script is named plotnine.py)
sys.path = [p for p in sys.path if os.path.abspath(p or ".") != os.path.dirname(os.path.abspath(__file__))]

import numpy as np
import pandas as pd
from plotnine import (
    aes,
    annotate,
    coord_cartesian,
    element_blank,
    element_line,
    element_rect,
    element_text,
    geom_boxplot,
    ggplot,
    labs,
    scale_fill_manual,
    scale_y_continuous,
    stat_summary,
    theme,
    theme_minimal,
)


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"]

# Data
np.random.seed(42)
categories = ["Engineering", "Marketing", "Sales", "Support", "Research"]
records = []

for cat in categories:
    n = np.random.randint(60, 120)
    if cat == "Engineering":
        values = np.random.normal(95000, 15000, n)
    elif cat == "Marketing":
        values = np.random.normal(75000, 12000, n)
    elif cat == "Sales":
        base = np.random.normal(68000, 18000, n)
        outliers = np.random.normal(135000, 6000, 4)
        values = np.concatenate([base, outliers])
    elif cat == "Support":
        values = np.random.normal(55000, 8000, n)
    else:  # Research
        values = np.random.normal(85000, 20000, n)
    records.extend({"department": cat, "salary": v} for v in values)

df = pd.DataFrame(records)
dept_order = ["Support", "Marketing", "Sales", "Research", "Engineering"]
df["department"] = pd.Categorical(df["department"], categories=dept_order, ordered=True)

medians = df.groupby("department", observed=True)["salary"].median()
eng_median = medians["Engineering"]
sup_median = medians["Support"]
gap = eng_median - sup_median

# Plot
plot = (
    ggplot(df, aes(x="department", y="salary", fill="department"))
    + geom_boxplot(
        outlier_size=2.5, outlier_alpha=0.65, outlier_colour=INK_SOFT, size=0.5, alpha=0.85, width=0.6, color=INK_SOFT
    )
    + stat_summary(fun_y=np.median, geom="point", size=4, shape="D", color=INK, fill=INK)
    + scale_fill_manual(values=IMPRINT_PALETTE[:5])
    + scale_y_continuous(labels=lambda vals: [f"${v / 1000:.0f}k" for v in vals], breaks=range(20000, 160001, 20000))
    + coord_cartesian(ylim=(12000, 160000))
    # Gap annotation — bracket style with ticks at Support (x=1) and Engineering (x=5)
    + annotate(
        "text",
        x=3,
        y=157000,
        label=f"Engineering earns ${gap / 1000:.0f}k more than Support",
        color=INK,
        size=4.0,
        ha="center",
        fontweight="bold",
    )
    + annotate("segment", x=1, xend=5, y=152500, yend=152500, color=INK_SOFT, size=0.6, alpha=0.65)
    + annotate("segment", x=1, xend=1, y=149000, yend=152500, color=INK_SOFT, size=0.6, alpha=0.65)
    + annotate("segment", x=5, xend=5, y=149000, yend=152500, color=INK_SOFT, size=0.6, alpha=0.65)
    # Senior hires: diagonal connector from Sales outlier cluster to label
    + annotate(
        "label",
        x=4.1,
        y=143000,
        label="Senior hires\nabove market rate",
        size=4.0,
        color=INK,
        fill=ELEVATED_BG,
        alpha=0.92,
        label_size=0,
        ha="center",
    )
    + annotate("segment", x=3.08, xend=4.0, y=136500, yend=141000, color=INK_SOFT, size=0.5, alpha=0.65)
    # Support tight distribution insight
    + annotate(
        "text",
        x=1,
        y=24000,
        label="Narrow spread\n(σ ≈ $8k)",
        color=INK_MUTED,
        size=3.8,
        ha="center",
        fontstyle="italic",
    )
    + labs(x="Department", y="Salary ($)", title="box-basic · python · plotnine · anyplot.ai")
    + theme_minimal()
    + theme(
        figure_size=(8, 4.5),
        text=element_text(size=7, color=INK),
        plot_title=element_text(size=12, color=INK),
        axis_title=element_text(size=10, color=INK),
        axis_text=element_text(size=8, color=INK_SOFT),
        legend_position="none",
        panel_grid_major_x=element_blank(),
        panel_grid_minor=element_blank(),
        panel_grid_major_y=element_line(color=INK, size=0.3, alpha=0.15),
        axis_ticks_major_x=element_blank(),
        axis_ticks_major_y=element_blank(),
        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        panel_border=element_blank(),
    )
)

# Save
plot.save(f"plot-{THEME}.png", dpi=400, width=8, height=4.5, units="in", verbose=False)

Part of Basic Box Plot on anyplot.ai.

Other implementations