use std::collections::HashMap;
pub const POSITION_SUFFIXES: &[&str] = &["min", "max", "end"];
pub const USER_FACET_AESTHETICS: &[&str] = &["panel", "row", "column"];
pub const MATERIAL_AESTHETICS: &[&str] = &[
"color",
"colour",
"fill",
"stroke",
"opacity",
"size",
"shape",
"linetype",
"linewidth",
"width",
"height",
"label",
"typeface",
"fontweight",
"italic",
"fontsize",
"hjust",
"vjust",
];
#[derive(Debug, Clone)]
pub struct AestheticContext {
user_to_internal: HashMap<String, String>,
internal_to_primary: HashMap<String, String>,
primary_to_internal_family: HashMap<String, Vec<String>>,
user_primaries: Vec<String>,
internal_primaries: Vec<String>,
user_facet: Vec<&'static str>,
internal_facet: Vec<String>,
material: &'static [&'static str],
}
impl AestheticContext {
pub fn new(position_names: &[String], facet_names: &[&'static str]) -> Self {
let mut user_to_internal = HashMap::new();
let mut internal_to_primary = HashMap::new();
let mut primary_to_internal_family = HashMap::new();
let mut user_primaries = Vec::new();
let mut internal_primaries = Vec::new();
for (i, user_primary) in position_names.iter().enumerate() {
let pos_num = i + 1;
let internal_primary = format!("pos{}", pos_num);
user_primaries.push(user_primary.clone());
internal_primaries.push(internal_primary.clone());
let mut internal_family = vec![internal_primary.clone()];
user_to_internal.insert(user_primary.clone(), internal_primary.clone());
internal_to_primary.insert(internal_primary.clone(), internal_primary.clone());
for suffix in POSITION_SUFFIXES {
let user_variant = format!("{}{}", user_primary, suffix);
let internal_variant = format!("{}{}", internal_primary, suffix);
user_to_internal.insert(user_variant, internal_variant.clone());
internal_to_primary.insert(internal_variant.clone(), internal_primary.clone());
internal_family.push(internal_variant);
}
primary_to_internal_family.insert(internal_primary, internal_family);
}
let internal_facet: Vec<String> = (1..=facet_names.len())
.map(|i| format!("facet{}", i))
.collect();
Self {
user_to_internal,
internal_to_primary,
primary_to_internal_family,
user_primaries,
internal_primaries,
user_facet: facet_names.to_vec(),
internal_facet,
material: MATERIAL_AESTHETICS,
}
}
pub fn from_static(position_names: &[&'static str], facet_names: &[&'static str]) -> Self {
let owned_position: Vec<String> = position_names.iter().map(|s| s.to_string()).collect();
Self::new(&owned_position, facet_names)
}
pub fn map_user_to_internal(&self, user_aesthetic: &str) -> Option<&str> {
if let Some(internal) = self.user_to_internal.get(user_aesthetic) {
return Some(internal.as_str());
}
if let Some(idx) = self.user_facet.iter().position(|u| *u == user_aesthetic) {
return Some(self.internal_facet[idx].as_str());
}
match user_aesthetic {
"panel" => Some("facet1"),
"row" => Some("facet1"),
"column" => Some("facet2"),
_ => None,
}
}
pub fn map_internal_to_user(&self, internal_aesthetic: &str) -> String {
if let Some(idx) = self
.internal_facet
.iter()
.position(|i| i == internal_aesthetic)
{
return self.user_facet[idx].to_string();
}
for (user, internal) in &self.user_to_internal {
if internal == internal_aesthetic {
return user.to_string();
}
}
internal_aesthetic.to_string()
}
pub fn is_primary_internal(&self, name: &str) -> bool {
self.internal_primaries.iter().any(|s| s == name)
}
pub fn is_material(&self, name: &str) -> bool {
self.material.contains(&name)
}
pub fn is_user_facet(&self, name: &str) -> bool {
self.user_facet.contains(&name)
}
pub fn is_internal_facet(&self, name: &str) -> bool {
self.internal_facet.iter().any(|f| f == name)
}
pub fn is_facet(&self, name: &str) -> bool {
self.is_user_facet(name) || self.is_internal_facet(name)
}
pub fn primary_internal_position<'a>(&'a self, name: &'a str) -> Option<&'a str> {
if let Some(primary) = self.internal_to_primary.get(name) {
return Some(primary.as_str());
}
if self.is_material(name) {
return Some(name);
}
None
}
pub fn internal_position_family(&self, primary: &str) -> Option<&[String]> {
self.primary_to_internal_family
.get(primary)
.map(|v| v.as_slice())
}
pub fn internal_position(&self) -> &[String] {
&self.internal_primaries
}
pub fn user_position(&self) -> &[String] {
&self.user_primaries
}
pub fn user_facet(&self) -> &[&'static str] {
&self.user_facet
}
pub fn flip_position(&self, name: &str) -> String {
if self.internal_primaries.len() != 2 {
return name.to_string();
}
if let Some(rest) = name.strip_prefix("pos1") {
return format!("pos2{}", rest);
}
if let Some(rest) = name.strip_prefix("pos2") {
return format!("pos1{}", rest);
}
name.to_string()
}
}
#[inline]
pub fn is_user_facet_aesthetic(aesthetic: &str) -> bool {
USER_FACET_AESTHETICS.contains(&aesthetic)
}
#[inline]
pub fn is_facet_aesthetic(aesthetic: &str) -> bool {
if aesthetic.starts_with("facet") && aesthetic.len() > 5 {
return aesthetic[5..].chars().all(|c| c.is_ascii_digit());
}
false
}
#[inline]
pub fn is_position_aesthetic(name: &str) -> bool {
if !name.starts_with("pos") || name.len() <= 3 {
return false;
}
let after_pos = &name[3..];
if after_pos.chars().all(|c| c.is_ascii_digit()) {
return true;
}
for suffix in POSITION_SUFFIXES {
if let Some(base) = name.strip_suffix(suffix) {
if base.starts_with("pos") && base.len() > 3 {
let num_part = &base[3..];
if num_part.chars().all(|c| c.is_ascii_digit()) {
return true;
}
}
}
}
false
}
pub fn parse_position(name: &str) -> Option<(u8, &str)> {
if !name.starts_with("pos") {
return None;
}
let rest = &name[3..];
let slot_char = rest.chars().next()?;
let slot = slot_char.to_digit(10)? as u8;
let suffix = &rest[1..];
Some((slot, suffix))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_facet_aesthetic() {
assert!(is_facet_aesthetic("facet1"));
assert!(is_facet_aesthetic("facet2"));
assert!(is_facet_aesthetic("facet10")); assert!(!is_facet_aesthetic("facet")); assert!(!is_facet_aesthetic("facetx"));
assert!(!is_facet_aesthetic("panel"));
assert!(!is_facet_aesthetic("row"));
assert!(!is_facet_aesthetic("column"));
assert!(!is_facet_aesthetic("x"));
assert!(!is_facet_aesthetic("color"));
assert!(!is_facet_aesthetic("pos1"));
}
#[test]
fn test_user_facet_aesthetic() {
assert!(is_user_facet_aesthetic("panel"));
assert!(is_user_facet_aesthetic("row"));
assert!(is_user_facet_aesthetic("column"));
assert!(!is_user_facet_aesthetic("facet1"));
assert!(!is_user_facet_aesthetic("facet2"));
assert!(!is_user_facet_aesthetic("x"));
assert!(!is_user_facet_aesthetic("color"));
}
#[test]
fn test_position_aesthetic() {
assert!(is_position_aesthetic("pos1"));
assert!(is_position_aesthetic("pos2"));
assert!(is_position_aesthetic("pos10"));
assert!(is_position_aesthetic("pos1min"));
assert!(is_position_aesthetic("pos1max"));
assert!(is_position_aesthetic("pos2min"));
assert!(is_position_aesthetic("pos2max"));
assert!(is_position_aesthetic("pos1end"));
assert!(is_position_aesthetic("pos2end"));
assert!(!is_position_aesthetic("x"));
assert!(!is_position_aesthetic("y"));
assert!(!is_position_aesthetic("xmin"));
assert!(!is_position_aesthetic("angle"));
assert!(!is_position_aesthetic("color"));
assert!(!is_position_aesthetic("size"));
assert!(!is_position_aesthetic("fill"));
assert!(!is_position_aesthetic("pos")); assert!(!is_position_aesthetic("position")); }
#[test]
fn test_aesthetic_context_cartesian() {
let ctx = AestheticContext::from_static(&["x", "y"], &[]);
assert_eq!(ctx.user_position(), &["x", "y"]);
let primary: Vec<&str> = ctx.internal_position().iter().map(|s| s.as_str()).collect();
assert_eq!(primary, vec!["pos1", "pos2"]);
}
#[test]
fn test_aesthetic_context_polar() {
let ctx = AestheticContext::from_static(&["angle", "radius"], &[]);
assert_eq!(ctx.user_position(), &["angle", "radius"]);
let primary: Vec<&str> = ctx.internal_position().iter().map(|s| s.as_str()).collect();
assert_eq!(primary, vec!["pos1", "pos2"]);
}
#[test]
fn test_aesthetic_context_user_to_internal() {
let ctx = AestheticContext::from_static(&["x", "y"], &[]);
assert_eq!(ctx.map_user_to_internal("x"), Some("pos1"));
assert_eq!(ctx.map_user_to_internal("y"), Some("pos2"));
assert_eq!(ctx.map_user_to_internal("xmin"), Some("pos1min"));
assert_eq!(ctx.map_user_to_internal("xmax"), Some("pos1max"));
assert_eq!(ctx.map_user_to_internal("xend"), Some("pos1end"));
assert_eq!(ctx.map_user_to_internal("ymin"), Some("pos2min"));
assert_eq!(ctx.map_user_to_internal("ymax"), Some("pos2max"));
assert_eq!(ctx.map_user_to_internal("yend"), Some("pos2end"));
assert_eq!(ctx.map_user_to_internal("color"), None);
assert_eq!(ctx.map_user_to_internal("fill"), None);
}
#[test]
fn test_aesthetic_context_polar_mapping() {
let ctx = AestheticContext::from_static(&["angle", "radius"], &[]);
assert_eq!(ctx.map_user_to_internal("angle"), Some("pos1"));
assert_eq!(ctx.map_user_to_internal("radius"), Some("pos2"));
assert_eq!(ctx.map_user_to_internal("angleend"), Some("pos1end"));
assert_eq!(ctx.map_user_to_internal("radiusmin"), Some("pos2min"));
}
#[test]
fn test_aesthetic_context_is_primary_internal() {
let ctx = AestheticContext::from_static(&["x", "y"], &[]);
assert!(ctx.is_primary_internal("pos1"));
assert!(ctx.is_primary_internal("pos2"));
assert!(!ctx.is_primary_internal("pos1min"));
assert!(!ctx.is_primary_internal("x"));
assert!(!ctx.is_primary_internal("color"));
}
#[test]
fn test_aesthetic_context_with_facets() {
let ctx = AestheticContext::from_static(&["x", "y"], &["panel"]);
assert!(ctx.is_user_facet("panel"));
assert!(!ctx.is_user_facet("row"));
assert_eq!(ctx.user_facet(), &["panel"]);
assert!(ctx.is_internal_facet("facet1"));
assert!(!ctx.is_internal_facet("panel"));
assert_eq!(ctx.map_user_to_internal("panel"), Some("facet1"));
assert!(ctx.is_facet("panel")); assert!(ctx.is_facet("facet1")); }
#[test]
fn test_aesthetic_context_with_grid_facets() {
let ctx = AestheticContext::from_static(&["x", "y"], &["row", "column"]);
assert!(ctx.is_user_facet("row"));
assert!(ctx.is_user_facet("column"));
assert!(!ctx.is_user_facet("panel"));
assert_eq!(ctx.user_facet(), &["row", "column"]);
assert!(ctx.is_internal_facet("facet1"));
assert!(ctx.is_internal_facet("facet2"));
assert_eq!(ctx.map_user_to_internal("row"), Some("facet1"));
assert_eq!(ctx.map_user_to_internal("column"), Some("facet2"));
}
#[test]
fn test_aesthetic_context_families() {
let ctx = AestheticContext::from_static(&["x", "y"], &[]);
let pos1_family = ctx.internal_position_family("pos1").unwrap();
let pos1_strs: Vec<&str> = pos1_family.iter().map(|s| s.as_str()).collect();
assert_eq!(pos1_strs, vec!["pos1", "pos1min", "pos1max", "pos1end"]);
assert_eq!(ctx.primary_internal_position("pos1"), Some("pos1"));
assert_eq!(ctx.primary_internal_position("pos1min"), Some("pos1"));
assert_eq!(ctx.primary_internal_position("pos2end"), Some("pos2"));
assert_eq!(ctx.primary_internal_position("color"), Some("color"));
}
#[test]
fn test_aesthetic_context_internal_to_user_cartesian() {
let ctx = AestheticContext::from_static(&["x", "y"], &[]);
assert_eq!(ctx.map_internal_to_user("pos1"), "x");
assert_eq!(ctx.map_internal_to_user("pos2"), "y");
assert_eq!(ctx.map_internal_to_user("pos1min"), "xmin");
assert_eq!(ctx.map_internal_to_user("pos1max"), "xmax");
assert_eq!(ctx.map_internal_to_user("pos1end"), "xend");
assert_eq!(ctx.map_internal_to_user("pos2min"), "ymin");
assert_eq!(ctx.map_internal_to_user("pos2max"), "ymax");
assert_eq!(ctx.map_internal_to_user("pos2end"), "yend");
assert_eq!(ctx.map_internal_to_user("color"), "color");
assert_eq!(ctx.map_internal_to_user("size"), "size");
assert_eq!(ctx.map_internal_to_user("fill"), "fill");
}
#[test]
fn test_aesthetic_context_internal_to_user_polar() {
let ctx = AestheticContext::from_static(&["angle", "radius"], &[]);
assert_eq!(ctx.map_internal_to_user("pos1"), "angle");
assert_eq!(ctx.map_internal_to_user("pos2"), "radius");
assert_eq!(ctx.map_internal_to_user("pos1end"), "angleend");
assert_eq!(ctx.map_internal_to_user("pos2min"), "radiusmin");
assert_eq!(ctx.map_internal_to_user("pos2max"), "radiusmax");
}
#[test]
fn test_aesthetic_context_internal_to_user_facets() {
let ctx_wrap = AestheticContext::from_static(&["x", "y"], &["panel"]);
assert_eq!(ctx_wrap.map_internal_to_user("facet1"), "panel");
let ctx_grid = AestheticContext::from_static(&["x", "y"], &["row", "column"]);
assert_eq!(ctx_grid.map_internal_to_user("facet1"), "row");
assert_eq!(ctx_grid.map_internal_to_user("facet2"), "column");
}
#[test]
fn test_aesthetic_context_roundtrip() {
let ctx = AestheticContext::from_static(&["x", "y"], &["panel"]);
let internal = ctx.map_user_to_internal("x").unwrap();
assert_eq!(ctx.map_internal_to_user(internal), "x");
let internal = ctx.map_user_to_internal("ymin").unwrap();
assert_eq!(ctx.map_internal_to_user(internal), "ymin");
let internal = ctx.map_user_to_internal("panel").unwrap();
assert_eq!(ctx.map_internal_to_user(internal), "panel");
}
#[test]
fn test_parse_position() {
assert_eq!(parse_position("pos1"), Some((1, "")));
assert_eq!(parse_position("pos2"), Some((2, "")));
assert_eq!(parse_position("pos1min"), Some((1, "min")));
assert_eq!(parse_position("pos2max"), Some((2, "max")));
assert_eq!(parse_position("pos1end"), Some((1, "end")));
assert_eq!(parse_position("color"), None);
assert_eq!(parse_position("x"), None);
assert_eq!(parse_position("xmin"), None);
}
}