use crate::aes::Aesthetic;
use crate::data::Value;
use std::collections::HashMap;
use super::util::nice_step;
use super::Scale;
#[derive(Clone, Debug, Copy)]
pub struct RGBAColor {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: f64,
}
impl RGBAColor {
pub fn new(r: u8, g: u8, b: u8) -> Self {
RGBAColor { r, g, b, a: 1.0 }
}
pub fn with_alpha(mut self, a: f64) -> Self {
self.a = a;
self
}
pub fn lerp(&self, other: &RGBAColor, t: f64) -> RGBAColor {
let t = t.clamp(0.0, 1.0);
RGBAColor {
r: (self.r as f64 * (1.0 - t) + other.r as f64 * t) as u8,
g: (self.g as f64 * (1.0 - t) + other.g as f64 * t) as u8,
b: (self.b as f64 * (1.0 - t) + other.b as f64 * t) as u8,
a: self.a * (1.0 - t) + other.a * t,
}
}
}
pub const DEFAULT_PALETTE: &[RGBAColor] = &[
RGBAColor {
r: 248,
g: 118,
b: 109,
a: 1.0,
}, RGBAColor {
r: 0,
g: 186,
b: 56,
a: 1.0,
}, RGBAColor {
r: 97,
g: 156,
b: 255,
a: 1.0,
}, RGBAColor {
r: 163,
g: 103,
b: 203,
a: 1.0,
}, RGBAColor {
r: 231,
g: 138,
b: 0,
a: 1.0,
}, RGBAColor {
r: 0,
g: 191,
b: 196,
a: 1.0,
}, RGBAColor {
r: 199,
g: 124,
b: 255,
a: 1.0,
}, RGBAColor {
r: 127,
g: 127,
b: 127,
a: 1.0,
}, ];
#[derive(Clone, Debug)]
pub struct ScaleColorDiscrete {
aesthetic: Aesthetic,
name: String,
levels: Vec<String>,
level_index: HashMap<String, usize>,
palette: Vec<RGBAColor>,
sorted: bool,
generated: Option<GeneratedPalette>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct HuePalette {
pub h: (f64, f64),
pub c: f64,
pub l: f64,
pub h_start: f64,
pub direction: i32,
}
impl Default for HuePalette {
fn default() -> Self {
HuePalette {
h: (15.0, 375.0),
c: 100.0,
l: 65.0,
h_start: 0.0,
direction: 1,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum GeneratedPalette {
Hue(HuePalette),
Viridis {
option: char,
begin: f64,
end: f64,
direction: i32,
},
}
impl GeneratedPalette {
pub fn color_at(&self, i: usize, n: usize) -> RGBAColor {
match self {
GeneratedPalette::Hue(p) => {
let n = n.max(1);
let i = if p.direction == -1 {
n - 1 - i % n
} else {
i % n
};
hue_color(i, n, p)
}
_ => {
let pal = self.colors(n);
pal[i % pal.len()]
}
}
}
pub fn colors(&self, n: usize) -> Vec<RGBAColor> {
let n = n.max(1);
match self {
GeneratedPalette::Hue(p) => hue_palette(n, p),
GeneratedPalette::Viridis {
option,
begin,
end,
direction,
} => {
let opt = option.to_ascii_uppercase();
let lut = super::viridis_lut::VIRIDIS_LUT
.iter()
.find(|(o, _)| *o == opt)
.map_or(&super::viridis_lut::VIRIDIS_LUT[3].1, |(_, l)| l);
let at = |t: f64| {
let f = t.clamp(0.0, 1.0) * 255.0;
let (i, j) = (f.floor() as usize, (f.ceil() as usize).min(255));
let w = f - i as f64;
let mix = |k: usize| {
(lut[i][k] as f64 * (1.0 - w) + lut[j][k] as f64 * w).round() as u8
};
RGBAColor::new(mix(0), mix(1), mix(2))
};
let mut out: Vec<RGBAColor> = (0..n)
.map(|i| {
let t = if n == 1 {
*begin
} else {
begin + (end - begin) * i as f64 / (n - 1) as f64
};
at(t)
})
.collect();
if *direction < 0 {
out.reverse();
}
out
}
}
}
}
pub fn hcl_color(h: f64, c: f64, l: f64) -> RGBAColor {
const WHITE_Y: f64 = 100.0;
const WHITE_U: f64 = 0.1978398;
const WHITE_V: f64 = 0.4683363;
let hr = h.to_radians();
let (u_, v_) = (c * hr.cos(), c * hr.sin());
let (x, y, z) = if l <= 0.0 && u_ == 0.0 && v_ == 0.0 {
(0.0, 0.0, 0.0)
} else {
let y = WHITE_Y
* if l > 7.999592 {
((l + 16.0) / 116.0).powi(3)
} else {
l / 903.3
};
let u = u_ / (13.0 * l) + WHITE_U;
let v = v_ / (13.0 * l) + WHITE_V;
let x = 9.0 * y * u / (4.0 * v);
let z = -x / 3.0 - 5.0 * y + 3.0 * y / v;
(x, y, z)
};
let gamma = |u: f64| {
let u = u / WHITE_Y;
if u > 0.00304 {
1.055 * u.powf(1.0 / 2.4) - 0.055
} else {
12.92 * u
}
};
let byte = |v: f64| (255.0 * v.clamp(0.0, 1.0) + 0.5) as u8;
RGBAColor::new(
byte(gamma(3.240479 * x - 1.537150 * y - 0.498535 * z)),
byte(gamma(-0.969256 * x + 1.875992 * y + 0.041556 * z)),
byte(gamma(0.055648 * x - 0.204043 * y + 1.057311 * z)),
)
}
pub fn hue_palette(n: usize, p: &HuePalette) -> Vec<RGBAColor> {
let n = n.max(1);
let mut out: Vec<RGBAColor> = (0..n).map(|i| hue_color(i, n, p)).collect();
if p.direction == -1 {
out.reverse();
}
out
}
fn hue_color(i: usize, n: usize, p: &HuePalette) -> RGBAColor {
let (h0, mut h1) = p.h;
if (h1 - h0).rem_euclid(360.0) < 1.0 {
h1 -= 360.0 / n as f64;
}
let h = if n == 1 {
h0
} else {
h0 + (h1 - h0) * i as f64 / (n - 1) as f64
};
hcl_color((h + p.h_start).rem_euclid(360.0), p.c, p.l)
}
impl ScaleColorDiscrete {
pub fn new(aesthetic: Aesthetic) -> Self {
ScaleColorDiscrete {
aesthetic,
name: String::new(),
levels: Vec::new(),
level_index: HashMap::new(),
palette: DEFAULT_PALETTE.to_vec(),
sorted: false,
generated: Some(GeneratedPalette::Hue(HuePalette::default())),
}
}
pub fn with_generated(mut self, p: GeneratedPalette) -> Self {
self.generated = Some(p);
self
}
pub fn sorted(mut self) -> Self {
self.sorted = true;
self.resort();
self
}
fn resort(&mut self) {
if !self.sorted {
return;
}
self.levels.sort();
self.level_index = self
.levels
.iter()
.enumerate()
.map(|(i, l)| (l.clone(), i))
.collect();
}
pub fn with_palette(mut self, colors: Vec<RGBAColor>) -> Self {
self.palette = colors;
self.generated = None;
self
}
pub fn with_named_palette(mut self, name: &super::palettes::PaletteName) -> Self {
self.palette = super::palettes::palette(name).to_vec();
self.generated = None;
self
}
pub fn with_levels(mut self, levels: Vec<String>) -> Self {
self.levels.clear();
self.level_index.clear();
for l in levels {
self.push_level(l);
}
self.resort();
self
}
fn push_level(&mut self, key: String) {
if !self.level_index.contains_key(&key) {
self.level_index.insert(key.clone(), self.levels.len());
self.levels.push(key);
}
}
fn level_position(&self, key: &str) -> Option<usize> {
self.level_index.get(key).copied()
}
pub fn color_for_index(&self, idx: usize) -> RGBAColor {
if let Some(g) = &self.generated {
return g.color_at(idx, self.levels.len().max(idx + 1));
}
if self.palette.is_empty() {
return DEFAULT_PALETTE[idx % DEFAULT_PALETTE.len()];
}
self.palette[idx % self.palette.len()]
}
pub fn color_for_value(&self, value: &Value) -> RGBAColor {
let key = value.key_str();
let idx = self.level_position(&key).unwrap_or(0);
self.color_for_index(idx)
}
pub fn levels(&self) -> &[String] {
&self.levels
}
}
impl Scale for ScaleColorDiscrete {
fn aesthetic(&self) -> Aesthetic {
self.aesthetic.clone()
}
fn train(&mut self, values: &[Value]) {
let before = self.levels.len();
for v in values {
let key = v.key_str();
if !self.level_index.contains_key(key.as_ref()) {
self.push_level(key.into_owned());
}
}
if self.levels.len() != before {
self.resort();
}
}
fn map(&self, value: &Value) -> f64 {
let key = value.key_str();
self.level_position(&key).map(|i| i as f64).unwrap_or(0.0)
}
fn breaks(&self) -> Vec<(f64, String)> {
self.levels
.iter()
.enumerate()
.map(|(i, label)| (i as f64, label.clone()))
.collect()
}
fn name(&self) -> &str {
&self.name
}
fn set_name(&mut self, name: &str) {
self.name = name.to_string();
}
fn is_discrete(&self) -> bool {
true
}
fn map_to_color(&self, value: &Value) -> Option<(u8, u8, u8)> {
let c = self.color_for_value(value);
Some((c.r, c.g, c.b))
}
fn clone_box(&self) -> Box<dyn Scale> {
Box::new(self.clone())
}
fn reset_training(&mut self) {
self.levels.clear();
self.level_index.clear();
}
}
#[derive(Clone)]
pub struct ScaleColorContinuous {
aesthetic: Aesthetic,
name: String,
low: RGBAColor,
high: RGBAColor,
min: f64,
max: f64,
locale: Option<crate::locale::Locale>,
limits: Option<(f64, f64)>,
oob: super::Oob,
na_value: (u8, u8, u8),
value_formatter: Option<super::format::UnwindSafeFormatter>,
}
impl std::fmt::Debug for ScaleColorContinuous {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ScaleColorContinuous")
.field("aesthetic", &self.aesthetic)
.field("name", &self.name)
.field("low", &self.low)
.field("high", &self.high)
.field("min", &self.min)
.field("max", &self.max)
.field("locale", &self.locale)
.field("limits", &self.limits)
.field("oob", &self.oob)
.field("na_value", &self.na_value)
.field("value_formatter", &self.value_formatter.is_some())
.finish()
}
}
impl ScaleColorContinuous {
pub fn new(aesthetic: Aesthetic) -> Self {
ScaleColorContinuous {
aesthetic,
name: String::new(),
low: RGBAColor::new(0, 0, 255),
high: RGBAColor::new(255, 0, 0),
min: f64::INFINITY,
max: f64::NEG_INFINITY,
locale: None,
limits: None,
oob: super::Oob::Censor,
na_value: super::DEFAULT_NA_VALUE,
value_formatter: None,
}
}
pub fn with_value_formatter<F>(mut self, f: F) -> Self
where
F: Fn(f64) -> String + Send + Sync + 'static,
{
self.value_formatter = Some(super::format::UnwindSafeFormatter(std::sync::Arc::new(f)));
self
}
pub fn with_colors(mut self, low: RGBAColor, high: RGBAColor) -> Self {
self.low = low;
self.high = high;
self
}
pub fn with_limits(mut self, lo: f64, hi: f64) -> Self {
self.limits = Some((lo, hi));
self
}
pub fn with_oob(mut self, oob: super::Oob) -> Self {
self.oob = oob;
self
}
pub fn with_na_value(mut self, color: (u8, u8, u8)) -> Self {
self.na_value = color;
self
}
pub fn color_at(&self, t: f64) -> RGBAColor {
self.low.lerp(&self.high, t)
}
}
impl Scale for ScaleColorContinuous {
fn set_default_locale(&mut self, locale: &crate::locale::Locale) {
if self.locale.is_none() {
self.locale = Some(locale.clone());
}
}
fn locale(&self) -> Option<&crate::locale::Locale> {
self.locale.as_ref()
}
fn format_value(&self, value: &Value) -> Option<String> {
let f = value.as_f64()?;
if !f.is_finite() {
return None;
}
Some(match &self.value_formatter {
Some(fmt) => fmt.call(f),
None => super::util::format_number_opt(f, self.locale.as_ref()),
})
}
fn aesthetic(&self) -> Aesthetic {
self.aesthetic.clone()
}
fn train(&mut self, values: &[Value]) {
if self.limits.is_some() {
return;
}
for v in values {
if let Some(f) = v.as_f64() {
if f.is_finite() {
if f < self.min {
self.min = f;
}
if f > self.max {
self.max = f;
}
}
}
}
}
fn map(&self, value: &Value) -> f64 {
let f = match value.as_f64() {
Some(f) => f,
None => return 0.0,
};
let (lo, hi) = self.limits.unwrap_or((self.min, self.max));
let range = hi - lo;
if range.abs() < f64::EPSILON {
0.5
} else {
(f - lo) / range
}
}
fn breaks(&self) -> Vec<(f64, String)> {
let (lo, hi) = self.limits.unwrap_or((self.min, self.max));
if lo > hi || !lo.is_finite() || !hi.is_finite() {
return vec![];
}
let range = hi - lo;
if super::util::is_degenerate_range(lo, hi) {
return vec![(
0.5,
super::util::format_number_opt(lo, self.locale.as_ref()),
)];
}
let n_breaks = 5;
let raw_step = range / n_breaks as f64;
let step = nice_step(raw_step);
let start = (lo / step).ceil() * step;
let values = super::util::stepped_breaks(start, hi, step);
let labels = super::util::format_numbers_opt(&values, self.locale.as_ref());
values
.iter()
.zip(labels)
.map(|(&v, l)| (self.map(&Value::Float(v)), l))
.collect()
}
fn name(&self) -> &str {
&self.name
}
fn set_name(&mut self, name: &str) {
self.name = name.to_string();
}
fn map_to_color(&self, value: &Value) -> Option<(u8, u8, u8)> {
if value.is_na() {
return Some(self.na_value);
}
if let Some(f) = value.as_f64() {
if !f.is_finite() {
return Some(self.na_value);
}
if super::resolve_oob(f, self.limits, self.oob).is_none() {
return Some(self.na_value);
}
}
let t = self.map(value);
let c = self.color_at(t);
Some((c.r, c.g, c.b))
}
fn domain(&self) -> Option<(f64, f64)> {
if let Some((lo, hi)) = self.limits {
return Some((lo, hi));
}
if self.min.is_finite() && self.max.is_finite() && self.min <= self.max {
Some((self.min, self.max))
} else {
None
}
}
fn clone_box(&self) -> Box<dyn Scale> {
Box::new(self.clone())
}
fn reset_training(&mut self) {
self.min = f64::INFINITY;
self.max = f64::NEG_INFINITY;
}
fn validate(&self, warnings: &mut Vec<String>) {
if let Some((lo, hi)) = self.limits {
if lo > hi {
warnings.push(format!(
"scale_color/fill_gradient: with_limits({lo}, {hi}) has lo > hi; every value maps to na_value"
));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::aes::Aesthetic;
#[test]
fn continuous_format_value_rejects_non_finite() {
let s = ScaleColorContinuous::new(Aesthetic::Fill);
assert_eq!(s.format_value(&Value::Float(3.5)), Some("3.5".to_string()));
assert_eq!(s.format_value(&Value::Float(f64::NAN)), None);
assert_eq!(s.format_value(&Value::Na), None);
}
#[test]
fn continuous_with_value_formatter_overrides_format_value() {
let s =
ScaleColorContinuous::new(Aesthetic::Fill).with_value_formatter(|v| format!("${v:.2}"));
assert_eq!(
s.format_value(&Value::Float(1234.5)),
Some("$1234.50".to_string())
);
assert_eq!(s.format_value(&Value::Float(f64::NAN)), None);
}
#[test]
fn continuous_debug_reports_whether_a_formatter_is_set() {
let without = format!("{:?}", ScaleColorContinuous::new(Aesthetic::Fill));
assert!(without.contains("ScaleColorContinuous"));
assert!(without.contains("value_formatter: false"));
let with = format!(
"{:?}",
ScaleColorContinuous::new(Aesthetic::Fill).with_value_formatter(|v| v.to_string())
);
assert!(with.contains("value_formatter: true"));
}
#[test]
fn scale_color_continuous_keeps_every_auto_trait() {
fn assert_traits<
T: Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe + Clone,
>() {
}
assert_traits::<ScaleColorContinuous>();
}
}
#[cfg(test)]
mod hue_tests {
use super::*;
fn hex(c: RGBAColor) -> String {
format!("#{:02X}{:02X}{:02X}", c.r, c.g, c.b)
}
#[test]
fn hue_pal_matches_scales() {
let d = HuePalette::default();
let got: Vec<String> = hue_palette(2, &d).into_iter().map(hex).collect();
assert_eq!(got, ["#F8766D", "#00BFC4"]);
let got: Vec<String> = hue_palette(5, &d).into_iter().map(hex).collect();
assert_eq!(got, ["#F8766D", "#A3A500", "#00BF7D", "#00B0F6", "#E76BF3"]);
let p = HuePalette {
h: (0.0, 180.0),
c: 50.0,
l: 80.0,
h_start: 30.0,
direction: -1,
};
let got: Vec<String> = hue_palette(4, &p).into_iter().map(hex).collect();
assert_eq!(got, ["#70D5E7", "#7BD9AD", "#C6CC82", "#F7B8A1"]);
}
#[test]
fn default_discrete_scale_is_hue_pal() {
let mut s = ScaleColorDiscrete::new(Aesthetic::Color);
s = s.with_levels(["a", "b", "c", "d", "e"].map(String::from).to_vec());
let got: Vec<String> = (0..5).map(|i| hex(s.color_for_index(i))).collect();
assert_eq!(got, ["#F8766D", "#A3A500", "#00BF7D", "#00B0F6", "#E76BF3"]);
let g = GeneratedPalette::Hue(HuePalette {
direction: -1,
..HuePalette::default()
});
let all = g.colors(4);
assert!((0..4).all(|i| hex(g.color_at(i, 4)) == hex(all[i])));
}
#[test]
fn viridis_d_is_close_to_scales() {
let want = [
(0x44, 0x01, 0x54),
(0x31, 0x68, 0x8E),
(0x35, 0xB7, 0x79),
(0xFD, 0xE7, 0x25),
];
let got = GeneratedPalette::Viridis {
option: 'D',
begin: 0.0,
end: 1.0,
direction: 1,
}
.colors(4);
for (g, w) in got.iter().zip(want) {
for (a, b) in [(g.r, w.0), (g.g, w.1), (g.b, w.2)] {
assert!((a as i32 - b).abs() <= 1, "{} vs {w:?}", hex(*g));
}
}
let want = [(0xFE, 0x9F, 0x6D), (0xB6, 0x36, 0x79), (0x3B, 0x0F, 0x70)];
let got = GeneratedPalette::Viridis {
option: 'A',
begin: 0.2,
end: 0.8,
direction: -1,
}
.colors(3);
for (g, w) in got.iter().zip(want) {
for (a, b) in [(g.r, w.0), (g.g, w.1), (g.b, w.2)] {
assert!((a as i32 - b).abs() <= 1, "{} vs {w:?}", hex(*g));
}
}
}
}