{"spec_id":"column-stratigraphic","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\ncolumn-stratigraphic: Stratigraphic Column with Lithology Patterns\nLibrary: plotnine 0.15.7 | Python 3.13.13\nQuality: 94/100 | Updated: 2026-06-17\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    annotate,\n    coord_cartesian,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_label,\n    geom_linerange,\n    geom_point,\n    geom_segment,\n    geom_text,\n    geom_tile,\n    ggplot,\n    guide_legend,\n    guides,\n    labs,\n    scale_fill_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\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\"\n\n# Data - synthetic sedimentary borehole section (Western US)\nlayers = pd.DataFrame(\n    {\n        \"top\": [0, 12, 28, 45, 58, 72, 95, 115, 138, 160],\n        \"bottom\": [12, 28, 45, 58, 72, 95, 115, 138, 160, 180],\n        \"lithology\": [\n            \"Sandstone\",\n            \"Shale\",\n            \"Limestone\",\n            \"Siltstone\",\n            \"Sandstone\",\n            \"Conglomerate\",\n            \"Shale\",\n            \"Limestone\",\n            \"Mudstone\",\n            \"Sandstone\",\n        ],\n        \"formation\": [\n            \"Frontier Fm\",\n            \"Frontier Fm\",\n            \"Madison Fm\",\n            \"Madison Fm\",\n            \"Kootenai Fm\",\n            \"Kootenai Fm\",\n            \"Morrison Fm\",\n            \"Morrison Fm\",\n            \"Sundance Fm\",\n            \"Sundance Fm\",\n        ],\n        \"age\": [\n            \"Late Cretaceous\",\n            \"Late Cretaceous\",\n            \"Early Cretaceous\",\n            \"Early Cretaceous\",\n            \"Late Jurassic\",\n            \"Late Jurassic\",\n            \"Middle Jurassic\",\n            \"Middle Jurassic\",\n            \"Triassic\",\n            \"Triassic\",\n        ],\n    }\n)\n\n# Derived columns for grammar-of-graphics mapping\nlayers[\"mid\"] = (layers[\"top\"] + layers[\"bottom\"]) / 2\nlayers[\"thickness\"] = layers[\"bottom\"] - layers[\"top\"]\n\n# Column geometry\ncol_left = 0.0\ncol_right = 3.5\nlayers[\"x_center\"] = (col_left + col_right) / 2\n\n# Lithology fills - Imprint palette (Sandstone = brand green, position 1).\n# Position 5 (matte red #AE3030) is reserved for the unconformity focal point.\nlith_colors = {\n    \"Sandstone\": \"#009E73\",\n    \"Shale\": \"#C475FD\",\n    \"Limestone\": \"#4467A3\",\n    \"Siltstone\": \"#BD8233\",\n    \"Conglomerate\": \"#2ABCCD\",\n    \"Mudstone\": \"#954477\",\n}\n\n# Unconformity depth (J/K boundary between Kootenai Fm and Morrison Fm)\nunconformity_depth = 95.0\n\n# Generate FGDC/USGS-style pattern overlays inline (flat KISS structure)\nnp.random.seed(42)\ndot_rows = []\ncircle_rows = []\nseg_rows = []\n\nfor _, row in layers.iterrows():\n    top_val, bot_val, lith = row[\"top\"], row[\"bottom\"], row[\"lithology\"]\n    thickness = bot_val - top_val\n\n    if lith == \"Sandstone\":  # stipple dots\n        n = int(thickness * 5)\n        xs = np.random.uniform(col_left + 0.3, col_right - 0.3, n)\n        ys = np.random.uniform(top_val + 0.5, bot_val - 0.5, n)\n        for px, py in zip(xs, ys, strict=True):\n            dot_rows.append({\"x\": px, \"y\": py})\n\n    elif lith == \"Shale\":  # horizontal dashes\n        y_pos = top_val + 1.0\n        while y_pos < bot_val - 0.3:\n            for x_start in np.arange(col_left + 0.3, col_right - 0.3, 0.8):\n                seg_rows.append({\"x\": x_start, \"y\": y_pos, \"xend\": x_start + 0.45, \"yend\": y_pos})\n            y_pos += 2.5\n\n    elif lith == \"Limestone\":  # brick (staggered horizontal + vertical lines)\n        y_pos = top_val + 2.0\n        ridx = 0\n        while y_pos < bot_val - 1.0:\n            seg_rows.append({\"x\": col_left + 0.2, \"y\": y_pos, \"xend\": col_right - 0.2, \"yend\": y_pos})\n            offset = 0.8 if ridx % 2 == 0 else 0.0\n            for vx in np.arange(col_left + 0.4 + offset, col_right - 0.3, 1.6):\n                seg_rows.append({\"x\": vx, \"y\": max(y_pos - 4.0, top_val + 0.2), \"xend\": vx, \"yend\": y_pos})\n            y_pos += 4.0\n            ridx += 1\n\n    elif lith == \"Siltstone\":  # short random dashes\n        n = int(thickness * 8)\n        xs = np.random.uniform(col_left + 0.3, col_right - 0.3, n)\n        ys = np.random.uniform(top_val + 0.5, bot_val - 0.5, n)\n        dxs = np.random.uniform(-0.18, 0.18, n)\n        for px, py, dx in zip(xs, ys, dxs, strict=True):\n            seg_rows.append({\"x\": px, \"y\": py, \"xend\": px + dx, \"yend\": py + 0.3})\n\n    elif lith == \"Conglomerate\":  # open clasts\n        n = int(thickness * 2.5)\n        xs = np.random.uniform(col_left + 0.5, col_right - 0.5, n)\n        ys = np.random.uniform(top_val + 1.0, bot_val - 1.0, n)\n        for px, py in zip(xs, ys, strict=True):\n            circle_rows.append({\"x\": px, \"y\": py})\n\n    elif lith == \"Mudstone\":  # fine horizontal dashes\n        y_pos = top_val + 0.7\n        while y_pos < bot_val - 0.3:\n            for x_start in np.arange(col_left + 0.3, col_right - 0.3, 0.5):\n                seg_rows.append({\"x\": x_start, \"y\": y_pos, \"xend\": x_start + 0.22, \"yend\": y_pos})\n            y_pos += 1.8\n\ndots_df = pd.DataFrame(dot_rows)\ncircles_df = pd.DataFrame(circle_rows)\nlines_df = pd.DataFrame(seg_rows)\n\n# Formation labels (one per formation, at its midpoint) on the right flank\nform_groups = layers.groupby(\"formation\", sort=False).agg({\"top\": \"min\", \"bottom\": \"max\"}).reset_index()\nform_groups[\"mid\"] = (form_groups[\"top\"] + form_groups[\"bottom\"]) / 2\nform_groups[\"x\"] = col_right + 0.3\n\n# Age labels (one per age, at its midpoint) on the left flank with bracket lines\nage_groups = layers.groupby(\"age\", sort=False).agg({\"top\": \"min\", \"bottom\": \"max\"}).reset_index()\nage_groups[\"mid\"] = (age_groups[\"top\"] + age_groups[\"bottom\"]) / 2\nage_groups[\"x\"] = col_left - 0.55\nage_groups[\"ymin\"] = age_groups[\"top\"] + 0.6\nage_groups[\"ymax\"] = age_groups[\"bottom\"] - 0.6\nage_groups[\"bracket_x\"] = col_left - 0.25\n\n# Layer boundary lines\nboundaries = sorted(set(layers[\"top\"].tolist() + layers[\"bottom\"].tolist()))\nboundary_df = pd.DataFrame(\n    {\"x\": [col_left] * len(boundaries), \"xend\": [col_right] * len(boundaries), \"y\": boundaries, \"yend\": boundaries}\n)\n\n# Unconformity wavy line\nwavy_x = np.linspace(col_left - 0.15, col_right + 0.15, 60)\nwavy_y = unconformity_depth + np.sin(wavy_x * 8) * 0.8\nwavy_df = pd.DataFrame({\"x\": wavy_x[:-1], \"y\": wavy_y[:-1], \"xend\": wavy_x[1:], \"yend\": wavy_y[1:]})\n\n# Build plot using plotnine grammar of graphics\nplot = (\n    ggplot()\n    # Layer fills - grammar-driven aesthetic mapping\n    + geom_tile(\n        data=layers,\n        mapping=aes(x=\"x_center\", y=\"mid\", width=col_right - col_left, height=\"thickness\", fill=\"lithology\"),\n        color=INK,\n        size=0.7,\n        alpha=0.62,\n    )\n    # Pattern overlays - line segments (dashes, brick, siltstone, mudstone)\n    + geom_segment(data=lines_df, mapping=aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), color=INK, size=0.45, alpha=0.85)\n    # Pattern overlays - stipple dots (sandstone)\n    + geom_point(data=dots_df, mapping=aes(x=\"x\", y=\"y\"), color=INK, size=0.9, alpha=0.8)\n    # Pattern overlays - open clasts (conglomerate)\n    + geom_point(\n        data=circles_df, mapping=aes(x=\"x\", y=\"y\"), color=INK, size=2.6, alpha=0.7, shape=\"o\", fill=\"none\", stroke=0.8\n    )\n    # Layer boundary lines\n    + geom_segment(data=boundary_df, mapping=aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), color=INK, size=0.7)\n    # Unconformity wavy line - Imprint matte red (#AE3030), the storytelling focal point\n    + geom_segment(\n        data=wavy_df, mapping=aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), color=\"#AE3030\", size=1.8, alpha=0.95\n    )\n    + annotate(\n        \"text\",\n        x=col_right + 0.3,\n        y=unconformity_depth,\n        label=\"Unconformity\",\n        ha=\"left\",\n        size=4.2,\n        color=\"#AE3030\",\n        fontstyle=\"italic\",\n        fontweight=\"bold\",\n    )\n    # Age bracket lines (idiomatic plotnine vertical ranges)\n    + geom_linerange(data=age_groups, mapping=aes(x=\"bracket_x\", ymin=\"ymin\", ymax=\"ymax\"), color=INK_SOFT, size=0.8)\n    # Formation labels (plotnine-native styled text with elevated background)\n    + geom_label(\n        data=form_groups,\n        mapping=aes(x=\"x\", y=\"mid\", label=\"formation\"),\n        ha=\"left\",\n        size=3.7,\n        fontstyle=\"italic\",\n        color=INK,\n        fill=ELEVATED_BG,\n        label_padding=0.28,\n        label_size=0.3,\n    )\n    # Age labels on the left flank\n    + geom_text(\n        data=age_groups,\n        mapping=aes(x=\"x\", y=\"mid\", label=\"age\"),\n        ha=\"right\",\n        size=3.6,\n        fontweight=\"bold\",\n        color=INK_SOFT,\n    )\n    # Scales - grammar-driven fill mapping\n    + scale_fill_manual(values=lith_colors, name=\"Lithology\")\n    + scale_x_continuous(limits=(-3.6, 7.2), breaks=[])\n    + scale_y_continuous(trans=\"reverse\", name=\"Depth (m)\", breaks=list(range(0, 200, 20)))\n    + coord_cartesian(xlim=(-3.6, 7.2), ylim=(185, -5))\n    + labs(title=\"column-stratigraphic · python · plotnine · anyplot.ai\", x=\"\")\n    + guides(fill=guide_legend(nrow=2))\n    # Theme - theme-adaptive chrome over a 2400×2400 square canvas\n    + theme_minimal()\n    + theme(\n        figure_size=(6, 6),\n        plot_title=element_text(size=12, face=\"bold\", ha=\"center\", color=INK),\n        axis_title_y=element_text(size=11, color=INK),\n        axis_title_x=element_blank(),\n        axis_text_y=element_text(size=9, color=INK_SOFT),\n        axis_text_x=element_blank(),\n        axis_ticks_major_x=element_blank(),\n        legend_title=element_text(size=11, face=\"bold\", color=INK),\n        legend_text=element_text(size=9, color=INK_SOFT),\n        legend_position=\"bottom\",\n        legend_direction=\"horizontal\",\n        legend_key_size=16,\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT, size=0.4),\n        legend_margin=8,\n        panel_grid_major_x=element_blank(),\n        panel_grid_minor_x=element_blank(),\n        panel_grid_major_y=element_line(color=INK, size=0.3, alpha=0.12),\n        panel_grid_minor_y=element_blank(),\n        panel_background=element_rect(fill=PAGE_BG, color=\"none\"),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    )\n)\n\n# Save (square 2400×2400)\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=6, height=6, units=\"in\")\n"}