Time in Heart Rate Zones Bar Chart — Seaborn

A vertical bar chart showing the time spent in each heart rate training zone (Z1–Z5) during a workout or training period. Each bar uses the conventional zone color semantics (grey for Z1 recovery, blue for Z2 endurance, green for Z3 aerobic, orange for Z4 threshold, red for Z5 maximum) and is annotated with its duration, making the distribution of training intensity immediately readable. This is the standard summary chart found in fitness platforms such as Garmin, Strava, and Polar, and it reveals at a glance whether a session was easy, balanced, or high-intensity.

Time in Heart Rate Zones Bar Chart rendered with Seaborn

Python source (Seaborn)

""" anyplot.ai
bar-heart-rate-zones: Time in Heart Rate Zones Bar Chart
Library: seaborn 0.13.2 | Python 3.13.13
Quality: 87/100 | Created: 2026-06-14
"""

import os

import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns


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

# Zone colors — semantic mapping per spec (conventional fitness zone colors)
# Z1 grey   → Imprint muted anchor (theme-adaptive)
# Z2 blue   → Imprint #4467A3
# Z3 green  → Imprint #009E73 (brand green; aerobic / moderate effort)
# Z4 orange → Imprint #BD8233 (ochre, nearest to orange in Imprint palette)
# Z5 red    → Imprint #AE3030 (matte red, semantic anchor for max / critical)
ZONE_COLORS = [INK_MUTED, "#4467A3", "#009E73", "#BD8233", "#AE3030"]

# Data — 60-minute tempo run: most time near threshold (Z4)
zones = ["Z1", "Z2", "Z3", "Z4", "Z5"]
zone_names = ["Recovery", "Endurance", "Aerobic", "Threshold", "Maximum"]
minutes = [5, 10, 18, 20, 7]
hr_ranges = ["< 111 bpm", "111–130 bpm", "130–148 bpm", "148–166 bpm", "> 166 bpm"]

# Multi-line x-tick labels: zone ID / zone name / HR boundary
x_tick_labels = [f"{z}\n{n}\n{h}" for z, n, h in zip(zones, zone_names, hr_ranges, strict=False)]

# Palette dict for sns.barplot hue mapping
zone_palette = dict(zip(zones, ZONE_COLORS, strict=False))

# Seaborn theme
sns.set_theme(
    style="ticks",
    rc={
        "figure.facecolor": PAGE_BG,
        "axes.facecolor": PAGE_BG,
        "axes.edgecolor": INK_SOFT,
        "axes.labelcolor": INK,
        "text.color": INK,
        "xtick.color": INK_SOFT,
        "ytick.color": INK_SOFT,
        "grid.color": INK,
        "grid.alpha": 0.15,
        "legend.facecolor": ELEVATED_BG,
        "legend.edgecolor": INK_SOFT,
    },
)

# DataFrame for seaborn — categorical order preserves Z1→Z5 sequence
df = pd.DataFrame({"zone": pd.Categorical(zones, categories=zones, ordered=True), "minutes": minutes})

# Plot — landscape 3200 × 1800 px
fig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)
ax.set_facecolor(PAGE_BG)

# sns.barplot — seaborn-native bar chart with hue for per-bar palette
sns.barplot(
    data=df, x="zone", y="minutes", hue="zone", palette=zone_palette, ax=ax, errorbar=None, legend=False, width=0.55
)

# Apply edge color to match page background (post-draw, patches are accessible)
for patch in ax.patches:
    patch.set_edgecolor(PAGE_BG)
    patch.set_linewidth(1.2)

# X-tick labels (zone ID / name / HR boundary) — restore after barplot
ax.set_xticks(range(len(zones)))
ax.set_xticklabels(x_tick_labels, fontsize=8)
ax.tick_params(axis="x", which="both", length=0)

# Y-tick labels
ax.tick_params(axis="y", labelsize=8, colors=INK_SOFT)

# Bar duration labels
for patch, mins in zip(ax.patches, minutes, strict=False):
    ax.text(
        patch.get_x() + patch.get_width() / 2,
        patch.get_height() + 0.4,
        f"{mins} min",
        ha="center",
        va="bottom",
        fontsize=9,
        fontweight="bold",
        color=INK,
    )

# Title and axis labels
title = "bar-heart-rate-zones · python · seaborn · anyplot.ai"
ax.set_title(title, fontsize=12, fontweight="medium", color=INK, pad=12)
ax.set_xlabel("Heart Rate Zone", fontsize=10, color=INK, labelpad=10)
ax.set_ylabel("Time (minutes)", fontsize=10, color=INK, labelpad=8)

# Y-axis grid
ax.yaxis.grid(True, alpha=0.15, linewidth=0.8, color=INK)
ax.set_axisbelow(True)

# Spines
sns.despine(ax=ax, top=True, right=True)
ax.spines["left"].set_color(INK_SOFT)
ax.spines["bottom"].set_color(INK_SOFT)

# Y-axis range — headroom for bar labels
ax.set_ylim(0, max(minutes) * 1.3)

# Layout
fig.subplots_adjust(left=0.09, right=0.97, top=0.93, bottom=0.22)

# Save
plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG)
plt.close()

Part of Time in Heart Rate Zones Bar Chart on anyplot.ai.

Other implementations