ramp.rs

7.8 kB · rust · 235 lines

1use super::colors::{gradient, shades, Color, ALPHA, BLACK, BLUE, ORANGE, RED, WHITE, YELLOW};2use super::errors::{value_error, Result};3use std::collections::HashMap;45/// A rule that turns counter values into colors.6#[derive(Clone, Debug)]7pub enum Colorizer {8    /// The background at zero, the foreground for everything else.9    TwoTone {10        /// The color at zero.11        background: Color,12        /// The color everywhere else.13        foreground: Color,14    },15    /// The background at zero, then the palette cycled by value.16    Wrap {17        /// The color at zero.18        background: Color,19        /// The colors cycled by value.20        palette: Vec<Color>,21    },22    /// The background at zero, then the ramp binned across the range.23    Bins {24        /// The color at zero.25        background: Color,26        /// The colors binned across the range.27        ramp: Vec<Color>,28    },29    /// A lookup table by exact value, with one color for the unlisted.30    Exact {31        /// The color per exact value.32        table: HashMap<usize, Color>,33        /// The color for values off the table.34        unlisted: Color,35    },36}3738impl Colorizer {39    /// Builds the white-to-black heat ramp.40    pub fn heat() -> Colorizer {41        let ramp = dedup(gradient(&[WHITE, BLACK], 128).unwrap_or_else(|_| vec![BLACK]));42        Colorizer::Bins {43            background: WHITE,44            ramp,45        }46    }47    /// Builds the black-through-ember fire ramp: black, dark red, orange, light yellow.48    pub fn fire() -> Colorizer {49        let stops = [BLACK, shades(RED)[2], ORANGE, shades(YELLOW)[0]];50        let ramp = dedup(gradient(&stops, 128).unwrap_or_else(|_| vec![BLACK]));51        Colorizer::Bins {52            background: ramp[0],53            ramp,54        }55    }56    /// Builds the blue-to-red diverging ramp around a white middle.57    pub fn diverge() -> Colorizer {58        let stops = [BLUE, WHITE, RED];59        let ramp = dedup(gradient(&stops, 128).unwrap_or_else(|_| vec![WHITE]));60        Colorizer::Bins {61            background: ramp[0],62            ramp,63        }64    }65    /// Builds a colorizer with one background and one foreground.66    pub fn two_tone(background: Color, foreground: Color) -> Colorizer {67        Colorizer::TwoTone {68            background,69            foreground,70        }71    }72    /// Builds a colorizer that cycles the palette for values above zero.73    pub fn wrap(background: Color, palette: Vec<Color>) -> Colorizer {74        Colorizer::Wrap {75            background,76            palette,77        }78    }79    /// Builds a binned colorizer from a gradient through the given stops.80    pub fn gradient_bins(background: Color, colors: &[Color], shades: usize) -> Result<Colorizer> {81        let ramp = dedup(gradient(colors, shades.max(1))?);82        Ok(Colorizer::Bins { background, ramp })83    }84    /// Builds an exact-table colorizer that leaves unlisted values transparent.85    pub fn exact(entries: &[(Color, Vec<usize>)]) -> Result<Colorizer> {86        Colorizer::exact_with(entries, ALPHA)87    }88    /// Builds an exact-table colorizer, or an error on a duplicate color or tag.89    pub fn exact_with(entries: &[(Color, Vec<usize>)], unlisted: Color) -> Result<Colorizer> {90        let mut seen_colors: Vec<Color> = Vec::with_capacity(entries.len());91        let mut table: HashMap<usize, Color> = HashMap::new();92        for (color, tags) in entries {93            if seen_colors.contains(color) {94                return value_error(format!(95                    "duplicate colour rgba({},{},{},{}) in ramp; list each colour once with all its tags.",96                    color.r, color.g, color.b, color.a97                ));98            }99            seen_colors.push(*color);100            for &tag in tags {101                if table.insert(tag, *color).is_some() {102                    return value_error(format!("tag {tag} assigned to more than one colour."));103                }104            }105        }106        Ok(Colorizer::Exact { table, unlisted })107    }108    /// Returns the color for one value against the range maximum.109    pub fn color(&self, value: usize, max: usize) -> Color {110        match self {111            Colorizer::TwoTone {112                background,113                foreground,114            } => {115                if value == 0 {116                    *background117                } else {118                    *foreground119                }120            }121            Colorizer::Wrap {122                background,123                palette,124            } => {125                if value == 0 || palette.is_empty() {126                    *background127                } else {128                    palette[(value - 1) % palette.len()]129                }130            }131            Colorizer::Bins { background, ramp } => {132                if value == 0 || ramp.is_empty() {133                    return *background;134                }135                if max <= 1 {136                    return ramp[ramp.len() - 1];137                }138                let idx = (value - 1) * (ramp.len() - 1) / (max - 1).max(1);139                ramp[idx.min(ramp.len() - 1)]140            }141            Colorizer::Exact { table, unlisted } => *table.get(&value).unwrap_or(unlisted),142        }143    }144    /// Maps a slice of values to rgba pixels against the range maximum.145    pub fn colors(&self, values: &[usize], max: usize) -> Vec<[u8; 4]> {146        values147            .iter()148            .map(|&v| {149                let c = self.color(v, max);150                [c.r, c.g, c.b, c.a]151            })152            .collect()153    }154}155156impl Default for Colorizer {157    fn default() -> Colorizer {158        Colorizer::heat()159    }160}161162fn dedup(colors: Vec<Color>) -> Vec<Color> {163    let mut out: Vec<Color> = Vec::with_capacity(colors.len());164    for c in colors {165        if out.last() != Some(&c) {166            out.push(c);167        }168    }169    out170}171172#[cfg(test)]173mod tests {174    use super::*;175    #[test]176    fn two_tone_ignores_magnitude() {177        let r = Colorizer::two_tone(WHITE, BLACK);178        assert_eq!(r.color(0, 9), WHITE);179        assert_eq!(r.color(1, 9), BLACK);180        assert_eq!(r.color(7, 9), BLACK);181    }182    #[test]183    fn wrap_cycles_palette() {184        let r = Colorizer::wrap(WHITE, vec![RED, BLACK]);185        assert_eq!(r.color(0, 9), WHITE);186        assert_eq!(r.color(1, 9), RED);187        assert_eq!(r.color(2, 9), BLACK);188        assert_eq!(r.color(3, 9), RED);189    }190    #[test]191    fn heat_is_white_bg_dark_max() {192        let r = Colorizer::heat();193        assert_eq!(r.color(0, 10), WHITE);194        assert_eq!(r.color(10, 10), BLACK);195    }196    #[test]197    fn bins_spread_across_range() {198        let r = Colorizer::gradient_bins(WHITE, &[WHITE, BLACK], 4).unwrap();199        let low = r.color(1, 100);200        let high = r.color(100, 100);201        assert!(low.r > high.r);202    }203    #[test]204    fn exact_maps_listed_tags_rest_transparent() {205        let r = Colorizer::exact(&[(RED, vec![2, 3, 5, 7]), (BLUE, vec![4, 6])]).unwrap();206        assert_eq!(r.color(3, 9), RED);207        assert_eq!(r.color(7, 9), RED);208        assert_eq!(r.color(4, 9), BLUE);209        assert_eq!(r.color(9, 9), ALPHA);210        assert_eq!(r.color(0, 9), ALPHA);211    }212    #[test]213    fn exact_with_paints_the_rest() {214        let r = Colorizer::exact_with(&[(RED, vec![2, 3])], BLUE).unwrap();215        assert_eq!(r.color(2, 9), RED);216        assert_eq!(r.color(8, 9), BLUE);217    }218    #[test]219    fn exact_rejects_overlapping_tags() {220        let err = Colorizer::exact(&[(RED, vec![2, 3]), (BLUE, vec![3, 4])]);221        assert!(err.is_err());222    }223    #[test]224    fn exact_rejects_duplicate_colors() {225        let err = Colorizer::exact(&[(RED, vec![2]), (RED, vec![5])]);226        assert!(err.is_err());227    }228    #[test]229    fn dedup_collapses_repeats() {230        assert_eq!(231            dedup(vec![BLACK, BLACK, WHITE, WHITE, BLACK]),232            vec![BLACK, WHITE, BLACK]233        );234    }235}