aprender-viz 0.29.0

SIMD/GPU/WASM-accelerated visualization library for data science and ML
# Test-Driven Visualization

This chapter explains the TDD methodology used throughout trueno-viz
development and how to apply it to your own visualization code.

## The TDD Cycle

```text
┌─────────────────────────────────────────────────────────────┐
│                     TDD CYCLE                               │
│                                                             │
│    ┌───────┐         ┌───────┐         ┌──────────┐        │
│    │  RED  │ ──────▶ │ GREEN │ ──────▶ │ REFACTOR │        │
│    │       │         │       │         │          │        │
│    │ Write │         │ Make  │         │ Improve  │        │
│    │ Test  │         │ Pass  │         │ Code     │        │
│    └───────┘         └───────┘         └──────────┘        │
│         ▲                                    │             │
│         └────────────────────────────────────┘             │
└─────────────────────────────────────────────────────────────┘
```

## RED Phase: Writing Failing Tests

Before implementing features, write tests that define expected behavior:

```rust
#[test]
fn test_scatter_plot_basic() {
    let x = vec![1.0, 2.0, 3.0];
    let y = vec![4.0, 5.0, 6.0];

    let plot = ScatterPlot::new()
        .x(&x)
        .y(&y)
        .build();

    // Verify data stored correctly
    assert_eq!(plot.x_data(), &x);
    assert_eq!(plot.y_data(), &y);
    assert_eq!(plot.len(), 3);
}
```

Run: `cargo test test_scatter_plot_basic` → FAILS (RED)

## GREEN Phase: Minimal Implementation

Implement just enough to pass:

```rust
impl ScatterPlot {
    pub fn new() -> Self {
        Self {
            x_data: Vec::new(),
            y_data: Vec::new(),
        }
    }

    pub fn x(mut self, data: &[f32]) -> Self {
        self.x_data = data.to_vec();
        self
    }

    pub fn y(mut self, data: &[f32]) -> Self {
        self.y_data = data.to_vec();
        self
    }

    pub fn build(self) -> ScatterPlotResult {
        ScatterPlotResult {
            x_data: self.x_data,
            y_data: self.y_data,
        }
    }
}
```

Run: `cargo test test_scatter_plot_basic` → PASSES (GREEN)

## REFACTOR Phase: Improve Code Quality

Improve without changing behavior:

```rust
impl ScatterPlot {
    pub fn x(mut self, data: &[f32]) -> Self {
        self.x_data = data.to_vec();
        self
    }

    // Refactored: validate data length matches
    pub fn build(self) -> Result<ScatterPlotResult> {
        if self.x_data.len() != self.y_data.len() {
            return Err(Error::DataLengthMismatch);
        }
        Ok(ScatterPlotResult {
            x_data: self.x_data,
            y_data: self.y_data,
        })
    }
}
```

Add test for the new validation:

```rust
#[test]
fn test_scatter_mismatched_lengths() {
    let x = vec![1.0, 2.0, 3.0];
    let y = vec![4.0, 5.0];  // Different length!

    let result = ScatterPlot::new()
        .x(&x)
        .y(&y)
        .build();

    assert!(result.is_err());
}
```

## Test Categories

### Unit Tests

Test individual functions in isolation:

```rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_linear_scale_transform() {
        let scale = LinearScale::new()
            .domain(0.0, 100.0)
            .range(0.0, 800.0);

        assert!((scale.transform(50.0) - 400.0).abs() < f32::EPSILON);
    }
}
```

### Integration Tests

Test components working together:

```rust
// tests/integration_test.rs
use trueno_viz::prelude::*;
use trueno_viz::plots::ScatterPlot;

#[test]
fn test_scatter_to_png() {
    let plot = ScatterPlot::new()
        .x(&[1.0, 2.0, 3.0])
        .y(&[1.0, 4.0, 9.0])
        .build();

    let result = plot.render_to_bytes(800, 600);
    assert!(result.is_ok());

    let bytes = result.unwrap();
    assert!(!bytes.is_empty());
    assert_eq!(&bytes[0..8], b"\x89PNG\r\n\x1a\n"); // PNG magic bytes
}
```

### Edge Case Tests

Test boundary conditions:

```rust
#[test]
fn test_histogram_empty_data() {
    let empty: Vec<f32> = vec![];
    let hist = Histogram::new(&empty).build();
    assert_eq!(hist.bin_count(), 0);
}

#[test]
fn test_histogram_single_value() {
    let single = vec![5.0];
    let hist = Histogram::new(&single).build();
    assert_eq!(hist.bin_count(), 1);
}

#[test]
fn test_histogram_all_same_value() {
    let same = vec![3.0, 3.0, 3.0, 3.0];
    let hist = Histogram::new(&same).build();
    // Should handle gracefully
    assert!(hist.bin_count() >= 1);
}
```

## Running Tests

```bash
# All tests
cargo test

# Specific test
cargo test test_scatter_plot_basic

# Tests with output
cargo test -- --nocapture

# Tests in specific module
cargo test plots::scatter

# With coverage
cargo llvm-cov --html
```

## Test Organization in trueno-viz

```text
src/
├── plots/
│   ├── scatter.rs     # Contains #[cfg(test)] mod tests
│   ├── histogram.rs   # Contains #[cfg(test)] mod tests
│   └── ...
└── ...

tests/                  # Integration tests
├── plots_test.rs
├── output_test.rs
└── integration_test.rs
```

## Complete TDD Example

Feature: Add regression line to scatter plot

### Step 1: Write Test (RED)

```rust
#[test]
fn test_scatter_with_regression() {
    let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
    let y = vec![2.1, 3.9, 6.2, 7.8, 10.1];

    let plot = ScatterPlot::new()
        .x(&x)
        .y(&y)
        .regression_line(true)
        .build();

    // Regression line should be calculated
    let (slope, intercept) = plot.regression_coefficients();
    assert!((slope - 2.0).abs() < 0.1);     // Should be ~2.0
    assert!((intercept - 0.0).abs() < 0.5); // Should be ~0
}
```

### Step 2: Implement (GREEN)

```rust
impl ScatterPlotBuilder {
    pub fn regression_line(mut self, show: bool) -> Self {
        self.show_regression = show;
        self
    }
}

impl ScatterPlotResult {
    pub fn regression_coefficients(&self) -> (f32, f32) {
        let n = self.x_data.len() as f32;
        let sum_x: f32 = self.x_data.iter().sum();
        let sum_y: f32 = self.y_data.iter().sum();
        let sum_xy: f32 = self.x_data.iter()
            .zip(self.y_data.iter())
            .map(|(x, y)| x * y)
            .sum();
        let sum_x2: f32 = self.x_data.iter().map(|x| x * x).sum();

        let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x);
        let intercept = (sum_y - slope * sum_x) / n;

        (slope, intercept)
    }
}
```

### Step 3: Refactor

- Extract statistics to separate module
- Add SIMD optimization
- Improve numerical stability

## Next Chapter

Continue to [Property-Based Testing](./property-testing.md) for advanced testing techniques.