Skip to main content

runmat_plot/plots/
pie.rs

1//! Pie chart (2D) implementation using triangle fan
2
3use crate::core::{BoundingBox, DrawCall, Material, PipelineType, RenderData, Vertex};
4use glam::{Vec3, Vec4};
5use std::f32::consts::PI;
6
7#[derive(Debug, Clone)]
8pub struct PieSliceMeta {
9    pub label: String,
10    pub color: Vec4,
11    pub mid_angle: f32,
12    pub offset: Vec3,
13    pub fraction: f32,
14}
15
16#[derive(Debug, Clone)]
17pub struct PieChart {
18    pub values: Vec<f64>,
19    pub colors: Vec<Vec4>,
20    pub label: Option<String>,
21    pub slice_labels: Vec<String>,
22    pub label_format: Option<String>,
23    pub explode: Vec<bool>,
24    pub visible: bool,
25    vertices: Option<Vec<Vertex>>,
26    indices: Option<Vec<u32>>,
27    bounds: Option<BoundingBox>,
28    slices: Option<Vec<PieSliceMeta>>,
29    dirty: bool,
30}
31
32impl PieChart {
33    pub fn new(values: Vec<f64>, colors: Option<Vec<Vec4>>) -> Result<Self, String> {
34        if values.is_empty() {
35            return Err("pie: empty values".to_string());
36        }
37        let mut v = Self {
38            values,
39            colors: colors.unwrap_or_default(),
40            label: None,
41            slice_labels: Vec::new(),
42            label_format: None,
43            explode: Vec::new(),
44            visible: true,
45            vertices: None,
46            indices: None,
47            bounds: None,
48            slices: None,
49            dirty: true,
50        };
51        if v.colors.is_empty() {
52            // simple color cycle
53            let palette = [
54                Vec4::new(0.0, 0.447, 0.741, 1.0),
55                Vec4::new(0.85, 0.325, 0.098, 1.0),
56                Vec4::new(0.929, 0.694, 0.125, 1.0),
57                Vec4::new(0.494, 0.184, 0.556, 1.0),
58                Vec4::new(0.466, 0.674, 0.188, 1.0),
59                Vec4::new(0.301, 0.745, 0.933, 1.0),
60                Vec4::new(0.635, 0.078, 0.184, 1.0),
61            ];
62            v.colors = (0..v.values.len())
63                .map(|i| palette[i % palette.len()])
64                .collect();
65        }
66        Ok(v)
67    }
68    pub fn with_label<S: Into<String>>(mut self, s: S) -> Self {
69        self.label = Some(s.into());
70        self
71    }
72    pub fn with_slice_labels(mut self, labels: Vec<String>) -> Self {
73        self.slice_labels = labels;
74        self.dirty = true;
75        self
76    }
77    pub fn set_slice_labels(&mut self, labels: Vec<String>) {
78        self.slice_labels = labels;
79        self.dirty = true;
80    }
81    pub fn with_label_format<S: Into<String>>(mut self, format: S) -> Self {
82        self.label_format = Some(format.into());
83        self.dirty = true;
84        self
85    }
86    pub fn with_explode(mut self, explode: Vec<bool>) -> Self {
87        self.explode = explode;
88        self.dirty = true;
89        self
90    }
91    pub fn set_visible(&mut self, v: bool) {
92        self.visible = v;
93    }
94    pub fn slice_labels(&self) -> Vec<String> {
95        self.slice_meta()
96            .into_iter()
97            .map(|slice| slice.label)
98            .collect()
99    }
100    pub fn slice_meta(&self) -> Vec<PieSliceMeta> {
101        self.slices
102            .clone()
103            .unwrap_or_else(|| self.compute_slice_meta())
104    }
105    fn compute_slice_meta(&self) -> Vec<PieSliceMeta> {
106        let positive_sum: f64 = self
107            .values
108            .iter()
109            .filter(|v| v.is_finite() && **v >= 0.0)
110            .sum();
111        let full_circle = positive_sum > 1.0 || positive_sum == 0.0;
112        let mut angle = 0.0f32;
113        let mut out = Vec::new();
114        for (i, &val) in self.values.iter().enumerate() {
115            if !val.is_finite() || val < 0.0 {
116                continue;
117            }
118            let frac = if full_circle {
119                if positive_sum == 0.0 {
120                    0.0
121                } else {
122                    (val / positive_sum) as f32
123                }
124            } else {
125                val as f32
126            };
127            let theta = frac * 2.0 * PI;
128            let mid = angle + theta * 0.5;
129            let exploded = self.explode.get(i).copied().unwrap_or(false);
130            let offset = if exploded {
131                Vec3::new(mid.cos() * 0.12, mid.sin() * 0.12, 0.0)
132            } else {
133                Vec3::ZERO
134            };
135            let label = self
136                .slice_labels
137                .get(i)
138                .cloned()
139                .unwrap_or_else(|| format_percentage_label(self.label_format.as_deref(), frac));
140            out.push(PieSliceMeta {
141                label,
142                color: self.colors[i % self.colors.len()],
143                mid_angle: mid,
144                offset,
145                fraction: frac,
146            });
147            angle += theta;
148        }
149        out
150    }
151    pub fn generate_vertices(&mut self) -> (&Vec<Vertex>, &Vec<u32>) {
152        if self.dirty || self.vertices.is_none() {
153            let mut verts = Vec::new();
154            let mut inds: Vec<u32> = Vec::new();
155            let mut angle = 0.0f32;
156            let mut acc_index = 0u32;
157            let slices = self.compute_slice_meta();
158            for (i, &val) in self.values.iter().enumerate() {
159                if !val.is_finite() || val < 0.0 {
160                    continue;
161                }
162                let Some(slice) = slices.get(i) else {
163                    continue;
164                };
165                let theta = slice.fraction * 2.0 * PI;
166                let steps = (theta * 20.0).ceil().max(1.0) as u32; // adaptive tessellation
167                let color = slice.color;
168                let start = angle;
169                let offset = slice.offset;
170                let center_index = acc_index;
171                verts.push(Vertex::new(offset, Vec4::new(1.0, 1.0, 1.0, 1.0)));
172                acc_index += 1;
173                for s in 0..=steps {
174                    let a = start + (theta * (s as f32 / steps as f32));
175                    verts.push(Vertex::new(
176                        Vec3::new(a.cos(), a.sin(), 0.0) + offset,
177                        color,
178                    ));
179                    if s > 0 {
180                        inds.extend_from_slice(&[center_index, acc_index + s - 1, acc_index + s]);
181                    }
182                }
183                acc_index += steps + 1;
184                angle += theta;
185            }
186            self.vertices = Some(verts);
187            self.indices = Some(inds);
188            self.slices = Some(slices);
189            self.dirty = false;
190        }
191        (
192            self.vertices.as_ref().unwrap(),
193            self.indices.as_ref().unwrap(),
194        )
195    }
196    pub fn bounds(&mut self) -> BoundingBox {
197        if self.bounds.is_none() || self.dirty {
198            let slices = self.compute_slice_meta();
199            let mut min = Vec3::new(-1.0, -1.0, 0.0);
200            let mut max = Vec3::new(1.0, 1.0, 0.0);
201            for slice in &slices {
202                min.x = min.x.min(-1.0 + slice.offset.x);
203                min.y = min.y.min(-1.0 + slice.offset.y);
204                max.x = max.x.max(1.0 + slice.offset.x);
205                max.y = max.y.max(1.0 + slice.offset.y);
206            }
207            self.bounds = Some(BoundingBox::new(min, max));
208        }
209        self.bounds.unwrap()
210    }
211    pub fn render_data(&mut self) -> RenderData {
212        let (v, i) = self.generate_vertices();
213        let vertices = v.clone();
214        let indices = i.clone();
215        let material = Material {
216            albedo: Vec4::new(1.0, 1.0, 1.0, 1.0),
217            ..Default::default()
218        };
219        let dc = DrawCall {
220            vertex_offset: 0,
221            vertex_count: vertices.len(),
222            index_offset: Some(0),
223            index_count: Some(indices.len()),
224            instance_count: 1,
225        };
226        RenderData {
227            pipeline_type: PipelineType::Triangles,
228            vertices,
229            indices: Some(indices),
230            material,
231            draw_calls: vec![dc],
232            gpu_vertices: None,
233            bounds: None,
234            image: None,
235        }
236    }
237    pub fn estimated_memory_usage(&self) -> usize {
238        self.vertices
239            .as_ref()
240            .map_or(0, |v| v.len() * std::mem::size_of::<Vertex>())
241            + self
242                .indices
243                .as_ref()
244                .map_or(0, |i| i.len() * std::mem::size_of::<u32>())
245    }
246}
247
248fn format_percentage_label(fmt: Option<&str>, frac: f32) -> String {
249    match fmt.unwrap_or("%.0f%%") {
250        "%.0f%%" => format!("{:.0}%", frac * 100.0),
251        "%.1f%%" => format!("{:.1}%", frac * 100.0),
252        "%.2f%%" => format!("{:.2}%", frac * 100.0),
253        other => {
254            if other.contains("%") {
255                other
256                    .replace("%.0f", &format!("{:.0}", frac * 100.0))
257                    .replace("%.1f", &format!("{:.1}", frac * 100.0))
258                    .replace("%.2f", &format!("{:.2}", frac * 100.0))
259            } else {
260                other.to_string()
261            }
262        }
263    }
264}