1use super::{
7 component::ComponentConfig, ComponentPosition, ComponentSpan, DashboardComponent,
8 DashboardTheme,
9};
10use crate::error::PdfError;
11use crate::graphics::Color;
12use crate::page::Page;
13
14#[derive(Debug, Clone)]
16pub struct ScatterPlot {
17 config: ComponentConfig,
19 data: Vec<ScatterPoint>,
21 options: ScatterPlotOptions,
23}
24
25impl ScatterPlot {
26 pub fn new(data: Vec<ScatterPoint>) -> Self {
28 Self {
29 config: ComponentConfig::new(ComponentSpan::new(6)), data,
31 options: ScatterPlotOptions::default(),
32 }
33 }
34
35 pub fn with_options(mut self, options: ScatterPlotOptions) -> Self {
37 self.options = options;
38 self
39 }
40
41 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 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 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 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 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 let (min_x, max_x, min_y, max_y) = self.get_bounds();
110
111 page.graphics()
113 .set_fill_color(Color::white())
114 .rect(plot_x, plot_y, plot_width, plot_height)
115 .fill();
116
117 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 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 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 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 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 for i in 0..=num_grid_lines {
177 let t = i as f64 / num_grid_lines as f64;
178
179 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 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 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 page.graphics()
210 .set_fill_color(color)
211 .circle(px, py, size)
212 .fill();
213
214 page.graphics()
216 .set_stroke_color(Color::white())
217 .set_line_width(0.5)
218 .circle(px, py, size)
219 .stroke();
220 }
221
222 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#[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#[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
279pub 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}