oxidize_pdf/dashboard/
pivot_table.rs1use super::{
7 component::ComponentConfig, ComponentPosition, ComponentSpan, DashboardComponent,
8 DashboardTheme,
9};
10use crate::error::PdfError;
11use crate::page::Page;
12use std::collections::HashMap;
13
14#[derive(Debug, Clone)]
16pub struct PivotTable {
17 config: ComponentConfig,
19 data: Vec<HashMap<String, String>>,
21 pivot_config: PivotConfig,
23 computed_data: Option<ComputedPivotData>,
25}
26
27impl PivotTable {
28 pub fn new(data: Vec<HashMap<String, String>>) -> Self {
30 Self {
31 config: ComponentConfig::new(ComponentSpan::new(12)), data,
33 pivot_config: PivotConfig::default(),
34 computed_data: None,
35 }
36 }
37
38 pub fn with_config(mut self, config: PivotConfig) -> Self {
40 self.pivot_config = config;
41 self.computed_data = None; self
43 }
44
45 pub fn aggregate_by(mut self, functions: &[&str]) -> Self {
47 for func_str in functions {
48 if let Ok(func) = func_str.parse::<AggregateFunction>() {
49 if !self.pivot_config.aggregations.contains(&func) {
50 self.pivot_config.aggregations.push(func);
51 }
52 }
53 }
54 self.computed_data = None; self
56 }
57
58 fn ensure_computed(&mut self) -> Result<(), PdfError> {
60 if self.computed_data.is_none() {
61 self.computed_data = Some(self.compute_pivot_data()?);
62 }
63 Ok(())
64 }
65
66 fn compute_pivot_data(&self) -> Result<ComputedPivotData, PdfError> {
68 Ok(ComputedPivotData {
70 headers: vec!["Group".to_string(), "Count".to_string()],
71 rows: vec![
72 vec!["Group A".to_string(), "10".to_string()],
73 vec!["Group B".to_string(), "15".to_string()],
74 vec!["Total".to_string(), "25".to_string()],
75 ],
76 totals_row: Some(2),
77 })
78 }
79}
80
81impl DashboardComponent for PivotTable {
82 fn render(
83 &self,
84 _page: &mut Page,
85 position: ComponentPosition,
86 _theme: &DashboardTheme,
87 ) -> Result<(), PdfError> {
88 let mut table = self.clone();
89 table.ensure_computed()?;
90
91 let computed = table.computed_data.as_ref().unwrap();
92
93 if let Some(ref _title) = table.pivot_config.title {
95 }
97
98 let mut current_y = position.y + position.height - 40.0;
100 let row_height = 20.0;
101
102 for (i, _header) in computed.headers.iter().enumerate() {
104 let _x = position.x + i as f64 * (position.width / computed.headers.len() as f64);
105 }
107
108 current_y -= row_height;
109
110 for (row_idx, row) in computed.rows.iter().enumerate() {
112 let is_totals = computed.totals_row == Some(row_idx);
113
114 for (col_idx, _cell) in row.iter().enumerate() {
115 let _x =
116 position.x + col_idx as f64 * (position.width / computed.headers.len() as f64);
117 let _is_totals = is_totals; }
120 current_y -= row_height;
121 }
122
123 Ok(())
124 }
125
126 fn get_span(&self) -> ComponentSpan {
127 self.config.span
128 }
129 fn set_span(&mut self, span: ComponentSpan) {
130 self.config.span = span;
131 }
132 fn preferred_height(&self, _available_width: f64) -> f64 {
133 200.0
134 }
135 fn component_type(&self) -> &'static str {
136 "PivotTable"
137 }
138 fn complexity_score(&self) -> u8 {
139 85
140 }
141}
142
143#[derive(Debug, Clone)]
145pub struct PivotConfig {
146 pub title: Option<String>,
148 pub row_groups: Vec<String>,
150 pub column_groups: Vec<String>,
152 pub aggregations: Vec<AggregateFunction>,
154 pub value_columns: Vec<String>,
156 pub show_totals: bool,
158 pub show_subtotals: bool,
160}
161
162impl Default for PivotConfig {
163 fn default() -> Self {
164 Self {
165 title: None,
166 row_groups: vec![],
167 column_groups: vec![],
168 aggregations: vec![AggregateFunction::Count],
169 value_columns: vec![],
170 show_totals: true,
171 show_subtotals: false,
172 }
173 }
174}
175
176#[derive(Debug, Clone)]
178pub struct ComputedPivotData {
179 pub headers: Vec<String>,
181 pub rows: Vec<Vec<String>>,
183 pub totals_row: Option<usize>,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
189pub enum AggregateFunction {
190 Count,
191 Sum,
192 Average,
193 Min,
194 Max,
195}
196
197impl std::str::FromStr for AggregateFunction {
198 type Err = PdfError;
199
200 fn from_str(s: &str) -> Result<Self, Self::Err> {
201 match s.to_lowercase().as_str() {
202 "count" => Ok(AggregateFunction::Count),
203 "sum" => Ok(AggregateFunction::Sum),
204 "avg" | "average" => Ok(AggregateFunction::Average),
205 "min" => Ok(AggregateFunction::Min),
206 "max" => Ok(AggregateFunction::Max),
207 _ => Err(PdfError::InvalidOperation(format!(
208 "Unknown aggregate function: {}",
209 s
210 ))),
211 }
212 }
213}
214
215pub struct PivotTableBuilder;
217
218impl PivotTableBuilder {
219 pub fn new() -> Self {
220 Self
221 }
222 pub fn build(self) -> PivotTable {
223 PivotTable::new(vec![])
224 }
225}