Skip to content

Commit f86803f

Browse files
feat(altair): implement column-stratigraphic
1 parent 4a54060 commit f86803f

1 file changed

Lines changed: 186 additions & 0 deletions

File tree

  • plots/column-stratigraphic/implementations
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
"""pyplots.ai
2+
column-stratigraphic: Stratigraphic Column with Lithology Patterns
3+
Library: altair | Python 3.13
4+
Quality: pending | Created: 2026-03-15
5+
"""
6+
7+
import altair as alt
8+
import pandas as pd
9+
10+
11+
# Data: Synthetic sedimentary section with 10 layers
12+
layers = pd.DataFrame(
13+
{
14+
"top": [0, 15, 35, 55, 70, 90, 110, 135, 155, 175],
15+
"bottom": [15, 35, 55, 70, 90, 110, 135, 155, 175, 200],
16+
"lithology": [
17+
"Sandstone",
18+
"Shale",
19+
"Limestone",
20+
"Siltstone",
21+
"Sandstone",
22+
"Conglomerate",
23+
"Shale",
24+
"Limestone",
25+
"Siltstone",
26+
"Sandstone",
27+
],
28+
"formation": [
29+
"Cedar Mesa Fm",
30+
"Organ Rock Fm",
31+
"White Rim Fm",
32+
"De Chelly Fm",
33+
"Coconino Fm",
34+
"Hermit Fm",
35+
"Supai Group",
36+
"Redwall Fm",
37+
"Temple Butte Fm",
38+
"Muav Fm",
39+
],
40+
"age": [
41+
"Permian",
42+
"Permian",
43+
"Permian",
44+
"Permian",
45+
"Permian",
46+
"Permian",
47+
"Pennsylvanian",
48+
"Mississippian",
49+
"Devonian",
50+
"Cambrian",
51+
],
52+
}
53+
)
54+
55+
layers["thickness"] = layers["bottom"] - layers["top"]
56+
layers["mid_depth"] = (layers["top"] + layers["bottom"]) / 2
57+
58+
# Lithology color palette (geologically conventional)
59+
lithology_colors = {
60+
"Sandstone": "#F5D76E",
61+
"Shale": "#7B8D8E",
62+
"Limestone": "#5DADE2",
63+
"Siltstone": "#A9CCE3",
64+
"Conglomerate": "#E67E22",
65+
}
66+
67+
lithology_order = ["Sandstone", "Shale", "Limestone", "Siltstone", "Conglomerate"]
68+
69+
# Lithology pattern symbols for overlay
70+
pattern_map = {
71+
"Sandstone": "· · ·",
72+
"Shale": "— — —",
73+
"Limestone": "▦ ▦ ▦",
74+
"Siltstone": "– – –",
75+
"Conglomerate": "○ ○ ○",
76+
}
77+
layers["pattern_label"] = layers["lithology"].map(pattern_map)
78+
79+
# Identify unique age boundaries for left-side labels
80+
age_groups = []
81+
current_age = None
82+
for _, row in layers.iterrows():
83+
if row["age"] != current_age:
84+
current_age = row["age"]
85+
group_rows = layers[layers["age"] == current_age]
86+
age_groups.append(
87+
{
88+
"age": current_age,
89+
"top": group_rows["top"].min(),
90+
"bottom": group_rows["bottom"].max(),
91+
"mid_depth": (group_rows["top"].min() + group_rows["bottom"].max()) / 2,
92+
}
93+
)
94+
age_df = pd.DataFrame(age_groups)
95+
96+
# Layer rectangles
97+
rects = (
98+
alt.Chart(layers)
99+
.mark_rect(stroke="#333333", strokeWidth=2)
100+
.encode(
101+
y=alt.Y(
102+
"top:Q",
103+
title="Depth (m)",
104+
scale=alt.Scale(domain=[0, 200]),
105+
axis=alt.Axis(labelFontSize=18, titleFontSize=22),
106+
),
107+
y2="bottom:Q",
108+
x=alt.X("x:Q", scale=alt.Scale(domain=[0, 14]), axis=None),
109+
x2="x2:Q",
110+
color=alt.Color(
111+
"lithology:N",
112+
title="Lithology",
113+
scale=alt.Scale(domain=lithology_order, range=[lithology_colors[k] for k in lithology_order]),
114+
legend=alt.Legend(
115+
titleFontSize=20,
116+
labelFontSize=18,
117+
symbolSize=400,
118+
orient="bottom",
119+
titlePadding=10,
120+
direction="horizontal",
121+
labelLimit=200,
122+
),
123+
),
124+
tooltip=[
125+
alt.Tooltip("formation:N", title="Formation"),
126+
alt.Tooltip("lithology:N", title="Lithology"),
127+
alt.Tooltip("age:N", title="Age"),
128+
alt.Tooltip("top:Q", title="Top (m)"),
129+
alt.Tooltip("bottom:Q", title="Bottom (m)"),
130+
alt.Tooltip("thickness:Q", title="Thickness (m)"),
131+
],
132+
)
133+
.transform_calculate(x="2.5", x2="7.5")
134+
)
135+
136+
# Pattern texture labels inside each layer
137+
pattern_text = (
138+
alt.Chart(layers)
139+
.mark_text(fontSize=16, color="#555555", opacity=0.6)
140+
.encode(y=alt.Y("mid_depth:Q"), x=alt.X("x_mid:Q", scale=alt.Scale(domain=[0, 14])), text="pattern_label:N")
141+
.transform_calculate(x_mid="5")
142+
)
143+
144+
# Formation name labels to the right
145+
formation_labels = (
146+
alt.Chart(layers)
147+
.mark_text(fontSize=16, fontWeight="bold", align="left", color="#1a1a1a")
148+
.encode(y=alt.Y("mid_depth:Q"), x=alt.X("x_pos:Q", scale=alt.Scale(domain=[0, 14])), text="formation:N")
149+
.transform_calculate(x_pos="7.8")
150+
)
151+
152+
# Age labels to the left
153+
age_labels = (
154+
alt.Chart(age_df)
155+
.mark_text(fontSize=15, fontStyle="italic", align="right", color="#444444")
156+
.encode(y=alt.Y("mid_depth:Q"), x=alt.X("x_pos:Q", scale=alt.Scale(domain=[0, 14])), text="age:N")
157+
.transform_calculate(x_pos="1.7")
158+
)
159+
160+
# Age boundary lines
161+
age_boundaries = age_df[age_df["top"] > 0][["top"]].copy()
162+
age_boundaries["x1"] = 2.5
163+
age_boundaries["x2"] = 7.5
164+
165+
age_rules = (
166+
alt.Chart(age_boundaries)
167+
.mark_rule(strokeDash=[8, 4], strokeWidth=1.5, color="#666666")
168+
.encode(y=alt.Y("top:Q"), x=alt.X("x1:Q", scale=alt.Scale(domain=[0, 14])), x2="x2:Q")
169+
)
170+
171+
# Combine all layers
172+
chart = (
173+
(rects + pattern_text + formation_labels + age_labels + age_rules)
174+
.properties(
175+
width=1200,
176+
height=900,
177+
title=alt.Title(
178+
"Stratigraphic Column · column-stratigraphic · altair · pyplots.ai", fontSize=26, anchor="middle", offset=20
179+
),
180+
)
181+
.configure_view(strokeWidth=0)
182+
)
183+
184+
# Save
185+
chart.save("plot.png", scale_factor=3.0)
186+
chart.save("plot.html")

0 commit comments

Comments
 (0)