use crate::primitives::Color;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Colormap {
Viridis,
Plasma,
Inferno,
Magma,
Cividis,
Turbo,
Coolwarm,
Spectral,
Blues,
Reds,
Greens,
Greys,
YlOrRd,
RdBu,
}
impl Colormap {
pub fn map(&self, t: f64) -> Color {
let t = t.clamp(0.0, 1.0);
let c = self.gradient().eval_continuous(t);
Color::rgb(c.r, c.g, c.b)
}
pub fn map_value(&self, val: f64, vmin: f64, vmax: f64) -> Color {
let t = if (vmax - vmin).abs() < f64::EPSILON {
0.5
} else {
(val - vmin) / (vmax - vmin)
};
self.map(t)
}
pub fn map_values(&self, vals: &[f64]) -> Vec<Color> {
let (vmin, vmax) = finite_bounds(vals);
vals.iter().map(|&v| self.map_value(v, vmin, vmax)).collect()
}
fn gradient(&self) -> colorous::Gradient {
match self {
Colormap::Viridis => colorous::VIRIDIS,
Colormap::Plasma => colorous::PLASMA,
Colormap::Inferno => colorous::INFERNO,
Colormap::Magma => colorous::MAGMA,
Colormap::Cividis => colorous::CIVIDIS,
Colormap::Turbo => colorous::TURBO,
Colormap::Coolwarm => colorous::COOL,
Colormap::Spectral => colorous::SPECTRAL,
Colormap::Blues => colorous::BLUES,
Colormap::Reds => colorous::REDS,
Colormap::Greens => colorous::GREENS,
Colormap::Greys => colorous::GREYS,
Colormap::YlOrRd => colorous::YELLOW_ORANGE_RED,
Colormap::RdBu => colorous::RED_BLUE,
}
}
}
fn finite_bounds(vals: &[f64]) -> (f64, f64) {
let mut lo = f64::INFINITY;
let mut hi = f64::NEG_INFINITY;
for &v in vals {
if v.is_finite() {
if v < lo {
lo = v;
}
if v > hi {
hi = v;
}
}
}
if lo.is_finite() && hi.is_finite() {
(lo, hi)
} else {
(0.0, 1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn viridis_at_zero_is_dark_purple() {
let c = Colormap::Viridis.map(0.0);
assert_eq!(c.r, 68);
assert_eq!(c.g, 1);
assert_eq!(c.b, 84);
assert_eq!(c.a, 255);
}
#[test]
fn viridis_at_one_is_yellow() {
let c = Colormap::Viridis.map(1.0);
assert_eq!(c.r, 253);
assert_eq!(c.g, 231);
assert_eq!(c.b, 37);
assert_eq!(c.a, 255);
}
#[test]
fn viridis_midpoint_is_teal() {
let c = Colormap::Viridis.map(0.5);
assert!(c.g > c.r, "midpoint green channel should exceed red");
assert!(c.g > c.b, "midpoint green channel should exceed blue");
assert_eq!(c.a, 255);
}
#[test]
fn map_clamps_below_zero() {
let c = Colormap::Viridis.map(-0.5);
let c0 = Colormap::Viridis.map(0.0);
assert_eq!(c, c0);
}
#[test]
fn map_clamps_above_one() {
let c = Colormap::Viridis.map(1.5);
let c1 = Colormap::Viridis.map(1.0);
assert_eq!(c, c1);
}
#[test]
fn map_value_normalises_correctly() {
let c = Colormap::Viridis.map_value(50.0, 0.0, 100.0);
let expected = Colormap::Viridis.map(0.5);
assert_eq!(c, expected);
}
#[test]
fn map_value_equal_bounds_gives_midpoint() {
let c = Colormap::Plasma.map_value(5.0, 5.0, 5.0);
let expected = Colormap::Plasma.map(0.5);
assert_eq!(c, expected);
}
#[test]
fn map_values_batch() {
let vals = vec![0.0, 50.0, 100.0];
let colors = Colormap::Viridis.map_values(&vals);
assert_eq!(colors.len(), 3);
assert_eq!(colors[0], Colormap::Viridis.map(0.0));
assert_eq!(colors[1], Colormap::Viridis.map(0.5));
assert_eq!(colors[2], Colormap::Viridis.map(1.0));
}
#[test]
fn map_values_empty_slice() {
let colors = Colormap::Viridis.map_values(&[]);
assert!(colors.is_empty());
}
#[test]
fn map_values_single_element() {
let colors = Colormap::Viridis.map_values(&[42.0]);
assert_eq!(colors.len(), 1);
assert_eq!(colors[0], Colormap::Viridis.map(0.5));
}
#[test]
fn all_colormaps_produce_opaque_colors() {
let all = [
Colormap::Viridis,
Colormap::Plasma,
Colormap::Inferno,
Colormap::Magma,
Colormap::Cividis,
Colormap::Turbo,
Colormap::Coolwarm,
Colormap::Spectral,
Colormap::Blues,
Colormap::Reds,
Colormap::Greens,
Colormap::Greys,
Colormap::YlOrRd,
Colormap::RdBu,
];
for cmap in &all {
for &t in &[0.0, 0.25, 0.5, 0.75, 1.0] {
let c = cmap.map(t);
assert_eq!(c.a, 255, "{cmap:?} at t={t} should be fully opaque");
}
}
}
#[test]
fn plasma_endpoints_differ() {
let lo = Colormap::Plasma.map(0.0);
let hi = Colormap::Plasma.map(1.0);
assert_ne!(lo, hi, "plasma endpoints should be different colors");
}
#[test]
fn finite_bounds_skips_nan() {
let vals = vec![f64::NAN, 1.0, 5.0, f64::NAN];
let (lo, hi) = finite_bounds(&vals);
assert!((lo - 1.0).abs() < f64::EPSILON);
assert!((hi - 5.0).abs() < f64::EPSILON);
}
#[test]
fn finite_bounds_all_nan_fallback() {
let vals = vec![f64::NAN, f64::NAN];
let (lo, hi) = finite_bounds(&vals);
assert!((lo - 0.0).abs() < f64::EPSILON);
assert!((hi - 1.0).abs() < f64::EPSILON);
}
}