rustlavel_chart/
series.rs1use crate::palette::Palette;
4use rustlavel_core::Json;
5
6#[derive(Debug, Clone, PartialEq)]
11pub struct Series {
12 pub label: String,
13 pub values: Vec<Option<f64>>,
14 pub colour: Option<String>,
18 pub stack: Option<String>,
21 pub filled: bool,
23 pub tension: f64,
26 pub right_axis: bool,
29}
30
31impl Series {
32 pub fn new(label: impl Into<String>, values: impl IntoIterator<Item = f64>) -> Series {
33 Series {
34 label: label.into(),
35 values: values.into_iter().map(Some).collect(),
36 colour: None,
37 stack: None,
38 filled: false,
39 tension: 0.0,
40 right_axis: false,
41 }
42 }
43
44 pub fn sparse(label: impl Into<String>, values: impl IntoIterator<Item = Option<f64>>) -> Series {
46 Series { values: values.into_iter().collect(), ..Series::new(label, []) }
47 }
48
49 pub fn colour(mut self, colour: impl Into<String>) -> Series {
50 self.colour = Some(colour.into());
51 self
52 }
53
54 pub fn stack(mut self, group: impl Into<String>) -> Series {
55 self.stack = Some(group.into());
56 self
57 }
58
59 pub fn filled(mut self) -> Series {
60 self.filled = true;
61 self
62 }
63
64 pub fn smooth(mut self) -> Series {
67 self.tension = 0.35;
68 self
69 }
70
71 pub fn right_axis(mut self) -> Series {
72 self.right_axis = true;
73 self
74 }
75
76 pub fn len(&self) -> usize {
77 self.values.len()
78 }
79
80 pub fn is_empty(&self) -> bool {
81 self.values.is_empty()
82 }
83
84 pub(crate) fn to_json(&self, index: usize, palette: &Palette, per_point: bool) -> Json {
87 let colour = self.colour.clone().unwrap_or_else(|| palette.at(index).to_string());
88 let data = Json::Array(
89 self.values.iter().map(|value| value.map_or(Json::Null, Json::from)).collect(),
90 );
91
92 let background = if per_point {
95 Json::Array((0..self.values.len()).map(|n| Json::from(palette.at(n))).collect())
96 } else if self.filled {
97 Json::from(palette.translucent(&colour).as_str())
98 } else {
99 Json::from(colour.as_str())
100 };
101
102 let mut fields = vec![
103 ("label", Json::from(self.label.as_str())),
104 ("data", data),
105 ("borderColor", Json::from(colour.as_str())),
106 ("backgroundColor", background),
107 ("borderWidth", Json::from(2)),
108 ("fill", Json::from(self.filled)),
109 ("tension", Json::from(self.tension)),
110 ("spanGaps", Json::from(false)),
112 ];
113 if let Some(stack) = &self.stack {
114 fields.push(("stack", Json::from(stack.as_str())));
115 }
116 if self.right_axis {
117 fields.push(("yAxisID", Json::from("right")));
118 }
119 Json::object(fields)
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn a_gap_is_null_and_is_not_bridged() {
129 let series = Series::sparse("visits", [Some(1.0), None, Some(3.0)]);
130 let json = series.to_json(0, &Palette::default(), false);
131
132 let data = json.get("data").and_then(Json::as_array).unwrap();
133 assert_eq!(data.len(), 3);
134 assert!(data[1].is_null(), "a missing reading became a number");
135 assert_eq!(
136 json.get("spanGaps").and_then(Json::as_bool),
137 Some(false),
138 "the line would be drawn straight through a month nobody measured"
139 );
140 }
141
142 #[test]
145 fn lines_are_straight_unless_asked_to_be_smooth() {
146 assert_eq!(Series::new("a", [1.0]).tension, 0.0);
147 assert_eq!(Series::new("a", [1.0]).smooth().tension, 0.35);
148 }
149
150 #[test]
151 fn a_named_colour_beats_the_palette() {
152 let palette = Palette::default();
153 let by_position = Series::new("a", [1.0]).to_json(1, &palette, false);
154 assert_eq!(by_position.get("borderColor").and_then(Json::as_str), Some(palette.at(1)));
155
156 let named = Series::new("a", [1.0]).colour("#123456").to_json(1, &palette, false);
157 assert_eq!(named.get("borderColor").and_then(Json::as_str), Some("#123456"));
158 }
159
160 #[test]
161 fn a_slice_chart_colours_each_point_rather_than_the_set() {
162 let json = Series::new("share", [1.0, 2.0, 3.0]).to_json(0, &Palette::default(), true);
163 let background = json.get("backgroundColor").and_then(Json::as_array).unwrap();
164 assert_eq!(background.len(), 3, "every slice would have been the same colour");
165 }
166}