{"spec_id":"column-stratigraphic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\ncolumn-stratigraphic: Stratigraphic Column with Lithology Patterns\nLibrary: bokeh 3.9.1 | Python 3.13.13\nQuality: 94/100 | Updated: 2026-06-17\n\"\"\"\n\nimport math\nimport os\nimport sys\n\n\n# Prevent self-import: this file is named bokeh.py, which shadows the installed\n# bokeh package when its directory sits at the front of sys.path.\n_this_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p or \".\") != _this_dir]\n\nimport time\nfrom pathlib import Path\n\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, FixedTicker, HoverTool, Label, Legend, LegendItem, Range1d, Span\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme-adaptive chrome (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# Imprint palette — data colors stay identical across light/dark themes\nIMPRINT_RED = \"#AE3030\"  # semantic anchor — K-Pg extinction event marker\n\n# Data: synthetic borehole section, depth increasing downward (law of\n# superposition — youngest beds on top, oldest at depth). Ages run from\n# Eocene (shallow) down through Paleocene to Late Cretaceous (deep).\nlayers = [\n    {\"top\": 0, \"bottom\": 22, \"lithology\": \"Siltstone\", \"formation\": \"Uinta Fm\", \"age\": \"Eocene\"},\n    {\"top\": 22, \"bottom\": 40, \"lithology\": \"Sandstone\", \"formation\": \"Wasatch Fm\", \"age\": \"Eocene\"},\n    {\"top\": 40, \"bottom\": 66, \"lithology\": \"Shale\", \"formation\": \"Green River Fm\", \"age\": \"Eocene\"},\n    {\"top\": 66, \"bottom\": 84, \"lithology\": \"Conglomerate\", \"formation\": \"Dawson Fm\", \"age\": \"Paleocene\"},\n    {\"top\": 84, \"bottom\": 100, \"lithology\": \"Sandstone\", \"formation\": \"Fort Union Fm\", \"age\": \"Paleocene\"},\n    {\"top\": 100, \"bottom\": 120, \"lithology\": \"Sandstone\", \"formation\": \"Fox Hills Fm\", \"age\": \"Late Cretaceous\"},\n    {\"top\": 120, \"bottom\": 150, \"lithology\": \"Shale\", \"formation\": \"Pierre Fm\", \"age\": \"Late Cretaceous\"},\n    {\"top\": 150, \"bottom\": 166, \"lithology\": \"Limestone\", \"formation\": \"Niobrara Fm\", \"age\": \"Late Cretaceous\"},\n    {\"top\": 166, \"bottom\": 184, \"lithology\": \"Shale\", \"formation\": \"Mancos Fm\", \"age\": \"Late Cretaceous\"},\n    {\"top\": 184, \"bottom\": 200, \"lithology\": \"Sandstone\", \"formation\": \"Dakota Fm\", \"age\": \"Late Cretaceous\"},\n]\n\n# Lithology styling: Imprint colors (constant) + FGDC-style hatch for a\n# redundant, colorblind-safe encoding (stipple=sandstone, dashes=shale, etc.)\nlithology_styles = {\n    \"Sandstone\": {\"color\": \"#009E73\", \"hatch_pattern\": \".\"},  # Imprint 1 (brand — first series)\n    \"Shale\": {\"color\": \"#C475FD\", \"hatch_pattern\": \"-\"},  # Imprint 2\n    \"Limestone\": {\"color\": \"#4467A3\", \"hatch_pattern\": \"+\"},  # Imprint 3\n    \"Siltstone\": {\"color\": \"#BD8233\", \"hatch_pattern\": \"/\"},  # Imprint 4\n    \"Conglomerate\": {\"color\": \"#2ABCCD\", \"hatch_pattern\": \"o\"},  # Imprint 6\n}\nHATCH_INK = \"#211F1A\"  # dark hatch reads on every (constant) Imprint fill\n\n# K-Pg boundary: contact between Paleocene (above) and Late Cretaceous (below)\nKPG_DEPTH = 100\n\n# Column geometry\nCOL_LEFT, COL_RIGHT = 0.0, 2.4\ncol_center = (COL_LEFT + COL_RIGHT) / 2\n\n# Plot\ntitle = \"column-stratigraphic · python · bokeh · anyplot.ai\"\np = figure(\n    width=3200,\n    height=1800,\n    title=title,\n    y_axis_label=\"Depth (m)\",\n    toolbar_location=None,\n    x_range=Range1d(-1.8, 3.9),\n    y_range=Range1d(208, -8),\n    min_border_left=180,\n    min_border_top=110,\n    min_border_bottom=70,\n    min_border_right=40,\n)\n\n# Draw each layer as a depth interval with its lithology pattern\nlegend_renderers = {}\nfor layer in layers:\n    style = lithology_styles[layer[\"lithology\"]]\n    source = ColumnDataSource(\n        data={\n            \"left\": [COL_LEFT],\n            \"right\": [COL_RIGHT],\n            \"top\": [layer[\"top\"]],\n            \"bottom\": [layer[\"bottom\"]],\n            \"lithology\": [layer[\"lithology\"]],\n            \"formation\": [layer[\"formation\"]],\n            \"age\": [layer[\"age\"]],\n            \"top_depth\": [layer[\"top\"]],\n            \"bottom_depth\": [layer[\"bottom\"]],\n            \"thickness\": [layer[\"bottom\"] - layer[\"top\"]],\n        }\n    )\n\n    renderer = p.quad(\n        left=\"left\",\n        right=\"right\",\n        top=\"top\",\n        bottom=\"bottom\",\n        source=source,\n        fill_color=style[\"color\"],\n        line_color=INK,\n        line_width=2.5,\n        hatch_pattern=style[\"hatch_pattern\"],\n        hatch_color=HATCH_INK,\n        hatch_alpha=0.6,\n        hatch_scale=20,\n        hatch_weight=2.2,\n    )\n\n    legend_renderers.setdefault(layer[\"lithology\"], renderer)\n\n    p.add_tools(\n        HoverTool(\n            renderers=[renderer],\n            tooltips=[\n                (\"Lithology\", \"@lithology\"),\n                (\"Formation\", \"@formation\"),\n                (\"Age\", \"@age\"),\n                (\"Top\", \"@top_depth{0.0} m\"),\n                (\"Bottom\", \"@bottom_depth{0.0} m\"),\n                (\"Thickness\", \"@thickness{0.0} m\"),\n            ],\n        )\n    )\n\n# Formation labels — to the right of the column\nfor layer in layers:\n    p.add_layout(\n        Label(\n            x=COL_RIGHT + 0.18,\n            y=(layer[\"top\"] + layer[\"bottom\"]) / 2,\n            text=layer[\"formation\"],\n            text_font_size=\"30pt\",\n            text_font_style=\"bold\",\n            text_align=\"left\",\n            text_baseline=\"middle\",\n            text_color=INK,\n        )\n    )\n\n# Geological age brackets on the left — rotated period names + bracket lines\nage_groups = {}\nfor layer in layers:\n    bounds = age_groups.setdefault(layer[\"age\"], {\"top\": layer[\"top\"], \"bottom\": layer[\"bottom\"]})\n    bounds[\"top\"] = min(bounds[\"top\"], layer[\"top\"])\n    bounds[\"bottom\"] = max(bounds[\"bottom\"], layer[\"bottom\"])\n\nbracket_x = -0.6\nfor age, bounds in age_groups.items():\n    mid_y = (bounds[\"top\"] + bounds[\"bottom\"]) / 2\n    p.add_layout(\n        Label(\n            x=-1.25,\n            y=mid_y,\n            text=age,\n            text_font_size=\"30pt\",\n            text_font_style=\"italic\",\n            text_align=\"center\",\n            text_baseline=\"middle\",\n            text_color=INK,\n            angle=math.pi / 2,\n        )\n    )\n    p.line(x=[bracket_x, bracket_x], y=[bounds[\"top\"] + 1.5, bounds[\"bottom\"] - 1.5], line_color=INK_SOFT, line_width=3)\n    for y in (bounds[\"top\"] + 1.5, bounds[\"bottom\"] - 1.5):\n        p.line(x=[bracket_x - 0.12, bracket_x], y=[y, y], line_color=INK_SOFT, line_width=3)\n\n# K-Pg boundary emphasis — dashed red rule + labelled event marker\np.add_layout(Span(location=KPG_DEPTH, dimension=\"width\", line_color=IMPRINT_RED, line_width=5, line_dash=\"dashed\"))\np.add_layout(\n    Label(\n        x=col_center,\n        y=KPG_DEPTH,\n        text=\"K–Pg Boundary  (~66 Ma)\",\n        text_font_size=\"30pt\",\n        text_font_style=\"bold\",\n        text_color=IMPRINT_RED,\n        text_align=\"center\",\n        text_baseline=\"bottom\",\n        y_offset=10,\n        background_fill_color=ELEVATED_BG,\n        background_fill_alpha=0.92,\n        border_line_color=IMPRINT_RED,\n        border_line_alpha=0.5,\n        padding=6,\n    )\n)\n\n# Legend — lithology key on the right panel (fills the wide landscape canvas)\nlegend = Legend(\n    items=[LegendItem(label=lith, renderers=[rend]) for lith, rend in legend_renderers.items()],\n    title=\"Lithology\",\n    location=\"center\",\n    label_text_font_size=\"34pt\",\n    label_text_color=INK,\n    title_text_font_size=\"36pt\",\n    title_text_font_style=\"bold\",\n    title_text_color=INK,\n    glyph_height=46,\n    glyph_width=46,\n    spacing=18,\n    padding=24,\n    margin=20,\n    background_fill_color=ELEVATED_BG,\n    border_line_color=INK_SOFT,\n)\np.add_layout(legend, \"right\")\n\n# Typography + chrome\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\np.title.offset = 8\np.yaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_style = \"bold\"\np.yaxis.axis_label_text_color = INK\np.yaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_color = INK_SOFT\np.yaxis.ticker = FixedTicker(ticks=sorted({lz[\"top\"] for lz in layers} | {lz[\"bottom\"] for lz in layers}))\np.yaxis.axis_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\np.yaxis.minor_tick_line_color = None\n\np.xaxis.visible = False\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = None\np.outline_line_color = None\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\n\n# Save — interactive HTML, then screenshot it with headless Chrome\noutput_file(f\"plot-{THEME}.html\", title=title)\nsave(p)\n\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\n# CDP override forces an exact W×H viewport regardless of outer window chrome\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}