Skip to content

Commit c747402

Browse files
feat(seaborn): implement column-stratigraphic (#4903)
## Implementation: `column-stratigraphic` - seaborn Implements the **seaborn** version of `column-stratigraphic`. **File:** `plots/column-stratigraphic/implementations/seaborn.py` **Parent Issue:** #4573 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23121198560)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent ea06a74 commit c747402

2 files changed

Lines changed: 466 additions & 0 deletions

File tree

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
""" pyplots.ai
2+
column-stratigraphic: Stratigraphic Column with Lithology Patterns
3+
Library: seaborn 0.13.2 | Python 3.14.3
4+
Quality: 89/100 | Created: 2026-03-15
5+
"""
6+
7+
import matplotlib.patches as mpatches
8+
import matplotlib.pyplot as plt
9+
import numpy as np
10+
import pandas as pd
11+
import seaborn as sns
12+
13+
14+
# Set seaborn theme for polished styling
15+
sns.set_theme(
16+
style="ticks",
17+
context="talk",
18+
font_scale=1.0,
19+
rc={"axes.linewidth": 1.2, "patch.linewidth": 1.5, "hatch.linewidth": 1.0},
20+
)
21+
22+
# Data - synthetic sedimentary section (Western Interior Seaway)
23+
layers = [
24+
{"top": 0, "bottom": 15, "lithology": "sandstone", "formation": "Dakota Fm", "age": "Late Cretaceous"},
25+
{"top": 15, "bottom": 30, "lithology": "shale", "formation": "Graneros Sh", "age": "Late Cretaceous"},
26+
{"top": 30, "bottom": 52, "lithology": "limestone", "formation": "Greenhorn Ls", "age": "Late Cretaceous"},
27+
{"top": 52, "bottom": 65, "lithology": "shale", "formation": "Carlile Sh", "age": "Late Cretaceous"},
28+
{"top": 65, "bottom": 78, "lithology": "siltstone", "formation": "Niobrara Fm", "age": "Late Cretaceous"},
29+
{"top": 78, "bottom": 100, "lithology": "limestone", "formation": "Fort Hays Ls", "age": "Late Cretaceous"},
30+
{"top": 100, "bottom": 118, "lithology": "conglomerate", "formation": "Morrison Fm", "age": "Late Jurassic"},
31+
{"top": 118, "bottom": 140, "lithology": "sandstone", "formation": "Entrada Ss", "age": "Middle Jurassic"},
32+
{"top": 140, "bottom": 162, "lithology": "shale", "formation": "Chinle Fm", "age": "Late Triassic"},
33+
{"top": 162, "bottom": 180, "lithology": "dolomite", "formation": "Kaibab Fm", "age": "Early Permian"},
34+
]
35+
36+
df = pd.DataFrame(layers)
37+
df["thickness"] = df["bottom"] - df["top"]
38+
df["mid_depth"] = (df["top"] + df["bottom"]) / 2
39+
df["col_width"] = 1.0
40+
41+
# Use seaborn color palette for distinct lithology colors (colorblind-safe)
42+
lith_types = ["sandstone", "shale", "limestone", "siltstone", "conglomerate", "dolomite"]
43+
palette = sns.color_palette("colorblind", n_colors=len(lith_types))
44+
lith_color_map = dict(zip(lith_types, [sns.desaturate(c, 0.7) for c in palette], strict=True))
45+
46+
lithology_styles = {
47+
"sandstone": {"color": lith_color_map["sandstone"], "hatch": "...", "label": "Sandstone"},
48+
"shale": {"color": lith_color_map["shale"], "hatch": "---", "label": "Shale"},
49+
"limestone": {"color": lith_color_map["limestone"], "hatch": "+++", "label": "Limestone"},
50+
"siltstone": {"color": lith_color_map["siltstone"], "hatch": "//", "label": "Siltstone"},
51+
"conglomerate": {"color": lith_color_map["conglomerate"], "hatch": "ooo", "label": "Conglomerate"},
52+
"dolomite": {"color": lith_color_map["dolomite"], "hatch": "xxx", "label": "Dolomite"},
53+
}
54+
55+
# Age period background colors via seaborn palette
56+
age_bg_palette = sns.color_palette("pastel", n_colors=5)
57+
age_list = ["Late Cretaceous", "Late Jurassic", "Middle Jurassic", "Late Triassic", "Early Permian"]
58+
age_bg_colors = {age: (*age_bg_palette[i], 0.10) for i, age in enumerate(age_list)}
59+
60+
total_depth = 180
61+
62+
# Build thickness data for side panel (seaborn distinctive feature)
63+
thickness_df = df[["lithology", "thickness"]].copy()
64+
thickness_df["lithology"] = thickness_df["lithology"].str.title()
65+
66+
# Plot - three-panel layout: age brackets | stratigraphic column | thickness strip
67+
fig, (ax_age, ax, ax_thick) = plt.subplots(1, 3, figsize=(16, 9), width_ratios=[0.16, 0.54, 0.30])
68+
69+
# Use sns.barplot for the lithology layers (seaborn plotting function)
70+
sns.barplot(
71+
data=df,
72+
y="formation",
73+
x="col_width",
74+
color="#306998",
75+
ax=ax,
76+
edgecolor="black",
77+
linewidth=1.5,
78+
order=df["formation"].tolist(),
79+
width=0.98,
80+
)
81+
82+
# Reposition bars from categorical to proportional depth scale and add hatching
83+
col_width = 0.55
84+
for i, (_, row) in enumerate(df.iterrows()):
85+
bar = ax.patches[i]
86+
style = lithology_styles[row["lithology"]]
87+
bar.set_facecolor(style["color"])
88+
bar.set_hatch(style["hatch"])
89+
bar.set_y(row["top"])
90+
bar.set_height(row["thickness"])
91+
bar.set_x(0)
92+
bar.set_width(col_width)
93+
94+
# Switch to continuous depth axis
95+
ax.set_ylim(total_depth, 0)
96+
ax.set_yticks([])
97+
98+
# Formation labels to the right of each layer
99+
for _, row in df.iterrows():
100+
ax.text(
101+
col_width + 0.05,
102+
row["mid_depth"],
103+
row["formation"],
104+
fontsize=15,
105+
fontweight="medium",
106+
va="center",
107+
ha="left",
108+
color="#2C2C2C",
109+
)
110+
111+
# Mark major unconformities with wavy lines for data storytelling
112+
unconformities = [(100, "Cretaceous–Jurassic"), (140, "Jurassic–Triassic"), (162, "Triassic–Permian")]
113+
114+
for depth, label in unconformities:
115+
x_wave = np.linspace(0, col_width, 80)
116+
y_wave = depth + 0.8 * np.sin(x_wave * 40)
117+
ax.plot(x_wave, y_wave, color="#CC3333", linewidth=2.5, zorder=5)
118+
# Place unconformity labels above the wavy line, inside column
119+
ax.text(
120+
col_width / 2,
121+
depth - 2.0,
122+
label,
123+
fontsize=10,
124+
fontweight="bold",
125+
va="bottom",
126+
ha="center",
127+
color="#CC3333",
128+
fontstyle="italic",
129+
zorder=6,
130+
bbox={"facecolor": "white", "alpha": 0.85, "edgecolor": "none", "pad": 1.5},
131+
)
132+
133+
# Age labels on the left panel
134+
age_positions = {}
135+
for _, row in df.iterrows():
136+
age = row["age"]
137+
if age not in age_positions:
138+
age_positions[age] = {"top": row["top"], "bottom": row["bottom"]}
139+
age_positions[age]["top"] = min(age_positions[age]["top"], row["top"])
140+
age_positions[age]["bottom"] = max(age_positions[age]["bottom"], row["bottom"])
141+
142+
ax_age.set_xlim(0, 1)
143+
ax_age.set_ylim(total_depth, 0)
144+
ax.set_xlim(-0.02, 1.20)
145+
146+
for age, pos in age_positions.items():
147+
mid_y = (pos["top"] + pos["bottom"]) / 2
148+
bg = age_bg_colors[age]
149+
150+
# Subtle background band across both panels
151+
ax_age.axhspan(pos["top"], pos["bottom"], color=bg, zorder=0)
152+
ax.axhspan(pos["top"], pos["bottom"], color=bg, zorder=0)
153+
154+
# Bracket lines
155+
ax_age.plot(
156+
[0.85, 0.85], [pos["top"] + 1, pos["bottom"] - 1], color="#555555", linewidth=2.5, solid_capstyle="butt"
157+
)
158+
ax_age.plot([0.80, 0.90], [pos["top"] + 1, pos["top"] + 1], color="#555555", linewidth=2)
159+
ax_age.plot([0.80, 0.90], [pos["bottom"] - 1, pos["bottom"] - 1], color="#555555", linewidth=2)
160+
161+
# Age text
162+
ax_age.text(
163+
0.35,
164+
mid_y,
165+
age.replace(" ", "\n"),
166+
fontsize=15,
167+
fontweight="bold",
168+
va="center",
169+
ha="center",
170+
fontstyle="italic",
171+
color="#333333",
172+
)
173+
174+
# Style age axis
175+
ax_age.set_ylabel("Depth (m)", fontsize=20, labelpad=10)
176+
ax_age.tick_params(axis="y", labelsize=16)
177+
ax_age.set_yticks(np.arange(0, total_depth + 1, 20))
178+
ax_age.set_xticks([])
179+
sns.despine(ax=ax_age, top=True, right=True, bottom=True)
180+
181+
ax.set_xticks([])
182+
ax.set_xlabel("")
183+
ax.set_ylabel("")
184+
ax.tick_params(axis="y", left=False, labelleft=False)
185+
sns.despine(ax=ax, left=True, bottom=True, top=True, right=True)
186+
187+
# Right panel: seaborn stripplot showing layer thicknesses by lithology
188+
# Uses distinctive seaborn categorical visualization (strip plot with jitter)
189+
lith_order = [s["label"] for s in lithology_styles.values()]
190+
strip_palette = {s["label"]: s["color"] for s in lithology_styles.values()}
191+
sns.stripplot(
192+
data=thickness_df,
193+
x="thickness",
194+
y="lithology",
195+
ax=ax_thick,
196+
palette=strip_palette,
197+
hue="lithology",
198+
order=lith_order,
199+
size=14,
200+
marker="D",
201+
edgecolor="black",
202+
linewidth=1.0,
203+
jitter=False,
204+
legend=False,
205+
)
206+
ax_thick.set_xlabel("Thickness (m)", fontsize=16)
207+
ax_thick.set_ylabel("")
208+
ax_thick.tick_params(axis="both", labelsize=13)
209+
ax_thick.set_title("Layer Thickness", fontsize=16, fontweight="medium", pad=8)
210+
ax_thick.xaxis.grid(True, alpha=0.3, linewidth=0.8)
211+
sns.despine(ax=ax_thick, top=True, right=True)
212+
213+
# Lithology legend on the thickness panel (avoids overlapping column labels)
214+
legend_handles = [
215+
mpatches.Patch(facecolor=style["color"], edgecolor="black", hatch=style["hatch"], label=style["label"])
216+
for style in lithology_styles.values()
217+
]
218+
219+
ax_thick.legend(
220+
handles=legend_handles,
221+
loc="lower right",
222+
fontsize=12,
223+
framealpha=0.95,
224+
title="Lithology",
225+
title_fontsize=14,
226+
edgecolor="#CCCCCC",
227+
fancybox=True,
228+
)
229+
230+
# Title
231+
fig.suptitle("column-stratigraphic · seaborn · pyplots.ai", fontsize=24, fontweight="medium", y=0.97)
232+
233+
plt.subplots_adjust(wspace=0.08)
234+
plt.tight_layout(rect=[0, 0, 1, 0.95])
235+
236+
# Save
237+
plt.savefig("plot.png", dpi=300, bbox_inches="tight")

0 commit comments

Comments
 (0)