Skip to main content

dioxus_ui_system/organisms/charts/
common.rs

1//! Common types and utilities for chart components
2
3#![allow(unpredictable_function_pointer_comparisons)]
4
5use crate::theme::tokens::Color;
6
7/// A single data point for charts
8#[derive(Clone, PartialEq, Debug)]
9pub struct ChartDataPoint {
10    /// Label for the data point (shown on axis/tooltip)
11    pub label: String,
12    /// Numeric value
13    pub value: f64,
14    /// Optional color override for this data point
15    pub color: Option<Color>,
16}
17
18impl ChartDataPoint {
19    /// Create a new data point
20    pub fn new(label: impl Into<String>, value: f64) -> Self {
21        Self {
22            label: label.into(),
23            value,
24            color: None,
25        }
26    }
27
28    /// Create a new data point with color
29    pub fn with_color(label: impl Into<String>, value: f64, color: Color) -> Self {
30        Self {
31            label: label.into(),
32            value,
33            color: Some(color),
34        }
35    }
36}
37
38/// A data series for multi-series charts
39#[derive(Clone, PartialEq, Debug)]
40pub struct ChartSeries {
41    /// Series name (shown in legend)
42    pub name: String,
43    /// Series color
44    pub color: Color,
45    /// Data points
46    pub data: Vec<ChartDataPoint>,
47}
48
49impl ChartSeries {
50    /// Create a new data series
51    pub fn new(name: impl Into<String>, color: Color, data: Vec<ChartDataPoint>) -> Self {
52        Self {
53            name: name.into(),
54            color,
55            data,
56        }
57    }
58
59    /// Get the minimum value in this series
60    pub fn min_value(&self) -> f64 {
61        self.data
62            .iter()
63            .map(|p| p.value)
64            .fold(f64::INFINITY, f64::min)
65    }
66
67    /// Get the maximum value in this series
68    pub fn max_value(&self) -> f64 {
69        self.data
70            .iter()
71            .map(|p| p.value)
72            .fold(f64::NEG_INFINITY, f64::max)
73    }
74}
75
76/// Chart margin/padding
77#[derive(Clone, PartialEq, Debug)]
78pub struct ChartMargin {
79    pub top: u16,
80    pub right: u16,
81    pub bottom: u16,
82    pub left: u16,
83}
84
85impl Default for ChartMargin {
86    fn default() -> Self {
87        Self {
88            top: 20,
89            right: 20,
90            bottom: 40,
91            left: 50,
92        }
93    }
94}
95
96impl ChartMargin {
97    /// Create uniform margins
98    pub fn uniform(margin: u16) -> Self {
99        Self {
100            top: margin,
101            right: margin,
102            bottom: margin,
103            left: margin,
104        }
105    }
106
107    /// Create margins with only horizontal/vertical
108    pub fn symmetric(vertical: u16, horizontal: u16) -> Self {
109        Self {
110            top: vertical,
111            right: horizontal,
112            bottom: vertical,
113            left: horizontal,
114        }
115    }
116}
117
118/// Axis configuration
119#[derive(Clone, PartialEq, Debug)]
120pub struct ChartAxis {
121    /// Show the axis line
122    pub show_line: bool,
123    /// Show tick marks
124    pub show_ticks: bool,
125    /// Show grid lines
126    pub show_grid: bool,
127    /// Number of ticks/grid lines
128    pub tick_count: u8,
129    /// Format for tick labels
130    pub label_format: Option<fn(f64) -> String>,
131    /// Minimum value (auto-calculated if None)
132    pub min: Option<f64>,
133    /// Maximum value (auto-calculated if None)
134    pub max: Option<f64>,
135}
136
137impl Default for ChartAxis {
138    fn default() -> Self {
139        Self {
140            show_line: true,
141            show_ticks: true,
142            show_grid: true,
143            tick_count: 5,
144            label_format: None,
145            min: None,
146            max: None,
147        }
148    }
149}
150
151impl ChartAxis {
152    /// Create a hidden axis
153    pub fn hidden() -> Self {
154        Self {
155            show_line: false,
156            show_ticks: false,
157            show_grid: false,
158            tick_count: 0,
159            label_format: None,
160            min: None,
161            max: None,
162        }
163    }
164}
165
166/// Legend position
167#[derive(Default, Clone, PartialEq, Debug)]
168pub enum LegendPosition {
169    /// No legend
170    None,
171    /// Top of chart
172    #[default]
173    Top,
174    /// Bottom of chart
175    Bottom,
176    /// Left side of chart
177    Left,
178    /// Right side of chart
179    Right,
180}
181
182/// Tooltip configuration
183#[derive(Clone, PartialEq, Debug)]
184pub struct ChartTooltip {
185    /// Show tooltip on hover
186    pub enabled: bool,
187    /// Format tooltip content (takes data point and optional series name)
188    pub formatter: Option<fn(&ChartDataPoint, Option<&str>) -> String>,
189    /// Custom tooltip values (if not using calculated values)
190    /// Key is "series_index:data_index" for lookup
191    pub custom_values: Option<std::collections::HashMap<String, String>>,
192    /// Show series name in tooltip (for multi-series charts)
193    pub show_series_name: bool,
194    /// Show value in tooltip
195    pub show_value: bool,
196    /// Value formatter (used when show_value is true and no custom formatter)
197    pub value_format: Option<fn(f64) -> String>,
198}
199
200impl Default for ChartTooltip {
201    fn default() -> Self {
202        Self {
203            enabled: true,
204            formatter: None,
205            custom_values: None,
206            show_series_name: true,
207            show_value: true,
208            value_format: None,
209        }
210    }
211}
212
213impl ChartTooltip {
214    /// Create a tooltip with custom formatter
215    pub fn with_formatter(formatter: fn(&ChartDataPoint, Option<&str>) -> String) -> Self {
216        Self {
217            enabled: true,
218            formatter: Some(formatter),
219            ..Default::default()
220        }
221    }
222
223    /// Create a tooltip with custom values
224    pub fn with_custom_values(values: std::collections::HashMap<String, String>) -> Self {
225        Self {
226            enabled: true,
227            custom_values: Some(values),
228            ..Default::default()
229        }
230    }
231
232    /// Disable tooltip
233    pub fn disabled() -> Self {
234        Self {
235            enabled: false,
236            ..Default::default()
237        }
238    }
239
240    /// Get tooltip content for a data point
241    pub fn get_content(&self, point: &ChartDataPoint, series_name: Option<&str>) -> String {
242        // Use custom formatter if provided
243        if let Some(formatter) = self.formatter {
244            return formatter(point, series_name);
245        }
246
247        // Build tooltip content
248        let mut parts = Vec::new();
249
250        // Series name
251        if self.show_series_name && series_name.is_some() {
252            parts.push(series_name.unwrap().to_string());
253        }
254
255        // Label
256        parts.push(point.label.clone());
257
258        // Value
259        if self.show_value {
260            let value_str = if let Some(format) = self.value_format {
261                format(point.value)
262            } else {
263                format_compact_number(point.value)
264            };
265            parts.push(format!(": {}", value_str));
266        }
267
268        parts.join("")
269    }
270}
271
272/// Animation configuration
273#[derive(Clone, PartialEq, Debug)]
274pub struct ChartAnimation {
275    /// Enable animations
276    pub enabled: bool,
277    /// Animation duration in milliseconds
278    pub duration_ms: u16,
279    /// Easing function
280    pub easing: AnimationEasing,
281}
282
283impl Default for ChartAnimation {
284    fn default() -> Self {
285        Self {
286            enabled: true,
287            duration_ms: 500,
288            easing: AnimationEasing::EaseOut,
289        }
290    }
291}
292
293/// Animation easing functions
294#[derive(Default, Clone, PartialEq, Debug)]
295pub enum AnimationEasing {
296    #[default]
297    Linear,
298    Ease,
299    EaseIn,
300    EaseOut,
301    EaseInOut,
302}
303
304impl AnimationEasing {
305    pub fn as_css(&self) -> &'static str {
306        match self {
307            AnimationEasing::Linear => "linear",
308            AnimationEasing::Ease => "ease",
309            AnimationEasing::EaseIn => "ease-in",
310            AnimationEasing::EaseOut => "ease-out",
311            AnimationEasing::EaseInOut => "ease-in-out",
312        }
313    }
314}
315
316/// Calculate nice round numbers for axis ticks
317pub fn calculate_nice_ticks(min: f64, max: f64, count: u8) -> Vec<f64> {
318    if min == max {
319        return vec![min];
320    }
321
322    let range = max - min;
323    let step = nice_number(range / count as f64, false);
324    let nice_min = (min / step).floor() * step;
325    let nice_max = (max / step).ceil() * step;
326
327    let mut ticks = Vec::new();
328    let mut current = nice_min;
329    while current <= nice_max + step / 2.0 {
330        ticks.push(current);
331        current += step;
332    }
333
334    ticks
335}
336
337/// Round a number to a nice value
338fn nice_number(x: f64, round: bool) -> f64 {
339    let exp = x.log10().floor() as i32;
340    let f = x / 10.0_f64.powi(exp);
341
342    let nf = if round {
343        if f < 1.5 {
344            1.0
345        } else if f < 3.0 {
346            2.0
347        } else if f < 7.0 {
348            5.0
349        } else {
350            10.0
351        }
352    } else {
353        if f <= 1.0 {
354            1.0
355        } else if f <= 2.0 {
356            2.0
357        } else if f <= 5.0 {
358            5.0
359        } else {
360            10.0
361        }
362    };
363
364    nf * 10.0_f64.powi(exp)
365}
366
367/// Format a number as a compact string
368pub fn format_compact_number(value: f64) -> String {
369    if value.abs() >= 1_000_000_000.0 {
370        format!("{:.1}B", value / 1_000_000_000.0)
371    } else if value.abs() >= 1_000_000.0 {
372        format!("{:.1}M", value / 1_000_000.0)
373    } else if value.abs() >= 1_000.0 {
374        format!("{:.1}K", value / 1_000.0)
375    } else {
376        format!("{:.0}", value)
377    }
378}
379
380/// Format a number as currency
381pub fn format_currency(value: f64) -> String {
382    format!("${:.2}", value)
383}
384
385/// Format a number as percentage
386pub fn format_percentage(value: f64) -> String {
387    format!("{:.1}%", value)
388}
389
390/// Convert a Color to CSS rgba string
391pub fn color_to_css(color: &Color) -> String {
392    color.to_rgba()
393}
394
395/// Calculate path for a smooth line (catmull-rom spline simplified)
396pub fn calculate_smooth_line(points: &[(f64, f64)]) -> String {
397    if points.len() < 2 {
398        return String::new();
399    }
400
401    let mut path = format!("M {},{} ", points[0].0, points[0].1);
402
403    for i in 1..points.len() {
404        let prev = if i > 0 { points[i - 1] } else { points[0] };
405        let curr = points[i];
406        let next = if i < points.len() - 1 {
407            points[i + 1]
408        } else {
409            curr
410        };
411
412        let cp1x = prev.0 + (curr.0 - prev.0) * 0.5;
413        let cp1y = prev.1;
414        let cp2x = curr.0 - (next.0 - prev.0) * 0.5;
415        let cp2y = curr.1;
416
417        path.push_str(&format!(
418            "C {},{} {},{} {},{} ",
419            cp1x, cp1y, cp2x, cp2y, curr.0, curr.1
420        ));
421    }
422
423    path
424}
425
426/// Calculate area path (line path closed to baseline)
427pub fn calculate_area_path(points: &[(f64, f64)], baseline_y: f64) -> String {
428    if points.is_empty() {
429        return String::new();
430    }
431
432    let mut path = format!("M {},{} ", points[0].0, baseline_y);
433    path.push_str(&format!("L {},{} ", points[0].0, points[0].1));
434
435    for i in 1..points.len() {
436        path.push_str(&format!("L {},{} ", points[i].0, points[i].1));
437    }
438
439    path.push_str(&format!(
440        "L {},{} Z",
441        points[points.len() - 1].0,
442        baseline_y
443    ));
444    path
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450
451    #[test]
452    fn test_chart_data_point() {
453        let point = ChartDataPoint::new("Jan", 100.0);
454        assert_eq!(point.label, "Jan");
455        assert_eq!(point.value, 100.0);
456        assert!(point.color.is_none());
457    }
458
459    #[test]
460    fn test_chart_data_point_with_color() {
461        let color = Color::new(255, 0, 0);
462        let point = ChartDataPoint::with_color("Feb", 200.0, color.clone());
463        assert_eq!(point.color, Some(color));
464    }
465
466    #[test]
467    fn test_chart_series() {
468        let data = vec![
469            ChartDataPoint::new("A", 10.0),
470            ChartDataPoint::new("B", 20.0),
471            ChartDataPoint::new("C", 30.0),
472        ];
473        let series = ChartSeries::new("Test", Color::new(0, 0, 255), data);
474
475        assert_eq!(series.name, "Test");
476        assert_eq!(series.min_value(), 10.0);
477        assert_eq!(series.max_value(), 30.0);
478    }
479
480    #[test]
481    fn test_nice_ticks() {
482        let ticks = calculate_nice_ticks(0.0, 100.0, 5);
483        assert!(!ticks.is_empty());
484        assert!(ticks[0] <= 0.0);
485        assert!(ticks[ticks.len() - 1] >= 100.0);
486    }
487
488    #[test]
489    fn test_format_compact_number() {
490        assert_eq!(format_compact_number(1500.0), "1.5K");
491        assert_eq!(format_compact_number(1500000.0), "1.5M");
492        assert_eq!(format_compact_number(1500000000.0), "1.5B");
493        assert_eq!(format_compact_number(150.0), "150");
494    }
495}