gpui_component/plot/shape/
stack.rs

1// @reference: https://d3js.org/d3-shape/stack
2
3/// Represents a stacked series data point with lower and upper values
4#[derive(Clone, Debug)]
5pub struct StackPoint<T> {
6    /// The lower value (baseline)
7    pub y0: f32,
8    /// The upper value (topline)
9    pub y1: f32,
10    /// Reference to the original data
11    pub data: T,
12}
13
14/// Represents a stacked series
15#[derive(Clone, Debug)]
16pub struct StackSeries<T> {
17    /// The key for this series
18    pub key: String,
19    /// The index of this series
20    pub index: usize,
21    /// The points in this series
22    pub points: Vec<StackPoint<T>>,
23}
24
25#[allow(clippy::type_complexity)]
26pub struct Stack<T> {
27    data: Vec<T>,
28    keys: Vec<String>,
29    value: Box<dyn Fn(&T, &str) -> Option<f32>>,
30}
31
32impl<T: Clone> Default for Stack<T> {
33    fn default() -> Self {
34        Self {
35            data: Vec::new(),
36            keys: Vec::new(),
37            value: Box::new(|_, _| None),
38        }
39    }
40}
41
42impl<T: Clone> Stack<T> {
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Set the data to be stacked
48    pub fn data<I>(mut self, data: I) -> Self
49    where
50        I: IntoIterator<Item = T>,
51    {
52        self.data = data.into_iter().collect();
53        self
54    }
55
56    /// Set the keys (series) for stacking
57    pub fn keys<I, S>(mut self, keys: I) -> Self
58    where
59        I: IntoIterator<Item = S>,
60        S: Into<String>,
61    {
62        self.keys = keys.into_iter().map(|s| s.into()).collect();
63        self
64    }
65
66    /// Set the value accessor function
67    pub fn value<F>(mut self, value: F) -> Self
68    where
69        F: Fn(&T, &str) -> Option<f32> + 'static,
70    {
71        self.value = Box::new(value);
72        self
73    }
74
75    /// Compute the stacked series
76    pub fn series(&self) -> Vec<StackSeries<T>> {
77        if self.data.is_empty() || self.keys.is_empty() {
78            return Vec::new();
79        }
80
81        let n = self.data.len(); // number of data points
82        let m = self.keys.len(); // number of series
83
84        // Extract values into a 2D matrix: series x data points
85        let mut matrix: Vec<Vec<f32>> = Vec::with_capacity(m);
86        for key in &self.keys {
87            let mut series_values = Vec::with_capacity(n);
88            for datum in &self.data {
89                let value = (self.value)(datum, key).unwrap_or(0.0);
90                series_values.push(value);
91            }
92            matrix.push(series_values);
93        }
94
95        // Use the natural key order for stacking
96        let order: Vec<usize> = (0..m).collect();
97
98        // Initialize stacks with zeros
99        let mut stacks: Vec<Vec<(f32, f32)>> = vec![vec![(0.0, 0.0); n]; m];
100
101        // Compute the stacks based on order
102        for j in 0..n {
103            let mut y0 = 0.0;
104            for &i in &order {
105                let y1 = y0 + matrix[i][j];
106                stacks[i][j] = (y0, y1);
107                y0 = y1;
108            }
109        }
110
111        // Build the result series
112        let mut result = Vec::with_capacity(m);
113        for (i, key) in self.keys.iter().enumerate() {
114            let points = self
115                .data
116                .iter()
117                .enumerate()
118                .map(|(j, datum)| StackPoint {
119                    y0: stacks[i][j].0,
120                    y1: stacks[i][j].1,
121                    data: datum.clone(),
122                })
123                .collect();
124
125            result.push(StackSeries {
126                key: key.clone(),
127                index: i,
128                points,
129            });
130        }
131
132        result
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[derive(Clone, Debug)]
141    struct SalesData {
142        #[allow(dead_code)]
143        date: String,
144        apples: f32,
145        bananas: f32,
146        cherries: f32,
147    }
148
149    #[test]
150    fn test_basic_stack() {
151        let data = vec![
152            SalesData {
153                date: "Jan".to_string(),
154                apples: 10.0,
155                bananas: 20.0,
156                cherries: 30.0,
157            },
158            SalesData {
159                date: "Feb".to_string(),
160                apples: 15.0,
161                bananas: 25.0,
162                cherries: 35.0,
163            },
164        ];
165
166        let stack = Stack::new()
167            .data(data)
168            .keys(vec!["apples", "bananas", "cherries"])
169            .value(|d, key| match key {
170                "apples" => Some(d.apples),
171                "bananas" => Some(d.bananas),
172                "cherries" => Some(d.cherries),
173                _ => None,
174            });
175
176        let series = stack.series();
177
178        assert_eq!(series.len(), 3);
179        assert_eq!(series[0].key, "apples");
180        assert_eq!(series[0].points[0].y0, 0.0);
181        assert_eq!(series[0].points[0].y1, 10.0);
182
183        assert_eq!(series[1].key, "bananas");
184        assert_eq!(series[1].points[0].y0, 10.0);
185        assert_eq!(series[1].points[0].y1, 30.0);
186
187        assert_eq!(series[2].key, "cherries");
188        assert_eq!(series[2].points[0].y0, 30.0);
189        assert_eq!(series[2].points[0].y1, 60.0);
190    }
191}