diff --git a/Cargo.toml b/Cargo.toml index 94a4fa6d..3556a9bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,7 +125,7 @@ rand_distr = "0.5.1" rand_chacha = "0.9.0" rfd = "0.17.1" rustybuzz = "0.20.1" -serde = { version = "1.0.228" } +serde = "1.0.228" serde_json = "1.0.149" strict-num = "0.2.0" tiny-skia = "0.11.4" diff --git a/base/src/color.rs b/base/src/color.rs index 6889f144..a6f9df9d 100644 --- a/base/src/color.rs +++ b/base/src/color.rs @@ -237,23 +237,23 @@ impl PartialEq for Rgb8 { /// Parsing error for Rgba8 #[derive(Debug)] pub enum ParseError { - InvalidFormat, - InvalidComponent, - InvalidAlphaComponent, - InvalidHex, - UnknownName, - IntError, + InvalidFormat(String), + InvalidComponent(String), + InvalidAlphaComponent(String), + InvalidHex(String), + UnknownName(String), + IntError(String), } impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ParseError::InvalidFormat => write!(f, "invalid color format"), - ParseError::InvalidComponent => write!(f, "invalid color component"), - ParseError::InvalidAlphaComponent => write!(f, "invalid alpha component"), - ParseError::InvalidHex => write!(f, "invalid hex color"), - ParseError::UnknownName => write!(f, "unknown color name"), - ParseError::IntError => write!(f, "integer parse error"), + ParseError::InvalidFormat(s) => write!(f, "invalid color format while parsing {}", s), + ParseError::InvalidComponent(s) => write!(f, "invalid color component: {}", s), + ParseError::InvalidAlphaComponent(s) => write!(f, "invalid alpha component: {}", s), + ParseError::InvalidHex(s) => write!(f, "invalid hex color: {}", s), + ParseError::UnknownName(s) => write!(f, "unknown color name: {}", s), + ParseError::IntError(s) => write!(f, "integer parse error: {}", s), } } } @@ -266,16 +266,18 @@ fn parse_component_0_255(s: &str) -> Result { let val = s[..s.len() - 1] .trim() .parse::() - .map_err(|_| ParseError::InvalidComponent)?; + .map_err(|_| ParseError::InvalidComponent(s.to_string()))?; if !(0.0..=100.0).contains(&val) { - return Err(ParseError::InvalidComponent); + return Err(ParseError::InvalidComponent(s.to_string())); } Ok(((val / 100.0) * 255.0).round().clamp(0.0, 255.0) as u8) } else { // integer 0-255 - let v: i32 = s.parse().map_err(|_| ParseError::InvalidComponent)?; + let v: i32 = s + .parse() + .map_err(|_| ParseError::InvalidComponent(s.to_string()))?; if !(0..=255).contains(&v) { - return Err(ParseError::InvalidComponent); + return Err(ParseError::InvalidComponent(s.to_string())); } Ok(v as u8) } @@ -288,23 +290,25 @@ fn parse_alpha(s: &str) -> Result { let val = s[..s.len() - 1] .trim() .parse::() - .map_err(|_| ParseError::InvalidAlphaComponent)?; + .map_err(|_| ParseError::InvalidAlphaComponent(s.to_string()))?; if !(0.0..=100.0).contains(&val) { - return Err(ParseError::InvalidAlphaComponent); + return Err(ParseError::InvalidAlphaComponent(s.to_string())); } Ok(((val / 100.0) * 255.0).round().clamp(0.0, 255.0) as u8) } else { // try float 0.0-1.0 if let Ok(f) = s.parse::() { if !(0.0..=1.0).contains(&f) { - return Err(ParseError::InvalidAlphaComponent); + return Err(ParseError::InvalidAlphaComponent(s.to_string())); } return Ok((f * 255.0).round().clamp(0.0, 255.0) as u8); } // try integer 0-255 - let v: i32 = s.parse().map_err(|_| ParseError::InvalidAlphaComponent)?; + let v: i32 = s + .parse() + .map_err(|_| ParseError::InvalidAlphaComponent(s.to_string()))?; if !(0..=255).contains(&v) { - return Err(ParseError::InvalidAlphaComponent); + return Err(ParseError::InvalidAlphaComponent(s.to_string())); } Ok(v as u8) } @@ -321,7 +325,7 @@ impl FromStr for Rgb8 { fn from_str(s: &str) -> Result { let raw = s.trim(); if raw.is_empty() { - return Err(ParseError::InvalidFormat); + return Err(ParseError::InvalidFormat(raw.to_string())); } // HTML hex: starts with '#' @@ -333,10 +337,10 @@ impl FromStr for Rgb8 { if bytes[1..].iter().all(|&c| is_hex_char(c)) { Ok(Rgb8::from_hex(bytes)) } else { - Err(ParseError::InvalidHex) + Err(ParseError::InvalidHex(raw.to_string())) } } - _ => Err(ParseError::InvalidHex), + _ => Err(ParseError::InvalidHex(raw.to_string())), } } // rgb(...) @@ -344,7 +348,7 @@ impl FromStr for Rgb8 { let inner = &raw[4..raw.len() - 1]; let parts: Vec<&str> = inner.split(',').collect(); if parts.len() != 3 { - return Err(ParseError::InvalidFormat); + return Err(ParseError::InvalidFormat(raw.to_string())); } let r = parse_component_0_255(parts[0])?; let g = parse_component_0_255(parts[1])?; @@ -355,11 +359,11 @@ impl FromStr for Rgb8 { else { if let Some(col) = names::lookup(raw) { if col.a() != 255 { - return Err(ParseError::InvalidAlphaComponent); + return Err(ParseError::InvalidAlphaComponent(raw.to_string())); } Ok(col.rgb()) } else { - Err(ParseError::UnknownName) + Err(ParseError::UnknownName(raw.to_string())) } } } @@ -376,7 +380,7 @@ impl FromStr for Rgba8 { fn from_str(s: &str) -> Result { let raw = s.trim(); if raw.is_empty() { - return Err(ParseError::InvalidFormat); + return Err(ParseError::InvalidFormat(raw.to_string())); } // HTML hex: starts with '#' @@ -388,10 +392,10 @@ impl FromStr for Rgba8 { if bytes[1..].iter().all(|&c| is_hex_char(c)) { Ok(Rgba8::from_hex(bytes)) } else { - Err(ParseError::InvalidHex) + Err(ParseError::InvalidHex(raw.to_string())) } } - _ => Err(ParseError::InvalidHex), + _ => Err(ParseError::InvalidHex(raw.to_string())), } } // rgb(...) or rgba(...) @@ -399,7 +403,7 @@ impl FromStr for Rgba8 { let inner = &raw[4..raw.len() - 1]; let parts: Vec<&str> = inner.split(',').collect(); if parts.len() != 3 { - return Err(ParseError::InvalidFormat); + return Err(ParseError::InvalidFormat(raw.to_string())); } let r = parse_component_0_255(parts[0])?; let g = parse_component_0_255(parts[1])?; @@ -409,7 +413,7 @@ impl FromStr for Rgba8 { let inner = &raw[5..raw.len() - 1]; let parts: Vec<&str> = inner.split(',').collect(); if parts.len() != 4 { - return Err(ParseError::InvalidFormat); + return Err(ParseError::InvalidFormat(raw.to_string())); } let r = parse_component_0_255(parts[0])?; let g = parse_component_0_255(parts[1])?; @@ -422,7 +426,7 @@ impl FromStr for Rgba8 { if let Some(col) = names::lookup(raw) { Ok(col) } else { - Err(ParseError::UnknownName) + Err(ParseError::UnknownName(raw.to_string())) } } } @@ -866,31 +870,31 @@ mod tests { // empty assert!(matches!( "".parse::(), - Err(ParseError::InvalidFormat) + Err(ParseError::InvalidFormat(_)) )); // invalid hex length assert!(matches!( "#12345".parse::(), - Err(ParseError::InvalidHex) + Err(ParseError::InvalidHex(_)) )); // invalid rgb component (out of 0-255) assert!(matches!( "rgb(300,0,0)".parse::(), - Err(ParseError::InvalidComponent) + Err(ParseError::InvalidComponent(_)) )); // invalid rgba alpha (float > 1.0) assert!(matches!( "rgba(255,0,0,2.0)".parse::(), - Err(ParseError::InvalidAlphaComponent) + Err(ParseError::InvalidAlphaComponent(_)) )); // unknown name assert!(matches!( "notacolor".parse::(), - Err(ParseError::UnknownName) + Err(ParseError::UnknownName(_)) )); } diff --git a/base/src/sd.rs b/base/src/sd.rs index 885afea1..10180639 100644 --- a/base/src/sd.rs +++ b/base/src/sd.rs @@ -5,6 +5,7 @@ use std::str::FromStr; use std::sync::LazyLock; use serde::Serialize; +use serde::de::IntoDeserializer; use serde::ser::SerializeStruct; use crate::color::{css4, xkcd}; @@ -12,29 +13,163 @@ use crate::geom::{Padding, Size}; use crate::style::{Color, DefaultColor, DefaultStroke, DefaultStrokeWidth, Stroke}; use crate::{Rgb8, Rgba8, geom, style}; +/// Macro to deserialize fields from a map, with support for optional fields and default values. +/// +/// It matches 3 types of fields differently: +/// - Option>: The field is optional, and if present, it can be null. +/// - If the field is missing, it will be None. +/// - If the field is present but null, it will be Some(None). +/// - If the field is present and has a value, it will be Some(Some(value)). +/// - Option: The field is optional, and if present, it can be null +/// - If the field is missing, it will be None. +/// - If the field is present and null, it will be None. +/// - If the field is present and has a value, it will be Some(value). +/// - T: The field is required, and if missing, it will return an error. +#[macro_export] macro_rules! deserialize_map_fields { - ($de:lifetime, $map:expr, $($key:expr => $name:ident: Option<$ty:ty>,)+) => { - $( - let mut $name = None::<$ty>; - )+ - - while let Some(key) = $map.next_key::>()? { - match key.as_ref() { - $($key => { + ($de:lifetime, $map:expr, $($fields:tt)+) => { + $crate::deserialize_map_fields!(@parse [$de, $map] [] [] [] [] ; $($fields)+); + }; + + (@parse + [$de:lifetime, $map:expr] + [$($decls:tt)*] + [$($arms:tt)*] + [$($field_names:expr,)*] + [$($binds:tt)*] + ; + $key:expr => $name:ident: Option>, + $($rest:tt)* + ) => { + $crate::deserialize_map_fields!( + @parse + [$de, $map] + [ + $($decls)* + let mut $name = None::>; + ] + [ + $($arms)* + $key => { + if $name.is_some() { + let _: Option<$inner> = $map.next_value()?; + return Err(serde::de::Error::duplicate_field($key)); + } + $name = Some($map.next_value::>()?); + } + ] + [$($field_names,)* $key,] + [ + $($binds)* + let $name = $name; + ] + ; + $($rest)* + ); + }; + + (@parse + [$de:lifetime, $map:expr] + [$($decls:tt)*] + [$($arms:tt)*] + [$($field_names:expr,)*] + [$($binds:tt)*] + ; + $key:expr => $name:ident: Option<$inner:ty>, + $($rest:tt)* + ) => { + $crate::deserialize_map_fields!( + @parse + [$de, $map] + [ + $($decls)* + let mut $name = None::>; + ] + [ + $($arms)* + $key => { + if $name.is_some() { + let _: Option<$inner> = $map.next_value()?; + return Err(serde::de::Error::duplicate_field($key)); + } + $name = Some($map.next_value::>()?); + } + ] + [$($field_names,)* $key,] + [ + $($binds)* + let $name = $name.flatten(); + ] + ; + $($rest)* + ); + }; + + (@parse + [$de:lifetime, $map:expr] + [$($decls:tt)*] + [$($arms:tt)*] + [$($field_names:expr,)*] + [$($binds:tt)*] + ; + $key:expr => $name:ident: $ty:ty, + $($rest:tt)* + ) => { + $crate::deserialize_map_fields!( + @parse + [$de, $map] + [ + $($decls)* + let mut $name = None::<$ty>; + ] + [ + $($arms)* + $key => { if $name.is_some() { let _: $ty = $map.next_value()?; return Err(serde::de::Error::duplicate_field($key)); } $name = Some($map.next_value::<$ty>()?); - })+ + } + ] + [$($field_names,)* $key,] + [ + $($binds)* + let $name = $name.ok_or_else(|| serde::de::Error::missing_field($key))?; + ] + ; + $($rest)* + ); + }; + + (@parse + [$de:lifetime, $map:expr] + [$($decls:tt)*] + [$($arms:tt)*] + [$($field_names:expr,)*] + [$($binds:tt)*] + ; + ) => { + $($decls)* + + while let Some(key) = $map.next_key::>()? { + match key.as_ref() { + $($arms)* _ => { - return Err(serde::de::Error::unknown_field(key.as_ref(), &[$($key),+])); + return Err(serde::de::Error::unknown_field( + key.as_ref(), + &[$($field_names),*], + )); } } } - } + + $($binds)* + }; } +pub use deserialize_map_fields; + // MARK: Color static INVERSE_COLOR_MAP: LazyLock> = LazyLock::new(|| { @@ -131,6 +266,118 @@ impl<'de> serde::Deserialize<'de> for Rgba8 { } } +/// A color that can be deserialized from a "auto" string (yielding None) +#[derive(Debug)] +struct AutoColor(Option); + +impl<'de, C> serde::de::Deserialize<'de> for AutoColor +where + C: serde::de::Deserialize<'de> + std::fmt::Debug, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(AutoColorVisitor { + phantom: std::marker::PhantomData, + }) + } +} + +struct AutoColorVisitor { + phantom: std::marker::PhantomData, +} + +impl<'de, C> serde::de::Visitor<'de> for AutoColorVisitor +where + C: serde::de::Deserialize<'de> + std::fmt::Debug, +{ + type Value = AutoColor; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("'auto' or any value deserializable as the target color type") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + if value == "auto" { + Ok(AutoColor(None)) + } else { + let color = C::deserialize(value.into_deserializer())?; + Ok(AutoColor(Some(color))) + } + } + + fn visit_borrowed_str(self, value: &'de str) -> Result + where + E: serde::de::Error, + { + self.visit_str(value) + } + + fn visit_string(self, value: String) -> Result + where + E: serde::de::Error, + { + if value == "auto" { + Ok(AutoColor(None)) + } else { + let color = C::deserialize(value.into_deserializer())?; + Ok(AutoColor(Some(color))) + } + } + + fn visit_bool(self, value: bool) -> Result + where + E: serde::de::Error, + { + let color = C::deserialize(value.into_deserializer())?; + Ok(AutoColor(Some(color))) + } + + fn visit_i64(self, value: i64) -> Result + where + E: serde::de::Error, + { + let color = C::deserialize(value.into_deserializer())?; + Ok(AutoColor(Some(color))) + } + + fn visit_u64(self, value: u64) -> Result + where + E: serde::de::Error, + { + let color = C::deserialize(value.into_deserializer())?; + Ok(AutoColor(Some(color))) + } + + fn visit_f64(self, value: f64) -> Result + where + E: serde::de::Error, + { + let color = C::deserialize(value.into_deserializer())?; + Ok(AutoColor(Some(color))) + } + + fn visit_seq(self, seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let color = C::deserialize(serde::de::value::SeqAccessDeserializer::new(seq))?; + Ok(AutoColor(Some(color))) + } + + fn visit_map(self, map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let color = C::deserialize(serde::de::value::MapAccessDeserializer::new(map))?; + Ok(AutoColor(Some(color))) + } +} + // MARK: style::Fill impl serde::Serialize for style::Fill @@ -160,7 +407,7 @@ where impl<'de, C> serde::Deserialize<'de> for style::Fill where - C: serde::Deserialize<'de> + Color + FromStr + DefaultColor, + C: serde::Deserialize<'de> + Color + FromStr + DefaultColor + std::fmt::Debug, ::Err: std::fmt::Display, { fn deserialize(deserializer: D) -> Result @@ -179,7 +426,7 @@ struct FillVisitor { impl<'de, C> serde::de::Visitor<'de> for FillVisitor where - C: serde::de::Deserialize<'de> + Color + FromStr + DefaultColor, + C: serde::de::Deserialize<'de> + Color + FromStr + DefaultColor + std::fmt::Debug, ::Err: std::fmt::Display, { type Value = style::Fill; @@ -192,7 +439,11 @@ where where E: serde::de::Error, { - let color = value.parse::().map_err(E::custom)?; + let color = if value == "auto" { + C::default_fill_color().ok_or_else(|| E::custom("No default color available"))? + } else { + value.parse().map_err(E::custom)? + }; Ok(style::Fill::Solid { color, opacity: None, @@ -205,14 +456,19 @@ where { deserialize_map_fields!( 'de, map, - "color" => color: Option, + "color" => color: Option>, "opacity" => opacity: Option, ); - let color = match (color, C::default_color()) { - (Some(color), _) => color, - (None, Some(color)) => color, - (None, None) => return Err(serde::de::Error::missing_field("color")), + let color = match (color, C::default_fill_color()) { + (Some(AutoColor(Some(color))), _) => color, + (_, Some(color)) => color, + (Some(AutoColor(None)), _) => { + return Err(serde::de::Error::custom( + "No default color available for 'auto'", + )); + } + (_, None) => return Err(serde::de::Error::missing_field("color")), }; Ok(style::Fill::Solid { color, opacity }) @@ -306,7 +562,12 @@ where impl<'de, C> serde::de::Deserialize<'de> for Stroke where - C: serde::de::Deserialize<'de> + DefaultStroke + DefaultStrokeWidth + FromStr, + C: serde::de::Deserialize<'de> + + DefaultColor + + DefaultStroke + + DefaultStrokeWidth + + FromStr + + std::fmt::Debug, ::Err: std::fmt::Display, { fn deserialize(deserializer: D) -> Result @@ -437,7 +698,12 @@ impl StrokeVisitor { impl<'de, C> serde::de::Visitor<'de> for StrokeVisitor where - C: serde::de::Deserialize<'de> + DefaultStroke + DefaultStrokeWidth + FromStr, + C: serde::de::Deserialize<'de> + + DefaultColor + + DefaultStroke + + DefaultStrokeWidth + + FromStr + + std::fmt::Debug, ::Err: std::fmt::Display, { type Value = Stroke; @@ -488,8 +754,6 @@ where where E: serde::de::Error, { - let invalid_string_message = self.invalid_string_message(); - if value == "auto" { if let Some(default) = self.default_stroke { Ok(default) @@ -499,7 +763,7 @@ where self.name ))) } - } else if let Some(default) = self.default_stroke { + } else if let Some(ref default) = self.default_stroke { if let Some(pattern) = str_to_line_pattern(value) { return Ok(Self::Value::from(Stroke { color: default.color, @@ -511,18 +775,18 @@ where let color = value .parse() - .map_err(|_| serde::de::Error::custom(invalid_string_message))?; + .map_err(|_| serde::de::Error::custom(self.invalid_string_message()))?; Ok(Self::Value::from(Stroke { color, width: default.width, - pattern: default.pattern, + pattern: default.pattern.clone(), opacity: default.opacity, })) } else { let color = value .parse() - .map_err(|_| serde::de::Error::custom(invalid_string_message))?; + .map_err(|_| serde::de::Error::custom(self.invalid_string_message()))?; Ok(Self::Value::from(Stroke { color, @@ -592,21 +856,38 @@ where { deserialize_map_fields!( 'de, map, - "color" => color: Option, + "color" => color: Option>, "width" => width: Option, "pattern" => pattern: Option, "opacity" => opacity: Option, ); + + let color = match ( + color, + self.default_stroke.as_ref(), + C::default_stroke_color(), + ) { + (Some(AutoColor(Some(color))), _, _) => color, + (_, Some(default), _) => default.color, + (_, _, Some(color)) => color, + (Some(AutoColor(None)), _, _) => { + return Err(serde::de::Error::custom( + "No default color available for 'auto'", + )); + } + (_, _, _) => return Err(serde::de::Error::missing_field("color")), + }; + if let Some(default) = self.default_stroke { Ok(Stroke { - color: color.unwrap_or(default.color), + color, width: width.unwrap_or(default.width), pattern: pattern.unwrap_or(default.pattern), opacity: opacity.or(default.opacity), }) } else { Ok(Stroke { - color: color.ok_or_else(|| serde::de::Error::missing_field("color"))?, + color, width: width.unwrap_or_else(|| C::default_stroke_width()), pattern: pattern.unwrap_or_default(), opacity, diff --git a/base/src/style.rs b/base/src/style.rs index 473ee6f2..e84202c5 100644 --- a/base/src/style.rs +++ b/base/src/style.rs @@ -3,6 +3,15 @@ use crate::Rgba8; /// Trait for color types that have a default value for serialization purposes pub trait DefaultColor: Color { fn default_color() -> Option; + fn default_fill_color() -> Option { + Self::default_color() + } + fn default_stroke_color() -> Option { + Self::default_color() + } + fn default_text_color() -> Option { + Self::default_color() + } } /// Trait for types that have a default stroke for serialization purposes @@ -28,7 +37,7 @@ pub trait ResolveColor { } /// Trait for color types that can be resolved to a concrete color. -pub trait Color: Clone + Copy + From { +pub trait Color: Clone + Copy + From + std::fmt::Debug { #[inline] fn resolve(&self, rc: &R) -> Rgba8 where diff --git a/src/des/axis.rs b/src/des/axis.rs index 5a2fc623..46ecf262 100644 --- a/src/des/axis.rs +++ b/src/des/axis.rs @@ -707,7 +707,7 @@ pub mod ticks { pub struct Ticks { locator: Locator, formatter: Option, - txt_props: text::TextProps, + lbl_props: text::TextProps, color: theme::Color, } @@ -720,7 +720,7 @@ pub mod ticks { Ticks { locator: Locator::default(), formatter: Some(Formatter::default()), - txt_props: text::TextProps::default(), + lbl_props: text::TextProps::default(), color: theme::Col::Foreground.into(), } } @@ -742,8 +742,8 @@ pub mod ticks { Self { formatter, ..self } } /// Returns a new ticks with the specified text properties - pub fn with_font(self, txt_props: text::TextProps) -> Self { - Self { txt_props, ..self } + pub fn with_label_props(self, lbl_props: text::TextProps) -> Self { + Self { lbl_props, ..self } } /// Returns a new ticks with the specified color pub fn with_color(self, color: theme::Color) -> Self { @@ -760,11 +760,11 @@ pub mod ticks { self.formatter.as_ref() } /// Text properties for the ticks labels - pub fn font(&self) -> &text::TextProps { - &self.txt_props + pub fn label_props(&self) -> &text::TextProps { + &self.lbl_props } /// Color for the ticks. - /// Will be used for the labels as well unless a specific color is set in [`font`](Self::font). + /// Will be used for the labels as well unless a specific color is set in [`label_props`](Self::label_props). pub fn color(&self) -> theme::Color { self.color } diff --git a/src/des/sd.rs b/src/des/sd.rs index 0208fd01..db49556a 100644 --- a/src/des/sd.rs +++ b/src/des/sd.rs @@ -1,5 +1,6 @@ //! Serialization and deserialization of figures +use plotive_base::deserialize_map_fields; use serde::ser::{SerializeSeq, SerializeStruct}; use super::Figure; @@ -348,75 +349,157 @@ macro_rules! serialize_tagged_map_variant { pub(crate) use serialize_tagged_map_variant; -macro_rules! deserialize_map_fields { - ($de:lifetime, $map:expr, $($key:expr => $name:ident: Option<$ty:ty>,)+) => { - $( - let mut $name = None::<$ty>; - )+ +/// Internal macro to deserialize tagged map fields. +/// This macro is used to generate code for deserializing fields of a struct from a map, handling both required and optional fields, and checking for duplicate or missing fields. +/// +/// It matches the field type either as a Option or T and generate the appropriate code to handle the deserialization and error checking. +#[macro_export] +macro_rules! deserialize_tagged_map_fields { + ($de:lifetime, $map:expr, $buffered:expr, $($fields:tt)+) => { + $crate::deserialize_tagged_map_fields!( + @parse [$de, $map, $buffered, value] [] [] [] [] [] ; $($fields)+ + ); + }; - while let Some(key) = $map.next_key::>()? { - match key.as_ref() { - $($key => { + (@parse + [$de:lifetime, $map:expr, $buffered:expr, $value:ident] + [$($decls:tt)*] + [$($arms1:tt)*] + [$($arms2:tt)*] + [$($field_names:expr,)*] + [$($binds:tt)*] + ; + $key:expr => $name:ident: Option<$inner:ty>, + $($rest:tt)* + ) => { + $crate::deserialize_tagged_map_fields!( + @parse + [$de, $map, $buffered, $value] + [ + $($decls)* + let mut $name = None::>; + ] + [ + $($arms1)* + $key => { if $name.is_some() { - let _: $ty = $map.next_value()?; - return Err(serde::de::Error::duplicate_field($key)); + return std::result::Result::Err(serde::de::Error::duplicate_field($key)); } - $name = Some($map.next_value::<$ty>()?); - })+ - _ => { - return Err(serde::de::Error::unknown_field(key.as_ref(), &[$($key),+])); - } - } - } - } -} - -pub(crate) use deserialize_map_fields; + $name = std::option::Option::Some( + $value + .deserialize_into::>() + .map_err(serde::de::Error::custom)?, + ); -macro_rules! deserialize_tagged_map_fields { - ($de:lifetime, $map:expr, $buffered:expr, $($key:expr => $name:ident: Option<$ty:ty>,)+) => { - $( - let mut $name = None::<$ty>; - )+ - for (key, value) in $buffered { - match key.as_str() { - "type" => { - return Err(serde::de::Error::duplicate_field("type")); } - $($key => { + ] + [ + $($arms2)* + $key => { if $name.is_some() { + let _: Option<$inner> = $map.next_value()?; return Err(serde::de::Error::duplicate_field($key)); } - $name = Some( - value + $name = Some($map.next_value::>()?); + } + ] + [$($field_names,)* $key,] + [ + $($binds)* + let $name = $name.flatten(); + ] + ; + $($rest)* + ); + }; + + (@parse + [$de:lifetime, $map:expr, $buffered:expr, $value:ident] + [$($decls:tt)*] + [$($arms1:tt)*] + [$($arms2:tt)*] + [$($field_names:expr,)*] + [$($binds:tt)*] + ; + $key:expr => $name:ident: $ty:ty, + $($rest:tt)* + ) => { + $crate::deserialize_tagged_map_fields!( + @parse + [$de, $map, $buffered, $value] + [ + $($decls)* + let mut $name = None::<$ty>; + ] + [ + $($arms1)* + $key => { + if $name.is_some() { + return std::result::Result::Err(serde::de::Error::duplicate_field($key)); + } + $name = std::option::Option::Some( + $value .deserialize_into::<$ty>() .map_err(serde::de::Error::custom)?, ); - })+ - _ => {} - } - } - - while let Some(key) = $map.next_key::>()? { - match key.as_ref() { - "type" => { - let _: String = $map.next_value()?; - return Err(serde::de::Error::duplicate_field("type")); } - $($key => { + ] + [ + $($arms2)* + $key => { if $name.is_some() { let _: $ty = $map.next_value()?; return Err(serde::de::Error::duplicate_field($key)); } $name = Some($map.next_value::<$ty>()?); - })+ + } + ] + [$($field_names,)* $key,] + [ + $($binds)* + let $name = $name.ok_or_else(|| serde::de::Error::missing_field($key))?; + ] + ; + $($rest)* + ); + }; + + (@parse + [$de:lifetime, $map:expr, $buffered:expr, $value:ident] + [$($decls:tt)*] + [$($arms1:tt)*] + [$($arms2:tt)*] + [$($field_names:expr,)*] + [$($binds:tt)*] + ; + ) => { + $($decls)* + + for (key, $value) in $buffered { + match key.as_str() { + "type" => { + return std::result::Result::Err(serde::de::Error::duplicate_field("type")); + } + $($arms1)* + _ => {} + } + } + + while let Some(key) = $map.next_key::>()? { + match key.as_ref() { + $($arms2)* _ => { - return Err(serde::de::Error::unknown_field(key.as_ref(), &[$($key),+])); + return Err(serde::de::Error::unknown_field( + key.as_ref(), + &[$($field_names),+] + )); } } } - } + + $($binds)* + }; } pub(crate) use deserialize_tagged_map_fields; diff --git a/src/des/sd/annot.rs b/src/des/sd/annot.rs index 75db3136..fb48605d 100644 --- a/src/des/sd/annot.rs +++ b/src/des/sd/annot.rs @@ -323,6 +323,7 @@ where annot::Line::two_points(x1, y1, x2, y2) }; + let mut stroke = stroke; if let Some(pattern) = pattern { let current = stroke.unwrap_or_else(|| theme::Stroke::from(theme::Col::Foreground)); stroke = Some(current.with_pattern(pattern)); @@ -352,8 +353,8 @@ where { super::deserialize_tagged_map_fields! { 'de, map, buffered, - "xy" => xy: Option<(f64, f64)>, - "dxy" => dxy: Option<(f32, f32)>, + "xy" => xy: (f64, f64), + "dxy" => dxy: (f32, f32), "headSize" => head_size: Option, "stroke" => stroke: Option, "xAxis" => x_axis: Option, @@ -361,9 +362,6 @@ where "zPos" => z_pos: Option, } - let xy = xy.ok_or_else(|| A::Error::missing_field("xy"))?; - let dxy = dxy.ok_or_else(|| A::Error::missing_field("dxy"))?; - let mut annot = annot::Arrow::new(xy.0, xy.1, dxy.0, dxy.1); if let Some(head_size) = head_size { annot = annot.with_head_size(head_size); @@ -393,15 +391,13 @@ where { super::deserialize_tagged_map_fields! { 'de, map, buffered, - "xy" => xy: Option<(f64, f64)>, + "xy" => xy: (f64, f64), "marker" => marker: Option, "xAxis" => x_axis: Option, "yAxis" => y_axis: Option, "zPos" => z_pos: Option, } - let xy = xy.ok_or_else(|| A::Error::missing_field("xy"))?; - let mut annot = annot::Marker::new(xy.0, xy.1); if let Some(marker) = marker { annot = annot.with_marker(marker) @@ -428,8 +424,8 @@ where { super::deserialize_tagged_map_fields! { 'de, map, buffered, - "xy" => xy: Option<(f64, f64)>, - "text" => text: Option, + "xy" => xy: (f64, f64), + "text" => text: Text, "anchor" => anchor: Option, "frame" => frame: Option<(Option, Option)>, "angle" => angle: Option, @@ -438,9 +434,6 @@ where "zPos" => z_pos: Option, } - let xy = xy.ok_or_else(|| A::Error::missing_field("xy"))?; - let text = text.ok_or_else(|| A::Error::missing_field("text"))?; - let mut annot = annot::Label::new(text, xy.0, xy.1); if let Some(anchor) = anchor { annot = annot.with_anchor(anchor); diff --git a/src/des/sd/axis.rs b/src/des/sd/axis.rs index 8b38b0b1..0c60c9ad 100644 --- a/src/des/sd/axis.rs +++ b/src/des/sd/axis.rs @@ -1,12 +1,14 @@ use std::borrow::Cow; use std::marker::PhantomData; +use plotive_base::deserialize_map_fields; +use plotive_text::TextProps; use serde::de::{Error, SeqAccess}; use serde::ser::{SerializeSeq, SerializeStruct}; use serde::{Deserializer, Serializer}; use serde_value::Value; -use crate::des::sd::{deserialize_map_fields, deserialize_tagged_map_fields}; +use crate::des::sd::deserialize_tagged_map_fields; use crate::des::{self, axis, sd}; use crate::style::theme; @@ -397,32 +399,25 @@ impl serde::Serialize for axis::ticks::Locator { axis::ticks::Locator::DateTime(locator) => { let mut map = serializer.serialize_struct("DateTimeLocator", 2)?; map.serialize_field("type", "datetime")?; - match locator { - axis::ticks::DateTimeLocator::Auto => {} - axis::ticks::DateTimeLocator::Years(years) => { - map.serialize_field("years", years)?; - } - axis::ticks::DateTimeLocator::Months(months) => { - map.serialize_field("months", months)?; - } - axis::ticks::DateTimeLocator::Weeks(weeks) => { - map.serialize_field("weeks", weeks)?; - } - axis::ticks::DateTimeLocator::Days(days) => { - map.serialize_field("days", days)?; - } - axis::ticks::DateTimeLocator::Hours(hours) => { - map.serialize_field("hours", hours)?; - } - axis::ticks::DateTimeLocator::Minutes(minutes) => { - map.serialize_field("minutes", minutes)?; - } - axis::ticks::DateTimeLocator::Seconds(seconds) => { - map.serialize_field("seconds", seconds)?; - } + let period = match locator { + axis::ticks::DateTimeLocator::Auto => None, + axis::ticks::DateTimeLocator::Years(years) => Some((*years, "year")), + axis::ticks::DateTimeLocator::Months(months) => Some((*months, "month")), + axis::ticks::DateTimeLocator::Weeks(weeks) => Some((*weeks, "week")), + axis::ticks::DateTimeLocator::Days(days) => Some((*days, "day")), + axis::ticks::DateTimeLocator::Hours(hours) => Some((*hours, "hour")), + axis::ticks::DateTimeLocator::Minutes(minutes) => Some((*minutes, "min")), + axis::ticks::DateTimeLocator::Seconds(seconds) => Some((*seconds, "sec")), axis::ticks::DateTimeLocator::Micros(micros) => { - map.serialize_field("micros", micros)?; + if micros % 1000 == 0 { + Some((micros / 1000, "milli")) + } else { + Some((*micros, "micro")) + } } + }; + if let Some((value, unit)) = period { + map.serialize_field("period", &(value, unit))?; } map.end() } @@ -430,23 +425,22 @@ impl serde::Serialize for axis::ticks::Locator { axis::ticks::Locator::TimeDelta(locator) => { let mut map = serializer.serialize_struct("TimeDeltaLocator", 2)?; map.serialize_field("type", "timedelta")?; - match locator { - axis::ticks::TimeDeltaLocator::Auto => {} - axis::ticks::TimeDeltaLocator::Days(days) => { - map.serialize_field("days", days)?; - } - axis::ticks::TimeDeltaLocator::Hours(hours) => { - map.serialize_field("hours", hours)?; - } - axis::ticks::TimeDeltaLocator::Minutes(minutes) => { - map.serialize_field("minutes", minutes)?; - } - axis::ticks::TimeDeltaLocator::Seconds(seconds) => { - map.serialize_field("seconds", seconds)?; - } + let period = match locator { + axis::ticks::TimeDeltaLocator::Auto => None, + axis::ticks::TimeDeltaLocator::Days(days) => Some((*days, "day")), + axis::ticks::TimeDeltaLocator::Hours(hours) => Some((*hours, "hour")), + axis::ticks::TimeDeltaLocator::Minutes(minutes) => Some((*minutes, "min")), + axis::ticks::TimeDeltaLocator::Seconds(seconds) => Some((*seconds, "sec")), axis::ticks::TimeDeltaLocator::Micros(micros) => { - map.serialize_field("micros", micros)?; + if micros % 1000 == 0 { + Some((micros / 1000, "milli")) + } else { + Some((*micros, "micro")) + } } + }; + if let Some((value, unit)) = period { + map.serialize_field("period", &(value, unit))?; } map.end() } @@ -604,17 +598,32 @@ fn deserialize_datetime_locator<'de, A>( where A: serde::de::MapAccess<'de>, { - super::deserialize_tagged_enum!( - 'de, map, buffered, axis::ticks::DateTimeLocator, - "years" => Years, - "months" => Months, - "weeks" => Weeks, - "days" => Days, - "hours" => Hours, - "minutes" => Minutes, - "seconds" => Seconds, - "micros" => Micros, - ) + deserialize_tagged_map_fields!( + 'de, map, buffered, + "period" => period: Option<(u32, String)>, + ); + if let Some((period, unit)) = period { + let locator = match unit.as_str() { + "year" => axis::ticks::DateTimeLocator::Years(period), + "month" => axis::ticks::DateTimeLocator::Months(period), + "week" => axis::ticks::DateTimeLocator::Weeks(period), + "day" => axis::ticks::DateTimeLocator::Days(period), + "hour" => axis::ticks::DateTimeLocator::Hours(period), + "min" => axis::ticks::DateTimeLocator::Minutes(period), + "sec" => axis::ticks::DateTimeLocator::Seconds(period), + "milli" => axis::ticks::DateTimeLocator::Micros(period * 1000), + "micro" => axis::ticks::DateTimeLocator::Micros(period), + _ => { + return Err(serde::de::Error::custom(format!( + "invalid datetime locator period unit: {}", + unit + ))); + } + }; + Ok(locator) + } else { + Ok(axis::ticks::DateTimeLocator::Auto) + } } #[cfg(feature = "time")] @@ -625,14 +634,30 @@ fn deserialize_timedelta_locator<'de, A>( where A: serde::de::MapAccess<'de>, { - super::deserialize_tagged_enum!( - 'de, map, buffered, axis::ticks::TimeDeltaLocator, - "days" => Days, - "hours" => Hours, - "minutes" => Minutes, - "seconds" => Seconds, - "micros" => Micros, - ) + deserialize_tagged_map_fields!( + 'de, map, buffered, + "period" => period: Option<(u32, String)>, + ); + + if let Some((period, unit)) = period { + let locator = match unit.as_str() { + "day" => axis::ticks::TimeDeltaLocator::Days(period), + "hour" => axis::ticks::TimeDeltaLocator::Hours(period), + "min" => axis::ticks::TimeDeltaLocator::Minutes(period), + "sec" => axis::ticks::TimeDeltaLocator::Seconds(period), + "milli" => axis::ticks::TimeDeltaLocator::Micros(period * 1000), + "micro" => axis::ticks::TimeDeltaLocator::Micros(period), + _ => { + return Err(serde::de::Error::custom(format!( + "invalid timedelta locator period unit: {}", + unit + ))); + } + }; + Ok(locator) + } else { + Ok(axis::ticks::TimeDeltaLocator::Auto) + } } fn deserialize_locator<'de, A>(type_: &str, mut map: A) -> Result @@ -857,7 +882,7 @@ where } } -// MARK: axis::Ticks +// MARK: Ticks impl serde::Serialize for axis::Ticks { fn serialize(&self, serializer: S) -> Result @@ -871,13 +896,13 @@ impl serde::Serialize for axis::Ticks { let has_default_locator = self.locator() == default.locator(); let has_default_formatter = self.formatter() == default.formatter(); - let has_default_font = self.font() == default.font(); + let has_default_label_props = self.label_props() == default.label_props(); let has_default_color = self.color() == default.color(); match ( has_default_locator, has_default_formatter, - has_default_font, + has_default_label_props, has_default_color, ) { (true, true, true, true) => "auto".serialize(serializer), @@ -896,8 +921,8 @@ impl serde::Serialize for axis::Ticks { if !has_default_formatter { state.serialize_field("formatter", &self.formatter())?; } - if !has_default_font { - state.serialize_field("font", &self.font())?; + if !has_default_label_props { + state.serialize_field("labelProps", &self.label_props())?; } if !has_default_color { state.serialize_field("color", &self.color())?; @@ -976,63 +1001,126 @@ impl<'de> serde::de::Visitor<'de> for TicksVisitor { where A: serde::de::MapAccess<'de>, { + deserialize_map_fields!( + 'de, map, + "locator" => locator: Option, + "formatter" => formatter: Option>, + "labelProps" => label_props: Option>, + "color" => color: Option, + + "type" => type_: Option, + "bins" => bins: Option, + "steps" => steps: Option>, + "base" => base: Option, + "decimals" => decimals: Option, + "format" => format: Option, + "period" => period: Option<(u32, String)>, + ); + let mut ticks = axis::Ticks::default(); - while let Some(key) = map.next_key::>()? { - match &*key { - "locator" => { - let locator = map.next_value()?; - ticks = ticks.with_locator(locator); + if let Some(locator) = locator { + ticks = ticks.with_locator(locator); + } + if let Some(formatter) = formatter { + ticks = ticks.with_formatter(formatter); + } + if let Some(label_props) = label_props { + ticks = ticks.with_label_props(label_props); + } + if let Some(color) = color { + ticks = ticks.with_color(color); + } + + if let Some(type_) = type_ { + match &*type_ { + "maxn" => { + let mut locator = axis::ticks::MaxNLocator::default(); + if let Some(bins) = bins { + locator.bins = bins; + } + if let Some(steps) = steps { + locator.steps = steps; + } + ticks = ticks.with_locator(locator.into()); } - "formatter" => { - let formatter = map.next_value()?; - ticks = ticks.with_formatter(formatter); + "pimultiple" => { + let mut locator = axis::ticks::PiMultipleLocator::default(); + if let Some(bins) = bins { + locator.bins = bins; + } + ticks = ticks.with_locator(locator.into()); } - "font" => { - let font = map.next_value()?; - ticks = ticks.with_font(font); + "log" => { + let mut locator = axis::ticks::LogLocator::default(); + if let Some(base) = base { + locator.base = base; + } + ticks = ticks.with_locator(locator.into()); } - "color" => { - let color = map.next_value()?; - ticks = ticks.with_color(color); + #[cfg(feature = "time")] + "datetime" => { + let mut locator = axis::ticks::DateTimeLocator::Auto; + if let Some((period, unit)) = period { + locator = match unit.as_str() { + "year" => axis::ticks::DateTimeLocator::Years(period), + "month" => axis::ticks::DateTimeLocator::Months(period), + "week" => axis::ticks::DateTimeLocator::Weeks(period), + "day" => axis::ticks::DateTimeLocator::Days(period), + "hour" => axis::ticks::DateTimeLocator::Hours(period), + "min" => axis::ticks::DateTimeLocator::Minutes(period), + "sec" => axis::ticks::DateTimeLocator::Seconds(period), + "milli" => axis::ticks::DateTimeLocator::Micros(period * 1000), + "micro" => axis::ticks::DateTimeLocator::Micros(period), + _ => { + return Err(A::Error::custom(format!( + "invalid datetime locator period unit: {}", + unit + ))); + } + }; + } + ticks = ticks.with_locator(locator.into()); } - // serialized directly as a locator or formatter - "type" => { - let type_: &str = map.next_value()?; - match type_ { - "maxn" | "pimultiple" | "log" => { - ticks = - ticks.with_locator(deserialize_locator(type_, &mut map)?.into()); - } - #[cfg(feature = "time")] - "datetime" | "timedelta" => { - ticks = - ticks.with_locator(deserialize_locator(type_, &mut map)?.into()); - } - "percent" => { - ticks = ticks.with_formatter(Some( - deserialize_percent_formatter(&mut map, Vec::new())?.into(), - )); - } - _ => { - return Err(A::Error::unknown_variant( - type_, - &[ - "maxn", - "pimultiple", - "log", - "timedelta", - "datetime", - "percent", - ], - )); - } + #[cfg(feature = "time")] + "timedelta" => { + let mut locator = axis::ticks::TimeDeltaLocator::Auto; + if let Some((period, unit)) = period { + locator = match unit.as_str() { + "day" => axis::ticks::TimeDeltaLocator::Days(period), + "hour" => axis::ticks::TimeDeltaLocator::Hours(period), + "min" => axis::ticks::TimeDeltaLocator::Minutes(period), + "sec" => axis::ticks::TimeDeltaLocator::Seconds(period), + "milli" => axis::ticks::TimeDeltaLocator::Micros(period * 1000), + "micro" => axis::ticks::TimeDeltaLocator::Micros(period), + _ => { + return Err(A::Error::custom(format!( + "invalid timedelta locator period unit: {}", + unit + ))); + } + }; + } + ticks = ticks.with_locator(locator.into()); + } + "percent" => { + let mut formatter = axis::ticks::PercentFormatter::default(); + if let Some(decimals) = decimals { + formatter.decimal_places = Some(decimals); } + ticks = ticks.with_formatter(Some(formatter.into())); } _ => { - return Err(serde::de::Error::unknown_field( - &key, - &["locator", "formatter", "font", "color", "type"], + return Err(A::Error::unknown_variant( + &type_, + &[ + "maxn", + "pimultiple", + "log", + "timedelta", + "datetime", + "percent", + ], )); } } @@ -1129,8 +1217,8 @@ impl<'de> serde::de::Visitor<'de> for MinorTicksVisitor { } "type" => { // directly a locator - let type_: &str = map.next_value()?; - let locator = deserialize_locator(type_, &mut map)?; + let type_: Cow<'de, str> = map.next_value()?; + let locator = deserialize_locator(&*type_, &mut map)?; minor_ticks = minor_ticks.with_locator(locator); } _ => { @@ -1302,8 +1390,13 @@ trait DeDir { fn dir() -> Dir; } +#[derive(Debug)] pub struct DeX; + +#[derive(Debug)] pub struct DeY; + +#[derive(Debug)] struct DeUnknown; impl DeDir for DeX { @@ -1327,6 +1420,7 @@ impl DeDir for DeUnknown { pub type DeXAxis = DeAxis; pub type DeYAxis = DeAxis; +#[derive(Debug)] pub struct DeAxis { pub axis: axis::Axis, phantom: PhantomData, diff --git a/src/des/sd/cmap.rs b/src/des/sd/cmap.rs index cf1a779d..5667d770 100644 --- a/src/des/sd/cmap.rs +++ b/src/des/sd/cmap.rs @@ -96,72 +96,72 @@ impl<'de> serde::Deserialize<'de> for DeStops { where D: Deserializer<'de>, { - struct StopsVisitor; + deserializer.deserialize_seq(StopsVisitor) + } +} - impl<'de> serde::de::Visitor<'de> for StopsVisitor { - type Value = DeStops; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a sequence of stops") - } +struct StopsVisitor; - fn visit_seq(self, seq: A) -> Result - where - A: SeqAccess<'de>, - { - let mut seq = seq; - let mut de_stops = Vec::with_capacity(seq.size_hint().unwrap_or(0)); - while let Some(de_stop) = seq.next_element::()? { - de_stops.push(de_stop); - } - if de_stops.len() < 2 { - return Err(A::Error::invalid_length(de_stops.len(), &"stops")); - } - let monotonic = de_stops[0].pos.is_none(); - if monotonic { - if !de_stops.iter().skip(1).all(|stop| stop.pos.is_none()) { - return Err(A::Error::custom( - "Can't mix stops with and without position", - )); - } - let start = de_stops[0].color; - let end = de_stops.pop().unwrap().color; - let monotonic_div = 1.0 / (de_stops.len() as f32); - let stops = de_stops - .iter() - .skip(1) - .enumerate() - .map(|(i, stop)| (monotonic_div * (i as f32 + 1.0), stop.color)) - .collect(); - Ok(DeStops { start, end, stops }) - } else { - if !de_stops.iter().skip(1).all(|stop| stop.pos.is_some()) { - return Err(A::Error::custom( - "Can't mix stops with and without position", - )); - } - const EPS: f32 = 0.001; - fn near(value: f32, target: f32) -> bool { - target - EPS < value && value < target + EPS - } - if !near(*de_stops[0].pos.as_ref().unwrap(), 0.0) { - return Err(A::Error::custom("First stop must have position 0.0")); - } - if !near(*de_stops.last().unwrap().pos.as_ref().unwrap(), 1.0) { - return Err(A::Error::custom("Last stop must have position 1.0")); - } - let start = de_stops[0].color; - let end = de_stops.pop().unwrap().color; - let stops = de_stops - .into_iter() - .skip(1) - .map(|stop| (stop.pos.unwrap(), stop.color)) - .collect(); - Ok(DeStops { start, end, stops }) - } +impl<'de> serde::de::Visitor<'de> for StopsVisitor { + type Value = DeStops; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a sequence of stops") + } + + fn visit_seq(self, seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut seq = seq; + let mut de_stops = Vec::with_capacity(seq.size_hint().unwrap_or(0)); + while let Some(de_stop) = seq.next_element::()? { + de_stops.push(de_stop); + } + if de_stops.len() < 2 { + return Err(A::Error::invalid_length(de_stops.len(), &"stops")); + } + let monotonic = de_stops[0].pos.is_none(); + if monotonic { + if !de_stops.iter().skip(1).all(|stop| stop.pos.is_none()) { + return Err(A::Error::custom( + "Can't mix stops with and without position", + )); + } + let start = de_stops[0].color; + let end = de_stops.pop().unwrap().color; + let monotonic_div = 1.0 / (de_stops.len() as f32); + let stops = de_stops + .iter() + .skip(1) + .enumerate() + .map(|(i, stop)| (monotonic_div * (i as f32 + 1.0), stop.color)) + .collect(); + Ok(DeStops { start, end, stops }) + } else { + if !de_stops.iter().skip(1).all(|stop| stop.pos.is_some()) { + return Err(A::Error::custom( + "Can't mix stops with and without position", + )); + } + const EPS: f32 = 0.001; + fn near(value: f32, target: f32) -> bool { + target - EPS < value && value < target + EPS + } + if !near(*de_stops[0].pos.as_ref().unwrap(), 0.0) { + return Err(A::Error::custom("First stop must have position 0.0")); + } + if !near(*de_stops.last().unwrap().pos.as_ref().unwrap(), 1.0) { + return Err(A::Error::custom("Last stop must have position 1.0")); } + let start = de_stops[0].color; + let end = de_stops.pop().unwrap().color; + let stops = de_stops + .into_iter() + .skip(1) + .map(|stop| (stop.pos.unwrap(), stop.color)) + .collect(); + Ok(DeStops { start, end, stops }) } - - deserializer.deserialize_seq(StopsVisitor) } } @@ -176,39 +176,41 @@ impl<'de> serde::Deserialize<'de> for DeStop { where D: Deserializer<'de>, { - struct StopVisitor; - impl<'de> serde::de::Visitor<'de> for StopVisitor { - type Value = DeStop; - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a color stop") - } - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - let color = value.parse::().map_err(E::custom)?; - Ok(DeStop { pos: None, color }) - } - fn visit_seq(self, mut seq: A) -> Result - where - A: SeqAccess<'de>, - { - let pos = seq - .next_element::()? - .ok_or_else(|| serde::de::Error::custom("expected position"))?; - let color = seq - .next_element::()? - .ok_or_else(|| serde::de::Error::custom("expected color"))?; - Ok(DeStop { - pos: Some(pos), - color, - }) - } - } deserializer.deserialize_any(StopVisitor) } } +struct StopVisitor; + +impl<'de> serde::de::Visitor<'de> for StopVisitor { + type Value = DeStop; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a color stop") + } + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + let color = value.parse::().map_err(E::custom)?; + Ok(DeStop { pos: None, color }) + } + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let pos = seq + .next_element::()? + .ok_or_else(|| serde::de::Error::custom("expected position"))?; + let color = seq + .next_element::()? + .ok_or_else(|| serde::de::Error::custom("expected color"))?; + Ok(DeStop { + pos: Some(pos), + color, + }) + } +} + impl serde::Serialize for LerpColorMap { fn serialize(&self, serializer: S) -> Result where @@ -258,6 +260,18 @@ impl<'de> serde::Deserialize<'de> for LerpColorMap { } } + fn visit_seq(self, seq: A) -> Result + where + A: SeqAccess<'de>, + { + let stops = StopsVisitor.visit_seq(seq)?; + + Ok( + LerpColorMap::new(LerpMethod::default(), stops.start, stops.end) + .with_stops(stops.stops), + ) + } + fn visit_map(self, mut map: A) -> Result where A: serde::de::MapAccess<'de>, @@ -265,20 +279,36 @@ impl<'de> serde::Deserialize<'de> for LerpColorMap { super::deserialize_map_fields!( 'de, map, "method" => method: Option, + "cmap" => cmap: Option, "stops" => stops: Option, - "scale" => scale: Option, + "scale" => scale: Option>, ); - let Some(stops) = stops else { - return Err(A::Error::missing_field("stops")); + let mut cmap = if let Some(cmap) = cmap { + if stops.is_some() { + return Err(A::Error::custom("Can't specify both cmap and stops")); + } + if method.is_some() { + return Err(A::Error::custom("Can't specify both cmap and method")); + } + let Some(cmap) = cmap::from_name(&cmap) else { + return Err(A::Error::custom(format!("Unknown ColorMap name: {}", cmap))); + }; + cmap + } else { + let Some(stops) = stops else { + return Err(A::Error::missing_field("stops")); + }; + let method = method.unwrap_or(LerpMethod::default()); + let mut cmap = LerpColorMap::new(method, stops.start, stops.end); + if !stops.stops.is_empty() { + cmap = cmap.with_stops(stops.stops); + } + cmap }; - let method = method.unwrap_or(LerpMethod::default()); - let mut cmap = LerpColorMap::new(method, stops.start, stops.end); - if !stops.stops.is_empty() { - cmap = cmap.with_stops(stops.stops); - } + if let Some(scale) = scale { - cmap = cmap.with_scale(scale); + cmap = cmap.with_scale(scale.unwrap_or_default()); } Ok(cmap) } diff --git a/src/des/sd/legend.rs b/src/des/sd/legend.rs index 4773ca4a..956dd292 100644 --- a/src/des/sd/legend.rs +++ b/src/des/sd/legend.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; +use plotive_base::deserialize_map_fields; use serde::ser::SerializeStruct; use crate::des::{Legend, figure, plot}; @@ -30,6 +31,7 @@ impl<'de> serde::de::Deserialize<'de> for figure::LegendPos { { let s: Cow<'de, str> = serde::Deserialize::deserialize(deserializer)?; match &*s { + "auto" => Ok(figure::LegendPos::default()), "top" => Ok(figure::LegendPos::Top), "right" => Ok(figure::LegendPos::Right), "bottom" => Ok(figure::LegendPos::Bottom), @@ -74,6 +76,7 @@ impl<'de> serde::de::Deserialize<'de> for plot::LegendPos { { let s: Cow<'de, str> = serde::Deserialize::deserialize(deserializer)?; match &*s { + "auto" => Ok(plot::LegendPos::default()), "out-top" => Ok(plot::LegendPos::OutTop), "out-right" => Ok(plot::LegendPos::OutRight), "out-bottom" => Ok(plot::LegendPos::OutBottom), @@ -166,7 +169,7 @@ where impl<'de, P> serde::de::Deserialize<'de> for Legend

where - P: serde::de::Deserialize<'de> + Default, + P: serde::de::Deserialize<'de> + Default + std::fmt::Debug, { fn deserialize(deserializer: D) -> Result where @@ -184,7 +187,7 @@ struct LegendVisitor

{ impl<'de, P> serde::de::Visitor<'de> for LegendVisitor

where - P: serde::de::Deserialize<'de> + Default, + P: serde::de::Deserialize<'de> + Default + std::fmt::Debug, { type Value = Legend

; @@ -210,27 +213,44 @@ where where M: serde::de::MapAccess<'de>, { - let mut legend = Legend::

::default(); - while let Some(key) = map.next_key::>()? { - match &*key { - "pos" => legend = legend.with_pos(map.next_value()?), - "font" => legend = legend.with_font(map.next_value()?), - "fill" => legend = legend.with_fill(map.next_value()?), - "border" => legend = legend.with_border(map.next_value()?), - "columns" => legend = legend.with_columns(map.next_value()?), - "padding" => legend = legend.with_padding(map.next_value()?), - "margin" => legend = legend.with_margin(map.next_value()?), - "spacing" => legend = legend.with_spacing(map.next_value()?), - _ => { - return Err(serde::de::Error::unknown_field( - &key, - &[ - "pos", "font", "fill", "border", "columns", "padding", "margin", - "spacing", - ], - )); - } - } + deserialize_map_fields!( + 'de, map, + "pos" => pos: Option

, + "font" => font: Option>, + "fill" => fill: Option>, + "border" => border: Option>, + "columns" => columns: Option, + "padding" => padding: Option, + "margin" => margin: Option, + "spacing" => spacing: Option, + ); + + let mut legend = if let Some(pos) = pos { + Legend::

::new(pos) + } else { + Legend::

::default() + }; + + if let Some(font) = font { + legend = legend.with_font(font); + } + if let Some(fill) = fill { + legend = legend.with_fill(fill); + } + if let Some(border) = border { + legend = legend.with_border(border); + } + if let Some(columns) = columns { + legend = legend.with_columns(columns); + } + if let Some(padding) = padding { + legend = legend.with_padding(padding); + } + if let Some(margin) = margin { + legend = legend.with_margin(margin); + } + if let Some(spacing) = spacing { + legend = legend.with_spacing(spacing); } Ok(legend) } diff --git a/src/des/sd/plot.rs b/src/des/sd/plot.rs index 5817d4b2..b6c91aa0 100644 --- a/src/des/sd/plot.rs +++ b/src/des/sd/plot.rs @@ -9,11 +9,13 @@ use crate::style::theme; // MARK: Plot +#[derive(Debug)] struct SerPlot<'a> { plot: &'a Plot, subplot: Option<(u32, u32)>, } +#[derive(Debug)] struct DePlot { plot: Plot, subplot: Option<(u32, u32)>, @@ -113,6 +115,7 @@ where Ok(()) } +#[derive(Debug)] struct DeSeries(Vec); impl<'de> serde::Deserialize<'de> for DeSeries { @@ -193,8 +196,9 @@ impl<'de> serde::de::Visitor<'de> for PlotVisitor { { deserialize_map_fields!( 'de, map, + "series" => series: DeSeries, + "subplot" => subplot: Option<(u32, u32)>, - "series" => series: Option, "title" => title: Option, "xAxis" => x_axis: Option, "yAxis" => y_axis: Option, @@ -208,14 +212,11 @@ impl<'de> serde::de::Visitor<'de> for PlotVisitor { "annotations" => annotations: Option>, ); - let Some(series) = series.map(|s| s.0) else { - return Err(serde::de::Error::missing_field("series")); - }; - - let mut plot = Plot::new(series); + let mut plot = Plot::new(series.0); if let Some(title) = title { plot = plot.with_title(title); } + match (x_axis, x_axes) { (Some(x_axis), None) => plot = plot.with_x_axis(x_axis.axis), (None, Some(x_axes)) => { @@ -225,7 +226,7 @@ impl<'de> serde::de::Visitor<'de> for PlotVisitor { } (Some(_), Some(_)) => { return Err(serde::de::Error::custom( - "Both 'x_axis' and 'x_axes' fields cannot be specified at the same time", + "Both 'xAxis' and 'xAxes' fields cannot be specified at the same time", )); } (None, None) => {} @@ -239,7 +240,7 @@ impl<'de> serde::de::Visitor<'de> for PlotVisitor { } (Some(_), Some(_)) => { return Err(serde::de::Error::custom( - "Both 'y_axis' and 'y_axes' fields cannot be specified at the same time", + "Both 'yAxis' and 'yAxes' fields cannot be specified at the same time", )); } (None, None) => {} @@ -264,12 +265,17 @@ impl<'de> serde::de::Visitor<'de> for PlotVisitor { plot = plot.with_annotation(annotation); } } - Ok(DePlot { plot, subplot }) + + Ok(DePlot { + plot, + subplot: subplot, + }) } } // MARK: Subplots +#[derive(Debug)] struct DePlots(Vec); impl serde::Serialize for Subplots { diff --git a/src/des/sd/series.rs b/src/des/sd/series.rs index 956b96ac..ec85d887 100644 --- a/src/des/sd/series.rs +++ b/src/des/sd/series.rs @@ -174,6 +174,7 @@ fn deserialize_datetime_vec( values: &[Value], ) -> Result>, Box> { const FMT: &str = "%Y-%m-%d %H:%M:%S%.f"; + const ISOFMT: &str = "%Y-%m-%dT%H:%M:%S%.f"; values .iter() .map(|v| match v { @@ -181,6 +182,7 @@ fn deserialize_datetime_vec( Value::String(s) => { // Try to parse the string as a DateTime crate::time::DateTime::fmt_parse(s, FMT) + .or_else(|_| crate::time::DateTime::fmt_parse(s, ISOFMT)) .map(Some) .map_err(|e| Box::new(e) as Box) } @@ -377,29 +379,19 @@ where { deserialize_tagged_map_fields! { 'de, map, buffered, - "x" => x_data: Option, - "y" => y_data: Option, + "x" => x_data: series::DataCol, + "y" => y_data: series::DataCol, + + "stroke" => stroke: Option, + "marker" => marker: Option, + "interpolation" => interpolation: Option, + "name" => name: Option, "xAxis" => x_axis: Option, "yAxis" => y_axis: Option, - "stroke" => stroke: Option, - "marker" => marker: Option, - "interpolation" => interpolation: Option , } - let x_data = x_data.ok_or_else(|| serde::de::Error::missing_field("x"))?; - let y_data = y_data.ok_or_else(|| serde::de::Error::missing_field("y"))?; - let mut line = series::Line::new(x_data, y_data); - if let Some(name) = name { - line = line.with_name(name); - } - if let Some(x_axis) = x_axis { - line = line.with_x_axis(x_axis); - } - if let Some(y_axis) = y_axis { - line = line.with_y_axis(y_axis); - } if let Some(marker) = marker { line = line.with_marker(marker); } @@ -410,6 +402,16 @@ where line = line.with_interpolation(interpolation); } + if let Some(name) = name { + line = line.with_name(name); + } + if let Some(x_axis) = x_axis { + line = line.with_x_axis(x_axis); + } + if let Some(y_axis) = y_axis { + line = line.with_y_axis(y_axis); + } + Ok(line) } @@ -465,31 +467,21 @@ where { deserialize_tagged_map_fields! { 'de, map, buffered, - "x" => x_data: Option, - "y" => y_data: Option, - "name" => name: Option, - "xAxis" => x_axis: Option, - "yAxis" => y_axis: Option, - "stroke" => stroke: Option, + "x" => x_data: series::DataCol, + "y" => y_data: series::DataCol, + "marker" => marker: Option, "sizes" => sizes: Option, "colors" => colors: Option, "cmap" => cmap: Option, - } - let x_data = x_data.ok_or_else(|| serde::de::Error::missing_field("x"))?; - let y_data = y_data.ok_or_else(|| serde::de::Error::missing_field("y"))?; + "name" => name: Option, + "xAxis" => x_axis: Option, + "yAxis" => y_axis: Option, + } let mut scatter = series::Scatter::new(x_data, y_data); - if let Some(name) = name { - scatter = scatter.with_name(name); - } - if let Some(x_axis) = x_axis { - scatter = scatter.with_x_axis(x_axis); - } - if let Some(y_axis) = y_axis { - scatter = scatter.with_y_axis(y_axis); - } + if let Some(marker) = marker { scatter = scatter.with_marker(marker); } @@ -501,14 +493,19 @@ where let cmap = cmap.unwrap_or_default(); scatter = scatter.with_color_data(colors, cmap); } - (None, Some(_)) => { - return Err(serde::de::Error::custom( - "Can't provide cmap without colors", - )); - } _ => {} } + if let Some(name) = name { + scatter = scatter.with_name(name); + } + if let Some(x_axis) = x_axis { + scatter = scatter.with_x_axis(x_axis); + } + if let Some(y_axis) = y_axis { + scatter = scatter.with_y_axis(y_axis); + } + Ok(scatter) } @@ -628,22 +625,22 @@ where { deserialize_tagged_map_fields! { 'de, map, buffered, - "x" => x_data: Option, - "y1" => y1_data: Option, + "x" => x_data: series::DataCol, + "y1" => y1_data: series::DataCol, + "y2" => y2_raw: Option, "fill" => fill: Option, "y1Stroke" => y1_stroke: Option, "y2Stroke" => y2_stroke: Option, + "y1Interp" => y1_interp: Option, "y2Interp" => y2_interp: Option, + "name" => name: Option, "xAxis" => x_axis: Option, "yAxis" => y_axis: Option, } - let x_data = x_data.ok_or_else(|| serde::de::Error::missing_field("x"))?; - let y1_data = y1_data.ok_or_else(|| serde::de::Error::missing_field("y1"))?; - let y2_data = match (y2_raw, y2_interp) { (None, _) => series::AreaY2::default(), (Some(AreaY2Raw::Baseline(v)), _) => series::AreaY2::Baseline(v), @@ -651,17 +648,7 @@ where series::AreaY2::DataCol(col, interp.unwrap_or_default()) } }; - let mut area = series::Area::new(x_data, y1_data, y2_data); - if let Some(name) = name { - area = area.with_name(name); - } - if let Some(x_axis) = x_axis { - area = area.with_x_axis(x_axis); - } - if let Some(y_axis) = y_axis { - area = area.with_y_axis(y_axis); - } if let Some(fill) = fill { area = area.with_fill(fill); } @@ -675,6 +662,16 @@ where area = area.with_interpolation(interp); } + if let Some(name) = name { + area = area.with_name(name); + } + if let Some(x_axis) = x_axis { + area = area.with_x_axis(x_axis); + } + if let Some(y_axis) = y_axis { + area = area.with_y_axis(y_axis); + } + Ok(area) } @@ -730,28 +727,19 @@ where { deserialize_tagged_map_fields! { 'de, map, buffered, - "x" => x_data: Option, + "x" => x_data: series::DataCol, + "fill" => fill: Option, "stroke" => stroke: Option, "bins" => bins: Option, "density" => density: Option, + "name" => name: Option, "xAxis" => x_axis: Option, "yAxis" => y_axis: Option, } - let x_data = x_data.ok_or_else(|| serde::de::Error::missing_field("x"))?; - let mut hist = series::Histogram::new(x_data); - if let Some(name) = name { - hist = hist.with_name(name); - } - if let Some(x_axis) = x_axis { - hist = hist.with_x_axis(x_axis); - } - if let Some(y_axis) = y_axis { - hist = hist.with_y_axis(y_axis); - } if let Some(fill) = fill { hist = hist.with_fill(fill); } @@ -765,6 +753,16 @@ where hist = hist.with_density(); } + if let Some(name) = name { + hist = hist.with_name(name); + } + if let Some(x_axis) = x_axis { + hist = hist.with_x_axis(x_axis); + } + if let Some(y_axis) = y_axis { + hist = hist.with_y_axis(y_axis); + } + Ok(hist) } @@ -887,29 +885,19 @@ where { deserialize_tagged_map_fields! { 'de, map, buffered, - "x" => x_data: Option, - "y" => y_data: Option, + "x" => x_data: series::DataCol, + "y" => y_data: series::DataCol, + "fill" => fill: Option, "stroke" => stroke: Option, "position" => position: Option, + "name" => name: Option, "xAxis" => x_axis: Option, "yAxis" => y_axis: Option, } - let x_data = x_data.ok_or_else(|| serde::de::Error::missing_field("x"))?; - let y_data = y_data.ok_or_else(|| serde::de::Error::missing_field("y"))?; - let mut bars = series::Bars::new(x_data, y_data); - if let Some(name) = name { - bars = bars.with_name(name); - } - if let Some(x_axis) = x_axis { - bars = bars.with_x_axis(x_axis); - } - if let Some(y_axis) = y_axis { - bars = bars.with_y_axis(y_axis); - } if let Some(fill) = fill { bars = bars.with_fill(fill); } @@ -920,6 +908,16 @@ where bars = bars.with_position(position); } + if let Some(name) = name { + bars = bars.with_name(name); + } + if let Some(x_axis) = x_axis { + bars = bars.with_x_axis(x_axis); + } + if let Some(y_axis) = y_axis { + bars = bars.with_y_axis(y_axis); + } + Ok(bars) } @@ -1130,14 +1128,14 @@ impl<'de> serde::de::Visitor<'de> for BarSeriesVisitor { { super::deserialize_map_fields!( 'de, map, - "data" => data: Option, + "data" => data: series::DataCol, "name" => name: Option, "fill" => fill: Option, "stroke" => stroke: Option, ); - let data = data.ok_or_else(|| serde::de::Error::missing_field("data"))?; let mut bar_series = series::BarSeries::new(data); + if let Some(name) = name { bar_series = bar_series.with_name(name); } @@ -1198,17 +1196,15 @@ where { deserialize_tagged_map_fields! { 'de, map, buffered, - "categories" => categories: Option, - "series" => bar_series: Option>, + "categories" => categories: series::DataCol, + "series" => bar_series: Vec, + "orientation" => orientation: Option, "arrangement" => arrangement: Option, "xAxis" => x_axis: Option, "yAxis" => y_axis: Option, } - let categories = categories.ok_or_else(|| serde::de::Error::missing_field("categories"))?; - let bar_series = bar_series.ok_or_else(|| serde::de::Error::missing_field("series"))?; - let mut group = series::BarsGroup::new(categories, bar_series); if let Some(orientation) = orientation { group = group.with_orientation(orientation); diff --git a/src/des/sd/style.rs b/src/des/sd/style.rs index 52142dc5..c24e3c16 100644 --- a/src/des/sd/style.rs +++ b/src/des/sd/style.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::style::{self, theme}; -// MARK: style::theme::Color +// MARK: theme::Color impl Serialize for style::theme::Color { fn serialize(&self, serializer: S) -> Result @@ -32,7 +32,6 @@ impl<'de> Deserialize<'de> for style::theme::Color { D: serde::Deserializer<'de>, { let s: Cow<'de, str> = serde::Deserialize::deserialize(deserializer)?; - match s.as_ref() { "background" => Ok(style::theme::Color::Theme(theme::Col::Background)), "foreground" => Ok(style::theme::Color::Theme(theme::Col::Foreground)), @@ -47,7 +46,7 @@ impl<'de> Deserialize<'de> for style::theme::Color { } } -// MARK: style::series::Color +// MARK: series::Color impl Serialize for style::series::Color { fn serialize(&self, serializer: S) -> Result @@ -117,9 +116,7 @@ impl<'de> serde::de::Visitor<'de> for SeriesColorVisitor { } } -/////////////////////////// -// MARK: style::Marker -/////////////////////////// +// MARK: Marker impl Serialize for style::Marker where @@ -286,13 +283,12 @@ where if let Some(opacity) = fill_opacity { marker = marker.with_fill_opacity(opacity); } + Ok(marker) } } -/////////////////////////// -// MARK: style::MarkerShape -/////////////////////////// +// MARK: MarkerShape const SHAPE_STRS: &[&str] = &[ "circle", @@ -358,9 +354,7 @@ impl<'de> Deserialize<'de> for style::MarkerShape { } } -/////////////////////////// -// MARK: style::MarkerSize -/////////////////////////// +// MARK: MarkerSize impl serde::Serialize for style::MarkerSize { fn serialize(&self, serializer: S) -> Result diff --git a/src/des/sd/time.rs b/src/des/sd/time.rs index 5da4f02a..35fef9f9 100644 --- a/src/des/sd/time.rs +++ b/src/des/sd/time.rs @@ -1,6 +1,7 @@ use crate::time; const FMT: &str = "%Y-%m-%d %H:%M:%S%.f"; +const ISOFMT: &str = "%Y-%m-%dT%H:%M:%S%.f"; impl serde::Serialize for time::DateTime { fn serialize(&self, serializer: S) -> Result @@ -26,7 +27,6 @@ impl<'de> serde::Deserialize<'de> for time::DateTime { where E: serde::de::Error, { - println!("parsing '{}' as time", value); time::DateTime::from_timestamp(value as f64) .ok_or_else(|| serde::de::Error::custom("invalid time")) } @@ -35,7 +35,6 @@ impl<'de> serde::Deserialize<'de> for time::DateTime { where E: serde::de::Error, { - println!("parsing '{}' as time", value); time::DateTime::from_timestamp(value) .ok_or_else(|| serde::de::Error::custom("invalid time")) } @@ -44,8 +43,8 @@ impl<'de> serde::Deserialize<'de> for time::DateTime { where E: serde::de::Error, { - println!("parsing '{}' as time", value); time::DateTime::fmt_parse(value, FMT) + .or_else(|_| time::DateTime::fmt_parse(value, ISOFMT)) .map_err(|_| serde::de::Error::custom("invalid time")) } } diff --git a/src/drawing/axis.rs b/src/drawing/axis.rs index 524df40a..33d0d481 100644 --- a/src/drawing/axis.rs +++ b/src/drawing/axis.rs @@ -389,7 +389,10 @@ where } height += missing_params::TICK_SIZE; height += missing_params::TICK_LABEL_MARGIN - + ticks.font().size.unwrap_or(defaults::TICKS_LABEL_FONT_SIZE); + + ticks + .label_props() + .size + .unwrap_or(defaults::TICKS_LABEL_FONT_SIZE); } } let key = AxisCacheKey { @@ -515,10 +518,10 @@ where &self, major_ticks: &des::axis::Ticks, ) -> Result<(text::Font, f32, theme::Color), Error> { - let font_props = major_ticks.font(); + let font_props = major_ticks.label_props(); let font = super::resolve_line_font(font_props, text::Font::default()); let font_size = major_ticks - .font() + .label_props() .size .unwrap_or(defaults::TICKS_LABEL_FONT_SIZE); let color = font_props diff --git a/src/drawing/series.rs b/src/drawing/series.rs index 4f64688e..65c1f7f2 100644 --- a/src/drawing/series.rs +++ b/src/drawing/series.rs @@ -1045,7 +1045,6 @@ impl Area { pb.cubic_to(p1.x, p1.y, p2.x, p2.y, p.x, p.y); } PathSegment::Close => { - println!("Z"); pb.close(); } } diff --git a/src/drawing/ticks.rs b/src/drawing/ticks.rs index 464957e5..19bd44cc 100644 --- a/src/drawing/ticks.rs +++ b/src/drawing/ticks.rs @@ -37,10 +37,8 @@ pub fn locate_num( } #[cfg(feature = "time")] (Locator::DateTime(_), Scale::Auto | Scale::Linear { .. }) => { - Ok(locate_datetime(&locator, nb.into())? - .into_iter() - .map(|dt| dt.timestamp()) - .collect()) + let dt = locate_datetime(&locator, nb.into())?; + Ok(dt.into_iter().map(|dt| dt.timestamp()).collect()) } #[cfg(feature = "time")] (Locator::TimeDelta(loc), Scale::Auto | Scale::Linear { .. }) => { diff --git a/src/style/theme.rs b/src/style/theme.rs index 85fd49b3..ac5f1882 100644 --- a/src/style/theme.rs +++ b/src/style/theme.rs @@ -176,8 +176,8 @@ impl std::str::FromStr for Col { "background" => Ok(Col::Background), "foreground" => Ok(Col::Foreground), "grid" => Ok(Col::Grid), - "legend_fill" => Ok(Col::LegendFill), - "legend_border" => Ok(Col::LegendBorder), + "legend-fill" => Ok(Col::LegendFill), + "legend-border" => Ok(Col::LegendBorder), _ => Err(()), } } @@ -189,8 +189,8 @@ impl std::fmt::Display for Col { Col::Background => "background", Col::Foreground => "foreground", Col::Grid => "grid", - Col::LegendFill => "legend_fill", - Col::LegendBorder => "legend_border", + Col::LegendFill => "legend-fill", + Col::LegendBorder => "legend-border", }; write!(f, "{}", s) } @@ -269,6 +269,18 @@ impl super::DefaultColor for Color { fn default_color() -> Option { None } + + fn default_fill_color() -> Option { + Some(Color::Theme(Col::Background)) + } + + fn default_stroke_color() -> Option { + Some(Color::Theme(Col::Foreground)) + } + + fn default_text_color() -> Option { + Some(Color::Theme(Col::Foreground)) + } } impl super::DefaultStroke for Color {