use crate::color::{Color, Colormap};
use crate::core::{Canvas, Drawable};
use crate::error::Result;
use crate::plots::bar::BarOrientation;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BarLegendPosition {
Right,
Bottom,
Custom(i32, i32),
}
pub struct BarLegend {
min_value: f64,
max_value: f64,
colormap: Colormap,
label: Option<String>,
position: BarLegendPosition,
orientation: BarOrientation,
width: f32,
height: f32,
num_ticks: usize,
show_ticks: bool,
show_values: bool,
text_color: Color,
border_color: Color,
show_border: bool,
}
impl BarLegend {
#[must_use]
pub fn new(min_value: f64, max_value: f64, colormap: Colormap) -> Self {
Self {
min_value,
max_value,
colormap,
label: None,
position: BarLegendPosition::Right,
orientation: BarOrientation::Vertical,
width: 30.0,
height: 200.0,
num_ticks: 5,
show_ticks: true,
show_values: true,
text_color: Color::BLACK,
border_color: Color::BLACK,
show_border: true,
}
}
#[must_use]
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn position(mut self, position: BarLegendPosition) -> Self {
self.position = position;
self
}
#[must_use]
pub fn orientation(mut self, orientation: BarOrientation) -> Self {
self.orientation = orientation;
self
}
#[must_use]
pub fn dimensions(mut self, width: f32, height: f32) -> Self {
self.width = width.max(10.0);
self.height = height.max(50.0);
self
}
#[must_use]
pub fn num_ticks(mut self, num: usize) -> Self {
self.num_ticks = num.max(2);
self
}
#[must_use]
pub fn show_ticks(mut self, show: bool) -> Self {
self.show_ticks = show;
self
}
#[must_use]
pub fn show_values(mut self, show: bool) -> Self {
self.show_values = show;
self
}
#[must_use]
pub fn show_border(mut self, show: bool) -> Self {
self.show_border = show;
self
}
#[allow(clippy::cast_precision_loss)]
fn calculate_position(&self, canvas: &dyn Canvas) -> (f32, f32) {
let (canvas_width, canvas_height) = canvas.dimensions();
let margin = 20.0;
match self.position {
BarLegendPosition::Right => {
let x = canvas_width as f32 - self.width - margin - 80.0; let y = (canvas_height as f32 - self.height) / 2.0;
(x, y)
}
BarLegendPosition::Bottom => {
let x = (canvas_width as f32 - self.height) / 2.0; let y = canvas_height as f32 - self.width - margin - 60.0;
(x, y)
}
BarLegendPosition::Custom(x, y) => (x as f32, y as f32),
}
}
fn draw_gradient(&self, canvas: &mut dyn Canvas, x: f32, y: f32) -> Result<()> {
match self.orientation {
BarOrientation::Vertical => {
let steps = self.height as i32;
for i in 0..steps {
let t = 1.0 - (i as f32 / steps as f32); let color = self.colormap.get(t);
let y_pos = y + i as f32;
canvas.draw_line_pixels(
x,
y_pos,
x + self.width,
y_pos,
&color.to_rgba(),
1.0,
)?;
}
}
BarOrientation::Horizontal => {
let steps = self.height as i32; for i in 0..steps {
let t = i as f32 / steps as f32;
let color = self.colormap.get(t);
let x_pos = x + i as f32;
canvas.draw_line_pixels(
x_pos,
y,
x_pos,
y + self.width, &color.to_rgba(),
1.0,
)?;
}
}
}
Ok(())
}
fn draw_border(&self, canvas: &mut dyn Canvas, x: f32, y: f32) -> Result<()> {
if !self.show_border {
return Ok(());
}
let (bar_width, bar_height) = match self.orientation {
BarOrientation::Vertical => (self.width, self.height),
BarOrientation::Horizontal => (self.height, self.width),
};
canvas.draw_line_pixels(x, y, x + bar_width, y, &self.border_color.to_rgba(), 1.0)?;
canvas.draw_line_pixels(
x + bar_width,
y,
x + bar_width,
y + bar_height,
&self.border_color.to_rgba(),
1.0,
)?;
canvas.draw_line_pixels(
x,
y + bar_height,
x + bar_width,
y + bar_height,
&self.border_color.to_rgba(),
1.0,
)?;
canvas.draw_line_pixels(x, y, x, y + bar_height, &self.border_color.to_rgba(), 1.0)?;
Ok(())
}
fn draw_ticks(&self, canvas: &mut dyn Canvas, x: f32, y: f32) -> Result<()> {
if !self.show_ticks && !self.show_values {
return Ok(());
}
for i in 0..self.num_ticks {
let t = i as f64 / (self.num_ticks - 1) as f64;
let value = self.min_value + t * (self.max_value - self.min_value);
match self.orientation {
BarOrientation::Vertical => {
let tick_y = y + self.height - (t as f32 * self.height);
if self.show_ticks {
canvas.draw_line_pixels(
x + self.width,
tick_y,
x + self.width + 5.0,
tick_y,
&self.text_color.to_rgba(),
1.0,
)?;
}
if self.show_values {
let value_str = format!("{value:.1}");
canvas.draw_text_pixels(
&value_str,
x + self.width + 8.0,
tick_y - 5.0, 10.0,
&self.text_color.to_rgba(),
)?;
}
}
BarOrientation::Horizontal => {
let tick_x = x + (t as f32 * self.height);
if self.show_ticks {
canvas.draw_line_pixels(
tick_x,
y + self.width,
tick_x,
y + self.width + 5.0,
&self.text_color.to_rgba(),
1.0,
)?;
}
if self.show_values {
let value_str = format!("{value:.1}");
canvas.draw_text_pixels(
&value_str,
tick_x - 10.0, y + self.width + 15.0,
10.0,
&self.text_color.to_rgba(),
)?;
}
}
}
}
Ok(())
}
fn draw_label(&self, canvas: &mut dyn Canvas, x: f32, y: f32) -> Result<()> {
if let Some(label) = &self.label {
match self.orientation {
BarOrientation::Vertical => {
canvas.draw_text_pixels(
label,
x,
y - 15.0,
12.0,
&self.text_color.to_rgba(),
)?;
}
BarOrientation::Horizontal => {
canvas.draw_text_pixels(
label,
x - 80.0,
y + self.width / 2.0,
12.0,
&self.text_color.to_rgba(),
)?;
}
}
}
Ok(())
}
}
impl Drawable for BarLegend {
fn draw(&self, canvas: &mut dyn Canvas) -> Result<()> {
let (x, y) = self.calculate_position(canvas);
self.draw_gradient(canvas, x, y)?;
self.draw_border(canvas, x, y)?;
self.draw_ticks(canvas, x, y)?;
self.draw_label(canvas, x, y)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bar_legend_creation() {
let bar = BarLegend::new(0.0, 100.0, Colormap::viridis());
assert_eq!(bar.min_value, 0.0);
assert_eq!(bar.max_value, 100.0);
assert_eq!(bar.num_ticks, 5);
}
#[test]
fn test_bar_legend_builder() {
let bar = BarLegend::new(0.0, 100.0, Colormap::plasma())
.label("Test")
.dimensions(40.0, 250.0)
.num_ticks(7)
.show_border(false);
assert!(bar.label.is_some());
assert_eq!(bar.width, 40.0);
assert_eq!(bar.height, 250.0);
assert_eq!(bar.num_ticks, 7);
assert!(!bar.show_border);
}
#[test]
fn test_bar_legend_min_dimensions() {
let bar = BarLegend::new(0.0, 100.0, Colormap::viridis()).dimensions(5.0, 30.0);
assert_eq!(bar.width, 10.0); assert_eq!(bar.height, 50.0); }
}