From 13b612bdca47a763fe13a7c33aa838f630c3e031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Thebault?= Date: Fri, 13 Mar 2026 18:04:20 +0100 Subject: [PATCH 1/3] remove warning From 5638d145f1b8e737b7d1bf5930a6837458e92b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Thebault?= Date: Fri, 17 Apr 2026 22:45:09 +0200 Subject: [PATCH 2/3] test DateTime format --- src/data.rs | 16 +++++++++++ src/des/series.rs | 16 +++++++++++ tests/Cargo.toml | 2 +- tests/refs/axes/datetime-locator.png | Bin 0 -> 24160 bytes tests/refs/axes/datetime-locator.svg | 16 +++++++++++ tests/src/tests/axes.rs | 41 +++++++++++++++++++++++++++ 6 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 tests/refs/axes/datetime-locator.png create mode 100644 tests/refs/axes/datetime-locator.svg diff --git a/src/data.rs b/src/data.rs index 5a6bafc..ac426ed 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1484,6 +1484,22 @@ impl From> for VecColumn { } } +#[cfg(feature = "time")] +impl From> for VecColumn { + fn from(v: Vec) -> Self { + let v: Vec> = v.into_iter().map(Some).collect(); + VecColumn::Time(v) + } +} + +#[cfg(feature = "time")] +impl From> for VecColumn { + fn from(v: Vec) -> Self { + let v: Vec> = v.into_iter().map(Some).collect(); + VecColumn::TimeDelta(v) + } +} + impl Column for VecColumn { fn len(&self) -> usize { match self { diff --git a/src/des/series.rs b/src/des/series.rs index ab059e5..755ad6a 100644 --- a/src/des/series.rs +++ b/src/des/series.rs @@ -2,6 +2,8 @@ use crate::data; use crate::des::axis; use crate::style::{self, defaults}; +#[cfg(feature = "time")] +use crate::time; /// A data column, either inline or a reference to a data source. /// @@ -59,6 +61,20 @@ impl From> for DataCol { } } +#[cfg(feature = "time")] +impl From> for DataCol { + fn from(col: Vec) -> Self { + DataCol::Inline(col.into()) + } +} + +#[cfg(feature = "time")] +impl From> for DataCol { + fn from(col: Vec) -> Self { + DataCol::Inline(col.into()) + } +} + /// A data series to be plotted in a plot. /// /// This enum represents the different types of series that can be visualized. diff --git a/tests/Cargo.toml b/tests/Cargo.toml index bae5bca..e2475c5 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -10,7 +10,7 @@ keywords.workspace = true publish = false [dependencies] -plotive.workspace = true +plotive = { workspace = true, features=["time"] } plotive-pxl.workspace = true plotive-svg.workspace = true tiny-skia.workspace = true diff --git a/tests/refs/axes/datetime-locator.png b/tests/refs/axes/datetime-locator.png new file mode 100644 index 0000000000000000000000000000000000000000..74f963d12e1c085054aceef93f9bdf1e1eceabcb GIT binary patch literal 24160 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 literal 0 HcmV?d00001 diff --git a/tests/refs/axes/datetime-locator.svg b/tests/refs/axes/datetime-locator.svg new file mode 100644 index 0000000..70630bd --- /dev/null +++ b/tests/refs/axes/datetime-locator.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/src/tests/axes.rs b/tests/src/tests/axes.rs index 94d4161..df51671 100644 --- a/tests/src/tests/axes.rs +++ b/tests/src/tests/axes.rs @@ -364,3 +364,44 @@ fn axes_multiple_trbl_titles() { assert_fig_eq_ref!(&fig, "axes/multiple-trbl-titles"); } + +#[test] +fn axes_datetime_locator() { + use plotive::time; + + let start = time::DateTime::fmt_parse("2020-01-01", "%Y-%m-%d").unwrap(); + let x = (0..10) + .map(|i| start + time::TimeDelta::from_days(i as f64)) + .collect::>(); + let y = (0..10).map(|i| 1.0 / (i as f64 + 1.0)).collect::>(); + + let series = des::series::Line::new(x.into(), y.into()); + + let plot = des::Plot::new(vec![series.into()]).with_x_axis(des::Axis::new().with_ticks( + des::axis::Ticks::new().with_locator(des::axis::ticks::DateTimeLocator::Days(2).into()), + )); + let fig = fig_small(plot); + + assert_fig_eq_ref!(&fig, "axes/datetime-locator"); +} + +#[test] +fn axes_num_datetime_locator() { + use plotive::time; + + let start = time::DateTime::fmt_parse("2020-01-01", "%Y-%m-%d").unwrap(); + let x = (0..10) + .map(|i| start + time::TimeDelta::from_days(i as f64)) + .map(|dt| dt.timestamp()) + .collect::>(); + let y = (0..10).map(|i| 1.0 / (i as f64 + 1.0)).collect::>(); + + let series = des::series::Line::new(x.into(), y.into()); + + let plot = des::Plot::new(vec![series.into()]).with_x_axis(des::Axis::new().with_ticks( + des::axis::Ticks::new().with_locator(des::axis::ticks::DateTimeLocator::Days(2).into()), + )); + let fig = fig_small(plot); + + assert_fig_eq_ref!(&fig, "axes/datetime-locator"); +} From ce5a4aa6adb91ebd90d8af193e4f9657269884c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Thebault?= Date: Fri, 17 Apr 2026 22:34:16 +0200 Subject: [PATCH 3/3] handle datetime formatter --- src/drawing/ticks.rs | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/drawing/ticks.rs b/src/drawing/ticks.rs index 51f977d..f07776d 100644 --- a/src/drawing/ticks.rs +++ b/src/drawing/ticks.rs @@ -35,6 +35,13 @@ pub fn locate_num( Ok(LogLocator::new_major(*base).ticks(nb)) } #[cfg(feature = "time")] + (Locator::DateTime(_), Scale::Auto | Scale::Linear { .. }) => { + Ok(locate_datetime(&locator, nb.into())? + .into_iter() + .map(|dt| dt.timestamp()) + .collect()) + } + #[cfg(feature = "time")] (Locator::TimeDelta(loc), Scale::Auto | Scale::Linear { .. }) => { locate_timedelta_num(loc, nb) } @@ -492,9 +499,7 @@ pub fn num_label_formatter( match ticks.formatter() { None => Arc::new(NullFormat), Some(Formatter::Auto) if scale.is_shared() => Arc::new(NullFormat), - Some(Formatter::Auto | Formatter::SharedAuto) => { - auto_label_formatter(ticks.locator(), ab, scale) - } + Some(Formatter::Auto | Formatter::SharedAuto) => auto_label_formatter(ticks, ab, scale), Some(Formatter::Prec(prec)) => Arc::new(PrecLabelFormat(*prec)), Some(Formatter::Percent(fmt)) => { let prec = fmt @@ -505,16 +510,16 @@ pub fn num_label_formatter( #[cfg(feature = "time")] Some(Formatter::TimeDelta(tdfmt)) => timedelta_label_formatter(ab, tdfmt), #[cfg(feature = "time")] - _ => todo!(), + Some(Formatter::DateTime(_)) => datetime_label_formatter(ticks, ab.into(), scale).unwrap(), } } fn auto_label_formatter( - locator: &Locator, + ticks: &Ticks, ab: axis::NumBounds, scale: &Scale, ) -> Arc { - match (locator, scale) { + match (ticks.locator(), scale) { (Locator::PiMultiple { .. }, _) => Arc::new(PiMultipleLabelFormat { prec: 2 }), (Locator::Auto, Scale::Log(LogScale { base, .. })) if *base == 10.0 => { Arc::new(SciLabelFormat) @@ -531,7 +536,13 @@ fn auto_label_formatter( Arc::new(PrecLabelFormat(2)) } } - _ => todo!(), + #[cfg(feature = "time")] + (Locator::DateTime(_), _) => datetime_label_formatter(ticks, ab.into(), scale).unwrap(), + _ => todo!( + "auto label formatter for locator {:?} and scale {:?}", + ticks.locator(), + scale + ), } } @@ -694,8 +705,14 @@ struct DateTimeLabelFormat { #[cfg(feature = "time")] impl LabelFormatter for DateTimeLabelFormat { fn format_label(&self, data: data::SampleRef) -> String { - let dt = data.as_time().unwrap(); - format!("{}", dt.fmt_to_string(&self.fmt)) + match data { + data::SampleRef::Time(dt) => format!("{}", dt.fmt_to_string(&self.fmt)), + data::SampleRef::Num(num) => { + let dt = DateTime::from_timestamp(num).expect("Invalid timestamp"); + format!("{}", dt.fmt_to_string(&self.fmt)) + } + _ => panic!("data is not compatible with formatter"), + } } }