diff --git a/src/des/cmap.rs b/src/des/cmap.rs index 8f15fbc..d8445c3 100644 --- a/src/des/cmap.rs +++ b/src/des/cmap.rs @@ -305,8 +305,6 @@ pub enum CatColorMap { Auto, /// A categorical color map that uses a predefined set of colors indexed by string categories Strings(HashMap), - /// A categorical color map that uses a predefined set of colors indexed by integer categories - Integers(HashMap), } /// A colormap that interpret string values as literal colors using `Rgb8::parse` and integer values as RGBA 32 bit colors. diff --git a/src/drawing/cmap.rs b/src/drawing/cmap.rs index 28e949d..4cf93c7 100644 --- a/src/drawing/cmap.rs +++ b/src/drawing/cmap.rs @@ -97,9 +97,6 @@ impl ColorMapBuild for des::cmap::ColorMap { .iter() .map(|(cat_val, color)| (cat_val.clone(), *color)) .collect(), - des::cmap::CatColorMap::Integers(..) => { - todo!("Integer category color map is not implemented yet") - } }; Some(Arc::new(CatColorMapImpl { hash: self.hash(bounds), @@ -133,9 +130,6 @@ impl ColorMapBuild for des::cmap::ColorMap { .iter() .map(|(cat_val, color)| (cat_val.clone(), *color)) .collect(), - des::cmap::CatColorMap::Integers(..) => { - todo!("Integer category color map is not implemented yet") - } }; Ok(Arc::new(CatColorMapImpl { hash: self.hash(bounds), @@ -417,14 +411,6 @@ fn hash_cat_cmap(cat: &des::cmap::CatColorMap) -> u64 { } hasher.finish() } - des::cmap::CatColorMap::Integers(map) => { - let mut hasher = DefaultHasher::new(); - for (cat_val, color) in map.iter() { - cat_val.hash(&mut hasher); - color.hash(&mut hasher); - } - hasher.finish() - } } } diff --git a/src/sd/cmap.rs b/src/sd/cmap.rs index 7dccc71..fa218b4 100644 --- a/src/sd/cmap.rs +++ b/src/sd/cmap.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use plotive_base::Rgb8; use serde::Deserializer; -use serde::de::{Error, IntoDeserializer, SeqAccess}; +use serde::de::{Error, SeqAccess}; use serde::ser::{SerializeMap, SerializeSeq}; use crate::des::cmap::{self, CatColorMap, ColorMap, LerpColorMap, LerpMethod}; @@ -63,11 +63,15 @@ where where E: serde::de::Error, { - if let Ok(color) = value.parse::() { + if let Ok(t) = T::deserialize(serde::de::value::StrDeserializer::::new(value)) { + Ok(ColorNoneT::T(t)) + } else if let Ok(color) = value.parse::() { Ok(ColorNoneT::Color(color)) } else { - let t = T::deserialize(value.into_deserializer())?; - Ok(ColorNoneT::T(t)) + Err(E::custom(format!( + "expected a color or one of the LerpColorMap fields, got: {}", + value + ))) } } @@ -94,14 +98,6 @@ where } } -/// Helper type deserialize either a string or an integer as a key for a categorical color map. -#[derive(Debug, serde::Deserialize)] -#[serde(untagged)] -enum CatKey { - String(String), - Integer(i64), -} - impl<'de> serde::de::Deserialize<'de> for ColorMap { fn deserialize(deserializer: D) -> Result where @@ -158,19 +154,9 @@ impl<'de> serde::de::Deserialize<'de> for ColorMap { let mut is_cats = false; let mut cats: HashMap = HashMap::new(); - let mut icats: HashMap = HashMap::new(); - - while let Some(key) = map.next_key::()? { - let str_key = match key { - CatKey::String(s) => s, - CatKey::Integer(i) => { - let val = map.next_value()?; - icats.insert(i, val); - is_cats = true; - continue; - } - }; - match str_key.as_str() { + + while let Some(key) = map.next_key::()? { + match key.as_str() { "method" if !is_cats => { let val: ColorNoneT = map.next_value()?; match val { @@ -241,35 +227,30 @@ impl<'de> serde::de::Deserialize<'de> for ColorMap { } _ => { let val: style::series::Color = map.next_value()?; - cats.insert(str_key, val); + cats.insert(key, val); is_cats = true; } } } - if is_cats { - if is_lerp { - return Err(A::Error::custom( - "Can't mix categorical and lerp color map fields", - )); - } - if !icats.is_empty() && !cats.is_empty() { - return Err(A::Error::custom( - "Can't mix integer and string keys in categorical color map", - )); - } - if !icats.is_empty() { - return Ok(CatColorMap::Integers(icats).into()); - } else { - return Ok(CatColorMap::Strings(cats).into()); - } + if is_cats && is_lerp { + println!("method: {:?}", method); + println!("cmap: {:?}", cmap); + println!("scale: {:?}", scale); + println!("stops: {:?}", stops); + println!("cats: {:?}", cats); + return Err(A::Error::custom( + "Can't mix categorical and lerp color map fields", + )); } - if is_lerp { - return Ok( - lerp_color_map_from_fields::(method, cmap, stops, scale)?.into(), - ); + + if is_cats { + Ok(CatColorMap::Strings(cats).into()) + } else if is_lerp { + Ok(lerp_color_map_from_fields::(method, cmap, stops, scale)?.into()) + } else { + Err(A::Error::custom("Missing fields for ColorMap")) } - Err(A::Error::custom("Missing fields for ColorMap")) } } @@ -613,13 +594,6 @@ impl serde::Serialize for CatColorMap { } state.end() } - CatColorMap::Integers(cmap) => { - let mut state = serializer.serialize_map(Some(cmap.len() + 1))?; - for (key, value) in cmap { - state.serialize_entry(&key.to_string(), value)?; - } - state.end() - } } } } diff --git a/src/sd/style.rs b/src/sd/style.rs index f548e7b..9c4aba6 100644 --- a/src/sd/style.rs +++ b/src/sd/style.rs @@ -3,6 +3,7 @@ use std::marker::PhantomData; use std::str::FromStr; use plotive_base::deserialize_map_fields; +use serde::de::IntoDeserializer; use serde::ser::SerializeMap; use serde::{Deserialize, Serialize}; @@ -203,6 +204,85 @@ where } } +#[derive(Debug)] +enum AutoOrT { + Auto, + T(T), +} + +impl<'de, T> serde::de::Deserialize<'de> for AutoOrT +where + T: serde::de::Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct AutoOrTVisitor { + _phantom: PhantomData, + } + + impl<'de, T> serde::de::Visitor<'de> for AutoOrTVisitor + where + T: serde::de::Deserialize<'de>, + { + type Value = AutoOrT; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a series color string or an index or a color") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + if value == "auto" { + Ok(AutoOrT::Auto) + } else { + let t = T::deserialize(value.into_deserializer())?; + Ok(AutoOrT::T(t)) + } + } + + fn visit_f64(self, v: f64) -> Result + where + E: serde::de::Error, + { + let t = T::deserialize(v.into_deserializer())?; + Ok(AutoOrT::T(t)) + } + + fn visit_i64(self, v: i64) -> Result + where + E: serde::de::Error, + { + let t = T::deserialize(v.into_deserializer())?; + Ok(AutoOrT::T(t)) + } + + fn visit_map(self, map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let t = T::deserialize(serde::de::value::MapAccessDeserializer::new(map))?; + Ok(AutoOrT::T(t)) + } + + fn visit_seq(self, seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let t = T::deserialize(serde::de::value::SeqAccessDeserializer::new(seq))?; + Ok(AutoOrT::T(t)) + } + } + + deserializer.deserialize_any(AutoOrTVisitor { + _phantom: PhantomData, + }) + } +} + struct MarkerVisitor { _phantom: PhantomData, } @@ -252,29 +332,41 @@ where 'de, map, "shape" => shape: Option, "size" => size: Option, - "fill" => fill: Option>>, - "stroke" => stroke: Option>>, + "fill" => fill: Option>>>, + "stroke" => stroke: Option>>>, "color" => color: Option, "fillOpacity" => fill_opacity: Option, ); - let mut marker = style::Marker { - shape: shape.unwrap_or_default(), - size: size.unwrap_or_default(), - fill: fill.unwrap_or_else(|| { + let fill = match fill { + None | Some(Some(AutoOrT::Auto)) => { C::default_stroke_color().map(|color| style::Fill::Solid { color, opacity: None, }) - }), - stroke: stroke.unwrap_or_else(|| { + } + Some(Some(AutoOrT::T(fill))) => Some(fill), + Some(None) => None, + }; + + let stroke = match stroke { + None | Some(Some(AutoOrT::Auto)) => { C::default_stroke_color().map(|color| style::Stroke { color, width: C::default_stroke_width(), pattern: style::LinePattern::default(), opacity: None, }) - }), + } + Some(Some(AutoOrT::T(stroke))) => Some(stroke), + Some(None) => None, + }; + + let mut marker = style::Marker { + shape: shape.unwrap_or_default(), + size: size.unwrap_or_default(), + fill, + stroke, }; if let Some(color) = color { @@ -377,6 +469,8 @@ impl<'de> Deserialize<'de> for style::MarkerSize { #[cfg(test)] mod tests { + use plotive_base::Rgb8; + use super::*; #[test] @@ -471,4 +565,124 @@ mod tests { assert_eq!(json, "\"dash-dot\""); assert_eq!(deserialized, stroke); } + + #[test] + fn deserialize_marker_without_fill_and_stroke() { + let json = r##"{ + "shape": "circle", + "size": 5.0 + }"##; + + let marker: style::Marker = serde_json::from_str(json).unwrap(); + + assert_eq!(marker.shape, style::MarkerShape::Circle); + assert_eq!(marker.size, style::MarkerSize(5.0)); + assert_eq!(marker.fill, Some(style::series::Fill::default())); + assert_eq!(marker.stroke, Some(style::series::Stroke::default())); + } + + #[test] + fn deserialize_marker_with_auto_fill_and_stroke() { + let json = r##"{ + "shape": "circle", + "size": 5.0, + "fill": "auto", + "stroke": "auto" + }"##; + + let marker: style::Marker = serde_json::from_str(json).unwrap(); + + assert_eq!(marker.shape, style::MarkerShape::Circle); + assert_eq!(marker.size, style::MarkerSize(5.0)); + assert_eq!(marker.fill, Some(style::series::Fill::default())); + assert_eq!(marker.stroke, Some(style::series::Stroke::default())); + } + + #[test] + fn deserialize_theme_marker_with_auto_fill_and_stroke() { + let json = r##"{ + "shape": "circle", + "size": 5.0, + "fill": "auto", + "stroke": "auto" + }"##; + + let marker: style::Marker = serde_json::from_str(json).unwrap(); + + let foreground_fill = Some(style::theme::Fill::Solid { + color: style::theme::Color::Theme(style::theme::Col::Foreground), + opacity: None, + }); + let foreground_stroke = Some(style::theme::Stroke { + color: style::theme::Color::Theme(style::theme::Col::Foreground), + width: 1.0, + pattern: style::LinePattern::default(), + opacity: None, + }); + + assert_eq!(marker.shape, style::MarkerShape::Circle); + assert_eq!(marker.size, style::MarkerSize(5.0)); + assert_eq!(marker.fill, foreground_fill); + assert_eq!(marker.stroke, foreground_stroke); + } + + #[test] + fn deserialize_marker_with_null_fill_some_stroke() { + let json = r##"{ + "shape": "circle", + "size": 5.0, + "fill": null, + "stroke": { + "color": "#ff0000", + "width": 1.0 + } + }"##; + + let marker: style::Marker = serde_json::from_str(json).unwrap(); + + assert_eq!(marker.shape, style::MarkerShape::Circle); + assert_eq!(marker.size, style::MarkerSize(5.0)); + assert_eq!(marker.fill, None); + assert_eq!( + marker.stroke, + Some(style::Stroke { + color: style::series::Color::Fixed(Rgb8::new(255, 0, 0).opaque()), + width: 1.0, + pattern: style::LinePattern::default(), + opacity: None, + }) + ); + } + + #[test] + fn deserialize_marker_with_purple_color() { + let json = r##"{ + "shape": "circle", + "size": 5.0, + "color": "purple" + }"##; + + let marker: style::Marker = serde_json::from_str(json).unwrap(); + + let purple: style::series::Color = "purple".parse().unwrap(); + + assert_eq!(marker.shape, style::MarkerShape::Circle); + assert_eq!(marker.size, style::MarkerSize(5.0)); + assert_eq!( + marker.fill, + Some(style::Fill::Solid { + color: purple, + opacity: None, + }) + ); + assert_eq!( + marker.stroke, + Some(style::Stroke { + color: purple, + width: 1.5, + pattern: style::LinePattern::default(), + opacity: None, + }) + ); + } }