Skip to content

Commit cb21109

Browse files
committed
Add ColorSequence and Plotly Express qualitative color sequences
Add plotly::qualitative with ColorSequence (len, modular get, reversed) and all 19 px.colors.qualitative sequences plus reversed *_R variants, sourced from plotly.py 7.1.0. Fixes #396
1 parent 00fe051 commit cb21109

4 files changed

Lines changed: 387 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
2020
- [[#422](https://github.com/plotly/plotly.rs/issues/422)] Add `Indicator`, `Histogram2d`, `Icicle` trace types
2121
- [[#425](https://github.com/plotly/plotly.rs/issues/425)] Add `Splom` and `Parcats` trace types (scatter-plot matrix and parallel categories)
2222
- [[#432](https://github.com/plotly/plotly.rs/issues/432)] Add `Funnel` and `Waterfall` trace types
23+
- [[#396](https://github.com/plotly/plotly.rs/issues/396)] Add `ColorSequence` and Plotly Express qualitative color sequences (`plotly::qualitative`), including reversed (`*_R`) variants
2324

2425
### Changed
2526

‎plotly/src/common/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod color;
2+
pub mod qualitative;
23

34
use plotly_derive::FieldSetter;
45
use serde::{Serialize, Serializer};

‎plotly/src/common/qualitative.rs‎

Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
//! Discrete (qualitative) color sequences from Plotly Express.
2+
//!
3+
//! These mirror [`plotly.colors.qualitative`] in Plotly.py
4+
//! (<https://plotly.com/python/discrete-color/#color-sequences-in-plotly-express>)
5+
//! and can be used wherever a list of CSS colors is accepted, e.g. marker
6+
//! colors or `layout.colorway`.
7+
//!
8+
//! Colors are stored as uppercase `#RRGGBB` hex strings. Plotly.py stores some
9+
//! sequences as `rgb(...)` triplets; the RGB channel values are identical.
10+
//!
11+
//! Each sequence has a reversed counterpart (`*_R`), equivalent to the Python
12+
//! `*_r` lists (e.g. `PLOTLY_R` ↔ `qualitative.Plotly_r`).
13+
//!
14+
//! # Example
15+
//! ```rust
16+
//! use plotly::qualitative;
17+
//!
18+
//! let mut colors = Vec::new();
19+
//! for i in 0..12 {
20+
//! // Cycles when there are more series than colors
21+
//! colors.push(qualitative::PLOTLY.get(i).to_string());
22+
//! }
23+
//! assert_eq!(colors[0], "#636EFA");
24+
//! assert_eq!(colors[10], "#636EFA");
25+
//! ```
26+
27+
/// A fixed sequence of CSS colors with modular indexing.
28+
///
29+
/// Inspired by `plotly.express` color sequences (`color_discrete_sequence`).
30+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31+
pub struct ColorSequence {
32+
colors: &'static [&'static str],
33+
reversed: bool,
34+
}
35+
36+
impl ColorSequence {
37+
/// Create a color sequence from a static slice of CSS colors.
38+
pub const fn new(colors: &'static [&'static str]) -> Self {
39+
Self {
40+
colors,
41+
reversed: false,
42+
}
43+
}
44+
45+
/// Number of colors in the sequence (independent of reversal).
46+
pub const fn len(&self) -> usize {
47+
self.colors.len()
48+
}
49+
50+
/// Returns `true` if the sequence contains no colors.
51+
pub const fn is_empty(&self) -> bool {
52+
self.colors.is_empty()
53+
}
54+
55+
/// Whether this sequence yields colors in reverse declaration order.
56+
pub const fn is_reversed(&self) -> bool {
57+
self.reversed
58+
}
59+
60+
/// Return a sequence with the same colors yielded in reverse order.
61+
pub const fn reversed(&self) -> Self {
62+
Self {
63+
colors: self.colors,
64+
reversed: !self.reversed,
65+
}
66+
}
67+
68+
/// Get the color at `index`, wrapping around with modulo arithmetic.
69+
///
70+
/// Indexing follows display order: for a reversed sequence, index `0` is
71+
/// the last declared color.
72+
///
73+
/// # Panics
74+
///
75+
/// Panics if the sequence is empty.
76+
pub fn get(&self, index: usize) -> &'static str {
77+
let n = self.colors.len();
78+
debug_assert!(n > 0, "ColorSequence::get on empty sequence");
79+
let i = index % n;
80+
if self.reversed {
81+
self.colors[n - 1 - i]
82+
} else {
83+
self.colors[i]
84+
}
85+
}
86+
87+
/// Iterate colors in display order.
88+
pub fn iter(&self) -> impl Iterator<Item = &'static str> + use<'_> {
89+
let n = self.colors.len();
90+
(0..n).map(move |i| self.get(i))
91+
}
92+
93+
/// Collect colors in display order into a `Vec` (convenient for APIs
94+
/// expecting `Vec<impl Color>` / marker color lists).
95+
pub fn as_vec(&self) -> Vec<&'static str> {
96+
self.iter().collect()
97+
}
98+
}
99+
100+
impl IntoIterator for ColorSequence {
101+
type Item = &'static str;
102+
type IntoIter = std::vec::IntoIter<&'static str>;
103+
104+
fn into_iter(self) -> Self::IntoIter {
105+
self.as_vec().into_iter()
106+
}
107+
}
108+
109+
impl IntoIterator for &ColorSequence {
110+
type Item = &'static str;
111+
type IntoIter = std::vec::IntoIter<&'static str>;
112+
113+
fn into_iter(self) -> Self::IntoIter {
114+
self.as_vec().into_iter()
115+
}
116+
}
117+
118+
const PLOTLY_COLORS: [&str; 10] = [
119+
"#636EFA", "#EF553B", "#00CC96", "#AB63FA", "#FFA15A", "#19D3F3", "#FF6692", "#B6E880",
120+
"#FF97FF", "#FECB52",
121+
];
122+
const D3_COLORS: [&str; 10] = [
123+
"#1F77B4", "#FF7F0E", "#2CA02C", "#D62728", "#9467BD", "#8C564B", "#E377C2", "#7F7F7F",
124+
"#BCBD22", "#17BECF",
125+
];
126+
const G10_COLORS: [&str; 10] = [
127+
"#3366CC", "#DC3912", "#FF9900", "#109618", "#990099", "#0099C6", "#DD4477", "#66AA00",
128+
"#B82E2E", "#316395",
129+
];
130+
const T10_COLORS: [&str; 10] = [
131+
"#4C78A8", "#F58518", "#E45756", "#72B7B2", "#54A24B", "#EECA3B", "#B279A2", "#FF9DA6",
132+
"#9D755D", "#BAB0AC",
133+
];
134+
const ALPHABET_COLORS: [&str; 26] = [
135+
"#AA0DFE", "#3283FE", "#85660D", "#782AB6", "#565656", "#1C8356", "#16FF32", "#F7E1A0",
136+
"#E2E2E2", "#1CBE4F", "#C4451C", "#DEA0FD", "#FE00FA", "#325A9B", "#FEAF16", "#F8A19F",
137+
"#90AD1C", "#F6222E", "#1CFFCE", "#2ED9FF", "#B10DA1", "#C075A6", "#FC1CBF", "#B00068",
138+
"#FBE426", "#FA0087",
139+
];
140+
const DARK24_COLORS: [&str; 24] = [
141+
"#2E91E5", "#E15F99", "#1CA71C", "#FB0D0D", "#DA16FF", "#222A2A", "#B68100", "#750D86",
142+
"#EB663B", "#511CFB", "#00A08B", "#FB00D1", "#FC0080", "#B2828D", "#6C7C32", "#778AAE",
143+
"#862A16", "#A777F1", "#620042", "#1616A7", "#DA60CA", "#6C4516", "#0D2A63", "#AF0038",
144+
];
145+
const LIGHT24_COLORS: [&str; 24] = [
146+
"#FD3216", "#00FE35", "#6A76FC", "#FED4C4", "#FE00CE", "#0DF9FF", "#F6F926", "#FF9616",
147+
"#479B55", "#EEA6FB", "#DC587D", "#D626FF", "#6E899C", "#00B5F7", "#B68E00", "#C9FBE5",
148+
"#FF0092", "#22FFA7", "#E3EE9E", "#86CE00", "#BC7196", "#7E7DCD", "#FC6955", "#E48F72",
149+
];
150+
const SET1_COLORS: [&str; 9] = [
151+
"#E41A1C", "#377EB8", "#4DAF4A", "#984EA3", "#FF7F00", "#FFFF33", "#A65628", "#F781BF",
152+
"#999999",
153+
];
154+
const PASTEL1_COLORS: [&str; 9] = [
155+
"#FBB4AE", "#B3CDE3", "#CCEBC5", "#DECBE4", "#FED9A6", "#FFFFCC", "#E5D8BD", "#FDDAEC",
156+
"#F2F2F2",
157+
];
158+
const DARK2_COLORS: [&str; 8] = [
159+
"#1B9E77", "#D95F02", "#7570B3", "#E7298A", "#66A61E", "#E6AB02", "#A6761D", "#666666",
160+
];
161+
const SET2_COLORS: [&str; 8] = [
162+
"#66C2A5", "#FC8D62", "#8DA0CB", "#E78AC3", "#A6D854", "#FFD92F", "#E5C494", "#B3B3B3",
163+
];
164+
const PASTEL2_COLORS: [&str; 8] = [
165+
"#B3E2CD", "#FDCDAC", "#CBD5E8", "#F4CAE4", "#E6F5C9", "#FFF2AE", "#F1E2CC", "#CCCCCC",
166+
];
167+
const SET3_COLORS: [&str; 12] = [
168+
"#8DD3C7", "#FFFFB3", "#BEBADA", "#FB8072", "#80B1D3", "#FDB462", "#B3DE69", "#FCCDE5",
169+
"#D9D9D9", "#BC80BD", "#CCEBC5", "#FFED6F",
170+
];
171+
const ANTIQUE_COLORS: [&str; 11] = [
172+
"#855C75", "#D9AF6B", "#AF6458", "#736F4C", "#526A83", "#625377", "#68855C", "#9C9C5E",
173+
"#A06177", "#8C785D", "#7C7C7C",
174+
];
175+
const BOLD_COLORS: [&str; 11] = [
176+
"#7F3C8D", "#11A579", "#3969AC", "#F2B701", "#E73F74", "#80BA5A", "#E68310", "#008695",
177+
"#CF1C90", "#F97B72", "#A5AA99",
178+
];
179+
const PASTEL_COLORS: [&str; 11] = [
180+
"#66C5CC", "#F6CF71", "#F89C74", "#DCB0F2", "#87C55F", "#9EB9F3", "#FE88B1", "#C9DB74",
181+
"#8BE0A4", "#B497E7", "#B3B3B3",
182+
];
183+
const PRISM_COLORS: [&str; 11] = [
184+
"#5F4690", "#1D6996", "#38A6A5", "#0F8554", "#73AF48", "#EDAD08", "#E17C05", "#CC503E",
185+
"#94346E", "#6F4070", "#666666",
186+
];
187+
const SAFE_COLORS: [&str; 11] = [
188+
"#88CCEE", "#CC6677", "#DDCC77", "#117733", "#332288", "#AA4499", "#44AA99", "#999933",
189+
"#882255", "#661100", "#888888",
190+
];
191+
const VIVID_COLORS: [&str; 11] = [
192+
"#E58606", "#5D69B1", "#52BCA3", "#99C945", "#CC61B0", "#24796C", "#DAA51B", "#2F8AC4",
193+
"#764E9F", "#ED645A", "#A5AA99",
194+
];
195+
196+
/// `Plotly` color sequence (10 colors).
197+
pub const PLOTLY: ColorSequence = ColorSequence::new(&PLOTLY_COLORS);
198+
/// Reversed `Plotly` (Python `plotly.colors.qualitative.Plotly_r`).
199+
pub const PLOTLY_R: ColorSequence = PLOTLY.reversed();
200+
/// `D3` color sequence (10 colors).
201+
pub const D3: ColorSequence = ColorSequence::new(&D3_COLORS);
202+
/// Reversed `D3` (Python `plotly.colors.qualitative.D3_r`).
203+
pub const D3_R: ColorSequence = D3.reversed();
204+
/// `G10` color sequence (10 colors).
205+
pub const G10: ColorSequence = ColorSequence::new(&G10_COLORS);
206+
/// Reversed `G10` (Python `plotly.colors.qualitative.G10_r`).
207+
pub const G10_R: ColorSequence = G10.reversed();
208+
/// `T10` color sequence (10 colors).
209+
pub const T10: ColorSequence = ColorSequence::new(&T10_COLORS);
210+
/// Reversed `T10` (Python `plotly.colors.qualitative.T10_r`).
211+
pub const T10_R: ColorSequence = T10.reversed();
212+
/// `Alphabet` color sequence (26 colors).
213+
pub const ALPHABET: ColorSequence = ColorSequence::new(&ALPHABET_COLORS);
214+
/// Reversed `Alphabet` (Python `plotly.colors.qualitative.Alphabet_r`).
215+
pub const ALPHABET_R: ColorSequence = ALPHABET.reversed();
216+
/// `Dark24` color sequence (24 colors).
217+
pub const DARK24: ColorSequence = ColorSequence::new(&DARK24_COLORS);
218+
/// Reversed `Dark24` (Python `plotly.colors.qualitative.Dark24_r`).
219+
pub const DARK24_R: ColorSequence = DARK24.reversed();
220+
/// `Light24` color sequence (24 colors).
221+
pub const LIGHT24: ColorSequence = ColorSequence::new(&LIGHT24_COLORS);
222+
/// Reversed `Light24` (Python `plotly.colors.qualitative.Light24_r`).
223+
pub const LIGHT24_R: ColorSequence = LIGHT24.reversed();
224+
/// `Set1` color sequence (9 colors).
225+
pub const SET1: ColorSequence = ColorSequence::new(&SET1_COLORS);
226+
/// Reversed `Set1` (Python `plotly.colors.qualitative.Set1_r`).
227+
pub const SET1_R: ColorSequence = SET1.reversed();
228+
/// `Pastel1` color sequence (9 colors).
229+
pub const PASTEL1: ColorSequence = ColorSequence::new(&PASTEL1_COLORS);
230+
/// Reversed `Pastel1` (Python `plotly.colors.qualitative.Pastel1_r`).
231+
pub const PASTEL1_R: ColorSequence = PASTEL1.reversed();
232+
/// `Dark2` color sequence (8 colors).
233+
pub const DARK2: ColorSequence = ColorSequence::new(&DARK2_COLORS);
234+
/// Reversed `Dark2` (Python `plotly.colors.qualitative.Dark2_r`).
235+
pub const DARK2_R: ColorSequence = DARK2.reversed();
236+
/// `Set2` color sequence (8 colors).
237+
pub const SET2: ColorSequence = ColorSequence::new(&SET2_COLORS);
238+
/// Reversed `Set2` (Python `plotly.colors.qualitative.Set2_r`).
239+
pub const SET2_R: ColorSequence = SET2.reversed();
240+
/// `Pastel2` color sequence (8 colors).
241+
pub const PASTEL2: ColorSequence = ColorSequence::new(&PASTEL2_COLORS);
242+
/// Reversed `Pastel2` (Python `plotly.colors.qualitative.Pastel2_r`).
243+
pub const PASTEL2_R: ColorSequence = PASTEL2.reversed();
244+
/// `Set3` color sequence (12 colors).
245+
pub const SET3: ColorSequence = ColorSequence::new(&SET3_COLORS);
246+
/// Reversed `Set3` (Python `plotly.colors.qualitative.Set3_r`).
247+
pub const SET3_R: ColorSequence = SET3.reversed();
248+
/// `Antique` color sequence (11 colors).
249+
pub const ANTIQUE: ColorSequence = ColorSequence::new(&ANTIQUE_COLORS);
250+
/// Reversed `Antique` (Python `plotly.colors.qualitative.Antique_r`).
251+
pub const ANTIQUE_R: ColorSequence = ANTIQUE.reversed();
252+
/// `Bold` color sequence (11 colors).
253+
pub const BOLD: ColorSequence = ColorSequence::new(&BOLD_COLORS);
254+
/// Reversed `Bold` (Python `plotly.colors.qualitative.Bold_r`).
255+
pub const BOLD_R: ColorSequence = BOLD.reversed();
256+
/// `Pastel` color sequence (11 colors).
257+
pub const PASTEL: ColorSequence = ColorSequence::new(&PASTEL_COLORS);
258+
/// Reversed `Pastel` (Python `plotly.colors.qualitative.Pastel_r`).
259+
pub const PASTEL_R: ColorSequence = PASTEL.reversed();
260+
/// `Prism` color sequence (11 colors).
261+
pub const PRISM: ColorSequence = ColorSequence::new(&PRISM_COLORS);
262+
/// Reversed `Prism` (Python `plotly.colors.qualitative.Prism_r`).
263+
pub const PRISM_R: ColorSequence = PRISM.reversed();
264+
/// `Safe` color sequence (11 colors).
265+
pub const SAFE: ColorSequence = ColorSequence::new(&SAFE_COLORS);
266+
/// Reversed `Safe` (Python `plotly.colors.qualitative.Safe_r`).
267+
pub const SAFE_R: ColorSequence = SAFE.reversed();
268+
/// `Vivid` color sequence (11 colors).
269+
pub const VIVID: ColorSequence = ColorSequence::new(&VIVID_COLORS);
270+
/// Reversed `Vivid` (Python `plotly.colors.qualitative.Vivid_r`).
271+
pub const VIVID_R: ColorSequence = VIVID.reversed();
272+
273+
/// All qualitative sequences as `(python_name, sequence)` pairs, in Plotly.py
274+
/// module order.
275+
pub const ALL: &[(&str, ColorSequence)] = &[
276+
("Plotly", PLOTLY),
277+
("D3", D3),
278+
("G10", G10),
279+
("T10", T10),
280+
("Alphabet", ALPHABET),
281+
("Dark24", DARK24),
282+
("Light24", LIGHT24),
283+
("Set1", SET1),
284+
("Pastel1", PASTEL1),
285+
("Dark2", DARK2),
286+
("Set2", SET2),
287+
("Pastel2", PASTEL2),
288+
("Set3", SET3),
289+
("Antique", ANTIQUE),
290+
("Bold", BOLD),
291+
("Pastel", PASTEL),
292+
("Prism", PRISM),
293+
("Safe", SAFE),
294+
("Vivid", VIVID),
295+
];
296+
297+
#[cfg(test)]
298+
mod tests {
299+
use super::*;
300+
301+
#[test]
302+
fn lengths_match_python() {
303+
assert_eq!(PLOTLY.len(), 10);
304+
assert_eq!(D3.len(), 10);
305+
assert_eq!(G10.len(), 10);
306+
assert_eq!(T10.len(), 10);
307+
assert_eq!(ALPHABET.len(), 26);
308+
assert_eq!(DARK24.len(), 24);
309+
assert_eq!(LIGHT24.len(), 24);
310+
assert_eq!(SET1.len(), 9);
311+
assert_eq!(PASTEL1.len(), 9);
312+
assert_eq!(DARK2.len(), 8);
313+
assert_eq!(SET2.len(), 8);
314+
assert_eq!(PASTEL2.len(), 8);
315+
assert_eq!(SET3.len(), 12);
316+
assert_eq!(ANTIQUE.len(), 11);
317+
assert_eq!(BOLD.len(), 11);
318+
assert_eq!(PASTEL.len(), 11);
319+
assert_eq!(PRISM.len(), 11);
320+
assert_eq!(SAFE.len(), 11);
321+
assert_eq!(VIVID.len(), 11);
322+
}
323+
324+
#[test]
325+
fn first_and_last_colors() {
326+
assert_eq!(PLOTLY.get(0), "#636EFA");
327+
assert_eq!(PLOTLY.get(9), "#FECB52");
328+
assert_eq!(D3.get(0), "#1F77B4");
329+
assert_eq!(ALPHABET.get(0), "#AA0DFE");
330+
assert_eq!(ALPHABET.get(25), "#FA0087");
331+
// plotly.py carto.Safe[9] is rgb(102, 17, 0)
332+
assert_eq!(SAFE.get(9), "#661100");
333+
}
334+
335+
#[test]
336+
fn modular_indexing() {
337+
assert_eq!(PLOTLY.get(10), PLOTLY.get(0));
338+
assert_eq!(PLOTLY.get(23), PLOTLY.get(3));
339+
assert_eq!(PLOTLY_R.get(10), PLOTLY_R.get(0));
340+
}
341+
342+
#[test]
343+
fn reversed_order() {
344+
assert_eq!(PLOTLY_R.len(), PLOTLY.len());
345+
assert_eq!(PLOTLY_R.get(0), PLOTLY.get(PLOTLY.len() - 1));
346+
assert_eq!(PLOTLY_R.get(1), PLOTLY.get(PLOTLY.len() - 2));
347+
assert_eq!(PLOTLY_R.get(9), PLOTLY.get(0));
348+
assert!(PLOTLY_R.is_reversed());
349+
assert!(!PLOTLY.is_reversed());
350+
// Double reverse is identity
351+
assert_eq!(PLOTLY.reversed().reversed(), PLOTLY);
352+
}
353+
354+
#[test]
355+
fn all_has_19_forward_entries() {
356+
assert_eq!(ALL.len(), 19);
357+
let names: Vec<&str> = ALL.iter().map(|(n, _)| *n).collect();
358+
assert_eq!(
359+
names,
360+
vec![
361+
"Plotly", "D3", "G10", "T10", "Alphabet", "Dark24", "Light24", "Set1", "Pastel1",
362+
"Dark2", "Set2", "Pastel2", "Set3", "Antique", "Bold", "Pastel", "Prism", "Safe",
363+
"Vivid",
364+
]
365+
);
366+
}
367+
368+
#[test]
369+
fn iter_collect_display_order() {
370+
let forward: Vec<&str> = PLOTLY.iter().collect();
371+
let back: Vec<&str> = PLOTLY_R.iter().collect();
372+
assert_eq!(forward.len(), 10);
373+
assert_eq!(back, forward.iter().copied().rev().collect::<Vec<_>>());
374+
}
375+
376+
#[test]
377+
fn empty_sequence_get_panics() {
378+
let empty = ColorSequence::new(&[]);
379+
assert!(empty.is_empty());
380+
assert_eq!(empty.len(), 0);
381+
let result = std::panic::catch_unwind(|| empty.get(0));
382+
assert!(result.is_err());
383+
}
384+
}

0 commit comments

Comments
 (0)