use crate::core::error::{PlottingError, Result};
use crate::core::types::{BoundingBox, Point2f};
use std::sync::atomic::{AtomicU32, Ordering};
pub struct DataShaderCanvas {
width: usize,
height: usize,
canvas: Vec<AtomicU32>,
bounds: BoundingBox,
total_points: u64,
}
impl DataShaderCanvas {
pub fn new(width: usize, height: usize) -> Self {
let canvas_size = width * height;
let canvas = (0..canvas_size).map(|_| AtomicU32::new(0)).collect();
Self {
width,
height,
canvas,
bounds: BoundingBox::new(0.0, 1.0, 0.0, 1.0),
total_points: 0,
}
}
pub fn calculate_bounds(points: &[(f64, f64)]) -> Result<BoundingBox> {
if points.is_empty() {
return Err(PlottingError::EmptyDataSet);
}
let mut min_x = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut min_y = f64::INFINITY;
let mut max_y = f64::NEG_INFINITY;
for &(x, y) in points {
min_x = min_x.min(x);
max_x = max_x.max(x);
min_y = min_y.min(y);
max_y = max_y.max(y);
}
Ok(BoundingBox::new(
min_x as f32,
max_x as f32,
min_y as f32,
max_y as f32,
))
}
pub fn with_bounds(width: usize, height: usize, bounds: BoundingBox) -> Self {
let canvas_size = width * height;
let canvas = (0..canvas_size).map(|_| AtomicU32::new(0)).collect();
Self {
width,
height,
canvas,
bounds,
total_points: 0,
}
}
pub fn set_bounds(&mut self, bounds: BoundingBox) {
self.bounds = bounds;
}
pub fn bounds(&self) -> BoundingBox {
self.bounds
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn clear(&self) {
for cell in &self.canvas {
cell.store(0, Ordering::Relaxed);
}
}
fn world_to_grid(&self, point: &Point2f) -> Option<(usize, usize)> {
if point.x < self.bounds.min_x
|| point.x > self.bounds.max_x
|| point.y < self.bounds.min_y
|| point.y > self.bounds.max_y
{
return None;
}
let x_norm = (point.x - self.bounds.min_x) / (self.bounds.max_x - self.bounds.min_x);
let y_norm = (point.y - self.bounds.min_y) / (self.bounds.max_y - self.bounds.min_y);
let grid_x = (x_norm * (self.width - 1) as f32) as usize;
let grid_y = (y_norm * (self.height - 1) as f32) as usize;
Some((grid_x, grid_y))
}
pub fn aggregate(&mut self, points: &[(f64, f64)]) {
let point2f_vec: Vec<Point2f> = points
.iter()
.map(|&(x, y)| Point2f::new(x as f32, y as f32))
.collect();
self.aggregate_points(&point2f_vec);
}
pub fn aggregate_points(&mut self, points: &[Point2f]) {
for point in points {
if let Some((grid_x, grid_y)) = self.world_to_grid(point) {
let idx = grid_y * self.width + grid_x;
if idx < self.canvas.len() {
self.canvas[idx].fetch_add(1, Ordering::Relaxed);
}
}
}
self.total_points += points.len() as u64;
}
pub fn get_count(&self, grid_x: usize, grid_y: usize) -> Option<u32> {
if grid_x >= self.width || grid_y >= self.height {
return None;
}
let idx = grid_y * self.width + grid_x;
Some(self.canvas[idx].load(Ordering::Relaxed))
}
pub fn max_count(&self) -> u32 {
self.canvas
.iter()
.map(|cell| cell.load(Ordering::Relaxed))
.max()
.unwrap_or(0)
}
pub fn statistics(&self) -> DataShaderStats {
let counts: Vec<u32> = self
.canvas
.iter()
.map(|cell| cell.load(Ordering::Relaxed))
.collect();
let non_zero_counts: Vec<u32> = counts.iter().filter(|&&c| c > 0).cloned().collect();
let max_count = *non_zero_counts.iter().max().unwrap_or(&0);
let min_count = *non_zero_counts.iter().min().unwrap_or(&0);
let total_count: u64 = counts.iter().map(|&c| c as u64).sum();
let filled_pixels = non_zero_counts.len();
DataShaderStats {
total_points: self.total_points,
filled_pixels,
total_pixels: self.width * self.height,
max_count,
min_count,
total_count,
canvas_utilization: filled_pixels as f64 / (self.width * self.height) as f64,
}
}
pub fn to_image_data(&self) -> Vec<u8> {
let max_count = self.max_count();
let mut pixels = Vec::with_capacity(self.width * self.height * 4);
for y in 0..self.height {
for x in 0..self.width {
let idx = y * self.width + x;
let count = self.canvas[idx].load(Ordering::Relaxed);
let intensity = if max_count > 0 {
((count as f64 / max_count as f64) * 255.0) as u8
} else {
0
};
pixels.push(intensity); pixels.push(intensity); pixels.push(intensity); pixels.push(255); }
}
pixels
}
}
#[derive(Debug, Clone)]
pub struct DataShaderStats {
pub total_points: u64,
pub filled_pixels: usize,
pub total_pixels: usize,
pub max_count: u32,
pub min_count: u32,
pub total_count: u64,
pub canvas_utilization: f64,
}
pub struct DataShaderImage {
pub width: usize,
pub height: usize,
pub pixels: Vec<u8>,
}
impl DataShaderImage {
pub fn new(width: usize, height: usize, pixels: Vec<u8>) -> Self {
Self {
width,
height,
pixels,
}
}
}
pub struct DataShader {
canvas: DataShaderCanvas,
}
impl Default for DataShader {
fn default() -> Self {
Self::new()
}
}
impl DataShader {
pub fn new() -> Self {
Self::with_canvas_size(512, 512)
}
pub fn with_canvas_size(width: usize, height: usize) -> Self {
Self {
canvas: DataShaderCanvas::new(width, height),
}
}
pub fn should_activate(point_count: usize) -> bool {
point_count >= 100_000
}
pub fn create_canvas(point_count: usize, width: usize, height: usize) -> (usize, usize) {
(width, height)
}
fn validate_explicit_bounds(x_min: f64, x_max: f64, y_min: f64, y_max: f64) -> Result<()> {
if !x_min.is_finite() || !x_max.is_finite() || !y_min.is_finite() || !y_max.is_finite() {
return Err(PlottingError::InvalidInput(format!(
"DataShader bounds must be finite, got x=({x_min}, {x_max}), y=({y_min}, {y_max})"
)));
}
if x_min >= x_max || y_min >= y_max {
return Err(PlottingError::InvalidInput(format!(
"DataShader bounds must satisfy x_min < x_max and y_min < y_max, got x=({x_min}, {x_max}), y=({y_min}, {y_max})"
)));
}
Ok(())
}
pub fn set_bounds(&mut self, min_x: f64, min_y: f64, max_x: f64, max_y: f64) {
let bounds = BoundingBox::new(min_x as f32, max_x as f32, min_y as f32, max_y as f32);
self.canvas.set_bounds(bounds);
}
pub fn aggregate(&mut self, x_data: &[f64], y_data: &[f64]) -> Result<()> {
if x_data.len() != y_data.len() {
return Err(PlottingError::DataLengthMismatch {
x_len: x_data.len(),
y_len: y_data.len(),
series_index: None,
});
}
if x_data.is_empty() {
return Err(PlottingError::EmptyDataSet);
}
let x_min = x_data.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let x_max = x_data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let y_min = y_data.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let y_max = y_data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
self.set_bounds(x_min, y_min, x_max, y_max);
self.aggregate_with_current_bounds(x_data, y_data)
}
pub fn aggregate_with_bounds(
&mut self,
x_data: &[f64],
y_data: &[f64],
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
) -> Result<()> {
if x_data.len() != y_data.len() {
return Err(PlottingError::DataLengthMismatch {
x_len: x_data.len(),
y_len: y_data.len(),
series_index: None,
});
}
if x_data.is_empty() {
return Err(PlottingError::EmptyDataSet);
}
Self::validate_explicit_bounds(x_min, x_max, y_min, y_max)?;
self.set_bounds(x_min, y_min, x_max, y_max);
self.aggregate_with_current_bounds(x_data, y_data)
}
fn aggregate_with_current_bounds(&mut self, x_data: &[f64], y_data: &[f64]) -> Result<()> {
self.canvas.clear();
let points: Vec<(f64, f64)> = x_data
.iter()
.zip(y_data.iter())
.map(|(&x, &y)| (x, y))
.collect();
self.canvas.aggregate(&points);
Ok(())
}
pub fn statistics(&self) -> DataShaderStats {
self.canvas.statistics()
}
pub fn render(&self) -> DataShaderImage {
let pixels = self.canvas.to_image_data();
DataShaderImage::new(self.canvas.width(), self.canvas.height(), pixels)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_datashader_canvas_creation() {
let canvas = DataShaderCanvas::new(800, 600);
assert_eq!(canvas.width(), 800);
assert_eq!(canvas.height(), 600);
}
#[test]
fn test_point_aggregation() {
let mut canvas = DataShaderCanvas::new(10, 10);
canvas.set_bounds(BoundingBox::new(0.0, 1.0, 0.0, 1.0));
let points = vec![
Point2f::new(0.1, 0.1),
Point2f::new(0.12, 0.12),
Point2f::new(0.08, 0.08),
];
canvas.aggregate_points(&points);
assert!(canvas.max_count() > 0);
}
#[test]
fn test_should_activate() {
assert!(!DataShader::should_activate(1000));
assert!(!DataShader::should_activate(50000));
assert!(DataShader::should_activate(100000));
assert!(DataShader::should_activate(150000));
assert!(DataShader::should_activate(1000000));
}
#[test]
fn test_bounds_calculation() {
let points = vec![(1.0, 2.0), (5.0, 8.0), (3.0, 4.0)];
let bounds = DataShaderCanvas::calculate_bounds(&points).unwrap();
assert_eq!(bounds.min_x, 1.0);
assert_eq!(bounds.max_x, 5.0);
assert_eq!(bounds.min_y, 2.0);
assert_eq!(bounds.max_y, 8.0);
}
#[test]
fn test_statistics() {
let mut canvas = DataShaderCanvas::new(10, 10);
canvas.set_bounds(BoundingBox::new(0.0, 1.0, 0.0, 1.0));
let points = vec![Point2f::new(0.1, 0.1), Point2f::new(0.9, 0.9)];
canvas.aggregate_points(&points);
let stats = canvas.statistics();
assert_eq!(stats.total_points, 2);
assert!(stats.filled_pixels > 0);
assert_eq!(stats.total_pixels, 100);
assert!(stats.canvas_utilization > 0.0);
}
#[test]
fn test_datashader_aggregate() {
let mut ds = DataShader::with_canvas_size(100, 100);
let x_data = vec![0.1, 0.2, 0.3, 0.4, 0.5];
let y_data = vec![0.1, 0.2, 0.3, 0.4, 0.5];
let result = ds.aggregate(&x_data, &y_data);
assert!(result.is_ok());
let stats = ds.statistics();
assert_eq!(stats.total_points, 5);
}
#[test]
fn test_datashader_render() {
let mut ds = DataShader::with_canvas_size(10, 10);
let x_data = vec![0.5];
let y_data = vec![0.5];
ds.aggregate(&x_data, &y_data).unwrap();
let image = ds.render();
assert_eq!(image.width, 10);
assert_eq!(image.height, 10);
assert_eq!(image.pixels.len(), 10 * 10 * 4); }
#[test]
fn test_datashader_aggregate_with_explicit_bounds_uses_named_order() {
let mut ds = DataShader::with_canvas_size(16, 16);
let x_data = vec![0.25, 0.75];
let y_data = vec![10.5, 19.5];
ds.aggregate_with_bounds(&x_data, &y_data, 0.0, 1.0, 10.0, 20.0)
.unwrap();
let bounds = ds.canvas.bounds();
assert_eq!(bounds.min_x, 0.0);
assert_eq!(bounds.max_x, 1.0);
assert_eq!(bounds.min_y, 10.0);
assert_eq!(bounds.max_y, 20.0);
}
#[test]
fn test_datashader_aggregate_with_explicit_bounds_rejects_invalid_range() {
let mut ds = DataShader::with_canvas_size(16, 16);
let x_data = vec![0.25, 0.75];
let y_data = vec![10.5, 19.5];
let err = ds
.aggregate_with_bounds(&x_data, &y_data, 1.0, 0.0, 10.0, 20.0)
.unwrap_err();
match err {
PlottingError::InvalidInput(message) => {
assert!(message.contains("x_min < x_max"));
}
other => panic!("expected InvalidInput, got {other:?}"),
}
}
#[test]
fn test_datashader_aggregate_with_explicit_bounds_rejects_non_finite_values() {
let mut ds = DataShader::with_canvas_size(16, 16);
let x_data = vec![0.25, 0.75];
let y_data = vec![10.5, 19.5];
let err = ds
.aggregate_with_bounds(&x_data, &y_data, f64::NAN, 1.0, 10.0, 20.0)
.unwrap_err();
match err {
PlottingError::InvalidInput(message) => {
assert!(message.contains("must be finite"));
}
other => panic!("expected InvalidInput, got {other:?}"),
}
}
}