1use 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 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; 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}