Box Plot with Strip Overlay — Bokeh

A combined visualization that overlays individual data points (strip plot) on top of a box plot. This provides both summary statistics (median, quartiles, whiskers) and visibility of the actual data distribution.

Box Plot with Strip Overlay rendered with Bokeh

Python source (Bokeh)

""" anyplot.ai
cat-box-strip: Box Plot with Strip Overlay
Library: bokeh 3.9.0 | Python 3.13.13
Quality: 82/100 | Updated: 2026-05-13
"""

import numpy as np
import pandas as pd
from bokeh.io import export_png, output_file, save
from bokeh.models import ColumnDataSource, Whisker
from bokeh.plotting import figure
from bokeh.transform import jitter


# Data - Plant growth measurements across different soil types
np.random.seed(42)

categories = ["Sandy", "Clay", "Loamy", "Silty"]
n_per_group = [35, 40, 45, 38]

# Generate data with different distributions per group
data = []
for cat, n in zip(categories, n_per_group, strict=True):
    if cat == "Sandy":
        values = np.random.normal(25, 6, n)  # Lower growth, moderate variance
    elif cat == "Clay":
        values = np.random.normal(32, 8, n)  # Medium growth, high variance
        values = np.append(values, [55, 58])  # Add outliers
    elif cat == "Loamy":
        values = np.random.normal(42, 5, n)  # High growth, low variance
    else:  # Silty
        values = np.random.normal(35, 7, n)  # Medium-high growth
        values = np.append(values, [12, 14])  # Add low outliers

    for v in values:
        data.append({"category": cat, "value": v})

df = pd.DataFrame(data)

# Calculate box plot statistics for each category
box_data = {"category": [], "q1": [], "q2": [], "q3": [], "upper": [], "lower": []}

for cat in categories:
    group = df[df["category"] == cat]["value"]
    q1 = group.quantile(0.25)
    q2 = group.quantile(0.50)
    q3 = group.quantile(0.75)
    iqr = q3 - q1
    upper_whisker = group[group <= q3 + 1.5 * iqr].max()
    lower_whisker = group[group >= q1 - 1.5 * iqr].min()

    box_data["category"].append(cat)
    box_data["q1"].append(q1)
    box_data["q2"].append(q2)
    box_data["q3"].append(q3)
    box_data["upper"].append(upper_whisker)
    box_data["lower"].append(lower_whisker)

box_source = ColumnDataSource(data=box_data)

# Create figure with categorical x-axis
p = figure(
    width=4800,
    height=2700,
    x_range=categories,
    title="cat-box-strip · bokeh · pyplots.ai",
    x_axis_label="Soil Type",
    y_axis_label="Plant Growth (cm)",
    tools="",
    toolbar_location=None,
)

# Styling - scaled for 4800x2700 canvas
p.title.text_font_size = "36pt"
p.xaxis.axis_label_text_font_size = "28pt"
p.yaxis.axis_label_text_font_size = "28pt"
p.xaxis.major_label_text_font_size = "24pt"
p.yaxis.major_label_text_font_size = "22pt"
p.xaxis.axis_label_standoff = 25
p.yaxis.axis_label_standoff = 25

# Grid styling
p.grid.grid_line_alpha = 0.3
p.grid.grid_line_dash = [6, 4]
p.xgrid.grid_line_color = None

# Background
p.background_fill_color = "#fafafa"

# Draw whiskers using the Whisker annotation
upper_whisker = Whisker(
    source=box_source, base="category", upper="upper", lower="q3", line_color="#306998", line_width=2.5
)
upper_whisker.upper_head.size = 30
upper_whisker.upper_head.line_color = "#306998"
upper_whisker.upper_head.line_width = 2.5
upper_whisker.lower_head.size = 0
p.add_layout(upper_whisker)

lower_whisker = Whisker(
    source=box_source, base="category", upper="q1", lower="lower", line_color="#306998", line_width=2.5
)
lower_whisker.lower_head.size = 30
lower_whisker.lower_head.line_color = "#306998"
lower_whisker.lower_head.line_width = 2.5
lower_whisker.upper_head.size = 0
p.add_layout(lower_whisker)

# Draw boxes (IQR range) - upper half
p.vbar(
    x="category",
    top="q3",
    bottom="q2",
    width=0.5,
    source=box_source,
    fill_color="#306998",
    fill_alpha=0.4,
    line_color="#306998",
    line_width=3,
)

# Draw boxes (IQR range) - lower half
p.vbar(
    x="category",
    top="q2",
    bottom="q1",
    width=0.5,
    source=box_source,
    fill_color="#306998",
    fill_alpha=0.4,
    line_color="#306998",
    line_width=3,
)

# Median line (horizontal segment across the box)
p.segment(x0="category", x1="category", y0="q2", y1="q2", source=box_source, line_color="#1a3d5c", line_width=4)

# Strip plot overlay with jitter
strip_source = ColumnDataSource(data={"category": df["category"], "value": df["value"]})

p.scatter(
    x=jitter("category", width=0.3, range=p.x_range),
    y="value",
    source=strip_source,
    size=16,
    fill_color="#FFD43B",
    fill_alpha=0.75,
    line_color="#b8860b",
    line_width=2,
)

# Save PNG
export_png(p, filename="plot.png")

# Save HTML for interactive version
output_file("plot.html")
save(p)

Part of Box Plot with Strip Overlay on anyplot.ai.

Other implementations