Skip to main content

ggplot_rs/scale/
discrete.rs

1use crate::aes::Aesthetic;
2use crate::data::Value;
3
4use super::Scale;
5
6/// Discrete scale: maps categorical values to evenly-spaced positions.
7#[derive(Clone, Debug)]
8pub struct ScaleDiscrete {
9    aesthetic: Aesthetic,
10    name: String,
11    levels: Vec<String>,
12    custom_labels: Option<Vec<String>>,
13    /// Pre-set level order/filter. When set, only these levels are shown (in this order).
14    limits: Option<Vec<String>>,
15}
16
17impl ScaleDiscrete {
18    pub fn new() -> Self {
19        ScaleDiscrete {
20            aesthetic: Aesthetic::X,
21            name: String::new(),
22            levels: Vec::new(),
23            custom_labels: None,
24            limits: None,
25        }
26    }
27
28    pub fn for_aesthetic(mut self, aes: Aesthetic) -> Self {
29        self.aesthetic = aes;
30        self
31    }
32
33    pub fn with_name(mut self, name: &str) -> Self {
34        self.name = name.to_string();
35        self
36    }
37
38    /// Set custom display labels for each level. Must match the number of levels.
39    pub fn with_labels(mut self, labels: Vec<String>) -> Self {
40        self.custom_labels = Some(labels);
41        self
42    }
43
44    /// Set level order and filter. Only these levels are shown, in this order.
45    /// Data values not in limits are mapped to the middle (0.5).
46    pub fn with_limits(mut self, limits: Vec<&str>) -> Self {
47        self.limits = Some(limits.into_iter().map(|s| s.to_string()).collect());
48        self
49    }
50}
51
52impl ScaleDiscrete {
53    /// Get the effective levels (filtered by limits if set).
54    fn effective_levels(&self) -> &[String] {
55        if let Some(ref limits) = self.limits {
56            limits
57        } else {
58            &self.levels
59        }
60    }
61}
62
63impl Default for ScaleDiscrete {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69impl Scale for ScaleDiscrete {
70    fn aesthetic(&self) -> Aesthetic {
71        self.aesthetic.clone()
72    }
73
74    fn train(&mut self, values: &[Value]) {
75        if let Some(ref limits) = self.limits {
76            // When limits are set, use them as the level order (ignore data order)
77            self.levels = limits.clone();
78        } else {
79            for v in values {
80                let key = v.to_group_key();
81                if !self.levels.contains(&key) {
82                    self.levels.push(key);
83                }
84            }
85        }
86    }
87
88    fn map(&self, value: &Value) -> f64 {
89        let key = value.to_group_key();
90        let effective = self.effective_levels();
91        let n = effective.len();
92        if n == 0 {
93            return 0.5;
94        }
95        match effective.iter().position(|l| l == &key) {
96            Some(idx) => (idx as f64 + 0.5) / n as f64,
97            None => 0.5, // Not in limits → maps to middle
98        }
99    }
100
101    fn breaks(&self) -> Vec<(f64, String)> {
102        let effective = self.effective_levels();
103        let n = effective.len();
104        if n == 0 {
105            return vec![];
106        }
107        effective
108            .iter()
109            .enumerate()
110            .map(|(i, level)| {
111                let pos = (i as f64 + 0.5) / n as f64;
112                let label = if let Some(ref labels) = self.custom_labels {
113                    labels.get(i).cloned().unwrap_or_else(|| level.clone())
114                } else {
115                    level.clone()
116                };
117                (pos, label)
118            })
119            .collect()
120    }
121
122    fn name(&self) -> &str {
123        &self.name
124    }
125
126    fn set_name(&mut self, name: &str) {
127        self.name = name.to_string();
128    }
129
130    fn is_discrete(&self) -> bool {
131        true
132    }
133
134    fn clone_box(&self) -> Box<dyn Scale> {
135        Box::new(self.clone())
136    }
137
138    fn reset_training(&mut self) {
139        if self.limits.is_none() {
140            self.levels.clear();
141        }
142    }
143}