use crate::color::Color;
use crate::core::{Bounds, Canvas, Drawable, Point2D};
use crate::error::Result;
#[derive(Debug, Clone)]
pub struct TreemapItem {
pub label: String,
pub value: f64,
pub color: Color,
}
#[derive(Debug, Clone)]
struct TreemapRect {
x: f64,
y: f64,
width: f64,
height: f64,
item: TreemapItem,
}
pub struct Treemap {
items: Vec<TreemapItem>,
show_labels: bool,
show_values: bool,
border_width: f32,
border_color: Color,
padding: f64,
}
impl Treemap {
#[must_use]
pub fn new() -> Self {
Self {
items: Vec::new(),
show_labels: true,
show_values: true,
border_width: 1.5,
border_color: Color::WHITE,
padding: 2.0,
}
}
pub fn add_item(&mut self, label: impl Into<String>, value: f64, color: Color) -> &mut Self {
if value > 0.0 {
self.items.push(TreemapItem {
label: label.into(),
value,
color,
});
}
self
}
#[must_use]
pub fn show_labels(mut self, show: bool) -> Self {
self.show_labels = show;
self
}
#[must_use]
pub fn show_values(mut self, show: bool) -> Self {
self.show_values = show;
self
}
#[must_use]
pub fn border_width(mut self, width: f32) -> Self {
self.border_width = width.max(0.0);
self
}
#[must_use]
pub fn border_color(mut self, color: Color) -> Self {
self.border_color = color;
self
}
#[must_use]
pub fn padding(mut self, padding: f64) -> Self {
self.padding = padding.max(0.0);
self
}
#[must_use]
pub fn bounds(&self) -> Option<Bounds> {
if self.items.is_empty() {
return None;
}
Some(Bounds::new(0.0, 100.0, 0.0, 100.0))
}
fn calculate_layout(&self) -> Vec<TreemapRect> {
if self.items.is_empty() {
return Vec::new();
}
let mut sorted_items = self.items.clone();
sorted_items.sort_by(|a, b| b.value.partial_cmp(&a.value).unwrap());
let total: f64 = sorted_items.iter().map(|item| item.value).sum();
if total <= 0.0 {
return Vec::new();
}
let normalized: Vec<(TreemapItem, f64)> = sorted_items
.into_iter()
.map(|item| {
let normalized_value = (item.value / total) * 10000.0; (item, normalized_value)
})
.collect();
self.squarify(&normalized, 0.0, 0.0, 100.0, 100.0)
}
fn squarify(
&self,
items: &[(TreemapItem, f64)],
x: f64,
y: f64,
width: f64,
height: f64,
) -> Vec<TreemapRect> {
if items.is_empty() {
return Vec::new();
}
if items.len() == 1 {
return vec![TreemapRect {
x: x + self.padding,
y: y + self.padding,
width: width - 2.0 * self.padding,
height: height - 2.0 * self.padding,
item: items[0].0.clone(),
}];
}
let vertical = width >= height;
let total: f64 = items.iter().map(|(_, v)| v).sum();
let mut current_offset = 0.0;
let mut rects = Vec::new();
for (item, value) in items {
let proportion = value / total;
let rect = if vertical {
let rect_width = width * proportion;
TreemapRect {
x: x + current_offset + self.padding,
y: y + self.padding,
width: rect_width - 2.0 * self.padding,
height: height - 2.0 * self.padding,
item: item.clone(),
}
} else {
let rect_height = height * proportion;
TreemapRect {
x: x + self.padding,
y: y + current_offset + self.padding,
width: width - 2.0 * self.padding,
height: rect_height - 2.0 * self.padding,
item: item.clone(),
}
};
if vertical {
current_offset += width * proportion;
} else {
current_offset += height * proportion;
}
rects.push(rect);
}
rects
}
}
impl Default for Treemap {
fn default() -> Self {
Self::new()
}
}
impl Drawable for Treemap {
fn draw(&self, canvas: &mut dyn Canvas) -> Result<()> {
let rects = self.calculate_layout();
for rect in &rects {
let top_left = Point2D::new(rect.x, rect.y);
canvas.draw_rectangle(
&top_left,
rect.width,
rect.height,
&rect.item.color.to_rgba(),
)?;
if self.border_width > 0.0 {
let border_color = self.border_color.to_rgba();
canvas.draw_line(
&Point2D::new(rect.x, rect.y),
&Point2D::new(rect.x + rect.width, rect.y),
&border_color,
self.border_width,
)?;
canvas.draw_line(
&Point2D::new(rect.x + rect.width, rect.y),
&Point2D::new(rect.x + rect.width, rect.y + rect.height),
&border_color,
self.border_width,
)?;
canvas.draw_line(
&Point2D::new(rect.x + rect.width, rect.y + rect.height),
&Point2D::new(rect.x, rect.y + rect.height),
&border_color,
self.border_width,
)?;
canvas.draw_line(
&Point2D::new(rect.x, rect.y + rect.height),
&Point2D::new(rect.x, rect.y),
&border_color,
self.border_width,
)?;
}
if rect.width > 15.0 && rect.height > 10.0 {
let text_x = rect.x + rect.width / 2.0;
let text_y = rect.y + rect.height / 2.0;
let text_color = if self.is_dark_color(&rect.item.color) {
Color::WHITE
} else {
Color::from_hex("#2c3e50").unwrap()
};
if self.show_labels {
let label_y = if self.show_values {
text_y - 6.0
} else {
text_y
};
canvas.draw_text(
&rect.item.label,
text_x as f32,
label_y as f32,
11.0,
&text_color.to_rgba(),
)?;
}
if self.show_values && rect.height > 25.0 {
let value_y = if self.show_labels {
text_y + 6.0
} else {
text_y
};
canvas.draw_text(
&format!("{:.1}", rect.item.value),
text_x as f32,
value_y as f32,
9.0,
&text_color.to_rgba(),
)?;
}
}
}
Ok(())
}
}
impl Treemap {
fn is_dark_color(&self, color: &Color) -> bool {
let rgba = color.to_rgba();
let r = f64::from(rgba[0]) / 255.0;
let g = f64::from(rgba[1]) / 255.0;
let b = f64::from(rgba[2]) / 255.0;
let luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
luminance < 0.5
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_treemap_creation() {
let treemap = Treemap::new();
assert!(treemap.items.is_empty());
}
#[test]
fn test_add_item() {
let mut treemap = Treemap::new();
treemap.add_item("Test", 100.0, Color::RED);
assert_eq!(treemap.items.len(), 1);
assert_eq!(treemap.items[0].label, "Test");
assert_eq!(treemap.items[0].value, 100.0);
}
#[test]
fn test_negative_value_ignored() {
let mut treemap = Treemap::new();
treemap.add_item("Test", -50.0, Color::RED);
assert_eq!(treemap.items.len(), 0);
}
#[test]
fn test_bounds() {
let mut treemap = Treemap::new();
treemap.add_item("A", 50.0, Color::RED);
let bounds = treemap.bounds().unwrap();
assert_eq!(bounds.x_min, 0.0);
assert_eq!(bounds.x_max, 100.0);
assert_eq!(bounds.y_min, 0.0);
assert_eq!(bounds.y_max, 100.0);
}
#[test]
fn test_empty_bounds() {
let treemap = Treemap::new();
assert!(treemap.bounds().is_none());
}
#[test]
fn test_layout_calculation() {
let mut treemap = Treemap::new();
treemap.add_item("A", 50.0, Color::RED);
treemap.add_item("B", 30.0, Color::BLUE);
treemap.add_item("C", 20.0, Color::GREEN);
let rects = treemap.calculate_layout();
assert_eq!(rects.len(), 3);
let total_area: f64 = rects
.iter()
.map(|r| (r.width + 2.0 * treemap.padding) * (r.height + 2.0 * treemap.padding))
.sum();
assert!((total_area - 10000.0).abs() < 100.0); }
}