Skip to content

Commit 6004102

Browse files
feat(matplotlib): implement column-stratigraphic (#4905)
## Implementation: `column-stratigraphic` - matplotlib Implements the **matplotlib** version of `column-stratigraphic`. **File:** `plots/column-stratigraphic/implementations/matplotlib.py` **Parent Issue:** #4573 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23121198649)* --------- 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 adca784 commit 6004102

2 files changed

Lines changed: 423 additions & 0 deletions

File tree

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
""" pyplots.ai
2+
column-stratigraphic: Stratigraphic Column with Lithology Patterns
3+
Library: matplotlib 3.10.8 | Python 3.14.3
4+
Quality: 91/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+
11+
12+
# Data - synthetic sedimentary borehole section (depth increasing downward, younger at top)
13+
layers = [
14+
{"top": 0, "bottom": 12, "lithology": "conglomerate", "formation": "Ogallala Fm", "age": "Miocene"},
15+
{"top": 12, "bottom": 30, "lithology": "sandstone", "formation": "Arikaree Fm", "age": "Miocene"},
16+
{"top": 30, "bottom": 50, "lithology": "siltstone", "formation": "White River Fm", "age": "Oligocene"},
17+
{"top": 50, "bottom": 72, "lithology": "shale", "formation": "Chadron Fm", "age": "Eocene"},
18+
{"top": 72, "bottom": 95, "lithology": "limestone", "formation": "Niobrara Fm", "age": "Cretaceous"},
19+
{"top": 95, "bottom": 118, "lithology": "shale", "formation": "Carlile Shale", "age": "Cretaceous"},
20+
{"top": 118, "bottom": 140, "lithology": "limestone", "formation": "Greenhorn Fm", "age": "Cretaceous"},
21+
{"top": 140, "bottom": 158, "lithology": "sandstone", "formation": "Dakota Fm", "age": "Cretaceous"},
22+
{"top": 158, "bottom": 175, "lithology": "siltstone", "formation": "Morrison Fm", "age": "Jurassic"},
23+
{"top": 175, "bottom": 200, "lithology": "sandstone", "formation": "Entrada Fm", "age": "Jurassic"},
24+
]
25+
26+
# Lithology styles: color, hatch pattern
27+
lithology_styles = {
28+
"sandstone": {"color": "#F5DEB3", "hatch": "...", "edgecolor": "#8B7355"},
29+
"shale": {"color": "#A9A9A9", "hatch": "---", "edgecolor": "#555555"},
30+
"limestone": {"color": "#87CEEB", "hatch": "++", "edgecolor": "#4682B4"},
31+
"siltstone": {"color": "#C4B69C", "hatch": "//", "edgecolor": "#8B7D6B"},
32+
"conglomerate": {"color": "#DEB887", "hatch": "ooo", "edgecolor": "#8B6914"},
33+
}
34+
35+
# Age group background colors for subtle shading
36+
age_colors = {
37+
"Miocene": "#FFF8E7",
38+
"Oligocene": "#F5F0E0",
39+
"Eocene": "#EDE8D8",
40+
"Cretaceous": "#E8EEF5",
41+
"Jurassic": "#F0E8E0",
42+
}
43+
44+
# Plot
45+
fig, ax = plt.subplots(figsize=(12, 16))
46+
47+
column_left = 1.5
48+
column_width = 5.0
49+
max_depth = 200
50+
51+
# Compute age spans
52+
age_spans = {}
53+
for layer in layers:
54+
age = layer["age"]
55+
if age not in age_spans:
56+
age_spans[age] = {"top": layer["top"], "bottom": layer["bottom"]}
57+
else:
58+
age_spans[age]["top"] = min(age_spans[age]["top"], layer["top"])
59+
age_spans[age]["bottom"] = max(age_spans[age]["bottom"], layer["bottom"])
60+
61+
# Draw subtle age-group background shading
62+
for age, span in age_spans.items():
63+
bg_rect = mpatches.FancyBboxPatch(
64+
(column_left - 0.1, span["top"]),
65+
column_width + 0.2,
66+
span["bottom"] - span["top"],
67+
boxstyle="square,pad=0",
68+
facecolor=age_colors[age],
69+
edgecolor="none",
70+
zorder=0,
71+
)
72+
ax.add_patch(bg_rect)
73+
74+
# Draw lithology layers
75+
for layer in layers:
76+
top = layer["top"]
77+
bottom = layer["bottom"]
78+
thickness = bottom - top
79+
style = lithology_styles[layer["lithology"]]
80+
81+
rect = mpatches.FancyBboxPatch(
82+
(column_left, top),
83+
column_width,
84+
thickness,
85+
boxstyle="square,pad=0",
86+
facecolor=style["color"],
87+
edgecolor=style["edgecolor"],
88+
linewidth=1.5,
89+
hatch=style["hatch"],
90+
zorder=1,
91+
)
92+
ax.add_patch(rect)
93+
94+
mid_depth = (top + bottom) / 2
95+
ax.text(
96+
column_left + column_width + 0.4,
97+
mid_depth,
98+
layer["formation"],
99+
fontsize=16,
100+
va="center",
101+
ha="left",
102+
fontweight="semibold",
103+
color="#2C2C2C",
104+
)
105+
106+
# Unconformity between Eocene (Chadron Fm, bottom=72) and Cretaceous (Niobrara Fm, top=72)
107+
unconformity_depth = 72
108+
x_wave = np.linspace(column_left, column_left + column_width, 80)
109+
y_wave = unconformity_depth + 0.8 * np.sin(x_wave * 4)
110+
ax.plot(x_wave, y_wave, color="#B22222", linewidth=2.5, zorder=3)
111+
ax.text(
112+
column_left + column_width + 0.4,
113+
unconformity_depth,
114+
"unconformity",
115+
fontsize=14,
116+
va="center",
117+
ha="left",
118+
fontstyle="italic",
119+
color="#B22222",
120+
fontweight="medium",
121+
)
122+
123+
# Age labels on the left with bracket lines
124+
bracket_x = column_left - 1.2
125+
for age, span in age_spans.items():
126+
mid = (span["top"] + span["bottom"]) / 2
127+
ax.text(
128+
column_left - 1.8,
129+
mid,
130+
age,
131+
fontsize=16,
132+
va="center",
133+
ha="center",
134+
fontstyle="italic",
135+
color="#333333",
136+
fontweight="medium",
137+
clip_on=False,
138+
)
139+
ax.plot(
140+
[bracket_x, bracket_x + 0.4],
141+
[span["top"] + 0.5, span["top"] + 0.5],
142+
color="#555555",
143+
linewidth=1.2,
144+
clip_on=False,
145+
)
146+
ax.plot(
147+
[bracket_x, bracket_x + 0.4],
148+
[span["bottom"] - 0.5, span["bottom"] - 0.5],
149+
color="#555555",
150+
linewidth=1.2,
151+
clip_on=False,
152+
)
153+
ax.plot(
154+
[bracket_x + 0.2, bracket_x + 0.2],
155+
[span["top"] + 0.5, span["bottom"] - 0.5],
156+
color="#555555",
157+
linewidth=1.2,
158+
clip_on=False,
159+
)
160+
161+
# Legend
162+
legend_handles = []
163+
for lith, style in lithology_styles.items():
164+
patch = mpatches.Patch(
165+
facecolor=style["color"],
166+
edgecolor=style["edgecolor"],
167+
hatch=style["hatch"],
168+
label=lith.capitalize(),
169+
linewidth=1.0,
170+
)
171+
legend_handles.append(patch)
172+
173+
ax.legend(
174+
handles=legend_handles,
175+
loc="upper center",
176+
bbox_to_anchor=(0.55, -0.03),
177+
fontsize=16,
178+
framealpha=0.95,
179+
edgecolor="#bbbbbb",
180+
fancybox=True,
181+
shadow=True,
182+
title="Lithology",
183+
title_fontsize=17,
184+
borderpad=1.0,
185+
ncol=5,
186+
)
187+
188+
# Style
189+
ax.set_xlim(column_left - 2.8, column_left + column_width + 4.5)
190+
ax.set_ylim(max_depth, 0)
191+
ax.set_ylabel("Depth (m)", fontsize=20, labelpad=10)
192+
ax.set_title("column-stratigraphic · matplotlib · pyplots.ai", fontsize=24, fontweight="medium", pad=25)
193+
ax.tick_params(axis="y", labelsize=16, length=6)
194+
ax.set_xticks([])
195+
ax.spines["top"].set_visible(False)
196+
ax.spines["right"].set_visible(False)
197+
ax.spines["bottom"].set_visible(False)
198+
ax.yaxis.grid(True, alpha=0.12, linewidth=0.8, linestyle="--")
199+
200+
plt.tight_layout()
201+
plt.savefig("plot.png", dpi=300, bbox_inches="tight")

0 commit comments

Comments
 (0)