Horizon Chart — Seaborn

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 Seaborn

Renders

Python source (Seaborn)

""" anyplot.ai
horizon-basic: Horizon Chart
Library: seaborn 0.13.2 | Python 3.13.15
Quality: 85/100 | Updated: 2026-08-18
"""

import os

import matplotlib.patches as mpatches
import numpy as np
import pandas as pd
import seaborn as sns


# Theme tokens (see prompts/default-style-guide.md "Background" + "Theme-adaptive Chrome")
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"

# Imprint palette — blue (gain) vs. matte red (loss) diverging pair; both are
# CVD-distinguishable, unlike a green/red pairing that deuteranopes/protanopes
# cannot tell apart.
IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"]
GAIN_BASE = IMPRINT_PALETTE[2]  # blue
LOSS_BASE = IMPRINT_PALETTE[4]  # matte red — semantic anchor for loss

# Data - stock price deviations from 20-day moving average (5 stocks over 90 trading days)
np.random.seed(42)
trading_days = 90
stocks = ["TECH", "FINANCE", "ENERGY", "HEALTHCARE", "RETAIL"]

data = []
for stock_idx, stock in enumerate(stocks):
    np.random.seed(42 + stock_idx)
    if stock == "TECH":
        base = 8 * np.sin(np.linspace(0, 4 * np.pi, trading_days)) + 3
        noise = np.random.randn(trading_days) * 5
        values = base + noise
    elif stock == "FINANCE":
        base = np.zeros(trading_days)
        noise = np.random.randn(trading_days) * 6
        volatility_spikes = np.random.choice([-8, 0, 8], trading_days, p=[0.15, 0.7, 0.15])
        values = base + noise + volatility_spikes
    elif stock == "ENERGY":
        base = -5 * np.ones(trading_days)
        trend = np.linspace(-5, 5, trading_days)
        noise = np.random.randn(trading_days) * 4
        values = base + trend + noise
    elif stock == "HEALTHCARE":
        base = 6 * np.cos(np.linspace(0, 3 * np.pi, trading_days))
        noise = np.random.randn(trading_days) * 4
        values = base + noise
    else:
        drift = np.linspace(-8, 8, trading_days)
        noise = np.random.randn(trading_days) * 3
        values = drift + noise

    values = np.clip(values, -15, 15)
    for day, v in enumerate(values):
        data.append({"day": day, "stock": stock, "deviation": v})

df = pd.DataFrame(data)

# Horizon chart parameters — 3 intensity bands per polarity, generated from the
# Imprint gain/loss anchors via seaborn's own sequential-palette builder.
n_bands = 3
band_height = 15 / n_bands
gain_colors = sns.light_palette(GAIN_BASE, n_colors=n_bands + 1)[1:]
loss_colors = sns.light_palette(LOSS_BASE, n_colors=n_bands + 1)[1:]

# Theme-adaptive chrome via seaborn's rc-based theme context
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,
    },
)

# Plot — one row per stock via seaborn's FacetGrid, hand-filled with folded
# horizon bands (see prompts/library/seaborn.md "Canvas — hard rule")
g = sns.FacetGrid(df, row="stock", row_order=stocks, sharex=True, sharey=True, despine=True)
g.fig.set_size_inches(8, 4.5)
g.fig.set_dpi(400)
g.fig.subplots_adjust(hspace=0.06, top=0.82, bottom=0.34, left=0.24, right=0.97)
g.set_titles("")

tick_positions = np.arange(0, trading_days, 15)
tick_labels = [f"Day {i}" for i in tick_positions]

for idx, (stock, ax) in enumerate(zip(stocks, g.axes.flat, strict=True)):
    stock_data = df[df["stock"] == stock]
    x = np.arange(len(stock_data))
    values = stock_data["deviation"].to_numpy()

    ax.set_xlim(0, len(x))
    ax.set_ylim(0, band_height)
    ax.set_xticks(tick_positions)

    gain_vals = np.maximum(values, 0)
    loss_vals = np.abs(np.minimum(values, 0))

    for band_idx in range(n_bands):
        band_min = band_idx * band_height

        loss_folded = np.clip(loss_vals - band_min, 0, band_height)
        loss_mask = (loss_vals > band_min) & (values < 0)
        ax.fill_between(
            x, 0, np.where(loss_mask, loss_folded, np.nan), color=loss_colors[band_idx], alpha=0.95, linewidth=0
        )

        gain_folded = np.clip(gain_vals - band_min, 0, band_height)
        gain_mask = (gain_vals > band_min) & (values > 0)
        ax.fill_between(
            x, 0, np.where(gain_mask, gain_folded, np.nan), color=gain_colors[band_idx], alpha=0.95, linewidth=0
        )

    ax.set_ylabel(stock, fontsize=16, rotation=0, ha="right", va="center", labelpad=15, color=INK)
    ax.set_yticks([])
    ax.grid(True, axis="x", alpha=0.2, linewidth=0.8, color=INK_SOFT)
    ax.set_axisbelow(True)
    for spine in ("top", "right", "left"):
        ax.spines[spine].set_visible(False)
    ax.spines["bottom"].set_visible(idx == len(stocks) - 1)
    is_last = idx == len(stocks) - 1
    ax.tick_params(axis="x", labelsize=14, bottom=is_last, labelbottom=is_last)

# X-axis formatting — labels/ticklabels only on the last facet, but every facet
# shares the same tick_positions (set above) so gridlines align vertically across rows
last_ax = g.axes.flat[-1]
last_ax.set_xticklabels(tick_labels)
last_ax.set_xlabel("Trading Days (90-day period)", fontsize=18, color=INK)

# Title + subtitle clarifying what the bands measure
g.fig.suptitle(
    "horizon-basic · python · seaborn · anyplot.ai", fontsize=18, y=0.98, va="top", fontweight="bold", color=INK
)
g.fig.text(
    0.5,
    0.895,
    "Deviation from 20-Day Moving Average (percentage points)",
    ha="center",
    va="top",
    fontsize=13,
    color=INK_SOFT,
)

# Legend — single row anchored below the x-axis label, entirely clear of every facet's data
legend_patches = [
    mpatches.Patch(color=gain_colors[0], label="Gain 0-5 pp"),
    mpatches.Patch(color=gain_colors[1], label="Gain 5-10 pp"),
    mpatches.Patch(color=gain_colors[2], label="Gain 10-15 pp"),
    mpatches.Patch(color=loss_colors[0], label="Loss 0-5 pp"),
    mpatches.Patch(color=loss_colors[1], label="Loss 5-10 pp"),
    mpatches.Patch(color=loss_colors[2], label="Loss 10-15 pp"),
]
g.fig.legend(
    handles=legend_patches,
    loc="lower center",
    bbox_to_anchor=(0.5, 0.01),
    fontsize=15,
    framealpha=0.95,
    facecolor=ELEVATED_BG,
    edgecolor=INK_SOFT,
    ncol=3,
    handlelength=1.4,
    handletextpad=0.5,
    columnspacing=1.3,
)

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

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/horizon-basic/seaborn/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "horizon-basic",
  "language": "python",
  "library": "seaborn",
  "page": "https://anyplot.ai/horizon-basic/python/seaborn",
  "hub": "https://anyplot.ai/horizon-basic",
  "code_json": "https://api.anyplot.ai/specs/horizon-basic/seaborn/code",
  "spec_json": "https://api.anyplot.ai/specs/horizon-basic",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/horizon-basic/python/seaborn/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/horizon-basic/python/seaborn/plot-dark.png",
  "quality_score": 85.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Horizon Chart on anyplot.ai.

Other implementations