1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
//! This module defines a generalized trait, [`ColorMap`], for a colormap—a
//! mapping of the numbers between 0 and 1 to colors in a continuous way—and
//! provides some common ones used in programs like MATLAB and in data
//! visualization everywhere.
use color::{Color, RGBColor};
use colorpoint::ColorPoint;
use coord::Coord;
use matplotlib_cmaps;
use std::iter::Iterator;
/// A trait that models a colormap, a continuous mapping of the numbers between 0 and 1 to
/// colors. Any color output format is supported, but it must be consistent.
pub trait ColorMap<T: Color + Sized> {
/// Maps a given number between 0 and 1 to a given output `Color`. This should never fail or panic
/// except for NaN and similar: there should be some Color that marks out-of-range data.
fn transform_single(&self, color: f64) -> T;
/// Maps a given collection of numbers between 0 and 1 to an iterator of `Color`s. Does not evaluate
/// lazily, because the colormap could have some sort of state that changes between iterations otherwise.
fn transform<U: IntoIterator<Item = f64>>(&self, inputs: U) -> Vec<T> {
// TODO: make to work on references?
inputs
.into_iter()
.map(|x| self.transform_single(x))
.collect()
}
}
/// A struct that describes different transformations of the numbers between 0 and 1 to themselves,
/// used for controlling the linearity or nonlinearity of gradients.
#[derive(Debug, PartialEq, Clone)]
pub enum NormalizeMapping {
/// A normal linear mapping: each number maps to itself.
Linear,
/// A cube root mapping: 1/8 would map to 1/2, for example. This has the effect of emphasizing the
/// differences in the low end of the range, which is useful for some data like sound intensity
/// that isn't perceived linearly.
Cbrt,
/// A generic mapping, taking as a value any function or closure that maps the integers from 0-1
/// to the same range. This should never fail.
Generic(fn(f64) -> f64),
}
impl NormalizeMapping {
/// Performs the given mapping on an input number, with undefined behavior or panics if the given
/// number is outside of the range (0, 1). Given an input between 0 and 1, should always output
/// another number in the same range.
pub fn normalize(&self, x: f64) -> f64 {
match *self {
NormalizeMapping::Linear => x,
NormalizeMapping::Cbrt => x.cbrt(),
NormalizeMapping::Generic(func) => func(x),
}
}
}
/// A gradient colormap: a continuous, evenly-spaced shift between two colors A and B such that 0 maps
/// to A, 1 maps to B, and any number in between maps to a weighted mix of them in a given
/// coordinate space. Uses the gradient functions in the [`ColorPoint`] trait to complete this.
/// Out-of-range values are simply clamped to the correct range: calling this on negative numbers
/// will return A, and calling this on numbers larger than 1 will return B.
#[derive(Debug, Clone)]
pub struct GradientColorMap<T: ColorPoint> {
/// The start of the gradient. Calling this colormap on 0 or any negative number returns this color.
pub start: T,
/// The end of the gradient. Calling this colormap on 1 or any larger number returns this color.
pub end: T,
/// Any additional added nonlinearity imposed on the gradient: for example, a cube root mapping
/// emphasizes differences in the low end of the range.
pub normalization: NormalizeMapping,
/// Any desired padding: offsets introduced that artificially shift the limits of the
/// range. Expressed as `(new_min, new_max)`, where both are floats and `new_min < new_max`. For
/// example, having padding of `(1/8, 1)` would remove the lower eighth of the color map while
/// keeping the overall map smooth and continuous. Padding of `(0., 1.)` is the default and normal
/// behavior.
pub padding: (f64, f64),
}
impl<T: ColorPoint> GradientColorMap<T> {
/// Constructs a new linear [`GradientColorMap`], without padding, from two colors.
pub fn new_linear(start: T, end: T) -> GradientColorMap<T> {
GradientColorMap {
start,
end,
normalization: NormalizeMapping::Linear,
padding: (0., 1.),
}
}
/// Constructs a new cube root [`GradientColorMap`], without padding, from two colors.
pub fn new_cbrt(start: T, end: T) -> GradientColorMap<T> {
GradientColorMap {
start,
end,
normalization: NormalizeMapping::Cbrt,
padding: (0., 1.),
}
}
}
impl<T: ColorPoint> ColorMap<T> for GradientColorMap<T> {
fn transform_single(&self, x: f64) -> T {
// clamp between 0 and 1 beforehand
let clamped = if x < 0. {
0.
} else if x > 1. {
1.
} else {
x
};
self.start
.padded_gradient(&self.end, self.padding.0, self.padding.1)(
self.normalization.normalize(clamped),
)
}
}
/// A colormap that linearly interpolates between a given series of values in an equally-spaced
/// progression. This is modeled off of the `matplotlib` Python library's `ListedColormap`, and is
/// only used to provide reference implementations of the standard matplotlib colormaps. Clamps values
/// outside of 0 to 1.
#[derive(Debug, Clone)]
pub struct ListedColorMap {
/// The list of values, as a vector of `[f64]` arrays that provide equally-spaced RGB values.
pub vals: Vec<[f64; 3]>,
}
impl<T: ColorPoint> ColorMap<T> for ListedColorMap {
/// Linearly interpolates by first finding the two colors on either boundary, and then using a
/// simple linear gradient. There's no need to instantiate every single Color, because the vast
/// majority of them aren't important for one computation.
fn transform_single(&self, x: f64) -> T {
let clamped = if x < 0. {
0.
} else if x > 1. {
1.
} else {
x
};
// TODO: keeping every Color in memory might be more efficient for large-scale
// transformation; if it's a performance issue, try and fix
// now find the two values that bound the clamped x
// get the index as a floating point: the integers on either side bound it
// we subtract 1 because 0-n is n+1 numbers, not n
// otherwise, 1 would map out of range
let float_ind = clamped * (self.vals.len() as f64 - 1.);
let ind1 = float_ind.floor() as usize;
let ind2 = float_ind.ceil() as usize;
if ind1 == ind2 {
// x is exactly on the boundary, no interpolation needed
let arr = self.vals[ind1]; // guaranteed to be in range
RGBColor::from(Coord {
x: arr[0],
y: arr[1],
z: arr[2],
})
.convert()
} else {
// interpolate
let arr1 = self.vals[ind1];
let arr2 = self.vals[ind2];
let coord1 = Coord {
x: arr1[0],
y: arr1[1],
z: arr1[2],
};
let coord2 = Coord {
x: arr2[0],
y: arr2[1],
z: arr2[2],
};
// now interpolate and convert to the desired type
let rgb: RGBColor = coord2.weighted_midpoint(&coord1, clamped).into();
rgb.convert()
}
}
}
// now just constructors
impl ListedColorMap {
// TODO: In the future, I'd like to remove this weird array type bound if possible
/// Initializes a ListedColorMap from an iterator of arrays [R, G, B].
pub fn new<T: Iterator<Item = [f64; 3]>>(vals: T) -> ListedColorMap {
ListedColorMap {
vals: vals.collect(),
}
}
/// Initializes a viridis colormap, a pleasing blue-green-yellow colormap that is perceptually
/// uniform with respect to luminance, found in Python's `matplotlib` as the default
/// colormap.
pub fn viridis() -> ListedColorMap {
let vals = matplotlib_cmaps::VIRIDIS_DATA.to_vec();
ListedColorMap { vals }
}
/// Initializes a magma colormap, a pleasing blue-purple-red-yellow map that is perceptually
/// uniform with respect to luminance, found in Python's `matplotlib.`
pub fn magma() -> ListedColorMap {
let vals = matplotlib_cmaps::MAGMA_DATA.to_vec();
ListedColorMap { vals }
}
/// Initializes an inferno colormap, a pleasing blue-purple-red-yellow map similar to magma, but
/// with a slight shift towards red and yellow, that is perceptually uniform with respect to
/// luminance, found in Python's `matplotlib.`
pub fn inferno() -> ListedColorMap {
let vals = matplotlib_cmaps::INFERNO_DATA.to_vec();
ListedColorMap { vals }
}
/// Initializes a plasma colormap, a pleasing blue-purple-red-yellow map that is perceptually
/// uniform with respect to luminance, found in Python's `matplotlib.` It eschews the really dark
/// blue found in inferno and magma, instead starting at a fairly bright blue.
pub fn plasma() -> ListedColorMap {
let vals = matplotlib_cmaps::PLASMA_DATA.to_vec();
ListedColorMap { vals }
}
/// Initializes a cividis colormap, a pleasing shades of blue-yellow map that is perceptually
/// uniform with respect to luminance, found in Python's `matplotlib.`
pub fn cividis() -> ListedColorMap {
let vals = matplotlib_cmaps::CIVIDIS_DATA.to_vec();
ListedColorMap { vals }
}
/// Initializes a turbo colormap, a pleasing blue-green-red map that is perceptually
/// uniform with respect to luminance, found in Python's `matplotlib.`
pub fn turbo() -> ListedColorMap {
let vals = matplotlib_cmaps::TURBO_DATA.to_vec();
ListedColorMap { vals }
}
/// "circle" is a constant-brightness, perceptually uniform cyclic rainbow map
/// going from magenta through blue, green and red back to magenta.
pub fn circle() -> ListedColorMap {
let vals = matplotlib_cmaps::CIRCLE_DATA.to_vec();
ListedColorMap { vals }
}
/// "bluered" is a diverging colormap going from dark magenta/blue/cyan to yellow/red/dark purple,
/// analogously to "RdBu_r" but with higher contrast and more uniform gradient. It is suitable for
/// plotting velocity maps (blue/redshifted) and is similar to "breeze" and "mist" in this respect,
/// but has (nearly) white as the central color instead of green.
/// It is also cyclic (same colors at endpoints).
pub fn bluered() -> ListedColorMap {
let vals = matplotlib_cmaps::BLUERED_DATA.to_vec();
ListedColorMap { vals }
}
/// "breeze" is a better-balanced version of "jet", with diverging luminosity profile,
/// going from dark blue to bright green in the center and then back to dark red.
/// It is nearly perceptually uniform, unlike the original jet map.
pub fn breeze() -> ListedColorMap {
let vals = matplotlib_cmaps::BREEZE_DATA.to_vec();
ListedColorMap { vals }
}
/// "mist" is another replacement for "jet" or "rainbow" maps, which differs from "breeze" by
/// having smaller dynamical range in brightness. The red and blue endpoints are darker than
/// the green center, but not as dark as in "breeze", while the center is not as bright.
pub fn mist() -> ListedColorMap {
let vals = matplotlib_cmaps::MIST_DATA.to_vec();
ListedColorMap { vals }
}
/// "earth" is a rainbow-like colormap with increasing luminosity, going from black through
// dark blue, medium green in the middle and light red/orange to white.
// # It is nearly perceptually uniform, monotonic in luminosity, and is suitable for
// # plotting nearly anything, especially velocity maps (blue/redshifted).
// # It resembles "gist_earth" (but with more vivid colors) or MATLAB's "parula".
pub fn earth() -> ListedColorMap {
let vals = matplotlib_cmaps::EARTH_DATA.to_vec();
ListedColorMap { vals }
}
/// "hell" is a slightly tuned version of "inferno", with the main difference that it goes to
// # pure white at the bright end (starts from black, then dark blue/purple, red in the middle,
// # yellow and white). It is fully perceptually uniform and monotonic in luminosity.
pub fn hell() -> ListedColorMap {
let vals = matplotlib_cmaps::HELL_DATA.to_vec();
ListedColorMap { vals }
}
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
use color::RGBColor;
#[test]
fn test_linear_gradient() {
let red = RGBColor::from_hex_code("#ff0000").unwrap();
let blue = RGBColor::from_hex_code("#0000ff").unwrap();
let cmap = GradientColorMap::new_linear(red, blue);
let vals = vec![-0.2, 0., 1. / 15., 1. / 5., 4. / 5., 1., 100.];
let cols = cmap.transform(vals);
let strs = vec![
"#FF0000", "#FF0000", "#EE0011", "#CC0033", "#3300CC", "#0000FF", "#0000FF",
];
for (i, col) in cols.into_iter().enumerate() {
assert_eq!(col.to_string(), strs[i]);
}
}
#[test]
fn test_cbrt_gradient() {
let red = RGBColor::from_hex_code("#CC0000").unwrap();
let blue = RGBColor::from_hex_code("#0000CC").unwrap();
let cmap = GradientColorMap::new_cbrt(red, blue);
let vals = vec![-0.2, 0., 1. / 27., 1. / 8., 8. / 27., 1., 100.];
let cols = cmap.transform(vals);
let strs = vec![
"#CC0000", "#CC0000", "#880044", "#660066", "#440088", "#0000CC", "#0000CC",
];
for (i, col) in cols.into_iter().enumerate() {
assert_eq!(col.to_string(), strs[i]);
}
}
#[test]
fn test_padding() {
let red = RGBColor::from_hex_code("#CC0000").unwrap();
let blue = RGBColor::from_hex_code("#0000CC").unwrap();
let mut cmap = GradientColorMap::new_cbrt(red, blue);
cmap.padding = (0.25, 0.75);
// essentially, start and end are now #990033 and #330099
let vals = vec![-0.2, 0., 1. / 27., 1. / 8., 8. / 27., 1., 100.];
let cols = cmap.transform(vals);
let strs = vec![
"#990033", "#990033", "#770055", "#660066", "#550077", "#330099", "#330099",
];
for (i, col) in cols.into_iter().enumerate() {
assert_eq!(col.to_string(), strs[i]);
}
}
#[test]
fn test_mpl_colormaps() {
let viridis = ListedColorMap::viridis();
let magma = ListedColorMap::magma();
let inferno = ListedColorMap::inferno();
let plasma = ListedColorMap::plasma();
let vals = vec![-0.2, 0., 0.2, 0.4, 0.6, 0.8, 1., 100.];
// these values were taken using matplotlib
let viridis_colors = [
[0.267004, 0.004874, 0.329415],
[0.267004, 0.004874, 0.329415],
[0.253935, 0.265254, 0.529983],
[0.163625, 0.471133, 0.558148],
[0.134692, 0.658636, 0.517649],
[0.477504, 0.821444, 0.318195],
[0.993248, 0.906157, 0.143936],
[0.993248, 0.906157, 0.143936],
];
let magma_colors = [
[1.46200000e-03, 4.66000000e-04, 1.38660000e-02],
[1.46200000e-03, 4.66000000e-04, 1.38660000e-02],
[2.32077000e-01, 5.98890000e-02, 4.37695000e-01],
[5.50287000e-01, 1.61158000e-01, 5.05719000e-01],
[8.68793000e-01, 2.87728000e-01, 4.09303000e-01],
[9.94738000e-01, 6.24350000e-01, 4.27397000e-01],
[9.87053000e-01, 9.91438000e-01, 7.49504000e-01],
[9.87053000e-01, 9.91438000e-01, 7.49504000e-01],
];
let plasma_colors = [
[5.03830000e-02, 2.98030000e-02, 5.27975000e-01],
[5.03830000e-02, 2.98030000e-02, 5.27975000e-01],
[4.17642000e-01, 5.64000000e-04, 6.58390000e-01],
[6.92840000e-01, 1.65141000e-01, 5.64522000e-01],
[8.81443000e-01, 3.92529000e-01, 3.83229000e-01],
[9.88260000e-01, 6.52325000e-01, 2.11364000e-01],
[9.40015000e-01, 9.75158000e-01, 1.31326000e-01],
[9.40015000e-01, 9.75158000e-01, 1.31326000e-01],
];
let inferno_colors = [
[1.46200000e-03, 4.66000000e-04, 1.38660000e-02],
[1.46200000e-03, 4.66000000e-04, 1.38660000e-02],
[2.58234000e-01, 3.85710000e-02, 4.06485000e-01],
[5.78304000e-01, 1.48039000e-01, 4.04411000e-01],
[8.65006000e-01, 3.16822000e-01, 2.26055000e-01],
[9.87622000e-01, 6.45320000e-01, 3.98860000e-02],
[9.88362000e-01, 9.98364000e-01, 6.44924000e-01],
[9.88362000e-01, 9.98364000e-01, 6.44924000e-01],
];
let colors = vec![viridis_colors, magma_colors, inferno_colors, plasma_colors];
let cmaps = vec![viridis, magma, inferno, plasma];
for (colors, cmap) in colors.iter().zip(cmaps.iter()) {
for (ref_arr, test_color) in colors.iter().zip(cmap.transform(vals.clone()).iter()) {
let ref_color = RGBColor {
r: ref_arr[0],
g: ref_arr[1],
b: ref_arr[2],
};
let deref_test_color: RGBColor = *test_color;
assert_eq!(deref_test_color.to_string(), ref_color.to_string());
}
}
}
}