pub mod export;
pub mod matrix;
pub mod perf;
pub mod plot2d;
pub mod plot3d;
pub mod stats;
pub use export::{ExportConfig, ExportFormat, Exporter};
pub use matrix::MatrixPlot;
pub use perf::{BenchmarkResult, PerfPlot, ScalingPoint};
pub use plot2d::Plot2D;
pub use plot3d::Plot3D;
pub use stats::{BinStrategy, StatPlot};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum VizError {
#[error("Plot rendering error: {0}")]
RenderError(String),
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Invalid data: {0}")]
InvalidData(String),
#[error("Invalid configuration: {0}")]
InvalidConfig(String),
#[error("Backend error: {0}")]
BackendError(String),
#[error("Export error: {0}")]
ExportError(String),
#[error("Dimension mismatch: {0}")]
DimensionMismatch(String),
}
pub type VizResult<T> = Result<T, VizError>;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Color {
pub r: f64,
pub g: f64,
pub b: f64,
pub a: f64,
}
impl Color {
pub fn rgb(r: f64, g: f64, b: f64) -> Self {
Self { r, g, b, a: 1.0 }
}
pub fn rgba(r: f64, g: f64, b: f64, a: f64) -> Self {
Self { r, g, b, a }
}
pub fn from_hex(hex: &str) -> VizResult<Self> {
let hex = hex.trim_start_matches('#');
let (r, g, b, a) = match hex.len() {
6 => {
let r = u8::from_str_radix(&hex[0..2], 16)
.map_err(|_| VizError::InvalidData("Invalid hex color".to_string()))?;
let g = u8::from_str_radix(&hex[2..4], 16)
.map_err(|_| VizError::InvalidData("Invalid hex color".to_string()))?;
let b = u8::from_str_radix(&hex[4..6], 16)
.map_err(|_| VizError::InvalidData("Invalid hex color".to_string()))?;
(r, g, b, 255)
}
8 => {
let r = u8::from_str_radix(&hex[0..2], 16)
.map_err(|_| VizError::InvalidData("Invalid hex color".to_string()))?;
let g = u8::from_str_radix(&hex[2..4], 16)
.map_err(|_| VizError::InvalidData("Invalid hex color".to_string()))?;
let b = u8::from_str_radix(&hex[4..6], 16)
.map_err(|_| VizError::InvalidData("Invalid hex color".to_string()))?;
let a = u8::from_str_radix(&hex[6..8], 16)
.map_err(|_| VizError::InvalidData("Invalid hex color".to_string()))?;
(r, g, b, a)
}
_ => {
return Err(VizError::InvalidData(
"Hex color must be 6 or 8 characters".to_string(),
))
}
};
Ok(Self {
r: r as f64 / 255.0,
g: g as f64 / 255.0,
b: b as f64 / 255.0,
a: a as f64 / 255.0,
})
}
pub fn to_rgb_u8(&self) -> (u8, u8, u8) {
(
(self.r * 255.0) as u8,
(self.g * 255.0) as u8,
(self.b * 255.0) as u8,
)
}
pub fn to_rgba_u8(&self) -> (u8, u8, u8, u8) {
(
(self.r * 255.0) as u8,
(self.g * 255.0) as u8,
(self.b * 255.0) as u8,
(self.a * 255.0) as u8,
)
}
pub const BLACK: Color = Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
pub const WHITE: Color = Color {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
};
pub const RED: Color = Color {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
pub const GREEN: Color = Color {
r: 0.0,
g: 1.0,
b: 0.0,
a: 1.0,
};
pub const BLUE: Color = Color {
r: 0.0,
g: 0.0,
b: 1.0,
a: 1.0,
};
pub const YELLOW: Color = Color {
r: 1.0,
g: 1.0,
b: 0.0,
a: 1.0,
};
pub const CYAN: Color = Color {
r: 0.0,
g: 1.0,
b: 1.0,
a: 1.0,
};
pub const MAGENTA: Color = Color {
r: 1.0,
g: 0.0,
b: 1.0,
a: 1.0,
};
pub const ORANGE: Color = Color {
r: 1.0,
g: 0.647,
b: 0.0,
a: 1.0,
};
pub const PURPLE: Color = Color {
r: 0.5,
g: 0.0,
b: 0.5,
a: 1.0,
};
pub const GRAY: Color = Color {
r: 0.5,
g: 0.5,
b: 0.5,
a: 1.0,
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineStyle {
Solid,
Dashed,
Dotted,
DashDot,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MarkerStyle {
Circle,
Square,
Triangle,
Diamond,
Plus,
Cross,
Star,
None,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LineWidth(pub f64);
impl LineWidth {
pub fn new(width: f64) -> VizResult<Self> {
if width > 0.0 {
Ok(Self(width))
} else {
Err(VizError::InvalidConfig(
"Line width must be positive".to_string(),
))
}
}
pub const THIN: LineWidth = LineWidth(1.0);
pub const NORMAL: LineWidth = LineWidth(2.0);
pub const THICK: LineWidth = LineWidth(3.0);
pub const VERY_THICK: LineWidth = LineWidth(5.0);
}
impl Default for LineWidth {
fn default() -> Self {
Self::NORMAL
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlotBackend {
Png,
Svg,
Html,
}
#[derive(Debug, Clone)]
pub struct GridConfig {
pub show_major: bool,
pub show_minor: bool,
pub major_color: Color,
pub minor_color: Color,
pub major_style: LineStyle,
pub minor_style: LineStyle,
}
impl Default for GridConfig {
fn default() -> Self {
Self {
show_major: true,
show_minor: false,
major_color: Color::rgba(0.8, 0.8, 0.8, 0.5),
minor_color: Color::rgba(0.9, 0.9, 0.9, 0.3),
major_style: LineStyle::Solid,
minor_style: LineStyle::Dotted,
}
}
}
#[derive(Debug, Clone)]
pub struct AxisConfig {
pub label: String,
pub min: Option<f64>,
pub max: Option<f64>,
pub log_scale: bool,
pub show: bool,
}
impl Default for AxisConfig {
fn default() -> Self {
Self {
label: String::new(),
min: None,
max: None,
log_scale: false,
show: true,
}
}
}
impl AxisConfig {
pub fn with_label(label: impl Into<String>) -> Self {
Self {
label: label.into(),
..Default::default()
}
}
pub fn with_range(mut self, min: f64, max: f64) -> VizResult<Self> {
if max <= min {
return Err(VizError::InvalidConfig(
"Max must be greater than min".to_string(),
));
}
self.min = Some(min);
self.max = Some(max);
Ok(self)
}
pub fn with_log_scale(mut self) -> Self {
self.log_scale = true;
self
}
}
#[derive(Debug, Clone)]
pub struct LegendConfig {
pub show: bool,
pub position: LegendPosition,
pub background: Color,
pub border_color: Color,
}
impl Default for LegendConfig {
fn default() -> Self {
Self {
show: true,
position: LegendPosition::TopRight,
background: Color::rgba(1.0, 1.0, 1.0, 0.9),
border_color: Color::BLACK,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LegendPosition {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
OutsideRight,
OutsideBottom,
}
#[derive(Debug, Clone)]
pub struct PlotConfig {
pub title: String,
pub x_axis: AxisConfig,
pub y_axis: AxisConfig,
pub grid: GridConfig,
pub legend: LegendConfig,
pub width: u32,
pub height: u32,
pub background: Color,
pub backend: PlotBackend,
}
impl Default for PlotConfig {
fn default() -> Self {
Self {
title: String::new(),
x_axis: AxisConfig::default(),
y_axis: AxisConfig::default(),
grid: GridConfig::default(),
legend: LegendConfig::default(),
width: 800,
height: 600,
background: Color::WHITE,
backend: PlotBackend::Png,
}
}
}
impl PlotConfig {
pub fn with_title(title: impl Into<String>) -> Self {
Self {
title: title.into(),
..Default::default()
}
}
pub fn with_size(mut self, width: u32, height: u32) -> VizResult<Self> {
if width == 0 || height == 0 {
return Err(VizError::InvalidConfig(
"Width and height must be positive".to_string(),
));
}
self.width = width;
self.height = height;
Ok(self)
}
pub fn with_backend(mut self, backend: PlotBackend) -> Self {
self.backend = backend;
self
}
pub fn with_x_label(mut self, label: impl Into<String>) -> Self {
self.x_axis.label = label.into();
self
}
pub fn with_y_label(mut self, label: impl Into<String>) -> Self {
self.y_axis.label = label.into();
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorMap {
Viridis,
Plasma,
Inferno,
Magma,
Coolwarm,
RdBu,
Gray,
Hot,
Jet,
}
impl ColorMap {
pub fn get_color(&self, value: f64) -> Color {
let value = value.clamp(0.0, 1.0);
match self {
ColorMap::Viridis => viridis(value),
ColorMap::Plasma => plasma(value),
ColorMap::Inferno => inferno(value),
ColorMap::Magma => magma(value),
ColorMap::Coolwarm => coolwarm(value),
ColorMap::RdBu => rdbu(value),
ColorMap::Gray => Color::rgb(value, value, value),
ColorMap::Hot => hot(value),
ColorMap::Jet => jet(value),
}
}
}
fn viridis(t: f64) -> Color {
let r = 0.267 + 0.004 * t + 0.328 * t * t;
let g = 0.005 + 0.506 * t + 0.344 * t * t;
let b = 0.329 + 1.112 * t - 0.722 * t * t;
Color::rgb(r, g, b)
}
fn plasma(t: f64) -> Color {
let r = 0.050 + 1.475 * t - 0.823 * t * t;
let g = 0.029 + 0.234 * t + 2.091 * t * t - 2.174 * t * t * t;
let b = 0.533 + 1.206 * t - 2.614 * t * t + 1.909 * t * t * t;
Color::rgb(r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0))
}
fn inferno(t: f64) -> Color {
let r = -0.002 + 1.815 * t - 1.259 * t * t + 0.699 * t * t * t;
let g = 0.001 + 0.209 * t + 1.398 * t * t - 0.908 * t * t * t;
let b = 0.015 + 2.955 * t - 7.677 * t * t + 5.172 * t * t * t;
Color::rgb(r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0))
}
fn magma(t: f64) -> Color {
let r = -0.002 + 1.797 * t - 1.224 * t * t + 0.663 * t * t * t;
let g = 0.001 + 0.226 * t + 1.368 * t * t - 0.896 * t * t * t;
let b = 0.332 + 2.293 * t - 5.538 * t * t + 3.371 * t * t * t;
Color::rgb(r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0))
}
fn coolwarm(t: f64) -> Color {
let r = 0.231 + 2.113 * t - 2.420 * t * t + 1.076 * t * t * t;
let g = 0.299 + 1.205 * t - 1.066 * t * t;
let b = 0.754 - 1.021 * t + 0.267 * t * t;
Color::rgb(r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0))
}
fn rdbu(t: f64) -> Color {
if t < 0.5 {
let s = t * 2.0;
Color::rgb(s, s, 1.0)
} else {
let s = (t - 0.5) * 2.0;
Color::rgb(1.0, 1.0 - s, 1.0 - s)
}
}
fn hot(t: f64) -> Color {
let r = (t * 3.0).clamp(0.0, 1.0);
let g = ((t - 0.333) * 3.0).clamp(0.0, 1.0);
let b = ((t - 0.666) * 3.0).clamp(0.0, 1.0);
Color::rgb(r, g, b)
}
fn jet(t: f64) -> Color {
let r = ((t - 0.5) * 4.0).clamp(0.0, 1.0);
let g = (1.0 - (t - 0.5).abs() * 4.0).clamp(0.0, 1.0);
let b = ((0.5 - t) * 4.0).clamp(0.0, 1.0);
Color::rgb(r, g, b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_from_hex() {
let color = Color::from_hex("#FF0000").expect("Failed to parse hex color");
assert_eq!(color.r, 1.0);
assert_eq!(color.g, 0.0);
assert_eq!(color.b, 0.0);
assert_eq!(color.a, 1.0);
let color = Color::from_hex("#FF000080").expect("Failed to parse hex color with alpha");
assert!((color.a - 0.5019607843137255).abs() < 1e-6);
}
#[test]
fn test_color_to_rgb() {
let color = Color::RED;
let (r, g, b) = color.to_rgb_u8();
assert_eq!(r, 255);
assert_eq!(g, 0);
assert_eq!(b, 0);
}
#[test]
fn test_line_width() {
let width = LineWidth::new(2.5).expect("Failed to create line width");
assert_eq!(width.0, 2.5);
assert!(LineWidth::new(-1.0).is_err());
assert!(LineWidth::new(0.0).is_err());
}
#[test]
fn test_axis_config() {
let axis = AxisConfig::with_label("X Axis");
assert_eq!(axis.label, "X Axis");
let axis = axis.with_range(0.0, 10.0).expect("Failed to set range");
assert_eq!(axis.min, Some(0.0));
assert_eq!(axis.max, Some(10.0));
assert!(AxisConfig::default().with_range(10.0, 5.0).is_err());
}
#[test]
fn test_plot_config() {
let config = PlotConfig::with_title("Test Plot")
.with_x_label("X")
.with_y_label("Y");
assert_eq!(config.title, "Test Plot");
assert_eq!(config.x_axis.label, "X");
assert_eq!(config.y_axis.label, "Y");
let config = config.with_size(1024, 768).expect("Failed to set size");
assert_eq!(config.width, 1024);
assert_eq!(config.height, 768);
assert!(PlotConfig::default().with_size(0, 600).is_err());
}
#[test]
fn test_color_map() {
let color = ColorMap::Viridis.get_color(0.5);
assert!(color.r >= 0.0 && color.r <= 1.0);
assert!(color.g >= 0.0 && color.g <= 1.0);
assert!(color.b >= 0.0 && color.b <= 1.0);
let color = ColorMap::Gray.get_color(-0.5);
assert_eq!(color.r, 0.0);
let color = ColorMap::Gray.get_color(1.5);
assert_eq!(color.r, 1.0);
}
}