#![forbid(unsafe_code)]
use std::collections::HashMap;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Rgba {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl Rgba {
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
pub fn rgb(r: u8, g: u8, b: u8) -> Self {
Self::new(r, g, b, 255)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MarkerShape {
Circle,
Square,
Triangle,
Diamond,
Cross,
X,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LineDash {
Solid,
Dash,
Dot,
DashDot,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SeriesKey {
pub country: String,
pub indicator: String,
}
impl SeriesKey {
pub fn new(country: String, indicator: String) -> Self {
Self { country, indicator }
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SeriesStyle {
pub base_hue: f64, pub shade: Rgba,
pub marker: MarkerShape,
pub dash: LineDash,
}
impl SeriesStyle {
pub fn new(base_hue: f64, shade: Rgba, marker: MarkerShape, dash: LineDash) -> Self {
Self {
base_hue,
shade,
marker,
dash,
}
}
}
const MS_OFFICE_PALETTE: [(u8, u8, u8); 10] = [
(68, 114, 196), (237, 125, 49), (165, 165, 165), (255, 192, 0), (91, 155, 213), (112, 173, 71), (38, 68, 120), (158, 72, 14), (99, 99, 99), (153, 115, 0), ];
pub fn assign_country_styles_with_palette(
series: &[SeriesKey],
palette: &[(u8, u8, u8)],
alpha: u8,
) -> HashMap<SeriesKey, SeriesStyle> {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut result = HashMap::new();
fn stable_hash<T: Hash>(value: &T) -> u64 {
let mut hasher = DefaultHasher::new();
value.hash(&mut hasher);
hasher.finish()
}
fn hash_to_marker(hash: u64) -> MarkerShape {
match hash % 6 {
0 => MarkerShape::Circle,
1 => MarkerShape::Square,
2 => MarkerShape::Triangle,
3 => MarkerShape::Diamond,
4 => MarkerShape::Cross,
_ => MarkerShape::X,
}
}
fn hash_to_dash(hash: u64) -> LineDash {
match hash % 4 {
0 => LineDash::Solid,
1 => LineDash::Dash,
2 => LineDash::Dot,
_ => LineDash::DashDot,
}
}
fn adjust_brightness(color: (u8, u8, u8), factor: f64) -> (u8, u8, u8) {
let (r, g, b) = color;
let new_r = (r as f64 * factor).clamp(0.0, 255.0) as u8;
let new_g = (g as f64 * factor).clamp(0.0, 255.0) as u8;
let new_b = (b as f64 * factor).clamp(0.0, 255.0) as u8;
(new_r, new_g, new_b)
}
let unique_countries: Vec<String> = series
.iter()
.map(|s| s.country.clone())
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
let country_to_index: HashMap<String, usize> = unique_countries
.iter()
.map(|country| {
let hash = stable_hash(country);
let index = (hash as usize) % palette.len();
(country.clone(), index)
})
.collect();
for series_key in series {
let country_index = country_to_index[&series_key.country];
let base_color = palette[country_index];
let base_hue = rgb_to_hue(base_color);
let indicator_hash = stable_hash(&series_key.indicator);
let brightness_factor = 0.7 + 0.6 * ((indicator_hash % 100) as f64 / 100.0);
let adjusted_color = adjust_brightness(base_color, brightness_factor);
let shade = Rgba::new(adjusted_color.0, adjusted_color.1, adjusted_color.2, alpha);
let marker = hash_to_marker(indicator_hash);
let dash = hash_to_dash(indicator_hash.rotate_left(16));
let style = SeriesStyle::new(base_hue, shade, marker, dash);
result.insert(series_key.clone(), style);
}
result
}
fn rgb_to_hue(rgb: (u8, u8, u8)) -> f64 {
let (r, g, b) = (
rgb.0 as f64 / 255.0,
rgb.1 as f64 / 255.0,
rgb.2 as f64 / 255.0,
);
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let delta = max - min;
if delta == 0.0 {
return 0.0;
}
let hue = if max == r {
60.0 * (((g - b) / delta) % 6.0)
} else if max == g {
60.0 * ((b - r) / delta + 2.0)
} else {
60.0 * ((r - g) / delta + 4.0)
};
if hue < 0.0 { hue + 360.0 } else { hue }
}
pub fn assign_country_styles(series: &[SeriesKey], alpha: u8) -> HashMap<SeriesKey, SeriesStyle> {
assign_country_styles_with_palette(series, &MS_OFFICE_PALETTE, alpha)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_series_key_creation() {
let key = SeriesKey::new("USA".to_string(), "GDP".to_string());
assert_eq!(key.country, "USA");
assert_eq!(key.indicator, "GDP");
}
#[test]
fn test_rgba_creation() {
let color = Rgba::rgb(255, 128, 64);
assert_eq!(color.r, 255);
assert_eq!(color.g, 128);
assert_eq!(color.b, 64);
assert_eq!(color.a, 255);
}
#[test]
fn test_country_consistent_styles() {
let series = vec![
SeriesKey::new("USA".to_string(), "GDP".to_string()),
SeriesKey::new("USA".to_string(), "Population".to_string()),
SeriesKey::new("DEU".to_string(), "GDP".to_string()),
SeriesKey::new("DEU".to_string(), "Population".to_string()),
];
let styles = assign_country_styles(&series, 255);
let usa_gdp_hue = styles[&series[0]].base_hue;
let usa_pop_hue = styles[&series[1]].base_hue;
assert_eq!(usa_gdp_hue, usa_pop_hue);
let deu_gdp_hue = styles[&series[2]].base_hue;
let deu_pop_hue = styles[&series[3]].base_hue;
assert_eq!(deu_gdp_hue, deu_pop_hue);
assert_ne!(usa_gdp_hue, deu_gdp_hue);
let _usa_gdp_marker = styles[&series[0]].marker;
let _deu_gdp_marker = styles[&series[2]].marker;
let styles2 = assign_country_styles(&series, 255);
assert_eq!(styles, styles2);
}
#[test]
fn test_deterministic_assignment() {
let series = vec![
SeriesKey::new("FRA".to_string(), "inflation".to_string()),
SeriesKey::new("GBR".to_string(), "inflation".to_string()),
];
let styles1 = assign_country_styles(&series, 200);
let styles2 = assign_country_styles(&series, 200);
assert_eq!(styles1, styles2);
assert_eq!(styles1.len(), 2);
assert!(styles1.contains_key(&series[0]));
assert!(styles1.contains_key(&series[1]));
}
}