Skip to content

Commit 3e329c6

Browse files
fix(plotnine): address review feedback for column-stratigraphic
Attempt 1/3 - fixes based on AI review
1 parent 109362e commit 3e329c6

1 file changed

Lines changed: 87 additions & 45 deletions

File tree

plots/column-stratigraphic/implementations/plotnine.py

Lines changed: 87 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
""" pyplots.ai
1+
"""pyplots.ai
22
column-stratigraphic: Stratigraphic Column with Lithology Patterns
33
Library: plotnine 0.15.3 | Python 3.14.3
4-
Quality: 81/100 | Created: 2026-03-15
54
"""
65

76
import numpy as np
87
import pandas as pd
98
from plotnine import (
109
aes,
10+
annotate,
1111
element_blank,
1212
element_line,
1313
element_rect,
@@ -17,6 +17,8 @@
1717
geom_segment,
1818
geom_text,
1919
ggplot,
20+
guide_legend,
21+
guides,
2022
labs,
2123
scale_fill_manual,
2224
scale_x_continuous,
@@ -26,7 +28,7 @@
2628
)
2729

2830

29-
# Data - synthetic sedimentary borehole section
31+
# Data - synthetic sedimentary borehole section (Western US)
3032
layers = pd.DataFrame(
3133
{
3234
"top": [0, 12, 28, 45, 58, 72, 95, 115, 138, 160],
@@ -76,14 +78,13 @@
7678
# Column position constants
7779
col_left = 0.0
7880
col_right = 4.0
79-
col_mid = 2.0
8081

81-
# Lithology colors (geologically conventional tones)
82+
# Lithology colors - improved contrast between siltstone and shale
8283
lith_colors = {
8384
"Sandstone": "#F5D76E",
84-
"Shale": "#8E8E8E",
85+
"Shale": "#7A7A7A",
8586
"Limestone": "#6DAEDB",
86-
"Siltstone": "#B5C7D3",
87+
"Siltstone": "#C8DCC0",
8788
"Conglomerate": "#D4785C",
8889
"Mudstone": "#7B6B5E",
8990
}
@@ -95,6 +96,10 @@
9596
rect_df["ymin"] = rect_df["top"]
9697
rect_df["ymax"] = rect_df["bottom"]
9798

99+
# Highlight the unconformity between Kootenai Fm and Morrison Fm (J/K boundary)
100+
# This adds storytelling: a major geological event
101+
unconformity_depth = 95.0
102+
98103
# Generate pattern overlay data for each lithology
99104
pattern_rows = []
100105
np.random.seed(42)
@@ -106,18 +111,18 @@
106111
thickness = bot_val - top_val
107112

108113
if lith == "Sandstone":
109-
# Stipple dots pattern
110-
n_dots = int(thickness * 3)
114+
# Stipple dots pattern - denser
115+
n_dots = int(thickness * 4)
111116
for _ in range(n_dots):
112117
px = np.random.uniform(col_left + 0.3, col_right - 0.3)
113118
py = np.random.uniform(top_val + 0.5, bot_val - 0.5)
114119
pattern_rows.append({"x": px, "y": py, "xend": px, "yend": py, "ptype": "dot", "lithology": lith})
115120

116121
elif lith == "Shale":
117122
# Horizontal dashes
118-
spacing = 3.0
119-
y_pos = top_val + 1.5
120-
while y_pos < bot_val - 1.0:
123+
spacing = 2.5
124+
y_pos = top_val + 1.0
125+
while y_pos < bot_val - 0.5:
121126
for x_start in np.arange(col_left + 0.3, col_right - 0.5, 0.8):
122127
pattern_rows.append(
123128
{"x": x_start, "y": y_pos, "xend": x_start + 0.5, "yend": y_pos, "ptype": "dash", "lithology": lith}
@@ -130,7 +135,6 @@
130135
y_pos = top_val + 2.0
131136
row_idx = 0
132137
while y_pos < bot_val - 1.0:
133-
# Horizontal line
134138
pattern_rows.append(
135139
{
136140
"x": col_left + 0.2,
@@ -141,7 +145,6 @@
141145
"lithology": lith,
142146
}
143147
)
144-
# Vertical lines (offset every other row)
145148
offset = 1.0 if row_idx % 2 == 0 else 0.0
146149
for vx in np.arange(col_left + 0.5 + offset, col_right - 0.3, 2.0):
147150
y_top = max(y_pos - spacing, top_val + 0.2)
@@ -152,35 +155,35 @@
152155
row_idx += 1
153156

154157
elif lith == "Siltstone":
155-
# Short random dashes at various angles
156-
n_dashes = int(thickness * 2)
158+
# Short random dashes - much denser for visibility
159+
n_dashes = int(thickness * 6)
157160
for _ in range(n_dashes):
158-
px = np.random.uniform(col_left + 0.4, col_right - 0.4)
159-
py = np.random.uniform(top_val + 1.0, bot_val - 1.0)
160-
dx = np.random.uniform(-0.2, 0.2)
161+
px = np.random.uniform(col_left + 0.3, col_right - 0.3)
162+
py = np.random.uniform(top_val + 0.5, bot_val - 0.5)
163+
dx = np.random.uniform(-0.15, 0.15)
161164
pattern_rows.append(
162-
{"x": px, "y": py, "xend": px + dx, "yend": py + 0.3, "ptype": "short_dash", "lithology": lith}
165+
{"x": px, "y": py, "xend": px + dx, "yend": py + 0.2, "ptype": "short_dash", "lithology": lith}
163166
)
164167

165168
elif lith == "Conglomerate":
166169
# Scattered circles (represented as large dots)
167-
n_circles = int(thickness * 1.5)
170+
n_circles = int(thickness * 2)
168171
for _ in range(n_circles):
169172
px = np.random.uniform(col_left + 0.5, col_right - 0.5)
170173
py = np.random.uniform(top_val + 1.0, bot_val - 1.0)
171174
pattern_rows.append({"x": px, "y": py, "xend": px, "yend": py, "ptype": "circle", "lithology": lith})
172175

173176
elif lith == "Mudstone":
174177
# Dense horizontal dashes (finer than shale)
175-
spacing = 2.5
176-
y_pos = top_val + 1.0
177-
while y_pos < bot_val - 0.5:
178-
for x_start in np.arange(col_left + 0.2, col_right - 0.3, 0.6):
178+
spacing = 2.0
179+
y_pos = top_val + 0.8
180+
while y_pos < bot_val - 0.3:
181+
for x_start in np.arange(col_left + 0.2, col_right - 0.3, 0.5):
179182
pattern_rows.append(
180183
{
181184
"x": x_start,
182185
"y": y_pos,
183-
"xend": x_start + 0.3,
186+
"xend": x_start + 0.25,
184187
"yend": y_pos,
185188
"ptype": "fine_dash",
186189
"lithology": lith,
@@ -205,12 +208,23 @@
205208
age_groups["mid"] = (age_groups["top"] + age_groups["bottom"]) / 2
206209
age_labels = pd.DataFrame({"x": col_left - 0.3, "y": age_groups["mid"].values, "label": age_groups.index.tolist()})
207210

211+
# Age bracket lines connecting age labels to the column
212+
age_brackets = []
213+
for _, grp in age_groups.iterrows():
214+
age_brackets.append({"x": col_left - 0.15, "y": grp["top"], "xend": col_left - 0.15, "yend": grp["bottom"]})
215+
age_bracket_df = pd.DataFrame(age_brackets)
216+
208217
# Layer boundary lines
209218
boundaries = sorted(set(layers["top"].tolist() + layers["bottom"].tolist()))
210219
boundary_df = pd.DataFrame(
211220
{"x": [col_left] * len(boundaries), "xend": [col_right] * len(boundaries), "y": boundaries, "yend": boundaries}
212221
)
213222

223+
# Unconformity wavy line data (zigzag at Jurassic/Cretaceous boundary)
224+
wavy_x = np.linspace(col_left, col_right, 40)
225+
wavy_y = unconformity_depth + np.sin(wavy_x * 8) * 0.8
226+
wavy_df = pd.DataFrame({"x": wavy_x[:-1], "y": wavy_y[:-1], "xend": wavy_x[1:], "yend": wavy_y[1:]})
227+
214228
# Plot
215229
plot = (
216230
ggplot()
@@ -220,66 +234,94 @@
220234
mapping=aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax", fill="lithology"),
221235
color="#2C2C2C",
222236
size=0.8,
223-
alpha=0.55,
237+
alpha=0.6,
224238
)
225239
# Pattern overlays - line segments
226240
+ geom_segment(
227-
data=lines_df, mapping=aes(x="x", y="y", xend="xend", yend="yend"), color="#2C2C2C", size=0.6, alpha=0.7
241+
data=lines_df, mapping=aes(x="x", y="y", xend="xend", yend="yend"), color="#2C2C2C", size=0.6, alpha=0.75
228242
)
229243
# Pattern overlays - dots (sandstone)
230-
+ geom_point(data=dots_df, mapping=aes(x="x", y="y"), color="#2C2C2C", size=0.8, alpha=0.6)
244+
+ geom_point(data=dots_df, mapping=aes(x="x", y="y"), color="#2C2C2C", size=1.0, alpha=0.65)
231245
# Pattern overlays - circles (conglomerate)
232246
+ geom_point(
233247
data=circles_df,
234248
mapping=aes(x="x", y="y"),
235249
color="#2C2C2C",
236-
size=3,
237-
alpha=0.5,
250+
size=3.5,
251+
alpha=0.55,
238252
shape="o",
239253
fill="none",
240-
stroke=0.8,
254+
stroke=0.9,
241255
)
242256
# Layer boundary lines
243257
+ geom_segment(data=boundary_df, mapping=aes(x="x", y="y", xend="xend", yend="yend"), color="#2C2C2C", size=0.8)
244-
# Formation labels (right side)
258+
# Unconformity wavy line (focal point - major geological event)
259+
+ geom_segment(
260+
data=wavy_df, mapping=aes(x="x", y="y", xend="xend", yend="yend"), color="#B22222", size=1.8, alpha=0.9
261+
)
262+
# Unconformity annotation
263+
+ annotate(
264+
"text",
265+
x=col_right + 0.3,
266+
y=unconformity_depth,
267+
label="~ Unconformity ~",
268+
ha="left",
269+
size=12,
270+
color="#B22222",
271+
fontstyle="italic",
272+
fontweight="bold",
273+
)
274+
# Age bracket lines
275+
+ geom_segment(data=age_bracket_df, mapping=aes(x="x", y="y", xend="xend", yend="yend"), color="#444444", size=0.7)
276+
# Formation labels (right side) - larger font
245277
+ geom_text(
246278
data=form_labels,
247279
mapping=aes(x="x", y="y", label="label"),
248280
ha="left",
249-
size=11,
281+
size=14,
250282
fontstyle="italic",
251283
color="#1A1A1A",
252284
)
253-
# Age labels (left side)
254-
+ geom_text(data=age_labels, mapping=aes(x="x", y="y", label="label"), ha="right", size=10, color="#1A1A1A")
285+
# Age labels (left side) - larger font
286+
+ geom_text(
287+
data=age_labels,
288+
mapping=aes(x="x", y="y", label="label"),
289+
ha="right",
290+
size=13,
291+
fontweight="bold",
292+
color="#333333",
293+
)
255294
# Scales
256295
+ scale_fill_manual(values=lith_colors)
257296
+ scale_y_reverse(name="Depth (m)", breaks=list(range(0, 200, 20)))
258-
+ scale_x_continuous(limits=(-3.5, 8.5), breaks=[])
297+
+ scale_x_continuous(limits=(-4.0, 9.5), breaks=[])
259298
# Labels
260299
+ labs(title="column-stratigraphic · plotnine · pyplots.ai", fill="Lithology", x="")
300+
# Legend styling
301+
+ guides(fill=guide_legend(nrow=1))
261302
# Theme
262303
+ theme_minimal()
263304
+ theme(
264-
figure_size=(10, 16),
265-
plot_title=element_text(size=22, face="bold", ha="center"),
266-
axis_title_y=element_text(size=18),
305+
figure_size=(11, 16),
306+
plot_title=element_text(size=24, face="bold", ha="center"),
307+
axis_title_y=element_text(size=20),
267308
axis_title_x=element_blank(),
268-
axis_text_y=element_text(size=14),
309+
axis_text_y=element_text(size=16),
269310
axis_text_x=element_blank(),
270311
axis_ticks_major_x=element_blank(),
271312
legend_title=element_text(size=16, face="bold"),
272-
legend_text=element_text(size=13),
313+
legend_text=element_text(size=14),
273314
legend_position="bottom",
274315
legend_direction="horizontal",
316+
legend_key_size=20,
275317
panel_grid_major_x=element_blank(),
276318
panel_grid_minor_x=element_blank(),
277-
panel_grid_major_y=element_line(color="#E0E0E0", size=0.3, alpha=0.4),
319+
panel_grid_major_y=element_line(color="#E8E8E8", size=0.25, alpha=0.3),
278320
panel_grid_minor_y=element_blank(),
279-
panel_background=element_rect(fill="white", color="none"),
321+
panel_background=element_rect(fill="#FAFAFA", color="none"),
280322
plot_background=element_rect(fill="white", color="none"),
281323
)
282324
)
283325

284326
# Save
285-
plot.save("plot.png", dpi=300, width=10, height=16)
327+
plot.save("plot.png", dpi=300, width=11, height=16)

0 commit comments

Comments
 (0)