use uzor::render::{RenderContext, TextAlign, TextBaseline};
use uzor::types::Rect;
use crate::scale::color::ColorScale;
use crate::scale::linear::format_value;
use crate::scale::ClassScale;
use crate::theme::FigureTheme;
const BAR_WIDTH: f64 = 14.0;
const PAD: f64 = 6.0;
const TICK_GAP: f64 = 6.0;
const GRADIENT_STOPS: usize = 12;
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct ColorbarSize {
pub width: f64,
}
pub fn measure_colorbar(ctx: &mut dyn RenderContext, theme: &FigureTheme, scale: &ColorScale) -> ColorbarSize {
ctx.set_font(&theme.label_font);
let (min, max) = scale.domain();
let label_w = ctx.measure_text(&format_value(min, 1.0)).max(ctx.measure_text(&format_value(max, 1.0)));
ColorbarSize { width: PAD + BAR_WIDTH + TICK_GAP + label_w }
}
pub fn draw_colorbar(ctx: &mut dyn RenderContext, rect: Rect, theme: &FigureTheme, scale: &ColorScale) {
if rect.width <= 0.0 || rect.height <= 0.0 {
return;
}
let (min, max) = scale.domain();
let bar_x = rect.x;
let bar_y = rect.y;
let bar_h = rect.height;
let raw_stops = scale.stops(GRADIENT_STOPS);
let n = raw_stops.len().max(1);
let gradient_stops: Vec<(f32, String)> = raw_stops
.iter()
.rev()
.enumerate()
.map(|(i, (_, hex))| (i as f32 / (n - 1).max(1) as f32, hex.clone()))
.collect();
let stop_refs: Vec<(f32, &str)> = gradient_stops.iter().map(|(o, c)| (*o, c.as_str())).collect();
ctx.begin_path();
ctx.rect(bar_x, bar_y, BAR_WIDTH, bar_h);
ctx.fill_linear_gradient(&stop_refs, bar_x, bar_y, bar_x, bar_y + bar_h);
ctx.set_stroke_color(&theme.axis_color);
ctx.set_stroke_width(1.0);
ctx.stroke_rect(bar_x, bar_y, BAR_WIDTH, bar_h);
ctx.set_font(&theme.label_font);
ctx.set_fill_color(&theme.label_color);
ctx.set_text_align(TextAlign::Left);
ctx.set_text_baseline(TextBaseline::Top);
ctx.fill_text(&format_value(max, 1.0), bar_x + BAR_WIDTH + TICK_GAP, bar_y);
ctx.set_text_baseline(TextBaseline::Bottom);
ctx.fill_text(&format_value(min, 1.0), bar_x + BAR_WIDTH + TICK_GAP, bar_y + bar_h);
}
pub fn measure_discrete_colorbar(ctx: &mut dyn RenderContext, theme: &FigureTheme, scale: &dyn ClassScale) -> ColorbarSize {
ctx.set_font(&theme.label_font);
let n = scale.class_count();
let widest = (0..n).map(|i| ctx.measure_text(&scale.class_label(i))).fold(0.0_f64, f64::max);
ColorbarSize { width: PAD + BAR_WIDTH + TICK_GAP + widest }
}
pub fn draw_discrete_colorbar(ctx: &mut dyn RenderContext, rect: Rect, theme: &FigureTheme, scale: &dyn ClassScale, colors: &[String]) {
if rect.width <= 0.0 || rect.height <= 0.0 {
return;
}
let n = scale.class_count().max(1);
let bar_x = rect.x;
let bar_y = rect.y;
let swatch_h = rect.height / n as f64;
let color_source: &[String] = if colors.is_empty() { &theme.palette } else { colors };
let len = color_source.len().max(1);
ctx.set_font(&theme.label_font);
ctx.set_text_align(TextAlign::Left);
ctx.set_text_baseline(TextBaseline::Middle);
for i in 0..n {
let row_from_top = n - 1 - i;
let y0 = bar_y + row_from_top as f64 * swatch_h;
let color = color_source.get(i % len).map(String::as_str).unwrap_or("#808080");
ctx.set_fill_color(color);
ctx.fill_rect(bar_x, y0, BAR_WIDTH, swatch_h);
ctx.set_stroke_color(&theme.axis_color);
ctx.set_stroke_width(1.0);
ctx.stroke_rect(bar_x, y0, BAR_WIDTH, swatch_h);
ctx.set_fill_color(&theme.label_color);
ctx.fill_text(&scale.class_label(i), bar_x + BAR_WIDTH + TICK_GAP, y0 + swatch_h / 2.0);
}
}
#[cfg(test)]
mod tests {
use super::*;
use uzor_export::{render_to_png, ExportSpec};
#[test]
fn measure_colorbar_reserves_a_positive_width() {
let theme = FigureTheme::dark();
let scale = ColorScale::sequential(0.0, 100.0);
let spec = ExportSpec { width_px: 10, height_px: 10, dpr: 1.0, background: None };
let mut size = ColorbarSize::default();
render_to_png(&spec, |ctx| {
size = measure_colorbar(ctx, &theme, &scale);
})
.expect("render");
assert!(size.width > BAR_WIDTH);
}
#[test]
fn draw_colorbar_renders_without_panicking_including_a_degenerate_rect() {
let theme = FigureTheme::dark();
let scale = ColorScale::sequential(0.0, 100.0);
let spec = ExportSpec { width_px: 200, height_px: 200, dpr: 1.0, background: None };
let result = render_to_png(&spec, |ctx| {
draw_colorbar(ctx, Rect::new(10.0, 10.0, 60.0, 150.0), &theme, &scale);
draw_colorbar(ctx, Rect::new(0.0, 0.0, 0.0, 0.0), &theme, &scale);
});
assert!(result.is_ok());
}
#[test]
fn measure_discrete_colorbar_reserves_a_positive_width() {
use crate::scale::QuantizeScale;
let theme = FigureTheme::dark();
let scale = QuantizeScale::new(0.0, 100.0, 5);
let spec = ExportSpec { width_px: 10, height_px: 10, dpr: 1.0, background: None };
let mut size = ColorbarSize::default();
render_to_png(&spec, |ctx| {
size = measure_discrete_colorbar(ctx, &theme, &scale);
})
.expect("render");
assert!(size.width > BAR_WIDTH);
}
#[test]
fn draw_discrete_colorbar_renders_without_panicking_including_a_degenerate_rect() {
use crate::scale::QuantizeScale;
let theme = FigureTheme::dark();
let scale = QuantizeScale::new(-50.0, 150.0, 5);
let colors: Vec<String> = vec!["#111111".to_owned(), "#222222".to_owned(), "#333333".to_owned(), "#444444".to_owned(), "#555555".to_owned()];
let spec = ExportSpec { width_px: 200, height_px: 200, dpr: 1.0, background: None };
let result = render_to_png(&spec, |ctx| {
draw_discrete_colorbar(ctx, Rect::new(10.0, 10.0, 60.0, 150.0), &theme, &scale, &colors);
draw_discrete_colorbar(ctx, Rect::new(0.0, 0.0, 0.0, 0.0), &theme, &scale, &colors);
});
assert!(result.is_ok());
}
#[test]
fn draw_discrete_colorbar_is_visibly_banded_not_a_smooth_ramp() {
use crate::scale::QuantizeScale;
let theme = FigureTheme::dark();
let scale = QuantizeScale::new(0.0, 100.0, 3);
let colors: Vec<String> = vec!["#000000".to_owned(), "#808080".to_owned(), "#ffffff".to_owned()];
let spec = ExportSpec { width_px: 100, height_px: 90, dpr: 1.0, background: None };
let discrete = render_to_png(&spec, |ctx| {
ctx.set_fill_color(&theme.background);
ctx.fill_rect(0.0, 0.0, 100.0, 90.0);
draw_discrete_colorbar(ctx, Rect::new(10.0, 10.0, 60.0, 70.0), &theme, &scale, &colors);
})
.expect("discrete render");
let continuous = render_to_png(&spec, |ctx| {
ctx.set_fill_color(&theme.background);
ctx.fill_rect(0.0, 0.0, 100.0, 90.0);
let ramp = ColorScale::sequential_colors(0.0, 100.0, &["#000000", "#808080", "#ffffff"]);
draw_colorbar(ctx, Rect::new(10.0, 10.0, 60.0, 70.0), &theme, &ramp);
})
.expect("continuous render");
assert_ne!(discrete, continuous, "a discrete banded colorbar must render visibly differently from a smooth gradient ramp over comparable colors");
}
#[test]
fn discrete_colorbar_falls_back_to_theme_palette_for_empty_colors() {
use crate::scale::QuantizeScale;
let theme = FigureTheme::dark();
let scale = QuantizeScale::new(0.0, 100.0, 3);
let spec = ExportSpec { width_px: 100, height_px: 90, dpr: 1.0, background: None };
let result = render_to_png(&spec, |ctx| {
draw_discrete_colorbar(ctx, Rect::new(10.0, 10.0, 60.0, 70.0), &theme, &scale, &[]);
});
assert!(result.is_ok());
}
}