use crate::core::Result;
use crate::plots::traits::{PlotArea, PlotCompute, PlotConfig, PlotData, PlotRender};
use crate::render::skia::SkiaRenderer;
use crate::render::{Color, ColorMap, LineStyle, Theme};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct HexbinConfig {
pub gridsize: usize,
pub cmap: String,
pub reduce_fn: ReduceFunction,
pub mincnt: Option<usize>,
pub maxcnt: Option<usize>,
pub edge_color: Option<Color>,
pub edge_width: f32,
pub alpha: f32,
pub log_scale: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReduceFunction {
Count,
Mean,
Sum,
Min,
Max,
Std,
}
impl Default for HexbinConfig {
fn default() -> Self {
Self {
gridsize: 30,
cmap: "viridis".to_string(),
reduce_fn: ReduceFunction::Count,
mincnt: None,
maxcnt: None,
edge_color: None,
edge_width: 0.0,
alpha: 1.0,
log_scale: false,
}
}
}
impl HexbinConfig {
pub fn new() -> Self {
Self::default()
}
pub fn gridsize(mut self, size: usize) -> Self {
self.gridsize = size.max(5);
self
}
pub fn cmap(mut self, cmap: &str) -> Self {
self.cmap = cmap.to_string();
self
}
pub fn reduce_fn(mut self, reduce: ReduceFunction) -> Self {
self.reduce_fn = reduce;
self
}
pub fn mincnt(mut self, cnt: usize) -> Self {
self.mincnt = Some(cnt);
self
}
pub fn edge_color(mut self, color: Color) -> Self {
self.edge_color = Some(color);
self
}
pub fn alpha(mut self, alpha: f32) -> Self {
self.alpha = alpha.clamp(0.0, 1.0);
self
}
pub fn log_scale(mut self, log: bool) -> Self {
self.log_scale = log;
self
}
}
impl PlotConfig for HexbinConfig {}
pub struct Hexbin;
#[derive(Debug, Clone)]
pub struct HexBin {
pub cx: f64,
pub cy: f64,
pub value: f64,
pub count: usize,
pub vertices: [(f64, f64); 6],
}
impl HexBin {
pub fn compute_vertices(cx: f64, cy: f64, size: f64) -> [(f64, f64); 6] {
let mut vertices = [(0.0, 0.0); 6];
for (i, vertex) in vertices.iter_mut().enumerate() {
let angle = std::f64::consts::PI / 3.0 * i as f64;
*vertex = (cx + size * angle.cos(), cy + size * angle.sin());
}
vertices
}
}
#[derive(Debug, Clone)]
pub struct HexbinPlotData {
pub bins: Vec<HexBin>,
pub hex_size: f64,
pub value_range: (f64, f64),
pub bounds: ((f64, f64), (f64, f64)),
pub(crate) config: HexbinConfig,
}
pub struct HexbinInput<'a> {
pub x: &'a [f64],
pub y: &'a [f64],
pub values: Option<&'a [f64]>,
}
impl<'a> HexbinInput<'a> {
pub fn new(x: &'a [f64], y: &'a [f64]) -> Self {
Self { x, y, values: None }
}
pub fn with_values(x: &'a [f64], y: &'a [f64], values: &'a [f64]) -> Self {
Self {
x,
y,
values: Some(values),
}
}
}
fn hex_index(x: f64, y: f64, size: f64) -> (i64, i64) {
let q = (2.0 / 3.0 * x) / size;
let r = (-1.0 / 3.0 * x + 3.0_f64.sqrt() / 3.0 * y) / size;
hex_round(q, r)
}
fn hex_round(q: f64, r: f64) -> (i64, i64) {
let s = -q - r;
let mut rq = q.round();
let mut rr = r.round();
let rs = s.round();
let q_diff = (rq - q).abs();
let r_diff = (rr - r).abs();
let s_diff = (rs - s).abs();
if q_diff > r_diff && q_diff > s_diff {
rq = -rr - rs;
} else if r_diff > s_diff {
rr = -rq - rs;
}
(rq as i64, rr as i64)
}
fn hex_to_center(q: i64, r: i64, size: f64) -> (f64, f64) {
let x = size * (3.0 / 2.0 * q as f64);
let y = size * (3.0_f64.sqrt() / 2.0 * q as f64 + 3.0_f64.sqrt() * r as f64);
(x, y)
}
pub fn compute_hexbin(
x: &[f64],
y: &[f64],
values: Option<&[f64]>,
config: &HexbinConfig,
) -> HexbinPlotData {
if x.is_empty() || y.is_empty() {
return HexbinPlotData {
bins: vec![],
hex_size: 1.0,
value_range: (0.0, 1.0),
bounds: ((0.0, 1.0), (0.0, 1.0)),
config: config.clone(),
};
}
let n = x.len().min(y.len());
let x_min = x.iter().copied().fold(f64::INFINITY, f64::min);
let x_max = x.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let y_min = y.iter().copied().fold(f64::INFINITY, f64::min);
let y_max = y.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let x_range = x_max - x_min;
let hex_size = x_range / (config.gridsize as f64 * 1.5);
let mut bin_data: HashMap<(i64, i64), Vec<f64>> = HashMap::new();
for i in 0..n {
let (q, r) = hex_index(x[i] - x_min, y[i] - y_min, hex_size);
let val = values.map_or(1.0, |v| v.get(i).copied().unwrap_or(1.0));
bin_data.entry((q, r)).or_default().push(val);
}
let mut bins = Vec::new();
let mut min_value = f64::INFINITY;
let mut max_value = f64::NEG_INFINITY;
for ((q, r), vals) in bin_data {
let count = vals.len();
if let Some(min) = config.mincnt {
if count < min {
continue;
}
}
let value = match config.reduce_fn {
ReduceFunction::Count => count as f64,
ReduceFunction::Mean => vals.iter().sum::<f64>() / count as f64,
ReduceFunction::Sum => vals.iter().sum(),
ReduceFunction::Min => vals.iter().copied().fold(f64::INFINITY, f64::min),
ReduceFunction::Max => vals.iter().copied().fold(f64::NEG_INFINITY, f64::max),
ReduceFunction::Std => {
let mean = vals.iter().sum::<f64>() / count as f64;
let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / count as f64;
variance.sqrt()
}
};
min_value = min_value.min(value);
max_value = max_value.max(value);
let (cx, cy) = hex_to_center(q, r, hex_size);
let vertices = HexBin::compute_vertices(cx + x_min, cy + y_min, hex_size);
bins.push(HexBin {
cx: cx + x_min,
cy: cy + y_min,
value,
count,
vertices,
});
}
if let Some(max) = config.maxcnt {
max_value = max_value.min(max as f64);
}
if config.log_scale && min_value > 0.0 {
for bin in &mut bins {
bin.value = bin.value.ln();
}
min_value = min_value.ln();
max_value = max_value.ln();
}
HexbinPlotData {
bins,
hex_size,
value_range: (min_value, max_value),
bounds: ((x_min, x_max), (y_min, y_max)),
config: config.clone(),
}
}
pub fn hexbin_range(data: &HexbinPlotData) -> ((f64, f64), (f64, f64)) {
data.bounds
}
impl PlotCompute for Hexbin {
type Input<'a> = HexbinInput<'a>;
type Config = HexbinConfig;
type Output = HexbinPlotData;
fn compute(input: Self::Input<'_>, config: &Self::Config) -> Result<Self::Output> {
if input.x.is_empty() || input.y.is_empty() {
return Err(crate::core::PlottingError::EmptyDataSet);
}
Ok(compute_hexbin(input.x, input.y, input.values, config))
}
}
impl PlotData for HexbinPlotData {
fn data_bounds(&self) -> ((f64, f64), (f64, f64)) {
self.bounds
}
fn is_empty(&self) -> bool {
self.bins.is_empty()
}
}
impl PlotRender for HexbinPlotData {
fn render(
&self,
renderer: &mut SkiaRenderer,
area: &PlotArea,
_theme: &Theme,
_color: Color,
) -> Result<()> {
if self.bins.is_empty() {
return Ok(());
}
let config = &self.config;
let (min_value, max_value) = self.value_range;
let value_range = max_value - min_value;
let cmap = ColorMap::by_name(&config.cmap).unwrap_or_else(ColorMap::viridis);
for bin in &self.bins {
let t = if value_range > 0.0 {
(bin.value - min_value) / value_range
} else {
0.5
};
let fill_color = cmap.sample(t).with_alpha(config.alpha);
let screen_vertices: Vec<(f32, f32)> = bin
.vertices
.iter()
.map(|(x, y)| area.data_to_screen(*x, *y))
.collect();
renderer.draw_filled_polygon(&screen_vertices, fill_color)?;
if let Some(edge_color) = config.edge_color {
if config.edge_width > 0.0 {
for i in 0..6 {
let (x1, y1) = screen_vertices[i];
let (x2, y2) = screen_vertices[(i + 1) % 6];
renderer.draw_line(
x1,
y1,
x2,
y2,
edge_color,
config.edge_width,
LineStyle::Solid,
)?;
}
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hexbin_basic() {
let x: Vec<f64> = (0..100).map(|i| (i as f64) / 10.0).collect();
let y: Vec<f64> = (0..100).map(|i| ((i as f64) / 10.0).sin()).collect();
let config = HexbinConfig::default().gridsize(10);
let data = compute_hexbin(&x, &y, None, &config);
assert!(!data.bins.is_empty());
for bin in &data.bins {
assert!(bin.count >= 1);
}
}
#[test]
fn test_hexbin_with_values() {
let x = vec![0.0, 0.1, 0.2, 1.0, 1.1, 1.2];
let y = vec![0.0, 0.1, 0.2, 0.0, 0.1, 0.2];
let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let config = HexbinConfig::default()
.gridsize(5)
.reduce_fn(ReduceFunction::Mean);
let data = compute_hexbin(&x, &y, Some(&values), &config);
assert!(!data.bins.is_empty());
}
#[test]
fn test_hexbin_mincnt() {
let x = vec![0.0, 0.0, 0.0, 10.0];
let y = vec![0.0, 0.0, 0.0, 10.0];
let config = HexbinConfig::default().gridsize(5).mincnt(2);
let data = compute_hexbin(&x, &y, None, &config);
for bin in &data.bins {
assert!(bin.count >= 2);
}
}
#[test]
fn test_hex_vertices() {
let vertices = HexBin::compute_vertices(0.0, 0.0, 1.0);
assert_eq!(vertices.len(), 6);
for (x, y) in vertices {
let dist = (x * x + y * y).sqrt();
assert!((dist - 1.0).abs() < 1e-10);
}
}
#[test]
fn test_hexbin_empty() {
let x: Vec<f64> = vec![];
let y: Vec<f64> = vec![];
let config = HexbinConfig::default();
let data = compute_hexbin(&x, &y, None, &config);
assert!(data.bins.is_empty());
}
#[test]
fn test_hexbin_config_implements_plot_config() {
fn assert_plot_config<T: PlotConfig>() {}
assert_plot_config::<HexbinConfig>();
}
#[test]
fn test_hexbin_plot_compute_trait() {
use crate::plots::traits::PlotCompute;
let x: Vec<f64> = (0..100).map(|i| (i as f64) / 10.0).collect();
let y: Vec<f64> = (0..100).map(|i| ((i as f64) / 10.0).sin()).collect();
let config = HexbinConfig::default().gridsize(10);
let input = HexbinInput::new(&x, &y);
let result = Hexbin::compute(input, &config);
assert!(result.is_ok());
let hexbin_data = result.unwrap();
assert!(!hexbin_data.bins.is_empty());
}
#[test]
fn test_hexbin_plot_compute_empty() {
use crate::plots::traits::PlotCompute;
let x: Vec<f64> = vec![];
let y: Vec<f64> = vec![];
let config = HexbinConfig::default();
let input = HexbinInput::new(&x, &y);
let result = Hexbin::compute(input, &config);
assert!(result.is_err());
}
#[test]
fn test_hexbin_plot_data_trait() {
use crate::plots::traits::{PlotCompute, PlotData};
let x: Vec<f64> = (0..50).map(|i| (i as f64) / 5.0).collect();
let y: Vec<f64> = (0..50).map(|i| ((i as f64) / 5.0).cos()).collect();
let config = HexbinConfig::default().gridsize(5);
let input = HexbinInput::new(&x, &y);
let hexbin_data = Hexbin::compute(input, &config).unwrap();
let ((x_min, x_max), (y_min, y_max)) = hexbin_data.data_bounds();
assert!(x_min <= x_max);
assert!(y_min <= y_max);
assert!(!hexbin_data.is_empty());
}
#[test]
fn test_hexbin_plot_compute_with_values() {
use crate::plots::traits::PlotCompute;
let x = vec![0.0, 0.1, 0.2, 1.0, 1.1, 1.2];
let y = vec![0.0, 0.1, 0.2, 0.0, 0.1, 0.2];
let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let config = HexbinConfig::default()
.gridsize(5)
.reduce_fn(ReduceFunction::Mean);
let input = HexbinInput::with_values(&x, &y, &values);
let result = Hexbin::compute(input, &config);
assert!(result.is_ok());
let hexbin_data = result.unwrap();
assert!(!hexbin_data.bins.is_empty());
}
}