use crate::axis::ticker::{AutoLocator, Formatter, Locator, ScalarFormatter};
use crate::core::{Affine2D, Bbox, Path, color::Colormap, color::Rgba, color::colormap};
use crate::render::{GraphicsContext, Renderer};
use crate::text::FontSource;
use crate::figure::figure::Figure;
const N_SLICES: usize = 256;
const BAR_LEFT: f64 = 0.90;
const BAR_WIDTH: f64 = 0.03;
const BAR_BOTTOM: f64 = 0.10;
const BAR_HEIGHT: f64 = 0.80;
const TICK_FONT_SIZE: f64 = 9.0;
const TICK_LEN: f64 = 4.0;
const TICK_GAP: f64 = 3.0;
pub(crate) struct Colorbar {
cmap_name: String,
vmin: f64,
vmax: f64,
}
impl Figure {
pub fn colorbar(&mut self, cmap_name: &str, vmin: f64, vmax: f64) -> &mut Self {
self.colorbars.push(Colorbar {
cmap_name: cmap_name.to_string(),
vmin,
vmax,
});
self
}
pub(crate) fn draw_colorbars(&self, renderer: &mut dyn Renderer, w: f64, h: f64) {
for cb in &self.colorbars {
cb.draw(renderer, w, h, self.font_source());
}
}
}
impl Colorbar {
fn draw(&self, renderer: &mut dyn Renderer, fig_w: f64, fig_h: f64, font: &FontSource) {
let Some(cmap) = colormap(&self.cmap_name) else {
return;
};
let bar = Bbox::from_extents(
BAR_LEFT * fig_w,
BAR_BOTTOM * fig_h,
(BAR_LEFT + BAR_WIDTH) * fig_w,
(BAR_BOTTOM + BAR_HEIGHT) * fig_h,
);
self.draw_gradient(renderer, &bar, cmap.as_ref());
self.draw_frame(renderer, &bar);
self.draw_ticks(renderer, &bar, font);
}
fn draw_gradient(&self, renderer: &mut dyn Renderer, bar: &Bbox, cmap: &dyn Colormap) {
let id = Affine2D::identity();
let slice_h = bar.height() / N_SLICES as f64;
for i in 0..N_SLICES {
let t = i as f64 / (N_SLICES as f64 - 1.0);
let y0 = bar.ymin() + i as f64 * slice_h;
let y1 = y0 + slice_h + 0.5;
let rect = rect_path(&Bbox::from_extents(bar.xmin(), y0, bar.xmax(), y1));
renderer.draw_path(&GraphicsContext::new(), &rect, &id, Some(cmap.sample(t)));
}
}
fn draw_frame(&self, renderer: &mut dyn Renderer, bar: &Bbox) {
let frame_gc = GraphicsContext::new()
.with_stroke(Rgba::BLACK)
.with_line_width(0.8);
renderer.draw_path(&frame_gc, &rect_path(bar), &Affine2D::identity(), None);
}
fn draw_ticks(&self, renderer: &mut dyn Renderer, bar: &Bbox, font: &FontSource) {
let locator = AutoLocator::new();
let ticks = locator.tick_values(self.vmin, self.vmax);
let mut formatter = ScalarFormatter::new();
formatter.set_locs(&ticks);
let id = Affine2D::identity();
let tick_gc = GraphicsContext::new()
.with_stroke(Rgba::BLACK)
.with_line_width(0.8);
let span = self.vmax - self.vmin;
if span.abs() < f64::EPSILON {
return;
}
for (i, &value) in ticks.iter().enumerate() {
if value < self.vmin.min(self.vmax) || value > self.vmin.max(self.vmax) {
continue;
}
let frac = (value - self.vmin) / span;
let y = bar.ymin() + frac * bar.height();
let tx0 = bar.xmax();
let tx1 = bar.xmax() + TICK_LEN;
let mark = Path::from_polyline(&[[tx0, y], [tx1, y]]);
renderer.draw_path(&tick_gc, &mark, &id, None);
let label = formatter.format(value, Some(i));
let lx = tx1 + TICK_GAP;
let ly = y - TICK_FONT_SIZE / 3.0;
let text = font.text_to_path(&label, TICK_FONT_SIZE, [lx, ly]);
renderer.draw_path(&GraphicsContext::new(), &text, &id, Some(Rgba::BLACK));
}
}
}
fn rect_path(bbox: &Bbox) -> Path {
let (x0, y0) = (bbox.xmin(), bbox.ymin());
let (x1, y1) = (bbox.xmax(), bbox.ymax());
Path::from_polyline(&[[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]])
}
#[cfg(test)]
mod tests {
use crate::figure::Figure;
use crate::skia::SkiaRenderer;
fn pixel(r: &SkiaRenderer, x: u32, y: u32) -> [u8; 4] {
let p = r.pixmap().pixel(x, y).expect("pixel in bounds");
[p.red(), p.green(), p.blue(), p.alpha()]
}
#[test]
fn colorbar_strip_has_varied_colors() {
let mut fig = Figure::new(4.0, 4.0).with_dpi(100.0);
fig.add_axes(0.1, 0.1, 0.7, 0.8);
fig.colorbar("viridis", 0.0, 1.0);
let r = fig.render();
let (w, h) = fig.size_px();
let (w, h) = (w as u32, h as u32);
let x = (0.915 * w as f64) as u32;
let mut colors = std::collections::HashSet::new();
let mut non_bg = 0;
for y in 0..h {
let p = pixel(&r, x, y);
if p != [255, 255, 255, 255] {
non_bg += 1;
colors.insert([p[0], p[1], p[2]]);
}
}
assert!(non_bg > 0, "expected non-background pixels in the strip");
assert!(
colors.len() > 5,
"expected a varied gradient, got {} distinct colors",
colors.len()
);
}
}