1use crate::artist::HexbinArtist;
8use crate::colormap::Colormap;
9use crate::primitives::Color;
10use std::collections::HashMap;
11
12impl HexbinArtist {
17 pub fn gridsize(&mut self, gridsize: usize) -> &mut Self {
19 self.gridsize = gridsize;
20 self
21 }
22
23 pub fn colormap(&mut self, cmap: Colormap) -> &mut Self {
25 self.cmap = cmap;
26 self
27 }
28
29 pub fn mincnt(&mut self, mincnt: usize) -> &mut Self {
31 self.mincnt = mincnt;
32 self
33 }
34
35 pub fn alpha(&mut self, alpha: f64) -> &mut Self {
37 self.alpha = alpha.clamp(0.0, 1.0);
38 self
39 }
40
41 pub fn label(&mut self, label: &str) -> &mut Self {
43 self.label = Some(label.to_string());
44 self
45 }
46
47 pub fn edgecolor(&mut self, color: Color) -> &mut Self {
49 self.edgecolor = Some(color);
50 self
51 }
52
53 pub fn colorbar(&mut self, show: bool) -> &mut Self {
55 self.show_colorbar = show;
56 self
57 }
58}
59
60#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
70pub struct HexCell {
71 pub q: i64,
73 pub r: i64,
75}
76
77#[derive(Debug, Clone)]
79pub struct HexBinResult {
80 pub cells: Vec<(f64, f64, usize)>,
82 pub max_count: usize,
84 pub min_count: usize,
86}
87
88pub fn hexagon_vertices(cx: f64, cy: f64, size: f64) -> [(f64, f64); 6] {
94 let mut verts = [(0.0, 0.0); 6];
95 for (i, vert) in verts.iter_mut().enumerate() {
96 let angle = std::f64::consts::PI / 3.0 * i as f64;
97 *vert = (cx + size * angle.cos(), cy + size * angle.sin());
98 }
99 verts
100}
101
102pub fn bin_hexagonal(
117 x: &[f64],
118 y: &[f64],
119 gridsize: usize,
120 mincnt: usize,
121) -> HexBinResult {
122 if x.is_empty() || y.is_empty() {
123 return HexBinResult {
124 cells: Vec::new(),
125 max_count: 0,
126 min_count: 0,
127 };
128 }
129
130 let (xmin, xmax, ymin, ymax) = data_extent(x, y);
131 let x_range = (xmax - xmin).max(f64::EPSILON);
132 let _y_range = (ymax - ymin).max(f64::EPSILON);
133
134 let size = x_range / (gridsize as f64 * 1.5 + 0.5);
139 let size = size.max(f64::EPSILON);
140
141 let sqrt3 = 3.0_f64.sqrt();
142
143 let mut counts: HashMap<HexCell, usize> = HashMap::new();
145
146 let n = x.len().min(y.len());
147 for i in 0..n {
148 let px = x[i];
149 let py = y[i];
150 if !px.is_finite() || !py.is_finite() {
151 continue;
152 }
153
154 let fq = (2.0 / 3.0 * (px - xmin)) / size;
156 let fr = (-(px - xmin) / 3.0 + sqrt3 / 3.0 * (py - ymin)) / size;
157
158 let (q, r) = axial_round(fq, fr);
160
161 let cell = HexCell { q, r };
162 *counts.entry(cell).or_insert(0) += 1;
163 }
164
165 let mut cells = Vec::with_capacity(counts.len());
167 let mut max_count = 0usize;
168 let mut min_count = usize::MAX;
169
170 for (cell, count) in &counts {
171 if *count < mincnt {
172 continue;
173 }
174 let cx = xmin + size * (3.0 / 2.0 * cell.q as f64);
176 let cy = ymin + size * (sqrt3 / 2.0 * cell.q as f64 + sqrt3 * cell.r as f64);
177 cells.push((cx, cy, *count));
178 if *count > max_count {
179 max_count = *count;
180 }
181 if *count < min_count {
182 min_count = *count;
183 }
184 }
185
186 if cells.is_empty() {
187 min_count = 0;
188 }
189
190 cells.sort_by(|a, b| {
192 a.1.partial_cmp(&b.1)
193 .unwrap_or(std::cmp::Ordering::Equal)
194 .then(a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal))
195 });
196
197 HexBinResult {
198 cells,
199 max_count,
200 min_count,
201 }
202}
203
204pub fn hex_size_for_gridsize(x_range: f64, gridsize: usize) -> f64 {
209 let x_range = x_range.max(f64::EPSILON);
210 let size = x_range / (gridsize as f64 * 1.5 + 0.5);
211 size.max(f64::EPSILON)
212}
213
214fn axial_round(fq: f64, fr: f64) -> (i64, i64) {
221 let fx = fq;
223 let fz = fr;
224 let fy = -fx - fz;
225
226 let mut rx = fx.round();
227 let mut ry = fy.round();
228 let mut rz = fz.round();
229
230 let dx = (rx - fx).abs();
231 let dy = (ry - fy).abs();
232 let dz = (rz - fz).abs();
233
234 if dx > dy && dx > dz {
235 rx = -ry - rz;
236 } else if dy > dz {
237 ry = -rx - rz;
238 } else {
239 rz = -rx - ry;
240 }
241
242 let _ = ry; (rx as i64, rz as i64)
244}
245
246fn data_extent(x: &[f64], y: &[f64]) -> (f64, f64, f64, f64) {
248 let mut xmin = f64::INFINITY;
249 let mut xmax = f64::NEG_INFINITY;
250 let mut ymin = f64::INFINITY;
251 let mut ymax = f64::NEG_INFINITY;
252
253 for &v in x {
254 if v.is_finite() {
255 if v < xmin { xmin = v; }
256 if v > xmax { xmax = v; }
257 }
258 }
259 for &v in y {
260 if v.is_finite() {
261 if v < ymin { ymin = v; }
262 if v > ymax { ymax = v; }
263 }
264 }
265
266 if !xmin.is_finite() { xmin = 0.0; }
267 if !xmax.is_finite() { xmax = 1.0; }
268 if !ymin.is_finite() { ymin = 0.0; }
269 if !ymax.is_finite() { ymax = 1.0; }
270 if (xmax - xmin).abs() < f64::EPSILON {
271 xmin -= 0.5;
272 xmax += 0.5;
273 }
274 if (ymax - ymin).abs() < f64::EPSILON {
275 ymin -= 0.5;
276 ymax += 0.5;
277 }
278
279 (xmin, xmax, ymin, ymax)
280}
281
282#[cfg(test)]
287mod tests {
288 use super::*;
289 use crate::artist::HexbinArtist;
290
291 fn sample_hexbin() -> HexbinArtist {
292 HexbinArtist {
293 x: vec![0.0, 1.0, 2.0, 3.0, 4.0],
294 y: vec![0.0, 1.0, 2.0, 3.0, 4.0],
295 gridsize: 10,
296 cmap: Colormap::Viridis,
297 mincnt: 1,
298 alpha: 1.0,
299 color: Color::TAB_BLUE,
300 label: None,
301 edgecolor: None,
302 show_colorbar: false,
303 }
304 }
305
306 #[test]
309 fn hexagon_vertices_count() {
310 let verts = hexagon_vertices(0.0, 0.0, 1.0);
311 assert_eq!(verts.len(), 6);
312 }
313
314 #[test]
315 fn hexagon_vertices_symmetry() {
316 let verts = hexagon_vertices(0.0, 0.0, 1.0);
317 assert!((verts[0].0 - 1.0).abs() < 1e-10);
319 assert!((verts[0].1 - 0.0).abs() < 1e-10);
320 assert!((verts[3].0 - (-1.0)).abs() < 1e-10);
322 assert!((verts[3].1 - 0.0).abs() < 1e-10);
323 }
324
325 #[test]
326 fn hexagon_vertices_at_center() {
327 let cx = 5.0;
328 let cy = 3.0;
329 let size = 2.0;
330 let verts = hexagon_vertices(cx, cy, size);
331 assert!((verts[0].0 - (cx + size)).abs() < 1e-10);
333 assert!((verts[0].1 - cy).abs() < 1e-10);
334 }
335
336 #[test]
337 fn hexagon_vertices_equidistant_from_center() {
338 let cx = 1.0;
339 let cy = 2.0;
340 let size = 3.0;
341 let verts = hexagon_vertices(cx, cy, size);
342 for (vx, vy) in &verts {
343 let dist = ((vx - cx).powi(2) + (vy - cy).powi(2)).sqrt();
344 assert!(
345 (dist - size).abs() < 1e-10,
346 "vertex ({vx}, {vy}) distance {dist} should equal size {size}"
347 );
348 }
349 }
350
351 #[test]
354 fn bin_empty_data() {
355 let result = bin_hexagonal(&[], &[], 10, 1);
356 assert!(result.cells.is_empty());
357 assert_eq!(result.max_count, 0);
358 }
359
360 #[test]
361 fn bin_single_point() {
362 let result = bin_hexagonal(&[1.0], &[1.0], 10, 1);
363 assert_eq!(result.cells.len(), 1);
364 assert_eq!(result.cells[0].2, 1); }
366
367 #[test]
368 fn bin_identical_points_same_cell() {
369 let x = vec![5.0; 100];
371 let y = vec![5.0; 100];
372 let result = bin_hexagonal(&x, &y, 10, 1);
373 assert_eq!(result.cells.len(), 1);
374 assert_eq!(result.cells[0].2, 100);
375 assert_eq!(result.max_count, 100);
376 }
377
378 #[test]
379 fn bin_count_accuracy() {
380 let mut x = vec![0.0; 50];
382 let mut y = vec![0.0; 50];
383 x.extend(vec![100.0; 30]);
384 y.extend(vec![100.0; 30]);
385 let result = bin_hexagonal(&x, &y, 5, 1);
386
387 let total: usize = result.cells.iter().map(|c| c.2).sum();
389 assert_eq!(total, 80);
390 assert_eq!(result.max_count, 50);
391 }
392
393 #[test]
394 fn bin_mincnt_filtering() {
395 let mut x = vec![0.0; 10];
397 let mut y = vec![0.0; 10];
398 x.push(100.0);
399 y.push(100.0);
400 let result = bin_hexagonal(&x, &y, 5, 2);
401
402 for cell in &result.cells {
404 assert!(cell.2 >= 2, "all cells should have count >= 2");
405 }
406 }
407
408 #[test]
409 fn bin_gridsize_effect() {
410 let x: Vec<f64> = (0..100).map(|i| i as f64 * 0.1).collect();
412 let y: Vec<f64> = (0..100).map(|i| i as f64 * 0.1).collect();
413
414 let result_coarse = bin_hexagonal(&x, &y, 5, 1);
415 let result_fine = bin_hexagonal(&x, &y, 20, 1);
416
417 assert!(
418 result_fine.cells.len() >= result_coarse.cells.len(),
419 "finer grid ({}) should produce >= cells than coarse grid ({})",
420 result_fine.cells.len(),
421 result_coarse.cells.len()
422 );
423 }
424
425 #[test]
426 fn bin_nan_points_skipped() {
427 let x = vec![1.0, f64::NAN, 3.0];
428 let y = vec![1.0, 2.0, f64::NAN];
429 let result = bin_hexagonal(&x, &y, 10, 1);
430 let total: usize = result.cells.iter().map(|c| c.2).sum();
432 assert_eq!(total, 1);
433 }
434
435 #[test]
438 fn data_bounds_basic() {
439 let h = HexbinArtist {
440 x: vec![1.0, 2.0, 3.0],
441 y: vec![10.0, 20.0, 30.0],
442 gridsize: 10,
443 cmap: Colormap::Viridis,
444 mincnt: 1,
445 alpha: 1.0,
446 color: Color::TAB_BLUE,
447 label: None,
448 edgecolor: None,
449 show_colorbar: false,
450 };
451 let (xmin, xmax, ymin, ymax) = h.data_bounds();
452 assert!((xmin - 1.0).abs() < f64::EPSILON);
453 assert!((xmax - 3.0).abs() < f64::EPSILON);
454 assert!((ymin - 10.0).abs() < f64::EPSILON);
455 assert!((ymax - 30.0).abs() < f64::EPSILON);
456 }
457
458 #[test]
459 fn data_bounds_empty() {
460 let h = HexbinArtist {
461 x: vec![],
462 y: vec![],
463 gridsize: 10,
464 cmap: Colormap::Viridis,
465 mincnt: 1,
466 alpha: 1.0,
467 color: Color::TAB_BLUE,
468 label: None,
469 edgecolor: None,
470 show_colorbar: false,
471 };
472 assert_eq!(h.data_bounds(), (0.0, 1.0, 0.0, 1.0));
473 }
474
475 #[test]
478 fn builder_gridsize() {
479 let mut h = sample_hexbin();
480 h.gridsize(30);
481 assert_eq!(h.gridsize, 30);
482 }
483
484 #[test]
485 fn builder_colormap() {
486 let mut h = sample_hexbin();
487 h.colormap(Colormap::Plasma);
488 assert_eq!(h.cmap, Colormap::Plasma);
489 }
490
491 #[test]
492 fn builder_mincnt() {
493 let mut h = sample_hexbin();
494 h.mincnt(5);
495 assert_eq!(h.mincnt, 5);
496 }
497
498 #[test]
499 fn builder_alpha() {
500 let mut h = sample_hexbin();
501 h.alpha(0.5);
502 assert!((h.alpha - 0.5).abs() < f64::EPSILON);
503 }
504
505 #[test]
506 fn builder_alpha_clamp() {
507 let mut h = sample_hexbin();
508 h.alpha(2.0);
509 assert!((h.alpha - 1.0).abs() < f64::EPSILON);
510 h.alpha(-0.5);
511 assert!((h.alpha - 0.0).abs() < f64::EPSILON);
512 }
513
514 #[test]
515 fn builder_label() {
516 let mut h = sample_hexbin();
517 h.label("hexbin test");
518 assert_eq!(h.label.as_deref(), Some("hexbin test"));
519 }
520
521 #[test]
522 fn builder_edgecolor() {
523 let mut h = sample_hexbin();
524 h.edgecolor(Color::BLACK);
525 assert_eq!(h.edgecolor, Some(Color::BLACK));
526 }
527
528 #[test]
529 fn builder_chaining() {
530 let mut h = sample_hexbin();
531 h.gridsize(15)
532 .colormap(Colormap::Inferno)
533 .mincnt(3)
534 .alpha(0.7)
535 .label("chained")
536 .edgecolor(Color::WHITE);
537 assert_eq!(h.gridsize, 15);
538 assert_eq!(h.cmap, Colormap::Inferno);
539 assert_eq!(h.mincnt, 3);
540 assert!((h.alpha - 0.7).abs() < f64::EPSILON);
541 assert_eq!(h.label.as_deref(), Some("chained"));
542 assert_eq!(h.edgecolor, Some(Color::WHITE));
543 }
544
545 #[test]
548 fn axial_round_at_origin() {
549 let (q, r) = axial_round(0.0, 0.0);
550 assert_eq!(q, 0);
551 assert_eq!(r, 0);
552 }
553
554 #[test]
555 fn axial_round_exact_integer() {
556 let (q, r) = axial_round(3.0, -2.0);
557 assert_eq!(q, 3);
558 assert_eq!(r, -2);
559 }
560
561 #[test]
562 fn axial_round_near_boundary() {
563 let (q1, r1) = axial_round(0.49, 0.0);
565 let (q2, r2) = axial_round(0.51, 0.0);
566 assert!(q1 == 0 || q1 == 1);
568 assert!(q2 == 0 || q2 == 1);
569 let _ = (r1, r2); }
571
572 #[test]
575 fn hex_size_positive() {
576 let size = hex_size_for_gridsize(10.0, 20);
577 assert!(size > 0.0);
578 }
579
580 #[test]
581 fn hex_size_increases_with_range() {
582 let s1 = hex_size_for_gridsize(5.0, 10);
583 let s2 = hex_size_for_gridsize(10.0, 10);
584 assert!(s2 > s1);
585 }
586
587 #[test]
588 fn hex_size_decreases_with_gridsize() {
589 let s1 = hex_size_for_gridsize(10.0, 5);
590 let s2 = hex_size_for_gridsize(10.0, 20);
591 assert!(s2 < s1);
592 }
593}