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}