Skip to content

Commit 1e3b414

Browse files
feat(bokeh): implement column-stratigraphic
1 parent 4a54060 commit 1e3b414

1 file changed

Lines changed: 194 additions & 0 deletions

File tree

  • plots/column-stratigraphic/implementations
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
"""pyplots.ai
2+
column-stratigraphic: Stratigraphic Column with Lithology Patterns
3+
Library: bokeh | Python 3.13
4+
Quality: pending | Created: 2026-03-15
5+
"""
6+
7+
from bokeh.io import export_png
8+
from bokeh.models import ColumnDataSource, HoverTool, Label, Legend, LegendItem, Range1d
9+
from bokeh.plotting import figure, output_file, save
10+
11+
12+
# Data: Synthetic sedimentary section with 10 layers
13+
layers = [
14+
{"top": 0, "bottom": 15, "lithology": "Sandstone", "formation": "Dakota Fm", "age": "Late Cretaceous"},
15+
{"top": 15, "bottom": 35, "lithology": "Shale", "formation": "Mancos Fm", "age": "Late Cretaceous"},
16+
{"top": 35, "bottom": 50, "lithology": "Limestone", "formation": "Niobrara Fm", "age": "Late Cretaceous"},
17+
{"top": 50, "bottom": 65, "lithology": "Siltstone", "formation": "Pierre Fm", "age": "Late Cretaceous"},
18+
{"top": 65, "bottom": 90, "lithology": "Sandstone", "formation": "Fox Hills Fm", "age": "Late Cretaceous"},
19+
{"top": 90, "bottom": 110, "lithology": "Conglomerate", "formation": "Dawson Fm", "age": "Paleocene"},
20+
{"top": 110, "bottom": 135, "lithology": "Shale", "formation": "Green River Fm", "age": "Eocene"},
21+
{"top": 135, "bottom": 155, "lithology": "Limestone", "formation": "Leadville Fm", "age": "Eocene"},
22+
{"top": 155, "bottom": 175, "lithology": "Sandstone", "formation": "Wasatch Fm", "age": "Eocene"},
23+
{"top": 175, "bottom": 200, "lithology": "Siltstone", "formation": "Uinta Fm", "age": "Eocene"},
24+
]
25+
26+
# Lithology style mapping: color, hatch_pattern
27+
lithology_styles = {
28+
"Sandstone": {"color": "#F5DEB3", "hatch_pattern": ".", "hatch_color": "#8B7355"},
29+
"Shale": {"color": "#A9A9A9", "hatch_pattern": "-", "hatch_color": "#4A4A4A"},
30+
"Limestone": {"color": "#87CEEB", "hatch_pattern": "+", "hatch_color": "#2E5A88"},
31+
"Siltstone": {"color": "#C4B7A6", "hatch_pattern": "/", "hatch_color": "#6B5B4A"},
32+
"Conglomerate": {"color": "#D2691E", "hatch_pattern": "o", "hatch_color": "#5C2E00"},
33+
}
34+
35+
# Build data arrays
36+
tops = [layer["top"] for layer in layers]
37+
bottoms = [layer["bottom"] for layer in layers]
38+
lithologies = [layer["lithology"] for layer in layers]
39+
formations = [layer["formation"] for layer in layers]
40+
ages = [layer["age"] for layer in layers]
41+
thicknesses = [layer["bottom"] - layer["top"] for layer in layers]
42+
43+
fill_colors = [lithology_styles[lith]["color"] for lith in lithologies]
44+
hatch_patterns = [lithology_styles[lith]["hatch_pattern"] for lith in lithologies]
45+
hatch_colors = [lithology_styles[lith]["hatch_color"] for lith in lithologies]
46+
47+
# Column x-position and width
48+
col_left = 0.0
49+
col_right = 1.0
50+
col_center = 0.5
51+
col_width = 1.0
52+
53+
# Plot
54+
p = figure(
55+
width=4800,
56+
height=2700,
57+
title="column-stratigraphic · bokeh · pyplots.ai",
58+
y_axis_label="Depth (m)",
59+
toolbar_location=None,
60+
x_range=Range1d(-0.8, 2.5),
61+
y_range=Range1d(210, -10),
62+
)
63+
64+
# Draw each layer as a rectangle with hatch pattern
65+
legend_items_dict = {}
66+
for layer in layers:
67+
source = ColumnDataSource(
68+
data={
69+
"x": [col_center],
70+
"y": [(layer["top"] + layer["bottom"]) / 2],
71+
"width": [col_width],
72+
"height": [layer["bottom"] - layer["top"]],
73+
"lithology": [layer["lithology"]],
74+
"formation": [layer["formation"]],
75+
"age": [layer["age"]],
76+
"top_depth": [layer["top"]],
77+
"bottom_depth": [layer["bottom"]],
78+
"thickness": [layer["bottom"] - layer["top"]],
79+
}
80+
)
81+
82+
style = lithology_styles[layer["lithology"]]
83+
renderer = p.rect(
84+
x="x",
85+
y="y",
86+
width="width",
87+
height="height",
88+
source=source,
89+
fill_color=style["color"],
90+
line_color="#2C2C2C",
91+
line_width=2,
92+
hatch_pattern=style["hatch_pattern"],
93+
hatch_color=style["hatch_color"],
94+
hatch_alpha=0.7,
95+
hatch_scale=16,
96+
hatch_weight=2,
97+
)
98+
99+
# Track renderers for legend (one entry per lithology)
100+
lith = layer["lithology"]
101+
if lith not in legend_items_dict:
102+
legend_items_dict[lith] = renderer
103+
104+
# Hover tool per layer
105+
hover = HoverTool(
106+
renderers=[renderer],
107+
tooltips=[
108+
("Lithology", "@lithology"),
109+
("Formation", "@formation"),
110+
("Age", "@age"),
111+
("Top", "@top_depth{0.0} m"),
112+
("Bottom", "@bottom_depth{0.0} m"),
113+
("Thickness", "@thickness{0.0} m"),
114+
],
115+
)
116+
p.add_tools(hover)
117+
118+
# Formation labels on the right side
119+
for layer in layers:
120+
mid_y = (layer["top"] + layer["bottom"]) / 2
121+
label = Label(
122+
x=col_right + 0.08,
123+
y=mid_y,
124+
text=layer["formation"],
125+
text_font_size="18pt",
126+
text_align="left",
127+
text_baseline="middle",
128+
text_color="#2C2C2C",
129+
)
130+
p.add_layout(label)
131+
132+
# Age labels on the left side
133+
age_groups = {}
134+
for layer in layers:
135+
age = layer["age"]
136+
if age not in age_groups:
137+
age_groups[age] = {"top": layer["top"], "bottom": layer["bottom"]}
138+
else:
139+
age_groups[age]["bottom"] = max(age_groups[age]["bottom"], layer["bottom"])
140+
age_groups[age]["top"] = min(age_groups[age]["top"], layer["top"])
141+
142+
for age, bounds in age_groups.items():
143+
mid_y = (bounds["top"] + bounds["bottom"]) / 2
144+
label = Label(
145+
x=-0.08,
146+
y=mid_y,
147+
text=age,
148+
text_font_size="18pt",
149+
text_align="right",
150+
text_baseline="middle",
151+
text_color="#2C2C2C",
152+
text_font_style="italic",
153+
)
154+
p.add_layout(label)
155+
156+
# Draw bracket line for age span
157+
p.line(x=[-0.05, -0.05], y=[bounds["top"], bounds["bottom"]], line_color="#2C2C2C", line_width=2)
158+
p.line(x=[-0.07, -0.05], y=[bounds["top"], bounds["top"]], line_color="#2C2C2C", line_width=2)
159+
p.line(x=[-0.07, -0.05], y=[bounds["bottom"], bounds["bottom"]], line_color="#2C2C2C", line_width=2)
160+
161+
# Legend
162+
legend_items = [LegendItem(label=lith, renderers=[rend]) for lith, rend in legend_items_dict.items()]
163+
legend = Legend(
164+
items=legend_items,
165+
location="bottom_right",
166+
label_text_font_size="20pt",
167+
spacing=12,
168+
padding=20,
169+
background_fill_alpha=0.85,
170+
glyph_height=30,
171+
glyph_width=30,
172+
title="Lithology",
173+
title_text_font_size="22pt",
174+
title_text_font_style="bold",
175+
)
176+
p.add_layout(legend)
177+
178+
# Style
179+
p.title.text_font_size = "36pt"
180+
p.yaxis.axis_label_text_font_size = "28pt"
181+
p.yaxis.major_label_text_font_size = "22pt"
182+
183+
p.xaxis.visible = False
184+
p.xgrid.grid_line_color = None
185+
p.ygrid.grid_line_alpha = 0.15
186+
p.ygrid.grid_line_dash = "dashed"
187+
188+
p.yaxis.minor_tick_line_color = None
189+
p.outline_line_color = None
190+
191+
# Save
192+
export_png(p, filename="plot.png")
193+
output_file("plot.html", title="column-stratigraphic · bokeh · pyplots.ai")
194+
save(p)

0 commit comments

Comments
 (0)