{"spec_id":"column-stratigraphic","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\ncolumn-stratigraphic: Stratigraphic Column with Lithology Patterns\nLibrary: matplotlib 3.11.0 | Python 3.13.14\nQuality: 94/100 | Updated: 2026-06-17\n\"\"\"\n\nimport os\n\nimport matplotlib.patches as mpatches\nimport matplotlib.pyplot as plt\nimport numpy as np\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Theme-adaptive Chrome\")\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — first categorical series is ALWAYS brand green (#009E73).\n# Lithologies are ordered by grain size (coarse → fine), so conglomerate leads.\n# Semantic exception: sand → ochre, carbonate → blue, mudrock → muted gray.\nIMPRINT_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Data — synthetic sedimentary borehole section (depth increases downward,\n# youngest at top), using real US Great Plains formations and ages.\nlayers = [\n    {\"top\": 0, \"bottom\": 12, \"lithology\": \"conglomerate\", \"formation\": \"Ogallala Fm\", \"age\": \"Miocene\"},\n    {\"top\": 12, \"bottom\": 30, \"lithology\": \"sandstone\", \"formation\": \"Arikaree Fm\", \"age\": \"Miocene\"},\n    {\"top\": 30, \"bottom\": 50, \"lithology\": \"siltstone\", \"formation\": \"White River Fm\", \"age\": \"Oligocene\"},\n    {\"top\": 50, \"bottom\": 72, \"lithology\": \"shale\", \"formation\": \"Chadron Fm\", \"age\": \"Eocene\"},\n    {\"top\": 72, \"bottom\": 95, \"lithology\": \"limestone\", \"formation\": \"Niobrara Fm\", \"age\": \"Cretaceous\"},\n    {\"top\": 95, \"bottom\": 118, \"lithology\": \"shale\", \"formation\": \"Carlile Shale\", \"age\": \"Cretaceous\"},\n    {\"top\": 118, \"bottom\": 140, \"lithology\": \"limestone\", \"formation\": \"Greenhorn Fm\", \"age\": \"Cretaceous\"},\n    {\"top\": 140, \"bottom\": 158, \"lithology\": \"sandstone\", \"formation\": \"Dakota Fm\", \"age\": \"Cretaceous\"},\n    {\"top\": 158, \"bottom\": 175, \"lithology\": \"siltstone\", \"formation\": \"Morrison Fm\", \"age\": \"Jurassic\"},\n    {\"top\": 175, \"bottom\": 200, \"lithology\": \"sandstone\", \"formation\": \"Entrada Fm\", \"age\": \"Jurassic\"},\n]\n\n# Lithology styles: Imprint color, FGDC-like hatch, grain-size column width.\n# Grain size sets the block width (wider = coarser), a standard sed-log convention.\nlithology_styles = {\n    \"conglomerate\": {\"color\": \"#009E73\", \"hatch\": \"o\", \"grain\": 3.2},  # brand green — first series\n    \"sandstone\": {\"color\": \"#BD8233\", \"hatch\": \"..\", \"grain\": 2.6},  # ochre — sand\n    \"siltstone\": {\"color\": \"#C475FD\", \"hatch\": \"//\", \"grain\": 1.9},  # lavender\n    \"shale\": {\"color\": IMPRINT_MUTED, \"hatch\": \"--\", \"grain\": 1.3},  # muted gray — mudrock\n    \"limestone\": {\"color\": \"#4467A3\", \"hatch\": \"+\", \"grain\": 2.3},  # blue — carbonate\n}\nlithology_order = [\"conglomerate\", \"sandstone\", \"siltstone\", \"shale\", \"limestone\"]\n\n# Plot — square canvas, depth has no preferred horizontal axis\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\ncolumn_left = 0.0\nmax_depth = 200\n\n# Compute the depth span of each geological period\nage_spans = {}\nfor layer in layers:\n    age = layer[\"age\"]\n    if age not in age_spans:\n        age_spans[age] = {\"top\": layer[\"top\"], \"bottom\": layer[\"bottom\"]}\n    else:\n        age_spans[age][\"top\"] = min(age_spans[age][\"top\"], layer[\"top\"])\n        age_spans[age][\"bottom\"] = max(age_spans[age][\"bottom\"], layer[\"bottom\"])\n\n# Subtle alternating period-band shading behind the column (theme-adaptive)\nfor idx, span in enumerate(age_spans.values()):\n    if idx % 2 == 0:\n        band = mpatches.Rectangle(\n            (-0.8, span[\"top\"]),\n            4.3,\n            span[\"bottom\"] - span[\"top\"],\n            facecolor=INK,\n            edgecolor=\"none\",\n            alpha=0.045,\n            zorder=0,\n        )\n        ax.add_patch(band)\n\n# Lithology layers — width encodes grain size, hatch encodes rock type\nfor layer in layers:\n    top, bottom = layer[\"top\"], layer[\"bottom\"]\n    style = lithology_styles[layer[\"lithology\"]]\n\n    rect = mpatches.Rectangle(\n        (column_left, top),\n        style[\"grain\"],\n        bottom - top,\n        facecolor=style[\"color\"],\n        edgecolor=INK_SOFT,\n        linewidth=1.0,\n        hatch=style[\"hatch\"],\n        alpha=0.9,\n        zorder=2,\n    )\n    ax.add_patch(rect)\n\n    mid_depth = (top + bottom) / 2\n    ax.text(3.6, mid_depth, layer[\"formation\"], fontsize=10, va=\"center\", ha=\"left\", color=INK, zorder=4)\n\n# Unconformity — wavy erosional surface between Eocene and Cretaceous (72 m)\nunconformity_depth = 72\nx_wave = np.linspace(column_left, 3.3, 120)\ny_wave = unconformity_depth + 1.1 * np.sin(x_wave * 7)\nax.plot(x_wave, y_wave, color=\"#AE3030\", linewidth=2.0, zorder=3)\nax.text(\n    3.6,\n    unconformity_depth,\n    \"unconformity\",\n    fontsize=8,\n    va=\"center\",\n    ha=\"left\",\n    fontstyle=\"italic\",\n    color=\"#AE3030\",\n    zorder=4,\n)\n\n# Period (age) labels on the left with bracket lines\nbracket_x = -0.65\nfor age, span in age_spans.items():\n    mid = (span[\"top\"] + span[\"bottom\"]) / 2\n    ax.text(-1.05, mid, age, fontsize=10, va=\"center\", ha=\"right\", fontstyle=\"italic\", color=INK_SOFT, clip_on=False)\n    ax.plot([bracket_x, bracket_x + 0.35], [span[\"top\"] + 0.6, span[\"top\"] + 0.6], color=INK_SOFT, linewidth=1.0)\n    ax.plot([bracket_x, bracket_x + 0.35], [span[\"bottom\"] - 0.6, span[\"bottom\"] - 0.6], color=INK_SOFT, linewidth=1.0)\n    ax.plot([bracket_x, bracket_x], [span[\"top\"] + 0.6, span[\"bottom\"] - 0.6], color=INK_SOFT, linewidth=1.0)\n\n# Grain-size guide above the column (explains the varying block width)\nax.annotate(\"\", xy=(3.3, -9), xytext=(0.1, -9), arrowprops={\"arrowstyle\": \"->\", \"color\": INK_MUTED, \"linewidth\": 1.2})\nax.text(1.7, -16, \"grain size: fine → coarse\", ha=\"center\", fontsize=8, color=INK_MUTED)\n\n# Legend — lithology patterns, in grain-size order\nlegend_handles = [\n    mpatches.Patch(\n        facecolor=lithology_styles[lith][\"color\"],\n        edgecolor=INK_SOFT,\n        hatch=lithology_styles[lith][\"hatch\"],\n        label=lith.capitalize(),\n        alpha=0.9,\n        linewidth=1.0,\n    )\n    for lith in lithology_order\n]\nleg = ax.legend(\n    handles=legend_handles,\n    loc=\"center left\",\n    bbox_to_anchor=(0.72, 0.5),\n    fontsize=9,\n    title=\"Lithology\",\n    title_fontsize=10,\n    framealpha=0.95,\n    borderpad=1.0,\n    labelspacing=0.8,\n    handleheight=1.6,\n)\nleg.get_frame().set_facecolor(ELEVATED_BG)\nleg.get_frame().set_edgecolor(INK_SOFT)\nleg.get_title().set_color(INK)\nfor txt in leg.get_texts():\n    txt.set_color(INK_SOFT)\n\n# Style\ntitle = \"column-stratigraphic · python · matplotlib · anyplot.ai\"\ntitle_fontsize = round(12 * 67 / len(title)) if len(title) > 67 else 12\n\nax.set_xlim(-5.0, 14.2)\nax.set_ylim(210, -22)\nax.set_ylabel(\"Depth (m)\", fontsize=11, color=INK, labelpad=8)\nax.set_title(title, fontsize=title_fontsize, fontweight=\"medium\", color=INK, pad=12)\nax.set_yticks(np.arange(0, max_depth + 1, 25))\nax.tick_params(axis=\"y\", labelsize=9, length=4, colors=INK_SOFT)\nax.set_xticks([])\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nax.spines[\"bottom\"].set_visible(False)\nax.spines[\"left\"].set_color(INK_SOFT)\nax.yaxis.grid(True, alpha=0.12, linewidth=0.8, color=INK)\n\nfig.subplots_adjust(left=0.085, right=0.985, top=0.9, bottom=0.06)\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}