diff --git a/Cargo.toml b/Cargo.toml index 3556a9bd..a3b16c50 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 = "1.0.228" +serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" strict-num = "0.2.0" tiny-skia = "0.11.4" diff --git a/base/src/sd.rs b/base/src/sd.rs index 10180639..ef370165 100644 --- a/base/src/sd.rs +++ b/base/src/sd.rs @@ -615,7 +615,6 @@ where ) { (true, true, true, true) => "auto".serialize(serializer), (false, true, true, true) => stroke.color.serialize(serializer), - (true, false, true, true) => stroke.width.serialize(serializer), (true, true, false, true) => stroke.pattern.serialize(serializer), _ => { let fields = (!has_default_color as usize) @@ -681,13 +680,6 @@ impl StrokeVisitor { ) } - fn no_default_numeric_message(&self) -> String { - format!( - "Numeric value is not valid for {} because there is no default stroke defined", - self.name - ) - } - fn no_default_dash_array_message(&self) -> String { format!( "Dash array is not valid for {} because there is no default stroke defined", @@ -712,44 +704,6 @@ where formatter.write_str(self.expecting_description()) } - fn visit_i64(self, value: i64) -> Result - where - E: serde::de::Error, - { - self.visit_f64(value as f64) - } - - fn visit_u64(self, value: u64) -> Result - where - E: serde::de::Error, - { - self.visit_f64(value as f64) - } - - fn visit_f64(self, value: f64) -> Result - where - E: serde::de::Error, - { - let Some(default) = self.default_stroke else { - return Err(serde::de::Error::custom(self.no_default_numeric_message())); - }; - - if value <= 0.0 { - return Err(serde::de::Error::custom(format!( - "Invalid stroke width for {}: width cannot be null or negative", - self.name - ))); - } - - let width = value as f32; - Ok(Stroke { - color: default.color, - width, - pattern: default.pattern, - opacity: default.opacity, - }) - } - fn visit_str(self, value: &str) -> Result where E: serde::de::Error, diff --git a/iced/examples/audio_scope.rs b/iced/examples/audio_scope.rs index 37efcad4..03130a1d 100644 --- a/iced/examples/audio_scope.rs +++ b/iced/examples/audio_scope.rs @@ -399,8 +399,9 @@ fn build_figure() -> des::Figure { .with_title("Frequency (Hz)".to_string().into()) .with_scale(des::axis::Range(Some(0.0), Some(4000.0)).into()) .with_ticks( - des::axis::Ticks::new() - .with_formatter(des::axis::ticks::Formatter::Prec(0).into()), + des::axis::Ticks::new().with_formatter( + des::axis::ticks::Formatter::Decimal(0.into()).into(), + ), ) .with_grid(Default::default()), ) @@ -409,8 +410,9 @@ fn build_figure() -> des::Figure { .with_title("Amplitude (dBFS)".to_string().into()) .with_scale(des::axis::Range(Some(-100.0), Some(0.0)).into()) .with_ticks( - des::axis::Ticks::new() - .with_formatter(des::axis::ticks::Formatter::Prec(0).into()), + des::axis::Ticks::new().with_formatter( + des::axis::ticks::Formatter::Decimal(0.into()).into(), + ), ) .with_grid(Default::default()), ), diff --git a/src/data.rs b/src/data.rs index cd63d572..0f1468fa 100644 --- a/src/data.rs +++ b/src/data.rs @@ -391,6 +391,13 @@ pub trait Column: std::fmt::Debug { /// /// Panics if none of the f64, i64, str, time or time_delta methods return Some. fn boxed_copy(&self) -> Box { + #[cfg(feature = "time")] + if let Some(col) = self.time() { + return Box::new(col.time_iter().collect::>()); + } else if let Some(col) = self.time_delta() { + return Box::new(col.time_delta_iter().collect::>()); + } + if let Some(col) = self.f64() { let mut vec = Vec::with_capacity(col.len()); for v in col.f64_iter() { @@ -407,13 +414,6 @@ pub trait Column: std::fmt::Debug { ); } - #[cfg(feature = "time")] - if let Some(col) = self.time() { - return Box::new(col.time_iter().collect::>()); - } else if let Some(col) = self.time_delta() { - return Box::new(col.time_delta_iter().collect::>()); - } - panic!("Cannot box copy column: no known type"); } diff --git a/src/des.rs b/src/des.rs index ec117bbc..e0d29404 100644 --- a/src/des.rs +++ b/src/des.rs @@ -26,6 +26,9 @@ pub use series::{DataCol, Series, data_inline, data_src_ref}; use crate::style::theme; use crate::text; +/// Rich-Text properties for titles, labels, legends, etc. +pub type TextProps = text::TextProps; + /// Text content for titles, labels, legends, etc. #[derive(Debug, Clone, PartialEq)] pub enum Text { @@ -34,12 +37,12 @@ pub enum Text { /// Rich text, the format string is parsed to produce a rich text, using the standard classes Rich(String), /// Rich text, the format string is parsed to produce a rich text, - /// and the non-standard classes can be used to define the properties of the spans - RichWithClasses { - /// The format string for the rich text, with optional classes + /// and the user defined props can be used to define the properties of the spans + RichWithProps { + /// The format string for the rich text, with optional properties classes fmt: String, - /// The classes that can be used in the format string - classes: Vec<(String, text::TextProps)>, + /// The properties that can be used in the format string + props: Vec<(String, TextProps)>, }, } @@ -60,7 +63,10 @@ impl Text { let builder = parsed_text.into_builder(base).with_layout(layout); builder.done(db) } - Text::RichWithClasses { fmt, classes } => { + Text::RichWithProps { + fmt, + props: classes, + } => { let parsed_text = text::parse_rich_text_with_classes(fmt, &classes)?; let builder = parsed_text.into_builder(base).with_layout(layout); builder.done(db) @@ -147,9 +153,9 @@ impl From<(&str, &str, &str)> for Text { impl From<(String, Vec<(String, text::TextProps)>)> for Text { fn from(tuple: (String, Vec<(String, text::TextProps)>)) -> Self { - Text::RichWithClasses { + Text::RichWithProps { fmt: tuple.0, - classes: tuple.1, + props: tuple.1, } } } diff --git a/src/des/axis.rs b/src/des/axis.rs index 46ecf262..debed92c 100644 --- a/src/des/axis.rs +++ b/src/des/axis.rs @@ -572,7 +572,7 @@ pub mod ticks { /// Same as [Formatter::Auto] for all axes, even those that are shared. SharedAuto, /// Format the ticks with decimal precision - Prec(usize), + Decimal(DecimalFormatter), /// The labels are percentages (E.g. `0.5` will be formatted as `50%`) Percent(PercentFormatter), #[cfg(feature = "time")] @@ -586,7 +586,29 @@ pub mod ticks { TimeDelta(TimeDeltaFormatter), } - /// A label formatter for DateTime ticks + /// A label formatter that formats the ticks with a specified number of decimal places + #[derive(Debug, Clone, Copy, Default, PartialEq)] + pub struct DecimalFormatter { + /// Number of decimal places + /// None means automatic + pub decimal_places: Option, + } + + impl From for Formatter { + fn from(fmt: DecimalFormatter) -> Self { + Formatter::Decimal(fmt) + } + } + + impl From for DecimalFormatter { + fn from(digits: usize) -> Self { + DecimalFormatter { + decimal_places: Some(digits), + } + } + } + + /// A label formatter that convert values to percentage and formats with provided decimal places #[derive(Debug, Clone, Copy, Default, PartialEq)] pub struct PercentFormatter { /// Number of decimal places @@ -600,6 +622,14 @@ pub mod ticks { } } + impl From for PercentFormatter { + fn from(digits: usize) -> Self { + PercentFormatter { + decimal_places: Some(digits), + } + } + } + #[cfg(feature = "time")] /// A label formatter for DateTime ticks #[derive(Debug, Clone, Default, PartialEq)] diff --git a/src/des/sd.rs b/src/des/sd.rs index db49556a..2f28d0f2 100644 --- a/src/des/sd.rs +++ b/src/des/sd.rs @@ -1,7 +1,7 @@ //! Serialization and deserialization of figures use plotive_base::deserialize_map_fields; -use serde::ser::{SerializeSeq, SerializeStruct}; +use serde::ser::SerializeStruct; use super::Figure; use crate::des::{FigLegend, Plot, Subplots, Text, figure}; @@ -16,181 +16,10 @@ mod legend; mod plot; mod series; mod style; +mod text; #[cfg(feature = "time")] mod time; -use crate::text; - -// MARK: Text - -impl serde::Serialize for Text { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self { - Text::Plain(text) => serializer.serialize_str(text), - Text::Rich(fmt) => { - let mut seq = serializer.serialize_seq(None)?; - for l in fmt.lines() { - seq.serialize_element(l)?; - } - seq.end() - } - Text::RichWithClasses { fmt, classes } => { - let mut seq = serializer.serialize_seq(Some(2))?; - seq.serialize_element(fmt)?; - seq.serialize_element(classes)?; - seq.end() - } - } - } -} - -enum TextPropsMapOrString { - Props(Vec<(String, text::TextProps)>), - String(String), -} - -impl<'de> serde::Deserialize<'de> for TextPropsMapOrString { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - struct Visitor; - - impl<'de> serde::de::Visitor<'de> for Visitor { - type Value = TextPropsMapOrString; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a string or a text properties object") - } - - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - Ok(TextPropsMapOrString::String(value.to_string())) - } - - fn visit_map(self, mut map: A) -> Result - where - A: serde::de::MapAccess<'de>, - { - let mut result = Vec::new(); - - while let Some((key, value)) = - map.next_entry::>()? - { - result.push((key, value)); - } - Ok(TextPropsMapOrString::Props(result)) - } - } - - deserializer.deserialize_any(Visitor) - } -} - -impl<'de> serde::Deserialize<'de> for Text { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - struct TextVisitor; - - impl<'de> serde::de::Visitor<'de> for TextVisitor { - type Value = Text; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a string or a rich text array") - } - - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - Ok(Text::Plain(value.to_string())) - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: serde::de::SeqAccess<'de>, - { - let fmt: String = seq - .next_element()? - .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; - let next = seq.next_element::()?; - match next { - Some(TextPropsMapOrString::Props(props)) => Ok(Text::RichWithClasses { - fmt, - classes: props, - }), - Some(TextPropsMapOrString::String(s2)) => { - let mut fmt = fmt + "\n" + &s2; - while let Some(s) = seq.next_element::()? { - fmt.push('\n'); - fmt.push_str(&s); - } - Ok(Text::Rich(fmt)) - } - None => Ok(Text::Rich(fmt)), - } - } - - fn visit_map(self, mut map: A) -> Result - where - A: serde::de::MapAccess<'de>, - { - let mut fmt = Option::::None; - let mut classes = Vec::new(); - while let Some((key, value)) = map.next_entry::()? { - match key.as_str() { - "fmt" => { - if fmt.is_some() { - return Err(serde::de::Error::duplicate_field("fmt")); - } - match value { - TextPropsMapOrString::String(s) => fmt = Some(s), - TextPropsMapOrString::Props(_) => { - return Err(serde::de::Error::custom( - "The 'fmt' field must be a string, not an object", - )); - } - } - } - "classes" => { - if !classes.is_empty() { - return Err(serde::de::Error::duplicate_field("classes")); - } - match value { - TextPropsMapOrString::Props(props) => classes = props, - TextPropsMapOrString::String(_) => { - return Err(serde::de::Error::custom( - "The 'classes' field must be an object, not a string", - )); - } - } - } - _ => { - return Err(serde::de::Error::unknown_field( - key.as_str(), - &["fmt", "classes"], - )); - } - } - } - - let Some(fmt) = fmt else { - return Err(serde::de::Error::missing_field("fmt")); - }; - Ok(Text::RichWithClasses { fmt, classes }) - } - } - deserializer.deserialize_any(TextVisitor) - } -} - // MARK: Figure impl serde::Serialize for Figure { diff --git a/src/des/sd/axis.rs b/src/des/sd/axis.rs index 0c60c9ad..d258dd1e 100644 --- a/src/des/sd/axis.rs +++ b/src/des/sd/axis.rs @@ -401,22 +401,27 @@ impl serde::Serialize for axis::ticks::Locator { map.serialize_field("type", "datetime")?; 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::Years(years) => Some((*years, "years")), + axis::ticks::DateTimeLocator::Months(months) => Some((*months, "months")), + axis::ticks::DateTimeLocator::Weeks(weeks) => Some((*weeks, "weeks")), + axis::ticks::DateTimeLocator::Days(days) => Some((*days, "days")), + axis::ticks::DateTimeLocator::Hours(hours) => Some((*hours, "hours")), + axis::ticks::DateTimeLocator::Minutes(minutes) => Some((*minutes, "mins")), + axis::ticks::DateTimeLocator::Seconds(seconds) => Some((*seconds, "secs")), axis::ticks::DateTimeLocator::Micros(micros) => { if micros % 1000 == 0 { - Some((micros / 1000, "milli")) + Some((micros / 1000, "millis")) } else { - Some((*micros, "micro")) + Some((*micros, "micros")) } } }; if let Some((value, unit)) = period { + let unit = if value <= 1 { + unit.strip_suffix('s').unwrap_or(unit) + } else { + unit + }; map.serialize_field("period", &(value, unit))?; } map.end() @@ -427,19 +432,24 @@ impl serde::Serialize for axis::ticks::Locator { map.serialize_field("type", "timedelta")?; 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::Days(days) => Some((*days, "days")), + axis::ticks::TimeDeltaLocator::Hours(hours) => Some((*hours, "hours")), + axis::ticks::TimeDeltaLocator::Minutes(minutes) => Some((*minutes, "mins")), + axis::ticks::TimeDeltaLocator::Seconds(seconds) => Some((*seconds, "secs")), axis::ticks::TimeDeltaLocator::Micros(micros) => { if micros % 1000 == 0 { - Some((micros / 1000, "milli")) + Some((micros / 1000, "millis")) } else { - Some((*micros, "micro")) + Some((*micros, "micros")) } } }; if let Some((value, unit)) = period { + let unit = if value <= 1 { + unit.strip_suffix('s').unwrap_or(unit) + } else { + unit + }; map.serialize_field("period", &(value, unit))?; } map.end() @@ -604,15 +614,15 @@ where ); 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), + "year" | "years" => axis::ticks::DateTimeLocator::Years(period), + "month" | "months" => axis::ticks::DateTimeLocator::Months(period), + "week" | "weeks" => axis::ticks::DateTimeLocator::Weeks(period), + "day" | "days" => axis::ticks::DateTimeLocator::Days(period), + "hour" | "hours" => axis::ticks::DateTimeLocator::Hours(period), + "min" | "mins" => axis::ticks::DateTimeLocator::Minutes(period), + "sec" | "secs" => axis::ticks::DateTimeLocator::Seconds(period), + "milli" | "millis" => axis::ticks::DateTimeLocator::Micros(period * 1000), + "micro" | "micros" => axis::ticks::DateTimeLocator::Micros(period), _ => { return Err(serde::de::Error::custom(format!( "invalid datetime locator period unit: {}", @@ -641,12 +651,12 @@ where 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), + "day" | "days" => axis::ticks::TimeDeltaLocator::Days(period), + "hour" | "hours" => axis::ticks::TimeDeltaLocator::Hours(period), + "min" | "mins" => axis::ticks::TimeDeltaLocator::Minutes(period), + "sec" | "secs" => axis::ticks::TimeDeltaLocator::Seconds(period), + "milli" | "millis" => axis::ticks::TimeDeltaLocator::Micros(period * 1000), + "micro" | "micros" => axis::ticks::TimeDeltaLocator::Micros(period), _ => { return Err(serde::de::Error::custom(format!( "invalid timedelta locator period unit: {}", @@ -689,11 +699,15 @@ impl serde::Serialize for axis::ticks::Formatter { match self { axis::ticks::Formatter::Auto => "auto".serialize(serializer), axis::ticks::Formatter::SharedAuto => "shared-auto".serialize(serializer), - axis::ticks::Formatter::Prec(prec) => { - let mut map = serializer.serialize_struct("PrecFormatter", 2)?; - map.serialize_field("type", "prec")?; - map.serialize_field("digits", prec)?; - map.end() + axis::ticks::Formatter::Decimal(formatter) => { + if let Some(decimals) = formatter.decimal_places { + let mut map = serializer.serialize_struct("PrecFormatter", 2)?; + map.serialize_field("type", "decimal")?; + map.serialize_field("decimals", &decimals)?; + map.end() + } else { + "decimal".serialize(serializer) + } } axis::ticks::Formatter::Percent(formatter) => { if let Some(decimals) = formatter.decimal_places { @@ -755,7 +769,23 @@ impl<'de> serde::de::Visitor<'de> for FormatterVisitor { match value { "auto" => Ok(axis::ticks::Formatter::Auto), "shared-auto" => Ok(axis::ticks::Formatter::SharedAuto), - other => Err(E::unknown_variant(other, &["auto", "shared-auto"])), + "decimal" => Ok(axis::ticks::Formatter::Decimal(Default::default())), + "percent" => Ok(axis::ticks::Formatter::Percent(Default::default())), + #[cfg(feature = "time")] + "datetime" => Ok(axis::ticks::Formatter::DateTime(Default::default())), + #[cfg(feature = "time")] + "timedelta" => Ok(axis::ticks::Formatter::TimeDelta(Default::default())), + other => Err(E::unknown_variant( + other, + &[ + "auto", + "shared-auto", + "decimal", + "percent", + "datetime", + "timedelta", + ], + )), } } @@ -769,10 +799,12 @@ impl<'de> serde::de::Visitor<'de> for FormatterVisitor { if key == "type" { let tag = map.next_value::()?; return match tag.as_str() { - "prec" => deserialize_prec_formatter(&mut map, buffered) - .map(axis::ticks::Formatter::Prec), + "auto" => Ok(axis::ticks::Formatter::Auto), + "shared-auto" => Ok(axis::ticks::Formatter::SharedAuto), "percent" => deserialize_percent_formatter(&mut map, buffered) .map(axis::ticks::Formatter::Percent), + "decimal" => deserialize_decimal_formatter(&mut map, buffered) + .map(axis::ticks::Formatter::Decimal), #[cfg(feature = "time")] "datetime" => deserialize_datetime_formatter(&mut map, buffered) .map(axis::ticks::Formatter::DateTime), @@ -781,7 +813,14 @@ impl<'de> serde::de::Visitor<'de> for FormatterVisitor { .map(axis::ticks::Formatter::TimeDelta), _ => Err(serde::de::Error::unknown_variant( &tag, - &["prec", "percent", "datetime", "timedelta"], + &[ + "auto", + "shared-auto", + "decimal", + "percent", + "datetime", + "timedelta", + ], )), }; } @@ -794,21 +833,18 @@ impl<'de> serde::de::Visitor<'de> for FormatterVisitor { } } -fn deserialize_prec_formatter<'de, A>( +fn deserialize_decimal_formatter<'de, A>( map: &mut A, buffered: Vec<(String, Value)>, -) -> Result +) -> Result where A: serde::de::MapAccess<'de>, { deserialize_tagged_map_fields!( 'de, map, buffered, - "digits" => digits: Option, + "decimals" => decimal_places: Option, ); - let Some(prec) = digits else { - return Err(serde::de::Error::missing_field("digits")); - }; - Ok(prec) + Ok(axis::ticks::DecimalFormatter { decimal_places }) } fn deserialize_percent_formatter<'de, A>( @@ -965,6 +1001,8 @@ impl<'de> serde::de::Visitor<'de> for TicksVisitor { "log" => { Ok(axis::Ticks::default().with_locator(axis::ticks::LogLocator::default().into())) } + "decimal" => Ok(axis::Ticks::default() + .with_formatter(Some(axis::ticks::DecimalFormatter::default().into()))), "percent" => Ok(axis::Ticks::default() .with_formatter(Some(axis::ticks::PercentFormatter::default().into()))), _ => { @@ -976,6 +1014,7 @@ impl<'de> serde::de::Visitor<'de> for TicksVisitor { "maxn", "pimultiple", "log", + "decimal", "percent", "[color string]", ], @@ -1063,15 +1102,17 @@ impl<'de> serde::de::Visitor<'de> for TicksVisitor { 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), + "year" | "years" => axis::ticks::DateTimeLocator::Years(period), + "month" | "months" => axis::ticks::DateTimeLocator::Months(period), + "week" | "weeks" => axis::ticks::DateTimeLocator::Weeks(period), + "day" | "days" => axis::ticks::DateTimeLocator::Days(period), + "hour" | "hours" => axis::ticks::DateTimeLocator::Hours(period), + "min" | "mins" => axis::ticks::DateTimeLocator::Minutes(period), + "sec" | "secs" => axis::ticks::DateTimeLocator::Seconds(period), + "milli" | "millis" => { + axis::ticks::DateTimeLocator::Micros(period * 1000) + } + "micro" | "micros" => axis::ticks::DateTimeLocator::Micros(period), _ => { return Err(A::Error::custom(format!( "invalid datetime locator period unit: {}", @@ -1087,12 +1128,14 @@ impl<'de> serde::de::Visitor<'de> for TicksVisitor { 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), + "day" | "days" => axis::ticks::TimeDeltaLocator::Days(period), + "hour" | "hours" => axis::ticks::TimeDeltaLocator::Hours(period), + "min" | "mins" => axis::ticks::TimeDeltaLocator::Minutes(period), + "sec" | "secs" => axis::ticks::TimeDeltaLocator::Seconds(period), + "milli" | "millis" => { + axis::ticks::TimeDeltaLocator::Micros(period * 1000) + } + "micro" | "micros" => axis::ticks::TimeDeltaLocator::Micros(period), _ => { return Err(A::Error::custom(format!( "invalid timedelta locator period unit: {}", diff --git a/src/des/sd/style.rs b/src/des/sd/style.rs index c24e3c16..77185620 100644 --- a/src/des/sd/style.rs +++ b/src/des/sd/style.rs @@ -386,13 +386,6 @@ mod tests { assert_eq!(stroke, style::series::Stroke::default()); } - #[test] - fn deserialize_series_stroke_number_uses_default_color() { - let stroke: style::series::Stroke = serde_json::from_str("2.5").unwrap(); - - assert_eq!(stroke, style::series::Stroke::default().with_width(2.5),); - } - #[test] fn deserialize_theme_stroke_auto_still_fails_without_default() { let err = serde_json::from_str::("\"auto\"").unwrap_err(); @@ -423,16 +416,6 @@ mod tests { ); } - #[test] - fn deserialize_theme_stroke_number_without_default_has_precise_message() { - let err = serde_json::from_str::("2.5").unwrap_err(); - - assert_eq!( - err.to_string(), - "Numeric value is not valid for Stroke because there is no default stroke defined at line 1 column 3", - ); - } - #[test] fn deserialize_theme_stroke_dash_array_without_default_has_precise_message() { let err = serde_json::from_str::("[2.0,3.0]").unwrap_err(); @@ -450,24 +433,6 @@ mod tests { assert_eq!(json, "\"auto\""); } - #[test] - fn serialize_series_stroke_width_only_uses_number() { - let json = - serde_json::to_string(&style::series::Stroke::default().with_width(2.5)).unwrap(); - - assert_eq!(json, "2.5"); - } - - #[test] - fn serialize_series_stroke_roundtrip_width_only() { - let stroke = style::series::Stroke::default().with_width(2.5); - let json = serde_json::to_string(&stroke).unwrap(); - let deserialized: style::series::Stroke = serde_json::from_str(&json).unwrap(); - - assert_eq!(json, "2.5"); - assert_eq!(deserialized, stroke); - } - #[test] fn serialize_theme_stroke_without_default_stays_color_string() { let stroke: style::theme::Stroke = theme::Col::Foreground.into(); diff --git a/src/des/sd/text.rs b/src/des/sd/text.rs new file mode 100644 index 00000000..23e8b85a --- /dev/null +++ b/src/des/sd/text.rs @@ -0,0 +1,217 @@ +use des::Text; +use serde::ser::{SerializeMap, SerializeSeq}; + +use crate::des; + +#[derive(Debug)] +struct SerPropsMap<'a>(&'a [(String, des::TextProps)]); + +#[derive(Debug)] +struct DePropsMap(Vec<(String, des::TextProps)>); + +impl<'a> serde::Serialize for SerPropsMap<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(Some(self.0.len()))?; + for (key, value) in self.0 { + map.serialize_entry(key, value)?; + } + map.end() + } +} + +impl<'de> serde::Deserialize<'de> for DePropsMap { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = DePropsMap; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a text properties object") + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut result = Vec::new(); + + while let Some((key, value)) = map.next_entry::()? { + result.push((key, value)); + } + Ok(DePropsMap(result)) + } + } + + deserializer.deserialize_map(Visitor) + } +} + +#[derive(Debug, serde::Deserialize)] +#[serde(untagged)] +enum StringOrPropsMap { + String(String), + Props(DePropsMap), +} + +#[derive(Debug, serde::Deserialize)] +#[serde(untagged)] +enum StringOrVecString { + String(String), + VecString(Vec), +} + +// impl<'de> serde::Deserialize<'de> for PropsMapOrString { +// fn deserialize(deserializer: D) -> Result +// where +// D: serde::Deserializer<'de>, +// { +// struct Visitor; + +// impl<'de> serde::de::Visitor<'de> for Visitor { +// type Value = PropsMapOrString; + +// fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { +// formatter.write_str("a string or a text properties object") +// } + +// fn visit_str(self, value: &str) -> Result +// where +// E: serde::de::Error, +// { +// Ok(PropsMapOrString::String(value.to_string())) +// } + +// fn visit_map(self, map: A) -> Result +// where +// A: serde::de::MapAccess<'de>, +// { +// let DePropsMap(props) = PropsMapVisitor.visit_map(map)?; +// Ok(PropsMapOrString::Props(props)) +// } +// } + +// deserializer.deserialize_any(Visitor) +// } +// } + +impl serde::Serialize for Text { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Text::Plain(text) => serializer.serialize_str(text), + Text::Rich(fmt) => { + let mut seq = serializer.serialize_seq(None)?; + for l in fmt.lines() { + seq.serialize_element(l)?; + } + seq.end() + } + Text::RichWithProps { fmt, props } => { + let mut seq = serializer.serialize_seq(Some(2))?; + seq.serialize_element(fmt)?; + let props = SerPropsMap(props.as_slice()); + seq.serialize_element(&props)?; + seq.end() + } + } + } +} + +impl<'de> serde::Deserialize<'de> for Text { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct TextVisitor; + + impl<'de> serde::de::Visitor<'de> for TextVisitor { + type Value = Text; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string or a rich text array") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + Ok(Text::Plain(value.to_string())) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let fmt: String = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; + + let next = seq.next_element::()?; + match next { + Some(StringOrPropsMap::String(s2)) => { + let mut fmt = fmt + "\n" + &s2; + while let Some(s) = seq.next_element::()? { + fmt.push('\n'); + fmt.push_str(&s); + } + Ok(Text::Rich(fmt)) + } + Some(StringOrPropsMap::Props(DePropsMap(props))) => { + Ok(Text::RichWithProps { fmt, props }) + } + None => Ok(Text::Rich(fmt)), + } + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut fmt = Option::::None; + let mut props = Vec::new(); + while let Some(key) = map.next_key::>()? { + match key.as_ref() { + "fmt" => { + if fmt.is_some() { + return Err(serde::de::Error::duplicate_field("fmt")); + } + let defmt = map.next_value::()?; + fmt = match defmt { + StringOrVecString::String(s) => Some(s), + StringOrVecString::VecString(vec) => Some(vec.join("\n")), + }; + } + "props" => { + if !props.is_empty() { + return Err(serde::de::Error::duplicate_field("props")); + } + let DePropsMap(map) = map.next_value::()?; + props = map; + } + _ => { + return Err(serde::de::Error::unknown_field( + key.as_ref(), + &["fmt", "props"], + )); + } + } + } + + let Some(fmt) = fmt else { + return Err(serde::de::Error::missing_field("fmt")); + }; + Ok(Text::RichWithProps { fmt, props }) + } + } + deserializer.deserialize_any(TextVisitor) + } +} diff --git a/src/drawing/ticks.rs b/src/drawing/ticks.rs index 19bd44cc..11bed0dc 100644 --- a/src/drawing/ticks.rs +++ b/src/drawing/ticks.rs @@ -502,7 +502,10 @@ pub fn num_label_formatter( Some(Formatter::Auto | Formatter::SharedAuto) => { auto_label_formatter(locator, formatter, ab, scale) } - Some(Formatter::Prec(prec)) => Arc::new(PrecLabelFormat(*prec)), + Some(Formatter::Decimal(fmt)) => Arc::new(DecimalLabelFormat( + fmt.decimal_places + .unwrap_or_else(|| decimal_auto_precision(ab)), + )), Some(Formatter::Percent(fmt)) => { let prec = fmt .decimal_places @@ -534,11 +537,11 @@ fn auto_label_formatter( if max >= 100000.0 || max < 0.001 { Arc::new(SciLabelFormat) } else if max >= 100.0 { - Arc::new(PrecLabelFormat(0)) + Arc::new(DecimalLabelFormat(0)) } else if max >= 10.0 { - Arc::new(PrecLabelFormat(1)) + Arc::new(DecimalLabelFormat(1)) } else { - Arc::new(PrecLabelFormat(2)) + Arc::new(DecimalLabelFormat(2)) } } #[cfg(feature = "time")] @@ -551,6 +554,19 @@ fn auto_label_formatter( } } +fn decimal_auto_precision(ab: axis::NumBounds) -> usize { + let span = ab.span(); + if span >= 100.0 { + 0 + } else if span >= 10.0 { + 1 + } else if span >= 1.0 { + 2 + } else { + 3 + } +} + fn percent_auto_precision(ab: axis::NumBounds) -> usize { let span = ab.span(); if span >= 1.0 { @@ -607,9 +623,9 @@ fn auto_datetime_label_formatter(tb: axis::TimeBounds) -> Result String { let data = data.as_num().unwrap(); format!("{data:.*}", self.0)