use super::geom::{Geom, GeomType};
use super::Layer;
use crate::plot::aesthetic::{is_position_aesthetic, AestheticContext};
use crate::plot::scale::ScaleTypeKind;
use crate::plot::{AestheticValue, DefaultAestheticValue, Mappings, Scale};
use crate::{naming, DataFrame};
pub const ALIGNED: &str = "aligned";
pub const TRANSPOSED: &str = "transposed";
pub const ORIENTATION_VALUES: &[&str] = &[ALIGNED, TRANSPOSED];
pub fn resolve_orientation(layer: &Layer, scales: &[Scale]) -> &'static str {
if let Some(orientation) = layer.parameters.get("orientation").and_then(|v| v.as_str()) {
return if orientation == TRANSPOSED {
TRANSPOSED
} else {
ALIGNED
};
}
if !geom_has_implicit_orientation(&layer.geom.geom_type()) {
return ALIGNED;
}
detect_from_scales(
scales,
&layer.geom.geom_type(),
&layer.mappings,
&layer.remappings,
)
}
pub fn is_transposed(layer: &Layer) -> bool {
layer
.parameters
.get("orientation")
.and_then(|v| v.as_str())
.map(|s| s == TRANSPOSED)
.unwrap_or(false)
}
pub fn geom_has_implicit_orientation(geom: &GeomType) -> bool {
matches!(
geom,
GeomType::Bar
| GeomType::Histogram
| GeomType::Boxplot
| GeomType::Violin
| GeomType::Density
| GeomType::Ribbon
| GeomType::Rule
| GeomType::Range
)
}
fn detect_from_scales(
scales: &[Scale],
geom: &GeomType,
mappings: &Mappings,
remappings: &Mappings,
) -> &'static str {
let has_pos1_mapping = mappings.contains_key("pos1");
let has_pos2_mapping = mappings.contains_key("pos2");
if !has_pos1_mapping && !has_pos2_mapping {
let has_pos1_remapping = remappings.contains_key("pos1");
let has_pos2_remapping = remappings.contains_key("pos2");
if has_pos1_remapping && !has_pos2_remapping {
return TRANSPOSED;
}
if has_pos2_remapping && !has_pos1_remapping {
return ALIGNED;
}
}
let pos1_scale = scales.iter().find(|s| s.aesthetic == "pos1");
let pos2_scale = scales.iter().find(|s| s.aesthetic == "pos2");
let has_pos1 = pos1_scale.is_some();
let has_pos2 = pos2_scale.is_some();
if has_pos1_mapping || has_pos2_mapping {
let pos1_is_dummy = matches!(
Geom::from_type(*geom).aesthetics().get("pos1"),
Some(DefaultAestheticValue::Dummy)
);
if has_pos2 && !has_pos1 && (!pos1_is_dummy || has_pos1_mapping) {
return TRANSPOSED;
}
if has_pos1 && !has_pos2 {
return ALIGNED;
}
}
let pos1_continuous = pos1_scale.is_some_and(is_continuous_scale);
let pos2_continuous = pos2_scale.is_some_and(is_continuous_scale);
if pos1_continuous && pos2_continuous {
let has_pos1_range = mappings.contains_key("pos1min")
|| mappings.contains_key("pos1max")
|| mappings.contains_key("pos1end");
let has_pos2_range = mappings.contains_key("pos2min")
|| mappings.contains_key("pos2max")
|| mappings.contains_key("pos2end");
if has_pos1_range && !has_pos2_range {
return TRANSPOSED;
}
return ALIGNED;
}
let pos1_discrete = pos1_scale.is_some_and(is_discrete_scale);
let pos2_discrete = pos2_scale.is_some_and(is_discrete_scale);
if pos1_continuous && pos2_discrete {
return TRANSPOSED;
}
if pos1_discrete && pos2_continuous {
return ALIGNED;
}
ALIGNED
}
fn is_continuous_scale(scale: &Scale) -> bool {
scale
.scale_type
.as_ref()
.is_some_and(|st| st.scale_type_kind() == ScaleTypeKind::Continuous)
}
fn is_discrete_scale(scale: &Scale) -> bool {
scale.scale_type.as_ref().is_some_and(|st| {
matches!(
st.scale_type_kind(),
ScaleTypeKind::Discrete | ScaleTypeKind::Ordinal | ScaleTypeKind::Binned
)
})
}
pub fn flip_position_aesthetics(
aesthetics: &mut std::collections::HashMap<String, AestheticValue>,
) {
const PAIRS: [(&str, &str); 5] = [
("pos1", "pos2"),
("pos1min", "pos2min"),
("pos1max", "pos2max"),
("pos1end", "pos2end"),
("pos1offset", "pos2offset"),
];
for (a, b) in PAIRS {
let val_a = aesthetics.remove(a);
let val_b = aesthetics.remove(b);
if let Some(v) = val_a {
aesthetics.insert(b.to_string(), v);
}
if let Some(v) = val_b {
aesthetics.insert(a.to_string(), v);
}
}
}
pub fn flip_dataframe_position_columns(
df: DataFrame,
aesthetic_ctx: &AestheticContext,
) -> DataFrame {
let renames: Vec<(String, String)> = df
.get_column_names()
.iter()
.filter_map(|col_name| {
naming::extract_aesthetic_name(col_name).and_then(|aesthetic| {
if is_position_aesthetic(aesthetic) {
let flipped = aesthetic_ctx.flip_position(aesthetic);
if flipped != aesthetic {
return Some((col_name.to_string(), naming::aesthetic_column(&flipped)));
}
}
None
})
})
.collect();
if renames.is_empty() {
return df;
}
let mut result = df;
for (from, to) in &renames {
let temp = format!("{}_temp", to);
result = result.rename(from, &temp).expect("rename should not fail");
}
for (_, to) in &renames {
let temp = format!("{}_temp", to);
result = result.rename(&temp, to).expect("rename should not fail");
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plot::{AestheticValue, Geom, ScaleType};
#[test]
fn test_orientation_constants() {
assert_eq!(ALIGNED, "aligned");
assert_eq!(TRANSPOSED, "transposed");
}
#[test]
fn test_geom_has_implicit_orientation() {
assert!(geom_has_implicit_orientation(&GeomType::Bar));
assert!(geom_has_implicit_orientation(&GeomType::Histogram));
assert!(geom_has_implicit_orientation(&GeomType::Boxplot));
assert!(geom_has_implicit_orientation(&GeomType::Violin));
assert!(geom_has_implicit_orientation(&GeomType::Density));
assert!(geom_has_implicit_orientation(&GeomType::Ribbon));
assert!(geom_has_implicit_orientation(&GeomType::Rule));
assert!(!geom_has_implicit_orientation(&GeomType::Point));
assert!(!geom_has_implicit_orientation(&GeomType::Line));
assert!(!geom_has_implicit_orientation(&GeomType::Path));
assert!(!geom_has_implicit_orientation(&GeomType::Area));
}
#[test]
fn test_resolve_orientation_no_implicit() {
let layer = Layer::new(Geom::point());
let scales = vec![];
assert_eq!(resolve_orientation(&layer, &scales), ALIGNED);
}
#[test]
fn test_is_transposed_helper() {
use crate::plot::ParameterValue;
let mut layer = Layer::new(Geom::histogram());
layer
.mappings
.insert("pos2", AestheticValue::standard_column("y_col"));
let mut scale = Scale::new("pos2");
scale.scale_type = Some(ScaleType::continuous());
let scales = vec![scale];
let orientation = resolve_orientation(&layer, &scales);
layer.parameters.insert(
"orientation".to_string(),
ParameterValue::String(orientation.to_string()),
);
assert!(is_transposed(&layer));
let mut layer2 = Layer::new(Geom::histogram());
let mut scale2 = Scale::new("pos1");
scale2.scale_type = Some(ScaleType::continuous());
let scales2 = vec![scale2];
let orientation2 = resolve_orientation(&layer2, &scales2);
layer2.parameters.insert(
"orientation".to_string(),
ParameterValue::String(orientation2.to_string()),
);
assert!(!is_transposed(&layer2));
}
#[test]
fn test_resolve_orientation_histogram_default() {
let layer = Layer::new(Geom::histogram());
let mut scale = Scale::new("pos1");
scale.scale_type = Some(ScaleType::continuous());
let scales = vec![scale];
assert_eq!(resolve_orientation(&layer, &scales), ALIGNED);
}
#[test]
fn test_resolve_orientation_histogram_horizontal() {
let mut layer = Layer::new(Geom::histogram());
layer
.mappings
.insert("pos2", AestheticValue::standard_column("y_col"));
let mut scale = Scale::new("pos2");
scale.scale_type = Some(ScaleType::continuous());
let scales = vec![scale];
assert_eq!(resolve_orientation(&layer, &scales), TRANSPOSED);
}
#[test]
fn test_resolve_orientation_scale_only_no_flip() {
let layer = Layer::new(Geom::bar());
let mut scale = Scale::new("pos2");
scale.scale_type = Some(ScaleType::continuous());
let scales = vec![scale];
assert_eq!(resolve_orientation(&layer, &scales), ALIGNED);
}
#[test]
fn test_resolve_orientation_bar_horizontal() {
let layer = Layer::new(Geom::bar());
let mut scale1 = Scale::new("pos1");
scale1.scale_type = Some(ScaleType::continuous());
let mut scale2 = Scale::new("pos2");
scale2.scale_type = Some(ScaleType::discrete());
let scales = vec![scale1, scale2];
assert_eq!(resolve_orientation(&layer, &scales), TRANSPOSED);
}
#[test]
fn test_resolve_orientation_bar_vertical() {
let layer = Layer::new(Geom::bar());
let mut scale1 = Scale::new("pos1");
scale1.scale_type = Some(ScaleType::discrete());
let mut scale2 = Scale::new("pos2");
scale2.scale_type = Some(ScaleType::continuous());
let scales = vec![scale1, scale2];
assert_eq!(resolve_orientation(&layer, &scales), ALIGNED);
}
#[test]
fn test_flip_position_aesthetics() {
let mut layer = Layer::new(Geom::bar());
layer.mappings.insert(
"pos1".to_string(),
AestheticValue::standard_column("category".to_string()),
);
layer.mappings.insert(
"pos2".to_string(),
AestheticValue::standard_column("value".to_string()),
);
layer.mappings.insert(
"pos1end".to_string(),
AestheticValue::standard_column("x2".to_string()),
);
flip_position_aesthetics(&mut layer.mappings.aesthetics);
assert_eq!(
layer.mappings.get("pos2").unwrap().column_name(),
Some("category")
);
assert_eq!(
layer.mappings.get("pos1").unwrap().column_name(),
Some("value")
);
assert_eq!(
layer.mappings.get("pos2end").unwrap().column_name(),
Some("x2")
);
assert!(layer.mappings.get("pos1end").is_none());
}
#[test]
fn test_flip_position_aesthetics_empty() {
let mut layer = Layer::new(Geom::point());
flip_position_aesthetics(&mut layer.mappings.aesthetics);
assert!(layer.mappings.aesthetics.is_empty());
}
#[test]
fn test_flip_position_aesthetics_partial() {
let mut layer = Layer::new(Geom::bar());
layer.mappings.insert(
"pos1".to_string(),
AestheticValue::standard_column("x".to_string()),
);
flip_position_aesthetics(&mut layer.mappings.aesthetics);
assert!(layer.mappings.get("pos1").is_none());
assert_eq!(layer.mappings.get("pos2").unwrap().column_name(), Some("x"));
}
#[test]
fn test_resolve_orientation_ribbon_both_continuous_pos2_range() {
let mut layer = Layer::new(Geom::ribbon());
layer.mappings.insert(
"pos1".to_string(),
AestheticValue::standard_column("x".to_string()),
);
layer.mappings.insert(
"pos2min".to_string(),
AestheticValue::standard_column("ymin".to_string()),
);
layer.mappings.insert(
"pos2max".to_string(),
AestheticValue::standard_column("ymax".to_string()),
);
let mut scale1 = Scale::new("pos1");
scale1.scale_type = Some(ScaleType::continuous());
let mut scale2 = Scale::new("pos2");
scale2.scale_type = Some(ScaleType::continuous());
let scales = vec![scale1, scale2];
assert_eq!(resolve_orientation(&layer, &scales), ALIGNED);
}
#[test]
fn test_resolve_orientation_ribbon_both_continuous_pos1_range() {
let mut layer = Layer::new(Geom::ribbon());
layer.mappings.insert(
"pos2".to_string(),
AestheticValue::standard_column("y".to_string()),
);
layer.mappings.insert(
"pos1min".to_string(),
AestheticValue::standard_column("xmin".to_string()),
);
layer.mappings.insert(
"pos1max".to_string(),
AestheticValue::standard_column("xmax".to_string()),
);
let mut scale1 = Scale::new("pos1");
scale1.scale_type = Some(ScaleType::continuous());
let mut scale2 = Scale::new("pos2");
scale2.scale_type = Some(ScaleType::continuous());
let scales = vec![scale1, scale2];
assert_eq!(resolve_orientation(&layer, &scales), TRANSPOSED);
}
#[test]
fn test_resolve_orientation_ribbon_pos1_continuous_pos2_discrete() {
let mut layer = Layer::new(Geom::ribbon());
layer.mappings.insert(
"pos1".to_string(),
AestheticValue::standard_column("value".to_string()),
);
layer.mappings.insert(
"pos2".to_string(),
AestheticValue::standard_column("category".to_string()),
);
let mut scale1 = Scale::new("pos1");
scale1.scale_type = Some(ScaleType::continuous());
let mut scale2 = Scale::new("pos2");
scale2.scale_type = Some(ScaleType::discrete());
let scales = vec![scale1, scale2];
assert_eq!(resolve_orientation(&layer, &scales), TRANSPOSED);
}
#[test]
fn test_resolve_orientation_ribbon_pos1_discrete_pos2_continuous() {
let mut layer = Layer::new(Geom::ribbon());
layer.mappings.insert(
"pos1".to_string(),
AestheticValue::standard_column("category".to_string()),
);
layer.mappings.insert(
"pos2".to_string(),
AestheticValue::standard_column("value".to_string()),
);
let mut scale1 = Scale::new("pos1");
scale1.scale_type = Some(ScaleType::discrete());
let mut scale2 = Scale::new("pos2");
scale2.scale_type = Some(ScaleType::continuous());
let scales = vec![scale1, scale2];
assert_eq!(resolve_orientation(&layer, &scales), ALIGNED);
}
#[test]
fn test_resolve_orientation_ribbon_pos1_range_with_scales() {
let mut layer = Layer::new(Geom::ribbon());
layer.mappings.insert(
"pos2".to_string(),
AestheticValue::standard_column("Date".to_string()),
);
layer.mappings.insert(
"pos1min".to_string(),
AestheticValue::Literal(crate::plot::ParameterValue::Number(0.0)),
);
layer.mappings.insert(
"pos1max".to_string(),
AestheticValue::standard_column("Temp".to_string()),
);
let mut scale1 = Scale::new("pos1");
scale1.scale_type = Some(ScaleType::continuous());
let mut scale2 = Scale::new("pos2");
scale2.scale_type = Some(ScaleType::continuous());
let scales = vec![scale1, scale2];
assert_eq!(resolve_orientation(&layer, &scales), TRANSPOSED);
}
#[test]
fn test_resolve_orientation_ribbon_pos2_range_with_scales() {
let mut layer = Layer::new(Geom::ribbon());
layer.mappings.insert(
"pos1".to_string(),
AestheticValue::standard_column("Date".to_string()),
);
layer.mappings.insert(
"pos2min".to_string(),
AestheticValue::Literal(crate::plot::ParameterValue::Number(0.0)),
);
layer.mappings.insert(
"pos2max".to_string(),
AestheticValue::standard_column("Temp".to_string()),
);
let mut scale1 = Scale::new("pos1");
scale1.scale_type = Some(ScaleType::continuous());
let mut scale2 = Scale::new("pos2");
scale2.scale_type = Some(ScaleType::continuous());
let scales = vec![scale1, scale2];
assert_eq!(resolve_orientation(&layer, &scales), ALIGNED);
}
#[test]
fn test_resolve_orientation_remapping_to_pos1() {
let mut layer = Layer::new(Geom::bar());
layer.remappings.insert(
"pos1".to_string(),
AestheticValue::standard_column("proportion".to_string()),
);
let scales = vec![];
assert_eq!(resolve_orientation(&layer, &scales), TRANSPOSED);
}
#[test]
fn test_resolve_orientation_remapping_to_pos2() {
let mut layer = Layer::new(Geom::bar());
layer.remappings.insert(
"pos2".to_string(),
AestheticValue::standard_column("count".to_string()),
);
let scales = vec![];
assert_eq!(resolve_orientation(&layer, &scales), ALIGNED);
}
#[test]
fn test_resolve_orientation_remapping_both_axes() {
let mut layer = Layer::new(Geom::bar());
layer.remappings.insert(
"pos1".to_string(),
AestheticValue::standard_column("x_val".to_string()),
);
layer.remappings.insert(
"pos2".to_string(),
AestheticValue::standard_column("y_val".to_string()),
);
let scales = vec![];
assert_eq!(resolve_orientation(&layer, &scales), ALIGNED);
}
#[test]
fn test_resolve_orientation_mapping_overrides_remapping() {
let mut layer = Layer::new(Geom::bar());
layer.mappings.insert(
"pos1".to_string(),
AestheticValue::standard_column("category".to_string()),
);
layer.remappings.insert(
"pos1".to_string(),
AestheticValue::standard_column("proportion".to_string()),
);
let mut scale1 = Scale::new("pos1");
scale1.scale_type = Some(ScaleType::discrete());
let mut scale2 = Scale::new("pos2");
scale2.scale_type = Some(ScaleType::continuous());
let scales = vec![scale1, scale2];
assert_eq!(resolve_orientation(&layer, &scales), ALIGNED);
}
#[test]
fn test_resolve_orientation_rule_vertical() {
let mut layer = Layer::new(Geom::rule());
layer
.mappings
.insert("pos1", AestheticValue::standard_column("x_val"));
let mut scale = Scale::new("pos1");
scale.scale_type = Some(ScaleType::continuous());
let scales = vec![scale];
assert_eq!(resolve_orientation(&layer, &scales), ALIGNED);
}
#[test]
fn test_resolve_orientation_rule_horizontal() {
let mut layer = Layer::new(Geom::rule());
layer
.mappings
.insert("pos1", AestheticValue::standard_column("y_val"));
let mut scale = Scale::new("pos2");
scale.scale_type = Some(ScaleType::continuous());
let scales = vec![scale];
assert_eq!(resolve_orientation(&layer, &scales), TRANSPOSED);
}
#[test]
fn test_resolve_orientation_rule_default() {
let layer = Layer::new(Geom::rule());
let scales = vec![];
assert_eq!(resolve_orientation(&layer, &scales), ALIGNED);
}
}