Skip to content

Commit 0a06462

Browse files
fix(plotnine): address review feedback for stereonet-equal-area
Attempt 2/3 - fixes based on AI review
1 parent 8c8137f commit 0a06462

1 file changed

Lines changed: 64 additions & 51 deletions

File tree

plots/stereonet-equal-area/implementations/plotnine.py

Lines changed: 64 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
1-
""" pyplots.ai
1+
"""pyplots.ai
22
stereonet-equal-area: Structural Geology Stereonet (Equal-Area Projection)
33
Library: plotnine 0.15.3 | Python 3.14.3
44
Quality: 79/100 | Created: 2026-03-15
55
"""
66

77
import matplotlib
8-
import matplotlib.pyplot as plt
98
import numpy as np
109
import pandas as pd
1110
from plotnine import (
1211
aes,
12+
after_stat,
1313
coord_fixed,
1414
element_blank,
1515
element_text,
16+
geom_density_2d,
1617
geom_path,
1718
geom_point,
1819
geom_segment,
1920
geom_text,
2021
ggplot,
22+
guides,
2123
labs,
24+
scale_alpha_continuous,
2225
scale_color_manual,
2326
scale_shape_manual,
2427
scale_x_continuous,
2528
scale_y_continuous,
2629
theme,
2730
)
28-
from scipy.ndimage import gaussian_filter
2931

3032

3133
matplotlib.use("Agg")
@@ -57,7 +59,7 @@
5759
pole_y = pole_r * np.cos(pole_trend)
5860
poles_df = pd.DataFrame({"x": pole_x, "y": pole_y, "feature_type": feature_types})
5961

60-
# Compute great circles (sample representative planes per type)
62+
# Compute great circles for representative planes
6163
gc_rows = []
6264
gc_indices = {"Bedding": [0, 10, 20, 30], "Fault": [40, 48, 56], "Joint": [65, 75, 85]}
6365
gc_id = 0
@@ -72,17 +74,15 @@
7274
dip_vec = np.array(
7375
[np.sin(dip_dir_rad) * np.cos(dip_rad), np.cos(dip_dir_rad) * np.cos(dip_rad), -np.sin(dip_rad)]
7476
)
75-
angles = np.linspace(-np.pi / 2, np.pi / 2, 181)
76-
for a in angles:
77+
for a in np.linspace(-np.pi / 2, np.pi / 2, 181):
7778
pt = np.cos(a) * dip_vec + np.sin(a) * strike_vec
7879
if pt[2] > 0:
7980
pt = -pt
8081
horiz = np.sqrt(pt[0] ** 2 + pt[1] ** 2)
8182
plunge = np.arctan2(-pt[2], horiz)
8283
trend = np.arctan2(pt[0], pt[1])
8384
r = np.sqrt(2) * np.sin((np.pi / 2 - plunge) / 2)
84-
x = r * np.sin(trend)
85-
y = r * np.cos(trend)
85+
x, y = r * np.sin(trend), r * np.cos(trend)
8686
if x**2 + y**2 <= r_prim**2 * 1.01:
8787
gc_rows.append({"x": x, "y": y, "feature_type": ftype, "gc_id": f"{ftype}_{gc_id}"})
8888
gc_id += 1
@@ -93,6 +93,24 @@
9393
circle_angles = np.linspace(0, 2 * np.pi, 361)
9494
prim_df = pd.DataFrame({"x": r_prim * np.cos(circle_angles), "y": r_prim * np.sin(circle_angles), "group": "primitive"})
9595

96+
# Equal-area net grid lines (small circles at 30° dip intervals)
97+
grid_rows = []
98+
for dip_interval in range(30, 90, 30):
99+
plunge_rad = np.radians(90 - dip_interval)
100+
r_circle = np.sqrt(2) * np.sin((np.pi / 2 - plunge_rad) / 2)
101+
for angle in np.linspace(0, 2 * np.pi, 181):
102+
gx = r_circle * np.cos(angle)
103+
gy = r_circle * np.sin(angle)
104+
grid_rows.append({"x": gx, "y": gy, "grid_id": f"dip_{dip_interval}"})
105+
106+
# Radial lines at 30° azimuth intervals
107+
for az in range(0, 360, 30):
108+
az_rad = np.radians(az)
109+
for t in np.linspace(0, r_prim, 50):
110+
grid_rows.append({"x": t * np.sin(az_rad), "y": t * np.cos(az_rad), "grid_id": f"az_{az}"})
111+
112+
grid_df = pd.DataFrame(grid_rows)
113+
96114
# Degree tick marks every 10 degrees
97115
tick_rows = []
98116
for deg in range(0, 360, 10):
@@ -118,68 +136,63 @@
118136
if deg in (0, 90, 180, 270):
119137
continue
120138
rad = np.radians(deg)
121-
offset = r_prim * 1.07
139+
offset = r_prim * 1.08
122140
deg_labels.append({"x": offset * np.sin(rad), "y": offset * np.cos(rad), "label": f"{deg}°"})
123141
deg_label_df = pd.DataFrame(deg_labels)
124142

125-
# Density contours (Kamb-style kernel density on projected pole coordinates)
126-
grid_res = 200
127-
gx_lin = np.linspace(-r_prim, r_prim, grid_res)
128-
gy_lin = np.linspace(-r_prim, r_prim, grid_res)
129-
gx_grid, gy_grid = np.meshgrid(gx_lin, gy_lin)
130-
density = np.zeros_like(gx_grid)
131-
132-
sigma = 0.15
133-
for i in range(len(pole_x)):
134-
dist_sq = (gx_grid - pole_x[i]) ** 2 + (gy_grid - pole_y[i]) ** 2
135-
density += np.exp(-dist_sq / (2 * sigma**2))
136-
137-
density = gaussian_filter(density, sigma=3)
138-
circle_mask = gx_grid**2 + gy_grid**2 > r_prim**2
139-
density[circle_mask] = np.nan
140-
141-
# Extract contour coordinates (using matplotlib for numerical contour extraction only)
142-
fig_tmp, ax_tmp = plt.subplots()
143-
valid_density = density[~np.isnan(density)]
144-
levels = np.linspace(valid_density.max() * 0.2, valid_density.max() * 0.8, 5)
145-
cs = ax_tmp.contour(gx_lin, gy_lin, density, levels=levels)
146-
plt.close(fig_tmp)
147-
148-
contour_rows = []
149-
contour_id = 0
150-
for level_segs in cs.allsegs:
151-
for seg in level_segs:
152-
if len(seg) > 0:
153-
for xi, yi in seg:
154-
if xi**2 + yi**2 <= r_prim**2 * 1.01:
155-
contour_rows.append({"x": xi, "y": yi, "contour_id": contour_id})
156-
contour_id += 1
157-
158-
contour_df = pd.DataFrame(contour_rows) if contour_rows else pd.DataFrame({"x": [], "y": [], "contour_id": []})
159-
160-
# Colors (colorblind-safe: blue, orange, purple)
143+
# Cluster centroid annotations for data storytelling
144+
annotations = []
145+
for ftype in ["Bedding", "Fault", "Joint"]:
146+
mask = poles_df["feature_type"] == ftype
147+
cx, cy = poles_df.loc[mask, "x"].mean(), poles_df.loc[mask, "y"].mean()
148+
mean_strike = strikes[np.array(feature_types) == ftype].mean()
149+
mean_dip = dips[np.array(feature_types) == ftype].mean()
150+
annotations.append({"x": cx, "y": cy - 0.12, "feature_type": ftype, "label": f"{mean_strike:.0f}°/{mean_dip:.0f}°"})
151+
annot_df = pd.DataFrame(annotations)
152+
153+
# Colors (colorblind-safe)
161154
colors = {"Bedding": "#306998", "Fault": "#E5A023", "Joint": "#7B68A0"}
162155
shapes = {"Bedding": "o", "Fault": "D", "Joint": "s"}
163156

164-
# Plot
157+
# Plot - using plotnine's geom_density_2d (stat_density_2d) for Kamb-style contouring
165158
plot = (
166159
ggplot()
160+
# Grid lines (subtle equal-area net)
161+
+ geom_path(aes(x="x", y="y", group="grid_id"), data=grid_df, color="#CCCCCC", size=0.3, alpha=0.5)
162+
# Primitive circle
167163
+ geom_path(aes(x="x", y="y"), data=prim_df, color="#333333", size=1.2)
164+
# Tick marks
168165
+ geom_segment(aes(x="x1", y="y1", xend="x2", yend="y2"), data=tick_df, color="#333333", size=0.5)
169-
+ geom_path(
170-
aes(x="x", y="y", group="contour_id"), data=contour_df, color="#777777", size=0.7, alpha=0.7, linetype="dashed"
166+
# Density contours using plotnine's native stat_density_2d via geom_density_2d
167+
# after_stat maps computed density level to alpha for visual depth
168+
+ geom_density_2d(
169+
aes(x="x", y="y", alpha=after_stat("level")), data=poles_df, color="#666666", size=0.6, linetype="dashed"
171170
)
171+
+ scale_alpha_continuous(range=(0.3, 0.8))
172+
+ guides(alpha=False)
173+
# Great circles
172174
+ geom_path(aes(x="x", y="y", color="feature_type", group="gc_id"), data=gc_df, size=0.9, alpha=0.7)
175+
# Poles to planes
173176
+ geom_point(
174177
aes(x="x", y="y", color="feature_type", shape="feature_type"), data=poles_df, size=3.5, alpha=0.85, stroke=0.5
175178
)
179+
# Cardinal directions
176180
+ geom_text(aes(x="x", y="y", label="label"), data=dir_df, size=18, fontweight="bold", color="#222222")
177-
+ geom_text(aes(x="x", y="y", label="label"), data=deg_label_df, size=10, color="#555555")
181+
# Degree labels (increased size for readability)
182+
+ geom_text(aes(x="x", y="y", label="label"), data=deg_label_df, size=13, color="#444444")
183+
# Cluster orientation annotations
184+
+ geom_text(
185+
aes(x="x", y="y", label="label", color="feature_type"),
186+
data=annot_df,
187+
size=11,
188+
fontstyle="italic",
189+
show_legend=False,
190+
)
178191
+ scale_color_manual(name="Feature Type", values=colors)
179192
+ scale_shape_manual(name="Feature Type", values=shapes)
180193
+ coord_fixed(ratio=1)
181-
+ scale_x_continuous(limits=(-1.8, 1.8))
182-
+ scale_y_continuous(limits=(-1.8, 1.8))
194+
+ scale_x_continuous(limits=(-1.85, 1.85))
195+
+ scale_y_continuous(limits=(-1.85, 1.85))
183196
+ labs(title="stereonet-equal-area · plotnine · pyplots.ai")
184197
+ theme(
185198
figure_size=(12, 12),

0 commit comments

Comments
 (0)