1use bon::bon;
2
3use polars::frame::DataFrame;
4
5use crate::{
6 components::{FacetConfig, Legend, Rgb, Text},
7 ir::data::ColumnData,
8 ir::layout::LayoutIR,
9 ir::trace::{PieChartIR, TraceIR},
10};
11
12#[derive(Clone)]
73#[allow(dead_code)]
74pub struct PieChart {
75 traces: Vec<TraceIR>,
76 layout: LayoutIR,
77}
78
79struct FacetCell {
80 pie_x_start: f64,
81 pie_x_end: f64,
82 pie_y_start: f64,
83 pie_y_end: f64,
84}
85
86#[bon]
87impl PieChart {
88 #[builder(on(String, into), on(Text, into))]
89 pub fn new(
90 data: &DataFrame,
91 labels: &str,
92 facet: Option<&str>,
93 facet_config: Option<&FacetConfig>,
94 hole: Option<f64>,
95 pull: Option<f64>,
96 rotation: Option<f64>,
97 colors: Option<Vec<Rgb>>,
98 plot_title: Option<Text>,
99 legend_title: Option<Text>,
100 legend: Option<&Legend>,
101 ) -> Self {
102 let grid = facet.map(|facet_column| {
103 let config = facet_config.cloned().unwrap_or_default();
104 let facet_categories =
105 crate::data::get_unique_groups(data, facet_column, config.sorter);
106 let n_facets = facet_categories.len();
107 let (ncols, nrows) =
108 crate::faceting::calculate_grid_dimensions(n_facets, config.cols, config.rows);
109 crate::ir::facet::GridSpec {
110 kind: crate::ir::facet::FacetKind::Domain,
111 rows: nrows,
112 cols: ncols,
113 h_gap: config.h_gap,
114 v_gap: config.v_gap,
115 scales: config.scales.clone(),
116 n_facets,
117 facet_categories,
118 title_style: config.title_style.clone(),
119 x_title: None,
120 y_title: None,
121 x_axis: None,
122 y_axis: None,
123 legend_title: legend_title.clone(),
124 legend: legend.cloned(),
125 }
126 });
127
128 let layout = LayoutIR {
129 title: plot_title,
130 x_title: None,
131 y_title: None,
132 y2_title: None,
133 z_title: None,
134 legend_title: if grid.is_some() { None } else { legend_title },
135 legend: if grid.is_some() {
136 None
137 } else {
138 legend.cloned()
139 },
140 dimensions: None,
141 bar_mode: None,
142 box_mode: None,
143 box_gap: None,
144 margin_bottom: None,
145 axes_2d: None,
146 scene_3d: None,
147 polar: None,
148 mapbox: None,
149 grid,
150 annotations: vec![],
151 };
152
153 let traces = match facet {
154 Some(facet_column) => {
155 let config = facet_config.cloned().unwrap_or_default();
156 Self::create_ir_traces_faceted(
157 data,
158 labels,
159 facet_column,
160 &config,
161 hole,
162 pull,
163 rotation,
164 colors,
165 )
166 }
167 None => Self::create_ir_traces(data, labels, hole, pull, rotation, colors),
168 };
169 Self { traces, layout }
170 }
171}
172
173#[bon]
174impl PieChart {
175 #[builder(
176 start_fn = try_builder,
177 finish_fn = try_build,
178 builder_type = PieChartTryBuilder,
179 on(String, into),
180 on(Text, into),
181 )]
182 pub fn try_new(
183 data: &DataFrame,
184 labels: &str,
185 facet: Option<&str>,
186 facet_config: Option<&FacetConfig>,
187 hole: Option<f64>,
188 pull: Option<f64>,
189 rotation: Option<f64>,
190 colors: Option<Vec<Rgb>>,
191 plot_title: Option<Text>,
192 legend_title: Option<Text>,
193 legend: Option<&Legend>,
194 ) -> Result<Self, crate::io::PlotlarsError> {
195 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
196 Self::__orig_new(
197 data,
198 labels,
199 facet,
200 facet_config,
201 hole,
202 pull,
203 rotation,
204 colors,
205 plot_title,
206 legend_title,
207 legend,
208 )
209 }))
210 .map_err(|panic| {
211 let msg = panic
212 .downcast_ref::<String>()
213 .cloned()
214 .or_else(|| panic.downcast_ref::<&str>().map(|s| s.to_string()))
215 .unwrap_or_else(|| "unknown error".to_string());
216 crate::io::PlotlarsError::PlotBuild { message: msg }
217 })
218 }
219}
220
221impl PieChart {
222 fn create_ir_traces(
223 data: &DataFrame,
224 labels: &str,
225 hole: Option<f64>,
226 pull: Option<f64>,
227 rotation: Option<f64>,
228 colors: Option<Vec<Rgb>>,
229 ) -> Vec<TraceIR> {
230 vec![TraceIR::PieChart(PieChartIR {
231 labels: ColumnData::String(crate::data::get_string_column(data, labels)),
232 values: None,
233 name: None,
234 hole,
235 pull,
236 rotation,
237 colors,
238 domain_x: Some((0.0, 1.0)),
239 domain_y: Some((0.0, 0.9)),
240 })]
241 }
242
243 #[allow(clippy::too_many_arguments)]
244 fn create_ir_traces_faceted(
245 data: &DataFrame,
246 labels: &str,
247 facet_column: &str,
248 config: &FacetConfig,
249 hole: Option<f64>,
250 pull: Option<f64>,
251 rotation: Option<f64>,
252 colors: Option<Vec<Rgb>>,
253 ) -> Vec<TraceIR> {
254 const MAX_FACETS: usize = 8;
255
256 let facet_categories = crate::data::get_unique_groups(data, facet_column, config.sorter);
257
258 if facet_categories.len() > MAX_FACETS {
259 panic!(
260 "Facet column '{}' has {} unique values, but plotly.rs supports maximum {} subplots",
261 facet_column,
262 facet_categories.len(),
263 MAX_FACETS
264 );
265 }
266
267 let n_facets = facet_categories.len();
268 let (ncols, nrows) =
269 crate::faceting::calculate_grid_dimensions(n_facets, config.cols, config.rows);
270
271 let facet_categories_non_empty: Vec<String> = facet_categories
272 .iter()
273 .filter(|facet_value| {
274 let facet_data = crate::data::filter_data_by_group(data, facet_column, facet_value);
275 facet_data.height() > 0
276 })
277 .cloned()
278 .collect();
279
280 let mut traces = Vec::new();
281
282 for (idx, facet_value) in facet_categories_non_empty.iter().enumerate() {
283 let facet_data = crate::data::filter_data_by_group(data, facet_column, facet_value);
284
285 let cell = Self::calculate_facet_cell(idx, ncols, nrows, config.h_gap, config.v_gap);
286
287 traces.push(TraceIR::PieChart(PieChartIR {
288 labels: ColumnData::String(crate::data::get_string_column(&facet_data, labels)),
289 values: None,
290 name: None,
291 hole,
292 pull,
293 rotation,
294 colors: colors.clone(),
295 domain_x: Some((cell.pie_x_start, cell.pie_x_end)),
296 domain_y: Some((cell.pie_y_start, cell.pie_y_end)),
297 }));
298 }
299
300 traces
301 }
302 fn calculate_facet_cell(
308 subplot_index: usize,
309 ncols: usize,
310 nrows: usize,
311 x_gap: Option<f64>,
312 y_gap: Option<f64>,
313 ) -> FacetCell {
314 let row = subplot_index / ncols;
315 let col = subplot_index % ncols;
316
317 let x_gap_val = x_gap.unwrap_or(0.05);
318 let y_gap_val = y_gap.unwrap_or(0.10);
319
320 const TITLE_HEIGHT_RATIO: f64 = 0.10;
322
323 let cell_width = (1.0 - x_gap_val * (ncols - 1) as f64) / ncols as f64;
325 let cell_height = (1.0 - y_gap_val * (nrows - 1) as f64) / nrows as f64;
326
327 let cell_x_start = col as f64 * (cell_width + x_gap_val);
329 let cell_y_top = 1.0 - row as f64 * (cell_height + y_gap_val);
330 let cell_y_bottom = cell_y_top - cell_height;
331
332 let title_height = cell_height * TITLE_HEIGHT_RATIO;
334 let pie_y_top = cell_y_top - title_height;
335
336 let pie_x_start = cell_x_start;
338 let pie_x_end = cell_x_start + cell_width;
339 let pie_y_start = cell_y_bottom;
340 let pie_y_end = pie_y_top;
341
342 FacetCell {
344 pie_x_start,
345 pie_x_end,
346 pie_y_start,
347 pie_y_end,
348 }
349 }
350}
351
352impl crate::Plot for PieChart {
353 fn ir_traces(&self) -> &[TraceIR] {
354 &self.traces
355 }
356
357 fn ir_layout(&self) -> &LayoutIR {
358 &self.layout
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365 use crate::Plot;
366 use polars::prelude::*;
367
368 #[test]
369 fn test_facet_cell_single() {
370 let cell = PieChart::calculate_facet_cell(0, 1, 1, None, None);
371 assert!(cell.pie_x_start >= 0.0 && cell.pie_x_end <= 1.0);
372 assert!(cell.pie_y_start >= 0.0 && cell.pie_y_end <= 1.0);
373 assert!(cell.pie_x_start < cell.pie_x_end);
374 assert!(cell.pie_y_start < cell.pie_y_end);
375 }
376
377 #[test]
378 fn test_facet_cell_2x2_first() {
379 let cell = PieChart::calculate_facet_cell(0, 2, 2, None, None);
380 assert!(cell.pie_x_start < 0.01);
381 }
382
383 #[test]
384 fn test_facet_cell_2x2_last() {
385 let cell = PieChart::calculate_facet_cell(3, 2, 2, None, None);
386 assert!(cell.pie_x_start > 0.4);
387 }
388
389 #[test]
390 fn test_facet_cell_bounds() {
391 for idx in 0..4 {
392 let cell = PieChart::calculate_facet_cell(idx, 2, 2, None, None);
393 assert!(cell.pie_x_start < cell.pie_x_end);
394 assert!(cell.pie_y_start < cell.pie_y_end);
395 }
396 }
397
398 #[test]
399 fn test_basic_one_trace() {
400 let df = df!["labels" => ["a", "b", "c", "a", "b"]].unwrap();
401 let plot = PieChart::builder().data(&df).labels("labels").build();
402 assert_eq!(plot.ir_traces().len(), 1);
403 }
404
405 #[test]
406 fn test_faceted() {
407 let df = df![
408 "labels" => ["a", "b", "c", "a"],
409 "facet" => ["f1", "f1", "f2", "f2"]
410 ]
411 .unwrap();
412 let plot = PieChart::builder()
413 .data(&df)
414 .labels("labels")
415 .facet("facet")
416 .build();
417 assert_eq!(plot.ir_traces().len(), 2);
418 }
419}