Skip to content

Commit f1a67b6

Browse files
feat(highcharts): implement heatmap-interactive (#3461)
## Implementation: `heatmap-interactive` - highcharts Implements the **highcharts** version of `heatmap-interactive`. **File:** `plots/heatmap-interactive/implementations/highcharts.py` **Parent Issue:** #3289 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20846719177)* --------- 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 da75c6c commit f1a67b6

2 files changed

Lines changed: 437 additions & 0 deletions

File tree

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
""" pyplots.ai
2+
heatmap-interactive: Interactive Heatmap with Hover and Zoom
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 - Monthly website traffic by hour and day (20x24 matrix)
20+
np.random.seed(42)
21+
days = [
22+
"Monday",
23+
"Tuesday",
24+
"Wednesday",
25+
"Thursday",
26+
"Friday",
27+
"Saturday",
28+
"Sunday",
29+
"Week 2 Mon",
30+
"Week 2 Tue",
31+
"Week 2 Wed",
32+
"Week 2 Thu",
33+
"Week 2 Fri",
34+
"Week 2 Sat",
35+
"Week 2 Sun",
36+
"Week 3 Mon",
37+
"Week 3 Tue",
38+
"Week 3 Wed",
39+
"Week 3 Thu",
40+
"Week 3 Fri",
41+
"Week 3 Sat",
42+
]
43+
hours = [f"{h:02d}:00" for h in range(24)]
44+
45+
# Generate realistic traffic patterns
46+
base_traffic = np.zeros((len(days), len(hours)))
47+
for i, day in enumerate(days):
48+
for j in range(len(hours)):
49+
# Peak hours 9-17 on weekdays
50+
is_weekend = "Sat" in day or "Sun" in day
51+
hour_factor = 1.0
52+
if 9 <= j <= 17:
53+
hour_factor = 2.5 if not is_weekend else 1.5
54+
elif 6 <= j <= 8 or 18 <= j <= 21:
55+
hour_factor = 1.8 if not is_weekend else 1.3
56+
elif 0 <= j <= 5:
57+
hour_factor = 0.3
58+
59+
day_factor = 0.6 if is_weekend else 1.0
60+
base_value = 500 * day_factor * hour_factor
61+
noise = np.random.normal(0, base_value * 0.2)
62+
base_traffic[i, j] = max(0, base_value + noise)
63+
64+
# Convert to heatmap data format for Highcharts
65+
heatmap_data = []
66+
for i, _day in enumerate(days):
67+
for j, _hour in enumerate(hours):
68+
heatmap_data.append([j, i, round(base_traffic[i, j], 1)])
69+
70+
# Download Highcharts JS and heatmap module
71+
highcharts_url = "https://code.highcharts.com/highcharts.js"
72+
heatmap_url = "https://code.highcharts.com/modules/heatmap.js"
73+
boost_url = "https://code.highcharts.com/modules/boost.js"
74+
75+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
76+
highcharts_js = response.read().decode("utf-8")
77+
with urllib.request.urlopen(heatmap_url, timeout=30) as response:
78+
heatmap_js = response.read().decode("utf-8")
79+
with urllib.request.urlopen(boost_url, timeout=30) as response:
80+
boost_js = response.read().decode("utf-8")
81+
82+
# Create chart
83+
chart = Chart(container="container")
84+
chart.options = HighchartsOptions()
85+
86+
# Chart configuration with zooming
87+
chart.options.chart = {
88+
"type": "heatmap",
89+
"width": 4800,
90+
"height": 2700,
91+
"backgroundColor": "#ffffff",
92+
"zoomType": "xy",
93+
"panning": {"enabled": True, "type": "xy"},
94+
"panKey": "shift",
95+
"marginBottom": 180,
96+
"resetZoomButton": {
97+
"position": {"align": "right", "verticalAlign": "top", "x": -60, "y": 60},
98+
"theme": {"fill": "#306998", "stroke": "#306998", "style": {"color": "#ffffff", "fontSize": "20px"}},
99+
},
100+
}
101+
102+
# Title
103+
chart.options.title = {
104+
"text": "Website Traffic by Hour · heatmap-interactive · highcharts · pyplots.ai",
105+
"style": {"fontSize": "48px", "fontWeight": "bold"},
106+
}
107+
108+
chart.options.subtitle = {
109+
"text": "Drag to zoom, Shift+drag to pan, click reset button to restore view",
110+
"style": {"fontSize": "28px", "color": "#666666"},
111+
}
112+
113+
# X-axis (hours)
114+
chart.options.x_axis = {
115+
"categories": hours,
116+
"title": {"text": "Hour of Day", "style": {"fontSize": "32px"}},
117+
"labels": {"style": {"fontSize": "22px"}, "rotation": 315},
118+
"tickLength": 0,
119+
"gridLineWidth": 0,
120+
}
121+
122+
# Y-axis (days)
123+
chart.options.y_axis = {
124+
"categories": days,
125+
"title": {"text": "Day", "style": {"fontSize": "32px"}},
126+
"labels": {"style": {"fontSize": "22px"}},
127+
"reversed": True,
128+
"gridLineWidth": 0,
129+
}
130+
131+
# Color axis (legend)
132+
chart.options.color_axis = {
133+
"min": 0,
134+
"max": float(np.max(base_traffic)),
135+
"stops": [[0, "#f7fbff"], [0.2, "#c6dbef"], [0.4, "#6baed6"], [0.6, "#2171b5"], [0.8, "#08519c"], [1, "#08306b"]],
136+
"labels": {"style": {"fontSize": "22px"}},
137+
}
138+
139+
# Legend
140+
chart.options.legend = {
141+
"align": "right",
142+
"layout": "vertical",
143+
"verticalAlign": "middle",
144+
"symbolHeight": 600,
145+
"itemStyle": {"fontSize": "22px"},
146+
"title": {"text": "Visitors", "style": {"fontSize": "26px"}},
147+
}
148+
149+
# Tooltip
150+
chart.options.tooltip = {
151+
"useHTML": True,
152+
"headerFormat": "",
153+
"pointFormat": '<div style="font-size: 24px; padding: 12px;"><b>{series.xAxis.categories.(point.x)}</b> on <b>{series.yAxis.categories.(point.y)}</b><br/>Visitors: <b>{point.value:.0f}</b></div>',
154+
"backgroundColor": "rgba(255, 255, 255, 0.95)",
155+
"borderWidth": 2,
156+
"borderColor": "#306998",
157+
}
158+
159+
# Plot options for heatmap
160+
chart.options.plot_options = {
161+
"heatmap": {
162+
"borderWidth": 1,
163+
"borderColor": "#ffffff",
164+
"dataLabels": {"enabled": False},
165+
"cursor": "pointer",
166+
"states": {"hover": {"brightness": 0.2, "borderColor": "#000000", "borderWidth": 3}},
167+
}
168+
}
169+
170+
# Add series
171+
chart.add_series({"name": "Traffic", "type": "heatmap", "data": heatmap_data, "turboThreshold": 10000})
172+
173+
# Generate HTML with inline scripts
174+
html_str = chart.to_js_literal()
175+
html_content = f"""<!DOCTYPE html>
176+
<html>
177+
<head>
178+
<meta charset="utf-8">
179+
<script>{highcharts_js}</script>
180+
<script>{heatmap_js}</script>
181+
<script>{boost_js}</script>
182+
</head>
183+
<body style="margin:0;">
184+
<div id="container" style="width: 4800px; height: 2700px;"></div>
185+
<script>{html_str}</script>
186+
</body>
187+
</html>"""
188+
189+
# Save interactive HTML
190+
with open("plot.html", "w", encoding="utf-8") as f:
191+
# Generate standalone HTML with CDN links for the interactive version
192+
interactive_html = f"""<!DOCTYPE html>
193+
<html>
194+
<head>
195+
<meta charset="utf-8">
196+
<title>heatmap-interactive · highcharts · pyplots.ai</title>
197+
<script src="https://code.highcharts.com/highcharts.js"></script>
198+
<script src="https://code.highcharts.com/modules/heatmap.js"></script>
199+
<script src="https://code.highcharts.com/modules/boost.js"></script>
200+
</head>
201+
<body style="margin:0;">
202+
<div id="container" style="width: 100%; height: 100vh;"></div>
203+
<script>{html_str}</script>
204+
</body>
205+
</html>"""
206+
f.write(interactive_html)
207+
208+
# Write temp HTML and take screenshot
209+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
210+
f.write(html_content)
211+
temp_path = f.name
212+
213+
chrome_options = Options()
214+
chrome_options.add_argument("--headless")
215+
chrome_options.add_argument("--no-sandbox")
216+
chrome_options.add_argument("--disable-dev-shm-usage")
217+
chrome_options.add_argument("--disable-gpu")
218+
chrome_options.add_argument("--window-size=4800,2900")
219+
220+
driver = webdriver.Chrome(options=chrome_options)
221+
driver.get(f"file://{temp_path}")
222+
time.sleep(5)
223+
driver.save_screenshot("plot.png")
224+
driver.quit()
225+
226+
Path(temp_path).unlink()

0 commit comments

Comments
 (0)