|
1 | | -""" pyplots.ai |
| 1 | +"""pyplots.ai |
2 | 2 | column-stratigraphic: Stratigraphic Column with Lithology Patterns |
3 | 3 | Library: letsplot 4.9.0 | Python 3.14.3 |
4 | 4 | Quality: 80/100 | Created: 2026-03-15 |
|
62 | 62 | layers["xmin"] = 0.0 |
63 | 63 | layers["xmax"] = 1.0 |
64 | 64 |
|
65 | | -# Lithology color palette (earthy geological tones) |
| 65 | +# Lithology color palette (improved contrast between types) |
66 | 66 | 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", |
71 | 71 | "Conglomerate": "#D4726A", |
72 | 72 | } |
73 | 73 | lithology_order = ["Sandstone", "Shale", "Limestone", "Siltstone", "Conglomerate"] |
74 | 74 |
|
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 = [] |
77 | 150 | for _, row in layers.iterrows(): |
78 | 151 | 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) |
80 | 154 |
|
| 155 | +# Age labels (left side) with bracket indicators |
81 | 156 | age_spans = {"Triassic": (0, 35), "Jurassic": (35, 110), "Cretaceous": (110, 195), "Paleogene": (195, 260)} |
| 157 | +age_labels = [] |
| 158 | +age_brackets = [] |
82 | 159 | 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) |
86 | 167 |
|
87 | 168 | # Age boundary dashed lines |
88 | 169 | age_boundary_depths = [35, 110, 195] |
89 | 170 | boundaries_df = pd.DataFrame( |
90 | 171 | {"x": [-0.02] * 3, "y": age_boundary_depths, "xend": [1.02] * 3, "yend": age_boundary_depths} |
91 | 172 | ) |
92 | 173 |
|
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 |
94 | 186 | plot = ( |
95 | 187 | ggplot() |
| 188 | + # Layer rectangles with interactive tooltips |
96 | 189 | + geom_rect( |
97 | 190 | aes(xmin="xmin", xmax="xmax", ymin="top", ymax="bottom", fill="lithology"), |
98 | 191 | data=layers, |
99 | | - color="black", |
100 | | - size=0.8, |
101 | | - alpha=0.85, |
| 192 | + color="#2C2C2C", |
| 193 | + size=1.0, |
| 194 | + alpha=0.8, |
102 | 195 | tooltips=layer_tooltips() |
103 | 196 | .line("@lithology") |
104 | 197 | .line("Depth: @top\u2013@bottom m") |
105 | 198 | .line("Thickness: @thickness m") |
106 | 199 | .line("Formation: @formation") |
107 | 200 | .line("Age: @age"), |
108 | 201 | ) |
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( |
110 | 207 | 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, |
115 | 212 | show_legend=False, |
116 | 213 | ) |
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 |
118 | 255 | + scale_fill_manual(values=lithology_colors, name="Lithology", limits=lithology_order) |
119 | 256 | + scale_y_reverse() |
120 | | - + scale_x_continuous(limits=[-0.35, 1.85]) |
| 257 | + + scale_x_continuous(limits=[-0.35, 1.65]) |
121 | 258 | + labs(title="column-stratigraphic \u00b7 letsplot \u00b7 pyplots.ai", y="Depth (m)", x="") |
122 | 259 | + theme_minimal() |
123 | 260 | + 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"), |
126 | 263 | axis_title_x=element_blank(), |
127 | | - axis_text_y=element_text(size=16), |
| 264 | + axis_text_y=element_text(size=16, color="#444444"), |
128 | 265 | axis_text_x=element_blank(), |
129 | 266 | axis_ticks_x=element_blank(), |
130 | 267 | legend_title=element_text(size=16, face="bold"), |
131 | 268 | legend_text=element_text(size=14), |
132 | 269 | legend_position="bottom", |
133 | 270 | panel_grid_major_x=element_blank(), |
134 | 271 | 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"), |
136 | 273 | panel_grid_minor_y=element_blank(), |
| 274 | + plot_background=element_rect(color="white", fill="white"), |
137 | 275 | ) |
138 | 276 | + ggsize(1600, 900) |
139 | 277 | ) |
|
0 commit comments