ramp.rs

3.5 kB · rust · 114 lines

1use super::colors::{gradient, shades, Color, BLACK, BLUE, ORANGE, RED, WHITE, YELLOW};2use super::errors::Result;34/// A rule that turns counter values into colors.5#[derive(Clone, Debug)]6pub enum Colorizer {7    /// The background at zero, then the ramp binned across the range.8    Bins {9        /// The color at zero.10        background: Color,11        /// The colors binned across the range.12        ramp: Vec<Color>,13    },14}1516impl Colorizer {17    /// Builds the white-to-black heat ramp.18    pub fn heat() -> Colorizer {19        let ramp = dedup(gradient(&[WHITE, BLACK], 128).unwrap_or_else(|_| vec![BLACK]));20        Colorizer::Bins {21            background: WHITE,22            ramp,23        }24    }25    /// Builds the black-through-ember fire ramp: black, dark red, orange, light yellow.26    pub fn fire() -> Colorizer {27        let stops = [BLACK, shades(RED)[2], ORANGE, shades(YELLOW)[0]];28        let ramp = dedup(gradient(&stops, 128).unwrap_or_else(|_| vec![BLACK]));29        Colorizer::Bins {30            background: ramp[0],31            ramp,32        }33    }34    /// Builds the blue-to-red diverging ramp around a white middle.35    pub fn diverge() -> Colorizer {36        let stops = [BLUE, WHITE, RED];37        let ramp = dedup(gradient(&stops, 128).unwrap_or_else(|_| vec![WHITE]));38        Colorizer::Bins {39            background: ramp[0],40            ramp,41        }42    }43    /// Builds a binned colorizer from a gradient through the given stops.44    pub fn gradient_bins(background: Color, colors: &[Color], shades: usize) -> Result<Colorizer> {45        let ramp = dedup(gradient(colors, shades.max(1))?);46        Ok(Colorizer::Bins { background, ramp })47    }48    /// Returns the color for one value against the range maximum.49    pub fn color(&self, value: usize, max: usize) -> Color {50        match self {51            Colorizer::Bins { background, ramp } => {52                if value == 0 || ramp.is_empty() {53                    return *background;54                }55                if max <= 1 {56                    return ramp[ramp.len() - 1];57                }58                let idx = (value - 1) * (ramp.len() - 1) / (max - 1).max(1);59                ramp[idx.min(ramp.len() - 1)]60            }61        }62    }63    /// Maps a slice of values to rgba pixels against the range maximum.64    pub fn colors(&self, values: &[usize], max: usize) -> Vec<[u8; 4]> {65        values66            .iter()67            .map(|&v| {68                let c = self.color(v, max);69                [c.r, c.g, c.b, c.a]70            })71            .collect()72    }73}7475impl Default for Colorizer {76    fn default() -> Colorizer {77        Colorizer::heat()78    }79}8081fn dedup(colors: Vec<Color>) -> Vec<Color> {82    let mut out: Vec<Color> = Vec::with_capacity(colors.len());83    for c in colors {84        if out.last() != Some(&c) {85            out.push(c);86        }87    }88    out89}9091#[cfg(test)]92mod tests {93    use super::*;94    #[test]95    fn heat_is_white_bg_dark_max() {96        let r = Colorizer::heat();97        assert_eq!(r.color(0, 10), WHITE);98        assert_eq!(r.color(10, 10), BLACK);99    }100    #[test]101    fn bins_spread_across_range() {102        let r = Colorizer::gradient_bins(WHITE, &[WHITE, BLACK], 4).unwrap();103        let low = r.color(1, 100);104        let high = r.color(100, 100);105        assert!(low.r > high.r);106    }107    #[test]108    fn dedup_collapses_repeats() {109        assert_eq!(110            dedup(vec![BLACK, BLACK, WHITE, WHITE, BLACK]),111            vec![BLACK, WHITE, BLACK]112        );113    }114}