Stratigraphic Column with Lithology Patterns — Seaborn

A vertical column visualization showing geological rock layers with standardized lithology patterns, formation names, ages, and thickness scales. Each layer is represented as a stacked rectangular block filled with a distinctive pattern (e.g., brick pattern for limestone, dots for sandstone, dashes for shale) following FGDC/USGS conventions. This plot is essential for communicating subsurface geology and sedimentary sequences in a compact, standardized format.

Stratigraphic Column with Lithology Patterns rendered with Seaborn

Python source (Seaborn)

""" anyplot.ai
column-stratigraphic: Stratigraphic Column with Lithology Patterns
Library: seaborn 0.13.2 | Python 3.13.13
Quality: 92/100 | Updated: 2026-06-17
"""

import os

import matplotlib.patches as mpatches
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-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 — first lithology is always brand green; matte-red (#AE3030)
# is deliberately skipped here and reserved as the semantic boundary marker.
IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#2ABCCD", "#954477"]
UNCONF_RED = "#AE3030"  # Imprint matte-red — semantic unconformity anchor

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,
        "axes.linewidth": 1.0,
        "patch.linewidth": 1.0,
        "hatch.linewidth": 0.8,
    },
)

# Data — synthetic sedimentary section (Western Interior Seaway)
layers = [
    {"top": 0, "bottom": 15, "lithology": "sandstone", "formation": "Dakota Fm", "age": "Late Cretaceous"},
    {"top": 15, "bottom": 30, "lithology": "shale", "formation": "Graneros Sh", "age": "Late Cretaceous"},
    {"top": 30, "bottom": 52, "lithology": "limestone", "formation": "Greenhorn Ls", "age": "Late Cretaceous"},
    {"top": 52, "bottom": 65, "lithology": "shale", "formation": "Carlile Sh", "age": "Late Cretaceous"},
    {"top": 65, "bottom": 78, "lithology": "siltstone", "formation": "Niobrara Fm", "age": "Late Cretaceous"},
    {"top": 78, "bottom": 100, "lithology": "limestone", "formation": "Fort Hays Ls", "age": "Late Cretaceous"},
    {"top": 100, "bottom": 118, "lithology": "conglomerate", "formation": "Morrison Fm", "age": "Late Jurassic"},
    {"top": 118, "bottom": 140, "lithology": "sandstone", "formation": "Entrada Ss", "age": "Middle Jurassic"},
    {"top": 140, "bottom": 162, "lithology": "shale", "formation": "Chinle Fm", "age": "Late Triassic"},
    {"top": 162, "bottom": 180, "lithology": "dolomite", "formation": "Kaibab Fm", "age": "Early Permian"},
]

df = pd.DataFrame(layers)
df["thickness"] = df["bottom"] - df["top"]
df["mid_depth"] = (df["top"] + df["bottom"]) / 2

# Lithology styling — Imprint colors + hatch approximations of FGDC/USGS symbols
lith_order = ["sandstone", "shale", "limestone", "siltstone", "conglomerate", "dolomite"]
lith_hatch = {
    "sandstone": "...",  # stipple dots
    "shale": "---",  # horizontal dashes
    "limestone": "+++",  # brick / grid
    "siltstone": "//",  # diagonal dashes
    "conglomerate": "ooo",  # clasts / pebbles
    "dolomite": "xxx",  # rhombic
}
lith_label = {name: name.title() for name in lith_order}
lith_color = {name: IMPRINT_PALETTE[i] for i, name in enumerate(lith_order)}

total_depth = 180
col_width = 0.5

# Geological-age extents (min top → max bottom across that age's layers)
age_order = ["Late Cretaceous", "Late Jurassic", "Middle Jurassic", "Late Triassic", "Early Permian"]
age_spans = {age: (df[df["age"] == age]["top"].min(), df[df["age"] == age]["bottom"].max()) for age in age_order}
age_tint = dict(zip(age_order, IMPRINT_PALETTE[:5], strict=True))

# Plot — three panels: geological age | stratigraphic column | layer thickness
fig, (ax_age, ax, ax_thick) = plt.subplots(
    1, 3, figsize=(8, 4.5), dpi=400, width_ratios=[0.16, 0.50, 0.34], facecolor=PAGE_BG
)
for a in (ax_age, ax, ax_thick):
    a.set_facecolor(PAGE_BG)

# Stratigraphic column — bars drawn directly at true depth coordinates (no
# categorical-to-continuous repositioning). barh centers on mid_depth.
for _, row in df.iterrows():
    ax.barh(
        row["mid_depth"],
        col_width,
        height=row["thickness"],
        color=lith_color[row["lithology"]],
        hatch=lith_hatch[row["lithology"]],
        edgecolor=INK,
        linewidth=1.1,
        zorder=3,
    )

ax.set_ylim(total_depth, 0)  # depth increases downward (borehole convention)
ax.set_xlim(0, 1.6)

# Formation names to the right of each layer
for _, row in df.iterrows():
    ax.text(
        col_width + 0.07,
        row["mid_depth"],
        row["formation"],
        fontsize=7,
        fontweight="medium",
        va="center",
        ha="left",
        color=INK,
    )

# Major unconformities — wavy red boundary lines with labels
unconformities = [(100, "Cretaceous–Jurassic"), (140, "Jurassic–Triassic"), (162, "Triassic–Permian")]
for depth, label in unconformities:
    x_wave = np.linspace(0, col_width, 120)
    y_wave = depth + 1.1 * np.sin(x_wave * 45)
    ax.plot(x_wave, y_wave, color=UNCONF_RED, linewidth=1.6, zorder=5, solid_capstyle="round")
    ax.text(
        col_width / 2,
        depth - 1.6,
        label,
        fontsize=6,
        fontweight="bold",
        fontstyle="italic",
        va="bottom",
        ha="center",
        color=UNCONF_RED,
        zorder=6,
        bbox={"facecolor": ELEVATED_BG, "edgecolor": "none", "alpha": 0.85, "pad": 1.0},
    )

ax.set_xticks([])
ax.set_yticks([])
ax.set_xlabel("")
ax.set_ylabel("")
sns.despine(ax=ax, left=True, bottom=True, top=True, right=True)

# Age panel — depth scale, period brackets, age labels
ax_age.set_xlim(0, 1)
ax_age.set_ylim(total_depth, 0)
for age in age_order:
    top_d, bot_d = age_spans[age]
    mid_y = (top_d + bot_d) / 2
    # Faint Imprint tint groups each period across both panels
    ax_age.axhspan(top_d, bot_d, color=age_tint[age], alpha=0.06, zorder=0)
    ax.axhspan(top_d, bot_d, color=age_tint[age], alpha=0.05, zorder=0)
    # Period bracket
    ax_age.plot([0.82, 0.82], [top_d + 1, bot_d - 1], color=INK_SOFT, linewidth=1.4)
    ax_age.plot([0.76, 0.88], [top_d + 1, top_d + 1], color=INK_SOFT, linewidth=1.2)
    ax_age.plot([0.76, 0.88], [bot_d - 1, bot_d - 1], color=INK_SOFT, linewidth=1.2)
    ax_age.text(
        0.36,
        mid_y,
        age.replace(" ", "\n"),
        fontsize=8,
        fontweight="medium",
        fontstyle="italic",
        va="center",
        ha="center",
        color=INK,
    )

ax_age.set_ylabel("Depth (m)", fontsize=10, color=INK, labelpad=4)
ax_age.set_yticks(np.arange(0, total_depth + 1, 20))
ax_age.tick_params(axis="y", labelsize=8, colors=INK_SOFT)
ax_age.set_xticks([])
sns.despine(ax=ax_age, top=True, right=True, bottom=True)
ax_age.spines["left"].set_color(INK_SOFT)

# Thickness panel — seaborn strip plot of layer thickness by lithology
thickness_df = df[["lithology", "thickness"]].copy()
sns.stripplot(
    data=thickness_df,
    x="thickness",
    y="lithology",
    ax=ax_thick,
    hue="lithology",
    palette=lith_color,
    order=lith_order,
    hue_order=lith_order,
    size=10,
    marker="D",
    edgecolor=INK,
    linewidth=0.8,
    jitter=False,
    legend=False,
)
ax_thick.set_xlabel("Thickness (m)", fontsize=9, color=INK)
ax_thick.set_ylabel("")
ax_thick.set_yticks(range(len(lith_order)))
ax_thick.set_yticklabels([lith_label[name] for name in lith_order])
ax_thick.tick_params(axis="both", labelsize=8, colors=INK_SOFT)
ax_thick.set_title("Layer Thickness", fontsize=9, fontweight="medium", color=INK, pad=6)
ax_thick.set_axisbelow(True)
ax_thick.xaxis.grid(True, alpha=0.15, linewidth=0.8, color=INK)
sns.despine(ax=ax_thick, top=True, right=True)
ax_thick.spines["left"].set_color(INK_SOFT)
ax_thick.spines["bottom"].set_color(INK_SOFT)

# Lithology legend (decodes column hatching) — figure-level strip at the bottom
legend_handles = [
    mpatches.Patch(facecolor=lith_color[name], edgecolor=INK, hatch=lith_hatch[name], label=lith_label[name])
    for name in lith_order
]
legend = fig.legend(
    handles=legend_handles,
    loc="lower center",
    bbox_to_anchor=(0.53, 0.005),
    ncol=6,
    fontsize=7,
    title="Lithology",
    title_fontsize=8,
    frameon=True,
    facecolor=ELEVATED_BG,
    edgecolor=INK_SOFT,
    framealpha=0.95,
    columnspacing=1.1,
    handlelength=1.4,
)
legend.get_title().set_color(INK)
for text in legend.get_texts():
    text.set_color(INK)

# Title
fig.suptitle(
    "column-stratigraphic · python · seaborn · anyplot.ai", fontsize=14, fontweight="medium", color=INK, y=0.955
)

fig.subplots_adjust(left=0.07, right=0.99, top=0.86, bottom=0.18, wspace=0.07)

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

Part of Stratigraphic Column with Lithology Patterns on anyplot.ai.

Other implementations