use crate::core::Result;
use crate::plots::traits::{PlotArea, PlotCompute, PlotConfig, PlotData, PlotRender};
use crate::render::skia::SkiaRenderer;
use crate::render::{Color, LineStyle, Theme};
#[derive(Debug, Clone)]
pub struct ErrorBarConfig {
pub color: Option<Color>,
pub line_width: f32,
pub cap_size: f32,
pub line_style: ErrorLineStyle,
pub xerr: bool,
pub yerr: bool,
pub alpha: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorLineStyle {
Solid,
Dashed,
}
impl Default for ErrorBarConfig {
fn default() -> Self {
Self {
color: None,
line_width: 1.5,
cap_size: 8.0, line_style: ErrorLineStyle::Solid,
xerr: false,
yerr: true,
alpha: 1.0,
}
}
}
impl ErrorBarConfig {
pub fn new() -> Self {
Self::default()
}
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
pub fn line_width(mut self, width: f32) -> Self {
self.line_width = width.max(0.1);
self
}
pub fn cap_size(mut self, size: f32) -> Self {
self.cap_size = size.max(0.0);
self
}
pub fn xerr(mut self, enable: bool) -> Self {
self.xerr = enable;
self
}
pub fn yerr(mut self, enable: bool) -> Self {
self.yerr = enable;
self
}
pub fn alpha(mut self, alpha: f32) -> Self {
self.alpha = alpha.clamp(0.0, 1.0);
self
}
}
impl PlotConfig for ErrorBarConfig {}
pub struct ErrorBarPlot;
#[derive(Debug, Clone)]
pub enum ErrorValues {
Symmetric(Vec<f64>),
Asymmetric(Vec<f64>, Vec<f64>),
}
impl ErrorValues {
pub fn symmetric(errors: Vec<f64>) -> Self {
Self::Symmetric(errors)
}
pub fn asymmetric(lower: Vec<f64>, upper: Vec<f64>) -> Self {
Self::Asymmetric(lower, upper)
}
pub fn bounds_at(&self, idx: usize) -> Option<(f64, f64)> {
match self {
Self::Symmetric(errors) => errors.get(idx).map(|&e| (e, e)),
Self::Asymmetric(lower, upper) => match (lower.get(idx), upper.get(idx)) {
(Some(&l), Some(&u)) => Some((l, u)),
_ => None,
},
}
}
pub fn len(&self) -> usize {
match self {
Self::Symmetric(errors) => errors.len(),
Self::Asymmetric(lower, upper) => lower.len().min(upper.len()),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Debug, Clone, Copy)]
pub struct ErrorBar {
pub x: f64,
pub y: f64,
pub x_lower: f64,
pub x_upper: f64,
pub y_lower: f64,
pub y_upper: f64,
}
impl ErrorBar {
pub fn symmetric_y(x: f64, y: f64, yerr: f64) -> Self {
Self {
x,
y,
x_lower: x,
x_upper: x,
y_lower: y - yerr,
y_upper: y + yerr,
}
}
pub fn symmetric_x(x: f64, y: f64, xerr: f64) -> Self {
Self {
x,
y,
x_lower: x - xerr,
x_upper: x + xerr,
y_lower: y,
y_upper: y,
}
}
pub fn asymmetric_y(x: f64, y: f64, yerr_lower: f64, yerr_upper: f64) -> Self {
Self {
x,
y,
x_lower: x,
x_upper: x,
y_lower: y - yerr_lower,
y_upper: y + yerr_upper,
}
}
pub fn full(
x: f64,
y: f64,
xerr_lower: f64,
xerr_upper: f64,
yerr_lower: f64,
yerr_upper: f64,
) -> Self {
Self {
x,
y,
x_lower: x - xerr_lower,
x_upper: x + xerr_upper,
y_lower: y - yerr_lower,
y_upper: y + yerr_upper,
}
}
pub fn y_line(&self) -> ((f64, f64), (f64, f64)) {
((self.x, self.y_lower), (self.x, self.y_upper))
}
pub fn x_line(&self) -> ((f64, f64), (f64, f64)) {
((self.x_lower, self.y), (self.x_upper, self.y))
}
#[allow(clippy::type_complexity)]
pub fn y_caps(&self, cap_size: f64) -> (((f64, f64), (f64, f64)), ((f64, f64), (f64, f64))) {
let half_cap = cap_size / 2.0;
let lower_cap = (
(self.x - half_cap, self.y_lower),
(self.x + half_cap, self.y_lower),
);
let upper_cap = (
(self.x - half_cap, self.y_upper),
(self.x + half_cap, self.y_upper),
);
(lower_cap, upper_cap)
}
#[allow(clippy::type_complexity)]
pub fn x_caps(&self, cap_size: f64) -> (((f64, f64), (f64, f64)), ((f64, f64), (f64, f64))) {
let half_cap = cap_size / 2.0;
let left_cap = (
(self.x_lower, self.y - half_cap),
(self.x_lower, self.y + half_cap),
);
let right_cap = (
(self.x_upper, self.y - half_cap),
(self.x_upper, self.y + half_cap),
);
(left_cap, right_cap)
}
}
pub fn compute_error_bars(
x: &[f64],
y: &[f64],
yerr: Option<&ErrorValues>,
xerr: Option<&ErrorValues>,
) -> Vec<ErrorBar> {
let n = x.len().min(y.len());
let mut bars = Vec::with_capacity(n);
for i in 0..n {
let (yerr_lower, yerr_upper) = yerr.and_then(|e| e.bounds_at(i)).unwrap_or((0.0, 0.0));
let (xerr_lower, xerr_upper) = xerr.and_then(|e| e.bounds_at(i)).unwrap_or((0.0, 0.0));
bars.push(ErrorBar::full(
x[i], y[i], xerr_lower, xerr_upper, yerr_lower, yerr_upper,
));
}
bars
}
pub fn error_bar_range(bars: &[ErrorBar]) -> ((f64, f64), (f64, f64)) {
if bars.is_empty() {
return ((0.0, 1.0), (0.0, 1.0));
}
let mut x_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for bar in bars {
x_min = x_min.min(bar.x_lower);
x_max = x_max.max(bar.x_upper);
y_min = y_min.min(bar.y_lower);
y_max = y_max.max(bar.y_upper);
}
((x_min, x_max), (y_min, y_max))
}
#[derive(Debug, Clone)]
pub struct ErrorBarData {
pub bars: Vec<ErrorBar>,
pub bounds: ((f64, f64), (f64, f64)),
pub(crate) config: ErrorBarConfig,
}
pub struct ErrorBarInput<'a> {
pub x: &'a [f64],
pub y: &'a [f64],
pub yerr: Option<&'a ErrorValues>,
pub xerr: Option<&'a ErrorValues>,
}
impl<'a> ErrorBarInput<'a> {
pub fn new(x: &'a [f64], y: &'a [f64], yerr: &'a ErrorValues) -> Self {
Self {
x,
y,
yerr: Some(yerr),
xerr: None,
}
}
pub fn with_both(
x: &'a [f64],
y: &'a [f64],
yerr: &'a ErrorValues,
xerr: &'a ErrorValues,
) -> Self {
Self {
x,
y,
yerr: Some(yerr),
xerr: Some(xerr),
}
}
pub fn x_only(x: &'a [f64], y: &'a [f64], xerr: &'a ErrorValues) -> Self {
Self {
x,
y,
yerr: None,
xerr: Some(xerr),
}
}
}
impl PlotCompute for ErrorBarPlot {
type Input<'a> = ErrorBarInput<'a>;
type Config = ErrorBarConfig;
type Output = ErrorBarData;
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);
}
let bars = compute_error_bars(input.x, input.y, input.yerr, input.xerr);
let bounds = error_bar_range(&bars);
Ok(ErrorBarData {
bars,
bounds,
config: config.clone(),
})
}
}
impl PlotData for ErrorBarData {
fn data_bounds(&self) -> ((f64, f64), (f64, f64)) {
self.bounds
}
fn is_empty(&self) -> bool {
self.bars.is_empty()
}
}
impl PlotRender for ErrorBarData {
fn render(
&self,
renderer: &mut SkiaRenderer,
area: &PlotArea,
_theme: &Theme,
color: Color,
) -> Result<()> {
if self.bars.is_empty() {
return Ok(());
}
let config = &self.config;
let bar_color = config.color.unwrap_or(color).with_alpha(config.alpha);
let get_line_style = || match config.line_style {
ErrorLineStyle::Solid => LineStyle::Solid,
ErrorLineStyle::Dashed => LineStyle::Dashed,
};
let ((x_min, x_max), _) = self.bounds;
let x_range = x_max - x_min;
let pixels_per_unit = area.width / x_range as f32;
let cap_size = if pixels_per_unit > 0.0 {
(config.cap_size / pixels_per_unit) as f64
} else {
0.0
};
for bar in &self.bars {
if config.yerr {
let ((x1, y1), (x2, y2)) = bar.y_line();
let (sx1, sy1) = area.data_to_screen(x1, y1);
let (sx2, sy2) = area.data_to_screen(x2, y2);
renderer.draw_line(
sx1,
sy1,
sx2,
sy2,
bar_color,
config.line_width,
get_line_style(),
)?;
let (lower_cap, upper_cap) = bar.y_caps(cap_size);
let (lc1, lc2) = lower_cap;
let (uc1, uc2) = upper_cap;
let (slc1x, slc1y) = area.data_to_screen(lc1.0, lc1.1);
let (slc2x, slc2y) = area.data_to_screen(lc2.0, lc2.1);
renderer.draw_line(
slc1x,
slc1y,
slc2x,
slc2y,
bar_color,
config.line_width,
get_line_style(),
)?;
let (suc1x, suc1y) = area.data_to_screen(uc1.0, uc1.1);
let (suc2x, suc2y) = area.data_to_screen(uc2.0, uc2.1);
renderer.draw_line(
suc1x,
suc1y,
suc2x,
suc2y,
bar_color,
config.line_width,
get_line_style(),
)?;
}
if config.xerr {
let ((x1, y1), (x2, y2)) = bar.x_line();
let (sx1, sy1) = area.data_to_screen(x1, y1);
let (sx2, sy2) = area.data_to_screen(x2, y2);
renderer.draw_line(
sx1,
sy1,
sx2,
sy2,
bar_color,
config.line_width,
get_line_style(),
)?;
let (left_cap, right_cap) = bar.x_caps(cap_size);
let (lc1, lc2) = left_cap;
let (rc1, rc2) = right_cap;
let (slc1x, slc1y) = area.data_to_screen(lc1.0, lc1.1);
let (slc2x, slc2y) = area.data_to_screen(lc2.0, lc2.1);
renderer.draw_line(
slc1x,
slc1y,
slc2x,
slc2y,
bar_color,
config.line_width,
get_line_style(),
)?;
let (src1x, src1y) = area.data_to_screen(rc1.0, rc1.1);
let (src2x, src2y) = area.data_to_screen(rc2.0, rc2.1);
renderer.draw_line(
src1x,
src1y,
src2x,
src2y,
bar_color,
config.line_width,
get_line_style(),
)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_symmetric_y_error() {
let bar = ErrorBar::symmetric_y(1.0, 5.0, 0.5);
assert!((bar.y_lower - 4.5).abs() < 1e-10);
assert!((bar.y_upper - 5.5).abs() < 1e-10);
}
#[test]
fn test_asymmetric_y_error() {
let bar = ErrorBar::asymmetric_y(1.0, 5.0, 0.3, 0.7);
assert!((bar.y_lower - 4.7).abs() < 1e-10);
assert!((bar.y_upper - 5.7).abs() < 1e-10);
}
#[test]
fn test_error_values_symmetric() {
let errors = ErrorValues::symmetric(vec![0.5, 1.0]);
let (lower, upper) = errors.bounds_at(0).unwrap();
assert!((lower - 0.5).abs() < 1e-10);
assert!((upper - 0.5).abs() < 1e-10);
}
#[test]
fn test_error_values_asymmetric() {
let errors = ErrorValues::asymmetric(vec![0.3], vec![0.7]);
let (lower, upper) = errors.bounds_at(0).unwrap();
assert!((lower - 0.3).abs() < 1e-10);
assert!((upper - 0.7).abs() < 1e-10);
}
#[test]
fn test_compute_error_bars() {
let x = vec![1.0, 2.0, 3.0];
let y = vec![10.0, 20.0, 15.0];
let yerr = ErrorValues::symmetric(vec![1.0, 2.0, 1.5]);
let bars = compute_error_bars(&x, &y, Some(&yerr), None);
assert_eq!(bars.len(), 3);
assert!((bars[0].y_lower - 9.0).abs() < 1e-10);
assert!((bars[0].y_upper - 11.0).abs() < 1e-10);
}
#[test]
fn test_error_bar_range() {
let bars = vec![
ErrorBar::symmetric_y(1.0, 5.0, 0.5),
ErrorBar::symmetric_y(2.0, 10.0, 1.0),
];
let ((x_min, x_max), (y_min, y_max)) = error_bar_range(&bars);
assert!((x_min - 1.0).abs() < 1e-10);
assert!((x_max - 2.0).abs() < 1e-10);
assert!((y_min - 4.5).abs() < 1e-10);
assert!((y_max - 11.0).abs() < 1e-10);
}
#[test]
fn test_errorbar_config_implements_plot_config() {
fn assert_plot_config<T: PlotConfig>() {}
assert_plot_config::<ErrorBarConfig>();
}
#[test]
fn test_errorbar_plot_compute_trait() {
use crate::plots::traits::PlotCompute;
let x = vec![1.0, 2.0, 3.0];
let y = vec![10.0, 20.0, 15.0];
let yerr = ErrorValues::symmetric(vec![1.0, 2.0, 1.5]);
let config = ErrorBarConfig::default();
let input = ErrorBarInput::new(&x, &y, &yerr);
let result = ErrorBarPlot::compute(input, &config);
assert!(result.is_ok());
let error_data = result.unwrap();
assert_eq!(error_data.bars.len(), 3);
}
#[test]
fn test_errorbar_plot_compute_empty() {
use crate::plots::traits::PlotCompute;
let x: Vec<f64> = vec![];
let y: Vec<f64> = vec![];
let yerr = ErrorValues::symmetric(vec![]);
let config = ErrorBarConfig::default();
let input = ErrorBarInput::new(&x, &y, &yerr);
let result = ErrorBarPlot::compute(input, &config);
assert!(result.is_err());
}
#[test]
fn test_errorbar_plot_data_trait() {
use crate::plots::traits::{PlotCompute, PlotData};
let x = vec![1.0, 2.0, 3.0];
let y = vec![10.0, 20.0, 15.0];
let yerr = ErrorValues::symmetric(vec![1.0, 2.0, 1.5]);
let config = ErrorBarConfig::default();
let input = ErrorBarInput::new(&x, &y, &yerr);
let error_data = ErrorBarPlot::compute(input, &config).unwrap();
let ((x_min, x_max), (y_min, y_max)) = error_data.data_bounds();
assert!((x_min - 1.0).abs() < 1e-10);
assert!((x_max - 3.0).abs() < 1e-10);
assert!(y_min <= y_max);
assert!(!error_data.is_empty());
}
#[test]
fn test_errorbar_with_both_errors() {
use crate::plots::traits::PlotCompute;
let x = vec![1.0, 2.0];
let y = vec![10.0, 20.0];
let yerr = ErrorValues::symmetric(vec![1.0, 2.0]);
let xerr = ErrorValues::symmetric(vec![0.1, 0.2]);
let config = ErrorBarConfig::default().xerr(true);
let input = ErrorBarInput::with_both(&x, &y, &yerr, &xerr);
let result = ErrorBarPlot::compute(input, &config);
assert!(result.is_ok());
let error_data = result.unwrap();
assert_eq!(error_data.bars.len(), 2);
}
}