oxidize_pdf/dashboard/
scatter_plot.rs

1//! ScatterPlot Visualization Component
2//!
3//! This module implements scatter plots for showing correlations and distributions
4//! in two-dimensional data.
5
6use super::{
7    component::ComponentConfig, ComponentPosition, ComponentSpan, DashboardComponent,
8    DashboardTheme,
9};
10use crate::error::PdfError;
11use crate::graphics::Color;
12use crate::page::Page;
13
14/// ScatterPlot visualization component
15#[derive(Debug, Clone)]
16pub struct ScatterPlot {
17    /// Component configuration
18    config: ComponentConfig,
19    /// Scatter plot data
20    data: Vec<ScatterPoint>,
21    /// Configuration options
22    options: ScatterPlotOptions,
23}
24
25impl ScatterPlot {
26    /// Create a new scatter plot
27    pub fn new(data: Vec<ScatterPoint>) -> Self {
28        Self {
29            config: ComponentConfig::new(ComponentSpan::new(6)), // Half width by default
30            data,
31            options: ScatterPlotOptions::default(),
32        }
33    }
34
35    /// Set scatter plot options
36    pub fn with_options(mut self, options: ScatterPlotOptions) -> Self {
37        self.options = options;
38        self
39    }
40
41    /// Get data bounds (min/max for x and y)
42    fn get_bounds(&self) -> (f64, f64, f64, f64) {
43        if self.data.is_empty() {
44            return (0.0, 100.0, 0.0, 100.0);
45        }
46
47        let mut min_x = f64::INFINITY;
48        let mut max_x = f64::NEG_INFINITY;
49        let mut min_y = f64::INFINITY;
50        let mut max_y = f64::NEG_INFINITY;
51
52        for point in &self.data {
53            min_x = min_x.min(point.x);
54            max_x = max_x.max(point.x);
55            min_y = min_y.min(point.y);
56            max_y = max_y.max(point.y);
57        }
58
59        // Add 10% padding
60        let x_range = max_x - min_x;
61        let y_range = max_y - min_y;
62        let x_padding = x_range * 0.1;
63        let y_padding = y_range * 0.1;
64
65        (
66            min_x - x_padding,
67            max_x + x_padding,
68            min_y - y_padding,
69            max_y + y_padding,
70        )
71    }
72
73    /// Map data coordinate to plot coordinate
74    fn map_to_plot(&self, value: f64, min: f64, max: f64, plot_min: f64, plot_max: f64) -> f64 {
75        if max == min {
76            return (plot_min + plot_max) / 2.0;
77        }
78        plot_min + (value - min) / (max - min) * (plot_max - plot_min)
79    }
80}
81
82impl DashboardComponent for ScatterPlot {
83    fn render(
84        &self,
85        page: &mut Page,
86        position: ComponentPosition,
87        theme: &DashboardTheme,
88    ) -> Result<(), PdfError> {
89        let title = self.options.title.as_deref().unwrap_or("Scatter Plot");
90
91        // Calculate layout
92        let title_height = 30.0;
93        let axis_label_space = 40.0;
94        let margin = 10.0;
95
96        let plot_x = position.x + axis_label_space + margin;
97        let plot_y = position.y + axis_label_space;
98        let plot_width = position.width - axis_label_space - 2.0 * margin;
99        let plot_height = position.height - title_height - axis_label_space - margin;
100
101        // Render title
102        page.text()
103            .set_font(crate::Font::HelveticaBold, theme.typography.heading_size)
104            .set_fill_color(theme.colors.text_primary)
105            .at(position.x, position.y + position.height - 15.0)
106            .write(title)?;
107
108        // Get data bounds
109        let (min_x, max_x, min_y, max_y) = self.get_bounds();
110
111        // Draw plot background
112        page.graphics()
113            .set_fill_color(Color::white())
114            .rect(plot_x, plot_y, plot_width, plot_height)
115            .fill();
116
117        // Draw grid lines
118        let grid_color = Color::gray(0.9);
119        let num_grid_lines = 5;
120
121        for i in 0..=num_grid_lines {
122            let t = i as f64 / num_grid_lines as f64;
123
124            // Vertical grid lines
125            let x = plot_x + t * plot_width;
126            page.graphics()
127                .set_stroke_color(grid_color)
128                .set_line_width(0.5)
129                .move_to(x, plot_y)
130                .line_to(x, plot_y + plot_height)
131                .stroke();
132
133            // Horizontal grid lines
134            let y = plot_y + t * plot_height;
135            page.graphics()
136                .set_stroke_color(grid_color)
137                .set_line_width(0.5)
138                .move_to(plot_x, y)
139                .line_to(plot_x + plot_width, y)
140                .stroke();
141        }
142
143        // Draw axes
144        page.graphics()
145            .set_stroke_color(Color::black())
146            .set_line_width(1.5)
147            .move_to(plot_x, plot_y)
148            .line_to(plot_x, plot_y + plot_height)
149            .stroke();
150
151        page.graphics()
152            .set_stroke_color(Color::black())
153            .set_line_width(1.5)
154            .move_to(plot_x, plot_y)
155            .line_to(plot_x + plot_width, plot_y)
156            .stroke();
157
158        // Draw axis labels
159        if let Some(ref x_label) = self.options.x_label {
160            page.text()
161                .set_font(crate::Font::Helvetica, 10.0)
162                .set_fill_color(theme.colors.text_secondary)
163                .at(plot_x + plot_width / 2.0 - 20.0, plot_y - 25.0)
164                .write(x_label)?;
165        }
166
167        if let Some(ref y_label) = self.options.y_label {
168            page.text()
169                .set_font(crate::Font::Helvetica, 10.0)
170                .set_fill_color(theme.colors.text_secondary)
171                .at(position.x + 5.0, plot_y + plot_height / 2.0)
172                .write(y_label)?;
173        }
174
175        // Draw axis tick labels
176        for i in 0..=num_grid_lines {
177            let t = i as f64 / num_grid_lines as f64;
178
179            // X-axis labels
180            let x_value = min_x + t * (max_x - min_x);
181            let x_pos = plot_x + t * plot_width;
182            page.text()
183                .set_font(crate::Font::Helvetica, 8.0)
184                .set_fill_color(theme.colors.text_secondary)
185                .at(x_pos - 10.0, plot_y - 15.0)
186                .write(&format!("{:.1}", x_value))?;
187
188            // Y-axis labels
189            let y_value = min_y + t * (max_y - min_y);
190            let y_pos = plot_y + t * plot_height;
191            page.text()
192                .set_font(crate::Font::Helvetica, 8.0)
193                .set_fill_color(theme.colors.text_secondary)
194                .at(plot_x - 35.0, y_pos - 3.0)
195                .write(&format!("{:.1}", y_value))?;
196        }
197
198        // Draw data points
199        let default_color = Color::hex("#007bff");
200        let default_size = 3.0;
201
202        for point in &self.data {
203            let px = self.map_to_plot(point.x, min_x, max_x, plot_x, plot_x + plot_width);
204            let py = self.map_to_plot(point.y, min_y, max_y, plot_y, plot_y + plot_height);
205            let size = point.size.unwrap_or(default_size);
206            let color = point.color.unwrap_or(default_color);
207
208            // Draw point as filled circle
209            page.graphics()
210                .set_fill_color(color)
211                .circle(px, py, size)
212                .fill();
213
214            // Draw point border for visibility
215            page.graphics()
216                .set_stroke_color(Color::white())
217                .set_line_width(0.5)
218                .circle(px, py, size)
219                .stroke();
220        }
221
222        // Draw plot border
223        page.graphics()
224            .set_stroke_color(Color::black())
225            .set_line_width(1.0)
226            .rect(plot_x, plot_y, plot_width, plot_height)
227            .stroke();
228
229        Ok(())
230    }
231
232    fn get_span(&self) -> ComponentSpan {
233        self.config.span
234    }
235    fn set_span(&mut self, span: ComponentSpan) {
236        self.config.span = span;
237    }
238    fn preferred_height(&self, _available_width: f64) -> f64 {
239        300.0
240    }
241    fn component_type(&self) -> &'static str {
242        "ScatterPlot"
243    }
244    fn complexity_score(&self) -> u8 {
245        60
246    }
247}
248
249/// Scatter plot data point
250#[derive(Debug, Clone)]
251pub struct ScatterPoint {
252    pub x: f64,
253    pub y: f64,
254    pub size: Option<f64>,
255    pub color: Option<Color>,
256    pub label: Option<String>,
257}
258
259/// Scatter plot options
260#[derive(Debug, Clone)]
261pub struct ScatterPlotOptions {
262    pub title: Option<String>,
263    pub x_label: Option<String>,
264    pub y_label: Option<String>,
265    pub show_trend_line: bool,
266}
267
268impl Default for ScatterPlotOptions {
269    fn default() -> Self {
270        Self {
271            title: None,
272            x_label: None,
273            y_label: None,
274            show_trend_line: false,
275        }
276    }
277}
278
279/// Builder for ScatterPlot
280pub struct ScatterPlotBuilder;
281
282impl ScatterPlotBuilder {
283    pub fn new() -> Self {
284        Self
285    }
286    pub fn build(self) -> ScatterPlot {
287        ScatterPlot::new(vec![])
288    }
289}