Skip to content

Commit 6250d88

Browse files
feat(highcharts): implement timeseries-forecast-uncertainty (#3284)
## Implementation: `timeseries-forecast-uncertainty` - highcharts Implements the **highcharts** version of `timeseries-forecast-uncertainty`. **File:** `plots/timeseries-forecast-uncertainty/implementations/highcharts.py` **Parent Issue:** #3188 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20797279910)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent eaad117 commit 6250d88

File tree

2 files changed

+453
-9
lines changed

2 files changed

+453
-9
lines changed
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
""" pyplots.ai
2+
timeseries-forecast-uncertainty: Time Series Forecast with Uncertainty Band
3+
Library: highcharts unknown | Python 3.13.11
4+
Quality: 93/100 | Created: 2026-01-07
5+
"""
6+
7+
import tempfile
8+
import time
9+
import urllib.request
10+
from datetime import datetime, timedelta
11+
from pathlib import Path
12+
13+
import numpy as np
14+
from highcharts_core.chart import Chart
15+
from highcharts_core.options import HighchartsOptions
16+
from highcharts_core.options.series.area import AreaRangeSeries, LineSeries
17+
from selenium import webdriver
18+
from selenium.webdriver.chrome.options import Options
19+
20+
21+
# Data - Monthly product demand with forecast
22+
np.random.seed(42)
23+
24+
# Historical data: 36 months (3 years)
25+
n_historical = 36
26+
n_forecast = 12
27+
28+
# Create dates
29+
start_date = datetime(2022, 1, 1)
30+
historical_dates = [start_date + timedelta(days=30 * i) for i in range(n_historical)]
31+
forecast_dates = [historical_dates[-1] + timedelta(days=30 * (i + 1)) for i in range(n_forecast)]
32+
all_dates = historical_dates + forecast_dates
33+
34+
# Generate historical data with trend and seasonality
35+
trend = np.linspace(100, 150, n_historical)
36+
seasonality = 20 * np.sin(np.linspace(0, 6 * np.pi, n_historical))
37+
noise = np.random.normal(0, 8, n_historical)
38+
historical_values = trend + seasonality + noise
39+
40+
# Generate forecast with increasing uncertainty
41+
last_value = historical_values[-1]
42+
forecast_trend = np.linspace(last_value, last_value + 20, n_forecast)
43+
forecast_seasonality = 20 * np.sin(np.linspace(6 * np.pi, 8 * np.pi, n_forecast))
44+
forecast_values = forecast_trend + forecast_seasonality
45+
46+
# Confidence intervals widen over time
47+
time_factor = np.linspace(1, 3, n_forecast)
48+
ci_80 = 10 * time_factor
49+
ci_95 = 18 * time_factor
50+
51+
lower_80 = forecast_values - ci_80
52+
upper_80 = forecast_values + ci_80
53+
lower_95 = forecast_values - ci_95
54+
upper_95 = forecast_values + ci_95
55+
56+
# Convert dates to timestamps (milliseconds for Highcharts)
57+
historical_timestamps = [int(d.timestamp() * 1000) for d in historical_dates]
58+
forecast_timestamps = [int(d.timestamp() * 1000) for d in forecast_dates]
59+
forecast_start_ts = forecast_timestamps[0]
60+
61+
# Download Highcharts JS
62+
highcharts_url = "https://code.highcharts.com/highcharts.js"
63+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
64+
highcharts_js = response.read().decode("utf-8")
65+
66+
# Download Highcharts More for arearange
67+
highcharts_more_url = "https://code.highcharts.com/highcharts-more.js"
68+
with urllib.request.urlopen(highcharts_more_url, timeout=30) as response:
69+
highcharts_more_js = response.read().decode("utf-8")
70+
71+
# Create chart
72+
chart = Chart(container="container")
73+
chart.options = HighchartsOptions()
74+
75+
# Chart configuration
76+
chart.options.chart = {
77+
"type": "line",
78+
"width": 4800,
79+
"height": 2700,
80+
"backgroundColor": "#ffffff",
81+
"spacingTop": 60,
82+
"spacingBottom": 120,
83+
"spacingLeft": 100,
84+
"spacingRight": 120,
85+
}
86+
87+
# Title
88+
chart.options.title = {
89+
"text": "timeseries-forecast-uncertainty \u00b7 highcharts \u00b7 pyplots.ai",
90+
"style": {"fontSize": "56px", "fontWeight": "bold"},
91+
"margin": 40,
92+
}
93+
94+
chart.options.subtitle = {
95+
"text": "Monthly Product Demand with 80% and 95% Confidence Intervals",
96+
"style": {"fontSize": "36px", "color": "#666666"},
97+
}
98+
99+
# X-axis (datetime)
100+
chart.options.x_axis = {
101+
"type": "datetime",
102+
"title": {"text": "Date", "style": {"fontSize": "36px"}, "margin": 25},
103+
"labels": {"style": {"fontSize": "28px"}},
104+
"dateTimeLabelFormats": {"month": "%b %Y"},
105+
"gridLineWidth": 1,
106+
"gridLineColor": "rgba(0, 0, 0, 0.1)",
107+
"plotLines": [
108+
{
109+
"value": forecast_start_ts,
110+
"color": "#666666",
111+
"width": 4,
112+
"dashStyle": "Dash",
113+
"label": {
114+
"text": "Forecast Start",
115+
"style": {"fontSize": "28px", "color": "#555555", "fontWeight": "bold"},
116+
"rotation": 0,
117+
"y": -15,
118+
},
119+
"zIndex": 5,
120+
}
121+
],
122+
}
123+
124+
# Y-axis
125+
chart.options.y_axis = {
126+
"title": {"text": "Product Demand (Units)", "style": {"fontSize": "36px"}, "margin": 25},
127+
"labels": {"style": {"fontSize": "28px"}},
128+
"gridLineWidth": 1,
129+
"gridLineColor": "rgba(0, 0, 0, 0.1)",
130+
}
131+
132+
# Legend
133+
chart.options.legend = {
134+
"enabled": True,
135+
"itemStyle": {"fontSize": "32px"},
136+
"symbolWidth": 50,
137+
"symbolHeight": 24,
138+
"margin": 30,
139+
}
140+
141+
# Tooltip
142+
chart.options.tooltip = {"shared": True, "style": {"fontSize": "28px"}, "xDateFormat": "%B %Y", "valueDecimals": 1}
143+
144+
# Plot options for line width
145+
chart.options.plot_options = {"line": {"lineWidth": 5, "marker": {"radius": 8}}, "arearange": {"lineWidth": 0}}
146+
147+
# 95% confidence band (lighter, behind 80%)
148+
ci_95_series = AreaRangeSeries()
149+
ci_95_series.name = "95% Confidence Interval"
150+
ci_95_series.data = [
151+
{"x": forecast_timestamps[i], "low": float(lower_95[i]), "high": float(upper_95[i])} for i in range(n_forecast)
152+
]
153+
ci_95_series.color = "rgba(255, 212, 59, 0.25)"
154+
ci_95_series.fill_opacity = 0.25
155+
ci_95_series.line_width = 0
156+
ci_95_series.marker = {"enabled": False}
157+
ci_95_series.z_index = 0
158+
159+
# 80% confidence band (darker)
160+
ci_80_series = AreaRangeSeries()
161+
ci_80_series.name = "80% Confidence Interval"
162+
ci_80_series.data = [
163+
{"x": forecast_timestamps[i], "low": float(lower_80[i]), "high": float(upper_80[i])} for i in range(n_forecast)
164+
]
165+
ci_80_series.color = "rgba(255, 212, 59, 0.45)"
166+
ci_80_series.fill_opacity = 0.45
167+
ci_80_series.line_width = 0
168+
ci_80_series.marker = {"enabled": False}
169+
ci_80_series.z_index = 1
170+
171+
# Historical data series
172+
historical_series = LineSeries()
173+
historical_series.name = "Historical (Actual)"
174+
historical_series.data = [
175+
{"x": historical_timestamps[i], "y": float(historical_values[i])} for i in range(n_historical)
176+
]
177+
historical_series.color = "#306998"
178+
historical_series.line_width = 4
179+
historical_series.marker = {"enabled": True, "radius": 6, "symbol": "circle"}
180+
historical_series.z_index = 3
181+
182+
# Forecast series
183+
forecast_series = LineSeries()
184+
forecast_series.name = "Forecast"
185+
forecast_series.data = [{"x": forecast_timestamps[i], "y": float(forecast_values[i])} for i in range(n_forecast)]
186+
forecast_series.color = "#E67E22"
187+
forecast_series.line_width = 4
188+
forecast_series.dash_style = "Dash"
189+
forecast_series.marker = {"enabled": True, "radius": 6, "symbol": "diamond"}
190+
forecast_series.z_index = 4
191+
192+
# Add series in order (back to front)
193+
chart.add_series(ci_95_series)
194+
chart.add_series(ci_80_series)
195+
chart.add_series(historical_series)
196+
chart.add_series(forecast_series)
197+
198+
# Disable credits
199+
chart.options.credits = {"enabled": False}
200+
201+
# Generate HTML with inline scripts
202+
html_str = chart.to_js_literal()
203+
html_content = f"""<!DOCTYPE html>
204+
<html>
205+
<head>
206+
<meta charset="utf-8">
207+
<script>{highcharts_js}</script>
208+
<script>{highcharts_more_js}</script>
209+
</head>
210+
<body style="margin:0;">
211+
<div id="container" style="width: 4800px; height: 2700px;"></div>
212+
<script>{html_str}</script>
213+
</body>
214+
</html>"""
215+
216+
# Save HTML version for interactive viewing (uses CDN for portability)
217+
cdn_html = f"""<!DOCTYPE html>
218+
<html>
219+
<head>
220+
<meta charset="utf-8">
221+
<script src="https://code.highcharts.com/highcharts.js"></script>
222+
<script src="https://code.highcharts.com/highcharts-more.js"></script>
223+
</head>
224+
<body style="margin:0;">
225+
<div id="container" style="width: 100%; height: 600px;"></div>
226+
<script>{html_str}</script>
227+
</body>
228+
</html>"""
229+
with open("plot.html", "w", encoding="utf-8") as f:
230+
f.write(cdn_html)
231+
232+
# Take screenshot with Selenium
233+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
234+
f.write(html_content)
235+
temp_path = f.name
236+
237+
chrome_options = Options()
238+
chrome_options.add_argument("--headless")
239+
chrome_options.add_argument("--no-sandbox")
240+
chrome_options.add_argument("--disable-dev-shm-usage")
241+
chrome_options.add_argument("--disable-gpu")
242+
chrome_options.add_argument("--window-size=5000,3000")
243+
244+
driver = webdriver.Chrome(options=chrome_options)
245+
driver.get(f"file://{temp_path}")
246+
time.sleep(5)
247+
248+
# Screenshot the chart container element for exact dimensions
249+
container = driver.find_element("id", "container")
250+
container.screenshot("plot.png")
251+
driver.quit()
252+
253+
Path(temp_path).unlink()

0 commit comments

Comments
 (0)