use crate::core::style_utils::defaults;
use crate::plots::traits::PlotConfig;
use crate::render::{Color, MarkerStyle};
#[derive(Debug, Clone)]
pub struct ScatterConfig {
pub marker: MarkerStyle,
pub size: f32,
pub color: Option<Color>,
pub alpha: f32,
pub edge_color: Option<Color>,
pub edge_width: f32,
pub show_edge: bool,
}
impl Default for ScatterConfig {
fn default() -> Self {
Self {
marker: MarkerStyle::Circle,
size: 6.0,
color: None,
alpha: 1.0,
edge_color: None,
edge_width: defaults::PATCH_LINE_WIDTH,
show_edge: false,
}
}
}
impl PlotConfig for ScatterConfig {}
impl ScatterConfig {
pub fn new() -> Self {
Self::default()
}
pub fn marker(mut self, marker: MarkerStyle) -> Self {
self.marker = marker;
self
}
pub fn size(mut self, size: f32) -> Self {
self.size = size.max(0.1);
self
}
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
pub fn alpha(mut self, alpha: f32) -> Self {
self.alpha = alpha.clamp(0.0, 1.0);
self
}
pub fn edge_color(mut self, color: Color) -> Self {
self.edge_color = Some(color);
self.show_edge = true;
self
}
pub fn edge_width(mut self, width: f32) -> Self {
self.edge_width = width.max(0.0);
self.show_edge = self.edge_width > 0.0;
self
}
pub fn show_edge(mut self, show: bool) -> Self {
self.show_edge = show;
self
}
pub fn resolved_edge_spec(&self) -> Option<(Option<Color>, f32)> {
super::marker_edge_spec(self.show_edge, self.edge_color, self.edge_width)
}
pub fn s(self, size: f32) -> Self {
self.size(size)
}
pub fn c(self, color: Color) -> Self {
self.color(color)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = ScatterConfig::default();
assert!(matches!(config.marker, MarkerStyle::Circle));
assert!((config.size - 6.0).abs() < f32::EPSILON);
assert!(config.color.is_none());
assert!((config.alpha - 1.0).abs() < f32::EPSILON);
assert!(
!config.show_edge,
"a default marker is exactly its series colour: a contrasting rim \
darkens whatever it overlaps and swallows small markers"
);
}
#[test]
fn test_builder_methods() {
let config = ScatterConfig::new()
.marker(MarkerStyle::Square)
.size(12.0)
.color(Color::BLUE)
.alpha(0.8)
.edge_color(Color::BLACK)
.edge_width(1.5);
assert!(matches!(config.marker, MarkerStyle::Square));
assert!((config.size - 12.0).abs() < f32::EPSILON);
assert_eq!(config.color, Some(Color::BLUE));
assert!((config.alpha - 0.8).abs() < f32::EPSILON);
assert_eq!(config.edge_color, Some(Color::BLACK));
assert!((config.edge_width - 1.5).abs() < f32::EPSILON);
}
#[test]
fn test_size_clamping() {
let config = ScatterConfig::new().size(-5.0);
assert!((config.size - 0.1).abs() < f32::EPSILON);
}
#[test]
fn test_resolved_edge_spec_defaults_to_no_rim() {
assert_eq!(
ScatterConfig::default().resolved_edge_spec(),
None,
"a plain .scatter() must ask for no rim at all, so overlapping \
markers cannot darken each other and small markers survive"
);
}
#[test]
fn test_resolved_edge_spec_is_enabled_by_show_edge() {
assert_eq!(
ScatterConfig::new().show_edge(true).resolved_edge_spec(),
Some((None, defaults::PATCH_LINE_WIDTH)),
"show_edge(true) must ask for a rim derived from the fill"
);
}
#[test]
fn test_resolved_edge_spec_is_enabled_by_asking_for_a_width() {
assert_eq!(
ScatterConfig::new().edge_width(1.5).resolved_edge_spec(),
Some((None, 1.5)),
"a width alone must produce an edge derived from the fill"
);
}
#[test]
fn test_resolved_edge_spec_is_enabled_by_asking_for_a_colour() {
assert_eq!(
ScatterConfig::new()
.edge_color(Color::BLACK)
.resolved_edge_spec(),
Some((Some(Color::BLACK), defaults::PATCH_LINE_WIDTH)),
"an edge colour alone must produce an edge at the default width"
);
}
#[test]
fn test_resolved_edge_spec_honours_show_edge() {
assert_eq!(
ScatterConfig::new()
.edge_width(1.5)
.show_edge(false)
.resolved_edge_spec(),
None,
"show_edge(false) must reach the renderer as 'no edge'"
);
}
#[test]
fn test_resolved_edge_spec_is_disabled_by_zero_width() {
assert_eq!(
ScatterConfig::new()
.edge_color(Color::BLACK)
.edge_width(0.0)
.resolved_edge_spec(),
None,
"edge_width(0.0) must switch the edge off, not floor it to a hairline"
);
}
#[test]
fn test_resolved_edge_spec_keeps_an_explicit_colour() {
assert_eq!(
ScatterConfig::new()
.edge_color(Color::BLACK)
.edge_width(1.5)
.resolved_edge_spec(),
Some((Some(Color::BLACK), 1.5)),
"an explicit edge colour and width must survive to the renderer"
);
}
#[test]
fn test_matplotlib_aliases() {
let config = ScatterConfig::new().s(10.0).c(Color::GREEN);
assert!((config.size - 10.0).abs() < f32::EPSILON);
assert_eq!(config.color, Some(Color::GREEN));
}
}