Files
brahman/crates/modules/pineal/treemap/src/paint.rs
T
sergio 590572b5bb feat(pineal): cierra stub treemap — squarified
Fase F: cuarto stub de pineal cerrado.

- squarify — algoritmo de Bruls, Huizing & van Wijk (2000): asigna a
  cada peso un rect de área proporcional minimizando el peor aspect
  ratio (rects lo más cuadrados posible). Pre-escala pesos al área del
  rect; ordena descendente; tiende filas sobre el lado corto cerrándolas
  cuando agregar un item empeora el ratio. Pesos <=0 → rect vacío.
- paint — painter agnóstico: tiles → fill_rect con gap configurable.

7 tests verdes (proporcionalidad, bounds, edge cases). cargo check
--workspace verde.

Pineal: 4/6 stubs cerrados (export, heatmap, polar, treemap).
Restan flow (sankey) y mesh (graph layout: force-directed/Sugiyama) —
ambos requieren algoritmos de layout sustantivos, foco dedicado.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 14:16:31 +00:00

75 lines
2.2 KiB
Rust

//! Painter agnóstico del treemap: tiles → `fill_rect` contra un `Canvas`.
use crate::squarify::squarify;
use pineal_render::{Canvas, Color, Rect};
/// Una tile del treemap: su peso (área relativa) y su color.
#[derive(Debug, Clone, Copy)]
pub struct Tile {
pub weight: f64,
pub color: Color,
}
impl Tile {
pub fn new(weight: f64, color: Color) -> Self {
Self { weight, color }
}
}
/// Dibuja un treemap de `tiles` dentro de `area`. `gap` es el margen
/// (en px) que se recorta de cada lado de cada tile, para separarlas
/// visualmente. Tiles cuya área no alcanza para el gap se omiten.
pub fn paint_treemap(tiles: &[Tile], area: Rect, gap: f32, canvas: &mut dyn Canvas) {
let weights: Vec<f64> = tiles.iter().map(|t| t.weight).collect();
let rects = squarify(&weights, area);
for (tile, r) in tiles.iter().zip(&rects) {
let inset = Rect::new(
r.x + gap,
r.y + gap,
r.w - 2.0 * gap,
r.h - 2.0 * gap,
);
if inset.w > 0.0 && inset.h > 0.0 {
canvas.fill_rect(inset, tile.color);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pineal_render::{PlanRecorder, RenderCmd};
#[test]
fn one_fill_rect_per_visible_tile() {
let tiles = [
Tile::new(3.0, Color::WHITE),
Tile::new(2.0, Color::BLACK),
Tile::new(1.0, Color::from_hex(0x00ff00)),
];
let mut rec = PlanRecorder::new();
paint_treemap(&tiles, Rect::new(0.0, 0.0, 300.0, 200.0), 1.0, &mut rec);
let n = rec
.into_plan()
.cmds
.iter()
.filter(|c| matches!(c, RenderCmd::FillRect { .. }))
.count();
assert_eq!(n, 3);
}
#[test]
fn zero_weight_tile_is_skipped() {
let tiles = [Tile::new(1.0, Color::WHITE), Tile::new(0.0, Color::BLACK)];
let mut rec = PlanRecorder::new();
paint_treemap(&tiles, Rect::new(0.0, 0.0, 100.0, 100.0), 0.0, &mut rec);
let n = rec
.into_plan()
.cmds
.iter()
.filter(|c| matches!(c, RenderCmd::FillRect { .. }))
.count();
assert_eq!(n, 1);
}
}