1use serde::ser::{Serialize, SerializeSeq, SerializeStruct, Serializer};
8
9#[derive(Debug, Clone, PartialEq)]
18pub struct Datum {
19 label: String,
20 value: f64,
21}
22
23impl Datum {
24 #[must_use]
26 pub fn new(label: impl Into<String>, value: f64) -> Self {
27 Self {
28 label: label.into(),
29 value,
30 }
31 }
32
33 #[must_use]
35 pub fn label(&self) -> &str {
36 &self.label
37 }
38
39 #[must_use]
41 pub fn value(&self) -> Option<f64> {
42 finite(self.value)
43 }
44}
45
46impl<S: Into<String>> From<(S, f64)> for Datum {
47 fn from((label, value): (S, f64)) -> Self {
48 Self::new(label, value)
49 }
50}
51
52impl Serialize for Datum {
53 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
54 let mut s = serializer.serialize_struct("Datum", 2)?;
55 s.serialize_field("label", &self.label)?;
56 s.serialize_field("value", &self.value())?;
57 s.end()
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq)]
65pub struct Point {
66 pub x: f64,
68 pub y: f64,
70}
71
72impl Point {
73 #[must_use]
75 pub const fn new(x: f64, y: f64) -> Self {
76 Self { x, y }
77 }
78}
79
80impl From<(f64, f64)> for Point {
81 fn from((x, y): (f64, f64)) -> Self {
82 Self::new(x, y)
83 }
84}
85
86impl Serialize for Point {
87 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
88 let mut s = serializer.serialize_seq(Some(2))?;
89 s.serialize_element(&finite(self.x))?;
90 s.serialize_element(&finite(self.y))?;
91 s.end()
92 }
93}
94
95#[derive(Debug, Clone, PartialEq)]
107pub struct Series {
108 name: String,
109 points: Vec<Point>,
110}
111
112impl Series {
113 #[must_use]
115 pub fn new<P: Into<Point>>(
116 name: impl Into<String>,
117 points: impl IntoIterator<Item = P>,
118 ) -> Self {
119 Self {
120 name: name.into(),
121 points: points.into_iter().map(Into::into).collect(),
122 }
123 }
124
125 #[must_use]
127 pub fn name(&self) -> &str {
128 &self.name
129 }
130
131 #[must_use]
133 pub fn points(&self) -> &[Point] {
134 &self.points
135 }
136}
137
138impl Serialize for Series {
139 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
140 let mut s = serializer.serialize_struct("Series", 2)?;
141 s.serialize_field("name", &self.name)?;
142 s.serialize_field("points", &self.points)?;
143 s.end()
144 }
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
156pub struct Color(u32);
157
158impl Color {
159 #[must_use]
161 pub const fn hex(rgb: u32) -> Self {
162 Self(rgb & 0x00ff_ffff)
163 }
164
165 #[must_use]
167 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
168 Self(((r as u32) << 16) | ((g as u32) << 8) | b as u32)
169 }
170}
171
172impl std::fmt::Display for Color {
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 write!(f, "#{:06x}", self.0)
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
187pub struct Format(String);
188
189const MAX_DECIMALS: u8 = 12;
191
192impl Format {
193 #[must_use]
195 pub fn integer() -> Self {
196 Self(",.0f".into())
197 }
198
199 #[must_use]
201 pub fn decimal(places: u8) -> Self {
202 Self(format!(",.{}f", places.min(MAX_DECIMALS)))
203 }
204
205 #[must_use]
207 pub fn percent(places: u8) -> Self {
208 Self(format!(".{}%", places.min(MAX_DECIMALS)))
209 }
210
211 #[must_use]
213 pub fn si(digits: u8) -> Self {
214 Self(format!(".{}~s", digits.clamp(1, 21)))
215 }
216
217 #[must_use]
219 pub fn currency(places: u8) -> Self {
220 Self(format!("$,.{}f", places.min(MAX_DECIMALS)))
221 }
222
223 #[must_use]
226 pub fn d3(spec: impl Into<String>) -> Self {
227 Self(spec.into())
228 }
229
230 #[must_use]
232 pub fn spec(&self) -> &str {
233 &self.0
234 }
235
236 pub(crate) fn into_spec(self) -> String {
238 self.0
239 }
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
244#[non_exhaustive]
245pub enum Curve {
246 #[default]
248 Linear,
249 Monotone,
251 Step,
253 Natural,
255}
256
257impl Curve {
258 #[must_use]
260 pub const fn as_str(self) -> &'static str {
261 match self {
262 Self::Linear => "linear",
263 Self::Monotone => "monotone",
264 Self::Step => "step",
265 Self::Natural => "natural",
266 }
267 }
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
272#[non_exhaustive]
273pub enum Scale {
274 #[default]
276 Linear,
277 Time,
279 Log,
281}
282
283impl Scale {
284 #[must_use]
286 pub const fn as_str(self) -> &'static str {
287 match self {
288 Self::Linear => "linear",
289 Self::Time => "time",
290 Self::Log => "log",
291 }
292 }
293}
294
295pub(crate) fn finite(v: f64) -> Option<f64> {
297 v.is_finite().then_some(v)
298}
299
300pub(crate) fn cell(v: f64) -> String {
304 let Some(v) = finite(v) else {
305 return "—".to_owned();
306 };
307 if v == 0.0 {
308 return "0".to_owned();
309 }
310 if (1e-6..1e21).contains(&v.abs()) {
311 return v.to_string();
312 }
313 let text = format!("{v:e}");
314 match text.split_once('e') {
315 Some((mantissa, exp)) if !exp.starts_with('-') => format!("{mantissa}e+{exp}"),
316 _ => text,
317 }
318}
319
320pub(crate) fn utc(ms: f64) -> String {
323 const MAX_MS: f64 = 8.64e15;
325 if !ms.is_finite() || ms.abs() > MAX_MS {
326 return cell(f64::NAN);
327 }
328 #[allow(clippy::cast_possible_truncation)] let ms = ms.floor() as i64;
330 let days = ms.div_euclid(86_400_000);
331 let rest = ms.rem_euclid(86_400_000);
332 let (year, month, day) = civil_from_days(days);
333 let date = format!("{year:04}-{month:02}-{day:02}");
334 if rest == 0 {
335 return date;
336 }
337 let (hour, minute, second, milli) = (
338 rest / 3_600_000,
339 rest / 60_000 % 60,
340 rest / 1000 % 60,
341 rest % 1000,
342 );
343 let time = format!("{date}T{hour:02}:{minute:02}:{second:02}");
344 if milli == 0 {
345 format!("{time}Z")
346 } else {
347 format!("{time}.{milli:03}Z")
348 }
349}
350
351const fn civil_from_days(days: i64) -> (i64, i64, i64) {
354 let z = days + 719_468;
355 let era = z.div_euclid(146_097);
356 let doe = z.rem_euclid(146_097);
357 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
358 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
359 let mp = (5 * doy + 2) / 153;
360 let d = doy - (153 * mp + 2) / 5 + 1;
361 let m = if mp < 10 { mp + 3 } else { mp - 9 };
362 let y = yoe + era * 400 + if m <= 2 { 1 } else { 0 };
363 (y, m, d)
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use proptest::prelude::*;
370
371 fn json<T: Serialize>(v: &T) -> String {
372 serde_json::to_string(v).expect("serializes")
373 }
374
375 #[test]
376 fn non_finite_values_serialize_as_null() {
377 assert_eq!(
378 json(&Datum::new("a", f64::INFINITY)),
379 r#"{"label":"a","value":null}"#
380 );
381 assert_eq!(json(&Point::new(f64::NAN, 1.0)), "[null,1.0]");
382 assert_eq!(Datum::new("a", f64::NAN).value(), None);
383 assert_eq!(Datum::new("a", 2.0).value(), Some(2.0));
384 }
385
386 #[test]
387 fn labels_escape_as_json_strings() {
388 assert_eq!(
389 json(&Datum::new("\"<b>\"\n", 1.0)),
390 r#"{"label":"\"<b>\"\n","value":1.0}"#
391 );
392 }
393
394 #[test]
395 fn tuples_convert() {
396 let d: Datum = ("x", 1.5).into();
397 assert_eq!(d.label(), "x");
398 let s = Series::new(String::from("s"), [Point::new(1.0, 2.0)]);
399 assert_eq!(s.name(), "s");
400 assert_eq!(s.points(), [Point::new(1.0, 2.0)]);
401 }
402
403 #[test]
404 fn colors_render_as_lowercase_hex() {
405 assert_eq!(Color::hex(0x00ab_cdef).to_string(), "#abcdef");
406 assert_eq!(Color::hex(0xff00_0000).to_string(), "#000000");
407 assert_eq!(Color::rgb(1, 2, 3).to_string(), "#010203");
408 }
409
410 #[test]
411 fn formats_map_to_d3_specifiers() {
412 assert_eq!(Format::integer().spec(), ",.0f");
413 assert_eq!(Format::decimal(2).spec(), ",.2f");
414 assert_eq!(Format::decimal(99).spec(), ",.12f");
415 assert_eq!(Format::percent(0).spec(), ".0%");
416 assert_eq!(Format::si(0).spec(), ".1~s");
417 assert_eq!(Format::si(3).spec(), ".3~s");
418 assert_eq!(Format::currency(0).spec(), "$,.0f");
419 assert_eq!(Format::d3("+.2e").spec(), "+.2e");
420 }
421
422 #[test]
423 fn enums_map_to_attribute_values() {
424 let curves = [Curve::Linear, Curve::Monotone, Curve::Step, Curve::Natural];
425 let names: Vec<_> = curves.iter().map(|c| c.as_str()).collect();
426 assert_eq!(names, ["linear", "monotone", "step", "natural"]);
427 let scales = [Scale::Linear, Scale::Time, Scale::Log];
428 let names: Vec<_> = scales.iter().map(|s| s.as_str()).collect();
429 assert_eq!(names, ["linear", "time", "log"]);
430 assert_eq!(Curve::default(), Curve::Linear);
431 assert_eq!(Scale::default(), Scale::Linear);
432 }
433
434 #[test]
435 fn utc_formats_dates_and_times() {
436 assert_eq!(utc(0.0), "1970-01-01");
437 assert_eq!(utc(951_782_400_000.0), "2000-02-29");
438 assert_eq!(utc(1_767_225_600_000.0), "2026-01-01");
439 assert_eq!(
440 utc(1_767_225_600_000.0 + 3_723_000.0),
441 "2026-01-01T01:02:03Z"
442 );
443 assert_eq!(
444 utc(1_767_225_600_000.0 + 3_723_004.0),
445 "2026-01-01T01:02:03.004Z"
446 );
447 assert_eq!(utc(-86_400_000.0), "1969-12-31");
448 assert_eq!(utc(-1.0), "1969-12-31T23:59:59.999Z");
449 assert_eq!(utc(f64::NAN), "—");
450 assert_eq!(utc(9e15), "—");
451 }
452
453 #[test]
454 fn cells_show_missing_values_as_a_dash() {
455 assert_eq!(cell(1.5), "1.5");
456 assert_eq!(cell(f64::NEG_INFINITY), "—");
457 assert_eq!(cell(-0.0), "0");
459 assert_eq!(cell(1e21), "1e+21");
460 assert_eq!(cell(-2.5e30), "-2.5e+30");
461 assert_eq!(cell(1e-7), "1e-7");
462 assert_eq!(cell(0.000_001), "0.000001");
463 assert_eq!(cell(123_456_789.0), "123456789");
464 }
465
466 proptest! {
467 #[test]
470 fn json_is_always_valid(label in ".*", v in any::<f64>(), x in any::<f64>(), y in any::<f64>()) {
471 let text = json(&(Datum::new(label.clone(), v), Series::new("s", [(x, y)])));
472 let parsed: serde_json::Value = serde_json::from_str(&text).expect("valid JSON");
473 prop_assert_eq!(parsed[0]["label"].as_str(), Some(label.as_str()));
474 for (got, want) in [
475 (&parsed[0]["value"], v),
476 (&parsed[1]["points"][0][0], x),
477 (&parsed[1]["points"][0][1], y),
478 ] {
479 prop_assert_eq!(got.is_null(), !want.is_finite(), "{}", text);
480 prop_assert_eq!(got.is_f64(), want.is_finite(), "{}", text);
481 }
482 }
483
484 #[test]
486 fn civil_dates_are_consecutive(days in -3_000_000i64..3_000_000) {
487 let (y0, m0, d0) = civil_from_days(days);
488 let (y1, m1, d1) = civil_from_days(days + 1);
489 let next_day = y1 == y0 && m1 == m0 && d1 == d0 + 1;
490 let next_month = y1 == y0 && m1 == m0 + 1 && d1 == 1;
491 let next_year = y1 == y0 + 1 && m0 == 12 && d0 == 31 && m1 == 1 && d1 == 1;
492 prop_assert!(next_day || next_month || next_year, "{:?} {:?}", (y0, m0, d0), (y1, m1, d1));
493 prop_assert!((1..=12).contains(&m0) && (1..=31).contains(&d0));
494 }
495 }
496}