use crate::core::position::Position;
use crate::render::{Color, LineStyle, MarkerStyle};
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum LegendPosition {
#[default]
Best,
UpperRight,
UpperLeft,
LowerLeft,
LowerRight,
Right,
CenterLeft,
CenterRight,
LowerCenter,
UpperCenter,
Center,
OutsideRight,
OutsideLeft,
OutsideUpper,
OutsideLower,
Custom {
x: f32,
y: f32,
anchor: LegendAnchor,
},
}
impl LegendPosition {
pub fn is_outside(&self) -> bool {
matches!(
self,
LegendPosition::OutsideRight
| LegendPosition::OutsideLeft
| LegendPosition::OutsideUpper
| LegendPosition::OutsideLower
) || matches!(self, LegendPosition::Custom { x, y, .. } if *x > 1.0 || *y > 1.0 || *x < 0.0 || *y < 0.0)
}
pub fn from_code(code: u8) -> Self {
match code {
0 => LegendPosition::Best,
1 => LegendPosition::UpperRight,
2 => LegendPosition::UpperLeft,
3 => LegendPosition::LowerLeft,
4 => LegendPosition::LowerRight,
5 => LegendPosition::Right,
6 => LegendPosition::CenterLeft,
7 => LegendPosition::CenterRight,
8 => LegendPosition::LowerCenter,
9 => LegendPosition::UpperCenter,
10 => LegendPosition::Center,
_ => LegendPosition::UpperRight,
}
}
pub fn from_position(pos: Position) -> Self {
match pos {
Position::Best => LegendPosition::Best,
Position::TopLeft => LegendPosition::UpperLeft,
Position::TopCenter => LegendPosition::UpperCenter,
Position::TopRight => LegendPosition::UpperRight,
Position::CenterLeft => LegendPosition::CenterLeft,
Position::Center => LegendPosition::Center,
Position::CenterRight => LegendPosition::CenterRight,
Position::BottomLeft => LegendPosition::LowerLeft,
Position::BottomCenter => LegendPosition::LowerCenter,
Position::BottomRight => LegendPosition::LowerRight,
Position::Custom { x, y } => LegendPosition::Custom {
x,
y,
anchor: LegendAnchor::NorthWest,
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum LegendAnchor {
#[default]
NorthWest,
North,
NorthEast,
West,
Center,
East,
SouthWest,
South,
SouthEast,
}
impl LegendAnchor {
pub fn offset_multipliers(&self) -> (f32, f32) {
match self {
LegendAnchor::NorthWest => (0.0, 0.0),
LegendAnchor::North => (0.5, 0.0),
LegendAnchor::NorthEast => (1.0, 0.0),
LegendAnchor::West => (0.0, 0.5),
LegendAnchor::Center => (0.5, 0.5),
LegendAnchor::East => (1.0, 0.5),
LegendAnchor::SouthWest => (0.0, 1.0),
LegendAnchor::South => (0.5, 1.0),
LegendAnchor::SouthEast => (1.0, 1.0),
}
}
}
#[derive(Debug, Clone)]
pub struct LegendItem {
pub label: String,
pub color: Color,
pub item_type: LegendItemType,
pub has_error_bars: bool,
}
#[derive(Debug, Clone)]
pub enum LegendItemType {
Line { style: LineStyle, width: f32 },
Scatter { marker: MarkerStyle, size: f32 },
LineMarker {
line_style: LineStyle,
line_width: f32,
marker: MarkerStyle,
marker_size: f32,
},
Bar,
Area { edge_color: Option<Color> },
Histogram,
ErrorBar,
}
impl LegendItem {
pub fn line(label: impl Into<String>, color: Color, style: LineStyle, width: f32) -> Self {
Self {
label: label.into(),
color,
item_type: LegendItemType::Line { style, width },
has_error_bars: false,
}
}
pub fn scatter(label: impl Into<String>, color: Color, marker: MarkerStyle, size: f32) -> Self {
Self {
label: label.into(),
color,
item_type: LegendItemType::Scatter { marker, size },
has_error_bars: false,
}
}
pub fn line_marker(
label: impl Into<String>,
color: Color,
line_style: LineStyle,
line_width: f32,
marker: MarkerStyle,
marker_size: f32,
) -> Self {
Self {
label: label.into(),
color,
item_type: LegendItemType::LineMarker {
line_style,
line_width,
marker,
marker_size,
},
has_error_bars: false,
}
}
pub fn bar(label: impl Into<String>, color: Color) -> Self {
Self {
label: label.into(),
color,
item_type: LegendItemType::Bar,
has_error_bars: false,
}
}
pub fn histogram(label: impl Into<String>, color: Color) -> Self {
Self {
label: label.into(),
color,
item_type: LegendItemType::Histogram,
has_error_bars: false,
}
}
pub fn area(label: impl Into<String>, color: Color, edge_color: Option<Color>) -> Self {
Self {
label: label.into(),
color,
item_type: LegendItemType::Area { edge_color },
has_error_bars: false,
}
}
pub fn error_bar(label: impl Into<String>, color: Color) -> Self {
Self {
label: label.into(),
color,
item_type: LegendItemType::ErrorBar,
has_error_bars: true, }
}
pub fn from_tuple(label: String, color: Color) -> Self {
Self {
label,
color,
item_type: LegendItemType::Bar,
has_error_bars: false,
}
}
pub fn with_error_bars(mut self, has_error_bars: bool) -> Self {
self.has_error_bars = has_error_bars;
self
}
}
#[derive(Debug, Clone, Copy)]
pub struct LegendSpacing {
pub handle_length: f32,
pub handle_height: f32,
pub handle_text_pad: f32,
pub label_spacing: f32,
pub border_pad: f32,
pub border_axes_pad: f32,
pub column_spacing: f32,
}
impl Default for LegendSpacing {
fn default() -> Self {
Self {
handle_length: 2.0, handle_height: 0.7, handle_text_pad: 1.0, label_spacing: 0.7, border_pad: 0.6, border_axes_pad: 1.0, column_spacing: 2.0, }
}
}
impl LegendSpacing {
pub fn to_pixels(self, font_size: f32) -> LegendSpacingPixels {
LegendSpacingPixels {
handle_length: self.handle_length * font_size,
handle_height: self.handle_height * font_size,
handle_text_pad: self.handle_text_pad * font_size,
label_spacing: self.label_spacing * font_size,
border_pad: self.border_pad * font_size,
border_axes_pad: self.border_axes_pad * font_size,
column_spacing: self.column_spacing * font_size,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct LegendSpacingPixels {
pub handle_length: f32,
pub handle_height: f32,
pub handle_text_pad: f32,
pub label_spacing: f32,
pub border_pad: f32,
pub border_axes_pad: f32,
pub column_spacing: f32,
}
#[derive(Debug, Clone)]
pub struct LegendStyle {
pub visible: bool,
pub alpha: f32,
pub face_color: Color,
pub edge_color: Option<Color>,
pub border_width: f32,
pub fancy_box: bool,
pub corner_radius: f32,
pub shadow: bool,
pub shadow_offset: (f32, f32),
pub shadow_color: Color,
}
impl Default for LegendStyle {
fn default() -> Self {
Self {
visible: true,
alpha: 0.8,
face_color: Color::WHITE,
edge_color: Some(Color::from_gray(204)), border_width: 0.8,
fancy_box: true,
corner_radius: 4.0,
shadow: false,
shadow_offset: (2.0, -2.0),
shadow_color: Color::new_rgba(0, 0, 0, 50),
}
}
}
impl LegendStyle {
pub fn new() -> Self {
Self::default()
}
pub fn invisible() -> Self {
Self {
visible: false,
..Default::default()
}
}
pub fn rounded(radius: f32) -> Self {
Self {
fancy_box: true,
corner_radius: radius,
..Default::default()
}
}
pub fn sharp() -> Self {
Self {
fancy_box: false,
corner_radius: 0.0,
..Default::default()
}
}
pub fn visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
pub fn alpha(mut self, alpha: f32) -> Self {
self.alpha = alpha.clamp(0.0, 1.0);
self
}
pub fn face_color(mut self, color: Color) -> Self {
self.face_color = color;
self
}
pub fn edge_color(mut self, color: Option<Color>) -> Self {
self.edge_color = color;
self
}
pub fn border_width(mut self, width: f32) -> Self {
self.border_width = width.max(0.0);
self
}
pub fn fancy_box(mut self, enabled: bool) -> Self {
self.fancy_box = enabled;
self
}
pub fn corner_radius(mut self, radius: f32) -> Self {
self.corner_radius = radius.max(0.0);
self
}
pub fn shadow(mut self, enabled: bool) -> Self {
self.shadow = enabled;
self
}
pub fn effective_corner_radius(&self) -> f32 {
if self.fancy_box {
self.corner_radius
} else {
0.0
}
}
pub fn effective_face_color(&self) -> Color {
self.face_color.with_alpha(self.alpha)
}
}
#[deprecated(since = "0.2.0", note = "Use LegendStyle instead")]
pub type LegendFrame = LegendStyle;
#[derive(Debug, Clone)]
pub struct Legend {
pub enabled: bool,
pub position: LegendPosition,
pub spacing: LegendSpacing,
pub style: LegendStyle,
pub font_size: f32,
pub text_color: Color,
pub columns: usize,
pub title: Option<String>,
}
impl Default for Legend {
fn default() -> Self {
Self {
enabled: false,
position: LegendPosition::default(),
spacing: LegendSpacing::default(),
style: LegendStyle::default(),
font_size: 10.0,
text_color: Color::BLACK,
columns: 1,
title: None,
}
}
}
impl Legend {
pub fn new() -> Self {
Self::default()
}
pub fn upper_right() -> Self {
Self {
enabled: true,
position: LegendPosition::UpperRight,
..Default::default()
}
}
pub fn best() -> Self {
Self {
enabled: true,
position: LegendPosition::Best,
..Default::default()
}
}
pub fn outside_right() -> Self {
Self {
enabled: true,
position: LegendPosition::OutsideRight,
..Default::default()
}
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn at(mut self, position: LegendPosition) -> Self {
self.position = position;
self
}
pub fn position(mut self, position: LegendPosition) -> Self {
self.position = position;
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn font_size(mut self, size: f32) -> Self {
self.font_size = size;
self
}
pub fn columns(mut self, cols: usize) -> Self {
self.columns = cols.max(1);
self
}
pub fn style(mut self, style: LegendStyle) -> Self {
self.style = style;
self
}
#[deprecated(since = "0.2.0", note = "Use style() instead")]
pub fn frame(mut self, style: LegendStyle) -> Self {
self.style = style;
self
}
pub fn spacing(mut self, spacing: LegendSpacing) -> Self {
self.spacing = spacing;
self
}
pub fn calculate_size(&self, items: &[LegendItem], char_width: f32) -> (f32, f32) {
if items.is_empty() {
return (0.0, 0.0);
}
let spacing_px = self.spacing.to_pixels(self.font_size);
let max_label_len = items.iter().map(|item| item.label.len()).max().unwrap_or(0);
let label_width = max_label_len as f32 * char_width;
let item_width = spacing_px.handle_length + spacing_px.handle_text_pad + label_width;
let items_per_col = items.len().div_ceil(self.columns);
let content_width = item_width * self.columns as f32
+ (self.columns.saturating_sub(1)) as f32 * spacing_px.column_spacing;
let content_height = items_per_col as f32 * self.font_size
+ (items_per_col.saturating_sub(1)) as f32 * spacing_px.label_spacing;
let title_height = if self.title.is_some() {
self.font_size + spacing_px.label_spacing
} else {
0.0
};
let width = content_width + spacing_px.border_pad * 2.0;
let height = content_height + title_height + spacing_px.border_pad * 2.0;
(width, height)
}
pub fn calculate_position(
&self,
legend_size: (f32, f32),
plot_area: (f32, f32, f32, f32),
) -> (f32, f32) {
let (width, height) = legend_size;
let (left, top, right, bottom) = plot_area;
let spacing_px = self.spacing.to_pixels(self.font_size);
let pad = spacing_px.border_axes_pad;
match self.position {
LegendPosition::Best => {
(right - width - pad, top + pad)
}
LegendPosition::UpperRight | LegendPosition::Right => (right - width - pad, top + pad),
LegendPosition::UpperLeft => (left + pad, top + pad),
LegendPosition::LowerLeft => (left + pad, bottom - height - pad),
LegendPosition::LowerRight => (right - width - pad, bottom - height - pad),
LegendPosition::CenterLeft => {
let center_y = (top + bottom) / 2.0;
(left + pad, center_y - height / 2.0)
}
LegendPosition::CenterRight => {
let center_y = (top + bottom) / 2.0;
(right - width - pad, center_y - height / 2.0)
}
LegendPosition::LowerCenter => {
let center_x = (left + right) / 2.0;
(center_x - width / 2.0, bottom - height - pad)
}
LegendPosition::UpperCenter => {
let center_x = (left + right) / 2.0;
(center_x - width / 2.0, top + pad)
}
LegendPosition::Center => {
let center_x = (left + right) / 2.0;
let center_y = (top + bottom) / 2.0;
(center_x - width / 2.0, center_y - height / 2.0)
}
LegendPosition::OutsideRight => (right + pad, top),
LegendPosition::OutsideLeft => (left - width - pad, top),
LegendPosition::OutsideUpper => (right - width, top - height - pad),
LegendPosition::OutsideLower => (right - width, bottom + pad),
LegendPosition::Custom { x, y, anchor } => {
let plot_width = right - left;
let plot_height = bottom - top;
let (x_mult, y_mult) = anchor.offset_multipliers();
let base_x = left + x * plot_width;
let base_y = top + (1.0 - y) * plot_height;
(base_x - x_mult * width, base_y - y_mult * height)
}
}
}
}
pub fn find_best_position(
legend_size: (f32, f32),
plot_area: (f32, f32, f32, f32),
data_bboxes: &[(f32, f32, f32, f32)], spacing: &LegendSpacing,
font_size: f32,
) -> LegendPosition {
let candidates = [
LegendPosition::UpperRight,
LegendPosition::UpperLeft,
LegendPosition::LowerLeft,
LegendPosition::LowerRight,
LegendPosition::CenterRight,
LegendPosition::CenterLeft,
LegendPosition::UpperCenter,
LegendPosition::LowerCenter,
LegendPosition::Center,
];
let legend = Legend {
position: LegendPosition::UpperRight, spacing: *spacing,
font_size,
..Default::default()
};
let mut best_position = LegendPosition::UpperRight;
let mut min_overlap = f32::MAX;
for &candidate in &candidates {
let mut test_legend = legend.clone();
test_legend.position = candidate;
let (x, y) = test_legend.calculate_position(legend_size, plot_area);
let legend_bbox = (x, y, x + legend_size.0, y + legend_size.1);
let overlap = calculate_total_overlap(legend_bbox, data_bboxes);
if overlap < min_overlap {
min_overlap = overlap;
best_position = candidate;
}
}
best_position
}
fn calculate_total_overlap(
legend_bbox: (f32, f32, f32, f32),
data_bboxes: &[(f32, f32, f32, f32)],
) -> f32 {
data_bboxes
.iter()
.map(|data_bbox| calculate_bbox_overlap(legend_bbox, *data_bbox))
.sum()
}
fn calculate_bbox_overlap(bbox1: (f32, f32, f32, f32), bbox2: (f32, f32, f32, f32)) -> f32 {
let (l1, t1, r1, b1) = bbox1;
let (l2, t2, r2, b2) = bbox2;
let x_overlap = (r1.min(r2) - l1.max(l2)).max(0.0);
let y_overlap = (b1.min(b2) - t1.max(t2)).max(0.0);
x_overlap * y_overlap
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_legend_item_creation() {
let line_item = LegendItem::line("sin(x)", Color::BLUE, LineStyle::Solid, 1.5);
assert_eq!(line_item.label, "sin(x)");
assert!(matches!(line_item.item_type, LegendItemType::Line { .. }));
let scatter_item = LegendItem::scatter("data", Color::RED, MarkerStyle::Circle, 6.0);
assert!(matches!(
scatter_item.item_type,
LegendItemType::Scatter { .. }
));
let line_marker = LegendItem::line_marker(
"combined",
Color::GREEN,
LineStyle::Dashed,
1.5,
MarkerStyle::Circle,
6.0,
);
assert!(matches!(
line_marker.item_type,
LegendItemType::LineMarker { .. }
));
}
#[test]
fn test_spacing_to_pixels() {
let spacing = LegendSpacing::default();
let pixels = spacing.to_pixels(10.0);
assert!((pixels.handle_length - 20.0).abs() < 0.001); assert!((pixels.label_spacing - 7.0).abs() < 0.001); assert!((pixels.border_pad - 6.0).abs() < 0.001); assert!((pixels.handle_text_pad - 10.0).abs() < 0.001); assert!((pixels.border_axes_pad - 10.0).abs() < 0.001); }
#[test]
fn test_legend_position_is_outside() {
assert!(!LegendPosition::UpperRight.is_outside());
assert!(!LegendPosition::Center.is_outside());
assert!(LegendPosition::OutsideRight.is_outside());
assert!(LegendPosition::OutsideUpper.is_outside());
let custom_inside = LegendPosition::Custom {
x: 0.5,
y: 0.5,
anchor: LegendAnchor::Center,
};
assert!(!custom_inside.is_outside());
let custom_outside = LegendPosition::Custom {
x: 1.1,
y: 0.5,
anchor: LegendAnchor::NorthWest,
};
assert!(custom_outside.is_outside());
}
#[test]
fn test_legend_size_calculation() {
let legend = Legend::new();
let items = vec![
LegendItem::line("sin(x)", Color::BLUE, LineStyle::Solid, 1.5),
LegendItem::line("cos(x)", Color::RED, LineStyle::Dashed, 1.5),
];
let (width, height) = legend.calculate_size(&items, 6.0);
assert!(width > 0.0);
assert!(height > 0.0);
}
#[test]
fn test_legend_position_calculation() {
let legend = Legend::new().at(LegendPosition::UpperRight);
let plot_area = (100.0, 50.0, 500.0, 400.0); let legend_size = (80.0, 60.0);
let (x, y) = legend.calculate_position(legend_size, plot_area);
assert!(x < 500.0);
assert!(x > 400.0);
assert!(y > 50.0);
assert!(y < 100.0);
}
#[test]
fn test_anchor_offsets() {
assert_eq!(LegendAnchor::NorthWest.offset_multipliers(), (0.0, 0.0));
assert_eq!(LegendAnchor::Center.offset_multipliers(), (0.5, 0.5));
assert_eq!(LegendAnchor::SouthEast.offset_multipliers(), (1.0, 1.0));
}
#[test]
fn test_bbox_overlap() {
let overlap1 = calculate_bbox_overlap((0.0, 0.0, 10.0, 10.0), (20.0, 20.0, 30.0, 30.0));
assert_eq!(overlap1, 0.0);
let overlap2 = calculate_bbox_overlap((0.0, 0.0, 10.0, 10.0), (5.0, 5.0, 15.0, 15.0));
assert_eq!(overlap2, 25.0);
let overlap3 = calculate_bbox_overlap((0.0, 0.0, 20.0, 20.0), (5.0, 5.0, 10.0, 10.0));
assert_eq!(overlap3, 25.0); }
#[test]
fn test_find_best_position() {
let legend_size = (80.0, 60.0);
let plot_area = (100.0, 50.0, 500.0, 400.0);
let data_bboxes = vec![(400.0, 50.0, 500.0, 150.0)];
let best = find_best_position(
legend_size,
plot_area,
&data_bboxes,
&LegendSpacing::default(),
10.0,
);
assert_ne!(best, LegendPosition::UpperRight);
}
}