Skip to content

Commit cfe9eb9

Browse files
feat(highcharts): implement area-stacked-confidence (#3569)
## Implementation: `area-stacked-confidence` - highcharts Implements the **highcharts** version of `area-stacked-confidence`. **File:** `plots/area-stacked-confidence/implementations/highcharts.py` **Parent Issue:** #3549 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20866600918)* --------- 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 24d8a6f commit cfe9eb9

2 files changed

Lines changed: 490 additions & 0 deletions

File tree

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
""" pyplots.ai
2+
area-stacked-confidence: Stacked Area Chart with Confidence Bands
3+
Library: highcharts unknown | Python 3.13.11
4+
Quality: 91/100 | Created: 2026-01-09
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 selenium import webdriver
16+
from selenium.webdriver.chrome.options import Options
17+
18+
19+
# Data: Quarterly energy consumption by source with uncertainty
20+
np.random.seed(42)
21+
quarters = ["Q1 2023", "Q2 2023", "Q3 2023", "Q4 2023", "Q1 2024", "Q2 2024", "Q3 2024", "Q4 2024"]
22+
n_points = len(quarters)
23+
24+
# Three energy sources with realistic seasonal patterns
25+
# Solar: higher in summer, lower in winter
26+
solar_base = np.array([15, 25, 30, 18, 17, 28, 32, 20])
27+
solar_lower = solar_base - np.random.uniform(3, 6, n_points)
28+
solar_upper = solar_base + np.random.uniform(3, 6, n_points)
29+
30+
# Wind: more variable, higher in winter
31+
wind_base = np.array([28, 22, 18, 32, 30, 24, 20, 35])
32+
wind_lower = wind_base - np.random.uniform(4, 8, n_points)
33+
wind_upper = wind_base + np.random.uniform(4, 8, n_points)
34+
35+
# Hydro: relatively stable with seasonal variation
36+
hydro_base = np.array([40, 45, 35, 38, 42, 48, 37, 40])
37+
hydro_lower = hydro_base - np.random.uniform(5, 10, n_points)
38+
hydro_upper = hydro_base + np.random.uniform(5, 10, n_points)
39+
40+
# Colors for each series
41+
colors = {"Solar": "#FFD43B", "Wind": "#306998", "Hydro": "#17BECF"}
42+
43+
# Create chart
44+
chart = Chart(container="container")
45+
chart.options = HighchartsOptions()
46+
47+
# Chart configuration
48+
chart.options.chart = {
49+
"type": "areaspline",
50+
"width": 4800,
51+
"height": 2700,
52+
"backgroundColor": "#ffffff",
53+
"marginBottom": 250,
54+
"marginTop": 120,
55+
"spacingBottom": 80,
56+
}
57+
58+
# Title
59+
chart.options.title = {
60+
"text": "area-stacked-confidence · highcharts · pyplots.ai",
61+
"style": {"fontSize": "48px", "fontWeight": "bold"},
62+
}
63+
64+
chart.options.subtitle = {
65+
"text": "Renewable Energy Consumption by Source (GWh) with 90% Confidence Bands",
66+
"style": {"fontSize": "32px"},
67+
}
68+
69+
# X-axis
70+
chart.options.x_axis = {
71+
"categories": quarters,
72+
"title": {"text": "Quarter", "style": {"fontSize": "32px"}},
73+
"labels": {"style": {"fontSize": "24px"}},
74+
"crosshair": True,
75+
}
76+
77+
# Y-axis
78+
chart.options.y_axis = {
79+
"title": {"text": "Energy Consumption (GWh)", "style": {"fontSize": "32px"}},
80+
"labels": {"style": {"fontSize": "24px"}},
81+
"gridLineWidth": 1,
82+
"gridLineColor": "#e0e0e0",
83+
"min": 0,
84+
}
85+
86+
# Legend - positioned in top right corner to avoid bottom clipping
87+
chart.options.legend = {
88+
"enabled": True,
89+
"itemStyle": {"fontSize": "32px"},
90+
"layout": "vertical",
91+
"align": "right",
92+
"verticalAlign": "top",
93+
"x": -50,
94+
"y": 100,
95+
"floating": True,
96+
"backgroundColor": "rgba(255, 255, 255, 0.9)",
97+
"borderWidth": 1,
98+
"borderColor": "#e0e0e0",
99+
"padding": 15,
100+
}
101+
102+
# Tooltip
103+
chart.options.tooltip = {
104+
"shared": True,
105+
"style": {"fontSize": "20px"},
106+
"headerFormat": "<b>{point.key}</b><br/>",
107+
"pointFormat": "{series.name}: {point.y:.1f} GWh<br/>",
108+
}
109+
110+
# Plot options for stacking
111+
chart.options.plot_options = {
112+
"areaspline": {
113+
"stacking": "normal",
114+
"lineWidth": 4,
115+
"marker": {"enabled": True, "radius": 8, "lineWidth": 2, "lineColor": "#ffffff"},
116+
"fillOpacity": 0.7,
117+
},
118+
"arearange": {
119+
"lineWidth": 0,
120+
"fillOpacity": 0.25,
121+
"marker": {"enabled": False},
122+
"enableMouseTracking": False,
123+
"linkedTo": ":previous",
124+
},
125+
}
126+
127+
# Build series data - stacked areas with confidence bands
128+
# For proper stacking with confidence bands, we need to calculate cumulative values
129+
# The bands should show the range around each stacked layer
130+
131+
series_data = []
132+
133+
# Calculate cumulative bases for stacking
134+
solar_cumulative = solar_base.copy()
135+
wind_cumulative = solar_base + wind_base
136+
hydro_cumulative = solar_base + wind_base + hydro_base
137+
138+
# Solar (bottom layer) - main series
139+
series_data.append(
140+
{
141+
"name": "Solar",
142+
"type": "areaspline",
143+
"data": [float(v) for v in solar_base],
144+
"color": colors["Solar"],
145+
"fillColor": {
146+
"linearGradient": {"x1": 0, "y1": 0, "x2": 0, "y2": 1},
147+
"stops": [[0, colors["Solar"]], [1, colors["Solar"] + "40"]],
148+
},
149+
}
150+
)
151+
152+
# Solar confidence band (arearange)
153+
series_data.append(
154+
{
155+
"name": "Solar (90% CI)",
156+
"type": "arearange",
157+
"data": [[i, float(solar_lower[i]), float(solar_upper[i])] for i in range(n_points)],
158+
"color": colors["Solar"],
159+
"fillOpacity": 0.2,
160+
"showInLegend": False,
161+
}
162+
)
163+
164+
# Wind (middle layer) - main series
165+
series_data.append(
166+
{
167+
"name": "Wind",
168+
"type": "areaspline",
169+
"data": [float(v) for v in wind_base],
170+
"color": colors["Wind"],
171+
"fillColor": {
172+
"linearGradient": {"x1": 0, "y1": 0, "x2": 0, "y2": 1},
173+
"stops": [[0, colors["Wind"]], [1, colors["Wind"] + "40"]],
174+
},
175+
}
176+
)
177+
178+
# Wind confidence band (stacked on top of solar)
179+
wind_lower_stacked = solar_base + wind_lower
180+
wind_upper_stacked = solar_base + wind_upper
181+
series_data.append(
182+
{
183+
"name": "Wind (90% CI)",
184+
"type": "arearange",
185+
"data": [[i, float(wind_lower_stacked[i]), float(wind_upper_stacked[i])] for i in range(n_points)],
186+
"color": colors["Wind"],
187+
"fillOpacity": 0.2,
188+
"showInLegend": False,
189+
}
190+
)
191+
192+
# Hydro (top layer) - main series
193+
series_data.append(
194+
{
195+
"name": "Hydro",
196+
"type": "areaspline",
197+
"data": [float(v) for v in hydro_base],
198+
"color": colors["Hydro"],
199+
"fillColor": {
200+
"linearGradient": {"x1": 0, "y1": 0, "x2": 0, "y2": 1},
201+
"stops": [[0, colors["Hydro"]], [1, colors["Hydro"] + "40"]],
202+
},
203+
}
204+
)
205+
206+
# Hydro confidence band (stacked on top of solar + wind)
207+
hydro_lower_stacked = solar_base + wind_base + hydro_lower
208+
hydro_upper_stacked = solar_base + wind_base + hydro_upper
209+
series_data.append(
210+
{
211+
"name": "Hydro (90% CI)",
212+
"type": "arearange",
213+
"data": [[i, float(hydro_lower_stacked[i]), float(hydro_upper_stacked[i])] for i in range(n_points)],
214+
"color": colors["Hydro"],
215+
"fillOpacity": 0.2,
216+
"showInLegend": False,
217+
}
218+
)
219+
220+
# Set series
221+
chart.options.series = series_data
222+
223+
# Credits
224+
chart.options.credits = {"enabled": False}
225+
226+
# Download Highcharts JS and highcharts-more for arearange
227+
highcharts_url = "https://code.highcharts.com/highcharts.js"
228+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
229+
highcharts_js = response.read().decode("utf-8")
230+
231+
highcharts_more_url = "https://code.highcharts.com/highcharts-more.js"
232+
with urllib.request.urlopen(highcharts_more_url, timeout=30) as response:
233+
highcharts_more_js = response.read().decode("utf-8")
234+
235+
# Generate HTML with inline scripts
236+
html_str = chart.to_js_literal()
237+
html_content = f"""<!DOCTYPE html>
238+
<html>
239+
<head>
240+
<meta charset="utf-8">
241+
<script>{highcharts_js}</script>
242+
<script>{highcharts_more_js}</script>
243+
</head>
244+
<body style="margin:0;">
245+
<div id="container" style="width: 4800px; height: 2700px;"></div>
246+
<script>{html_str}</script>
247+
</body>
248+
</html>"""
249+
250+
# Write temp HTML and take screenshot
251+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
252+
f.write(html_content)
253+
temp_path = f.name
254+
255+
# Also save HTML for interactive version
256+
with open("plot.html", "w", encoding="utf-8") as f:
257+
f.write(html_content)
258+
259+
# Take screenshot
260+
chrome_options = Options()
261+
chrome_options.add_argument("--headless")
262+
chrome_options.add_argument("--no-sandbox")
263+
chrome_options.add_argument("--disable-dev-shm-usage")
264+
chrome_options.add_argument("--disable-gpu")
265+
chrome_options.add_argument("--window-size=4800,2700")
266+
267+
driver = webdriver.Chrome(options=chrome_options)
268+
driver.get(f"file://{temp_path}")
269+
time.sleep(5)
270+
driver.save_screenshot("plot.png")
271+
driver.quit()
272+
273+
Path(temp_path).unlink()

0 commit comments

Comments
 (0)