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 get_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 pre-computed lookup table for O(1) color lookups.
140///
141/// This table maps a range of values to RGBA colors using a fixed-size array.
142/// It is created from a [`DiscreteColorScale`] and provides fast color lookups
143/// during rendering.
144///
145/// # Example
146///
147/// ```
148/// use nexrad_render::{ColorLookupTable, get_nws_reflectivity_scale};
149///
150/// let scale = get_nws_reflectivity_scale();
151/// let lut = ColorLookupTable::from_scale(&scale, -32.0, 95.0, 256);
152///
153/// // O(1) lookup returning [R, G, B, A] bytes
154/// let color = lut.get_color(45.0);
155/// ```
156#[derive(Debug, Clone)]
157pub struct ColorLookupTable {
158 /// RGBA color values indexed by quantized input value.
159 table: Vec<[u8; 4]>,
160 /// Minimum value in the mapped range.
161 min_value: f32,
162 /// Value range (max - min).
163 range: f32,
164}
165
166impl ColorLookupTable {
167 /// Creates a lookup table from a discrete color scale.
168 ///
169 /// # Arguments
170 ///
171 /// * `scale` - The color scale to sample from
172 /// * `min_value` - The minimum value to map
173 /// * `max_value` - The maximum value to map
174 /// * `size` - The number of entries in the lookup table (256 recommended)
175 ///
176 /// Values outside the min/max range will be clamped to the nearest entry.
177 pub fn from_scale(
178 scale: &DiscreteColorScale,
179 min_value: f32,
180 max_value: f32,
181 size: usize,
182 ) -> Self {
183 let range = max_value - min_value;
184 let mut table = Vec::with_capacity(size);
185
186 for i in 0..size {
187 let value = min_value + (i as f32 / (size - 1) as f32) * range;
188 let color = scale.get_color(value);
189 table.push(color.to_rgba8());
190 }
191
192 Self {
193 table,
194 min_value,
195 range,
196 }
197 }
198
199 /// Returns the RGBA color for the given value.
200 ///
201 /// This is an O(1) operation using direct array indexing.
202 #[inline]
203 pub fn get_color(&self, value: f32) -> [u8; 4] {
204 let normalized = (value - self.min_value) / self.range;
205 let index = (normalized * (self.table.len() - 1) as f32) as usize;
206 let index = index.min(self.table.len() - 1);
207 self.table[index]
208 }
209}
210
211/// Returns the standard NWS (National Weather Service) reflectivity color scale.
212///
213/// This scale uses colors commonly seen in weather radar displays, ranging
214/// from cyan/blue for light precipitation to magenta/white for extreme values.
215///
216/// | dBZ Range | Color | Meaning |
217/// |-----------|-------|---------|
218/// | 0-5 | Black | Below detection threshold |
219/// | 5-20 | Cyan/Blue | Light precipitation |
220/// | 20-35 | Green | Light to moderate precipitation |
221/// | 35-50 | Yellow/Orange | Moderate to heavy precipitation |
222/// | 50-65 | Red/Magenta | Heavy precipitation, possible hail |
223/// | 65+ | Purple/White | Extreme precipitation, likely hail |
224pub fn get_nws_reflectivity_scale() -> DiscreteColorScale {
225 DiscreteColorScale::new(vec![
226 ColorScaleLevel::new(0.0, Color::rgb(0.0000, 0.0000, 0.0000)),
227 ColorScaleLevel::new(5.0, Color::rgb(0.0000, 1.0000, 1.0000)),
228 ColorScaleLevel::new(10.0, Color::rgb(0.5294, 0.8078, 0.9216)),
229 ColorScaleLevel::new(15.0, Color::rgb(0.0000, 0.0000, 1.0000)),
230 ColorScaleLevel::new(20.0, Color::rgb(0.0000, 1.0000, 0.0000)),
231 ColorScaleLevel::new(25.0, Color::rgb(0.1961, 0.8039, 0.1961)),
232 ColorScaleLevel::new(30.0, Color::rgb(0.1333, 0.5451, 0.1333)),
233 ColorScaleLevel::new(35.0, Color::rgb(0.9333, 0.9333, 0.0000)),
234 ColorScaleLevel::new(40.0, Color::rgb(0.9333, 0.8627, 0.5098)),
235 ColorScaleLevel::new(45.0, Color::rgb(0.9333, 0.4627, 0.1294)),
236 ColorScaleLevel::new(50.0, Color::rgb(1.0000, 0.1882, 0.1882)),
237 ColorScaleLevel::new(55.0, Color::rgb(0.6902, 0.1882, 0.3765)),
238 ColorScaleLevel::new(60.0, Color::rgb(0.6902, 0.1882, 0.3765)),
239 ColorScaleLevel::new(65.0, Color::rgb(0.7294, 0.3333, 0.8275)),
240 ColorScaleLevel::new(70.0, Color::rgb(1.0000, 0.0000, 1.0000)),
241 ColorScaleLevel::new(75.0, Color::rgb(1.0000, 1.0000, 1.0000)),
242 ])
243}
244
245/// Returns a color scale for radial velocity data.
246///
247/// This divergent scale uses green for motion toward the radar (negative values)
248/// and red for motion away from the radar (positive values), with gray near zero.
249/// Range: -64 to +64 m/s (standard precipitation mode Doppler velocity).
250///
251/// | Velocity (m/s) | Color | Meaning |
252/// |----------------|-------|---------|
253/// | -64 to -48 | Dark Green | Strong inbound |
254/// | -48 to -32 | Green | Moderate inbound |
255/// | -32 to -16 | Light Green | Light inbound |
256/// | -16 to -4 | Pale Green | Very light inbound |
257/// | -4 to +4 | Gray | Near zero / RF |
258/// | +4 to +16 | Pale Red | Very light outbound |
259/// | +16 to +32 | Light Red/Pink | Light outbound |
260/// | +32 to +48 | Red | Moderate outbound |
261/// | +48 to +64 | Dark Red | Strong outbound |
262pub fn get_velocity_scale() -> DiscreteColorScale {
263 DiscreteColorScale::new(vec![
264 // Strong inbound (toward radar) - dark green
265 ColorScaleLevel::new(-64.0, Color::rgb(0.0000, 0.3922, 0.0000)),
266 ColorScaleLevel::new(-48.0, Color::rgb(0.0000, 0.5451, 0.0000)),
267 ColorScaleLevel::new(-32.0, Color::rgb(0.0000, 0.8039, 0.0000)),
268 ColorScaleLevel::new(-16.0, Color::rgb(0.5647, 0.9333, 0.5647)),
269 // Near zero - gray
270 ColorScaleLevel::new(-4.0, Color::rgb(0.6627, 0.6627, 0.6627)),
271 ColorScaleLevel::new(4.0, Color::rgb(0.6627, 0.6627, 0.6627)),
272 // Outbound (away from radar) - reds
273 ColorScaleLevel::new(16.0, Color::rgb(1.0000, 0.7529, 0.7961)),
274 ColorScaleLevel::new(32.0, Color::rgb(1.0000, 0.4118, 0.4118)),
275 ColorScaleLevel::new(48.0, Color::rgb(0.8039, 0.0000, 0.0000)),
276 ColorScaleLevel::new(64.0, Color::rgb(0.5451, 0.0000, 0.0000)),
277 ])
278}
279
280/// Returns a color scale for spectrum width data.
281///
282/// This sequential scale ranges from cool colors (low turbulence) to warm colors
283/// (high turbulence). Range: 0 to 30 m/s.
284///
285/// | Width (m/s) | Color | Meaning |
286/// |-------------|-------|---------|
287/// | 0-4 | Gray | Very low turbulence |
288/// | 4-8 | Blue | Low turbulence |
289/// | 8-12 | Cyan | Light turbulence |
290/// | 12-16 | Green | Moderate turbulence |
291/// | 16-20 | Yellow | Moderate-high turbulence |
292/// | 20-25 | Orange | High turbulence |
293/// | 25-30 | Red | Very high turbulence |
294pub fn get_spectrum_width_scale() -> DiscreteColorScale {
295 DiscreteColorScale::new(vec![
296 ColorScaleLevel::new(0.0, Color::rgb(0.5020, 0.5020, 0.5020)),
297 ColorScaleLevel::new(4.0, Color::rgb(0.0000, 0.0000, 0.8039)),
298 ColorScaleLevel::new(8.0, Color::rgb(0.0000, 0.8039, 0.8039)),
299 ColorScaleLevel::new(12.0, Color::rgb(0.0000, 0.8039, 0.0000)),
300 ColorScaleLevel::new(16.0, Color::rgb(0.9333, 0.9333, 0.0000)),
301 ColorScaleLevel::new(20.0, Color::rgb(1.0000, 0.6471, 0.0000)),
302 ColorScaleLevel::new(25.0, Color::rgb(1.0000, 0.0000, 0.0000)),
303 ])
304}
305
306/// Returns a color scale for differential reflectivity (ZDR) data.
307///
308/// This divergent scale shows negative values (vertically-oriented particles) in
309/// blue/purple, near-zero in gray, and positive values (horizontally-oriented
310/// particles like large raindrops) in yellow/orange/red.
311/// Range: -2 to +6 dB.
312///
313/// | ZDR (dB) | Color | Meaning |
314/// |----------|-------|---------|
315/// | -2 to -1 | Purple | Vertically oriented |
316/// | -1 to 0 | Blue | Slightly vertical |
317/// | 0 to 0.5 | Gray | Spherical particles |
318/// | 0.5 to 1.5 | Light Green | Slightly oblate |
319/// | 1.5 to 2.5 | Yellow | Oblate drops |
320/// | 2.5 to 4 | Orange | Large oblate drops |
321/// | 4 to 6 | Red | Very large drops/hail |
322pub fn get_differential_reflectivity_scale() -> DiscreteColorScale {
323 DiscreteColorScale::new(vec![
324 // Negative (vertically oriented)
325 ColorScaleLevel::new(-2.0, Color::rgb(0.5020, 0.0000, 0.5020)),
326 ColorScaleLevel::new(-1.0, Color::rgb(0.0000, 0.0000, 0.8039)),
327 // Near zero (spherical)
328 ColorScaleLevel::new(0.0, Color::rgb(0.6627, 0.6627, 0.6627)),
329 // Positive (horizontally oriented / oblate)
330 ColorScaleLevel::new(0.5, Color::rgb(0.5647, 0.9333, 0.5647)),
331 ColorScaleLevel::new(1.5, Color::rgb(0.9333, 0.9333, 0.0000)),
332 ColorScaleLevel::new(2.5, Color::rgb(1.0000, 0.6471, 0.0000)),
333 ColorScaleLevel::new(4.0, Color::rgb(1.0000, 0.0000, 0.0000)),
334 ])
335}
336
337/// Returns a color scale for correlation coefficient (CC/RhoHV) data.
338///
339/// This sequential scale emphasizes high values (0.9-1.0) which indicate
340/// meteorological targets. Lower values may indicate non-meteorological
341/// echoes, mixed precipitation, or tornadic debris.
342/// Range: 0.0 to 1.0.
343///
344/// | CC | Color | Meaning |
345/// |----|-------|---------|
346/// | 0.0-0.7 | Purple/Blue | Non-met or debris |
347/// | 0.7-0.85 | Cyan/Teal | Mixed phase/melting |
348/// | 0.85-0.92 | Light Green | Possible hail/graupel |
349/// | 0.92-0.96 | Green | Rain/snow mix |
350/// | 0.96-0.98 | Yellow | Pure rain or snow |
351/// | 0.98-1.0 | White/Light Gray | Uniform precipitation |
352pub fn get_correlation_coefficient_scale() -> DiscreteColorScale {
353 DiscreteColorScale::new(vec![
354 // Low CC - non-meteorological or debris
355 ColorScaleLevel::new(0.0, Color::rgb(0.0000, 0.0000, 0.0000)),
356 ColorScaleLevel::new(0.2, Color::rgb(0.3922, 0.0000, 0.5882)),
357 ColorScaleLevel::new(0.5, Color::rgb(0.0000, 0.0000, 0.8039)),
358 ColorScaleLevel::new(0.7, Color::rgb(0.0000, 0.5451, 0.5451)),
359 // Medium CC - mixed precipitation
360 ColorScaleLevel::new(0.85, Color::rgb(0.0000, 0.8039, 0.4000)),
361 ColorScaleLevel::new(0.92, Color::rgb(0.0000, 0.8039, 0.0000)),
362 // High CC - pure meteorological
363 ColorScaleLevel::new(0.96, Color::rgb(0.9333, 0.9333, 0.0000)),
364 ColorScaleLevel::new(0.98, Color::rgb(0.9020, 0.9020, 0.9020)),
365 ])
366}
367
368/// Returns a color scale for differential phase (PhiDP) data.
369///
370/// This sequential scale covers the full 0-360 degree range. Differential
371/// phase increases with propagation through precipitation.
372/// Range: 0 to 360 degrees.
373///
374/// | PhiDP (deg) | Color |
375/// |-------------|-------|
376/// | 0-45 | Purple |
377/// | 45-90 | Blue |
378/// | 90-135 | Cyan |
379/// | 135-180 | Green |
380/// | 180-225 | Yellow |
381/// | 225-270 | Orange |
382/// | 270-315 | Red |
383/// | 315-360 | Magenta |
384pub fn get_differential_phase_scale() -> DiscreteColorScale {
385 DiscreteColorScale::new(vec![
386 ColorScaleLevel::new(0.0, Color::rgb(0.5020, 0.0000, 0.5020)),
387 ColorScaleLevel::new(45.0, Color::rgb(0.0000, 0.0000, 0.8039)),
388 ColorScaleLevel::new(90.0, Color::rgb(0.0000, 0.8039, 0.8039)),
389 ColorScaleLevel::new(135.0, Color::rgb(0.0000, 0.8039, 0.0000)),
390 ColorScaleLevel::new(180.0, Color::rgb(0.9333, 0.9333, 0.0000)),
391 ColorScaleLevel::new(225.0, Color::rgb(1.0000, 0.6471, 0.0000)),
392 ColorScaleLevel::new(270.0, Color::rgb(1.0000, 0.0000, 0.0000)),
393 ColorScaleLevel::new(315.0, Color::rgb(1.0000, 0.0000, 1.0000)),
394 ])
395}
396
397/// Returns a color scale for specific differential phase (KDP) data.
398///
399/// This sequential scale shows the rate of differential phase change,
400/// which correlates with rainfall rate. Higher KDP indicates heavier rain.
401/// Range: 0 to 10 degrees/km.
402///
403/// | KDP (deg/km) | Color | Meaning |
404/// |--------------|-------|---------|
405/// | 0-0.5 | Gray | Very light/no rain |
406/// | 0.5-1.0 | Light Blue | Light rain |
407/// | 1.0-2.0 | Blue | Light-moderate rain |
408/// | 2.0-3.0 | Green | Moderate rain |
409/// | 3.0-4.5 | Yellow | Moderate-heavy rain |
410/// | 4.5-6.0 | Orange | Heavy rain |
411/// | 6.0-10.0 | Red | Very heavy rain |
412pub fn get_specific_diff_phase_scale() -> DiscreteColorScale {
413 DiscreteColorScale::new(vec![
414 ColorScaleLevel::new(0.0, Color::rgb(0.6627, 0.6627, 0.6627)),
415 ColorScaleLevel::new(0.5, Color::rgb(0.6784, 0.8471, 0.9020)),
416 ColorScaleLevel::new(1.0, Color::rgb(0.0000, 0.0000, 0.8039)),
417 ColorScaleLevel::new(2.0, Color::rgb(0.0000, 0.8039, 0.0000)),
418 ColorScaleLevel::new(3.0, Color::rgb(0.9333, 0.9333, 0.0000)),
419 ColorScaleLevel::new(4.5, Color::rgb(1.0000, 0.6471, 0.0000)),
420 ColorScaleLevel::new(6.0, Color::rgb(1.0000, 0.0000, 0.0000)),
421 ])
422}