Basic Ternary Plot — Seaborn

A ternary plot displays three-component compositional data on an equilateral triangle where each vertex represents 100% of one component. Points inside the triangle show compositions that sum to a constant total (usually 100%), with position indicating relative proportions. This visualization is essential for data where three variables are interdependent and constrained to sum to a fixed value.

Basic Ternary Plot rendered with Seaborn

Python source (Seaborn)

""" anyplot.ai
ternary-basic: Basic Ternary Plot
Library: seaborn 0.13.2 | Python 3.13.13
Quality: 96/100 | Updated: 2026-05-06
"""

import os
import sys


# Remove script directory from sys.path to avoid local matplotlib.py shadow
script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path = [p for p in sys.path if os.path.abspath(p) != script_dir]

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


# Theme tokens (see prompts/default-style-guide.md)
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
BRAND = "#009E73"  # Okabe-Ito position 1

# Set seaborn/matplotlib 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.10,
    },
)

# Data - Cement composition samples (Limestone, Clay, Gypsum)
# Realistic cement clinker composition percentages
np.random.seed(42)
n_points = 50

# Generate random compositions that sum to 100
raw = np.random.dirichlet(alpha=[2, 2, 2], size=n_points) * 100
df = pd.DataFrame({"Limestone": raw[:, 0], "Clay": raw[:, 1], "Gypsum": raw[:, 2]})

# Ternary coordinates transformation (vectorized)
# Convert (a, b, c) to Cartesian (x, y) where a + b + c = 100
# Triangle vertices: Bottom-left (0,0)=Limestone, Bottom-right (1,0)=Clay, Top (0.5, sqrt(3)/2)=Gypsum
sqrt3_2 = np.sqrt(3) / 2
limestone_norm = df["Limestone"].values / 100
clay_norm = df["Clay"].values / 100
gypsum_norm = df["Gypsum"].values / 100
x = 0.5 * (2 * clay_norm + gypsum_norm)
y = sqrt3_2 * gypsum_norm

# Create plot
fig, ax = plt.subplots(figsize=(12, 12), facecolor=PAGE_BG)

# Draw triangle outline
triangle = np.array([[0, 0], [1, 0], [0.5, sqrt3_2], [0, 0]])
ax.plot(triangle[:, 0], triangle[:, 1], color=INK_SOFT, linewidth=2, zorder=5)

# Draw grid lines at 10% intervals
grid_lw = 1

for level in [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]:
    # Lines parallel to bottom (constant Gypsum)
    x1, y1 = 0.5 * level, sqrt3_2 * level
    x2, y2 = 1 - 0.5 * level, sqrt3_2 * level
    ax.plot([x1, x2], [y1, y2], color=INK, alpha=0.10, linewidth=grid_lw, zorder=1)

    # Lines parallel to left edge (constant Clay)
    x1, y1 = level, 0
    x2, y2 = 0.5 + 0.5 * level, sqrt3_2 * (1 - level)
    ax.plot([x1, x2], [y1, y2], color=INK, alpha=0.10, linewidth=grid_lw, zorder=1)

    # Lines parallel to right edge (constant Limestone)
    x1, y1 = 0.5 * (1 - level), sqrt3_2 * (1 - level)
    x2, y2 = 1 - level, 0
    ax.plot([x1, x2], [y1, y2], color=INK, alpha=0.10, linewidth=grid_lw, zorder=1)

# Add tick marks along edges (at 20% intervals)
tick_length = 0.02

for level in [0.2, 0.4, 0.6, 0.8]:
    # Bottom edge ticks (Clay percentage increasing left to right)
    ax.plot([level, level], [-tick_length, 0], color=INK_SOFT, linewidth=1.5, zorder=5)
    ax.text(level, -0.05, f"{int(level * 100)}%", ha="center", va="top", fontsize=14, color=INK_SOFT)

    # Left edge ticks (Gypsum percentage)
    x_tick = 0.5 * level
    y_tick = sqrt3_2 * level
    dx, dy = -tick_length * np.cos(np.pi / 6), -tick_length * np.sin(np.pi / 6)
    ax.plot([x_tick, x_tick + dx], [y_tick, y_tick + dy], color=INK_SOFT, linewidth=1.5, zorder=5)
    ax.text(
        x_tick + dx - 0.03,
        y_tick + dy + 0.01,
        f"{int(level * 100)}%",
        ha="right",
        va="center",
        fontsize=14,
        color=INK_SOFT,
    )

    # Right edge ticks (Gypsum percentage from right side)
    x_tick = 1 - 0.5 * level
    y_tick = sqrt3_2 * level
    dx, dy = tick_length * np.cos(np.pi / 6), -tick_length * np.sin(np.pi / 6)
    ax.plot([x_tick, x_tick + dx], [y_tick, y_tick + dy], color=INK_SOFT, linewidth=1.5, zorder=5)
    ax.text(
        x_tick + dx + 0.03,
        y_tick + dy + 0.01,
        f"{int(level * 100)}%",
        ha="left",
        va="center",
        fontsize=14,
        color=INK_SOFT,
    )

# Plot data points
scatter_df = pd.DataFrame({"x": x, "y": y})
ax.scatter(scatter_df["x"], scatter_df["y"], color=BRAND, s=200, alpha=0.7, edgecolor=PAGE_BG, linewidth=1.5, zorder=10)

# Vertex labels
label_offset = 0.08
ax.text(0, -label_offset, "Limestone (100%)", ha="center", va="top", fontsize=20, fontweight="bold", color=INK)
ax.text(1, -label_offset, "Clay (100%)", ha="center", va="top", fontsize=20, fontweight="bold", color=INK)
ax.text(
    0.5, sqrt3_2 + label_offset, "Gypsum (100%)", ha="center", va="bottom", fontsize=20, fontweight="bold", color=INK
)

# Title
ax.set_title(
    "Cement Composition · ternary-basic · seaborn · pyplots.ai", fontsize=24, pad=20, color=INK, fontweight="medium"
)

# Clean up axes
ax.set_xlim(-0.15, 1.15)
ax.set_ylim(-0.18, 1.05)
ax.set_aspect("equal")
ax.axis("off")

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

Part of Basic Ternary Plot on anyplot.ai.

Other implementations