# BarChart — diseño
Documento de trabajo. No es API estable, no es documentación pública. Su propósito es alinear la API y la arquitectura del primer widget antes de escribir código.
## Objetivo
Cubrir los huecos del `ratatui::widgets::BarChart` con una sola API consistente:
- Múltiples series por categoría (stacked, grouped, percent-stacked).
- Ejes mejores (auto-tick, formato de números, labels rotados).
- Valores `f64` además de `u64`.
- Negativos y orientación horizontal (post-MVP, pero la API debe admitirlos sin rediseño).
## Comparativa con ratatui::BarChart (0.30)
| Single-series vertical | ✓ | ✓ (caso N=1) |
| Grouped (visual) | parcial¹ | ✓ nativo |
| Stacked | ✗ | ✓ |
| Percent-stacked | ✗ | ✓ |
| Horizontal | parcial² | ✓ (post-MVP) |
| Valores negativos | ✗ | ✓ (post-MVP) |
| Tipo de valor | `u64` | `f64` |
| Ejes con auto-tick | ✗ | ✓ |
| Format de números | manual | builtin (`AxisFormat`) |
| Labels rotados | ✗ | ✓ (post-MVP) |
| Legend | ✗ | ✓ opcional |
¹ `BarGroup` en ratatui solo separa visualmente, no permite barras side-by-side por serie.
² `direction(Direction::Horizontal)` existe pero los labels no se reposicionan bien.
## API propuesta
### Types
```rust
pub struct BarChart<'a> { /* ... */ }
pub struct BarGroup<'a> { /* ... */ }
pub struct Bar<'a> { /* ... */ }
pub enum BarMode {
Single, // una sola serie por grupo
Stacked, // series apiladas
Grouped, // series side-by-side
PercentStacked, // stacked normalizado a 100%
}
// Post-MVP, pero ya reservado en la API:
pub enum BarOrientation { Vertical, Horizontal }
```
### Modelo de datos
Un `BarChart` contiene N `BarGroup`. Un `BarGroup` representa una **categoría** del eje (p.ej. "Q1") y contiene N `Bar`, una por **serie** (p.ej. "ventas", "costes"). El orden de las series debe ser consistente entre grupos para que stacked/grouped tenga sentido.
```rust
let chart = BarChart::new()
.mode(BarMode::Stacked)
.data([
BarGroup::new("Q1").bars([("ventas", 120.0), ("costes", 80.0)]),
BarGroup::new("Q2").bars([("ventas", 150.0), ("costes", 90.0)]),
BarGroup::new("Q3").bars([("ventas", 180.0), ("costes", 100.0)]),
])
.series_colors([Color::Green, Color::Red])
.legend(true)
.bar_width(5)
.group_gap(2);
```
### Builders disponibles
| `.mode(BarMode)` | `Single` | Single si N=1 por grupo |
| `.data(impl IntoIterator<Item = BarGroup>)` | — | Reemplaza datos |
| `.series_colors(impl IntoIterator<Item = Color>)` | rainbow | Cíclico si hay menos colores que series |
| `.legend(bool)` | `false` | Si true, reserva rect para legend |
| `.bar_width(u16)` | auto | Auto reparte el ancho disponible |
| `.group_gap(u16)` | `1` | Celdas vacías entre grupos |
| `.bar_gap(u16)` | `0` | Solo aplica en `Grouped` |
| `.x_axis(AxisConfig)` | sensato | Configura ticks, labels, format |
| `.y_axis(AxisConfig)` | sensato | Idem |
| `.block(Block)` | sin block | Borde/title estilo ratatui |
| `.style(Style)` | default | Estilo base aplicado al fondo |
| `.render_style(RenderStyle)` | `HalfBlock` | Enum `#[non_exhaustive]`. MVP solo expone/acepta `HalfBlock`; otros backends llegan en el PR de polishing junto con el [playground](playground.md). |
### Trait impl
```rust
impl Widget for BarChart<'_> {
fn render(self, area: Rect, buf: &mut Buffer) { /* ... */ }
}
```
Stateless (`Widget` no `StatefulWidget`) en el MVP. Cuando llegue interactividad (hover, tooltip) consideraremos `StatefulWidget` con `BarChartState`.
## Mockups ASCII
Resolución vertical: usamos octants (`▁▂▃▄▅▆▇█`) para 8 sub-niveles por celda. Resolución horizontal: 1 carácter por columna mínima.
### Single (BarMode::Single)
```
ventas trimestrales (€k)
180 ┤ ███
│ ███
│ ███
150 ┤ ███ ███
│ ███ ███
120 ┤ ███ ███ ███
│ ███ ███ ███
│ ███ ███ ███
0 └──────────────────
Q1 Q2 Q3
```
### Stacked (BarMode::Stacked)
```
P&L Q1-Q3 (€k)
280 ┤ ███ ← costes (rojo)
│ ███
│ ███
240 ┤ ███ ▒▒▒ ← ventas (verde)
│ ███ ▒▒▒
200 ┤ ▒▒▒ ▒▒▒
│ ▒▒▒ ▒▒▒
120 ┤ ███ ▒▒▒ ▒▒▒
│ ▒▒▒ ▒▒▒ ▒▒▒
│ ▒▒▒ ▒▒▒ ▒▒▒
0 └──────────────────
Q1 Q2 Q3
▒ ventas █ costes
```
### Grouped (BarMode::Grouped)
```
P&L Q1-Q3 (€k)
180 ┤ ▒▒
│ ▒▒
150 ┤ ▒▒ ▒▒
│ ▒▒ ▒▒
120 ┤ ▒▒ ▒▒ ▒▒
│ ▒▒ ▒▒ ▒▒██
│ ▒▒ ▒▒██ ▒▒██
80 ┤ ▒▒██ ▒▒██ ▒▒██
│ ▒▒██ ▒▒██ ▒▒██
0 └──────────────────
Q1 Q2 Q3
▒ ventas █ costes
```
### Percent stacked (BarMode::PercentStacked)
```
share of revenue
100% ┤ ███ ███ ███ ← costes
│ ███ ███ ███
60% ┤ ▒▒▒ ▒▒▒ ▒▒▒ ← ventas
│ ▒▒▒ ▒▒▒ ▒▒▒
0% └──────────────────
Q1 Q2 Q3
```
## Estructura de módulos propuesta
```
src/
├── lib.rs — re-exports públicos
└── bar/
├── mod.rs — BarChart widget, impl Widget
├── group.rs — BarGroup, Bar (modelo de datos)
├── mode.rs — BarMode enum + helpers
├── render.rs — primitivas de render (1 fn por modo)
├── axis.rs — ejes específicos del bar chart
└── legend.rs — legend opcional
```
`axis.rs` y `legend.rs` arrancan en `bar/` pero si los reusan otros widgets (Line, Area…) los promocionamos a `src/axis/` y `src/legend/` en su momento. No diseñamos para reuse antes de tiempo.
## Alcance del primer PR (MVP)
Hace:
- `BarMode::Single` y `BarMode::Stacked` vertical, con f64.
- Ejes Y con auto-tick y formato básico (`%.0f`, `%.1f`).
- Eje X con labels de grupo, sin rotación.
- `BarGroup` + `Bar` API completa.
- Colores por serie (cíclico).
- `Block` integration (borde/title).
- Render via half-blocks + octants (`▀▄█ ▁..█`). El método `.render_style(RenderStyle)` se incluye en la API desde el MVP con enum `#[non_exhaustive]` que solo expone `HalfBlock`, para añadir backends en el PR de polishing sin breaking change.
- Example runnable: `examples/bar_basic.rs` con los dos modos.
- Tests con `Buffer` snapshots para single y stacked.
NO hace (sigue en backlog explícito):
- `Grouped` y `PercentStacked` (segundo PR).
- Orientation horizontal.
- Valores negativos.
- Labels rotados / truncados.
- Legend (la mention en el mockup pero sin implementación inicial).
- Animaciones / interactividad.
- Otros render styles (`Ascii`, `AsciiColor`, `Braille`, `Octants` standalone). El enum existe en la API pero solo `HalfBlock` está implementado. Los demás backends y el playground interactivo se incorporan en el PR siguiente — ver [docs/design/playground.md](playground.md).
## Open questions
Antes de empezar a codear, decisiones pendientes:
1. **Tipo numérico:** `f64` (más versátil) vs `u64` (lo que usa ratatui). Propuesta: **`f64`**, con `From<u64>` automático.
2. **¿Cómo se especifican los colores de serie?**
- (a) `.series_colors([…])` global como propongo arriba.
- (b) `Bar::with_style(Style)` por barra individual.
- (c) Ambos, con `series_colors` como default y `with_style` como override.
Propuesta: **(c)**.
3. **¿Anchura de barras por defecto auto o explícita?** Propuesta: **auto**, calcula a partir del ancho disponible y nº de grupos/series. `.bar_width(n)` lo fuerza.
4. **¿Spacing entre grupos vs entre barras dentro de un grupo:** Propuesta: dos métodos separados, `group_gap` y `bar_gap` (este último solo aplica en `Grouped`).
5. **¿Block integration:** Propuesta: aceptar `.block(Block)` igual que ratatui — el widget renderiza dentro del inner del block.
6. **¿Eje Y baseline en 0 forzado o auto?** Propuesta: **forzado a 0** para barras (convención de visualización), con escape hatch `.y_axis(AxisConfig::new().baseline(BaselineMode::Auto))` para casos avanzados.
7. **¿AxisConfig se introduce ya en el MVP o lo simplificamos?** Propuesta: introducir con defaults sensatos. Si el AxisConfig se complica demasiado lo recortamos al MVP a `.y_ticks(Vec<f64>)` y `.y_label_format(impl Fn(f64) -> String)`.
8. **¿Idioma de la documentación pública (rustdoc comments)?** El README está en inglés siguiendo el patrón de gitorii. Propuesta: rustdoc en inglés también para consistencia con el ecosistema crates.io. Este DESIGN.md sí en español por ser interno.
9. **¿Naming de la API:** ¿`BarChart` o `Bars` o `BarPlot`? Propuesta: **`BarChart`** por familiaridad con ratatui/usuarios.
10. **¿Versión del módulo:** ¿Quedaría como `trazo::bar::BarChart` o re-export plano `trazo::BarChart`? Propuesta: **ambos** — interno organizado en submódulos, re-export plano en `lib.rs`.
11. **¿Render styles del segundo PR:** Propuesta inicial: **`Ascii`**, **`AsciiColor`**, **`HalfBlock`** (default), **`Octants`**, **`Braille`**. Cada widget declara qué styles tiene sentido soportar; pedir uno no aplicable cae al default del widget con un debug log. Ver [playground.md § Render styles](playground.md) para la tabla completa.
## Para siguientes iteraciones (backlog)
- `Grouped`, `PercentStacked`.
- **Render styles & playground** (PR 2): activar los backends `Ascii`, `AsciiColor`, `Octants`, `Braille` además del `HalfBlock` del MVP, y abrir el playground interactivo en `examples/playground.rs`. Ver [docs/design/playground.md](playground.md).
- `BarOrientation::Horizontal`.
- Valores negativos (eje cruzado en cero).
- Labels rotados, truncados, multi-línea.
- `Legend` widget (compartible con futuros widgets).
- `AxisConfig` extendido (date formatting, log scale, dual axis).
- `StatefulWidget` con hover/tooltip cuando llegue interactividad.