plotpx 0.1.7

Pixel-focused plotting engine that renders magnitude grids, heatmaps, and spectra to RGBA buffers
Documentation
use crate::color_scheme::{default_color_scheme_data, get_color_count, Rgba};

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BarStyle {
    Solid,     // Solid color bars
    Gradient,  // Gradient color bars
    Segmented, // Segmented bars (like LED VU meters)
}

pub struct Spectrum {
    pub num_bins: u32,
    pub width: u32,
    pub height: u32,
    pub max_magnitude: f32,
    pub min_magnitude: f32,
    pub buffer: Vec<f32>,
    pub peak_values: Vec<f32>,
    pub style: BarStyle,
    pub peak_decay: f32,
    pub show_peaks: bool,
    pub bar_width_factor: f32,
    pub background_color: Rgba,
}

impl Spectrum {
    pub fn new(bins: u32, width: u32, height: u32) -> Self {
        Self {
            num_bins: bins,
            width,
            height,
            max_magnitude: f32::NEG_INFINITY,
            min_magnitude: f32::INFINITY,
            buffer: vec![0.0; bins as usize],
            peak_values: vec![0.0; bins as usize],
            style: BarStyle::Solid,
            peak_decay: 0.0,
            show_peaks: false,
            bar_width_factor: 0.8,
            background_color: [0, 0, 0, 0], // Transparent by default
        }
    }

    pub fn set_background_color(&mut self, r: u8, g: u8, b: u8, a: u8) {
        self.background_color = [r, g, b, a];
    }

    pub fn update(&mut self, magnitudes: &[f32]) {
        let len = magnitudes.len().min(self.buffer.len());

        for i in 0..len {
            self.buffer[i] = magnitudes[i];

            // Update peak values
            if self.show_peaks {
                if magnitudes[i] > self.peak_values[i] {
                    self.peak_values[i] = magnitudes[i];
                } else if self.peak_decay > 0.0 {
                    self.peak_values[i] *= 1.0 - self.peak_decay;
                }
            }

            // Update min/max
            if magnitudes[i] > self.max_magnitude {
                self.max_magnitude = magnitudes[i];
            }
            if magnitudes[i] < self.min_magnitude {
                self.min_magnitude = magnitudes[i];
            }
        }
    }

    pub fn update_bin(&mut self, bin: u32, magnitude_value: f32) {
        if bin >= self.num_bins {
            return;
        }

        let idx = bin as usize;
        self.buffer[idx] = magnitude_value;

        // Update peak
        if self.show_peaks {
            if magnitude_value > self.peak_values[idx] {
                self.peak_values[idx] = magnitude_value;
            } else if self.peak_decay > 0.0 {
                self.peak_values[idx] *= 1.0 - self.peak_decay;
            }
        }

        // Update min/max
        if magnitude_value > self.max_magnitude {
            self.max_magnitude = magnitude_value;
        }
        if magnitude_value < self.min_magnitude {
            self.min_magnitude = magnitude_value;
        }
    }

    pub fn shift_buffer_to_non_negative(&mut self) {
        if self.min_magnitude < 0.0 {
            let shift = -self.min_magnitude;
            for val in &mut self.buffer {
                *val += shift;
            }
            for val in &mut self.peak_values {
                *val += shift;
            }
            self.max_magnitude += shift;
            self.min_magnitude = 0.0;
        }
    }

    pub fn render(&mut self) -> Vec<u8> {
        let colors = default_color_scheme_data();
        self.render_with_colors(&colors)
    }

    pub fn render_with_colors(&mut self, colors: &[u8]) -> Vec<u8> {
        self.shift_buffer_to_non_negative();
        let saturation = if self.max_magnitude > 0.0 {
            self.max_magnitude
        } else {
            1.0
        };
        self.render_saturated(colors, saturation)
    }

    pub fn render_saturated(&self, colors: &[u8], saturation: f32) -> Vec<u8> {
        assert!(saturation > 0.0);

        let total_pixels = (self.width * self.height) as usize;
        let mut colorbuf = vec![0u8; total_pixels * 4];

        // Fill with background color
        for i in 0..total_pixels {
            let idx = i * 4;
            colorbuf[idx] = self.background_color[0];
            colorbuf[idx + 1] = self.background_color[1];
            colorbuf[idx + 2] = self.background_color[2];
            colorbuf[idx + 3] = self.background_color[3];
        }

        let ncolors = get_color_count(colors);
        if ncolors == 0 {
            return colorbuf;
        }

        // Determine rendering method based on bins vs pixels
        if self.num_bins <= self.width {
            // Fewer bins than pixels - expand bins to pixels
            self.render_bins_to_pixels(colors, saturation, &mut colorbuf, ncolors);
        } else {
            // More bins than pixels - aggregate bins to pixels
            self.render_pixels_from_bins(colors, saturation, &mut colorbuf, ncolors);
        }

        colorbuf
    }

    fn render_bins_to_pixels(
        &self,
        colors: &[u8],
        saturation: f32,
        colorbuf: &mut [u8],
        ncolors: usize,
    ) {
        let pixels_per_bin = self.width as f32 / self.num_bins as f32;
        let bar_width = (pixels_per_bin * self.bar_width_factor) as u32;
        let bar_spacing = ((pixels_per_bin - bar_width as f32) / 2.0) as u32;

        for bin in 0..self.num_bins {
            let val = self.buffer[bin as usize];
            let normalized = (val / saturation).clamp(0.0, 1.0);

            let bar_height = (normalized * self.height as f32) as u32;
            let bar_start_x = (bin as f32 * pixels_per_bin) as u32 + bar_spacing;
            let bar_end_x = (bar_start_x + bar_width).min(self.width);

            // Draw the bar
            match self.style {
                BarStyle::Solid => {
                    let color_idx = ((ncolors - 1) as f32 * normalized + 0.5) as usize;
                    let color_idx = color_idx.min(ncolors - 1);
                    let color = &colors[color_idx * 4..color_idx * 4 + 4];

                    for y in (self.height - bar_height)..self.height {
                        for x in bar_start_x..bar_end_x {
                            let pixel_idx = ((y * self.width + x) * 4) as usize;
                            colorbuf[pixel_idx..pixel_idx + 4].copy_from_slice(color);
                        }
                    }
                }
                BarStyle::Gradient => {
                    for y in (self.height - bar_height)..self.height {
                        let y_normalized = (self.height - y) as f32 / self.height as f32;
                        let gradient_val = y_normalized * normalized;
                        let color_idx = ((ncolors - 1) as f32 * gradient_val + 0.5) as usize;
                        let color_idx = color_idx.min(ncolors - 1);
                        let color = &colors[color_idx * 4..color_idx * 4 + 4];

                        for x in bar_start_x..bar_end_x {
                            let pixel_idx = ((y * self.width + x) * 4) as usize;
                            colorbuf[pixel_idx..pixel_idx + 4].copy_from_slice(color);
                        }
                    }
                }
                BarStyle::Segmented => {
                    let segment_height = 4; // Height of each segment
                    let segment_gap = 2; // Gap between segments
                    let segment_total = segment_height + segment_gap;

                    let mut y = self.height;
                    while y > self.height - bar_height {
                        let seg_bottom = y;
                        let seg_top =
                            (y.saturating_sub(segment_height)).max(self.height - bar_height);

                        let y_normalized = (self.height - seg_bottom) as f32 / self.height as f32;
                        let color_idx = ((ncolors - 1) as f32 * y_normalized + 0.5) as usize;
                        let color_idx = color_idx.min(ncolors - 1);
                        let color = &colors[color_idx * 4..color_idx * 4 + 4];

                        for seg_y in seg_top..seg_bottom {
                            for x in bar_start_x..bar_end_x {
                                let pixel_idx = ((seg_y * self.width + x) * 4) as usize;
                                colorbuf[pixel_idx..pixel_idx + 4].copy_from_slice(color);
                            }
                        }

                        if y <= segment_total {
                            break;
                        }
                        y -= segment_total;
                    }
                }
            }

            // Draw peak indicator if enabled
            if self.show_peaks {
                let peak_val = self.peak_values[bin as usize];
                let peak_normalized = (peak_val / saturation).clamp(0.0, 1.0);
                let peak_y = self.height - (peak_normalized * self.height as f32) as u32;

                if peak_y < self.height {
                    // Draw a white line for the peak
                    for x in bar_start_x..bar_end_x {
                        let pixel_idx = ((peak_y * self.width + x) * 4) as usize;
                        colorbuf[pixel_idx] = 255; // R
                        colorbuf[pixel_idx + 1] = 255; // G
                        colorbuf[pixel_idx + 2] = 255; // B
                        colorbuf[pixel_idx + 3] = 255; // A
                    }
                }
            }
        }
    }

    fn render_pixels_from_bins(
        &self,
        colors: &[u8],
        saturation: f32,
        colorbuf: &mut [u8],
        ncolors: usize,
    ) {
        let bins_per_pixel = self.num_bins as f32 / self.width as f32;

        for x in 0..self.width {
            let bin_start = (x as f32 * bins_per_pixel) as u32;
            let bin_end = ((x + 1) as f32 * bins_per_pixel) as u32;
            let bin_end = bin_end.min(self.num_bins);

            // Aggregate bins for this pixel column (take maximum)
            let mut max_val = 0.0f32;
            let mut max_peak = 0.0f32;
            for bin in bin_start..bin_end {
                max_val = max_val.max(self.buffer[bin as usize]);
                if self.show_peaks {
                    max_peak = max_peak.max(self.peak_values[bin as usize]);
                }
            }

            let normalized = (max_val / saturation).clamp(0.0, 1.0);
            let bar_height = (normalized * self.height as f32) as u32;

            // Draw the column
            match self.style {
                BarStyle::Solid => {
                    let color_idx = ((ncolors - 1) as f32 * normalized + 0.5) as usize;
                    let color_idx = color_idx.min(ncolors - 1);
                    let color = &colors[color_idx * 4..color_idx * 4 + 4];

                    for y in (self.height - bar_height)..self.height {
                        let pixel_idx = ((y * self.width + x) * 4) as usize;
                        colorbuf[pixel_idx..pixel_idx + 4].copy_from_slice(color);
                    }
                }
                BarStyle::Gradient => {
                    for y in (self.height - bar_height)..self.height {
                        let y_normalized = (self.height - y) as f32 / self.height as f32;
                        let gradient_val = y_normalized * normalized;
                        let color_idx = ((ncolors - 1) as f32 * gradient_val + 0.5) as usize;
                        let color_idx = color_idx.min(ncolors - 1);
                        let color = &colors[color_idx * 4..color_idx * 4 + 4];

                        let pixel_idx = ((y * self.width + x) * 4) as usize;
                        colorbuf[pixel_idx..pixel_idx + 4].copy_from_slice(color);
                    }
                }
                BarStyle::Segmented => {
                    // Similar to bins_to_pixels but for single column
                    let segment_height = 2;
                    let segment_gap = 1;
                    let segment_total = segment_height + segment_gap;

                    let mut y = self.height;
                    while y > self.height - bar_height {
                        let seg_bottom = y;
                        let seg_top =
                            (y.saturating_sub(segment_height)).max(self.height - bar_height);

                        let y_normalized = (self.height - seg_bottom) as f32 / self.height as f32;
                        let color_idx = ((ncolors - 1) as f32 * y_normalized + 0.5) as usize;
                        let color_idx = color_idx.min(ncolors - 1);
                        let color = &colors[color_idx * 4..color_idx * 4 + 4];

                        for seg_y in seg_top..seg_bottom {
                            let pixel_idx = ((seg_y * self.width + x) * 4) as usize;
                            colorbuf[pixel_idx..pixel_idx + 4].copy_from_slice(color);
                        }

                        if y <= segment_total {
                            break;
                        }
                        y -= segment_total;
                    }
                }
            }

            // Draw peak indicator
            if self.show_peaks && max_peak > 0.0 {
                let peak_normalized = (max_peak / saturation).clamp(0.0, 1.0);
                let peak_y = self.height - (peak_normalized * self.height as f32) as u32;

                if peak_y < self.height {
                    let pixel_idx = ((peak_y * self.width + x) * 4) as usize;
                    colorbuf[pixel_idx] = 255; // R
                    colorbuf[pixel_idx + 1] = 255; // G
                    colorbuf[pixel_idx + 2] = 255; // B
                    colorbuf[pixel_idx + 3] = 255; // A
                }
            }
        }
    }

    pub fn reset(&mut self) {
        self.buffer.fill(0.0);
        self.peak_values.fill(0.0);
        self.max_magnitude = f32::NEG_INFINITY;
        self.min_magnitude = f32::INFINITY;
    }
}