ggplot_rs/scale/
identity.rs1use crate::aes::Aesthetic;
2use crate::data::Value;
3
4use super::Scale;
5
6fn parse_hex(s: &str) -> Option<(u8, u8, u8)> {
9 let s = s.strip_prefix('#')?;
10 let digit = |c: u8| -> Option<u8> {
11 match c {
12 b'0'..=b'9' => Some(c - b'0'),
13 b'a'..=b'f' => Some(c - b'a' + 10),
14 b'A'..=b'F' => Some(c - b'A' + 10),
15 _ => None,
16 }
17 };
18 let bytes = s.as_bytes();
19 match bytes.len() {
20 6 => {
21 let byte =
22 |i: usize| -> Option<u8> { Some(digit(bytes[i])? * 16 + digit(bytes[i + 1])?) };
23 Some((byte(0)?, byte(2)?, byte(4)?))
24 }
25 3 => {
26 let byte = |i: usize| -> Option<u8> {
27 let d = digit(bytes[i])?;
28 Some(d * 16 + d)
29 };
30 Some((byte(0)?, byte(1)?, byte(2)?))
31 }
32 _ => None,
33 }
34}
35
36#[derive(Clone)]
41pub struct ScaleColorIdentity {
42 aesthetic: Aesthetic,
43}
44
45impl ScaleColorIdentity {
46 pub fn new(aes: Aesthetic) -> Self {
49 ScaleColorIdentity { aesthetic: aes }
50 }
51}
52
53impl Scale for ScaleColorIdentity {
54 fn aesthetic(&self) -> Aesthetic {
55 self.aesthetic.clone()
56 }
57
58 fn train(&mut self, _values: &[Value]) {
59 }
61
62 fn map(&self, _value: &Value) -> f64 {
63 0.0
64 }
65
66 fn breaks(&self) -> Vec<(f64, String)> {
67 Vec::new()
68 }
69
70 fn name(&self) -> &str {
71 ""
72 }
73
74 fn set_name(&mut self, _name: &str) {}
75
76 fn map_to_color(&self, value: &Value) -> Option<(u8, u8, u8)> {
77 value.as_str().and_then(parse_hex)
78 }
79
80 fn clone_box(&self) -> Box<dyn Scale> {
81 Box::new(self.clone())
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn parses_6_and_3_digit_hex() {
91 assert_eq!(parse_hex("#FF0000"), Some((255, 0, 0)));
92 assert_eq!(parse_hex("#00ff00"), Some((0, 255, 0)));
93 assert_eq!(parse_hex("#00F"), Some((0, 0, 255)));
94 assert_eq!(parse_hex("#abc"), Some((0xaa, 0xbb, 0xcc)));
95 }
96
97 #[test]
98 fn invalid_values_map_to_none() {
99 for bad in ["red", "#GGG", "#12345", "", "#"] {
100 assert_eq!(parse_hex(bad), None, "{bad} should not parse");
101 }
102 let scale = ScaleColorIdentity::new(Aesthetic::Color);
103 assert_eq!(scale.map_to_color(&Value::Str("not-a-colour".into())), None);
104 assert_eq!(scale.map_to_color(&Value::Float(1.0)), None);
105 }
106
107 #[test]
108 fn no_legend() {
109 let scale = ScaleColorIdentity::new(Aesthetic::Fill);
110 assert!(scale.breaks().is_empty());
111 }
112
113 #[test]
116 fn scale_color_identity_alias_applies_to_the_color_aesthetic() {
117 use crate::data::Value;
118 use crate::prelude::*;
119
120 let svg = GGPlot::new(vec![
121 ("x".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
122 ("y".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
123 (
124 "c".into(),
125 vec![Value::Str("#00FF00".into()), Value::Str("#0000FF".into())],
126 ),
127 ])
128 .aes(Aes::new().x("x").y("y").color("c"))
129 .geom_point()
130 .scale_color_identity()
131 .render_svg_native_with_size(200, 200)
132 .unwrap();
133
134 assert!(svg.contains("#00FF00"));
135 assert!(svg.contains("#0000FF"));
136 }
137
138 #[test]
139 fn renders_through_the_plot_with_no_legend_and_falls_back_on_invalid_values() {
140 use crate::data::Value;
141 use crate::prelude::*;
142
143 let svg = GGPlot::new(vec![
144 (
145 "x".into(),
146 vec![Value::Str("a".into()), Value::Str("b".into())],
147 ),
148 ("y".into(), vec![Value::Float(1.0), Value::Float(2.0)]),
149 (
150 "fill".into(),
151 vec![Value::Str("#FF0000".into()), Value::Str("bogus".into())],
152 ),
153 ])
154 .aes(Aes::new().x("x").y("y").fill("fill"))
155 .geom_col()
156 .scale_fill_identity()
157 .render_svg_native_with_size(200, 200)
158 .unwrap();
159
160 assert!(svg.contains("#FF0000"));
161 assert!(!svg.contains(">fill<"));
163 }
164
165 #[test]
170 fn scale_trait_contract_has_no_training_name_or_legend() {
171 let mut scale = ScaleColorIdentity::new(Aesthetic::Fill);
172 scale.train(&[Value::Str("#FF0000".into()), Value::Float(1.0)]);
173 assert_eq!(scale.name(), "");
174 scale.set_name("ignored");
175 assert_eq!(
176 scale.name(),
177 "",
178 "set_name is a no-op for an identity scale"
179 );
180 assert_eq!(scale.map(&Value::Str("#FF0000".into())), 0.0);
181 assert_eq!(scale.map(&Value::Na), 0.0);
182 assert!(scale.breaks().is_empty());
183
184 let cloned = scale.clone_box();
185 assert_eq!(cloned.aesthetic(), Aesthetic::Fill);
186 assert_eq!(
187 cloned.map_to_color(&Value::Str("#112233".into())),
188 Some((0x11, 0x22, 0x33))
189 );
190 }
191}