plotting/
colors.rs

1use plotly::color::Rgba;
2
3/// Named colors.
4///
5/// # Note
6///
7/// This enum is a direct re-implementation of the [`plotly::common::color::NamedColor`] enum from
8/// the [`plotly`] crate (Ref. \[1\]). As such, we have included the license of the [`plotly`] crate
9/// in the [`src/plotly_licenses`](https://github.com/tamaskis/plotting/tree/main/src/plotly_licenses/LICENSE)
10/// folder.
11///
12/// # References
13///
14/// * \[1\] <https://docs.rs/plotly/latest/plotly/common/color/enum.NamedColor.html>
15/// * \[2\] <https://www.w3schools.com/cssref/css_colors.asp>
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17#[allow(missing_docs)]
18pub enum NamedColor {
19    AliceBlue = 0xF0F8FF,
20    AntiqueWhite = 0xFAEBD7,
21    Aqua,
22    Aquamarine = 0x7FFFD4,
23    Azure = 0xF0FFFF,
24    Beige = 0xF5F5DC,
25    Bisque = 0xFFE4C4,
26    Black = 0x000000,
27    BlanchedAlmond = 0xFFEBCD,
28    Blue = 0x0000FF,
29    BlueViolet = 0x8A2BE2,
30    Brown = 0xA52A2A,
31    BurlyWood = 0xDEB887,
32    CadetBlue = 0x5F9EA0,
33    Chartreuse = 0x7FFF00,
34    Chocolate = 0xD2691E,
35    Coral = 0xFF7F50,
36    CornflowerBlue = 0x6495ED,
37    CornSilk = 0xFFF8DC,
38    Crimson = 0xDC143C,
39    Cyan = 0x00FFFF,
40    DarkBlue = 0x00008B,
41    DarkCyan = 0x008B8B,
42    DarkGoldenrod = 0xB8860B,
43    DarkGray = 0xA9A9A9,
44    DarkGreen = 0x006400,
45    DarkGrey,
46    DarkKhaki = 0xBDB76B,
47    DarkMagenta = 0x8B008B,
48    DarkOliveGreen = 0x556B2F,
49    DarkOrange = 0xFF8C00,
50    DarkOrchid = 0x9932CC,
51    DarkRed = 0x8B0000,
52    DarkSalmon = 0xE9967A,
53    DarkSeaGreen = 0x8FBC8F,
54    DarkSlateBlue = 0x483D8B,
55    DarkSlateGray = 0x2F4F4F,
56    DarkSlateGrey,
57    DarkTurquoise = 0x00CED1,
58    DarkViolet = 0x9400D3,
59    DeepPink = 0xFF1493,
60    DeepSkyBlue = 0x00BFFF,
61    DimGray = 0x696969,
62    DimGrey,
63    DodgerBlue = 0x1E90FF,
64    FireBrick = 0xB22222,
65    FloralWhite = 0xFFFAF0,
66    ForestGreen = 0x228B22,
67    Fuchsia,
68    Gainsboro = 0xDCDCDC,
69    GhostWhite = 0xF8F8FF,
70    Gold = 0xFFD700,
71    Goldenrod = 0xDAA520,
72    Gray = 0x808080,
73    Green = 0x008000,
74    GreenYellow = 0xADFF2F,
75    Grey,
76    Honeydew = 0xF0FFF0,
77    HotPink = 0xFF69B4,
78    IndianRed = 0xCD5C5C,
79    Indigo = 0x4B0082,
80    Ivory = 0xFFFFF0,
81    Khaki = 0xF0E68C,
82    Lavender = 0xE6E6FA,
83    LavenderBlush = 0xFFF0F5,
84    LawnGreen = 0x7CFC00,
85    LemonChiffon = 0xFFFACD,
86    LightBlue = 0xADD8E6,
87    LightCoral = 0xF08080,
88    LightCyan = 0xE0FFFF,
89    LightGoldenrodYellow = 0xFAFAD2,
90    LightGray = 0xD3D3D3,
91    LightGreen = 0x90EE90,
92    LightGrey,
93    LightPink = 0xFFB6C1,
94    LightSalmon = 0xFFA07A,
95    LightSeaGreen = 0x20B2AA,
96    LightSkyBlue = 0x87CEFA,
97    LightSlateGray = 0x778899,
98    LightSlateGrey,
99    LightSteelBlue = 0xB0C4DE,
100    LightYellow = 0xFFFFE0,
101    Lime = 0x00FF00,
102    LimeGreen = 0x32CD32,
103    Linen = 0xFAF0E6,
104    Magenta = 0xFF00FF,
105    Maroon = 0x800000,
106    MediumAquamarine = 0x66CDAA,
107    MediumBlue = 0x0000CD,
108    MediumOrchid = 0xBA55D3,
109    MediumPurple = 0x9370DB,
110    MediumSeaGreen = 0x3CB371,
111    MediumSlateBlue = 0x7B68EE,
112    MediumSpringGreen = 0x00FA9A,
113    MediumTurquoise = 0x48D1CC,
114    MediumVioletRed = 0xC71585,
115    MidnightBlue = 0x191970,
116    MintCream = 0xF5FFFA,
117    MistyRose = 0xFFE4E1,
118    Moccasin = 0xFFE4B5,
119    NavajoWhite = 0xFFDEAD,
120    Navy = 0x000080,
121    OldLace = 0xFDF5E6,
122    Olive = 0x808000,
123    OliveDrab = 0x6B8E23,
124    Orange = 0xFFA500,
125    OrangeRed = 0xFF4500,
126    Orchid = 0xDA70D6,
127    PaleGoldenrod = 0xEEE8AA,
128    PaleGreen = 0x98FB98,
129    PaleTurquoise = 0xAFEEEE,
130    PaleVioletRed = 0xDB7093,
131    PapayaWhip = 0xFFEFD5,
132    PeachPuff = 0xFFDAB9,
133    Peru = 0xCD853F,
134    Pink = 0xFFC0CB,
135    Plum = 0xDDA0DD,
136    PowderBlue = 0xB0E0E6,
137    Purple = 0x800080,
138    RebeccaPurple = 0x663399,
139    Red = 0xFF0000,
140    RosyBrown = 0xBC8F8F,
141    RoyalBlue = 0x4169E1,
142    SaddleBrown = 0x8B4513,
143    Salmon = 0xFA8072,
144    SandyBrown = 0xF4A460,
145    SeaGreen = 0x2E8B57,
146    Seashell = 0xFFF5EE,
147    Sienna = 0xA0522D,
148    Silver = 0xC0C0C0,
149    SkyBlue = 0x87CEEB,
150    SlateBlue = 0x6A5ACD,
151    SlateGray = 0x708090,
152    SlateGrey,
153    Snow = 0xFFFAFA,
154    SpringGreen = 0x00FF7F,
155    SteelBlue = 0x4682B4,
156    Tan = 0xD2B48C,
157    Teal = 0x008080,
158    Thistle = 0xD8BFD8,
159    Tomato = 0xFF6347,
160    Turquoise = 0x40E0D0,
161    Violet = 0xEE82EE,
162    Wheat = 0xF5DEB3,
163    White = 0xFFFFFF,
164    WhiteSmoke = 0xF5F5F5,
165    Yellow = 0xFFFF00,
166    YellowGreen = 0x9ACD32,
167}
168
169/// Color.
170///
171/// # Constructors from color representations
172///
173/// | From | Constructor |
174/// | ---- | ----------- |
175/// | RGB | [`Color::rgb`] |
176/// | RGBA | [`Color::rgba`] |
177/// | Named Color | [`Color::named`] |
178/// | Hexadecimal Literal | [`Color::hex_literal`] |
179/// | Hexadecimal String | [`Color::hex_str`] |
180///
181/// # Default constructor
182///
183/// [`Color::default`] constructs a color with RGB values of `0, 0, 0` (black) and an alpha value of
184/// `1.0` (opaque).
185///
186/// # Other methods
187///
188/// | Method | Description |
189/// | ------ | ----------- |
190/// | [`Color::alpha`] | Set the opacity/transparency of the color. |
191/// | [`Color::to_plotly_rgba`] | Convert the color to an [`Rgba`] from the [`plotly`] crate. |
192#[derive(Debug, PartialEq)]
193pub struct Color {
194    /// Red component.
195    r: u8,
196
197    /// Green component.
198    g: u8,
199
200    /// Blue component.
201    b: u8,
202
203    /// Opacity
204    ///
205    /// * `0.0` corresponds to completely transparent (fully see-through).
206    /// * `1.0` corresponds to completely opaque (fully solid).
207    a: f64,
208}
209
210impl Color {
211    /// Construct a color from RGB (red, green, blue) values.
212    ///
213    /// # Arguments
214    ///
215    /// * `r` - Red component.
216    /// * `g` - Green component.
217    /// * `b` - Blue component.
218    ///
219    /// # Returns
220    ///
221    /// Color.
222    pub fn rgb(r: u8, g: u8, b: u8) -> Color {
223        Color { r, g, b, a: 1.0 }
224    }
225
226    /// Construct a color from RGBA (red, green, blue, alpha) values.
227    ///
228    /// # Arguments
229    ///
230    /// * `r` - Red component.
231    /// * `g` - Green component.
232    /// * `b` - Blue component.
233    /// * `a` - Alpha (opacity) between 0 (transparent) and 1 (opaque). Inputs outside the range
234    ///   `[0, 1]` are clamped.
235    ///
236    /// # Returns
237    ///
238    /// Color.
239    pub fn rgba(r: u8, g: u8, b: u8, a: f64) -> Color {
240        Color::rgb(r, g, b).alpha(a)
241    }
242
243    /// Construct a color from a named color.
244    ///
245    /// # Arguments
246    ///
247    /// * `color` - Named color.
248    ///
249    /// # Returns.
250    ///
251    /// Color.
252    pub fn named(color: NamedColor) -> Color {
253        // Perform mappings for aliased colors.
254        let color = match color {
255            NamedColor::Aqua => NamedColor::Cyan,
256            NamedColor::DarkGrey => NamedColor::DarkGray,
257            NamedColor::DarkSlateGrey => NamedColor::DarkSlateGray,
258            NamedColor::DimGrey => NamedColor::DimGray,
259            NamedColor::Fuchsia => NamedColor::Magenta,
260            NamedColor::Grey => NamedColor::Gray,
261            NamedColor::LightGrey => NamedColor::LightGray,
262            NamedColor::LightSlateGrey => NamedColor::LightSlateGray,
263            NamedColor::SlateGrey => NamedColor::SlateGray,
264            _ => color, // Return the original if no mapping is needed
265        };
266
267        // Cast the color to a u32.
268        let hex = color as u32;
269
270        // Extract the red, green, and blue components.
271        let r = ((hex >> 16) & 0xFF) as u8;
272        let g = ((hex >> 8) & 0xFF) as u8;
273        let b = (hex & 0xFF) as u8;
274
275        // Construct the color from the RGB components.
276        Color::rgb(r, g, b)
277    }
278
279    /// Construct a color from a hexadecimal literal.
280    ///
281    /// # Arguments
282    ///
283    /// * `color` - Hexadecimal literal representing a color.
284    ///
285    /// # Returns
286    ///
287    /// Color.
288    ///
289    /// # Note
290    ///
291    /// If `color` is outside the valid RGB range (e.g. `color > 0xFFFFFF`), then the default color
292    /// is returned.
293    ///
294    /// # References
295    ///
296    /// * \[1\] <https://stackoverflow.com/questions/25004621/how-to-convert-returned-int-value-from-colordrawables-getcolor-to-rgb/25004682>
297    pub fn hex_literal(color: u32) -> Color {
298        // Return the default color if out of range.
299        if color > 0xFFFFFF {
300            return Color::default();
301        }
302
303        // Extract the red, green, and blue components.
304        let r = ((color >> 16) & 0xFF) as u8;
305        let g = ((color >> 8) & 0xFF) as u8;
306        let b = (color & 0xFF) as u8;
307
308        // Construct the color from the RGB components.
309        Color::rgb(r, g, b)
310    }
311
312    /// Construct a color from a hexadecimal string.
313    ///
314    /// # Arguments
315    ///
316    /// * `color` - Hexadecimal string representing a color.
317    ///
318    /// # Returns
319    ///
320    /// Color.
321    ///
322    /// # Note
323    ///
324    /// If `color` is outside the valid RGB range (e.g. `color > "#FFFFFF"), then the default
325    /// color is returned.
326    pub fn hex_str(color: &str) -> Color {
327        // Remove optional '#' at the beginning
328        let color = color.trim_start_matches('#');
329
330        // Ensure the hex string is valid
331        if color.len() != 6 {
332            return Color::default();
333        }
334
335        // Convert the hex string to a hex literal (u32).
336        let hex_literal = u32::from_str_radix(color, 16).unwrap_or(0x000000);
337
338        // Construct the color from the hex literal.
339        Color::hex_literal(hex_literal)
340    }
341
342    /// Set the opacity/transparency of the color.
343    ///
344    /// # Arguments
345    ///
346    /// * `a` - Alpha (opacity) between 0 (transparent) and 1 (opaque). Inputs outside the range
347    ///   `[0, 1]` are clamped.
348    ///
349    /// # Returns
350    ///
351    /// Color with the opacity set.
352    pub fn alpha(mut self, a: f64) -> Color {
353        self.a = a.clamp(0.0, 1.0);
354        self
355    }
356
357    /// Convert the color to an [`Rgba`] from the [`plotly`] crate.
358    ///
359    /// # Returns
360    ///
361    /// This color as an [`Rgba`] from the [`plotly`] crate.
362    pub fn to_plotly_rgba(&self) -> Rgba {
363        Rgba::new(self.r, self.g, self.b, self.a)
364    }
365}
366
367impl Default for Color {
368    /// Default constructor.
369    ///
370    /// # Returns
371    ///
372    /// Default-constructed color (black).
373    ///
374    /// # Example
375    ///
376    /// ```
377    /// use plotting::{Color, NamedColor};
378    ///
379    /// let color = Color::default();
380    ///
381    /// assert_eq!(color, Color::named(NamedColor::Black))
382    /// ```
383    fn default() -> Self {
384        Color::rgb(0, 0, 0)
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    #[test]
393    fn test_partial_eq() {
394        // Test equality.
395        let color_1 = Color::rgb(20, 30, 40);
396        let color_2 = Color::rgb(20, 30, 40);
397        assert_eq!(color_1, color_2);
398
399        // Test inequality in r.
400        let color_1 = Color::rgb(20, 30, 40);
401        let color_2 = Color::rgb(25, 30, 40);
402        assert_ne!(color_1, color_2);
403
404        // Test inequality in g.
405        let color_1 = Color::rgb(20, 30, 40);
406        let color_2 = Color::rgb(20, 35, 40);
407        assert_ne!(color_1, color_2);
408
409        // Test inequality in b.
410        let color_1 = Color::rgb(20, 30, 40);
411        let color_2 = Color::rgb(20, 30, 45);
412        assert_ne!(color_1, color_2);
413
414        // Test inequality in a.
415        let color_1 = Color::rgb(20, 30, 40).alpha(0.1);
416        let color_2 = Color::rgb(20, 30, 40).alpha(0.2);
417        assert_ne!(color_1, color_2);
418    }
419
420    #[test]
421    fn test_rgb() {
422        let color = Color::rgb(20, 30, 40);
423        assert_eq!(color.r, 20);
424        assert_eq!(color.g, 30);
425        assert_eq!(color.b, 40);
426        assert_eq!(color.a, 1.0);
427    }
428
429    #[test]
430    fn test_rgba() {
431        // Simple check.
432        let color = Color::rgba(20, 30, 40, 0.5);
433        assert_eq!(color.r, 20);
434        assert_eq!(color.g, 30);
435        assert_eq!(color.b, 40);
436        assert_eq!(color.a, 0.5);
437
438        // Check clamping at lower bound.
439        let color = Color::rgba(20, 30, 40, -0.1);
440        assert_eq!(color.a, 0.0);
441
442        // Check clamping at upper bound.
443        let color = Color::rgba(20, 30, 40, 1.1);
444        assert_eq!(color.a, 1.0);
445    }
446
447    #[test]
448    fn test_named() {
449        // Simple checks.
450        assert_eq!(Color::named(NamedColor::Black), Color::rgb(0, 0, 0));
451        assert_eq!(Color::named(NamedColor::White), Color::rgb(255, 255, 255));
452        assert_eq!(Color::named(NamedColor::Red), Color::rgb(255, 0, 0));
453        assert_eq!(Color::named(NamedColor::Lime), Color::rgb(0, 255, 0));
454        assert_eq!(Color::named(NamedColor::Blue), Color::rgb(0, 0, 255));
455        assert_eq!(Color::named(NamedColor::Yellow), Color::rgb(255, 255, 0));
456        assert_eq!(Color::named(NamedColor::Magenta), Color::rgb(255, 0, 255));
457        assert_eq!(Color::named(NamedColor::Cyan), Color::rgb(0, 255, 255));
458        assert_eq!(Color::named(NamedColor::Green), Color::rgb(0, 128, 0));
459
460        // Check aliasing.
461        assert_eq!(
462            Color::named(NamedColor::Aqua),
463            Color::named(NamedColor::Cyan)
464        );
465        assert_eq!(
466            Color::named(NamedColor::DarkGrey),
467            Color::named(NamedColor::DarkGray)
468        );
469        assert_eq!(
470            Color::named(NamedColor::DarkSlateGrey),
471            Color::named(NamedColor::DarkSlateGray)
472        );
473        assert_eq!(
474            Color::named(NamedColor::DimGrey),
475            Color::named(NamedColor::DimGray)
476        );
477        assert_eq!(
478            Color::named(NamedColor::Fuchsia),
479            Color::named(NamedColor::Magenta)
480        );
481        assert_eq!(
482            Color::named(NamedColor::Grey),
483            Color::named(NamedColor::Gray)
484        );
485        assert_eq!(
486            Color::named(NamedColor::LightGrey),
487            Color::named(NamedColor::LightGray)
488        );
489        assert_eq!(
490            Color::named(NamedColor::LightSlateGrey),
491            Color::named(NamedColor::LightSlateGray)
492        );
493        assert_eq!(
494            Color::named(NamedColor::SlateGrey),
495            Color::named(NamedColor::SlateGray)
496        );
497    }
498
499    #[test]
500    fn test_hex_literal() {
501        // Simple checks.
502        assert_eq!(Color::hex_literal(0x000000), Color::rgb(0, 0, 0));
503        assert_eq!(Color::hex_literal(0xFFFFFF), Color::rgb(255, 255, 255));
504        assert_eq!(Color::hex_literal(0xFF0000), Color::rgb(255, 0, 0));
505        assert_eq!(Color::hex_literal(0x00FF00), Color::rgb(0, 255, 0));
506        assert_eq!(Color::hex_literal(0x0000FF), Color::rgb(0, 0, 255));
507        assert_eq!(Color::hex_literal(0xFFFF00), Color::rgb(255, 255, 0));
508        assert_eq!(Color::hex_literal(0xFF00FF), Color::rgb(255, 0, 255));
509        assert_eq!(Color::hex_literal(0x00FFFF), Color::rgb(0, 255, 255));
510        assert_eq!(Color::hex_literal(0x008000), Color::rgb(0, 128, 0));
511
512        // Spot check for value outside the RGB range (should return default color).
513        assert_eq!(Color::hex_literal(0xFFFFFFFF), Color::default());
514    }
515
516    #[test]
517    fn test_hex_str() {
518        // Checks with #.
519        assert_eq!(Color::hex_str("#000000"), Color::rgb(0, 0, 0));
520        assert_eq!(Color::hex_str("#FFFFFF"), Color::rgb(255, 255, 255));
521        assert_eq!(Color::hex_str("#FF0000"), Color::rgb(255, 0, 0));
522        assert_eq!(Color::hex_str("#00FF00"), Color::rgb(0, 255, 0));
523        assert_eq!(Color::hex_str("#0000FF"), Color::rgb(0, 0, 255));
524        assert_eq!(Color::hex_str("#FFFF00"), Color::rgb(255, 255, 0));
525        assert_eq!(Color::hex_str("#FF00FF"), Color::rgb(255, 0, 255));
526        assert_eq!(Color::hex_str("#00FFFF"), Color::rgb(0, 255, 255));
527        assert_eq!(Color::hex_str("#008000"), Color::rgb(0, 128, 0));
528
529        // Checks without #.
530        assert_eq!(Color::hex_str("000000"), Color::rgb(0, 0, 0));
531        assert_eq!(Color::hex_str("FFFFFF"), Color::rgb(255, 255, 255));
532        assert_eq!(Color::hex_str("FF0000"), Color::rgb(255, 0, 0));
533        assert_eq!(Color::hex_str("00FF00"), Color::rgb(0, 255, 0));
534        assert_eq!(Color::hex_str("0000FF"), Color::rgb(0, 0, 255));
535        assert_eq!(Color::hex_str("FFFF00"), Color::rgb(255, 255, 0));
536        assert_eq!(Color::hex_str("FF00FF"), Color::rgb(255, 0, 255));
537        assert_eq!(Color::hex_str("00FFFF"), Color::rgb(0, 255, 255));
538        assert_eq!(Color::hex_str("008000"), Color::rgb(0, 128, 0));
539
540        // Spot check for value outside the RGB range (should return default color).
541        assert_eq!(Color::hex_str("#FFFFFFFF"), Color::default());
542    }
543
544    #[test]
545    fn test_alpha() {
546        // Simple check with valid alpha.
547        assert_eq!(
548            Color::rgb(20, 30, 40).alpha(0.5),
549            Color::rgba(20, 30, 40, 0.5)
550        );
551
552        // Check clamping at lower bound.
553        assert_eq!(
554            Color::rgb(20, 30, 40).alpha(-0.1),
555            Color::rgba(20, 30, 40, 0.0)
556        );
557
558        // Check clamping at upper bound.
559        assert_eq!(
560            Color::rgb(20, 30, 40).alpha(1.1),
561            Color::rgba(20, 30, 40, 1.0)
562        );
563    }
564
565    #[test]
566    fn test_to_plotly_rgba() {
567        assert_eq!(
568            Color::rgba(20, 30, 40, 0.5).to_plotly_rgba(),
569            Rgba::new(20, 30, 40, 0.5)
570        );
571    }
572
573    #[test]
574    fn test_default() {
575        assert_eq!(Color::default(), Color::rgba(0, 0, 0, 1.0));
576    }
577}