Skip to content

Commit 529dd3b

Browse files
feat(highcharts): implement heatmap-interactive (#3326)
## 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/20822856784)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6470518 commit 529dd3b

2 files changed

Lines changed: 251 additions & 0 deletions

File tree

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
"""pyplots.ai
2+
heatmap-interactive: Interactive Heatmap with Hover and Zoom
3+
Library: highcharts | Python 3.13
4+
Quality: pending | Created: 2026-01-08
5+
"""
6+
7+
import json
8+
import tempfile
9+
import time
10+
import urllib.request
11+
from pathlib import Path
12+
13+
import numpy as np
14+
from PIL import Image
15+
from selenium import webdriver
16+
from selenium.webdriver.chrome.options import Options
17+
18+
19+
# Data - Monthly website page views by category (20 categories x 12 months)
20+
np.random.seed(42)
21+
22+
categories = [
23+
"Homepage",
24+
"Products",
25+
"Services",
26+
"About",
27+
"Contact",
28+
"Blog",
29+
"FAQ",
30+
"Support",
31+
"Pricing",
32+
"Careers",
33+
"News",
34+
"Events",
35+
"Resources",
36+
"Documentation",
37+
"Tutorials",
38+
"API",
39+
"Downloads",
40+
"Partners",
41+
"Investors",
42+
"Legal",
43+
]
44+
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
45+
46+
# Generate realistic page view data (thousands of views)
47+
base_views = np.random.randint(10, 100, size=(len(categories), len(months)))
48+
seasonal = np.sin(np.linspace(0, 2 * np.pi, 12)) * 20 + 50
49+
values = base_views + seasonal.astype(int)
50+
values = np.clip(values, 5, 150)
51+
52+
# Convert to Highcharts heatmap data format: [x, y, value]
53+
heatmap_data = []
54+
for y_idx in range(len(categories)):
55+
for x_idx in range(len(months)):
56+
heatmap_data.append([x_idx, y_idx, int(values[y_idx, x_idx])])
57+
58+
# Render at 2400x1350 and scale up to 4800x2700 for better Chrome compatibility
59+
RENDER_WIDTH = 2400
60+
RENDER_HEIGHT = 1350
61+
OUTPUT_WIDTH = 4800
62+
OUTPUT_HEIGHT = 2700
63+
64+
# Download Highcharts JS and heatmap module
65+
highcharts_url = "https://code.highcharts.com/highcharts.js"
66+
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
67+
highcharts_js = response.read().decode("utf-8")
68+
69+
heatmap_url = "https://code.highcharts.com/modules/heatmap.js"
70+
with urllib.request.urlopen(heatmap_url, timeout=30) as response:
71+
heatmap_js = response.read().decode("utf-8")
72+
73+
# Create HTML with Highcharts configuration
74+
html_content = f"""<!DOCTYPE html>
75+
<html>
76+
<head>
77+
<meta charset="utf-8">
78+
<script>{highcharts_js}</script>
79+
<script>{heatmap_js}</script>
80+
</head>
81+
<body style="margin:0;">
82+
<div id="container" style="width: {RENDER_WIDTH}px; height: {RENDER_HEIGHT}px;"></div>
83+
<script>
84+
document.addEventListener('DOMContentLoaded', function() {{
85+
Highcharts.chart('container', {{
86+
chart: {{
87+
type: 'heatmap',
88+
width: {RENDER_WIDTH},
89+
height: {RENDER_HEIGHT},
90+
backgroundColor: '#ffffff',
91+
marginBottom: 90,
92+
marginRight: 100,
93+
marginLeft: 140,
94+
zoomType: 'xy',
95+
panning: {{ enabled: true, type: 'xy' }},
96+
panKey: 'shift',
97+
resetZoomButton: {{
98+
position: {{ align: 'right', verticalAlign: 'top', x: -10, y: 10 }},
99+
theme: {{ style: {{ fontSize: '14px' }} }}
100+
}}
101+
}},
102+
title: {{
103+
text: 'heatmap-interactive · highcharts · pyplots.ai',
104+
style: {{ fontSize: '24px', fontWeight: 'bold' }}
105+
}},
106+
subtitle: {{
107+
text: 'Website Page Views by Category and Month (thousands) - Click and drag to zoom, Shift+drag to pan',
108+
style: {{ fontSize: '14px', color: '#666666' }}
109+
}},
110+
xAxis: {{
111+
categories: {json.dumps(months)},
112+
title: {{ text: 'Month', style: {{ fontSize: '16px' }} }},
113+
labels: {{ style: {{ fontSize: '12px' }} }},
114+
gridLineWidth: 1,
115+
gridLineColor: '#e0e0e0'
116+
}},
117+
yAxis: {{
118+
categories: {json.dumps(categories)},
119+
title: {{ text: 'Page Category', style: {{ fontSize: '16px' }} }},
120+
labels: {{ style: {{ fontSize: '11px' }} }},
121+
reversed: true,
122+
gridLineWidth: 1,
123+
gridLineColor: '#e0e0e0'
124+
}},
125+
colorAxis: {{
126+
min: 5,
127+
max: 150,
128+
stops: [
129+
[0, '#ffffff'],
130+
[0.25, '#FFD43B'],
131+
[0.5, '#FFA500'],
132+
[0.75, '#306998'],
133+
[1, '#1a3a5c']
134+
],
135+
labels: {{ style: {{ fontSize: '11px' }} }}
136+
}},
137+
legend: {{
138+
align: 'right',
139+
layout: 'vertical',
140+
verticalAlign: 'middle',
141+
symbolHeight: 250,
142+
symbolWidth: 20,
143+
itemStyle: {{ fontSize: '11px' }},
144+
title: {{
145+
text: 'Page Views (K)',
146+
style: {{ fontSize: '12px', fontWeight: 'bold' }}
147+
}}
148+
}},
149+
tooltip: {{
150+
enabled: true,
151+
useHTML: true,
152+
formatter: function() {{
153+
return '<div style="font-size: 14px; padding: 8px;">' +
154+
'<b>Category:</b> ' + this.series.yAxis.categories[this.point.y] + '<br/>' +
155+
'<b>Month:</b> ' + this.series.xAxis.categories[this.point.x] + '<br/>' +
156+
'<b>Views:</b> ' + this.point.value + 'K</div>';
157+
}},
158+
style: {{ fontSize: '12px' }}
159+
}},
160+
plotOptions: {{
161+
heatmap: {{
162+
borderWidth: 2,
163+
borderColor: '#ffffff',
164+
dataLabels: {{ enabled: false }},
165+
states: {{
166+
hover: {{
167+
brightness: 0.1,
168+
borderWidth: 4,
169+
borderColor: '#000000'
170+
}}
171+
}}
172+
}},
173+
series: {{
174+
cursor: 'pointer',
175+
stickyTracking: true
176+
}}
177+
}},
178+
series: [{{
179+
type: 'heatmap',
180+
name: 'Page Views',
181+
data: {json.dumps(heatmap_data)},
182+
borderWidth: 2,
183+
borderColor: '#ffffff',
184+
nullColor: '#e0e0e0'
185+
}}]
186+
}});
187+
}});
188+
</script>
189+
</body>
190+
</html>"""
191+
192+
# Write temp HTML and take screenshot
193+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
194+
f.write(html_content)
195+
temp_path = f.name
196+
197+
# Save HTML for interactive viewing
198+
with open("plot.html", "w", encoding="utf-8") as f:
199+
f.write(html_content)
200+
201+
chrome_options = Options()
202+
chrome_options.add_argument("--headless=new")
203+
chrome_options.add_argument("--no-sandbox")
204+
chrome_options.add_argument("--disable-dev-shm-usage")
205+
chrome_options.add_argument("--disable-gpu")
206+
chrome_options.add_argument(f"--window-size={RENDER_WIDTH + 100},{RENDER_HEIGHT + 100}")
207+
chrome_options.add_argument("--force-device-scale-factor=1")
208+
209+
driver = webdriver.Chrome(options=chrome_options)
210+
driver.set_window_size(RENDER_WIDTH + 100, RENDER_HEIGHT + 100)
211+
driver.get(f"file://{temp_path}")
212+
time.sleep(5)
213+
214+
# Take screenshot at render size
215+
driver.save_screenshot("plot_temp.png")
216+
driver.quit()
217+
218+
# Resize to output dimensions using high-quality resampling
219+
img = Image.open("plot_temp.png")
220+
# Crop to exact chart dimensions, leaving out any browser chrome
221+
crop_height = min(RENDER_HEIGHT, img.height)
222+
img = img.crop((0, 0, RENDER_WIDTH, crop_height))
223+
# Create white canvas at target size and paste the cropped image
224+
canvas = Image.new("RGB", (RENDER_WIDTH, RENDER_HEIGHT), (255, 255, 255))
225+
canvas.paste(img, (0, 0))
226+
# Resize to output size with high-quality resampling
227+
img_resized = canvas.resize((OUTPUT_WIDTH, OUTPUT_HEIGHT), Image.Resampling.LANCZOS)
228+
img_resized.save("plot.png", "PNG")
229+
230+
# Clean up temp files
231+
Path("plot_temp.png").unlink()
232+
Path(temp_path).unlink()
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Per-library metadata for highcharts implementation of heatmap-interactive
2+
# Auto-generated by impl-generate.yml
3+
4+
library: highcharts
5+
specification_id: heatmap-interactive
6+
created: '2026-01-08T16:06:01Z'
7+
updated: '2026-01-08T16:06:01Z'
8+
generated_by: claude-opus-4-5-20251101
9+
workflow_run: 20822856784
10+
issue: 3289
11+
python_version: 3.13.11
12+
library_version: unknown
13+
preview_url: https://storage.googleapis.com/pyplots-images/plots/heatmap-interactive/highcharts/plot.png
14+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/heatmap-interactive/highcharts/plot_thumb.png
15+
preview_html: https://storage.googleapis.com/pyplots-images/plots/heatmap-interactive/highcharts/plot.html
16+
quality_score: null
17+
review:
18+
strengths: []
19+
weaknesses: []

0 commit comments

Comments
 (0)