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 TreeMap {
17 config: ComponentConfig,
19 data: Vec<TreeMapNode>,
21 options: TreeMapOptions,
23}
24
25impl TreeMap {
26 pub fn new(data: Vec<TreeMapNode>) -> Self {
28 Self {
29 config: ComponentConfig::new(ComponentSpan::new(6)), data,
31 options: TreeMapOptions::default(),
32 }
33 }
34
35 pub fn with_options(mut self, options: TreeMapOptions) -> Self {
37 self.options = options;
38 self
39 }
40
41 fn layout_nodes(
43 &self,
44 nodes: &[TreeMapNode],
45 x: f64,
46 y: f64,
47 width: f64,
48 height: f64,
49 rects: &mut Vec<(TreeMapNode, f64, f64, f64, f64)>,
50 ) {
51 if nodes.is_empty() || width <= 0.0 || height <= 0.0 {
52 return;
53 }
54
55 let total: f64 = nodes.iter().map(|n| n.value).sum();
56 if total <= 0.0 {
57 return;
58 }
59
60 let mut current_x = x;
61 let mut current_y = y;
62 let mut remaining_width = width;
63 let mut remaining_height = height;
64
65 for node in nodes {
66 let ratio = node.value / total;
67 let area = width * height * ratio;
68
69 let (rect_width, rect_height) = if remaining_width > remaining_height {
71 let w = area / remaining_height;
73 (w.min(remaining_width), remaining_height)
74 } else {
75 let h = area / remaining_width;
77 (remaining_width, h.min(remaining_height))
78 };
79
80 let padding = self.options.padding;
82 let rect_x = current_x + padding;
83 let rect_y = current_y + padding;
84 let rect_w = (rect_width - 2.0 * padding).max(0.0);
85 let rect_h = (rect_height - 2.0 * padding).max(0.0);
86
87 rects.push((node.clone(), rect_x, rect_y, rect_w, rect_h));
88
89 if remaining_width > remaining_height {
91 current_x += rect_width;
92 remaining_width -= rect_width;
93 } else {
94 current_y += rect_height;
95 remaining_height -= rect_height;
96 }
97 }
98 }
99}
100
101impl DashboardComponent for TreeMap {
102 fn render(
103 &self,
104 page: &mut Page,
105 position: ComponentPosition,
106 theme: &DashboardTheme,
107 ) -> Result<(), PdfError> {
108 let title = self.options.title.as_deref().unwrap_or("TreeMap");
109
110 let title_height = 30.0;
111 let plot_x = position.x;
112 let plot_y = position.y;
113 let plot_width = position.width;
114 let plot_height = position.height - title_height;
115
116 page.text()
118 .set_font(crate::Font::HelveticaBold, theme.typography.heading_size)
119 .set_fill_color(theme.colors.text_primary)
120 .at(position.x, position.y + position.height - 15.0)
121 .write(title)?;
122
123 let mut rects = Vec::new();
125 self.layout_nodes(
126 &self.data,
127 plot_x,
128 plot_y,
129 plot_width,
130 plot_height,
131 &mut rects,
132 );
133
134 let default_colors = vec![
136 Color::hex("#1f77b4"),
137 Color::hex("#ff7f0e"),
138 Color::hex("#2ca02c"),
139 Color::hex("#d62728"),
140 Color::hex("#9467bd"),
141 Color::hex("#8c564b"),
142 Color::hex("#e377c2"),
143 Color::hex("#7f7f7f"),
144 Color::hex("#bcbd22"),
145 Color::hex("#17becf"),
146 ];
147
148 for (idx, (node, x, y, w, h)) in rects.iter().enumerate() {
150 let color = node
151 .color
152 .unwrap_or(default_colors[idx % default_colors.len()]);
153
154 page.graphics()
156 .set_fill_color(color)
157 .rect(*x, *y, *w, *h)
158 .fill();
159
160 page.graphics()
162 .set_stroke_color(Color::white())
163 .set_line_width(1.5)
164 .rect(*x, *y, *w, *h)
165 .stroke();
166
167 if self.options.show_labels && *w > 40.0 && *h > 20.0 {
169 let text_color = if self.is_dark_color(&color) {
171 Color::white()
172 } else {
173 Color::black()
174 };
175
176 page.text()
178 .set_font(crate::Font::HelveticaBold, 9.0)
179 .set_fill_color(text_color)
180 .at(x + 5.0, y + h - 15.0)
181 .write(&node.name)?;
182
183 page.text()
185 .set_font(crate::Font::Helvetica, 8.0)
186 .set_fill_color(text_color)
187 .at(x + 5.0, y + h - 28.0)
188 .write(&format!("{:.0}", node.value))?;
189 }
190 }
191
192 Ok(())
193 }
194
195 fn get_span(&self) -> ComponentSpan {
196 self.config.span
197 }
198 fn set_span(&mut self, span: ComponentSpan) {
199 self.config.span = span;
200 }
201 fn preferred_height(&self, _available_width: f64) -> f64 {
202 250.0
203 }
204 fn component_type(&self) -> &'static str {
205 "TreeMap"
206 }
207 fn complexity_score(&self) -> u8 {
208 70
209 }
210}
211
212impl TreeMap {
213 fn is_dark_color(&self, color: &Color) -> bool {
215 let (r, g, b) = match color {
216 Color::Rgb(r, g, b) => (*r, *g, *b),
217 Color::Gray(v) => (*v, *v, *v),
218 Color::Cmyk(c, m, y, k) => {
219 let r = (1.0 - c) * (1.0 - k);
220 let g = (1.0 - m) * (1.0 - k);
221 let b = (1.0 - y) * (1.0 - k);
222 (r, g, b)
223 }
224 };
225 let luminance = 0.299 * r + 0.587 * g + 0.114 * b;
226 luminance < 0.5
227 }
228}
229
230#[derive(Debug, Clone)]
232pub struct TreeMapNode {
233 pub name: String,
234 pub value: f64,
235 pub color: Option<Color>,
236 pub children: Vec<TreeMapNode>,
237}
238
239#[derive(Debug, Clone)]
241pub struct TreeMapOptions {
242 pub title: Option<String>,
243 pub show_labels: bool,
244 pub padding: f64,
245}
246
247impl Default for TreeMapOptions {
248 fn default() -> Self {
249 Self {
250 title: None,
251 show_labels: true,
252 padding: 2.0,
253 }
254 }
255}
256
257pub struct TreeMapBuilder;
259
260impl TreeMapBuilder {
261 pub fn new() -> Self {
262 Self
263 }
264 pub fn build(self) -> TreeMap {
265 TreeMap::new(vec![])
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 fn sample_treemap_data() -> Vec<TreeMapNode> {
274 vec![
275 TreeMapNode {
276 name: "Category A".to_string(),
277 value: 100.0,
278 color: None,
279 children: vec![],
280 },
281 TreeMapNode {
282 name: "Category B".to_string(),
283 value: 50.0,
284 color: Some(Color::rgb(0.0, 0.5, 1.0)),
285 children: vec![],
286 },
287 TreeMapNode {
288 name: "Category C".to_string(),
289 value: 30.0,
290 color: None,
291 children: vec![],
292 },
293 ]
294 }
295
296 #[test]
297 fn test_treemap_new() {
298 let data = sample_treemap_data();
299 let treemap = TreeMap::new(data.clone());
300
301 assert_eq!(treemap.data.len(), 3);
302 assert_eq!(treemap.data[0].name, "Category A");
303 assert_eq!(treemap.data[0].value, 100.0);
304 }
305
306 #[test]
307 fn test_treemap_with_options() {
308 let data = sample_treemap_data();
309 let options = TreeMapOptions {
310 title: Some("My TreeMap".to_string()),
311 show_labels: false,
312 padding: 5.0,
313 };
314
315 let treemap = TreeMap::new(data).with_options(options);
316
317 assert_eq!(treemap.options.title, Some("My TreeMap".to_string()));
318 assert!(!treemap.options.show_labels);
319 assert_eq!(treemap.options.padding, 5.0);
320 }
321
322 #[test]
323 fn test_treemap_options_default() {
324 let options = TreeMapOptions::default();
325
326 assert!(options.title.is_none());
327 assert!(options.show_labels);
328 assert_eq!(options.padding, 2.0);
329 }
330
331 #[test]
332 fn test_treemap_builder() {
333 let builder = TreeMapBuilder::new();
334 let treemap = builder.build();
335
336 assert!(treemap.data.is_empty());
337 }
338
339 #[test]
340 fn test_treemap_node_creation() {
341 let node = TreeMapNode {
342 name: "Test Node".to_string(),
343 value: 42.0,
344 color: Some(Color::rgb(1.0, 0.0, 0.0)),
345 children: vec![TreeMapNode {
346 name: "Child".to_string(),
347 value: 10.0,
348 color: None,
349 children: vec![],
350 }],
351 };
352
353 assert_eq!(node.name, "Test Node");
354 assert_eq!(node.value, 42.0);
355 assert!(node.color.is_some());
356 assert_eq!(node.children.len(), 1);
357 assert_eq!(node.children[0].name, "Child");
358 }
359
360 #[test]
361 fn test_layout_nodes_empty() {
362 let treemap = TreeMap::new(vec![]);
363 let mut rects = Vec::new();
364
365 treemap.layout_nodes(&[], 0.0, 0.0, 100.0, 100.0, &mut rects);
366
367 assert!(rects.is_empty());
368 }
369
370 #[test]
371 fn test_layout_nodes_single() {
372 let data = vec![TreeMapNode {
373 name: "Single".to_string(),
374 value: 100.0,
375 color: None,
376 children: vec![],
377 }];
378 let treemap = TreeMap::new(data.clone());
379 let mut rects = Vec::new();
380
381 treemap.layout_nodes(&data, 0.0, 0.0, 100.0, 100.0, &mut rects);
382
383 assert_eq!(rects.len(), 1);
384 assert_eq!(rects[0].0.name, "Single");
385 }
386
387 #[test]
388 fn test_layout_nodes_multiple() {
389 let data = sample_treemap_data();
390 let treemap = TreeMap::new(data.clone());
391 let mut rects = Vec::new();
392
393 treemap.layout_nodes(&data, 0.0, 0.0, 300.0, 200.0, &mut rects);
394
395 assert_eq!(rects.len(), 3);
396
397 for (_, x, y, w, h) in &rects {
399 assert!(*x >= 0.0);
400 assert!(*y >= 0.0);
401 assert!(*w > 0.0);
402 assert!(*h > 0.0);
403 }
404 }
405
406 #[test]
407 fn test_layout_nodes_proportional() {
408 let data = vec![
409 TreeMapNode {
410 name: "A".to_string(),
411 value: 75.0,
412 color: None,
413 children: vec![],
414 },
415 TreeMapNode {
416 name: "B".to_string(),
417 value: 25.0,
418 color: None,
419 children: vec![],
420 },
421 ];
422 let treemap = TreeMap::new(data.clone());
423 let mut rects = Vec::new();
424
425 treemap.layout_nodes(&data, 0.0, 0.0, 100.0, 100.0, &mut rects);
426
427 let area_a = rects[0].3 * rects[0].4;
429 let area_b = rects[1].3 * rects[1].4;
430
431 assert!(area_a > area_b);
433 }
434
435 #[test]
436 fn test_layout_nodes_zero_size() {
437 let data = sample_treemap_data();
438 let treemap = TreeMap::new(data.clone());
439 let mut rects = Vec::new();
440
441 treemap.layout_nodes(&data, 0.0, 0.0, 0.0, 100.0, &mut rects);
443 assert!(rects.is_empty());
444
445 rects.clear();
447 treemap.layout_nodes(&data, 0.0, 0.0, 100.0, 0.0, &mut rects);
448 assert!(rects.is_empty());
449 }
450
451 #[test]
452 fn test_layout_nodes_zero_total_value() {
453 let data = vec![
454 TreeMapNode {
455 name: "A".to_string(),
456 value: 0.0,
457 color: None,
458 children: vec![],
459 },
460 TreeMapNode {
461 name: "B".to_string(),
462 value: 0.0,
463 color: None,
464 children: vec![],
465 },
466 ];
467 let treemap = TreeMap::new(data.clone());
468 let mut rects = Vec::new();
469
470 treemap.layout_nodes(&data, 0.0, 0.0, 100.0, 100.0, &mut rects);
471
472 assert!(rects.is_empty());
474 }
475
476 #[test]
477 fn test_is_dark_color_with_black() {
478 let treemap = TreeMap::new(vec![]);
479
480 assert!(treemap.is_dark_color(&Color::rgb(0.0, 0.0, 0.0)));
481 }
482
483 #[test]
484 fn test_is_dark_color_with_white() {
485 let treemap = TreeMap::new(vec![]);
486
487 assert!(!treemap.is_dark_color(&Color::rgb(1.0, 1.0, 1.0)));
488 }
489
490 #[test]
491 fn test_is_dark_color_with_gray() {
492 let treemap = TreeMap::new(vec![]);
493
494 assert!(treemap.is_dark_color(&Color::Gray(0.3)));
496 assert!(!treemap.is_dark_color(&Color::Gray(0.7)));
498 }
499
500 #[test]
501 fn test_is_dark_color_with_cmyk() {
502 let treemap = TreeMap::new(vec![]);
503
504 assert!(treemap.is_dark_color(&Color::Cmyk(0.0, 0.0, 0.0, 1.0)));
506 assert!(!treemap.is_dark_color(&Color::Cmyk(0.0, 0.0, 0.0, 0.0)));
508 }
509
510 #[test]
511 fn test_is_dark_color_with_primary_colors() {
512 let treemap = TreeMap::new(vec![]);
513
514 assert!(treemap.is_dark_color(&Color::rgb(1.0, 0.0, 0.0)));
516 assert!(!treemap.is_dark_color(&Color::rgb(0.0, 1.0, 0.0)));
518 assert!(treemap.is_dark_color(&Color::rgb(0.0, 0.0, 1.0)));
520 }
521
522 #[test]
523 fn test_component_span() {
524 let data = sample_treemap_data();
525 let mut treemap = TreeMap::new(data);
526
527 let span = treemap.get_span();
529 assert_eq!(span.columns, 6);
530
531 treemap.set_span(ComponentSpan::new(12));
533 assert_eq!(treemap.get_span().columns, 12);
534 }
535
536 #[test]
537 fn test_component_type() {
538 let treemap = TreeMap::new(vec![]);
539
540 assert_eq!(treemap.component_type(), "TreeMap");
541 }
542
543 #[test]
544 fn test_complexity_score() {
545 let treemap = TreeMap::new(vec![]);
546
547 assert_eq!(treemap.complexity_score(), 70);
548 }
549
550 #[test]
551 fn test_preferred_height() {
552 let treemap = TreeMap::new(vec![]);
553
554 assert_eq!(treemap.preferred_height(1000.0), 250.0);
555 }
556
557 #[test]
558 fn test_treemap_node_with_children() {
559 let data = vec![TreeMapNode {
560 name: "Parent".to_string(),
561 value: 100.0,
562 color: None,
563 children: vec![
564 TreeMapNode {
565 name: "Child 1".to_string(),
566 value: 60.0,
567 color: None,
568 children: vec![],
569 },
570 TreeMapNode {
571 name: "Child 2".to_string(),
572 value: 40.0,
573 color: None,
574 children: vec![],
575 },
576 ],
577 }];
578
579 let treemap = TreeMap::new(data.clone());
580
581 assert_eq!(treemap.data[0].children.len(), 2);
582 assert_eq!(treemap.data[0].children[0].value, 60.0);
583 assert_eq!(treemap.data[0].children[1].value, 40.0);
584 }
585
586 #[test]
587 fn test_layout_nodes_with_wide_rectangle() {
588 let data = vec![
589 TreeMapNode {
590 name: "A".to_string(),
591 value: 50.0,
592 color: None,
593 children: vec![],
594 },
595 TreeMapNode {
596 name: "B".to_string(),
597 value: 50.0,
598 color: None,
599 children: vec![],
600 },
601 ];
602 let treemap = TreeMap::new(data.clone());
603 let mut rects = Vec::new();
604
605 treemap.layout_nodes(&data, 0.0, 0.0, 200.0, 50.0, &mut rects);
607
608 assert_eq!(rects.len(), 2);
609 }
610
611 #[test]
612 fn test_layout_nodes_with_tall_rectangle() {
613 let data = vec![
614 TreeMapNode {
615 name: "A".to_string(),
616 value: 50.0,
617 color: None,
618 children: vec![],
619 },
620 TreeMapNode {
621 name: "B".to_string(),
622 value: 50.0,
623 color: None,
624 children: vec![],
625 },
626 ];
627 let treemap = TreeMap::new(data.clone());
628 let mut rects = Vec::new();
629
630 treemap.layout_nodes(&data, 0.0, 0.0, 50.0, 200.0, &mut rects);
632
633 assert_eq!(rects.len(), 2);
634 }
635
636 #[test]
637 fn test_layout_respects_padding() {
638 let data = vec![TreeMapNode {
639 name: "Single".to_string(),
640 value: 100.0,
641 color: None,
642 children: vec![],
643 }];
644
645 let options = TreeMapOptions {
646 title: None,
647 show_labels: true,
648 padding: 10.0,
649 };
650 let treemap = TreeMap::new(data.clone()).with_options(options);
651 let mut rects = Vec::new();
652
653 treemap.layout_nodes(&data, 0.0, 0.0, 100.0, 100.0, &mut rects);
654
655 let (_, x, y, w, h) = &rects[0];
658 assert!((*x - 10.0).abs() < 0.01);
659 assert!((*y - 10.0).abs() < 0.01);
660 assert!((*w - 80.0).abs() < 0.01);
661 assert!((*h - 80.0).abs() < 0.01);
662 }
663}