Skip to main content

entrenar/dashboard/
trend.rs

1//! Trend analysis for dashboard metrics.
2
3use serde::{Deserialize, Serialize};
4
5/// Trend direction for a metric.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
7pub enum Trend {
8    /// Metric is increasing
9    Rising,
10    /// Metric is decreasing
11    Falling,
12    /// Metric is relatively stable
13    Stable,
14}
15
16impl Trend {
17    /// Compute trend from a series of values.
18    ///
19    /// Uses linear regression slope to determine trend direction.
20    pub fn from_values(values: &[f64]) -> Self {
21        if values.len() < 2 {
22            return Self::Stable;
23        }
24
25        // Simple linear regression slope
26        let n = values.len() as f64;
27        let sum_x: f64 = (0..values.len()).map(|i| i as f64).sum();
28        let sum_y: f64 = values.iter().sum();
29        let sum_xy: f64 = values.iter().enumerate().map(|(i, &y)| i as f64 * y).sum();
30        let sum_x2: f64 = (0..values.len()).map(|i| (i as f64).powi(2)).sum();
31
32        let denominator = n * sum_x2 - sum_x.powi(2);
33        if denominator.abs() < f64::EPSILON {
34            return Self::Stable;
35        }
36
37        let slope = (n * sum_xy - sum_x * sum_y) / denominator;
38
39        // Normalize slope by mean to get relative change
40        let mean = sum_y / n;
41        if mean.abs() < f64::EPSILON {
42            return Self::Stable;
43        }
44
45        let relative_slope = slope / mean;
46
47        // Thresholds for trend detection (5% relative change)
48        const THRESHOLD: f64 = 0.05;
49
50        if relative_slope > THRESHOLD {
51            Self::Rising
52        } else if relative_slope < -THRESHOLD {
53            Self::Falling
54        } else {
55            Self::Stable
56        }
57    }
58
59    /// Get emoji representation.
60    pub fn emoji(&self) -> &'static str {
61        match self {
62            Self::Rising => "↑",
63            Self::Falling => "↓",
64            Self::Stable => "→",
65        }
66    }
67}
68
69impl std::fmt::Display for Trend {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        match self {
72            Self::Rising => write!(f, "rising"),
73            Self::Falling => write!(f, "falling"),
74            Self::Stable => write!(f, "stable"),
75        }
76    }
77}