use crate::core::Result;
use crate::plots::traits::{PlotArea, PlotCompute, PlotConfig, PlotData, PlotRender};
use crate::render::primitives::{Wedge, pie_wedges};
use crate::render::{Color, SkiaRenderer, Theme};
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct PieConfig {
pub labels: Vec<String>,
pub colors: Option<Vec<Color>>,
pub explode: Vec<f64>,
pub show_percentages: bool,
pub show_values: bool,
pub show_labels: bool,
pub inner_radius: f64,
pub start_angle: f64,
pub counter_clockwise: bool,
pub text_color: Color,
pub label_font_size: f32,
pub label_distance: f64,
pub shadow: f64,
pub edge_color: Option<Color>,
pub edge_width: f32,
}
impl Default for PieConfig {
fn default() -> Self {
Self {
labels: vec![],
colors: None,
explode: vec![],
show_percentages: true,
show_values: false,
show_labels: true,
inner_radius: 0.0,
start_angle: 90.0, counter_clockwise: true,
text_color: Color::new(0, 0, 0),
label_font_size: 10.0,
label_distance: 0.6,
shadow: 0.0,
edge_color: Some(Color::new(255, 255, 255)),
edge_width: 1.0,
}
}
}
impl PieConfig {
pub fn new(labels: Vec<String>) -> Self {
Self {
labels,
..Default::default()
}
}
pub fn colors(mut self, colors: Vec<Color>) -> Self {
self.colors = Some(colors);
self
}
pub fn explode(mut self, explode: Vec<f64>) -> Self {
self.explode = explode;
self
}
pub fn donut(mut self, inner_radius: f64) -> Self {
self.inner_radius = inner_radius.clamp(0.0, 0.95);
self
}
pub fn start_angle(mut self, angle: f64) -> Self {
self.start_angle = angle;
self
}
pub fn clockwise(mut self) -> Self {
self.counter_clockwise = false;
self
}
pub fn percentages(mut self, show: bool) -> Self {
self.show_percentages = show;
self
}
pub fn values(mut self, show: bool) -> Self {
self.show_values = show;
self
}
pub fn labels(mut self, show: bool) -> Self {
self.show_labels = show;
self
}
pub fn font_size(mut self, size: f32) -> Self {
self.label_font_size = size;
self
}
pub fn label_distance(mut self, distance: f64) -> Self {
self.label_distance = distance;
self
}
pub fn edge_color(mut self, color: Color) -> Self {
self.edge_color = Some(color);
self
}
pub fn no_edge(mut self) -> Self {
self.edge_color = None;
self
}
}
impl PlotConfig for PieConfig {}
pub struct Pie;
#[derive(Debug, Clone)]
pub struct PieData {
pub values: Vec<f64>,
pub wedges: Vec<Wedge>,
pub total: f64,
pub percentages: Vec<f64>,
pub start_angles: Vec<f64>,
pub end_angles: Vec<f64>,
pub(crate) config: PieConfig,
}
impl PieData {
pub fn from_values(values: &[f64], cx: f64, cy: f64, radius: f64, config: &PieConfig) -> Self {
let positive_values: Vec<f64> = values.iter().filter(|&&v| v > 0.0).copied().collect();
let total: f64 = positive_values.iter().sum();
let percentages: Vec<f64> = if total > 0.0 {
positive_values.iter().map(|v| v / total * 100.0).collect()
} else {
vec![0.0; positive_values.len()]
};
let start_angle_rad = config.start_angle * PI / 180.0 - PI / 2.0;
let mut wedges = pie_wedges(&positive_values, cx, cy, radius, Some(start_angle_rad));
let mut start_angles = Vec::with_capacity(wedges.len());
let mut end_angles = Vec::with_capacity(wedges.len());
for (i, wedge) in wedges.iter_mut().enumerate() {
start_angles.push(wedge.start_angle);
end_angles.push(wedge.end_angle);
if config.inner_radius > 0.0 {
*wedge = wedge.inner_radius(radius * config.inner_radius);
}
if i < config.explode.len() && config.explode[i] > 0.0 {
*wedge = wedge.explode(config.explode[i] * radius * 0.1);
}
}
Self {
values: positive_values,
wedges,
total,
percentages,
start_angles,
end_angles,
config: config.clone(),
}
}
pub fn compute(values: &[f64], config: &PieConfig) -> Self {
Self::from_values(values, 0.5, 0.5, 0.5, config)
}
}
pub fn render_pie(
renderer: &mut SkiaRenderer,
values: &[f64],
cx: f64,
cy: f64,
radius: f64,
config: &PieConfig,
theme: &Theme,
) -> crate::core::Result<PieData> {
let pie_data = PieData::from_values(values, cx, cy, radius, config);
if pie_data.wedges.is_empty() {
return Ok(pie_data);
}
let colors = if let Some(ref colors) = config.colors {
colors.clone()
} else {
let palette = theme.color_palette.clone();
(0..pie_data.wedges.len())
.map(|i| palette[i % palette.len()])
.collect()
};
let segments = 64;
if config.shadow > 0.0 {
let shadow_color = Color::new(100, 100, 100).with_alpha(0.3);
let offset = config.shadow;
for wedge in &pie_data.wedges {
let mut shadow_wedge = *wedge;
let polygon = shadow_wedge.as_polygon(segments);
let shadow_polygon: Vec<(f32, f32)> = polygon
.iter()
.map(|(x, y)| ((x + offset) as f32, (y + offset) as f32))
.collect();
renderer.draw_filled_polygon(&shadow_polygon, shadow_color)?;
}
}
for (i, wedge) in pie_data.wedges.iter().enumerate() {
let color = colors[i % colors.len()];
let polygon = wedge.as_polygon(segments);
let polygon_f32: Vec<(f32, f32)> = polygon
.iter()
.map(|(x, y)| (*x as f32, *y as f32))
.collect();
renderer.draw_filled_polygon(&polygon_f32, color)?;
if let Some(edge_color) = config.edge_color {
renderer.draw_polygon_outline(&polygon_f32, edge_color, config.edge_width)?;
}
}
if config.show_labels || config.show_percentages || config.show_values {
for (i, wedge) in pie_data.wedges.iter().enumerate() {
let label_parts: Vec<String> = [
if config.show_labels && i < config.labels.len() {
Some(config.labels[i].clone())
} else {
None
},
if config.show_percentages {
Some(format!("{:.1}%", pie_data.percentages[i]))
} else {
None
},
if config.show_values {
Some(format!("{:.1}", pie_data.values[i]))
} else {
None
},
]
.into_iter()
.flatten()
.collect();
if !label_parts.is_empty() {
let label = label_parts.join("\n");
let label_r = if config.inner_radius > 0.0 {
radius * (1.0 + config.inner_radius) / 2.0 * config.label_distance
} else {
radius * config.label_distance
};
let (lx, ly) = wedge.centroid();
let mid_angle = (wedge.start_angle + wedge.end_angle) / 2.0;
let label_x = cx + label_r * mid_angle.cos();
let label_y = cy + label_r * mid_angle.sin();
renderer.draw_text_centered(
&label,
label_x as f32,
label_y as f32,
config.label_font_size,
config.text_color,
)?;
}
}
}
Ok(pie_data)
}
impl PlotCompute for Pie {
type Input<'a> = &'a [f64];
type Config = PieConfig;
type Output = PieData;
fn compute(input: Self::Input<'_>, config: &Self::Config) -> Result<Self::Output> {
let positive_count = input.iter().filter(|&&v| v > 0.0).count();
if positive_count == 0 {
return Err(crate::core::PlottingError::EmptyDataSet);
}
Ok(PieData::compute(input, config))
}
}
impl PlotData for PieData {
fn data_bounds(&self) -> ((f64, f64), (f64, f64)) {
((0.0, 1.0), (0.0, 1.0))
}
fn is_empty(&self) -> bool {
self.values.is_empty()
}
}
impl PlotRender for PieData {
fn render(
&self,
renderer: &mut SkiaRenderer,
area: &PlotArea,
theme: &Theme,
_color: Color,
) -> Result<()> {
if self.wedges.is_empty() {
return Ok(());
}
let config = &self.config;
let (cx, cy) = area.data_to_screen(0.5, 0.5);
let (edge_x, _) = area.data_to_screen(1.0, 0.5);
let (_, edge_y) = area.data_to_screen(0.5, 1.0);
let radius = ((edge_x - cx).abs().min((edge_y - cy).abs())) * 0.9;
let screen_data =
PieData::from_values(&self.values, cx as f64, cy as f64, radius as f64, config);
let colors = if let Some(ref colors) = config.colors {
colors.clone()
} else {
let palette = theme.color_palette.clone();
(0..screen_data.wedges.len())
.map(|i| palette[i % palette.len()])
.collect()
};
let segments = 64;
if config.shadow > 0.0 {
let shadow_color = Color::new(100, 100, 100).with_alpha(0.3);
let offset = config.shadow;
for wedge in &screen_data.wedges {
let polygon = wedge.as_polygon(segments);
let shadow_polygon: Vec<(f32, f32)> = polygon
.iter()
.map(|(x, y)| ((x + offset) as f32, (y + offset) as f32))
.collect();
renderer.draw_filled_polygon(&shadow_polygon, shadow_color)?;
}
}
for (i, wedge) in screen_data.wedges.iter().enumerate() {
let color = colors[i % colors.len()];
let polygon = wedge.as_polygon(segments);
let polygon_f32: Vec<(f32, f32)> = polygon
.iter()
.map(|(x, y)| (*x as f32, *y as f32))
.collect();
renderer.draw_filled_polygon(&polygon_f32, color)?;
if let Some(edge_color) = config.edge_color {
renderer.draw_polygon_outline(&polygon_f32, edge_color, config.edge_width)?;
}
}
if config.show_labels || config.show_percentages || config.show_values {
for (i, wedge) in screen_data.wedges.iter().enumerate() {
let label_parts: Vec<String> = [
if config.show_labels && i < config.labels.len() {
Some(config.labels[i].clone())
} else {
None
},
if config.show_percentages {
Some(format!("{:.1}%", screen_data.percentages[i]))
} else {
None
},
if config.show_values {
Some(format!("{:.1}", screen_data.values[i]))
} else {
None
},
]
.into_iter()
.flatten()
.collect();
if !label_parts.is_empty() {
let label = label_parts.join("\n");
let label_r = if config.inner_radius > 0.0 {
radius as f64 * (1.0 + config.inner_radius) / 2.0 * config.label_distance
} else {
radius as f64 * config.label_distance
};
let mid_angle = (wedge.start_angle + wedge.end_angle) / 2.0;
let label_x = cx as f64 + label_r * mid_angle.cos();
let label_y = cy as f64 + label_r * mid_angle.sin();
renderer.draw_text_centered(
&label,
label_x as f32,
label_y as f32,
config.label_font_size,
config.text_color,
)?;
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pie_data_basic() {
let values = vec![30.0, 20.0, 50.0];
let config = PieConfig::default();
let data = PieData::from_values(&values, 100.0, 100.0, 50.0, &config);
assert_eq!(data.wedges.len(), 3);
assert!((data.total - 100.0).abs() < 1e-10);
assert!((data.percentages[0] - 30.0).abs() < 1e-10);
assert!((data.percentages[1] - 20.0).abs() < 1e-10);
assert!((data.percentages[2] - 50.0).abs() < 1e-10);
}
#[test]
fn test_pie_config_donut() {
let config = PieConfig::default().donut(0.5);
assert!((config.inner_radius - 0.5).abs() < 1e-10);
}
#[test]
fn test_pie_data_with_explode() {
let values = vec![25.0, 25.0, 50.0];
let config = PieConfig::default().explode(vec![0.1, 0.0, 0.0]);
let data = PieData::from_values(&values, 100.0, 100.0, 50.0, &config);
assert_eq!(data.wedges.len(), 3);
assert!(data.wedges[0].explode > 0.0);
assert!((data.wedges[1].explode - 0.0).abs() < 1e-10);
}
#[test]
fn test_pie_ignores_negative() {
let values = vec![30.0, -10.0, 20.0];
let config = PieConfig::default();
let data = PieData::from_values(&values, 100.0, 100.0, 50.0, &config);
assert_eq!(data.wedges.len(), 2);
}
#[test]
fn test_pie_config_implements_plot_config() {
fn assert_plot_config<T: PlotConfig>() {}
assert_plot_config::<PieConfig>();
}
#[test]
fn test_pie_plot_compute_trait() {
use crate::plots::traits::PlotCompute;
let values = vec![30.0, 20.0, 50.0];
let config = PieConfig::default();
let result = Pie::compute(&values, &config);
assert!(result.is_ok());
let pie_data = result.unwrap();
assert_eq!(pie_data.wedges.len(), 3);
assert!((pie_data.total - 100.0).abs() < 1e-10);
}
#[test]
fn test_pie_plot_compute_empty() {
use crate::plots::traits::PlotCompute;
let values: Vec<f64> = vec![];
let config = PieConfig::default();
let result = Pie::compute(&values, &config);
assert!(result.is_err());
}
#[test]
fn test_pie_plot_compute_all_negative() {
use crate::plots::traits::PlotCompute;
let values = vec![-10.0, -20.0];
let config = PieConfig::default();
let result = Pie::compute(&values, &config);
assert!(result.is_err());
}
#[test]
fn test_pie_plot_data_trait() {
use crate::plots::traits::{PlotCompute, PlotData};
let values = vec![30.0, 20.0, 50.0];
let config = PieConfig::default();
let pie_data = Pie::compute(&values, &config).unwrap();
let ((x_min, x_max), (y_min, y_max)) = pie_data.data_bounds();
assert!((x_min - 0.0).abs() < 1e-10);
assert!((x_max - 1.0).abs() < 1e-10);
assert!((y_min - 0.0).abs() < 1e-10);
assert!((y_max - 1.0).abs() < 1e-10);
assert!(!pie_data.is_empty());
}
}