use super::mark::Mark;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValidationSeverity {
Ok,
Warning,
Error,
}
#[derive(Debug, Clone)]
pub struct CardinalityConstraint {
pub max_recommended: usize,
pub max_allowed: usize,
pub warning_message: String,
}
impl Default for CardinalityConstraint {
fn default() -> Self {
Self {
max_recommended: usize::MAX,
max_allowed: usize::MAX,
warning_message: String::new(),
}
}
}
impl CardinalityConstraint {
pub fn for_chart(mark: Mark, axis: &str) -> Self {
match (mark, axis) {
(Mark::Bar, "X") => Self {
max_recommended: 50,
max_allowed: 200,
warning_message: "Bar charts with >50 categories become hard to read. Consider filtering or grouping categories.".into(),
},
(Mark::Arc, "X") => Self {
max_recommended: 10,
max_allowed: 30,
warning_message: "Pie charts with >10 slices are difficult to compare. Consider grouping smaller categories into 'Other'.".into(),
},
(Mark::Point | Mark::Circle, "Color") => Self {
max_recommended: 20,
max_allowed: 100,
warning_message: "Too many colors reduce distinguishability. Consider grouping categories or using a different visual encoding.".into(),
},
(Mark::Line | Mark::Area, "Color") => Self {
max_recommended: 10,
max_allowed: 50,
warning_message: "Too many series make the chart cluttered. Consider filtering or faceting.".into(),
},
_ => Self::default(),
}
}
pub fn validate(&self, cardinality: usize) -> ValidationSeverity {
if cardinality > self.max_allowed {
ValidationSeverity::Error
} else if cardinality > self.max_recommended {
ValidationSeverity::Warning
} else {
ValidationSeverity::Ok
}
}
pub fn is_ok(&self, cardinality: usize) -> bool {
cardinality <= self.max_recommended
}
pub fn is_warning(&self, cardinality: usize) -> bool {
cardinality > self.max_recommended && cardinality <= self.max_allowed
}
pub fn is_error(&self, cardinality: usize) -> bool {
cardinality > self.max_allowed
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bar_chart_x_axis_thresholds() {
let constraint = CardinalityConstraint::for_chart(Mark::Bar, "X");
assert_eq!(constraint.max_recommended, 50);
assert_eq!(constraint.max_allowed, 200);
assert!(!constraint.warning_message.is_empty());
}
#[test]
fn test_pie_chart_x_axis_thresholds() {
let constraint = CardinalityConstraint::for_chart(Mark::Arc, "X");
assert_eq!(constraint.max_recommended, 10);
assert_eq!(constraint.max_allowed, 30);
}
#[test]
fn test_validate_ok() {
let constraint = CardinalityConstraint::for_chart(Mark::Bar, "X");
assert_eq!(constraint.validate(30), ValidationSeverity::Ok);
assert_eq!(constraint.validate(50), ValidationSeverity::Ok);
assert!(constraint.is_ok(30));
}
#[test]
fn test_validate_warning() {
let constraint = CardinalityConstraint::for_chart(Mark::Bar, "X");
assert_eq!(constraint.validate(51), ValidationSeverity::Warning);
assert_eq!(constraint.validate(100), ValidationSeverity::Warning);
assert_eq!(constraint.validate(200), ValidationSeverity::Warning);
assert!(constraint.is_warning(75));
}
#[test]
fn test_validate_error() {
let constraint = CardinalityConstraint::for_chart(Mark::Bar, "X");
assert_eq!(constraint.validate(201), ValidationSeverity::Error);
assert_eq!(constraint.validate(1000), ValidationSeverity::Error);
assert!(constraint.is_error(250));
}
#[test]
fn test_pie_chart_warns_at_11_slices() {
let constraint = CardinalityConstraint::for_chart(Mark::Arc, "X");
assert_eq!(constraint.validate(10), ValidationSeverity::Ok);
assert_eq!(constraint.validate(11), ValidationSeverity::Warning);
}
#[test]
fn test_pie_chart_errors_at_31_slices() {
let constraint = CardinalityConstraint::for_chart(Mark::Arc, "X");
assert_eq!(constraint.validate(30), ValidationSeverity::Warning);
assert_eq!(constraint.validate(31), ValidationSeverity::Error);
}
#[test]
fn test_scatter_color_channel_constraint() {
let constraint = CardinalityConstraint::for_chart(Mark::Point, "Color");
assert_eq!(constraint.max_recommended, 20);
assert_eq!(constraint.max_allowed, 100);
}
#[test]
fn test_line_color_channel_constraint() {
let constraint = CardinalityConstraint::for_chart(Mark::Line, "Color");
assert_eq!(constraint.max_recommended, 10);
assert_eq!(constraint.max_allowed, 50);
}
#[test]
fn test_default_constraint_no_limits() {
let constraint = CardinalityConstraint::for_chart(Mark::Bar, "Y");
assert_eq!(constraint.max_recommended, usize::MAX);
assert_eq!(constraint.max_allowed, usize::MAX);
assert_eq!(constraint.validate(1_000_000), ValidationSeverity::Ok);
}
#[test]
fn test_circle_same_as_point() {
let point_constraint = CardinalityConstraint::for_chart(Mark::Point, "Color");
let circle_constraint = CardinalityConstraint::for_chart(Mark::Circle, "Color");
assert_eq!(
point_constraint.max_recommended,
circle_constraint.max_recommended
);
assert_eq!(point_constraint.max_allowed, circle_constraint.max_allowed);
}
#[test]
fn test_area_similar_to_line() {
let line_constraint = CardinalityConstraint::for_chart(Mark::Line, "Color");
let area_constraint = CardinalityConstraint::for_chart(Mark::Area, "Color");
assert_eq!(
line_constraint.max_recommended,
area_constraint.max_recommended
);
}
#[test]
fn test_boundary_cases() {
let constraint = CardinalityConstraint::for_chart(Mark::Bar, "X");
assert_eq!(constraint.validate(0), ValidationSeverity::Ok);
assert_eq!(constraint.validate(50), ValidationSeverity::Ok);
assert_eq!(constraint.validate(51), ValidationSeverity::Warning);
assert_eq!(constraint.validate(200), ValidationSeverity::Warning);
assert_eq!(constraint.validate(201), ValidationSeverity::Error);
}
}