Horizon Chart — Matplotlib

A horizon chart displays many time series compactly by folding values into color-coded bands, preserving local resolution while minimizing vertical space. It divides the y-axis into bands and uses color intensity to encode magnitude, allowing dozens of series to be compared in limited space. This technique is particularly effective when monitoring many metrics simultaneously where traditional line charts would become unreadable.

Horizon Chart rendered with Matplotlib

Python source (Matplotlib)

""" anyplot.ai
horizon-basic: Horizon Chart
Library: matplotlib 3.10.9 | Python 3.13.13
Quality: 91/100 | Updated: 2026-05-07
"""

import os

import matplotlib.colors as mcolors
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd


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

# Data - 8 server metrics over 24 hours with more dramatic variation
np.random.seed(42)
hours = pd.date_range("2024-01-15", periods=24, freq="h")

series_names = ["CPU Load", "Memory", "Network I/O", "Disk I/O", "Requests/s", "Latency", "Queue Depth", "Threads"]
n_series = len(series_names)
n_points = len(hours)

# Generate realistic patterns with increased variation
data = {}
for i, name in enumerate(series_names):
    # Stronger daily cycle patterns
    base = np.sin(np.linspace(0, 2 * np.pi, n_points) + i * np.pi / 4) * 0.5
    noise = np.random.randn(n_points) * 0.2
    # More pronounced spikes
    spikes = np.zeros(n_points)
    if i % 2 == 0:
        spike_idx = np.random.choice(n_points, 4, replace=False)
        spikes[spike_idx] = np.random.uniform(0.7, 1.2, 4) * (1 if np.random.random() > 0.3 else -1)
    data[name] = np.clip(base + noise + spikes, -1.5, 1.5)

# Horizon chart parameters
n_bands = 3
band_height = 1.0

# Color bases with theme-adaptive styling
pos_base = mcolors.to_rgb("#009E73")  # imprint green — positive
neg_base = mcolors.to_rgb("#AE3030")  # imprint red — negative

# Create figure
fig, axes = plt.subplots(n_series, 1, figsize=(16, 9), sharex=True, facecolor=PAGE_BG)
fig.subplots_adjust(hspace=0.08, top=0.92, bottom=0.10, left=0.12, right=0.92)

# Plot each series as a horizon chart
for idx, (name, values) in enumerate(data.items()):
    ax = axes[idx]
    ax.set_facecolor(PAGE_BG)

    # Normalize values to fit in bands
    max_abs = max(abs(values.min()), abs(values.max()), 0.01)
    normalized = values / max_abs

    # Create band boundaries
    band_edges = np.linspace(0, 1, n_bands + 1)

    # Plot positive bands
    for band_idx in range(n_bands):
        lower = band_edges[band_idx]
        upper = band_edges[band_idx + 1]

        band_values = np.clip(normalized, 0, None)
        folded = np.clip(band_values - lower, 0, upper - lower)

        alpha = 0.35 + 0.25 * band_idx
        color = (*pos_base, alpha)

        ax.fill_between(hours, 0, folded, color=color, linewidth=0)

    # Plot negative bands
    for band_idx in range(n_bands):
        lower = band_edges[band_idx]
        upper = band_edges[band_idx + 1]

        band_values = np.clip(-normalized, 0, None)
        folded = np.clip(band_values - lower, 0, upper - lower)

        alpha = 0.35 + 0.25 * band_idx
        color = (*neg_base, alpha)

        ax.fill_between(hours, 0, folded, color=color, linewidth=0)

    # Style each subplot
    ax.set_ylim(0, 1 / n_bands + 0.05)
    ax.set_xlim(hours[0], hours[-1])

    # Add series label on the right with theme-aware color
    ax.text(1.01, 0.5, name, transform=ax.transAxes, fontsize=18, fontweight="bold", va="center", ha="left", color=INK)

    # Remove spines and ticks for clean look
    ax.set_yticks([])
    ax.spines["top"].set_visible(False)
    ax.spines["right"].set_visible(False)
    ax.spines["left"].set_visible(False)

    if idx < n_series - 1:
        ax.spines["bottom"].set_visible(False)
        ax.tick_params(axis="x", length=0, colors=INK_SOFT)
    else:
        ax.spines["bottom"].set_color(INK_SOFT)
        ax.tick_params(axis="x", labelsize=16, colors=INK_SOFT, labelcolor=INK_SOFT)

# Configure x-axis on bottom subplot
axes[-1].set_xlabel("Time (Hour of Day)", fontsize=20, color=INK)

# Title
fig.suptitle("horizon-basic · matplotlib · anyplot.ai", fontsize=24, fontweight="medium", y=0.97, color=INK)

# Add legend for bands with theme-aware colors
legend_elements = [
    plt.Rectangle((0, 0), 1, 1, facecolor=(*pos_base, 0.35), label="Low +"),
    plt.Rectangle((0, 0), 1, 1, facecolor=(*pos_base, 0.60), label="Mid +"),
    plt.Rectangle((0, 0), 1, 1, facecolor=(*pos_base, 0.85), label="High +"),
    plt.Rectangle((0, 0), 1, 1, facecolor=(*neg_base, 0.35), label="Low −"),
    plt.Rectangle((0, 0), 1, 1, facecolor=(*neg_base, 0.60), label="Mid −"),
    plt.Rectangle((0, 0), 1, 1, facecolor=(*neg_base, 0.85), label="High −"),
]
leg = fig.legend(
    handles=legend_elements,
    loc="upper center",
    ncol=6,
    fontsize=16,
    frameon=True,
    bbox_to_anchor=(0.53, 0.94),
    facecolor=ELEVATED_BG,
    edgecolor=INK_SOFT,
)
plt.setp(leg.get_texts(), color=INK_SOFT)

plt.savefig(f"plot-{THEME}.png", dpi=300, bbox_inches="tight", facecolor=PAGE_BG)

Part of Horizon Chart on anyplot.ai.

Other implementations