Skip to main content

nexrad_render/
color.rs

1//! Color scales for radar data visualization.
2//!
3//! This module provides types for mapping radar moment values to colors.
4//! The primary type is [`DiscreteColorScale`], which maps value ranges to
5//! specific colors based on threshold levels. For rendering, the scale is
6//! converted to a [`ColorLookupTable`] which provides O(1) color lookups.
7
8/// An RGBA color with components in the range 0.0 to 1.0.
9///
10/// This type is used for defining color scales. Colors are converted to
11/// 8-bit RGBA values during rendering.
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct Color {
14    /// Red component (0.0 to 1.0)
15    pub r: f64,
16    /// Green component (0.0 to 1.0)
17    pub g: f64,
18    /// Blue component (0.0 to 1.0)
19    pub b: f64,
20    /// Alpha component (0.0 to 1.0)
21    pub a: f64,
22}
23
24impl Color {
25    /// Creates a new color from RGB components (alpha defaults to 1.0).
26    ///
27    /// Components should be in the range 0.0 to 1.0.
28    pub const fn rgb(r: f64, g: f64, b: f64) -> Self {
29        Self { r, g, b, a: 1.0 }
30    }
31
32    /// Creates a new color from RGBA components.
33    ///
34    /// Components should be in the range 0.0 to 1.0.
35    pub const fn rgba(r: f64, g: f64, b: f64, a: f64) -> Self {
36        Self { r, g, b, a }
37    }
38
39    /// Converts the color to 8-bit RGBA values.
40    pub fn to_rgba8(&self) -> [u8; 4] {
41        [
42            (self.r * 255.0) as u8,
43            (self.g * 255.0) as u8,
44            (self.b * 255.0) as u8,
45            (self.a * 255.0) as u8,
46        ]
47    }
48
49    /// Black color.
50    pub const BLACK: Color = Color::rgb(0.0, 0.0, 0.0);
51
52    /// White color.
53    pub const WHITE: Color = Color::rgb(1.0, 1.0, 1.0);
54
55    /// Transparent (fully transparent black).
56    pub const TRANSPARENT: Color = Color::rgba(0.0, 0.0, 0.0, 0.0);
57}
58
59/// A single level in a discrete color scale.
60///
61/// Represents a threshold value and its associated color. Values at or above
62/// this threshold (but below the next higher threshold) will be rendered with
63/// this color.
64#[derive(Debug, Clone)]
65pub struct ColorScaleLevel {
66    value: f32,
67    color: Color,
68}
69
70impl ColorScaleLevel {
71    /// Creates a new color scale level.
72    ///
73    /// # Arguments
74    ///
75    /// * `value` - The threshold value
76    /// * `color` - The color to use for values at or above this threshold
77    pub fn new(value: f32, color: Color) -> Self {
78        Self { value, color }
79    }
80}
81
82/// A discrete color scale that maps value ranges to colors.
83///
84/// The scale works by finding the highest threshold that the input value
85/// exceeds, and returning the corresponding color. Levels are automatically
86/// sorted from highest to lowest threshold during construction.
87///
88/// # Example
89///
90/// ```
91/// use nexrad_render::{ColorScaleLevel, DiscreteColorScale, Color};
92///
93/// let scale = DiscreteColorScale::new(vec![
94///     ColorScaleLevel::new(0.0, Color::BLACK),
95///     ColorScaleLevel::new(30.0, Color::rgb(0.0, 1.0, 0.0)),
96///     ColorScaleLevel::new(50.0, Color::rgb(1.0, 0.0, 0.0)),
97/// ]);
98///
99/// // Values >= 50 return red, >= 30 return green, >= 0 return black
100/// ```
101#[derive(Debug, Clone)]
102pub struct DiscreteColorScale {
103    levels: Vec<ColorScaleLevel>,
104}
105
106impl DiscreteColorScale {
107    /// Creates a new discrete color scale from the given levels.
108    ///
109    /// Levels are automatically sorted from highest to lowest threshold.
110    pub fn new(mut levels: Vec<ColorScaleLevel>) -> Self {
111        levels.sort_by(|a, b| b.value.total_cmp(&a.value));
112        Self { levels }
113    }
114
115    /// Returns the color for the given value.
116    ///
117    /// Finds the highest threshold that the value exceeds and returns its color.
118    /// If the value is below all thresholds, returns the color of the lowest threshold.
119    pub fn color(&self, value: f32) -> Color {
120        let mut color = Color::BLACK;
121
122        for level in &self.levels {
123            if value >= level.value {
124                return level.color;
125            }
126
127            color = level.color;
128        }
129
130        color
131    }
132
133    /// Returns the levels in this color scale (sorted highest to lowest).
134    pub fn levels(&self) -> &[ColorScaleLevel] {
135        &self.levels
136    }
137}
138
139/// A color stop for a continuous color scale.
140///
141/// Defines a value-to-color control point. Colors are linearly interpolated
142/// between stops.
143#[derive(Debug, Clone)]
144pub struct ColorStop {
145    /// The value at this stop.
146    pub value: f32,
147    /// The color at this stop.
148    pub color: Color,
149}
150
151impl ColorStop {
152    /// Creates a new color stop.
153    pub fn new(value: f32, color: Color) -> Self {
154        Self { value, color }
155    }
156}
157
158/// A continuous color scale that linearly interpolates between color stops.
159///
160/// Unlike [`DiscreteColorScale`], this scale produces smooth color gradients
161/// between control points. This is useful for products where smooth transitions
162/// are more informative than discrete steps.
163///
164/// # Example
165///
166/// ```
167/// use nexrad_render::{ColorStop, ContinuousColorScale, Color};
168///
169/// let scale = ContinuousColorScale::new(vec![
170///     ColorStop::new(0.0, Color::rgb(0.0, 0.0, 1.0)),   // Blue at 0
171///     ColorStop::new(50.0, Color::rgb(0.0, 1.0, 0.0)),  // Green at 50
172///     ColorStop::new(100.0, Color::rgb(1.0, 0.0, 0.0)), // Red at 100
173/// ]);
174///
175/// // Value 25 produces a blue-green blend
176/// let color = scale.color(25.0);
177/// ```
178#[derive(Debug, Clone)]
179pub struct ContinuousColorScale {
180    stops: Vec<ColorStop>,
181}
182
183impl ContinuousColorScale {
184    /// Creates a new continuous color scale from color stops.
185    ///
186    /// Stops are automatically sorted by value (lowest to highest).
187    pub fn new(mut stops: Vec<ColorStop>) -> Self {
188        stops.sort_by(|a, b| a.value.total_cmp(&b.value));
189        Self { stops }
190    }
191
192    /// Returns the linearly interpolated color for the given value.
193    ///
194    /// Values below the lowest stop get the lowest stop's color.
195    /// Values above the highest stop get the highest stop's color.
196    pub fn color(&self, value: f32) -> Color {
197        if self.stops.is_empty() {
198            return Color::BLACK;
199        }
200
201        if value <= self.stops[0].value {
202            return self.stops[0].color;
203        }
204
205        let last = self.stops.len() - 1;
206        if value >= self.stops[last].value {
207            return self.stops[last].color;
208        }
209
210        // Find the two stops that bracket this value
211        for i in 0..last {
212            let low = &self.stops[i];
213            let high = &self.stops[i + 1];
214
215            if value >= low.value && value <= high.value {
216                let range = high.value - low.value;
217                if range == 0.0 {
218                    return low.color;
219                }
220                let t = ((value - low.value) / range) as f64;
221                return Color::rgba(
222                    low.color.r + (high.color.r - low.color.r) * t,
223                    low.color.g + (high.color.g - low.color.g) * t,
224                    low.color.b + (high.color.b - low.color.b) * t,
225                    low.color.a + (high.color.a - low.color.a) * t,
226                );
227            }
228        }
229
230        self.stops[last].color
231    }
232
233    /// Returns the stops in this color scale (sorted lowest to highest).
234    pub fn stops(&self) -> &[ColorStop] {
235        &self.stops
236    }
237}
238
239/// A color scale that can be either discrete or continuous.
240///
241/// This enum allows rendering functions to accept either type of color scale.
242#[derive(Debug, Clone)]
243pub enum ColorScale {
244    /// A discrete step-function color scale.
245    Discrete(DiscreteColorScale),
246    /// A continuous linear-interpolation color scale.
247    Continuous(ContinuousColorScale),
248}
249
250impl ColorScale {
251    /// Returns the color for the given value.
252    pub fn color(&self, value: f32) -> Color {
253        match self {
254            ColorScale::Discrete(scale) => scale.color(value),
255            ColorScale::Continuous(scale) => scale.color(value),
256        }
257    }
258}
259
260impl From<DiscreteColorScale> for ColorScale {
261    fn from(scale: DiscreteColorScale) -> Self {
262        ColorScale::Discrete(scale)
263    }
264}
265
266impl From<ContinuousColorScale> for ColorScale {
267    fn from(scale: ContinuousColorScale) -> Self {
268        ColorScale::Continuous(scale)
269    }
270}
271
272/// A pre-computed lookup table for O(1) color lookups.
273///
274/// This table maps a range of values to RGBA colors using a fixed-size array.
275/// It is created from a [`DiscreteColorScale`] and provides fast color lookups
276/// during rendering.
277///
278/// # Example
279///
280/// ```
281/// use nexrad_render::{ColorLookupTable, nws_reflectivity_scale};
282///
283/// let scale = nws_reflectivity_scale();
284/// let lut = ColorLookupTable::from_scale(&scale, -32.0, 95.0, 256);
285///
286/// // O(1) lookup returning [R, G, B, A] bytes
287/// let color = lut.color(45.0);
288/// ```
289#[derive(Debug, Clone)]
290pub struct ColorLookupTable {
291    /// RGBA color values indexed by quantized input value.
292    table: Vec<[u8; 4]>,
293    /// Minimum value in the mapped range.
294    min_value: f32,
295    /// Value range (max - min).
296    range: f32,
297}
298
299impl ColorLookupTable {
300    /// Creates a lookup table from a discrete color scale.
301    ///
302    /// # Arguments
303    ///
304    /// * `scale` - The color scale to sample from
305    /// * `min_value` - The minimum value to map
306    /// * `max_value` - The maximum value to map
307    /// * `size` - The number of entries in the lookup table (256 recommended)
308    ///
309    /// Values outside the min/max range will be clamped to the nearest entry.
310    pub fn from_scale(
311        scale: &DiscreteColorScale,
312        min_value: f32,
313        max_value: f32,
314        size: usize,
315    ) -> Self {
316        let range = max_value - min_value;
317        let mut table = Vec::with_capacity(size);
318
319        for i in 0..size {
320            let value = min_value + (i as f32 / (size - 1) as f32) * range;
321            let color = scale.color(value);
322            table.push(color.to_rgba8());
323        }
324
325        Self {
326            table,
327            min_value,
328            range,
329        }
330    }
331
332    /// Creates a lookup table from any [`ColorScale`] (discrete or continuous).
333    ///
334    /// This is the preferred way to create a LUT for the new rendering entry points.
335    pub fn from_color_scale(
336        scale: &ColorScale,
337        min_value: f32,
338        max_value: f32,
339        size: usize,
340    ) -> Self {
341        let range = max_value - min_value;
342        let mut table = Vec::with_capacity(size);
343
344        for i in 0..size {
345            let value = min_value + (i as f32 / (size - 1) as f32) * range;
346            let color = scale.color(value);
347            table.push(color.to_rgba8());
348        }
349
350        Self {
351            table,
352            min_value,
353            range,
354        }
355    }
356
357    /// Returns the RGBA color for the given value.
358    ///
359    /// This is an O(1) operation using direct array indexing.
360    #[inline]
361    pub fn color(&self, value: f32) -> [u8; 4] {
362        let normalized = (value - self.min_value) / self.range;
363        let index = (normalized * (self.table.len() - 1) as f32) as usize;
364        let index = index.min(self.table.len() - 1);
365        self.table[index]
366    }
367}
368
369/// Returns the standard NWS (National Weather Service) reflectivity color scale.
370///
371/// This scale uses colors commonly seen in weather radar displays, ranging
372/// from cyan/blue for light precipitation to magenta/white for extreme values.
373///
374/// | dBZ Range | Color | Meaning |
375/// |-----------|-------|---------|
376/// | 0-5 | Black | Below detection threshold |
377/// | 5-20 | Cyan/Blue | Light precipitation |
378/// | 20-35 | Green | Light to moderate precipitation |
379/// | 35-50 | Yellow/Orange | Moderate to heavy precipitation |
380/// | 50-65 | Red/Magenta | Heavy precipitation, possible hail |
381/// | 65+ | Purple/White | Extreme precipitation, likely hail |
382pub fn nws_reflectivity_scale() -> DiscreteColorScale {
383    DiscreteColorScale::new(vec![
384        ColorScaleLevel::new(0.0, Color::rgb(0.0000, 0.0000, 0.0000)),
385        ColorScaleLevel::new(5.0, Color::rgb(0.0000, 1.0000, 1.0000)),
386        ColorScaleLevel::new(10.0, Color::rgb(0.5294, 0.8078, 0.9216)),
387        ColorScaleLevel::new(15.0, Color::rgb(0.0000, 0.0000, 1.0000)),
388        ColorScaleLevel::new(20.0, Color::rgb(0.0000, 1.0000, 0.0000)),
389        ColorScaleLevel::new(25.0, Color::rgb(0.1961, 0.8039, 0.1961)),
390        ColorScaleLevel::new(30.0, Color::rgb(0.1333, 0.5451, 0.1333)),
391        ColorScaleLevel::new(35.0, Color::rgb(0.9333, 0.9333, 0.0000)),
392        ColorScaleLevel::new(40.0, Color::rgb(0.9333, 0.8627, 0.5098)),
393        ColorScaleLevel::new(45.0, Color::rgb(0.9333, 0.4627, 0.1294)),
394        ColorScaleLevel::new(50.0, Color::rgb(1.0000, 0.1882, 0.1882)),
395        ColorScaleLevel::new(55.0, Color::rgb(0.6902, 0.1882, 0.3765)),
396        ColorScaleLevel::new(60.0, Color::rgb(0.6902, 0.1882, 0.3765)),
397        ColorScaleLevel::new(65.0, Color::rgb(0.7294, 0.3333, 0.8275)),
398        ColorScaleLevel::new(70.0, Color::rgb(1.0000, 0.0000, 1.0000)),
399        ColorScaleLevel::new(75.0, Color::rgb(1.0000, 1.0000, 1.0000)),
400    ])
401}
402
403/// Returns a color scale for radial velocity data.
404///
405/// This divergent scale uses green for motion toward the radar (negative values)
406/// and red for motion away from the radar (positive values), with gray near zero.
407/// Range: -64 to +64 m/s (standard precipitation mode Doppler velocity).
408///
409/// | Velocity (m/s) | Color | Meaning |
410/// |----------------|-------|---------|
411/// | -64 to -48 | Dark Green | Strong inbound |
412/// | -48 to -32 | Green | Moderate inbound |
413/// | -32 to -16 | Light Green | Light inbound |
414/// | -16 to -4 | Pale Green | Very light inbound |
415/// | -4 to +4 | Gray | Near zero / RF |
416/// | +4 to +16 | Pale Red | Very light outbound |
417/// | +16 to +32 | Light Red/Pink | Light outbound |
418/// | +32 to +48 | Red | Moderate outbound |
419/// | +48 to +64 | Dark Red | Strong outbound |
420pub fn velocity_scale() -> DiscreteColorScale {
421    DiscreteColorScale::new(vec![
422        // Strong inbound (toward radar) - dark green
423        ColorScaleLevel::new(-64.0, Color::rgb(0.0000, 0.3922, 0.0000)),
424        ColorScaleLevel::new(-48.0, Color::rgb(0.0000, 0.5451, 0.0000)),
425        ColorScaleLevel::new(-32.0, Color::rgb(0.0000, 0.8039, 0.0000)),
426        ColorScaleLevel::new(-16.0, Color::rgb(0.5647, 0.9333, 0.5647)),
427        // Near zero - gray
428        ColorScaleLevel::new(-4.0, Color::rgb(0.6627, 0.6627, 0.6627)),
429        ColorScaleLevel::new(4.0, Color::rgb(0.6627, 0.6627, 0.6627)),
430        // Outbound (away from radar) - reds
431        ColorScaleLevel::new(16.0, Color::rgb(1.0000, 0.7529, 0.7961)),
432        ColorScaleLevel::new(32.0, Color::rgb(1.0000, 0.4118, 0.4118)),
433        ColorScaleLevel::new(48.0, Color::rgb(0.8039, 0.0000, 0.0000)),
434        ColorScaleLevel::new(64.0, Color::rgb(0.5451, 0.0000, 0.0000)),
435    ])
436}
437
438/// Returns a color scale for spectrum width data.
439///
440/// This sequential scale ranges from cool colors (low turbulence) to warm colors
441/// (high turbulence). Range: 0 to 30 m/s.
442///
443/// | Width (m/s) | Color | Meaning |
444/// |-------------|-------|---------|
445/// | 0-4 | Gray | Very low turbulence |
446/// | 4-8 | Blue | Low turbulence |
447/// | 8-12 | Cyan | Light turbulence |
448/// | 12-16 | Green | Moderate turbulence |
449/// | 16-20 | Yellow | Moderate-high turbulence |
450/// | 20-25 | Orange | High turbulence |
451/// | 25-30 | Red | Very high turbulence |
452pub fn spectrum_width_scale() -> DiscreteColorScale {
453    DiscreteColorScale::new(vec![
454        ColorScaleLevel::new(0.0, Color::rgb(0.5020, 0.5020, 0.5020)),
455        ColorScaleLevel::new(4.0, Color::rgb(0.0000, 0.0000, 0.8039)),
456        ColorScaleLevel::new(8.0, Color::rgb(0.0000, 0.8039, 0.8039)),
457        ColorScaleLevel::new(12.0, Color::rgb(0.0000, 0.8039, 0.0000)),
458        ColorScaleLevel::new(16.0, Color::rgb(0.9333, 0.9333, 0.0000)),
459        ColorScaleLevel::new(20.0, Color::rgb(1.0000, 0.6471, 0.0000)),
460        ColorScaleLevel::new(25.0, Color::rgb(1.0000, 0.0000, 0.0000)),
461    ])
462}
463
464/// Returns a color scale for differential reflectivity (ZDR) data.
465///
466/// This divergent scale shows negative values (vertically-oriented particles) in
467/// blue/purple, near-zero in gray, and positive values (horizontally-oriented
468/// particles like large raindrops) in yellow/orange/red.
469/// Range: -2 to +6 dB.
470///
471/// | ZDR (dB) | Color | Meaning |
472/// |----------|-------|---------|
473/// | -2 to -1 | Purple | Vertically oriented |
474/// | -1 to 0 | Blue | Slightly vertical |
475/// | 0 to 0.5 | Gray | Spherical particles |
476/// | 0.5 to 1.5 | Light Green | Slightly oblate |
477/// | 1.5 to 2.5 | Yellow | Oblate drops |
478/// | 2.5 to 4 | Orange | Large oblate drops |
479/// | 4 to 6 | Red | Very large drops/hail |
480pub fn differential_reflectivity_scale() -> DiscreteColorScale {
481    DiscreteColorScale::new(vec![
482        // Negative (vertically oriented)
483        ColorScaleLevel::new(-2.0, Color::rgb(0.5020, 0.0000, 0.5020)),
484        ColorScaleLevel::new(-1.0, Color::rgb(0.0000, 0.0000, 0.8039)),
485        // Near zero (spherical)
486        ColorScaleLevel::new(0.0, Color::rgb(0.6627, 0.6627, 0.6627)),
487        // Positive (horizontally oriented / oblate)
488        ColorScaleLevel::new(0.5, Color::rgb(0.5647, 0.9333, 0.5647)),
489        ColorScaleLevel::new(1.5, Color::rgb(0.9333, 0.9333, 0.0000)),
490        ColorScaleLevel::new(2.5, Color::rgb(1.0000, 0.6471, 0.0000)),
491        ColorScaleLevel::new(4.0, Color::rgb(1.0000, 0.0000, 0.0000)),
492    ])
493}
494
495/// Returns a color scale for correlation coefficient (CC/RhoHV) data.
496///
497/// This sequential scale emphasizes high values (0.9-1.0) which indicate
498/// meteorological targets. Lower values may indicate non-meteorological
499/// echoes, mixed precipitation, or tornadic debris.
500/// Range: 0.0 to 1.0.
501///
502/// | CC | Color | Meaning |
503/// |----|-------|---------|
504/// | 0.0-0.7 | Purple/Blue | Non-met or debris |
505/// | 0.7-0.85 | Cyan/Teal | Mixed phase/melting |
506/// | 0.85-0.92 | Light Green | Possible hail/graupel |
507/// | 0.92-0.96 | Green | Rain/snow mix |
508/// | 0.96-0.98 | Yellow | Pure rain or snow |
509/// | 0.98-1.0 | White/Light Gray | Uniform precipitation |
510pub fn correlation_coefficient_scale() -> DiscreteColorScale {
511    DiscreteColorScale::new(vec![
512        // Low CC - non-meteorological or debris
513        ColorScaleLevel::new(0.0, Color::rgb(0.0000, 0.0000, 0.0000)),
514        ColorScaleLevel::new(0.2, Color::rgb(0.3922, 0.0000, 0.5882)),
515        ColorScaleLevel::new(0.5, Color::rgb(0.0000, 0.0000, 0.8039)),
516        ColorScaleLevel::new(0.7, Color::rgb(0.0000, 0.5451, 0.5451)),
517        // Medium CC - mixed precipitation
518        ColorScaleLevel::new(0.85, Color::rgb(0.0000, 0.8039, 0.4000)),
519        ColorScaleLevel::new(0.92, Color::rgb(0.0000, 0.8039, 0.0000)),
520        // High CC - pure meteorological
521        ColorScaleLevel::new(0.96, Color::rgb(0.9333, 0.9333, 0.0000)),
522        ColorScaleLevel::new(0.98, Color::rgb(0.9020, 0.9020, 0.9020)),
523    ])
524}
525
526/// Returns a color scale for differential phase (PhiDP) data.
527///
528/// This sequential scale covers the full 0-360 degree range. Differential
529/// phase increases with propagation through precipitation.
530/// Range: 0 to 360 degrees.
531///
532/// | PhiDP (deg) | Color |
533/// |-------------|-------|
534/// | 0-45 | Purple |
535/// | 45-90 | Blue |
536/// | 90-135 | Cyan |
537/// | 135-180 | Green |
538/// | 180-225 | Yellow |
539/// | 225-270 | Orange |
540/// | 270-315 | Red |
541/// | 315-360 | Magenta |
542pub fn differential_phase_scale() -> DiscreteColorScale {
543    DiscreteColorScale::new(vec![
544        ColorScaleLevel::new(0.0, Color::rgb(0.5020, 0.0000, 0.5020)),
545        ColorScaleLevel::new(45.0, Color::rgb(0.0000, 0.0000, 0.8039)),
546        ColorScaleLevel::new(90.0, Color::rgb(0.0000, 0.8039, 0.8039)),
547        ColorScaleLevel::new(135.0, Color::rgb(0.0000, 0.8039, 0.0000)),
548        ColorScaleLevel::new(180.0, Color::rgb(0.9333, 0.9333, 0.0000)),
549        ColorScaleLevel::new(225.0, Color::rgb(1.0000, 0.6471, 0.0000)),
550        ColorScaleLevel::new(270.0, Color::rgb(1.0000, 0.0000, 0.0000)),
551        ColorScaleLevel::new(315.0, Color::rgb(1.0000, 0.0000, 1.0000)),
552    ])
553}
554
555/// Returns a color scale for clutter filter power (CFP) data.
556///
557/// This divergent scale centers around 0 dB, showing negative values
558/// (filtered reflectivity lower than unfiltered) in blues and positive
559/// values in reds.
560/// Default range: -20 to +20 dB.
561///
562/// | CFP (dB) | Color | Meaning |
563/// |----------|-------|---------|
564/// | -20 to -10 | Dark Blue | Strong filtering |
565/// | -10 to -5 | Blue | Moderate filtering |
566/// | -5 to -1 | Light Blue | Light filtering |
567/// | -1 to +1 | Gray | Near zero |
568/// | +1 to +5 | Light Red | Slight increase |
569/// | +5 to +10 | Red | Moderate increase |
570/// | +10 to +20 | Dark Red | Strong increase |
571pub fn clutter_filter_power_scale() -> DiscreteColorScale {
572    DiscreteColorScale::new(vec![
573        ColorScaleLevel::new(-20.0, Color::rgb(0.0000, 0.0000, 0.5451)),
574        ColorScaleLevel::new(-10.0, Color::rgb(0.0000, 0.0000, 0.8039)),
575        ColorScaleLevel::new(-5.0, Color::rgb(0.6784, 0.8471, 0.9020)),
576        ColorScaleLevel::new(-1.0, Color::rgb(0.6627, 0.6627, 0.6627)),
577        ColorScaleLevel::new(1.0, Color::rgb(0.6627, 0.6627, 0.6627)),
578        ColorScaleLevel::new(5.0, Color::rgb(1.0000, 0.7529, 0.7961)),
579        ColorScaleLevel::new(10.0, Color::rgb(1.0000, 0.4118, 0.4118)),
580        ColorScaleLevel::new(20.0, Color::rgb(0.8039, 0.0000, 0.0000)),
581    ])
582}