oxidize-pdf 2.4.2

A pure Rust PDF generation and manipulation library with zero external dependencies
Documentation
//! # Dashboard Framework for Professional PDF Reports
#![allow(
    dead_code,
    unused_variables,
    clippy::new_without_default,
    clippy::derivable_impls
)] // Placeholder implementations
//!
//! This module provides a comprehensive dashboard system for creating professional
//! data visualization reports in PDF format. The framework includes:
//!
//! - **Grid Layout System**: Responsive 12-column grid for component positioning
//! - **KPI Cards**: Metric cards with values, trends, and sparklines
//! - **Advanced Charts**: Heatmaps, treemaps, scatter plots, pivot tables
//! - **Automatic Layout**: Smart positioning with minimal configuration
//! - **Professional Styling**: Consistent typography and color schemes
//!
//! # Core Concepts
//!
//! ## Dashboard Structure
//! A dashboard is composed of:
//! - **Header**: Title, subtitle, and metadata
//! - **Content Grid**: 12-column responsive layout
//! - **Components**: KPI cards, charts, tables, text blocks
//! - **Footer**: Date, page numbers, branding
//!
//! ## Layout System
//! The framework uses a 12-column grid system similar to Bootstrap:
//! - Components can span 1-12 columns
//! - Automatic row wrapping
//! - Configurable gutters and margins
//! - Responsive breakpoints
//!
//! # Quick Start Example
//!
//! ```rust,ignore
//! use oxidize_pdf::dashboard::{DashboardBuilder, KpiCard};
//! use oxidize_pdf::charts::BarChart;
//! use oxidize_pdf::graphics::Color;
//!
//! // Create a sales dashboard
//! let dashboard = DashboardBuilder::new()
//!     .title("Sales Performance Q4 2024")
//!     .subtitle("Executive Summary")
//!     
//!     // Row 1: KPI Cards (3 columns each = 4 KPIs total)
//!     .add_kpi_row(vec![
//!         KpiCard::new("Revenue", "$2.5M", "+12%").color(Color::green()),
//!         KpiCard::new("Orders", "1,247", "+5%").color(Color::blue()),
//!         KpiCard::new("Conversion", "3.2%", "-0.1%").color(Color::orange()),
//!         KpiCard::new("AOV", "$2,005", "+8%").color(Color::green()),
//!     ])
//!     
//!     // Row 2: Charts (6 columns each = 2 charts)
//!     .add_row(vec![
//!         BarChart::quarterly_sales().span(6),
//!         PieChart::revenue_breakdown().span(6),
//!     ])
//!     
//!     // Row 3: Data Table (full width)
//!     .add_component(
//!         PivotTable::sales_by_region()
//!             .span(12)
//!             .aggregate_by(["sum", "avg"])
//!     )
//!     
//!     .build()?;
//!
//! // Render to PDF
//! let mut document = Document::new();
//! dashboard.render_to_page(&mut document.add_page())?;
//! document.save("sales_dashboard.pdf")?;
//! ```
//!
//! # Advanced Features
//!
//! ## Custom Styling
//! ```rust,ignore
//! let dashboard = DashboardBuilder::new()
//!     .theme(DashboardTheme::corporate())
//!     .color_palette(vec![Color::hex("#1f77b4"), Color::hex("#ff7f0e")])
//!     .typography(Typography::professional())
//!     .build()?;
//! ```
//!
//! ## Interactive Elements
//! ```rust,ignore
//! // Add tooltips and annotations
//! let heatmap = HeatMap::new(data)
//!     .tooltip_format("Region: {x}, Month: {y}, Sales: ${value}")
//!     .annotation("Peak season in Q4")
//!     .build();
//! ```
//!
//! ## Data Integration
//! ```rust,ignore
//! // Load data from various sources
//! let data = DataSource::from_csv("sales.csv")?
//!     .filter_by("region", "North America")
//!     .group_by("month")
//!     .aggregate("revenue", AggregateFunction::Sum);
//! ```

pub mod builder;
pub mod component;
pub mod data_aggregation;
pub mod heatmap;
pub mod kpi_card;
pub mod layout;
pub mod pivot_table;
pub mod scatter_plot;
pub mod templates;
pub mod theme;
pub mod treemap;

pub use builder::{DashboardBuilder, DashboardConfig};
pub use component::{ComponentPosition, ComponentSpan, DashboardComponent};
pub use data_aggregation::{AggregateFunc, DataAggregator, GroupedData};
pub use heatmap::{ColorScale, HeatMap, HeatMapBuilder, HeatMapData, HeatMapOptions};
pub use kpi_card::{KpiCard, KpiCardBuilder, TrendDirection};
pub use layout::{DashboardLayout, GridPosition, LayoutManager};
pub use pivot_table::{AggregateFunction, PivotConfig, PivotTable, PivotTableBuilder};
pub use scatter_plot::{ScatterPlot, ScatterPlotBuilder, ScatterPlotOptions, ScatterPoint};
pub use templates::{
    AnalyticsDashboardTemplate, ChartData, FinancialReportTemplate, KpiData, PieSegmentData,
    SalesDashboardTemplate, SeriesData, TemplateData,
};
pub use theme::{DashboardTheme, Typography};
pub use treemap::{TreeMap, TreeMapBuilder, TreeMapNode, TreeMapOptions};

use crate::error::PdfError;
use crate::page::Page;
use crate::Font;

/// Main dashboard structure that contains all components and layout information
#[derive(Debug, Clone)]
pub struct Dashboard {
    /// Dashboard title
    pub title: String,
    /// Optional subtitle
    pub subtitle: Option<String>,
    /// Layout configuration
    pub layout: DashboardLayout,
    /// Theme and styling
    pub theme: DashboardTheme,
    /// All dashboard components
    pub components: Vec<Box<dyn DashboardComponent>>,
    /// Dashboard metadata
    pub metadata: DashboardMetadata,
}

/// Metadata associated with the dashboard
#[derive(Debug, Clone)]
pub struct DashboardMetadata {
    /// Creation timestamp
    pub created_at: chrono::DateTime<chrono::Utc>,
    /// Dashboard version
    pub version: String,
    /// Data source information
    pub data_sources: Vec<String>,
    /// Author information
    pub author: Option<String>,
    /// Tags for categorization
    pub tags: Vec<String>,
}

impl Dashboard {
    /// Render the entire dashboard to a PDF page
    pub fn render_to_page(&self, page: &mut Page) -> Result<(), PdfError> {
        // Set up page layout
        let page_bounds = page.content_area();
        let content_area = self.layout.calculate_content_area(page_bounds);

        // Render header (title, subtitle)
        self.render_header(page, content_area)?;

        // Calculate component positions using grid system
        let component_positions = self
            .layout
            .calculate_positions(&self.components, content_area)?;

        // Render each component
        for (component, position) in self.components.iter().zip(component_positions.iter()) {
            component.render(page, *position, &self.theme)?;
        }

        // Render footer (metadata, page numbers)
        self.render_footer(page, content_area)?;

        Ok(())
    }

    /// Render dashboard header with title and subtitle
    fn render_header(
        &self,
        page: &mut Page,
        content_area: (f64, f64, f64, f64),
    ) -> Result<(), PdfError> {
        let (x, y, _width, height) = content_area;

        // Render title with proper text rendering
        let title_y = y + height - 30.0;
        page.text()
            .set_font(Font::HelveticaBold, self.theme.typography.title_size)
            .set_fill_color(self.theme.colors.text_primary)
            .at(x + 20.0, title_y)
            .write(&self.title)?;

        // Render subtitle if present
        if let Some(subtitle) = &self.subtitle {
            let subtitle_y = title_y - 25.0;
            page.text()
                .set_font(Font::Helvetica, self.theme.typography.body_size)
                .set_fill_color(self.theme.colors.text_secondary)
                .at(x + 20.0, subtitle_y)
                .write(subtitle)?;
        }

        Ok(())
    }

    /// Render dashboard footer with metadata
    fn render_footer(
        &self,
        page: &mut Page,
        content_area: (f64, f64, f64, f64),
    ) -> Result<(), PdfError> {
        let (x, y, width, _height) = content_area;
        let footer_y = y + 15.0; // Bottom margin

        // Left side: creation date
        let date_text = format!(
            "Generated: {}",
            self.metadata.created_at.format("%Y-%m-%d %H:%M UTC")
        );
        page.text()
            .set_font(Font::Helvetica, self.theme.typography.caption_size)
            .set_fill_color(self.theme.colors.text_muted)
            .at(x + 20.0, footer_y)
            .write(&date_text)?;

        // Right side: data sources
        if !self.metadata.data_sources.is_empty() {
            let sources_text = format!("Data: {}", self.metadata.data_sources.join(", "));
            page.text()
                .set_font(Font::Helvetica, self.theme.typography.caption_size)
                .set_fill_color(self.theme.colors.text_muted)
                .at(x + width - 150.0, footer_y) // Right aligned
                .write(&sources_text)?;
        }

        Ok(())
    }

    /// Get dashboard statistics for debugging/monitoring
    pub fn get_stats(&self) -> DashboardStats {
        DashboardStats {
            component_count: self.components.len(),
            estimated_render_time: self.estimate_render_time(),
            memory_usage_mb: self.estimate_memory_usage(),
            complexity_score: self.calculate_complexity_score(),
        }
    }

    /// Estimate rendering time
    fn estimate_render_time(&self) -> std::time::Duration {
        use std::time::Duration;

        // Base render time + component complexity
        let base_time_ms = 50u64;
        let component_time_ms: u64 = self
            .components
            .iter()
            .map(|c| c.estimated_render_time_ms() as u64)
            .sum();

        Duration::from_millis(base_time_ms + component_time_ms)
    }

    /// Estimate memory usage in MB
    fn estimate_memory_usage(&self) -> f64 {
        // Base dashboard overhead + component memory
        0.5 + self
            .components
            .iter()
            .map(|c| c.estimated_memory_mb())
            .sum::<f64>()
    }

    /// Calculate complexity score (0-100)
    fn calculate_complexity_score(&self) -> u8 {
        let component_complexity: u32 = self
            .components
            .iter()
            .map(|c| c.complexity_score() as u32)
            .sum();

        // Normalize to 0-100 scale
        ((component_complexity / self.components.len().max(1) as u32).min(100)) as u8
    }
}

/// Dashboard performance and complexity statistics
#[derive(Debug, Clone)]
pub struct DashboardStats {
    /// Total number of components
    pub component_count: usize,
    /// Estimated rendering time
    pub estimated_render_time: std::time::Duration,
    /// Estimated memory usage in MB
    pub memory_usage_mb: f64,
    /// Complexity score (0-100, higher = more complex)
    pub complexity_score: u8,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{Document, Page};

    #[test]
    fn test_dashboard_creation() {
        let dashboard = DashboardBuilder::new()
            .title("Test Dashboard")
            .subtitle("Unit Test")
            .build()
            .unwrap();

        assert_eq!(dashboard.title, "Test Dashboard");
        assert_eq!(dashboard.subtitle, Some("Unit Test".to_string()));
    }

    #[test]
    fn test_dashboard_stats() {
        let dashboard = DashboardBuilder::new()
            .title("Performance Test")
            .build()
            .unwrap();

        let stats = dashboard.get_stats();
        assert!(stats.estimated_render_time.as_millis() > 0);
        assert!(stats.memory_usage_mb > 0.0);
    }

    #[test]
    fn test_dashboard_render() {
        let dashboard = DashboardBuilder::new()
            .title("Render Test")
            .build()
            .unwrap();

        let mut document = Document::new();
        let mut page = Page::new(595.0, 842.0); // A4 size

        // Should not panic
        let result = dashboard.render_to_page(&mut page);
        document.add_page(page);
        assert!(result.is_ok());
    }
}