Skip to content

Commit 045a81b

Browse files
feat(highcharts): implement area-elevation-profile (#4902)
## Implementation: `area-elevation-profile` - highcharts Implements the **highcharts** version of `area-elevation-profile`. **File:** `plots/area-elevation-profile/implementations/highcharts.py` **Parent Issue:** #4578 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23120069336)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent a7369f6 commit 045a81b

2 files changed

Lines changed: 488 additions & 0 deletions

File tree

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
""" pyplots.ai
2+
area-elevation-profile: Terrain Elevation Profile Along Transect
3+
Library: highcharts unknown | Python 3.14.3
4+
Quality: 91/100 | Created: 2026-03-15
5+
"""
6+
7+
import tempfile
8+
import time
9+
import urllib.request
10+
from pathlib import Path
11+
12+
import numpy as np
13+
from highcharts_core.chart import Chart
14+
from highcharts_core.options import HighchartsOptions
15+
from highcharts_core.options.series.area import AreaSeries
16+
from selenium import webdriver
17+
from selenium.webdriver.chrome.options import Options
18+
19+
20+
# Data - Alpine hiking trail elevation profile (~120 km)
21+
np.random.seed(42)
22+
n_points = 200
23+
distance = np.linspace(0, 120, n_points)
24+
25+
# Build realistic terrain with multiple peaks and valleys
26+
base = 1200
27+
terrain = np.zeros(n_points)
28+
peaks = [
29+
(15, 2450, 8), # First summit
30+
(35, 1850, 10), # Ridge
31+
(55, 2680, 7), # Main summit
32+
(75, 1600, 12), # Valley/pass
33+
(95, 2320, 9), # Second summit
34+
(110, 1400, 10), # Descent
35+
]
36+
for center, height, width in peaks:
37+
terrain += (height - base) * np.exp(-0.5 * ((distance - center) / width) ** 2)
38+
39+
terrain += base
40+
noise = np.random.normal(0, 30, n_points)
41+
terrain += noise
42+
terrain = np.clip(terrain, 800, 3000)
43+
elevation = terrain
44+
45+
# Landmarks along the trail
46+
landmarks = [
47+
(0, elevation[0], "Grindelwald (Start)"),
48+
(15, elevation[np.argmin(np.abs(distance - 15))], "Faulhorn Summit"),
49+
(35, elevation[np.argmin(np.abs(distance - 35))], "Schynige Platte"),
50+
(55, elevation[np.argmin(np.abs(distance - 55))], "Jungfraujoch Pass"),
51+
(75, elevation[np.argmin(np.abs(distance - 75))], "Kleine Scheidegg"),
52+
(95, elevation[np.argmin(np.abs(distance - 95))], "Männlichen Peak"),
53+
(120, elevation[-1], "Lauterbrunnen (End)"),
54+
]
55+
56+
# Create chart
57+
chart = Chart(container="container")
58+
chart.options = HighchartsOptions()
59+
60+
chart.options.chart = {
61+
"type": "area",
62+
"width": 4800,
63+
"height": 2700,
64+
"backgroundColor": "#ffffff",
65+
"marginBottom": 230,
66+
"marginLeft": 250,
67+
"marginRight": 320,
68+
"marginTop": 300,
69+
}
70+
71+
chart.options.title = {
72+
"text": "area-elevation-profile · highcharts · pyplots.ai",
73+
"style": {"fontSize": "60px", "fontWeight": "bold"},
74+
}
75+
76+
chart.options.subtitle = {
77+
"text": "Alpine Trail Bernese Oberland — 120 km Hiking Route · Grindelwald to Lauterbrunnen · Vertical Exaggeration ~10×",
78+
"style": {"fontSize": "38px", "color": "#666666"},
79+
}
80+
81+
# X-axis
82+
chart.options.x_axis = {
83+
"title": {"text": "Distance (km)", "style": {"fontSize": "44px"}, "margin": 25},
84+
"labels": {"style": {"fontSize": "34px"}, "format": "{value} km"},
85+
"gridLineWidth": 1,
86+
"gridLineColor": "rgba(0, 0, 0, 0.06)",
87+
"tickInterval": 10,
88+
"min": 0,
89+
"max": 120,
90+
"crosshair": {"width": 2, "color": "rgba(48, 105, 152, 0.3)", "dashStyle": "Dash"},
91+
"plotLines": [
92+
{
93+
"value": lm[0],
94+
"color": "rgba(100, 100, 100, 0.35)",
95+
"width": 2,
96+
"dashStyle": "Dot",
97+
"zIndex": 4,
98+
"label": {
99+
"text": f"{lm[2]}<br/>({lm[1]:.0f} m)",
100+
"rotation": 0,
101+
"verticalAlign": "bottom",
102+
"y": -25,
103+
"x": 20 if i == 0 else (-20 if i == len(landmarks) - 1 else 0),
104+
"textAlign": "left" if i == 0 else ("right" if i == len(landmarks) - 1 else "center"),
105+
"useHTML": True,
106+
"style": {
107+
"fontSize": "28px",
108+
"color": "#333333",
109+
"fontWeight": "bold",
110+
"textAlign": "left" if i == 0 else ("right" if i == len(landmarks) - 1 else "center"),
111+
},
112+
},
113+
}
114+
for i, lm in enumerate(landmarks)
115+
],
116+
}
117+
118+
# Y-axis
119+
chart.options.y_axis = {
120+
"title": {"text": "Elevation (m)", "style": {"fontSize": "44px"}},
121+
"labels": {"style": {"fontSize": "34px"}, "format": "{value} m"},
122+
"gridLineWidth": 1,
123+
"gridLineColor": "rgba(0, 0, 0, 0.06)",
124+
"min": 1000,
125+
"max": 3200,
126+
"startOnTick": False,
127+
"endOnTick": False,
128+
"plotBands": [
129+
{
130+
"from": 1000,
131+
"to": 1500,
132+
"color": "rgba(139, 195, 74, 0.08)",
133+
"label": {
134+
"text": "Valley",
135+
"align": "left",
136+
"x": 15,
137+
"style": {"fontSize": "28px", "color": "rgba(100, 130, 60, 0.5)", "fontStyle": "italic"},
138+
},
139+
},
140+
{
141+
"from": 1500,
142+
"to": 2500,
143+
"color": "rgba(255, 193, 7, 0.06)",
144+
"label": {
145+
"text": "Alpine",
146+
"align": "left",
147+
"x": 15,
148+
"style": {"fontSize": "28px", "color": "rgba(180, 140, 20, 0.5)", "fontStyle": "italic"},
149+
},
150+
},
151+
{
152+
"from": 2500,
153+
"to": 3200,
154+
"color": "rgba(156, 204, 232, 0.08)",
155+
"label": {
156+
"text": "High Alpine",
157+
"align": "left",
158+
"x": 15,
159+
"style": {"fontSize": "28px", "color": "rgba(80, 130, 170, 0.5)", "fontStyle": "italic"},
160+
},
161+
},
162+
],
163+
}
164+
165+
# Plot options
166+
chart.options.plot_options = {
167+
"area": {
168+
"fillColor": {
169+
"linearGradient": {"x1": 0, "y1": 0, "x2": 0, "y2": 1},
170+
"stops": [
171+
[0, "rgba(48, 105, 152, 0.55)"],
172+
[0.5, "rgba(48, 105, 152, 0.25)"],
173+
[1, "rgba(48, 105, 152, 0.03)"],
174+
],
175+
},
176+
"lineWidth": 5,
177+
"color": "#306998",
178+
"marker": {"enabled": False},
179+
"tooltip": {"headerFormat": "", "pointFormat": "<b>{point.x:.1f} km</b> — Elevation: <b>{point.y:.0f} m</b>"},
180+
"states": {"hover": {"lineWidthPlus": 1}},
181+
"threshold": 1000,
182+
}
183+
}
184+
185+
chart.options.legend = {"enabled": False}
186+
chart.options.credits = {"enabled": False}
187+
188+
chart.options.tooltip = {
189+
"style": {"fontSize": "28px"},
190+
"backgroundColor": "rgba(255, 255, 255, 0.95)",
191+
"borderColor": "#306998",
192+
"borderRadius": 8,
193+
}
194+
195+
# Elevation profile series
196+
area_series = AreaSeries()
197+
area_series.data = [[round(float(d), 2), round(float(e), 1)] for d, e in zip(distance, elevation, strict=True)]
198+
area_series.name = "Elevation"
199+
chart.add_series(area_series)
200+
201+
# Load Highcharts JS for inline embedding
202+
html_str = chart.to_js_literal()
203+
204+
highcharts_js_path = Path(__file__).resolve().parents[3] / "node_modules" / "highcharts" / "highcharts.js"
205+
if highcharts_js_path.exists():
206+
highcharts_js = highcharts_js_path.read_text(encoding="utf-8")
207+
else:
208+
highcharts_url = "https://code.highcharts.com/highcharts.js"
209+
req = urllib.request.Request(highcharts_url, headers={"User-Agent": "Mozilla/5.0"})
210+
with urllib.request.urlopen(req, timeout=30) as response:
211+
highcharts_js = response.read().decode("utf-8")
212+
213+
html_content = f"""<!DOCTYPE html>
214+
<html>
215+
<head>
216+
<meta charset="utf-8">
217+
<script>{highcharts_js}</script>
218+
</head>
219+
<body style="margin:0;">
220+
<div id="container" style="width: 4800px; height: 2700px;"></div>
221+
<script>{html_str}</script>
222+
</body>
223+
</html>"""
224+
225+
# Screenshot with headless Chrome
226+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
227+
f.write(html_content)
228+
temp_path = f.name
229+
230+
chrome_options = Options()
231+
chrome_options.add_argument("--headless")
232+
chrome_options.add_argument("--no-sandbox")
233+
chrome_options.add_argument("--disable-dev-shm-usage")
234+
chrome_options.add_argument("--disable-gpu")
235+
chrome_options.add_argument("--window-size=4800,2700")
236+
237+
driver = webdriver.Chrome(options=chrome_options)
238+
driver.get(f"file://{temp_path}")
239+
time.sleep(5)
240+
241+
container = driver.find_element("id", "container")
242+
container.screenshot("plot.png")
243+
driver.quit()
244+
245+
Path(temp_path).unlink()
246+
247+
# Save interactive HTML
248+
with open("plot.html", "w", encoding="utf-8") as f:
249+
interactive_html = f"""<!DOCTYPE html>
250+
<html>
251+
<head>
252+
<meta charset="utf-8">
253+
<script src="https://code.highcharts.com/highcharts.js"></script>
254+
</head>
255+
<body style="margin:0;">
256+
<div id="container" style="width: 100%; height: 100vh;"></div>
257+
<script>{html_str}</script>
258+
</body>
259+
</html>"""
260+
f.write(interactive_html)

0 commit comments

Comments
 (0)