Skip to content

Commit a4d3f8d

Browse files
update(hexbin-basic): highcharts — comprehensive quality review
- Major refactor (-100 lines) - Cleaner heatmap/tilemap approach - Simplified configuration
1 parent 60de10e commit a4d3f8d

2 files changed

Lines changed: 111 additions & 211 deletions

File tree

plots/hexbin-basic/implementations/highcharts.py

Lines changed: 107 additions & 207 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
""" pyplots.ai
22
hexbin-basic: Basic Hexbin Plot
3-
Library: highcharts 1.10.3 | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-14
3+
Library: highcharts 1.10.3 | Python 3.14.3
4+
Quality: /100 | Updated: 2026-02-21
55
"""
66

7+
import json
78
import tempfile
89
import time
910
import urllib.request
@@ -14,242 +15,141 @@
1415
from selenium.webdriver.chrome.options import Options
1516

1617

17-
# Data - generate clustered bivariate data (10,000 points)
18+
# Data - seismic sensor readings: 10,000 measurements across a monitoring grid
1819
np.random.seed(42)
1920
n_points = 10000
2021

21-
# Create clustered distribution with 3 centers
22-
centers = [(0, 0), (3, 3), (-2, 4)]
23-
points_per_cluster = n_points // 3
22+
# Three activity zones with different intensities
23+
zone_a = np.column_stack([np.random.randn(n_points // 3) * 1.2 + 2, np.random.randn(n_points // 3) * 1.0 + 3])
24+
zone_b = np.column_stack([np.random.randn(n_points // 3) * 1.5 - 1, np.random.randn(n_points // 3) * 1.5 - 1])
25+
zone_c = np.column_stack([np.random.randn(n_points // 3) * 0.8 + 4, np.random.randn(n_points // 3) * 0.9 - 2])
26+
points = np.vstack([zone_a, zone_b, zone_c])
2427

25-
x_data = []
26-
y_data = []
28+
# Hexagonal binning
29+
gridsize = 20
30+
x_min, x_max = points[:, 0].min() - 0.5, points[:, 0].max() + 0.5
31+
y_min, y_max = points[:, 1].min() - 0.5, points[:, 1].max() + 0.5
2732

28-
for cx, cy in centers:
29-
x_data.extend(np.random.randn(points_per_cluster) * 1.2 + cx)
30-
y_data.extend(np.random.randn(points_per_cluster) * 1.2 + cy)
33+
hex_width = (x_max - x_min) / gridsize
34+
hex_height = hex_width * 2 / np.sqrt(3)
35+
vert_spacing = hex_height * 0.75
3136

32-
x = np.array(x_data)
33-
y = np.array(y_data)
34-
35-
# Hexbin computation
36-
gridsize = 25
37-
x_min, x_max = x.min() - 0.5, x.max() + 0.5
38-
y_min, y_max = y.min() - 0.5, y.max() + 0.5
39-
40-
# Hexagon geometry: pointy-top orientation
41-
hex_size = (x_max - x_min) / gridsize / 2
42-
hex_width = hex_size * np.sqrt(3)
43-
hex_height = hex_size * 2
44-
hex_horiz_spacing = hex_width
45-
hex_vert_spacing = hex_height * 0.75
46-
47-
# Compute hexagonal bin centers and counts
4837
hex_bins = {}
49-
for xi, yi in zip(x, y, strict=True):
50-
row = int((yi - y_min) / hex_vert_spacing)
38+
for px, py in points:
39+
row = int((py - y_min) / vert_spacing)
5140
col_offset = (row % 2) * hex_width * 0.5
52-
col = int((xi - x_min - col_offset) / hex_horiz_spacing)
53-
hx = x_min + col * hex_horiz_spacing + col_offset + hex_width / 2
54-
hy = y_min + row * hex_vert_spacing + hex_height / 2
41+
col = int((px - x_min - col_offset) / hex_width)
5542
key = (col, row)
56-
if key not in hex_bins:
57-
hex_bins[key] = {"x": hx, "y": hy, "count": 0}
58-
hex_bins[key]["count"] += 1
59-
60-
# Extract bin data
61-
hex_centers_x = [v["x"] for v in hex_bins.values()]
62-
hex_centers_y = [v["y"] for v in hex_bins.values()]
63-
counts = np.array([v["count"] for v in hex_bins.values()])
64-
max_count = counts.max()
65-
66-
# Viridis colorscale
67-
viridis_colors = [(0.0, "#440154"), (0.25, "#3b528b"), (0.5, "#21918c"), (0.75, "#5ec962"), (1.0, "#fde725")]
68-
69-
70-
def get_viridis_color(val):
71-
"""Interpolate viridis color for value 0-1."""
72-
for i in range(len(viridis_colors) - 1):
73-
v1, c1 = viridis_colors[i]
74-
v2, c2 = viridis_colors[i + 1]
75-
if v1 <= val <= v2:
76-
t = (val - v1) / (v2 - v1)
77-
r1, g1, b1 = int(c1[1:3], 16), int(c1[3:5], 16), int(c1[5:7], 16)
78-
r2, g2, b2 = int(c2[1:3], 16), int(c2[3:5], 16), int(c2[5:7], 16)
79-
r = int(r1 + t * (r2 - r1))
80-
g = int(g1 + t * (g2 - g1))
81-
b = int(b1 + t * (b2 - b1))
82-
return f"#{r:02x}{g:02x}{b:02x}"
83-
return "#fde725"
84-
85-
86-
def hexagon_vertices(cx, cy, size):
87-
"""Pointy-top hexagon vertices."""
88-
angles = np.array([30, 90, 150, 210, 270, 330]) * np.pi / 180
89-
vx = cx + size * np.cos(angles)
90-
vy = cy + size * np.sin(angles)
91-
return list(zip(vx, vy, strict=True))
92-
93-
94-
# Download Highcharts JS for inline embedding
43+
hex_bins[key] = hex_bins.get(key, 0) + 1
44+
45+
# Build tilemap data: grid coordinates + count values
46+
tilemap_data = []
47+
for (col, row), count in hex_bins.items():
48+
tilemap_data.append({"x": col, "y": row, "value": count})
49+
50+
max_count = max(v["value"] for v in tilemap_data)
51+
52+
# Chart options using Highcharts tilemap with hexagonal tiles
53+
chart_options = {
54+
"chart": {
55+
"type": "tilemap",
56+
"width": 4800,
57+
"height": 2700,
58+
"backgroundColor": "#ffffff",
59+
"marginTop": 130,
60+
"marginBottom": 120,
61+
"marginLeft": 140,
62+
"marginRight": 220,
63+
"animation": False,
64+
},
65+
"title": {
66+
"text": "Seismic Activity Density \u00b7 hexbin-basic \u00b7 highcharts \u00b7 pyplots.ai",
67+
"style": {"fontSize": "44px", "fontWeight": "500"},
68+
},
69+
"xAxis": {"visible": False},
70+
"yAxis": {"visible": False},
71+
"colorAxis": {
72+
"min": 0,
73+
"max": int(max_count),
74+
"stops": [[0, "#440154"], [0.25, "#3b528b"], [0.5, "#21918c"], [0.75, "#5ec962"], [1, "#fde725"]],
75+
"labels": {"style": {"fontSize": "24px"}},
76+
},
77+
"legend": {
78+
"align": "right",
79+
"layout": "vertical",
80+
"verticalAlign": "middle",
81+
"symbolHeight": 600,
82+
"symbolWidth": 40,
83+
"title": {"text": "Event Count", "style": {"fontSize": "28px", "fontWeight": "bold"}},
84+
"itemStyle": {"fontSize": "24px"},
85+
},
86+
"tooltip": {"enabled": False},
87+
"credits": {"enabled": False},
88+
"plotOptions": {
89+
"tilemap": {
90+
"tileShape": "hexagon",
91+
"colsize": 1,
92+
"rowsize": 1,
93+
"borderWidth": 1,
94+
"borderColor": "rgba(255,255,255,0.3)",
95+
"animation": False,
96+
"states": {"hover": {"enabled": False}, "inactive": {"enabled": False}},
97+
}
98+
},
99+
"series": [
100+
{
101+
"type": "tilemap",
102+
"name": "Density",
103+
"data": tilemap_data,
104+
"tileShape": "hexagon",
105+
"dataLabels": {"enabled": False},
106+
}
107+
],
108+
}
109+
110+
# Download Highcharts JS and required modules
95111
highcharts_url = "https://code.highcharts.com/highcharts.js"
112+
heatmap_url = "https://code.highcharts.com/modules/heatmap.js"
113+
tilemap_url = "https://code.highcharts.com/modules/tilemap.js"
114+
96115
with urllib.request.urlopen(highcharts_url, timeout=30) as response:
97116
highcharts_js = response.read().decode("utf-8")
98117

99-
# Download highcharts-more for polygon support
100-
highcharts_more_url = "https://code.highcharts.com/highcharts-more.js"
101-
with urllib.request.urlopen(highcharts_more_url, timeout=30) as response:
102-
highcharts_more_js = response.read().decode("utf-8")
118+
with urllib.request.urlopen(heatmap_url, timeout=30) as response:
119+
heatmap_js = response.read().decode("utf-8")
103120

104-
# Build series data - each hexagon as a separate polygon series
105-
series_js_parts = []
106-
for hx, hy, count in zip(hex_centers_x, hex_centers_y, counts, strict=True):
107-
norm_count = count / max_count
108-
color = get_viridis_color(norm_count)
109-
vertices = hexagon_vertices(hx, hy, hex_size * 1.02)
110-
coords_str = ", ".join([f"[{vx:.4f}, {vy:.4f}]" for vx, vy in vertices])
111-
series_js_parts.append(
112-
f'{{type: "polygon", data: [{coords_str}], color: "{color}", enableMouseTracking: false, animation: false}}'
113-
)
121+
with urllib.request.urlopen(tilemap_url, timeout=30) as response:
122+
tilemap_js = response.read().decode("utf-8")
114123

115-
series_js = "[" + ", ".join(series_js_parts) + "]"
124+
# Convert options to JSON
125+
options_json = json.dumps(chart_options)
116126

117-
# Create custom HTML with Highcharts
127+
# Generate HTML with inline scripts
118128
html_content = f"""<!DOCTYPE html>
119129
<html>
120130
<head>
121131
<meta charset="utf-8">
122132
<script>{highcharts_js}</script>
123-
<script>{highcharts_more_js}</script>
124-
<style>
125-
body {{ margin: 0; padding: 0; background: #ffffff; }}
126-
#container {{ width: 4800px; height: 2700px; }}
127-
.colorbar-wrapper {{
128-
position: absolute;
129-
right: 60px;
130-
top: 350px;
131-
display: flex;
132-
flex-direction: row;
133-
font-family: Arial, sans-serif;
134-
}}
135-
.colorbar {{
136-
width: 40px;
137-
height: 800px;
138-
background: linear-gradient(to bottom, #fde725, #5ec962, #21918c, #3b528b, #440154);
139-
border: 1px solid #333;
140-
}}
141-
.colorbar-labels {{
142-
display: flex;
143-
flex-direction: column;
144-
justify-content: space-between;
145-
margin-left: 12px;
146-
font-size: 24px;
147-
height: 800px;
148-
}}
149-
.colorbar-title {{
150-
position: absolute;
151-
right: 45px;
152-
top: 290px;
153-
font-size: 32px;
154-
font-family: Arial, sans-serif;
155-
font-weight: bold;
156-
}}
157-
</style>
133+
<script>{heatmap_js}</script>
134+
<script>{tilemap_js}</script>
158135
</head>
159-
<body>
160-
<div id="container"></div>
161-
<div class="colorbar-title">Count</div>
162-
<div class="colorbar-wrapper">
163-
<div class="colorbar"></div>
164-
<div class="colorbar-labels">
165-
<span>{int(max_count)}</span>
166-
<span>{int(max_count // 2)}</span>
167-
<span>0</span>
168-
</div>
169-
</div>
136+
<body style="margin:0;">
137+
<div id="container" style="width: 4800px; height: 2700px;"></div>
170138
<script>
171-
Highcharts.chart('container', {{
172-
chart: {{
173-
width: 4800,
174-
height: 2700,
175-
backgroundColor: '#ffffff',
176-
marginRight: 250,
177-
marginBottom: 250,
178-
marginLeft: 150,
179-
marginTop: 120,
180-
animation: false
181-
}},
182-
title: {{
183-
text: 'hexbin-basic \\u00b7 highcharts \\u00b7 pyplots.ai',
184-
style: {{ fontSize: '48px' }}
185-
}},
186-
xAxis: {{
187-
title: {{
188-
text: 'X Value',
189-
style: {{ fontSize: '32px' }},
190-
margin: 20
191-
}},
192-
labels: {{
193-
style: {{ fontSize: '24px' }},
194-
y: 35
195-
}},
196-
gridLineWidth: 1,
197-
gridLineColor: 'rgba(128, 128, 128, 0.3)',
198-
lineWidth: 2,
199-
tickWidth: 2,
200-
tickLength: 10,
201-
min: {x_min:.2f},
202-
max: {x_max:.2f}
203-
}},
204-
yAxis: {{
205-
title: {{
206-
text: 'Y Value',
207-
style: {{ fontSize: '32px' }}
208-
}},
209-
labels: {{
210-
style: {{ fontSize: '24px' }}
211-
}},
212-
gridLineWidth: 1,
213-
gridLineColor: 'rgba(128, 128, 128, 0.3)',
214-
lineWidth: 2,
215-
min: {y_min:.2f},
216-
max: {y_max:.2f}
217-
}},
218-
legend: {{
219-
enabled: false
220-
}},
221-
credits: {{
222-
enabled: false
223-
}},
224-
tooltip: {{
225-
enabled: false
226-
}},
227-
plotOptions: {{
228-
polygon: {{
229-
animation: false,
230-
lineWidth: 0,
231-
states: {{
232-
hover: {{ enabled: false }},
233-
inactive: {{ enabled: false }}
234-
}}
235-
}}
236-
}},
237-
series: {series_js}
238-
}});
139+
Highcharts.chart('container', {options_json});
239140
</script>
240141
</body>
241142
</html>"""
242143

243-
# Write temp HTML
244-
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
245-
f.write(html_content)
246-
temp_path = f.name
247-
248144
# Save interactive HTML
249145
with open("plot.html", "w", encoding="utf-8") as f:
250146
f.write(html_content)
251147

252148
# Take screenshot with headless Chrome
149+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
150+
f.write(html_content)
151+
temp_path = f.name
152+
253153
chrome_options = Options()
254154
chrome_options.add_argument("--headless")
255155
chrome_options.add_argument("--no-sandbox")
@@ -259,7 +159,7 @@ def hexagon_vertices(cx, cy, size):
259159

260160
driver = webdriver.Chrome(options=chrome_options)
261161
driver.get(f"file://{temp_path}")
262-
time.sleep(5) # Wait for chart to render
162+
time.sleep(5)
263163
driver.save_screenshot("plot.png")
264164
driver.quit()
265165

plots/hexbin-basic/metadata/highcharts.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
library: highcharts
22
specification_id: hexbin-basic
33
created: 2025-12-14 22:26:07+00:00
4-
updated: 2025-12-14 22:26:07+00:00
5-
generated_by: claude-opus-4-5-20251101
4+
updated: '2026-02-21T21:02:15+00:00'
5+
generated_by: claude-opus-4-6
66
workflow_run: 20214865327
77
issue: 0
8-
python_version: 3.13.11
8+
python_version: 3.14.3
99
library_version: 1.10.3
1010
preview_url: https://storage.googleapis.com/pyplots-images/plots/hexbin-basic/highcharts/plot.png
1111
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/hexbin-basic/highcharts/plot_thumb.png
1212
preview_html: https://storage.googleapis.com/pyplots-images/plots/hexbin-basic/highcharts/plot.html
13-
quality_score: 91
13+
quality_score: null
1414
impl_tags:
1515
dependencies:
1616
- selenium

0 commit comments

Comments
 (0)