adui_dioxus/components/
statistic.rs

1use dioxus::prelude::*;
2
3/// Props for the Statistic component (MVP subset).
4#[derive(Props, Clone, PartialEq)]
5pub struct StatisticProps {
6    /// Optional title shown above the value.
7    #[props(optional)]
8    pub title: Option<Element>,
9    /// Numeric value to display.
10    #[props(optional)]
11    pub value: Option<f64>,
12    /// Optional preformatted value text. When provided, this takes
13    /// precedence over `value`.
14    #[props(optional)]
15    pub value_text: Option<String>,
16    /// Optional decimal precision applied to `value`.
17    #[props(optional)]
18    pub precision: Option<u8>,
19    /// Optional prefix element rendered before the value.
20    #[props(optional)]
21    pub prefix: Option<Element>,
22    /// Optional suffix element rendered after the value.
23    #[props(optional)]
24    pub suffix: Option<Element>,
25    /// Optional inline style for the value span (e.g. color).
26    #[props(optional)]
27    pub value_style: Option<String>,
28    /// Extra class on the root element.
29    #[props(optional)]
30    pub class: Option<String>,
31    /// Inline style on the root element.
32    #[props(optional)]
33    pub style: Option<String>,
34}
35
36fn format_value(props: &StatisticProps) -> String {
37    if let Some(text) = &props.value_text {
38        return text.clone();
39    }
40    let value = props.value.unwrap_or(0.0);
41    if let Some(precision) = props.precision {
42        let p: usize = precision as usize;
43        format!("{value:.p$}")
44    } else {
45        // Remove trailing .0 for integers
46        let s = format!("{value}");
47        if s.ends_with(".0") {
48            s.trim_end_matches(".0").to_string()
49        } else {
50            s
51        }
52    }
53}
54
55/// Ant Design flavored Statistic (MVP: value + prefix/suffix + precision).
56#[component]
57pub fn Statistic(props: StatisticProps) -> Element {
58    let display_text = format_value(&props);
59
60    let mut class_list = vec!["adui-statistic".to_string()];
61    if let Some(extra) = props.class.clone() {
62        class_list.push(extra);
63    }
64    let class_attr = class_list.join(" ");
65    let style_attr = props.style.clone().unwrap_or_default();
66    let value_style_attr = props.value_style.clone().unwrap_or_default();
67
68    rsx! {
69        div { class: "{class_attr}", style: "{style_attr}",
70            if let Some(title) = props.title.clone() {
71                div { class: "adui-statistic-title", {title} }
72            }
73            div { class: "adui-statistic-content",
74                if let Some(prefix) = props.prefix.clone() {
75                    span { class: "adui-statistic-prefix", {prefix} }
76                }
77                span { class: "adui-statistic-value", style: "{value_style_attr}", "{display_text}" }
78                if let Some(suffix) = props.suffix.clone() {
79                    span { class: "adui-statistic-suffix", {suffix} }
80                }
81            }
82        }
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn format_value_uses_value_text_first() {
92        let props = StatisticProps {
93            title: None,
94            value: Some(123.456),
95            value_text: Some("custom".into()),
96            precision: None,
97            prefix: None,
98            suffix: None,
99            value_style: None,
100            class: None,
101            style: None,
102        };
103        assert_eq!(format_value(&props), "custom");
104    }
105
106    #[test]
107    fn format_value_applies_precision() {
108        let props = StatisticProps {
109            title: None,
110            value: Some(std::f64::consts::PI),
111            value_text: None,
112            precision: Some(2),
113            prefix: None,
114            suffix: None,
115            value_style: None,
116            class: None,
117            style: None,
118        };
119        assert_eq!(format_value(&props), "3.14");
120    }
121
122    #[test]
123    fn format_value_trims_trailing_point_zero() {
124        let props = StatisticProps {
125            title: None,
126            value: Some(10.0),
127            value_text: None,
128            precision: None,
129            prefix: None,
130            suffix: None,
131            value_style: None,
132            class: None,
133            style: None,
134        };
135        assert_eq!(format_value(&props), "10");
136    }
137
138    #[test]
139    fn format_value_handles_negative_numbers() {
140        let props = StatisticProps {
141            title: None,
142            value: Some(-123.456),
143            value_text: None,
144            precision: None,
145            prefix: None,
146            suffix: None,
147            value_style: None,
148            class: None,
149            style: None,
150        };
151        assert_eq!(format_value(&props), "-123.456");
152    }
153
154    #[test]
155    fn format_value_handles_negative_integers() {
156        let props = StatisticProps {
157            title: None,
158            value: Some(-100.0),
159            value_text: None,
160            precision: None,
161            prefix: None,
162            suffix: None,
163            value_style: None,
164            class: None,
165            style: None,
166        };
167        assert_eq!(format_value(&props), "-100");
168    }
169
170    #[test]
171    fn format_value_handles_large_numbers() {
172        let props = StatisticProps {
173            title: None,
174            value: Some(1_000_000.0),
175            value_text: None,
176            precision: None,
177            prefix: None,
178            suffix: None,
179            value_style: None,
180            class: None,
181            style: None,
182        };
183        assert_eq!(format_value(&props), "1000000");
184    }
185
186    #[test]
187    fn format_value_handles_very_large_numbers() {
188        let props = StatisticProps {
189            title: None,
190            value: Some(1e15),
191            value_text: None,
192            precision: None,
193            prefix: None,
194            suffix: None,
195            value_style: None,
196            class: None,
197            style: None,
198        };
199        let result = format_value(&props);
200        assert!(!result.is_empty());
201    }
202
203    #[test]
204    fn format_value_precision_zero() {
205        let props = StatisticProps {
206            title: None,
207            value: Some(123.456),
208            value_text: None,
209            precision: Some(0),
210            prefix: None,
211            suffix: None,
212            value_style: None,
213            class: None,
214            style: None,
215        };
216        assert_eq!(format_value(&props), "123");
217    }
218
219    #[test]
220    fn format_value_precision_high() {
221        let props = StatisticProps {
222            title: None,
223            value: Some(123.456789),
224            value_text: None,
225            precision: Some(6),
226            prefix: None,
227            suffix: None,
228            value_style: None,
229            class: None,
230            style: None,
231        };
232        assert_eq!(format_value(&props), "123.456789");
233    }
234
235    #[test]
236    fn format_value_precision_with_negative() {
237        let props = StatisticProps {
238            title: None,
239            value: Some(-123.456),
240            value_text: None,
241            precision: Some(1),
242            prefix: None,
243            suffix: None,
244            value_style: None,
245            class: None,
246            style: None,
247        };
248        assert_eq!(format_value(&props), "-123.5");
249    }
250
251    #[test]
252    fn format_value_defaults_to_zero() {
253        let props = StatisticProps {
254            title: None,
255            value: None,
256            value_text: None,
257            precision: None,
258            prefix: None,
259            suffix: None,
260            value_style: None,
261            class: None,
262            style: None,
263        };
264        assert_eq!(format_value(&props), "0");
265    }
266
267    #[test]
268    fn format_value_defaults_to_zero_with_precision() {
269        let props = StatisticProps {
270            title: None,
271            value: None,
272            value_text: None,
273            precision: Some(2),
274            prefix: None,
275            suffix: None,
276            value_style: None,
277            class: None,
278            style: None,
279        };
280        assert_eq!(format_value(&props), "0.00");
281    }
282
283    #[test]
284    fn format_value_handles_small_decimals() {
285        let props = StatisticProps {
286            title: None,
287            value: Some(0.001),
288            value_text: None,
289            precision: None,
290            prefix: None,
291            suffix: None,
292            value_style: None,
293            class: None,
294            style: None,
295        };
296        assert_eq!(format_value(&props), "0.001");
297    }
298
299    #[test]
300    fn format_value_handles_zero() {
301        let props = StatisticProps {
302            title: None,
303            value: Some(0.0),
304            value_text: None,
305            precision: None,
306            prefix: None,
307            suffix: None,
308            value_style: None,
309            class: None,
310            style: None,
311        };
312        assert_eq!(format_value(&props), "0");
313    }
314
315    #[test]
316    fn format_value_handles_zero_with_precision() {
317        let props = StatisticProps {
318            title: None,
319            value: Some(0.0),
320            value_text: None,
321            precision: Some(2),
322            prefix: None,
323            suffix: None,
324            value_style: None,
325            class: None,
326            style: None,
327        };
328        assert_eq!(format_value(&props), "0.00");
329    }
330
331    #[test]
332    fn format_value_handles_nan() {
333        let props = StatisticProps {
334            title: None,
335            value: Some(f64::NAN),
336            value_text: None,
337            precision: None,
338            prefix: None,
339            suffix: None,
340            value_style: None,
341            class: None,
342            style: None,
343        };
344        let result = format_value(&props);
345        // NaN formatting is implementation-dependent, just verify it doesn't panic
346        assert!(!result.is_empty());
347    }
348
349    #[test]
350    fn format_value_handles_infinity() {
351        let props = StatisticProps {
352            title: None,
353            value: Some(f64::INFINITY),
354            value_text: None,
355            precision: None,
356            prefix: None,
357            suffix: None,
358            value_style: None,
359            class: None,
360            style: None,
361        };
362        let result = format_value(&props);
363        // Infinity formatting is implementation-dependent, just verify it doesn't panic
364        assert!(!result.is_empty());
365    }
366
367    #[test]
368    fn format_value_value_text_overrides_everything() {
369        let props = StatisticProps {
370            title: None,
371            value: Some(999.999),
372            value_text: Some("Custom Text".into()),
373            precision: Some(5),
374            prefix: None,
375            suffix: None,
376            value_style: None,
377            class: None,
378            style: None,
379        };
380        assert_eq!(format_value(&props), "Custom Text");
381    }
382
383    #[test]
384    fn format_value_decimal_that_ends_with_zero() {
385        let props = StatisticProps {
386            title: None,
387            value: Some(123.450),
388            value_text: None,
389            precision: None,
390            prefix: None,
391            suffix: None,
392            value_style: None,
393            class: None,
394            style: None,
395        };
396        // Should trim .0
397        assert_eq!(format_value(&props), "123.45");
398    }
399
400    #[test]
401    fn statistic_props_defaults() {
402        // StatisticProps requires no mandatory fields
403        // All fields are optional
404    }
405}