use super::data::{DataType, FieldRole};
use super::mark::Mark;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AxisType {
Categorical,
Continuous,
Any,
}
#[derive(Debug, Clone)]
pub struct AxisRequirement {
pub axis_name: &'static str,
pub axis_type: AxisType,
pub required_roles: Vec<FieldRole>,
pub allowed_data_types: Vec<DataType>,
}
impl AxisRequirement {
pub fn accepts_role(&self, role: FieldRole) -> bool {
self.required_roles.contains(&role)
}
pub fn accepts_data_type(&self, data_type: DataType) -> bool {
self.allowed_data_types.contains(&data_type)
}
pub fn is_compatible(&self, role: FieldRole, data_type: DataType) -> bool {
self.accepts_role(role) && self.accepts_data_type(data_type)
}
}
#[derive(Debug, Clone)]
pub struct MarkCompatibilityRules {
pub mark: Mark,
pub x_axis: AxisRequirement,
pub y_axis: AxisRequirement,
pub color_channel: Option<AxisRequirement>,
pub size_channel: Option<AxisRequirement>,
}
impl MarkCompatibilityRules {
pub fn for_mark(mark: Mark) -> Self {
match mark {
Mark::Line => Self::line_rules(),
Mark::Area => Self::area_rules(),
Mark::Bar => Self::bar_rules(),
Mark::Point => Self::point_rules(),
Mark::Circle => Self::circle_rules(),
Mark::Arc => Self::arc_rules(),
}
}
fn line_rules() -> Self {
Self {
mark: Mark::Line,
x_axis: AxisRequirement {
axis_name: "X",
axis_type: AxisType::Continuous,
required_roles: vec![FieldRole::Dimension, FieldRole::Measure],
allowed_data_types: vec![DataType::Quantitative, DataType::Temporal],
},
y_axis: AxisRequirement {
axis_name: "Y",
axis_type: AxisType::Continuous,
required_roles: vec![FieldRole::Measure],
allowed_data_types: vec![DataType::Quantitative],
},
color_channel: Some(AxisRequirement {
axis_name: "Color",
axis_type: AxisType::Any,
required_roles: vec![FieldRole::Dimension],
allowed_data_types: vec![DataType::Nominal, DataType::Ordinal],
}),
size_channel: None,
}
}
fn area_rules() -> Self {
Self {
mark: Mark::Area,
x_axis: AxisRequirement {
axis_name: "X",
axis_type: AxisType::Continuous,
required_roles: vec![FieldRole::Dimension, FieldRole::Measure],
allowed_data_types: vec![DataType::Quantitative, DataType::Temporal],
},
y_axis: AxisRequirement {
axis_name: "Y",
axis_type: AxisType::Continuous,
required_roles: vec![FieldRole::Measure],
allowed_data_types: vec![DataType::Quantitative],
},
color_channel: Some(AxisRequirement {
axis_name: "Color",
axis_type: AxisType::Any,
required_roles: vec![FieldRole::Dimension],
allowed_data_types: vec![DataType::Nominal, DataType::Ordinal],
}),
size_channel: None,
}
}
fn bar_rules() -> Self {
Self {
mark: Mark::Bar,
x_axis: AxisRequirement {
axis_name: "X",
axis_type: AxisType::Categorical,
required_roles: vec![FieldRole::Dimension],
allowed_data_types: vec![DataType::Nominal, DataType::Ordinal],
},
y_axis: AxisRequirement {
axis_name: "Y",
axis_type: AxisType::Continuous,
required_roles: vec![FieldRole::Measure],
allowed_data_types: vec![DataType::Quantitative],
},
color_channel: Some(AxisRequirement {
axis_name: "Color",
axis_type: AxisType::Any,
required_roles: vec![FieldRole::Dimension],
allowed_data_types: vec![DataType::Nominal, DataType::Ordinal],
}),
size_channel: None,
}
}
fn point_rules() -> Self {
Self {
mark: Mark::Point,
x_axis: AxisRequirement {
axis_name: "X",
axis_type: AxisType::Continuous,
required_roles: vec![FieldRole::Measure],
allowed_data_types: vec![DataType::Quantitative],
},
y_axis: AxisRequirement {
axis_name: "Y",
axis_type: AxisType::Continuous,
required_roles: vec![FieldRole::Measure],
allowed_data_types: vec![DataType::Quantitative],
},
color_channel: Some(AxisRequirement {
axis_name: "Color",
axis_type: AxisType::Any,
required_roles: vec![FieldRole::Dimension],
allowed_data_types: vec![DataType::Nominal, DataType::Ordinal],
}),
size_channel: Some(AxisRequirement {
axis_name: "Size",
axis_type: AxisType::Continuous,
required_roles: vec![FieldRole::Measure],
allowed_data_types: vec![DataType::Quantitative],
}),
}
}
fn circle_rules() -> Self {
Self {
mark: Mark::Circle,
x_axis: AxisRequirement {
axis_name: "X",
axis_type: AxisType::Continuous,
required_roles: vec![FieldRole::Measure],
allowed_data_types: vec![DataType::Quantitative],
},
y_axis: AxisRequirement {
axis_name: "Y",
axis_type: AxisType::Continuous,
required_roles: vec![FieldRole::Measure],
allowed_data_types: vec![DataType::Quantitative],
},
color_channel: Some(AxisRequirement {
axis_name: "Color",
axis_type: AxisType::Any,
required_roles: vec![FieldRole::Dimension],
allowed_data_types: vec![DataType::Nominal, DataType::Ordinal],
}),
size_channel: Some(AxisRequirement {
axis_name: "Size",
axis_type: AxisType::Continuous,
required_roles: vec![FieldRole::Measure],
allowed_data_types: vec![DataType::Quantitative],
}),
}
}
fn arc_rules() -> Self {
Self {
mark: Mark::Arc,
x_axis: AxisRequirement {
axis_name: "X",
axis_type: AxisType::Categorical,
required_roles: vec![FieldRole::Dimension],
allowed_data_types: vec![DataType::Nominal],
},
y_axis: AxisRequirement {
axis_name: "Y",
axis_type: AxisType::Continuous,
required_roles: vec![FieldRole::Measure],
allowed_data_types: vec![DataType::Quantitative],
},
color_channel: None,
size_channel: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_axis_requirement_accepts_role() {
let req = AxisRequirement {
axis_name: "X",
axis_type: AxisType::Categorical,
required_roles: vec![FieldRole::Dimension],
allowed_data_types: vec![DataType::Nominal],
};
assert!(req.accepts_role(FieldRole::Dimension));
assert!(!req.accepts_role(FieldRole::Measure));
}
#[test]
fn test_axis_requirement_accepts_data_type() {
let req = AxisRequirement {
axis_name: "X",
axis_type: AxisType::Categorical,
required_roles: vec![FieldRole::Dimension],
allowed_data_types: vec![DataType::Nominal, DataType::Ordinal],
};
assert!(req.accepts_data_type(DataType::Nominal));
assert!(req.accepts_data_type(DataType::Ordinal));
assert!(!req.accepts_data_type(DataType::Quantitative));
}
#[test]
fn test_axis_requirement_is_compatible() {
let req = AxisRequirement {
axis_name: "Y",
axis_type: AxisType::Continuous,
required_roles: vec![FieldRole::Measure],
allowed_data_types: vec![DataType::Quantitative],
};
assert!(req.is_compatible(FieldRole::Measure, DataType::Quantitative));
assert!(!req.is_compatible(FieldRole::Dimension, DataType::Quantitative));
assert!(!req.is_compatible(FieldRole::Measure, DataType::Nominal));
}
#[test]
fn test_line_chart_accepts_temporal_x() {
let rules = MarkCompatibilityRules::for_mark(Mark::Line);
assert!(rules.x_axis.accepts_data_type(DataType::Temporal));
assert!(rules
.x_axis
.is_compatible(FieldRole::Dimension, DataType::Temporal));
}
#[test]
fn test_line_chart_requires_quantitative_y() {
let rules = MarkCompatibilityRules::for_mark(Mark::Line);
assert!(rules.y_axis.accepts_data_type(DataType::Quantitative));
assert!(!rules.y_axis.accepts_data_type(DataType::Nominal));
}
#[test]
fn test_bar_chart_requires_categorical_x() {
let rules = MarkCompatibilityRules::for_mark(Mark::Bar);
assert_eq!(rules.x_axis.axis_type, AxisType::Categorical);
assert!(rules.x_axis.accepts_data_type(DataType::Nominal));
assert!(rules.x_axis.accepts_data_type(DataType::Ordinal));
assert!(!rules.x_axis.accepts_data_type(DataType::Quantitative));
}
#[test]
fn test_bar_chart_rejects_quantitative_x() {
let rules = MarkCompatibilityRules::for_mark(Mark::Bar);
assert!(!rules
.x_axis
.is_compatible(FieldRole::Measure, DataType::Quantitative));
}
#[test]
fn test_scatter_requires_quantitative_xy() {
let rules = MarkCompatibilityRules::for_mark(Mark::Point);
assert!(rules
.x_axis
.is_compatible(FieldRole::Measure, DataType::Quantitative));
assert!(rules
.y_axis
.is_compatible(FieldRole::Measure, DataType::Quantitative));
assert!(!rules
.x_axis
.is_compatible(FieldRole::Dimension, DataType::Nominal));
}
#[test]
fn test_scatter_has_size_channel() {
let rules = MarkCompatibilityRules::for_mark(Mark::Point);
assert!(rules.size_channel.is_some());
let size = rules.size_channel.unwrap();
assert_eq!(size.axis_name, "Size");
assert!(size.is_compatible(FieldRole::Measure, DataType::Quantitative));
}
#[test]
fn test_pie_chart_accepts_nominal_x() {
let rules = MarkCompatibilityRules::for_mark(Mark::Arc);
assert!(rules
.x_axis
.is_compatible(FieldRole::Dimension, DataType::Nominal));
assert!(!rules.x_axis.accepts_data_type(DataType::Ordinal));
}
#[test]
fn test_pie_chart_no_color_channel() {
let rules = MarkCompatibilityRules::for_mark(Mark::Arc);
assert!(rules.color_channel.is_none());
assert!(rules.size_channel.is_none());
}
#[test]
fn test_area_chart_same_as_line() {
let line_rules = MarkCompatibilityRules::for_mark(Mark::Line);
let area_rules = MarkCompatibilityRules::for_mark(Mark::Area);
assert_eq!(
line_rules.x_axis.allowed_data_types,
area_rules.x_axis.allowed_data_types
);
assert_eq!(
line_rules.y_axis.allowed_data_types,
area_rules.y_axis.allowed_data_types
);
}
#[test]
fn test_circle_chart_same_as_point() {
let point_rules = MarkCompatibilityRules::for_mark(Mark::Point);
let circle_rules = MarkCompatibilityRules::for_mark(Mark::Circle);
assert_eq!(
point_rules.x_axis.allowed_data_types,
circle_rules.x_axis.allowed_data_types
);
assert!(circle_rules.size_channel.is_some());
}
#[test]
fn test_all_marks_have_rules() {
let marks = [
Mark::Line,
Mark::Area,
Mark::Bar,
Mark::Point,
Mark::Circle,
Mark::Arc,
];
for mark in marks {
let rules = MarkCompatibilityRules::for_mark(mark);
assert_eq!(rules.mark, mark);
}
}
}