use crate::aes::{
validated_data_attr_internal_name, Aes, Aesthetic, MappingStage, INTERNAL_PREFIX,
TOOLTIP_INTERNAL_NAME,
};
use crate::data::DataFrame;
fn target_column(aesthetic: &Aesthetic, warnings: &mut Vec<String>) -> Option<String> {
match aesthetic {
Aesthetic::DataAttr(target) => match validated_data_attr_internal_name(target) {
Some(internal) => Some(internal),
None => {
warnings.push(format!(
"aes: data_attr target {target:?} is invalid or reserved; skipped"
));
None
}
},
Aesthetic::Tooltip => Some(TOOLTIP_INTERNAL_NAME.to_string()),
other => Some(other.col_name().to_string()),
}
}
pub fn resolve_mappings(data: &DataFrame, mapping: &Aes, warnings: &mut Vec<String>) -> DataFrame {
let mut result = DataFrame::new();
let nrows = data.nrows();
if nrows == 0 {
return result;
}
for m in &mapping.mappings {
if m.stage != MappingStage::BeforeStat {
continue;
}
let Some(col_name) = target_column(&m.aesthetic, warnings) else {
continue;
};
if let Some(values) = data.column(&m.column) {
result.add_column(col_name, values.to_vec());
} else if let Some(values) = super::expr::eval_expression(&m.column, data) {
result.add_column(col_name, values);
}
}
for name in data.column_names() {
if is_forged_internal_name(name) {
continue;
}
if !result.has_column(name) {
if let Some(values) = data.column(name) {
if result.nrows() == 0 || values.len() == result.nrows() {
result.add_column(name.to_string(), values.to_vec());
}
}
}
}
result
}
fn is_forged_internal_name(name: &str) -> bool {
name.contains('\0') || name.contains(INTERNAL_PREFIX)
}
pub fn apply_after_stat(data: &mut DataFrame, mapping: &Aes, warnings: &mut Vec<String>) {
for m in &mapping.mappings {
if m.stage != MappingStage::AfterStat {
continue;
}
let Some(target) = target_column(&m.aesthetic, warnings) else {
continue;
};
let source = &m.column;
let values = if let Some(values) = data.column(source) {
Some(values.to_vec())
} else {
super::expr::eval_expression(source, data)
};
if let Some(values) = values {
if data.has_column(&target) {
if let Some(col) = data.column_mut(&target) {
*col = values;
}
} else {
data.add_column(target, values);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::aes::AesMapping;
use crate::data::Value;
#[test]
fn resolve_mappings_drops_an_invalid_data_attr_target_with_a_warning() {
let mut data = DataFrame::new();
data.add_column("x".to_string(), vec![Value::Float(1.0)]);
data.add_column("v".to_string(), vec![Value::Str("payload".into())]);
let mut aes = Aes::new();
aes.mappings.push(AesMapping {
column: "v".to_string(),
aesthetic: Aesthetic::DataAttr("data-part".to_string()), stage: MappingStage::BeforeStat,
});
let mut warnings = Vec::new();
let result = resolve_mappings(&data, &aes, &mut warnings);
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("reserved"));
assert!(!result
.column_names()
.iter()
.any(|n| n.starts_with(crate::aes::DATA_ATTR_INTERNAL_PREFIX)));
}
#[test]
fn apply_after_stat_drops_an_invalid_data_attr_target_with_a_warning() {
let mut data = DataFrame::new();
data.add_column("count".to_string(), vec![Value::Float(5.0)]);
let mut aes = Aes::new();
aes.mappings.push(AesMapping {
column: "count".to_string(),
aesthetic: Aesthetic::DataAttr("data-Bad Name".to_string()),
stage: MappingStage::AfterStat,
});
let mut warnings = Vec::new();
apply_after_stat(&mut data, &aes, &mut warnings);
assert_eq!(warnings.len(), 1);
assert!(!data
.column_names()
.iter()
.any(|n| n.starts_with(crate::aes::DATA_ATTR_INTERNAL_PREFIX)));
}
}