Skip to main content

slack_messaging/blocks/data_visualization/charts/
line_chart.rs

1use super::{unique_series_names, AxisConfig, DataSeries, ValidateXYChart};
2
3use crate::errors::ValidationErrorKind;
4use crate::validators::*;
5
6use serde::Serialize;
7use slack_messaging_derive::Builder;
8
9/// [Line chart
10/// block](https://docs.slack.dev/reference/block-kit/blocks/data-visualization-block#line)
11/// representation.
12///
13/// # Fields and Validations
14///
15/// For more details, see the [official
16/// documentation](https://docs.slack.dev/reference/block-kit/blocks/data-visualization-block#line).
17///
18/// | Field | Type | Required | Validation |
19/// |-------|------|----------|------------|
20/// | series | Vec<[DataSeries]> | Yes | Min 1, Max 6 items, Unique names, Labels must match axis categories |
21/// | axis_config | [AxisConfig] | Yes | N/A |
22///
23/// # Example
24///
25/// ```
26/// use slack_messaging::blocks::data_visualization::charts::{data_points, DataSeries, LineChart,
27/// AxisConfig};
28/// # use std::error::Error;
29///
30/// # fn try_main() -> Result<(), Box<dyn Error>> {
31/// let line_chart = LineChart::builder()
32///     .series(vec![
33///         DataSeries::builder()
34///             .name("Website")
35///             .data(data_points(vec![
36///                 ("Week 1", 32000),
37///                 ("Week 2", 35000),
38///                 ("Week 3", 29000),
39///                 ("Week 4", 41000),
40///                 ("Week 5", 45000),
41///             ])?)
42///             .build()?,
43///         DataSeries::builder()
44///             .name("In-store")
45///             .data(data_points(vec![
46///                 ("Week 1", 17000),
47///                 ("Week 2", 20000),
48///                 ("Week 3", 15000),
49///                 ("Week 4", 22000),
50///                 ("Week 5", 30000),
51///             ])?)
52///             .build()?,
53///     ])
54///     .axis_config(
55///         AxisConfig::builder()
56///             .categories(vec![
57///                 "Week 1",
58///                 "Week 2",
59///                 "Week 3",
60///                 "Week 4",
61///                 "Week 5",
62///             ])
63///             .x_label("Week")
64///             .y_label("Paper Sales (USD)")
65///             .build()?
66///     )
67///     .build()?;
68///
69/// let expected = serde_json::json!({
70///     "type": "line",
71///     "series": [
72///         {
73///             "name": "Website",
74///             "data": [
75///                 { "label": "Week 1", "value": 32000 },
76///                 { "label": "Week 2", "value": 35000 },
77///                 { "label": "Week 3", "value": 29000 },
78///                 { "label": "Week 4", "value": 41000 },
79///                 { "label": "Week 5", "value": 45000 }
80///             ]
81///         },
82///         {
83///             "name": "In-store",
84///             "data": [
85///                 { "label": "Week 1", "value": 17000 },
86///                 { "label": "Week 2", "value": 20000 },
87///                 { "label": "Week 3", "value": 15000 },
88///                 { "label": "Week 4", "value": 22000 },
89///                 { "label": "Week 5", "value": 30000 }
90///             ]
91///         }
92///     ],
93///     "axis_config": {
94///         "categories": [
95///             "Week 1",
96///             "Week 2",
97///             "Week 3",
98///             "Week 4",
99///             "Week 5"
100///         ],
101///         "x_label": "Week",
102///         "y_label": "Paper Sales (USD)"
103///     }
104/// });
105///
106/// let json = serde_json::to_value(line_chart).unwrap();
107///
108/// assert_eq!(json, expected);
109/// #     Ok(())
110/// # }
111/// # fn main() {
112/// #     try_main().unwrap()
113/// # }
114/// ```
115#[derive(Debug, Clone, Serialize, PartialEq, Builder)]
116#[builder(validate = "validate")]
117#[serde(tag = "type", rename = "line")]
118pub struct LineChart {
119    #[builder(
120        push_item = "push_series",
121        validate("required", "list::not_empty", "list::max_item_6", "unique_series_names")
122    )]
123    pub(crate) series: Option<Vec<DataSeries>>,
124
125    #[builder(validate("required"))]
126    pub(crate) axis_config: Option<AxisConfig>,
127}
128
129impl ValidateXYChart for LineChart {
130    fn series(&self) -> Option<&[DataSeries]> {
131        self.series.as_deref()
132    }
133
134    fn axis_config(&self) -> Option<&AxisConfig> {
135        self.axis_config.as_ref()
136    }
137}
138
139fn validate(val: &LineChart) -> Vec<ValidationErrorKind> {
140    val.validate()
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use super::super::data_points;
147    use crate::errors::*;
148
149    #[test]
150    fn it_implements_builder() {
151        let expected = LineChart {
152            series: Some(vec![data_series("Pie")]),
153            axis_config: Some(axis_config()),
154        };
155
156        let val = LineChart::builder()
157            .set_series(Some(vec![data_series("Pie")]))
158            .set_axis_config(Some(axis_config()))
159            .build()
160            .unwrap();
161
162        assert_eq!(val, expected);
163
164        let val = LineChart::builder()
165            .series(vec![data_series("Pie")])
166            .axis_config(axis_config())
167            .build()
168            .unwrap();
169
170        assert_eq!(val, expected);
171    }
172
173    #[test]
174    fn it_implements_push_item_method() {
175        let expected = LineChart {
176            series: Some(vec![data_series("Pie")]),
177            axis_config: Some(axis_config()),
178        };
179
180        let val = LineChart::builder()
181            .push_series(data_series("Pie"))
182            .axis_config(axis_config())
183            .build()
184            .unwrap();
185
186        assert_eq!(val, expected);
187    }
188
189    #[test]
190    fn it_requires_series_field() {
191        let err = LineChart::builder()
192            .axis_config(axis_config())
193            .build()
194            .unwrap_err();
195        assert_eq!(err.object(), "LineChart");
196
197        let errors = err.field("series");
198        assert!(errors.includes(ValidationErrorKind::Required));
199    }
200
201    #[test]
202    fn it_requires_series_field_to_have_at_least_one_item() {
203        let err = LineChart::builder()
204            .series(vec![] as Vec<DataSeries>)
205            .axis_config(axis_config())
206            .build()
207            .unwrap_err();
208        assert_eq!(err.object(), "LineChart");
209
210        let errors = err.field("series");
211        assert!(errors.includes(ValidationErrorKind::EmptyArray));
212    }
213
214    #[test]
215    fn it_requires_series_field_to_have_no_more_than_six_items() {
216        let err = LineChart::builder()
217            .series(vec![
218                data_series("Series 1"),
219                data_series("Series 2"),
220                data_series("Series 3"),
221                data_series("Series 4"),
222                data_series("Series 5"),
223                data_series("Series 6"),
224                data_series("Series 7"),
225            ])
226            .axis_config(axis_config())
227            .build()
228            .unwrap_err();
229        assert_eq!(err.object(), "LineChart");
230
231        let errors = err.field("series");
232        assert!(errors.includes(ValidationErrorKind::MaxArraySize(6)));
233    }
234
235    #[test]
236    fn it_requires_axis_config_field() {
237        let err = LineChart::builder()
238            .series(vec![data_series("Pie")])
239            .build()
240            .unwrap_err();
241        assert_eq!(err.object(), "LineChart");
242
243        let errors = err.field("axis_config");
244        assert!(errors.includes(ValidationErrorKind::Required));
245    }
246
247    #[test]
248    fn it_requires_series_names_to_be_unique() {
249        let err = LineChart::builder()
250            .series(vec![
251                data_series("Sales"),
252                data_series("Revenue"),
253                data_series("Sales"),
254            ])
255            .axis_config(axis_config())
256            .build()
257            .unwrap_err();
258        assert_eq!(err.object(), "LineChart");
259
260        let errors = err.field("series");
261        assert!(errors.includes(ValidationErrorKind::UniqueSeriesName));
262    }
263
264    #[test]
265    fn it_requires_series_labels_to_match_axis_categories() {
266        let err = LineChart::builder()
267            .series(vec![
268                data_series("Pie"),
269                DataSeries::builder()
270                    .name("Cake")
271                    .data(data_points(vec![
272                        ("Chocolate", 90),
273                        ("Vanilla", 80),
274                    ]).unwrap())
275                    .build()
276                    .unwrap(),
277            ])
278            .axis_config(axis_config())
279            .build()
280            .unwrap_err();
281        assert_eq!(err.object(), "LineChart");
282
283        let errors = err.across_fields();
284        assert!(errors.includes(ValidationErrorKind::DataPointLabelMatching));
285    }
286
287    fn data_series(name: &str) -> DataSeries {
288        DataSeries::builder()
289            .name(name)
290            .data(data_points(vec![
291                ("Strawberry Rhubarb", 85),
292                ("Pumpkin", 70),
293            ]).unwrap())
294            .build()
295            .unwrap()
296    }
297
298    fn axis_config() -> AxisConfig {
299        AxisConfig::builder()
300            .categories(vec![
301                "Strawberry Rhubarb",
302                "Pumpkin",
303            ])
304            .x_label("Pies")
305            .y_label("Percentage of Tastiness")
306            .build()
307            .unwrap()
308    }
309}