Skip to content

Commit 7a1b8b5

Browse files
fix(letsplot): address review feedback for column-stratigraphic
Attempt 1/3 - fixes based on AI review
1 parent 582f8b3 commit 7a1b8b5

1 file changed

Lines changed: 165 additions & 27 deletions

File tree

plots/column-stratigraphic/implementations/letsplot.py

Lines changed: 165 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
""" pyplots.ai
1+
"""pyplots.ai
22
column-stratigraphic: Stratigraphic Column with Lithology Patterns
33
Library: letsplot 4.9.0 | Python 3.14.3
44
Quality: 80/100 | Created: 2026-03-15
@@ -62,78 +62,216 @@
6262
layers["xmin"] = 0.0
6363
layers["xmax"] = 1.0
6464

65-
# Lithology color palette (earthy geological tones)
65+
# Lithology color palette (improved contrast between types)
6666
lithology_colors = {
67-
"Sandstone": "#F2CC6B",
68-
"Shale": "#8B8B8B",
69-
"Limestone": "#5B9BD5",
70-
"Siltstone": "#B5A48B",
67+
"Sandstone": "#F5D76E",
68+
"Shale": "#7A7A7A",
69+
"Limestone": "#6BAED6",
70+
"Siltstone": "#C4956A",
7171
"Conglomerate": "#D4726A",
7272
}
7373
lithology_order = ["Sandstone", "Shale", "Limestone", "Siltstone", "Conglomerate"]
7474

75-
# Build label data: formation names (right) + age labels (left)
76-
label_rows = []
75+
# Generate lithology pattern overlay data
76+
pattern_segments = [] # For line-based patterns (shale dashes, limestone bricks)
77+
pattern_points = [] # For dot-based patterns (sandstone stipple, conglomerate circles)
78+
79+
for _, row in layers.iterrows():
80+
top, bottom, lith = row["top"], row["bottom"], row["lithology"]
81+
thickness = bottom - top
82+
margin = 0.03
83+
84+
if lith == "Shale":
85+
# Horizontal dashes
86+
spacing = 4
87+
n_lines = max(1, int(thickness / spacing))
88+
for i in range(n_lines):
89+
y = top + (i + 0.5) * thickness / n_lines
90+
for x_start in [0.05, 0.25, 0.5, 0.75]:
91+
pattern_segments.append({"x": x_start, "y": y, "xend": x_start + 0.15, "yend": y})
92+
93+
elif lith == "Limestone":
94+
# Brick pattern: horizontal lines + offset vertical lines
95+
spacing = 5
96+
n_rows = max(1, int(thickness / spacing))
97+
for i in range(n_rows + 1):
98+
y = top + margin + i * (thickness - 2 * margin) / max(n_rows, 1)
99+
if top + margin <= y <= bottom - margin:
100+
pattern_segments.append({"x": 0.02, "y": y, "xend": 0.98, "yend": y})
101+
for i in range(n_rows):
102+
y_top = top + margin + i * (thickness - 2 * margin) / max(n_rows, 1)
103+
y_bot = top + margin + (i + 1) * (thickness - 2 * margin) / max(n_rows, 1)
104+
offset = 0.25 if i % 2 == 0 else 0.0
105+
for vx in [0.25 + offset, 0.5 + offset, 0.75 + offset]:
106+
if 0.02 < vx < 0.98:
107+
pattern_segments.append({"x": vx, "y": y_top, "xend": vx, "yend": y_bot})
108+
109+
elif lith == "Sandstone":
110+
# Stipple dots
111+
spacing_y = 4
112+
spacing_x = 0.12
113+
n_rows = max(1, int(thickness / spacing_y))
114+
for i in range(n_rows):
115+
y = top + (i + 0.5) * thickness / n_rows
116+
offset = 0.06 if i % 2 == 0 else 0.0
117+
x = 0.08 + offset
118+
while x < 0.95:
119+
pattern_points.append({"x": x, "y": y, "shape": "dot"})
120+
x += spacing_x
121+
122+
elif lith == "Siltstone":
123+
# Short random-angle dashes (tilted segments)
124+
spacing_y = 5
125+
n_rows = max(1, int(thickness / spacing_y))
126+
for i in range(n_rows):
127+
y = top + (i + 0.5) * thickness / n_rows
128+
offset = 0.08 if i % 2 == 0 else 0.0
129+
for x in [0.12 + offset, 0.32 + offset, 0.52 + offset, 0.72 + offset]:
130+
if x < 0.95:
131+
pattern_segments.append({"x": x, "y": y - 0.8, "xend": x + 0.06, "yend": y + 0.8})
132+
133+
elif lith == "Conglomerate":
134+
# Circles (larger dots)
135+
spacing_y = 6
136+
n_rows = max(1, int(thickness / spacing_y))
137+
for i in range(n_rows):
138+
y = top + (i + 0.5) * thickness / n_rows
139+
offset = 0.1 if i % 2 == 0 else 0.0
140+
for x in [0.15 + offset, 0.4 + offset, 0.65 + offset]:
141+
if x < 0.95:
142+
pattern_points.append({"x": x, "y": y, "shape": "circle"})
143+
144+
pattern_seg_df = pd.DataFrame(pattern_segments) if pattern_segments else None
145+
pattern_dot_df = pd.DataFrame([p for p in pattern_points if p["shape"] == "dot"]) if pattern_points else None
146+
pattern_circle_df = pd.DataFrame([p for p in pattern_points if p["shape"] == "circle"]) if pattern_points else None
147+
148+
# Formation labels (right side)
149+
form_labels = []
77150
for _, row in layers.iterrows():
78151
mid_depth = (row["top"] + row["bottom"]) / 2
79-
label_rows.append({"x": 1.12, "y": mid_depth, "label": row["formation"]})
152+
form_labels.append({"x": 1.08, "y": mid_depth, "label": row["formation"]})
153+
form_df = pd.DataFrame(form_labels)
80154

155+
# Age labels (left side) with bracket indicators
81156
age_spans = {"Triassic": (0, 35), "Jurassic": (35, 110), "Cretaceous": (110, 195), "Paleogene": (195, 260)}
157+
age_labels = []
158+
age_brackets = []
82159
for age_name, (age_top, age_bottom) in age_spans.items():
83-
label_rows.append({"x": -0.12, "y": (age_top + age_bottom) / 2, "label": age_name})
84-
85-
labels_df = pd.DataFrame(label_rows)
160+
age_labels.append({"x": -0.18, "y": (age_top + age_bottom) / 2, "label": age_name})
161+
# Bracket lines on left
162+
age_brackets.append({"x": -0.06, "y": age_top + 1, "xend": -0.06, "yend": age_bottom - 1})
163+
age_brackets.append({"x": -0.06, "y": age_top + 1, "xend": -0.03, "yend": age_top + 1})
164+
age_brackets.append({"x": -0.06, "y": age_bottom - 1, "xend": -0.03, "yend": age_bottom - 1})
165+
age_df = pd.DataFrame(age_labels)
166+
bracket_df = pd.DataFrame(age_brackets)
86167

87168
# Age boundary dashed lines
88169
age_boundary_depths = [35, 110, 195]
89170
boundaries_df = pd.DataFrame(
90171
{"x": [-0.02] * 3, "y": age_boundary_depths, "xend": [1.02] * 3, "yend": age_boundary_depths}
91172
)
92173

93-
# Plot
174+
# Unconformity wavy line at Jurassic/Cretaceous boundary (110m)
175+
wavy_x = []
176+
wavy_y = []
177+
n_waves = 20
178+
for i in range(n_waves + 1):
179+
xi = i / n_waves
180+
yi = 110 + 1.5 * (1 if (i % 2 == 0) else -1)
181+
wavy_x.append(xi)
182+
wavy_y.append(yi)
183+
wavy_df = pd.DataFrame({"x": wavy_x, "y": wavy_y})
184+
185+
# Plot assembly
94186
plot = (
95187
ggplot()
188+
# Layer rectangles with interactive tooltips
96189
+ geom_rect(
97190
aes(xmin="xmin", xmax="xmax", ymin="top", ymax="bottom", fill="lithology"),
98191
data=layers,
99-
color="black",
100-
size=0.8,
101-
alpha=0.85,
192+
color="#2C2C2C",
193+
size=1.0,
194+
alpha=0.8,
102195
tooltips=layer_tooltips()
103196
.line("@lithology")
104197
.line("Depth: @top\u2013@bottom m")
105198
.line("Thickness: @thickness m")
106199
.line("Formation: @formation")
107200
.line("Age: @age"),
108201
)
109-
+ geom_segment(
202+
)
203+
204+
# Add pattern overlays
205+
if pattern_seg_df is not None and len(pattern_seg_df) > 0:
206+
plot = plot + geom_segment(
110207
aes(x="x", y="y", xend="xend", yend="yend"),
111-
data=boundaries_df,
112-
linetype="dashed",
113-
color="#888888",
114-
size=0.7,
208+
data=pattern_seg_df,
209+
color="#3C3C3C",
210+
size=0.4,
211+
alpha=0.55,
115212
show_legend=False,
116213
)
117-
+ geom_text(aes(x="x", y="y", label="label"), data=labels_df, size=10, color="#333333")
214+
215+
if pattern_dot_df is not None and len(pattern_dot_df) > 0:
216+
plot = plot + geom_point(
217+
aes(x="x", y="y"), data=pattern_dot_df, color="#5C4A1E", size=1.2, alpha=0.5, shape=16, show_legend=False
218+
)
219+
220+
if pattern_circle_df is not None and len(pattern_circle_df) > 0:
221+
plot = plot + geom_point(
222+
aes(x="x", y="y"), data=pattern_circle_df, color="#6B3A3A", size=4, alpha=0.45, shape=1, show_legend=False
223+
)
224+
225+
# Unconformity wavy line at 110m
226+
plot = plot + geom_line(aes(x="x", y="y"), data=wavy_df, color="#C44E52", size=1.2, show_legend=False)
227+
228+
# Age boundary dashed lines (non-unconformity)
229+
non_unconformity_boundaries = pd.DataFrame(
230+
{"x": [-0.02, -0.02], "y": [35, 195], "xend": [1.02, 1.02], "yend": [35, 195]}
231+
)
232+
plot = plot + geom_segment(
233+
aes(x="x", y="y", xend="xend", yend="yend"),
234+
data=non_unconformity_boundaries,
235+
linetype="dashed",
236+
color="#666666",
237+
size=0.6,
238+
show_legend=False,
239+
)
240+
241+
# Age brackets (left side)
242+
plot = plot + geom_segment(
243+
aes(x="x", y="y", xend="xend", yend="yend"), data=bracket_df, color="#444444", size=0.6, show_legend=False
244+
)
245+
246+
# Formation labels (right side)
247+
plot = plot + geom_text(aes(x="x", y="y", label="label"), data=form_df, size=12, color="#2C2C2C", hjust=0)
248+
249+
# Age labels (left side)
250+
plot = plot + geom_text(aes(x="x", y="y", label="label"), data=age_df, size=13, color="#2C2C2C", fontface="italic")
251+
252+
# Scales and theme
253+
plot = (
254+
plot
118255
+ scale_fill_manual(values=lithology_colors, name="Lithology", limits=lithology_order)
119256
+ scale_y_reverse()
120-
+ scale_x_continuous(limits=[-0.35, 1.85])
257+
+ scale_x_continuous(limits=[-0.35, 1.65])
121258
+ labs(title="column-stratigraphic \u00b7 letsplot \u00b7 pyplots.ai", y="Depth (m)", x="")
122259
+ theme_minimal()
123260
+ theme(
124-
plot_title=element_text(size=24, face="bold"),
125-
axis_title_y=element_text(size=20),
261+
plot_title=element_text(size=24, face="bold", color="#1A1A1A"),
262+
axis_title_y=element_text(size=20, color="#333333"),
126263
axis_title_x=element_blank(),
127-
axis_text_y=element_text(size=16),
264+
axis_text_y=element_text(size=16, color="#444444"),
128265
axis_text_x=element_blank(),
129266
axis_ticks_x=element_blank(),
130267
legend_title=element_text(size=16, face="bold"),
131268
legend_text=element_text(size=14),
132269
legend_position="bottom",
133270
panel_grid_major_x=element_blank(),
134271
panel_grid_minor_x=element_blank(),
135-
panel_grid_major_y=element_line(size=0.3, color="#dddddd"),
272+
panel_grid_major_y=element_line(size=0.3, color="#E0E0E0"),
136273
panel_grid_minor_y=element_blank(),
274+
plot_background=element_rect(color="white", fill="white"),
137275
)
138276
+ ggsize(1600, 900)
139277
)

0 commit comments

Comments
 (0)