use ratatui::style::Color;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum HeatmapColorScale {
#[default]
GreenToRed,
BlueToRed,
CoolToWarm,
Intensity(Color),
Viridis,
Inferno,
Plasma,
BlueWhiteRed,
RedWhiteBlue,
}
pub fn value_to_color(value: f64, min: f64, max: f64, scale: &HeatmapColorScale) -> Color {
let t = if (max - min).abs() < f64::EPSILON {
0.5
} else {
((value - min) / (max - min)).clamp(0.0, 1.0)
};
match scale {
HeatmapColorScale::GreenToRed => {
if t <= 0.5 {
let s = t * 2.0; let r = (255.0 * s) as u8;
let g = 255u8;
Color::Rgb(r, g, 0)
} else {
let s = (t - 0.5) * 2.0; let r = 255u8;
let g = (255.0 * (1.0 - s)) as u8;
Color::Rgb(r, g, 0)
}
}
HeatmapColorScale::BlueToRed => {
if t <= 0.5 {
let s = t * 2.0;
let r = (255.0 * s) as u8;
let b = 255u8;
Color::Rgb(r, 0, b)
} else {
let s = (t - 0.5) * 2.0;
let r = 255u8;
let b = (255.0 * (1.0 - s)) as u8;
Color::Rgb(r, 0, b)
}
}
HeatmapColorScale::CoolToWarm => {
if t <= 0.5 {
let s = t * 2.0;
let r = (200.0 * s) as u8;
let g = (200.0 * s) as u8;
let b = 200u8;
Color::Rgb(r, g, b)
} else {
let s = (t - 0.5) * 2.0;
let r = 200u8;
let g = 200u8;
let b = (200.0 * (1.0 - s)) as u8;
Color::Rgb(r, g, b)
}
}
HeatmapColorScale::Intensity(base_color) => {
let (br, bg, bb) = match base_color {
Color::Rgb(r, g, b) => (*r, *g, *b),
Color::Red => (255, 0, 0),
Color::Green => (0, 255, 0),
Color::Blue => (0, 0, 255),
Color::Yellow => (255, 255, 0),
Color::Cyan => (0, 255, 255),
Color::Magenta => (255, 0, 255),
Color::White => (255, 255, 255),
_ => (128, 128, 128),
};
let factor = 0.2 + 0.8 * t;
let r = (br as f64 * factor) as u8;
let g = (bg as f64 * factor) as u8;
let b = (bb as f64 * factor) as u8;
Color::Rgb(r, g, b)
}
HeatmapColorScale::Viridis => lookup_color(&VIRIDIS_LUT, t),
HeatmapColorScale::Inferno => lookup_color(&INFERNO_LUT, t),
HeatmapColorScale::Plasma => lookup_color(&PLASMA_LUT, t),
HeatmapColorScale::BlueWhiteRed => diverging_color(t, 0, 0, 255, 255, 0, 0),
HeatmapColorScale::RedWhiteBlue => diverging_color(t, 255, 0, 0, 0, 0, 255),
}
}
const VIRIDIS_LUT: [(u8, u8, u8); 16] = [
(68, 1, 84),
(72, 26, 108),
(71, 47, 126),
(65, 68, 135),
(57, 86, 140),
(48, 103, 141),
(39, 119, 142),
(31, 135, 141),
(30, 150, 138),
(44, 166, 130),
(73, 181, 117),
(110, 196, 98),
(155, 208, 72),
(199, 217, 46),
(238, 224, 29),
(253, 231, 37),
];
const INFERNO_LUT: [(u8, u8, u8); 16] = [
(0, 0, 4),
(11, 7, 36),
(32, 12, 74),
(59, 15, 99),
(87, 16, 110),
(114, 17, 112),
(140, 25, 101),
(165, 44, 81),
(187, 65, 58),
(205, 92, 35),
(219, 122, 12),
(230, 155, 0),
(237, 189, 12),
(239, 222, 52),
(237, 249, 121),
(252, 255, 164),
];
const PLASMA_LUT: [(u8, u8, u8); 16] = [
(13, 8, 135),
(49, 4, 150),
(80, 2, 162),
(108, 1, 168),
(134, 2, 166),
(156, 23, 158),
(177, 42, 144),
(195, 63, 126),
(210, 84, 107),
(222, 107, 87),
(231, 131, 67),
(238, 157, 46),
(242, 183, 28),
(243, 210, 22),
(238, 236, 38),
(240, 249, 33),
];
fn lookup_color(lut: &[(u8, u8, u8)], t: f64) -> Color {
let t = t.clamp(0.0, 1.0);
let idx = t * (lut.len() - 1) as f64;
let lo = idx.floor() as usize;
let hi = (lo + 1).min(lut.len() - 1);
let frac = idx - lo as f64;
let (r1, g1, b1) = lut[lo];
let (r2, g2, b2) = lut[hi];
Color::Rgb(
lerp_u8(r1, r2, frac),
lerp_u8(g1, g2, frac),
lerp_u8(b1, b2, frac),
)
}
fn lerp_u8(a: u8, b: u8, t: f64) -> u8 {
(a as f64 + (b as f64 - a as f64) * t).round() as u8
}
fn diverging_color(t: f64, lo_r: u8, lo_g: u8, lo_b: u8, hi_r: u8, hi_g: u8, hi_b: u8) -> Color {
if t <= 0.5 {
let s = t * 2.0; let r = lo_r as f64 + (255.0 - lo_r as f64) * s;
let g = lo_g as f64 + (255.0 - lo_g as f64) * s;
let b = lo_b as f64 + (255.0 - lo_b as f64) * s;
Color::Rgb(r as u8, g as u8, b as u8)
} else {
let s = (t - 0.5) * 2.0; let r = 255.0 + (hi_r as f64 - 255.0) * s;
let g = 255.0 + (hi_g as f64 - 255.0) * s;
let b = 255.0 + (hi_b as f64 - 255.0) * s;
Color::Rgb(r as u8, g as u8, b as u8)
}
}