|
1 | | -""" pyplots.ai |
| 1 | +"""pyplots.ai |
2 | 2 | column-stratigraphic: Stratigraphic Column with Lithology Patterns |
3 | 3 | Library: plotnine 0.15.3 | Python 3.14.3 |
4 | 4 | Quality: 86/100 | Created: 2026-03-15 |
|
85 | 85 | col_left = 0.0 |
86 | 86 | col_right = 4.0 |
87 | 87 |
|
88 | | -# Lithology colors - improved contrast (Shale lighter gray, Mudstone warm brown) |
| 88 | +# Lithology colors - enhanced contrast, colorblind-friendly palette |
89 | 89 | lith_colors = { |
90 | | - "Sandstone": "#F5D76E", |
91 | | - "Shale": "#A8A8A8", |
92 | | - "Limestone": "#6DAEDB", |
93 | | - "Siltstone": "#C8DCC0", |
| 90 | + "Sandstone": "#EEC946", |
| 91 | + "Shale": "#B0B0B0", |
| 92 | + "Limestone": "#5A9BD5", |
| 93 | + "Siltstone": "#7EBF8E", |
94 | 94 | "Conglomerate": "#D4785C", |
95 | 95 | "Mudstone": "#8B6914", |
96 | 96 | } |
97 | 97 |
|
98 | 98 | # Unconformity depth (J/K boundary between Kootenai Fm and Morrison Fm) |
99 | 99 | unconformity_depth = 95.0 |
100 | 100 |
|
101 | | -# Generate pattern overlay data for each lithology |
102 | | -pattern_rows = [] |
| 101 | +# Generate pattern overlay data with helper functions to reduce verbosity |
103 | 102 | np.random.seed(42) |
| 103 | +pattern_rows = [] |
| 104 | + |
| 105 | + |
| 106 | +def _add_random_points(rows, top_val, bot_val, lith, n, ptype, x_pad=0.3, y_pad=0.5): |
| 107 | + xs = np.random.uniform(col_left + x_pad, col_right - x_pad, n) |
| 108 | + ys = np.random.uniform(top_val + y_pad, bot_val - y_pad, n) |
| 109 | + for px, py in zip(xs, ys, strict=True): |
| 110 | + rows.append({"x": px, "y": py, "xend": px, "yend": py, "ptype": ptype, "lithology": lith}) |
| 111 | + |
| 112 | + |
| 113 | +def _add_horiz_dashes(rows, top_val, bot_val, lith, spacing, ptype, dash_len=0.5, x_step=0.8): |
| 114 | + y_pos = top_val + spacing * 0.4 |
| 115 | + while y_pos < bot_val - 0.3: |
| 116 | + for x_start in np.arange(col_left + 0.3, col_right - 0.3, x_step): |
| 117 | + rows.append( |
| 118 | + {"x": x_start, "y": y_pos, "xend": x_start + dash_len, "yend": y_pos, "ptype": ptype, "lithology": lith} |
| 119 | + ) |
| 120 | + y_pos += spacing |
| 121 | + |
104 | 122 |
|
105 | 123 | for _, row in layers.iterrows(): |
106 | | - top_val = row["top"] |
107 | | - bot_val = row["bottom"] |
108 | | - lith = row["lithology"] |
| 124 | + top_val, bot_val, lith = row["top"], row["bottom"], row["lithology"] |
109 | 125 | thickness = bot_val - top_val |
110 | 126 |
|
111 | 127 | if lith == "Sandstone": |
112 | | - n_dots = int(thickness * 4) |
113 | | - for _ in range(n_dots): |
114 | | - px = np.random.uniform(col_left + 0.3, col_right - 0.3) |
115 | | - py = np.random.uniform(top_val + 0.5, bot_val - 0.5) |
116 | | - pattern_rows.append({"x": px, "y": py, "xend": px, "yend": py, "ptype": "dot", "lithology": lith}) |
| 128 | + _add_random_points(pattern_rows, top_val, bot_val, lith, int(thickness * 5), "dot") |
117 | 129 |
|
118 | 130 | elif lith == "Shale": |
119 | | - spacing = 2.5 |
120 | | - y_pos = top_val + 1.0 |
121 | | - while y_pos < bot_val - 0.5: |
122 | | - for x_start in np.arange(col_left + 0.3, col_right - 0.5, 0.8): |
123 | | - pattern_rows.append( |
124 | | - {"x": x_start, "y": y_pos, "xend": x_start + 0.5, "yend": y_pos, "ptype": "dash", "lithology": lith} |
125 | | - ) |
126 | | - y_pos += spacing |
| 131 | + _add_horiz_dashes(pattern_rows, top_val, bot_val, lith, spacing=2.5, ptype="dash", dash_len=0.5, x_step=0.8) |
127 | 132 |
|
128 | 133 | elif lith == "Limestone": |
129 | 134 | spacing = 4.0 |
|
150 | 155 | row_idx += 1 |
151 | 156 |
|
152 | 157 | elif lith == "Siltstone": |
153 | | - n_dashes = int(thickness * 6) |
154 | | - for _ in range(n_dashes): |
155 | | - px = np.random.uniform(col_left + 0.3, col_right - 0.3) |
156 | | - py = np.random.uniform(top_val + 0.5, bot_val - 0.5) |
157 | | - dx = np.random.uniform(-0.15, 0.15) |
| 158 | + n_dashes = int(thickness * 8) |
| 159 | + xs = np.random.uniform(col_left + 0.3, col_right - 0.3, n_dashes) |
| 160 | + ys = np.random.uniform(top_val + 0.5, bot_val - 0.5, n_dashes) |
| 161 | + dxs = np.random.uniform(-0.2, 0.2, n_dashes) |
| 162 | + for px, py, dx in zip(xs, ys, dxs, strict=True): |
158 | 163 | pattern_rows.append( |
159 | | - {"x": px, "y": py, "xend": px + dx, "yend": py + 0.2, "ptype": "short_dash", "lithology": lith} |
| 164 | + {"x": px, "y": py, "xend": px + dx, "yend": py + 0.3, "ptype": "short_dash", "lithology": lith} |
160 | 165 | ) |
161 | 166 |
|
162 | 167 | elif lith == "Conglomerate": |
163 | | - n_circles = int(thickness * 2) |
164 | | - for _ in range(n_circles): |
165 | | - px = np.random.uniform(col_left + 0.5, col_right - 0.5) |
166 | | - py = np.random.uniform(top_val + 1.0, bot_val - 1.0) |
167 | | - pattern_rows.append({"x": px, "y": py, "xend": px, "yend": py, "ptype": "circle", "lithology": lith}) |
| 168 | + _add_random_points(pattern_rows, top_val, bot_val, lith, int(thickness * 2.5), "circle", x_pad=0.5, y_pad=1.0) |
168 | 169 |
|
169 | 170 | elif lith == "Mudstone": |
170 | | - spacing = 2.0 |
171 | | - y_pos = top_val + 0.8 |
172 | | - while y_pos < bot_val - 0.3: |
173 | | - for x_start in np.arange(col_left + 0.2, col_right - 0.3, 0.5): |
174 | | - pattern_rows.append( |
175 | | - { |
176 | | - "x": x_start, |
177 | | - "y": y_pos, |
178 | | - "xend": x_start + 0.25, |
179 | | - "yend": y_pos, |
180 | | - "ptype": "fine_dash", |
181 | | - "lithology": lith, |
182 | | - } |
183 | | - ) |
184 | | - y_pos += spacing |
| 171 | + _add_horiz_dashes( |
| 172 | + pattern_rows, top_val, bot_val, lith, spacing=1.8, ptype="fine_dash", dash_len=0.25, x_step=0.5 |
| 173 | + ) |
185 | 174 |
|
186 | 175 | pattern_df = pd.DataFrame(pattern_rows) |
187 | 176 |
|
|
210 | 199 | ) |
211 | 200 |
|
212 | 201 | # Unconformity wavy line data |
213 | | -wavy_x = np.linspace(col_left, col_right, 40) |
| 202 | +wavy_x = np.linspace(col_left - 0.15, col_right + 0.15, 45) |
214 | 203 | wavy_y = unconformity_depth + np.sin(wavy_x * 8) * 0.8 |
215 | 204 | wavy_df = pd.DataFrame({"x": wavy_x[:-1], "y": wavy_y[:-1], "xend": wavy_x[1:], "yend": wavy_y[1:]}) |
216 | 205 |
|
|
221 | 210 | + geom_tile( |
222 | 211 | data=layers, |
223 | 212 | mapping=aes(x="x_center", y="mid", width=col_right - col_left, height="thickness", fill="lithology"), |
224 | | - color="#2C2C2C", |
225 | | - size=0.8, |
226 | | - alpha=0.6, |
| 213 | + color="#3A3A3A", |
| 214 | + size=0.9, |
| 215 | + alpha=0.7, |
227 | 216 | ) |
228 | | - # Pattern overlays - line segments |
| 217 | + # Pattern overlays - line segments (thicker for visibility) |
229 | 218 | + geom_segment( |
230 | | - data=lines_df, mapping=aes(x="x", y="y", xend="xend", yend="yend"), color="#2C2C2C", size=0.6, alpha=0.75 |
| 219 | + data=lines_df, mapping=aes(x="x", y="y", xend="xend", yend="yend"), color="#2C2C2C", size=0.65, alpha=0.8 |
231 | 220 | ) |
232 | 221 | # Pattern overlays - stipple dots (sandstone) |
233 | | - + geom_point(data=dots_df, mapping=aes(x="x", y="y"), color="#2C2C2C", size=1.0, alpha=0.65) |
| 222 | + + geom_point(data=dots_df, mapping=aes(x="x", y="y"), color="#2C2C2C", size=1.2, alpha=0.7) |
234 | 223 | # Pattern overlays - open circles (conglomerate) |
235 | 224 | + geom_point( |
236 | 225 | data=circles_df, |
237 | 226 | mapping=aes(x="x", y="y"), |
238 | 227 | color="#2C2C2C", |
239 | 228 | size=3.5, |
240 | | - alpha=0.55, |
| 229 | + alpha=0.6, |
241 | 230 | shape="o", |
242 | 231 | fill="none", |
243 | 232 | stroke=0.9, |
244 | 233 | ) |
245 | 234 | # Layer boundary lines |
246 | | - + geom_segment(data=boundary_df, mapping=aes(x="x", y="y", xend="xend", yend="yend"), color="#2C2C2C", size=0.8) |
247 | | - # Unconformity wavy line (red - focal point for data storytelling) |
| 235 | + + geom_segment(data=boundary_df, mapping=aes(x="x", y="y", xend="xend", yend="yend"), color="#3A3A3A", size=0.9) |
| 236 | + # Unconformity wavy line (firebrick red - focal point for data storytelling) |
248 | 237 | + geom_segment( |
249 | | - data=wavy_df, mapping=aes(x="x", y="y", xend="xend", yend="yend"), color="#B22222", size=1.8, alpha=0.9 |
| 238 | + data=wavy_df, mapping=aes(x="x", y="y", xend="xend", yend="yend"), color="#B22222", size=2.2, alpha=0.95 |
250 | 239 | ) |
251 | 240 | # Unconformity annotation |
252 | 241 | + annotate( |
|
255 | 244 | y=unconformity_depth, |
256 | 245 | label="~ Unconformity ~", |
257 | 246 | ha="left", |
258 | | - size=14, |
| 247 | + size=15, |
259 | 248 | color="#B22222", |
260 | 249 | fontstyle="italic", |
261 | 250 | fontweight="bold", |
262 | 251 | ) |
263 | 252 | # Age bracket lines using geom_linerange (idiomatic plotnine for vertical ranges) |
264 | | - + geom_linerange(data=age_groups, mapping=aes(x="bracket_x", ymin="ymin", ymax="ymax"), color="#444444", size=0.8) |
| 253 | + + geom_linerange(data=age_groups, mapping=aes(x="bracket_x", ymin="ymin", ymax="ymax"), color="#444444", size=0.9) |
265 | 254 | # Formation labels using geom_label (plotnine-native styled text with background) |
266 | 255 | + geom_label( |
267 | 256 | data=form_groups, |
268 | 257 | mapping=aes(x="x", y="mid", label="formation"), |
269 | 258 | ha="left", |
270 | | - size=16, |
| 259 | + size=17, |
271 | 260 | fontstyle="italic", |
272 | 261 | color="#1A1A1A", |
273 | | - fill="#FFFFFF", |
274 | | - alpha=0.7, |
| 262 | + fill="#FFFFFFCC", |
275 | 263 | label_padding=0.3, |
276 | | - label_size=0, |
| 264 | + label_size=0.3, |
277 | 265 | ) |
278 | | - # Age labels (left side) |
| 266 | + # Age labels (left side) - increased size for legibility |
279 | 267 | + geom_text( |
280 | 268 | data=age_groups, |
281 | 269 | mapping=aes(x="x", y="mid", label="age"), |
282 | 270 | ha="right", |
283 | | - size=15, |
| 271 | + size=17, |
284 | 272 | fontweight="bold", |
285 | | - color="#333333", |
| 273 | + color="#2A2A2A", |
286 | 274 | ) |
287 | 275 | # Scales - grammar-driven fill mapping with manual color values |
288 | 276 | + scale_fill_manual(values=lith_colors, name="Lithology") |
289 | | - + scale_x_continuous(limits=(-4.5, 9.0), breaks=[]) |
| 277 | + + scale_x_continuous(limits=(-4.0, 8.5), breaks=[]) |
290 | 278 | + scale_y_continuous(trans="reverse", name="Depth (m)", breaks=list(range(0, 200, 20))) |
291 | 279 | # Clipping for clean edges |
292 | 280 | + coord_cartesian(ylim=(185, -5)) |
|
301 | 289 | plot_title=element_text(size=26, face="bold", ha="center"), |
302 | 290 | axis_title_y=element_text(size=22), |
303 | 291 | axis_title_x=element_blank(), |
304 | | - axis_text_y=element_text(size=18), |
| 292 | + axis_text_y=element_text(size=18, color="#333333"), |
305 | 293 | axis_text_x=element_blank(), |
306 | 294 | axis_ticks_major_x=element_blank(), |
307 | 295 | legend_title=element_text(size=18, face="bold"), |
308 | 296 | legend_text=element_text(size=16), |
309 | 297 | legend_position="bottom", |
310 | 298 | legend_direction="horizontal", |
311 | 299 | legend_key_size=22, |
312 | | - legend_background=element_rect(fill="#FAFAFA", color="#E0E0E0", size=0.3), |
| 300 | + legend_background=element_rect(fill="#F5F5F5", color="#CCCCCC", size=0.4), |
313 | 301 | legend_margin=10, |
314 | 302 | panel_grid_major_x=element_blank(), |
315 | 303 | panel_grid_minor_x=element_blank(), |
316 | | - panel_grid_major_y=element_line(color="#E0E0E0", size=0.3, alpha=0.4), |
| 304 | + panel_grid_major_y=element_line(color="#D8D8D8", size=0.3, alpha=0.35), |
317 | 305 | panel_grid_minor_y=element_blank(), |
318 | 306 | panel_background=element_rect(fill="#FAFAFA", color="none"), |
319 | | - plot_background=element_rect(fill="white", color="none"), |
| 307 | + plot_background=element_rect(fill="#FFFFFF", color="none"), |
320 | 308 | ) |
321 | 309 | ) |
322 | 310 |
|
|
0 commit comments