Skip to content

Commit d38b6e9

Browse files
Merge branch 'main' into implementation/stereonet-equal-area/seaborn
2 parents db494a7 + 1e170f9 commit d38b6e9

12 files changed

Lines changed: 2778 additions & 0 deletions

File tree

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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

Comments
 (0)