#![allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ColorBarConfig {
pub range_min: f64,
pub range_max: f64,
pub tick_count: u32,
pub unit: String,
pub pretty: bool,
}
#[derive(Debug, Clone)]
pub struct ColorStop {
pub position: f64,
pub r: u8,
pub g: u8,
pub b: u8,
pub label: String,
}
#[derive(Debug, Clone)]
pub struct ColorBarLegend {
pub stops: Vec<ColorStop>,
pub range_min: f64,
pub range_max: f64,
}
pub fn default_color_bar_config() -> ColorBarConfig {
ColorBarConfig {
range_min: 0.0,
range_max: 1.0,
tick_count: 5,
unit: String::new(),
pretty: true,
}
}
pub fn new_color_bar_legend(cfg: &ColorBarConfig) -> ColorBarLegend {
ColorBarLegend {
stops: Vec::new(),
range_min: cfg.range_min,
range_max: cfg.range_max,
}
}
pub fn color_bar_add_stop(legend: &mut ColorBarLegend, stop: ColorStop) {
legend.stops.push(stop);
legend
.stops
.sort_by(|a, b| a.position.partial_cmp(&b.position).unwrap_or(std::cmp::Ordering::Equal));
}
pub fn color_bar_sample(legend: &ColorBarLegend, t: f64) -> (u8, u8, u8) {
let t = t.clamp(0.0, 1.0);
if legend.stops.is_empty() {
return (0, 0, 0);
}
if legend.stops.len() == 1 {
let s = &legend.stops[0];
return (s.r, s.g, s.b);
}
let first = &legend.stops[0];
let last = &legend.stops[legend.stops.len() - 1];
if t <= first.position {
return (first.r, first.g, first.b);
}
if t >= last.position {
return (last.r, last.g, last.b);
}
for i in 0..legend.stops.len() - 1 {
let lo = &legend.stops[i];
let hi = &legend.stops[i + 1];
if t >= lo.position && t <= hi.position {
let span = hi.position - lo.position;
let f = if span > 0.0 { (t - lo.position) / span } else { 0.0 };
let r = (lo.r as f64 + f * (hi.r as f64 - lo.r as f64)).round() as u8;
let g = (lo.g as f64 + f * (hi.g as f64 - lo.g as f64)).round() as u8;
let b = (lo.b as f64 + f * (hi.b as f64 - lo.b as f64)).round() as u8;
return (r, g, b);
}
}
(0, 0, 0)
}
pub fn color_bar_stop_count(legend: &ColorBarLegend) -> usize {
legend.stops.len()
}
pub fn color_bar_label_at(legend: &ColorBarLegend, cfg: &ColorBarConfig, tick_idx: u32) -> String {
let n = cfg.tick_count.max(2) - 1;
let t = tick_idx as f64 / n as f64;
let value = legend.range_min + t * (legend.range_max - legend.range_min);
if cfg.unit.is_empty() {
format!("{:.2}", value)
} else {
format!("{:.2} {}", value, cfg.unit)
}
}
pub fn color_bar_set_range(legend: &mut ColorBarLegend, min: f64, max: f64) {
legend.range_min = min;
legend.range_max = max;
}
pub fn color_bar_to_json(legend: &ColorBarLegend, cfg: &ColorBarConfig) -> String {
let indent = if cfg.pretty { " " } else { "" };
let nl = if cfg.pretty { "\n" } else { "" };
let mut out = format!(
"{{{nl}{indent}\"range_min\":{:.6},{nl}{indent}\"range_max\":{:.6},{nl}{indent}\"unit\":\"{}\",{nl}{indent}\"stops\":[{nl}",
legend.range_min, legend.range_max, cfg.unit
);
for (i, s) in legend.stops.iter().enumerate() {
let comma = if i + 1 < legend.stops.len() { "," } else { "" };
out.push_str(&format!(
"{indent}{indent}{{\"pos\":{:.4},\"r\":{},\"g\":{},\"b\":{},\"label\":\"{}\"}}{}{nl}",
s.position, s.r, s.g, s.b, s.label, comma
));
}
out.push_str(&format!("{indent}]{nl}}}"));
out
}
pub fn color_bar_reset(legend: &mut ColorBarLegend) {
legend.stops.clear();
}
pub fn color_bar_min_max(legend: &ColorBarLegend) -> (f64, f64) {
(legend.range_min, legend.range_max)
}
fn make_stop(position: f64, r: u8, g: u8, b: u8, label: &str) -> ColorStop {
ColorStop {
position,
r,
g,
b,
label: label.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_values() {
let cfg = default_color_bar_config();
assert!((cfg.range_min - 0.0).abs() < 1e-9);
assert!((cfg.range_max - 1.0).abs() < 1e-9);
assert_eq!(cfg.tick_count, 5);
assert!(cfg.unit.is_empty());
}
#[test]
fn new_legend_is_empty() {
let cfg = default_color_bar_config();
let legend = new_color_bar_legend(&cfg);
assert_eq!(color_bar_stop_count(&legend), 0);
}
#[test]
fn add_stop_increments_count() {
let cfg = default_color_bar_config();
let mut legend = new_color_bar_legend(&cfg);
color_bar_add_stop(&mut legend, make_stop(0.0, 0, 0, 255, ""));
assert_eq!(color_bar_stop_count(&legend), 1);
}
#[test]
fn stops_sorted_by_position_after_insert() {
let cfg = default_color_bar_config();
let mut legend = new_color_bar_legend(&cfg);
color_bar_add_stop(&mut legend, make_stop(1.0, 255, 0, 0, ""));
color_bar_add_stop(&mut legend, make_stop(0.0, 0, 0, 255, ""));
assert!((legend.stops[0].position - 0.0).abs() < 1e-9);
assert!((legend.stops[1].position - 1.0).abs() < 1e-9);
}
#[test]
fn sample_interpolates_between_stops() {
let cfg = default_color_bar_config();
let mut legend = new_color_bar_legend(&cfg);
color_bar_add_stop(&mut legend, make_stop(0.0, 0, 0, 0, ""));
color_bar_add_stop(&mut legend, make_stop(1.0, 200, 100, 50, ""));
let (r, g, b) = color_bar_sample(&legend, 0.5);
assert_eq!(r, 100);
assert_eq!(g, 50);
assert_eq!(b, 25);
}
#[test]
fn sample_clamps_out_of_range() {
let cfg = default_color_bar_config();
let mut legend = new_color_bar_legend(&cfg);
color_bar_add_stop(&mut legend, make_stop(0.0, 10, 20, 30, ""));
let (r, g, b) = color_bar_sample(&legend, -0.5);
assert_eq!((r, g, b), (10, 20, 30));
let (r2, g2, b2) = color_bar_sample(&legend, 1.5);
assert_eq!((r2, g2, b2), (10, 20, 30));
}
#[test]
fn label_at_returns_formatted_value() {
let cfg = ColorBarConfig {
range_min: 0.0,
range_max: 100.0,
tick_count: 5,
unit: "°C".to_string(),
pretty: true,
};
let legend = new_color_bar_legend(&cfg);
let label = color_bar_label_at(&legend, &cfg, 0);
assert!(label.contains("0.00"));
let label_last = color_bar_label_at(&legend, &cfg, 4);
assert!(label_last.contains("100.00"));
}
#[test]
fn set_range_updates_min_max() {
let cfg = default_color_bar_config();
let mut legend = new_color_bar_legend(&cfg);
color_bar_set_range(&mut legend, -1.0, 5.0);
let (min, max) = color_bar_min_max(&legend);
assert!((min - (-1.0)).abs() < 1e-9);
assert!((max - 5.0).abs() < 1e-9);
}
#[test]
fn json_contains_stops_key() {
let cfg = default_color_bar_config();
let mut legend = new_color_bar_legend(&cfg);
color_bar_add_stop(&mut legend, make_stop(0.0, 0, 0, 255, "cold"));
let json = color_bar_to_json(&legend, &cfg);
assert!(json.contains("\"stops\""));
assert!(json.contains("cold"));
}
#[test]
fn reset_clears_stops() {
let cfg = default_color_bar_config();
let mut legend = new_color_bar_legend(&cfg);
color_bar_add_stop(&mut legend, make_stop(0.0, 255, 0, 0, ""));
color_bar_reset(&mut legend);
assert_eq!(color_bar_stop_count(&legend), 0);
}
}