oxidize_pdf/dashboard/
pivot_table.rs

1//! PivotTable Component
2//!
3//! This module implements pivot tables for data aggregation and analysis,
4//! with support for grouping, aggregation functions, and formatting.
5
6use super::{
7    component::ComponentConfig, ComponentPosition, ComponentSpan, DashboardComponent,
8    DashboardTheme,
9};
10use crate::error::PdfError;
11use crate::page::Page;
12use std::collections::HashMap;
13
14/// PivotTable component for data aggregation
15#[derive(Debug, Clone)]
16pub struct PivotTable {
17    /// Component configuration
18    config: ComponentConfig,
19    /// Raw data for the pivot table
20    data: Vec<HashMap<String, String>>,
21    /// Pivot configuration
22    pivot_config: PivotConfig,
23    /// Computed pivot data
24    computed_data: Option<ComputedPivotData>,
25}
26
27impl PivotTable {
28    /// Create a new pivot table
29    pub fn new(data: Vec<HashMap<String, String>>) -> Self {
30        Self {
31            config: ComponentConfig::new(ComponentSpan::new(12)), // Full width by default
32            data,
33            pivot_config: PivotConfig::default(),
34            computed_data: None,
35        }
36    }
37
38    /// Set pivot configuration
39    pub fn with_config(mut self, config: PivotConfig) -> Self {
40        self.pivot_config = config;
41        self.computed_data = None; // Reset computed data
42        self
43    }
44
45    /// Add aggregation
46    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; // Reset computed data
55        self
56    }
57
58    /// Compute pivot data if not already computed
59    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    /// Compute pivot table data
67    fn compute_pivot_data(&self) -> Result<ComputedPivotData, PdfError> {
68        // Implementation placeholder - real implementation would be complex
69        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        // Render title if present
94        if let Some(ref _title) = table.pivot_config.title {
95            // Placeholder: page.add_text replaced
96        }
97
98        // Simple table rendering (placeholder)
99        let mut current_y = position.y + position.height - 40.0;
100        let row_height = 20.0;
101
102        // Render headers
103        for (i, _header) in computed.headers.iter().enumerate() {
104            let _x = position.x + i as f64 * (position.width / computed.headers.len() as f64);
105            // Placeholder: page.add_text replaced
106        }
107
108        current_y -= row_height;
109
110        // Render data rows
111        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; // Suppress warning
118                                            // Placeholder: page.add_text replaced
119            }
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/// Pivot table configuration
144#[derive(Debug, Clone)]
145pub struct PivotConfig {
146    /// Table title
147    pub title: Option<String>,
148    /// Columns to group by (rows)
149    pub row_groups: Vec<String>,
150    /// Columns to group by (columns)
151    pub column_groups: Vec<String>,
152    /// Aggregation functions to apply
153    pub aggregations: Vec<AggregateFunction>,
154    /// Columns to aggregate
155    pub value_columns: Vec<String>,
156    /// Whether to show totals
157    pub show_totals: bool,
158    /// Whether to show subtotals
159    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/// Computed pivot table data
177#[derive(Debug, Clone)]
178pub struct ComputedPivotData {
179    /// Column headers
180    pub headers: Vec<String>,
181    /// Data rows
182    pub rows: Vec<Vec<String>>,
183    /// Index of totals row (if any)
184    pub totals_row: Option<usize>,
185}
186
187/// Aggregation functions for pivot tables
188#[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
215/// Builder for PivotTable
216pub 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}