11""" pyplots.ai
22hexbin-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
78import tempfile
89import time
910import urllib .request
1415from 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
1819np .random .seed (42 )
1920n_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
4837hex_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
95111highcharts_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+
96115with 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
118128html_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
249145with 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+
253153chrome_options = Options ()
254154chrome_options .add_argument ("--headless" )
255155chrome_options .add_argument ("--no-sandbox" )
@@ -259,7 +159,7 @@ def hexagon_vertices(cx, cy, size):
259159
260160driver = webdriver .Chrome (options = chrome_options )
261161driver .get (f"file://{ temp_path } " )
262- time .sleep (5 ) # Wait for chart to render
162+ time .sleep (5 )
263163driver .save_screenshot ("plot.png" )
264164driver .quit ()
265165
0 commit comments