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(x: &[f64], y: &[f64], gridsize: usize, mincnt: usize) -> HexBinResult {
117 if x.is_empty() || y.is_empty() {
118 return HexBinResult {
119 cells: Vec::new(),
120 max_count: 0,
121 min_count: 0,
122 };
123 }
124
125 let (xmin, xmax, ymin, ymax) = data_extent(x, y);
126 let x_range = (xmax - xmin).max(f64::EPSILON);
127 let _y_range = (ymax - ymin).max(f64::EPSILON);
128
129 let size = x_range / (gridsize as f64 * 1.5 + 0.5);
134 let size = size.max(f64::EPSILON);
135
136 let sqrt3 = 3.0_f64.sqrt();
137
138 let mut counts: HashMap<HexCell, usize> = HashMap::new();
140
141 let n = x.len().min(y.len());
142 for i in 0..n {
143 let px = x[i];
144 let py = y[i];
145 if !px.is_finite() || !py.is_finite() {
146 continue;
147 }
148
149 let fq = (2.0 / 3.0 * (px - xmin)) / size;
151 let fr = (-(px - xmin) / 3.0 + sqrt3 / 3.0 * (py - ymin)) / size;
152
153 let (q, r) = axial_round(fq, fr);
155
156 let cell = HexCell { q, r };
157 *counts.entry(cell).or_insert(0) += 1;
158 }
159
160 let mut cells = Vec::with_capacity(counts.len());
162 let mut max_count = 0usize;
163 let mut min_count = usize::MAX;
164
165 for (cell, count) in &counts {
166 if *count < mincnt {
167 continue;
168 }
169 let cx = xmin + size * (3.0 / 2.0 * cell.q as f64);
171 let cy = ymin + size * (sqrt3 / 2.0 * cell.q as f64 + sqrt3 * cell.r as f64);
172 cells.push((cx, cy, *count));
173 if *count > max_count {
174 max_count = *count;
175 }
176 if *count < min_count {
177 min_count = *count;
178 }
179 }
180
181 if cells.is_empty() {
182 min_count = 0;
183 }
184
185 cells.sort_by(|a, b| {
187 a.1.partial_cmp(&b.1)
188 .unwrap_or(std::cmp::Ordering::Equal)
189 .then(a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal))
190 });
191
192 HexBinResult {
193 cells,
194 max_count,
195 min_count,
196 }
197}
198
199pub fn hex_size_for_gridsize(x_range: f64, gridsize: usize) -> f64 {
204 let x_range = x_range.max(f64::EPSILON);
205 let size = x_range / (gridsize as f64 * 1.5 + 0.5);
206 size.max(f64::EPSILON)
207}
208
209fn axial_round(fq: f64, fr: f64) -> (i64, i64) {
216 let fx = fq;
218 let fz = fr;
219 let fy = -fx - fz;
220
221 let mut rx = fx.round();
222 let mut ry = fy.round();
223 let mut rz = fz.round();
224
225 let dx = (rx - fx).abs();
226 let dy = (ry - fy).abs();
227 let dz = (rz - fz).abs();
228
229 if dx > dy && dx > dz {
230 rx = -ry - rz;
231 } else if dy > dz {
232 ry = -rx - rz;
233 } else {
234 rz = -rx - ry;
235 }
236
237 let _ = ry; (rx as i64, rz as i64)
239}
240
241fn data_extent(x: &[f64], y: &[f64]) -> (f64, f64, f64, f64) {
243 let mut xmin = f64::INFINITY;
244 let mut xmax = f64::NEG_INFINITY;
245 let mut ymin = f64::INFINITY;
246 let mut ymax = f64::NEG_INFINITY;
247
248 for &v in x {
249 if v.is_finite() {
250 if v < xmin {
251 xmin = v;
252 }
253 if v > xmax {
254 xmax = v;
255 }
256 }
257 }
258 for &v in y {
259 if v.is_finite() {
260 if v < ymin {
261 ymin = v;
262 }
263 if v > ymax {
264 ymax = v;
265 }
266 }
267 }
268
269 if !xmin.is_finite() {
270 xmin = 0.0;
271 }
272 if !xmax.is_finite() {
273 xmax = 1.0;
274 }
275 if !ymin.is_finite() {
276 ymin = 0.0;
277 }
278 if !ymax.is_finite() {
279 ymax = 1.0;
280 }
281 if (xmax - xmin).abs() < f64::EPSILON {
282 xmin -= 0.5;
283 xmax += 0.5;
284 }
285 if (ymax - ymin).abs() < f64::EPSILON {
286 ymin -= 0.5;
287 ymax += 0.5;
288 }
289
290 (xmin, xmax, ymin, ymax)
291}
292
293#[cfg(test)]
298mod tests {
299 use super::*;
300 use crate::artist::HexbinArtist;
301
302 fn sample_hexbin() -> HexbinArtist {
303 HexbinArtist {
304 x: vec![0.0, 1.0, 2.0, 3.0, 4.0],
305 y: vec![0.0, 1.0, 2.0, 3.0, 4.0],
306 gridsize: 10,
307 cmap: Colormap::Viridis,
308 mincnt: 1,
309 alpha: 1.0,
310 color: Color::TAB_BLUE,
311 label: None,
312 edgecolor: None,
313 show_colorbar: false,
314 }
315 }
316
317 #[test]
320 fn hexagon_vertices_count() {
321 let verts = hexagon_vertices(0.0, 0.0, 1.0);
322 assert_eq!(verts.len(), 6);
323 }
324
325 #[test]
326 fn hexagon_vertices_symmetry() {
327 let verts = hexagon_vertices(0.0, 0.0, 1.0);
328 assert!((verts[0].0 - 1.0).abs() < 1e-10);
330 assert!((verts[0].1 - 0.0).abs() < 1e-10);
331 assert!((verts[3].0 - (-1.0)).abs() < 1e-10);
333 assert!((verts[3].1 - 0.0).abs() < 1e-10);
334 }
335
336 #[test]
337 fn hexagon_vertices_at_center() {
338 let cx = 5.0;
339 let cy = 3.0;
340 let size = 2.0;
341 let verts = hexagon_vertices(cx, cy, size);
342 assert!((verts[0].0 - (cx + size)).abs() < 1e-10);
344 assert!((verts[0].1 - cy).abs() < 1e-10);
345 }
346
347 #[test]
348 fn hexagon_vertices_equidistant_from_center() {
349 let cx = 1.0;
350 let cy = 2.0;
351 let size = 3.0;
352 let verts = hexagon_vertices(cx, cy, size);
353 for (vx, vy) in &verts {
354 let dist = ((vx - cx).powi(2) + (vy - cy).powi(2)).sqrt();
355 assert!(
356 (dist - size).abs() < 1e-10,
357 "vertex ({vx}, {vy}) distance {dist} should equal size {size}"
358 );
359 }
360 }
361
362 #[test]
365 fn bin_empty_data() {
366 let result = bin_hexagonal(&[], &[], 10, 1);
367 assert!(result.cells.is_empty());
368 assert_eq!(result.max_count, 0);
369 }
370
371 #[test]
372 fn bin_single_point() {
373 let result = bin_hexagonal(&[1.0], &[1.0], 10, 1);
374 assert_eq!(result.cells.len(), 1);
375 assert_eq!(result.cells[0].2, 1); }
377
378 #[test]
379 fn bin_identical_points_same_cell() {
380 let x = vec![5.0; 100];
382 let y = vec![5.0; 100];
383 let result = bin_hexagonal(&x, &y, 10, 1);
384 assert_eq!(result.cells.len(), 1);
385 assert_eq!(result.cells[0].2, 100);
386 assert_eq!(result.max_count, 100);
387 }
388
389 #[test]
390 fn bin_count_accuracy() {
391 let mut x = vec![0.0; 50];
393 let mut y = vec![0.0; 50];
394 x.extend(vec![100.0; 30]);
395 y.extend(vec![100.0; 30]);
396 let result = bin_hexagonal(&x, &y, 5, 1);
397
398 let total: usize = result.cells.iter().map(|c| c.2).sum();
400 assert_eq!(total, 80);
401 assert_eq!(result.max_count, 50);
402 }
403
404 #[test]
405 fn bin_mincnt_filtering() {
406 let mut x = vec![0.0; 10];
408 let mut y = vec![0.0; 10];
409 x.push(100.0);
410 y.push(100.0);
411 let result = bin_hexagonal(&x, &y, 5, 2);
412
413 for cell in &result.cells {
415 assert!(cell.2 >= 2, "all cells should have count >= 2");
416 }
417 }
418
419 #[test]
420 fn bin_gridsize_effect() {
421 let x: Vec<f64> = (0..100).map(|i| i as f64 * 0.1).collect();
423 let y: Vec<f64> = (0..100).map(|i| i as f64 * 0.1).collect();
424
425 let result_coarse = bin_hexagonal(&x, &y, 5, 1);
426 let result_fine = bin_hexagonal(&x, &y, 20, 1);
427
428 assert!(
429 result_fine.cells.len() >= result_coarse.cells.len(),
430 "finer grid ({}) should produce >= cells than coarse grid ({})",
431 result_fine.cells.len(),
432 result_coarse.cells.len()
433 );
434 }
435
436 #[test]
437 fn bin_nan_points_skipped() {
438 let x = vec![1.0, f64::NAN, 3.0];
439 let y = vec![1.0, 2.0, f64::NAN];
440 let result = bin_hexagonal(&x, &y, 10, 1);
441 let total: usize = result.cells.iter().map(|c| c.2).sum();
443 assert_eq!(total, 1);
444 }
445
446 #[test]
449 fn data_bounds_basic() {
450 let h = HexbinArtist {
451 x: vec![1.0, 2.0, 3.0],
452 y: vec![10.0, 20.0, 30.0],
453 gridsize: 10,
454 cmap: Colormap::Viridis,
455 mincnt: 1,
456 alpha: 1.0,
457 color: Color::TAB_BLUE,
458 label: None,
459 edgecolor: None,
460 show_colorbar: false,
461 };
462 let (xmin, xmax, ymin, ymax) = h.data_bounds();
463 assert!((xmin - 1.0).abs() < f64::EPSILON);
464 assert!((xmax - 3.0).abs() < f64::EPSILON);
465 assert!((ymin - 10.0).abs() < f64::EPSILON);
466 assert!((ymax - 30.0).abs() < f64::EPSILON);
467 }
468
469 #[test]
470 fn data_bounds_empty() {
471 let h = HexbinArtist {
472 x: vec![],
473 y: vec![],
474 gridsize: 10,
475 cmap: Colormap::Viridis,
476 mincnt: 1,
477 alpha: 1.0,
478 color: Color::TAB_BLUE,
479 label: None,
480 edgecolor: None,
481 show_colorbar: false,
482 };
483 assert_eq!(h.data_bounds(), (0.0, 1.0, 0.0, 1.0));
484 }
485
486 #[test]
489 fn builder_gridsize() {
490 let mut h = sample_hexbin();
491 h.gridsize(30);
492 assert_eq!(h.gridsize, 30);
493 }
494
495 #[test]
496 fn builder_colormap() {
497 let mut h = sample_hexbin();
498 h.colormap(Colormap::Plasma);
499 assert_eq!(h.cmap, Colormap::Plasma);
500 }
501
502 #[test]
503 fn builder_mincnt() {
504 let mut h = sample_hexbin();
505 h.mincnt(5);
506 assert_eq!(h.mincnt, 5);
507 }
508
509 #[test]
510 fn builder_alpha() {
511 let mut h = sample_hexbin();
512 h.alpha(0.5);
513 assert!((h.alpha - 0.5).abs() < f64::EPSILON);
514 }
515
516 #[test]
517 fn builder_alpha_clamp() {
518 let mut h = sample_hexbin();
519 h.alpha(2.0);
520 assert!((h.alpha - 1.0).abs() < f64::EPSILON);
521 h.alpha(-0.5);
522 assert!((h.alpha - 0.0).abs() < f64::EPSILON);
523 }
524
525 #[test]
526 fn builder_label() {
527 let mut h = sample_hexbin();
528 h.label("hexbin test");
529 assert_eq!(h.label.as_deref(), Some("hexbin test"));
530 }
531
532 #[test]
533 fn builder_edgecolor() {
534 let mut h = sample_hexbin();
535 h.edgecolor(Color::BLACK);
536 assert_eq!(h.edgecolor, Some(Color::BLACK));
537 }
538
539 #[test]
540 fn builder_chaining() {
541 let mut h = sample_hexbin();
542 h.gridsize(15)
543 .colormap(Colormap::Inferno)
544 .mincnt(3)
545 .alpha(0.7)
546 .label("chained")
547 .edgecolor(Color::WHITE);
548 assert_eq!(h.gridsize, 15);
549 assert_eq!(h.cmap, Colormap::Inferno);
550 assert_eq!(h.mincnt, 3);
551 assert!((h.alpha - 0.7).abs() < f64::EPSILON);
552 assert_eq!(h.label.as_deref(), Some("chained"));
553 assert_eq!(h.edgecolor, Some(Color::WHITE));
554 }
555
556 #[test]
559 fn axial_round_at_origin() {
560 let (q, r) = axial_round(0.0, 0.0);
561 assert_eq!(q, 0);
562 assert_eq!(r, 0);
563 }
564
565 #[test]
566 fn axial_round_exact_integer() {
567 let (q, r) = axial_round(3.0, -2.0);
568 assert_eq!(q, 3);
569 assert_eq!(r, -2);
570 }
571
572 #[test]
573 fn axial_round_near_boundary() {
574 let (q1, r1) = axial_round(0.49, 0.0);
576 let (q2, r2) = axial_round(0.51, 0.0);
577 assert!(q1 == 0 || q1 == 1);
579 assert!(q2 == 0 || q2 == 1);
580 let _ = (r1, r2); }
582
583 #[test]
586 fn hex_size_positive() {
587 let size = hex_size_for_gridsize(10.0, 20);
588 assert!(size > 0.0);
589 }
590
591 #[test]
592 fn hex_size_increases_with_range() {
593 let s1 = hex_size_for_gridsize(5.0, 10);
594 let s2 = hex_size_for_gridsize(10.0, 10);
595 assert!(s2 > s1);
596 }
597
598 #[test]
599 fn hex_size_decreases_with_gridsize() {
600 let s1 = hex_size_for_gridsize(10.0, 5);
601 let s2 = hex_size_for_gridsize(10.0, 20);
602 assert!(s2 < s1);
603 }
604}