Skip to main content

chartml_core/scales/
ordinal.rs

1/// Maps discrete domain values to discrete range values (typically colors).
2/// Equivalent to D3's `scaleOrdinal()`.
3pub struct ScaleOrdinal {
4    domain: Vec<String>,
5    range: Vec<String>,
6}
7
8impl ScaleOrdinal {
9    /// Create a new ordinal scale with the given domain and range.
10    pub fn new(domain: Vec<String>, range: Vec<String>) -> Self {
11        Self { domain, range }
12    }
13
14    /// Map a domain value to a range value.
15    /// Uses index-based lookup: domain[i] -> range[i % range.len()]
16    /// Returns None if the value is not in the domain or if the range is empty.
17    pub fn map(&self, value: &str) -> Option<&str> {
18        if self.range.is_empty() {
19            return None;
20        }
21        let index = self.domain.iter().position(|d| d == value)?;
22        Some(&self.range[index % self.range.len()])
23    }
24
25    /// Get the range value at index i (cycling if i >= range.len()).
26    /// Returns None if the range is empty.
27    pub fn map_index(&self, index: usize) -> Option<&str> {
28        if self.range.is_empty() {
29            return None;
30        }
31        Some(&self.range[index % self.range.len()])
32    }
33
34    /// Get the domain values.
35    pub fn domain(&self) -> &[String] {
36        &self.domain
37    }
38
39    /// Get the range values.
40    pub fn range(&self) -> &[String] {
41        &self.range
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    #[test]
50    fn ordinal_basic_mapping() {
51        let scale = ScaleOrdinal::new(
52            vec!["a".into(), "b".into(), "c".into()],
53            vec!["red".into(), "green".into(), "blue".into()],
54        );
55        assert_eq!(scale.map("a"), Some("red"));
56        assert_eq!(scale.map("b"), Some("green"));
57        assert_eq!(scale.map("c"), Some("blue"));
58    }
59
60    #[test]
61    fn ordinal_cycling() {
62        let scale = ScaleOrdinal::new(
63            vec!["a".into(), "b".into(), "c".into(), "d".into()],
64            vec!["red".into(), "green".into()],
65        );
66        assert_eq!(scale.map("a"), Some("red"));
67        assert_eq!(scale.map("b"), Some("green"));
68        assert_eq!(scale.map("c"), Some("red"));   // cycles back
69        assert_eq!(scale.map("d"), Some("green"));  // cycles back
70    }
71
72    #[test]
73    fn ordinal_unknown_returns_none() {
74        let scale = ScaleOrdinal::new(
75            vec!["a".into(), "b".into()],
76            vec!["red".into(), "green".into()],
77        );
78        assert_eq!(scale.map("unknown"), None);
79    }
80
81    #[test]
82    fn ordinal_empty_range() {
83        let scale = ScaleOrdinal::new(
84            vec!["a".into(), "b".into()],
85            vec![],
86        );
87        assert_eq!(scale.map("a"), None);
88    }
89
90    #[test]
91    fn ordinal_map_index() {
92        let scale = ScaleOrdinal::new(
93            vec!["a".into(), "b".into()],
94            vec!["red".into(), "green".into(), "blue".into()],
95        );
96        assert_eq!(scale.map_index(0), Some("red"));
97        assert_eq!(scale.map_index(1), Some("green"));
98        assert_eq!(scale.map_index(2), Some("blue"));
99        assert_eq!(scale.map_index(3), Some("red")); // cycles
100    }
101}