|
| 1 | +""" pyplots.ai |
| 2 | +column-stratigraphic: Stratigraphic Column with Lithology Patterns |
| 3 | +Library: bokeh 3.9.0 | Python 3.14.3 |
| 4 | +Quality: 89/100 | Created: 2026-03-15 |
| 5 | +""" |
| 6 | + |
| 7 | +from bokeh.io import export_png |
| 8 | +from bokeh.models import ColumnDataSource, HoverTool, Label, Legend, LegendItem, Range1d, Span |
| 9 | +from bokeh.plotting import figure, output_file, save |
| 10 | + |
| 11 | + |
| 12 | +# Data: Synthetic sedimentary section with 10 layers (varied thicknesses) |
| 13 | +layers = [ |
| 14 | + {"top": 0, "bottom": 12, "lithology": "Sandstone", "formation": "Dakota Fm", "age": "Late Cretaceous"}, |
| 15 | + {"top": 12, "bottom": 38, "lithology": "Shale", "formation": "Mancos Fm", "age": "Late Cretaceous"}, |
| 16 | + {"top": 38, "bottom": 52, "lithology": "Limestone", "formation": "Niobrara Fm", "age": "Late Cretaceous"}, |
| 17 | + {"top": 52, "bottom": 60, "lithology": "Siltstone", "formation": "Pierre Fm", "age": "Late Cretaceous"}, |
| 18 | + {"top": 60, "bottom": 88, "lithology": "Sandstone", "formation": "Fox Hills Fm", "age": "Late Cretaceous"}, |
| 19 | + {"top": 88, "bottom": 112, "lithology": "Conglomerate", "formation": "Dawson Fm", "age": "Paleocene"}, |
| 20 | + {"top": 112, "bottom": 140, "lithology": "Shale", "formation": "Green River Fm", "age": "Eocene"}, |
| 21 | + {"top": 140, "bottom": 158, "lithology": "Limestone", "formation": "Leadville Fm", "age": "Eocene"}, |
| 22 | + {"top": 158, "bottom": 180, "lithology": "Sandstone", "formation": "Wasatch Fm", "age": "Eocene"}, |
| 23 | + {"top": 180, "bottom": 200, "lithology": "Siltstone", "formation": "Uinta Fm", "age": "Eocene"}, |
| 24 | +] |
| 25 | + |
| 26 | +# Lithology style mapping: improved colorblind-safe palette |
| 27 | +# Sandstone: warm yellow, Siltstone: olive green (high contrast vs sandstone) |
| 28 | +lithology_styles = { |
| 29 | + "Sandstone": {"color": "#F5DEB3", "hatch_pattern": ".", "hatch_color": "#8B7355"}, |
| 30 | + "Shale": {"color": "#A9A9A9", "hatch_pattern": "-", "hatch_color": "#4A4A4A"}, |
| 31 | + "Limestone": {"color": "#87CEEB", "hatch_pattern": "+", "hatch_color": "#2E5A88"}, |
| 32 | + "Siltstone": {"color": "#7B9971", "hatch_pattern": "/", "hatch_color": "#3B5335"}, |
| 33 | + "Conglomerate": {"color": "#E8923F", "hatch_pattern": "o", "hatch_color": "#6B3A00"}, |
| 34 | +} |
| 35 | + |
| 36 | +# K-Pg boundary depth |
| 37 | +KPG_DEPTH = 88 |
| 38 | + |
| 39 | +# Column geometry — wider for better canvas fill |
| 40 | +col_center = 0.55 |
| 41 | +col_width = 1.1 |
| 42 | + |
| 43 | +# Plot — tighter x_range for better horizontal utilization |
| 44 | +p = figure( |
| 45 | + width=4800, |
| 46 | + height=2700, |
| 47 | + title="column-stratigraphic · bokeh · pyplots.ai", |
| 48 | + y_axis_label="Depth (m)", |
| 49 | + toolbar_location=None, |
| 50 | + x_range=Range1d(-0.55, 1.85), |
| 51 | + y_range=Range1d(210, -10), |
| 52 | +) |
| 53 | + |
| 54 | +# Draw each layer as a rectangle with hatch pattern |
| 55 | +legend_items_dict = {} |
| 56 | +for layer in layers: |
| 57 | + source = ColumnDataSource( |
| 58 | + data={ |
| 59 | + "x": [col_center], |
| 60 | + "y": [(layer["top"] + layer["bottom"]) / 2], |
| 61 | + "width": [col_width], |
| 62 | + "height": [layer["bottom"] - layer["top"]], |
| 63 | + "lithology": [layer["lithology"]], |
| 64 | + "formation": [layer["formation"]], |
| 65 | + "age": [layer["age"]], |
| 66 | + "top_depth": [layer["top"]], |
| 67 | + "bottom_depth": [layer["bottom"]], |
| 68 | + "thickness": [layer["bottom"] - layer["top"]], |
| 69 | + } |
| 70 | + ) |
| 71 | + |
| 72 | + style = lithology_styles[layer["lithology"]] |
| 73 | + renderer = p.rect( |
| 74 | + x="x", |
| 75 | + y="y", |
| 76 | + width="width", |
| 77 | + height="height", |
| 78 | + source=source, |
| 79 | + fill_color=style["color"], |
| 80 | + line_color="#2C2C2C", |
| 81 | + line_width=2, |
| 82 | + hatch_pattern=style["hatch_pattern"], |
| 83 | + hatch_color=style["hatch_color"], |
| 84 | + hatch_alpha=0.7, |
| 85 | + hatch_scale=16, |
| 86 | + hatch_weight=2, |
| 87 | + ) |
| 88 | + |
| 89 | + lith = layer["lithology"] |
| 90 | + if lith not in legend_items_dict: |
| 91 | + legend_items_dict[lith] = renderer |
| 92 | + |
| 93 | + hover = HoverTool( |
| 94 | + renderers=[renderer], |
| 95 | + tooltips=[ |
| 96 | + ("Lithology", "@lithology"), |
| 97 | + ("Formation", "@formation"), |
| 98 | + ("Age", "@age"), |
| 99 | + ("Top", "@top_depth{0.0} m"), |
| 100 | + ("Bottom", "@bottom_depth{0.0} m"), |
| 101 | + ("Thickness", "@thickness{0.0} m"), |
| 102 | + ], |
| 103 | + ) |
| 104 | + p.add_tools(hover) |
| 105 | + |
| 106 | +# Depth tick marks at each layer boundary for polished geological appearance |
| 107 | +boundary_depths = sorted({layer["top"] for layer in layers} | {layer["bottom"] for layer in layers}) |
| 108 | +for depth in boundary_depths: |
| 109 | + x_left = col_center - col_width / 2 |
| 110 | + p.line(x=[x_left - 0.04, x_left], y=[depth, depth], line_color="#555555", line_width=1.5, line_alpha=0.6) |
| 111 | + # Small depth label at boundary |
| 112 | + label = Label( |
| 113 | + x=x_left - 0.06, |
| 114 | + y=depth, |
| 115 | + text=f"{depth:.0f}", |
| 116 | + text_font_size="13pt", |
| 117 | + text_align="right", |
| 118 | + text_baseline="middle", |
| 119 | + text_color="#777777", |
| 120 | + ) |
| 121 | + p.add_layout(label) |
| 122 | + |
| 123 | +# K-Pg boundary emphasis — bold red dashed line with prominent annotation |
| 124 | +kpg_span = Span(location=KPG_DEPTH, dimension="width", line_color="#CC0000", line_width=5, line_dash="dashed") |
| 125 | +p.add_layout(kpg_span) |
| 126 | + |
| 127 | +kpg_label = Label( |
| 128 | + x=col_center, |
| 129 | + y=KPG_DEPTH, |
| 130 | + text="K-Pg Boundary (~66 Ma)", |
| 131 | + text_font_size="20pt", |
| 132 | + text_font_style="bold", |
| 133 | + text_color="#CC0000", |
| 134 | + text_align="center", |
| 135 | + text_baseline="bottom", |
| 136 | + y_offset=10, |
| 137 | + background_fill_color="white", |
| 138 | + background_fill_alpha=0.8, |
| 139 | +) |
| 140 | +p.add_layout(kpg_label) |
| 141 | + |
| 142 | +# Formation labels on the right side — closer to column |
| 143 | +for layer in layers: |
| 144 | + mid_y = (layer["top"] + layer["bottom"]) / 2 |
| 145 | + label = Label( |
| 146 | + x=col_center + col_width / 2 + 0.04, |
| 147 | + y=mid_y, |
| 148 | + text=layer["formation"], |
| 149 | + text_font_size="19pt", |
| 150 | + text_font_style="bold", |
| 151 | + text_align="left", |
| 152 | + text_baseline="middle", |
| 153 | + text_color="#2C2C2C", |
| 154 | + ) |
| 155 | + p.add_layout(label) |
| 156 | + |
| 157 | +# Age labels on the left side with brackets |
| 158 | +age_groups = {} |
| 159 | +for layer in layers: |
| 160 | + age = layer["age"] |
| 161 | + if age not in age_groups: |
| 162 | + age_groups[age] = {"top": layer["top"], "bottom": layer["bottom"]} |
| 163 | + else: |
| 164 | + age_groups[age]["bottom"] = max(age_groups[age]["bottom"], layer["bottom"]) |
| 165 | + age_groups[age]["top"] = min(age_groups[age]["top"], layer["top"]) |
| 166 | + |
| 167 | +bracket_x = -0.12 |
| 168 | +for age, bounds in age_groups.items(): |
| 169 | + mid_y = (bounds["top"] + bounds["bottom"]) / 2 |
| 170 | + label = Label( |
| 171 | + x=bracket_x - 0.04, |
| 172 | + y=mid_y, |
| 173 | + text=age, |
| 174 | + text_font_size="19pt", |
| 175 | + text_align="right", |
| 176 | + text_baseline="middle", |
| 177 | + text_color="#2C2C2C", |
| 178 | + text_font_style="italic", |
| 179 | + ) |
| 180 | + p.add_layout(label) |
| 181 | + |
| 182 | + # Bracket lines |
| 183 | + p.line(x=[bracket_x, bracket_x], y=[bounds["top"] + 1, bounds["bottom"] - 1], line_color="#2C2C2C", line_width=2.5) |
| 184 | + p.line( |
| 185 | + x=[bracket_x - 0.025, bracket_x], y=[bounds["top"] + 1, bounds["top"] + 1], line_color="#2C2C2C", line_width=2.5 |
| 186 | + ) |
| 187 | + p.line( |
| 188 | + x=[bracket_x - 0.025, bracket_x], |
| 189 | + y=[bounds["bottom"] - 1, bounds["bottom"] - 1], |
| 190 | + line_color="#2C2C2C", |
| 191 | + line_width=2.5, |
| 192 | + ) |
| 193 | + |
| 194 | +# Legend — positioned adjacent to column on right side |
| 195 | +legend_items = [LegendItem(label=lith, renderers=[rend]) for lith, rend in legend_items_dict.items()] |
| 196 | +legend = Legend( |
| 197 | + items=legend_items, |
| 198 | + location="top_right", |
| 199 | + label_text_font_size="20pt", |
| 200 | + spacing=14, |
| 201 | + padding=20, |
| 202 | + margin=10, |
| 203 | + background_fill_color="#F5F5F0", |
| 204 | + background_fill_alpha=0.9, |
| 205 | + border_line_color="#CCCCCC", |
| 206 | + border_line_width=1, |
| 207 | + glyph_height=32, |
| 208 | + glyph_width=32, |
| 209 | + title="Lithology", |
| 210 | + title_text_font_size="22pt", |
| 211 | + title_text_font_style="bold", |
| 212 | +) |
| 213 | +p.add_layout(legend, "right") |
| 214 | + |
| 215 | +# Typography hierarchy |
| 216 | +p.title.text_font_size = "36pt" |
| 217 | +p.title.text_color = "#1A1A1A" |
| 218 | +p.title.offset = 10 |
| 219 | +p.yaxis.axis_label_text_font_size = "28pt" |
| 220 | +p.yaxis.axis_label_text_font_style = "bold" |
| 221 | +p.yaxis.axis_label_text_color = "#333333" |
| 222 | +p.yaxis.major_label_text_font_size = "22pt" |
| 223 | +p.yaxis.major_label_text_color = "#444444" |
| 224 | + |
| 225 | +# Visual refinement |
| 226 | +p.xaxis.visible = False |
| 227 | +p.xgrid.grid_line_color = None |
| 228 | +p.ygrid.grid_line_alpha = 0.12 |
| 229 | +p.ygrid.grid_line_dash = [4, 4] |
| 230 | +p.ygrid.grid_line_color = "#999999" |
| 231 | + |
| 232 | +p.yaxis.minor_tick_line_color = None |
| 233 | +p.yaxis.major_tick_line_color = "#AAAAAA" |
| 234 | +p.yaxis.axis_line_color = "#888888" |
| 235 | +p.outline_line_color = None |
| 236 | +p.background_fill_color = "#FAFAF6" |
| 237 | +p.border_fill_color = "#FFFFFF" |
| 238 | + |
| 239 | +# Padding |
| 240 | +p.min_border_left = 100 |
| 241 | +p.min_border_right = 40 |
| 242 | +p.min_border_top = 60 |
| 243 | +p.min_border_bottom = 60 |
| 244 | + |
| 245 | +# Save |
| 246 | +export_png(p, filename="plot.png") |
| 247 | +output_file("plot.html", title="column-stratigraphic · bokeh · pyplots.ai") |
| 248 | +save(p) |
0 commit comments