|
1 | | -""" pyplots.ai |
| 1 | +"""pyplots.ai |
2 | 2 | area-elevation-profile: Terrain Elevation Profile Along Transect |
3 | 3 | Library: letsplot 4.9.0 | Python 3.14.3 |
4 | 4 | Quality: 86/100 | Created: 2026-03-15 |
|
44 | 44 | landmark_distances = [0, 20, 38, 50, 65, 80, 95, 120] |
45 | 45 | landmark_elevations = [float(np.interp(d, distance, elevation)) for d in landmark_distances] |
46 | 46 |
|
47 | | -# Stagger label offsets with larger gaps in crowded 80-100 km region |
48 | | -nudge_offsets = [120, 150, 120, 120, 120, 250, 420, 120] |
| 47 | +# Stagger label offsets — alternate high/low in crowded 50-100 km region |
| 48 | +# Alternating low/high nudge with horizontal shifts to prevent overlap |
| 49 | +nudge_y = [80, 250, 80, 250, 80, 250, 80, 80] |
| 50 | +nudge_x = [0, -2, 2, -3, 0, 0, 0, 0] |
49 | 51 | landmarks_df = pd.DataFrame( |
50 | 52 | { |
51 | 53 | "distance": landmark_distances, |
52 | 54 | "elevation": landmark_elevations, |
53 | 55 | "name": landmark_names, |
54 | | - "label_y": [e + n for e, n in zip(landmark_elevations, nudge_offsets, strict=True)], |
| 56 | + "label_y": [e + n for e, n in zip(landmark_elevations, nudge_y, strict=True)], |
| 57 | + "label_x": [d + n for d, n in zip(landmark_distances, nudge_x, strict=True)], |
55 | 58 | } |
56 | 59 | ) |
57 | 60 |
|
58 | | -# Compute slope for gradient coloring |
| 61 | +# Compute slope for gradient coloring — smoothed to reduce visual fragmentation |
59 | 62 | slope = np.gradient(elevation, distance) |
60 | 63 | slope_abs = np.abs(slope) |
61 | | -slope_category = pd.cut(slope_abs, bins=[0, 15, 40, np.inf], labels=["Flat/Gentle", "Moderate", "Steep"]) |
| 64 | +# Rolling average to prevent rapid color switching on steep transitions |
| 65 | +slope_smooth = pd.Series(slope_abs).rolling(window=25, center=True, min_periods=1).mean() |
| 66 | +slope_category = pd.cut(slope_smooth, bins=[0, 15, 40, np.inf], labels=["Flat/Gentle", "Moderate", "Steep"]) |
62 | 67 | df["slope_category"] = slope_category |
63 | 68 |
|
64 | 69 | # Segment data for vertical landmark lines (using geom_segment) |
|
115 | 120 | .line("Elevation: @elevation m") |
116 | 121 | .line("Distance: @distance km"), |
117 | 122 | ) |
118 | | - # Landmark labels — larger font with better staggering to prevent overlap |
| 123 | + # Connector lines from labels to landmark points |
| 124 | + + geom_segment( # noqa: F405 |
| 125 | + data=landmarks_df, |
| 126 | + mapping=aes(x="distance", y="elevation", xend="label_x", yend="label_y"), # noqa: F405 |
| 127 | + color="#BBBBBB", |
| 128 | + size=0.4, |
| 129 | + linetype="dotted", |
| 130 | + inherit_aes=False, |
| 131 | + ) |
| 132 | + # Landmark labels — sized to match other text elements, well-staggered |
119 | 133 | + geom_text( # noqa: F405 |
120 | 134 | data=landmarks_df, |
121 | | - mapping=aes(x="distance", y="label_y", label="name"), # noqa: F405 |
122 | | - size=12, |
| 135 | + mapping=aes(x="label_x", y="label_y", label="name"), # noqa: F405 |
| 136 | + size=14, |
123 | 137 | color="#2C3E50", |
124 | | - angle=35, |
| 138 | + angle=40, |
125 | 139 | hjust=0, |
126 | 140 | fontface="bold", |
127 | 141 | inherit_aes=False, |
128 | 142 | ) |
129 | 143 | # Scales and labels |
130 | 144 | + scale_x_continuous( # noqa: F405 |
131 | | - name="Distance (km)", breaks=list(range(0, 121, 20)), limits=[-2, 138] |
| 145 | + name="Distance (km)", breaks=list(range(0, 121, 20)), limits=[-2, 150] |
132 | 146 | ) |
133 | 147 | + scale_y_continuous( # noqa: F405 |
134 | | - name="Elevation (m)", limits=[y_floor, y_max], breaks=list(range(1000, y_max, 200)) |
| 148 | + name="Elevation (m)", limits=[y_floor, y_max + 200], breaks=list(range(1000, y_max + 200, 200)) |
135 | 149 | ) |
136 | 150 | + labs( # noqa: F405 |
137 | 151 | title="Alpine Trail Elevation Profile · area-elevation-profile · letsplot · pyplots.ai", |
|
0 commit comments