|
| 1 | +""" pyplots.ai |
| 2 | +column-stratigraphic: Stratigraphic Column with Lithology Patterns |
| 3 | +Library: seaborn 0.13.2 | Python 3.14.3 |
| 4 | +Quality: 89/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 | +import pandas as pd |
| 11 | +import seaborn as sns |
| 12 | + |
| 13 | + |
| 14 | +# Set seaborn theme for polished styling |
| 15 | +sns.set_theme( |
| 16 | + style="ticks", |
| 17 | + context="talk", |
| 18 | + font_scale=1.0, |
| 19 | + rc={"axes.linewidth": 1.2, "patch.linewidth": 1.5, "hatch.linewidth": 1.0}, |
| 20 | +) |
| 21 | + |
| 22 | +# Data - synthetic sedimentary section (Western Interior Seaway) |
| 23 | +layers = [ |
| 24 | + {"top": 0, "bottom": 15, "lithology": "sandstone", "formation": "Dakota Fm", "age": "Late Cretaceous"}, |
| 25 | + {"top": 15, "bottom": 30, "lithology": "shale", "formation": "Graneros Sh", "age": "Late Cretaceous"}, |
| 26 | + {"top": 30, "bottom": 52, "lithology": "limestone", "formation": "Greenhorn Ls", "age": "Late Cretaceous"}, |
| 27 | + {"top": 52, "bottom": 65, "lithology": "shale", "formation": "Carlile Sh", "age": "Late Cretaceous"}, |
| 28 | + {"top": 65, "bottom": 78, "lithology": "siltstone", "formation": "Niobrara Fm", "age": "Late Cretaceous"}, |
| 29 | + {"top": 78, "bottom": 100, "lithology": "limestone", "formation": "Fort Hays Ls", "age": "Late Cretaceous"}, |
| 30 | + {"top": 100, "bottom": 118, "lithology": "conglomerate", "formation": "Morrison Fm", "age": "Late Jurassic"}, |
| 31 | + {"top": 118, "bottom": 140, "lithology": "sandstone", "formation": "Entrada Ss", "age": "Middle Jurassic"}, |
| 32 | + {"top": 140, "bottom": 162, "lithology": "shale", "formation": "Chinle Fm", "age": "Late Triassic"}, |
| 33 | + {"top": 162, "bottom": 180, "lithology": "dolomite", "formation": "Kaibab Fm", "age": "Early Permian"}, |
| 34 | +] |
| 35 | + |
| 36 | +df = pd.DataFrame(layers) |
| 37 | +df["thickness"] = df["bottom"] - df["top"] |
| 38 | +df["mid_depth"] = (df["top"] + df["bottom"]) / 2 |
| 39 | +df["col_width"] = 1.0 |
| 40 | + |
| 41 | +# Use seaborn color palette for distinct lithology colors (colorblind-safe) |
| 42 | +lith_types = ["sandstone", "shale", "limestone", "siltstone", "conglomerate", "dolomite"] |
| 43 | +palette = sns.color_palette("colorblind", n_colors=len(lith_types)) |
| 44 | +lith_color_map = dict(zip(lith_types, [sns.desaturate(c, 0.7) for c in palette], strict=True)) |
| 45 | + |
| 46 | +lithology_styles = { |
| 47 | + "sandstone": {"color": lith_color_map["sandstone"], "hatch": "...", "label": "Sandstone"}, |
| 48 | + "shale": {"color": lith_color_map["shale"], "hatch": "---", "label": "Shale"}, |
| 49 | + "limestone": {"color": lith_color_map["limestone"], "hatch": "+++", "label": "Limestone"}, |
| 50 | + "siltstone": {"color": lith_color_map["siltstone"], "hatch": "//", "label": "Siltstone"}, |
| 51 | + "conglomerate": {"color": lith_color_map["conglomerate"], "hatch": "ooo", "label": "Conglomerate"}, |
| 52 | + "dolomite": {"color": lith_color_map["dolomite"], "hatch": "xxx", "label": "Dolomite"}, |
| 53 | +} |
| 54 | + |
| 55 | +# Age period background colors via seaborn palette |
| 56 | +age_bg_palette = sns.color_palette("pastel", n_colors=5) |
| 57 | +age_list = ["Late Cretaceous", "Late Jurassic", "Middle Jurassic", "Late Triassic", "Early Permian"] |
| 58 | +age_bg_colors = {age: (*age_bg_palette[i], 0.10) for i, age in enumerate(age_list)} |
| 59 | + |
| 60 | +total_depth = 180 |
| 61 | + |
| 62 | +# Build thickness data for side panel (seaborn distinctive feature) |
| 63 | +thickness_df = df[["lithology", "thickness"]].copy() |
| 64 | +thickness_df["lithology"] = thickness_df["lithology"].str.title() |
| 65 | + |
| 66 | +# Plot - three-panel layout: age brackets | stratigraphic column | thickness strip |
| 67 | +fig, (ax_age, ax, ax_thick) = plt.subplots(1, 3, figsize=(16, 9), width_ratios=[0.16, 0.54, 0.30]) |
| 68 | + |
| 69 | +# Use sns.barplot for the lithology layers (seaborn plotting function) |
| 70 | +sns.barplot( |
| 71 | + data=df, |
| 72 | + y="formation", |
| 73 | + x="col_width", |
| 74 | + color="#306998", |
| 75 | + ax=ax, |
| 76 | + edgecolor="black", |
| 77 | + linewidth=1.5, |
| 78 | + order=df["formation"].tolist(), |
| 79 | + width=0.98, |
| 80 | +) |
| 81 | + |
| 82 | +# Reposition bars from categorical to proportional depth scale and add hatching |
| 83 | +col_width = 0.55 |
| 84 | +for i, (_, row) in enumerate(df.iterrows()): |
| 85 | + bar = ax.patches[i] |
| 86 | + style = lithology_styles[row["lithology"]] |
| 87 | + bar.set_facecolor(style["color"]) |
| 88 | + bar.set_hatch(style["hatch"]) |
| 89 | + bar.set_y(row["top"]) |
| 90 | + bar.set_height(row["thickness"]) |
| 91 | + bar.set_x(0) |
| 92 | + bar.set_width(col_width) |
| 93 | + |
| 94 | +# Switch to continuous depth axis |
| 95 | +ax.set_ylim(total_depth, 0) |
| 96 | +ax.set_yticks([]) |
| 97 | + |
| 98 | +# Formation labels to the right of each layer |
| 99 | +for _, row in df.iterrows(): |
| 100 | + ax.text( |
| 101 | + col_width + 0.05, |
| 102 | + row["mid_depth"], |
| 103 | + row["formation"], |
| 104 | + fontsize=15, |
| 105 | + fontweight="medium", |
| 106 | + va="center", |
| 107 | + ha="left", |
| 108 | + color="#2C2C2C", |
| 109 | + ) |
| 110 | + |
| 111 | +# Mark major unconformities with wavy lines for data storytelling |
| 112 | +unconformities = [(100, "Cretaceous–Jurassic"), (140, "Jurassic–Triassic"), (162, "Triassic–Permian")] |
| 113 | + |
| 114 | +for depth, label in unconformities: |
| 115 | + x_wave = np.linspace(0, col_width, 80) |
| 116 | + y_wave = depth + 0.8 * np.sin(x_wave * 40) |
| 117 | + ax.plot(x_wave, y_wave, color="#CC3333", linewidth=2.5, zorder=5) |
| 118 | + # Place unconformity labels above the wavy line, inside column |
| 119 | + ax.text( |
| 120 | + col_width / 2, |
| 121 | + depth - 2.0, |
| 122 | + label, |
| 123 | + fontsize=10, |
| 124 | + fontweight="bold", |
| 125 | + va="bottom", |
| 126 | + ha="center", |
| 127 | + color="#CC3333", |
| 128 | + fontstyle="italic", |
| 129 | + zorder=6, |
| 130 | + bbox={"facecolor": "white", "alpha": 0.85, "edgecolor": "none", "pad": 1.5}, |
| 131 | + ) |
| 132 | + |
| 133 | +# Age labels on the left panel |
| 134 | +age_positions = {} |
| 135 | +for _, row in df.iterrows(): |
| 136 | + age = row["age"] |
| 137 | + if age not in age_positions: |
| 138 | + age_positions[age] = {"top": row["top"], "bottom": row["bottom"]} |
| 139 | + age_positions[age]["top"] = min(age_positions[age]["top"], row["top"]) |
| 140 | + age_positions[age]["bottom"] = max(age_positions[age]["bottom"], row["bottom"]) |
| 141 | + |
| 142 | +ax_age.set_xlim(0, 1) |
| 143 | +ax_age.set_ylim(total_depth, 0) |
| 144 | +ax.set_xlim(-0.02, 1.20) |
| 145 | + |
| 146 | +for age, pos in age_positions.items(): |
| 147 | + mid_y = (pos["top"] + pos["bottom"]) / 2 |
| 148 | + bg = age_bg_colors[age] |
| 149 | + |
| 150 | + # Subtle background band across both panels |
| 151 | + ax_age.axhspan(pos["top"], pos["bottom"], color=bg, zorder=0) |
| 152 | + ax.axhspan(pos["top"], pos["bottom"], color=bg, zorder=0) |
| 153 | + |
| 154 | + # Bracket lines |
| 155 | + ax_age.plot( |
| 156 | + [0.85, 0.85], [pos["top"] + 1, pos["bottom"] - 1], color="#555555", linewidth=2.5, solid_capstyle="butt" |
| 157 | + ) |
| 158 | + ax_age.plot([0.80, 0.90], [pos["top"] + 1, pos["top"] + 1], color="#555555", linewidth=2) |
| 159 | + ax_age.plot([0.80, 0.90], [pos["bottom"] - 1, pos["bottom"] - 1], color="#555555", linewidth=2) |
| 160 | + |
| 161 | + # Age text |
| 162 | + ax_age.text( |
| 163 | + 0.35, |
| 164 | + mid_y, |
| 165 | + age.replace(" ", "\n"), |
| 166 | + fontsize=15, |
| 167 | + fontweight="bold", |
| 168 | + va="center", |
| 169 | + ha="center", |
| 170 | + fontstyle="italic", |
| 171 | + color="#333333", |
| 172 | + ) |
| 173 | + |
| 174 | +# Style age axis |
| 175 | +ax_age.set_ylabel("Depth (m)", fontsize=20, labelpad=10) |
| 176 | +ax_age.tick_params(axis="y", labelsize=16) |
| 177 | +ax_age.set_yticks(np.arange(0, total_depth + 1, 20)) |
| 178 | +ax_age.set_xticks([]) |
| 179 | +sns.despine(ax=ax_age, top=True, right=True, bottom=True) |
| 180 | + |
| 181 | +ax.set_xticks([]) |
| 182 | +ax.set_xlabel("") |
| 183 | +ax.set_ylabel("") |
| 184 | +ax.tick_params(axis="y", left=False, labelleft=False) |
| 185 | +sns.despine(ax=ax, left=True, bottom=True, top=True, right=True) |
| 186 | + |
| 187 | +# Right panel: seaborn stripplot showing layer thicknesses by lithology |
| 188 | +# Uses distinctive seaborn categorical visualization (strip plot with jitter) |
| 189 | +lith_order = [s["label"] for s in lithology_styles.values()] |
| 190 | +strip_palette = {s["label"]: s["color"] for s in lithology_styles.values()} |
| 191 | +sns.stripplot( |
| 192 | + data=thickness_df, |
| 193 | + x="thickness", |
| 194 | + y="lithology", |
| 195 | + ax=ax_thick, |
| 196 | + palette=strip_palette, |
| 197 | + hue="lithology", |
| 198 | + order=lith_order, |
| 199 | + size=14, |
| 200 | + marker="D", |
| 201 | + edgecolor="black", |
| 202 | + linewidth=1.0, |
| 203 | + jitter=False, |
| 204 | + legend=False, |
| 205 | +) |
| 206 | +ax_thick.set_xlabel("Thickness (m)", fontsize=16) |
| 207 | +ax_thick.set_ylabel("") |
| 208 | +ax_thick.tick_params(axis="both", labelsize=13) |
| 209 | +ax_thick.set_title("Layer Thickness", fontsize=16, fontweight="medium", pad=8) |
| 210 | +ax_thick.xaxis.grid(True, alpha=0.3, linewidth=0.8) |
| 211 | +sns.despine(ax=ax_thick, top=True, right=True) |
| 212 | + |
| 213 | +# Lithology legend on the thickness panel (avoids overlapping column labels) |
| 214 | +legend_handles = [ |
| 215 | + mpatches.Patch(facecolor=style["color"], edgecolor="black", hatch=style["hatch"], label=style["label"]) |
| 216 | + for style in lithology_styles.values() |
| 217 | +] |
| 218 | + |
| 219 | +ax_thick.legend( |
| 220 | + handles=legend_handles, |
| 221 | + loc="lower right", |
| 222 | + fontsize=12, |
| 223 | + framealpha=0.95, |
| 224 | + title="Lithology", |
| 225 | + title_fontsize=14, |
| 226 | + edgecolor="#CCCCCC", |
| 227 | + fancybox=True, |
| 228 | +) |
| 229 | + |
| 230 | +# Title |
| 231 | +fig.suptitle("column-stratigraphic · seaborn · pyplots.ai", fontsize=24, fontweight="medium", y=0.97) |
| 232 | + |
| 233 | +plt.subplots_adjust(wspace=0.08) |
| 234 | +plt.tight_layout(rect=[0, 0, 1, 0.95]) |
| 235 | + |
| 236 | +# Save |
| 237 | +plt.savefig("plot.png", dpi=300, bbox_inches="tight") |
0 commit comments