1#[cfg(feature = "plot-svg")]
4use plotters::prelude::*;
5
6#[cfg(any(feature = "plot", feature = "plot-svg"))]
7use crate::FigureState;
8
9pub const VALID_COLORMAPS: &[&str] = &[
13 "viridis", "inferno", "magma", "plasma", "hot", "cool", "jet", "gray",
14];
15
16pub fn validate_colormap(name: &str) -> Result<(), String> {
21 if VALID_COLORMAPS.contains(&name) {
22 Ok(())
23 } else {
24 Err(format!(
25 "colormap: '{}' is not a recognised colormap. Valid colormaps: {}",
26 name,
27 VALID_COLORMAPS.join(", ")
28 ))
29 }
30}
31
32#[derive(Clone, Debug, PartialEq)]
35pub enum ColormapSpec {
36 Named(String),
40 Custom(Vec<(u8, u8, u8)>),
44}
45
46pub fn apply_colormap(t: f64, name: &str) -> (u8, u8, u8) {
61 let t = t.clamp(0.0, 1.0);
62 match name {
63 "viridis" => lut_lerp(t, &VIRIDIS),
64 "inferno" => lut_lerp(t, &INFERNO),
65 "magma" => lut_lerp(t, &MAGMA),
66 "plasma" => lut_lerp(t, &PLASMA),
67 "hot" => lut_lerp(t, &HOT),
68 "cool" => lut_lerp(t, &COOL),
69 "jet" => lut_lerp(t, &JET),
70 "gray" => {
71 let v = (t * 255.0).round() as u8;
72 (v, v, v)
73 }
74 _ => lut_lerp(t, &VIRIDIS),
75 }
76}
77
78pub fn apply_colormap_spec(t: f64, spec: &ColormapSpec) -> (u8, u8, u8) {
92 match spec {
93 ColormapSpec::Named(name) => apply_colormap(t, name),
94 ColormapSpec::Custom(lut) => lut_lerp(t, lut),
95 }
96}
97
98pub fn validate_colormap_spec(spec: &ColormapSpec) -> Result<(), String> {
103 match spec {
104 ColormapSpec::Named(name) => validate_colormap(name),
105 ColormapSpec::Custom(lut) => {
106 if lut.len() < 2 {
107 Err("colormap: custom colormap must have at least 2 rows".into())
108 } else {
109 Ok(())
110 }
111 }
112 }
113}
114
115fn lut_lerp(t: f64, lut: &[(u8, u8, u8)]) -> (u8, u8, u8) {
118 let n = lut.len();
119 if n == 1 {
120 return lut[0];
121 }
122 let ts = t * (n - 1) as f64;
123 let lo = (ts as usize).min(n - 2);
124 let hi = lo + 1;
125 let f = ts - lo as f64;
126 let lerp = |a: u8, b: u8| (a as f64 + f * (b as f64 - a as f64)).round() as u8;
127 (
128 lerp(lut[lo].0, lut[hi].0),
129 lerp(lut[lo].1, lut[hi].1),
130 lerp(lut[lo].2, lut[hi].2),
131 )
132}
133
134const VIRIDIS: [(u8, u8, u8); 8] = [
137 (68, 1, 84),
138 (72, 40, 120),
139 (62, 83, 160),
140 (49, 104, 142),
141 (53, 183, 121),
142 (101, 203, 94),
143 (180, 222, 44),
144 (253, 231, 37),
145];
146const INFERNO: [(u8, u8, u8); 8] = [
147 (0, 0, 4),
148 (40, 11, 84),
149 (101, 21, 110),
150 (159, 42, 99),
151 (212, 72, 66),
152 (245, 125, 21),
153 (252, 190, 44),
154 (252, 255, 164),
155];
156const MAGMA: [(u8, u8, u8); 8] = [
157 (0, 0, 4),
158 (28, 16, 68),
159 (79, 18, 123),
160 (129, 37, 129),
161 (181, 55, 122),
162 (229, 89, 104),
163 (251, 143, 107),
164 (252, 253, 191),
165];
166const PLASMA: [(u8, u8, u8); 8] = [
167 (13, 8, 135),
168 (84, 2, 163),
169 (139, 10, 165),
170 (185, 50, 137),
171 (219, 92, 104),
172 (243, 135, 72),
173 (253, 182, 44),
174 (240, 249, 33),
175];
176const HOT: [(u8, u8, u8); 8] = [
177 (0, 0, 0),
178 (96, 0, 0),
179 (192, 0, 0),
180 (255, 48, 0),
181 (255, 144, 0),
182 (255, 216, 0),
183 (255, 255, 96),
184 (255, 255, 255),
185];
186const COOL: [(u8, u8, u8); 8] = [
187 (0, 255, 255),
188 (36, 219, 255),
189 (73, 182, 255),
190 (109, 146, 255),
191 (146, 109, 255),
192 (182, 73, 255),
193 (219, 36, 255),
194 (255, 0, 255),
195];
196const JET: [(u8, u8, u8); 8] = [
197 (0, 0, 143),
198 (0, 0, 255),
199 (0, 218, 255),
200 (0, 255, 36),
201 (146, 255, 0),
202 (255, 218, 0),
203 (255, 36, 0),
204 (143, 0, 0),
205];
206
207pub(crate) fn data_range(z: &[f64]) -> (f64, f64) {
212 let mut lo = f64::INFINITY;
213 let mut hi = f64::NEG_INFINITY;
214 for &v in z {
215 if v.is_finite() {
216 lo = lo.min(v);
217 hi = hi.max(v);
218 }
219 }
220 if !lo.is_finite() {
221 lo = 0.0;
222 hi = 1.0;
223 }
224 if (hi - lo).abs() < f64::EPSILON {
225 hi = lo + 1.0;
226 }
227 (lo, hi)
228}
229
230#[cfg(feature = "plot")]
238pub fn render_imagesc_ascii(z: &[f64], nrows: usize, ncols: usize, state: &FigureState) {
239 const DENSITY: [char; 10] = [' ', '.', ':', '-', '=', '+', '*', '#', '@', '█'];
240
241 if nrows == 0 || ncols == 0 {
242 return;
243 }
244
245 let (z_min, z_max) = data_range(z);
246 let range = z_max - z_min;
247
248 if let Some(t) = &state.title {
249 println!("{t}");
250 }
251
252 for r in 0..nrows {
253 for c in 0..ncols {
254 let v = z[r * ncols + c];
255 let t = if range > 0.0 {
256 ((v - z_min) / range).clamp(0.0, 1.0)
257 } else {
258 0.5
259 };
260 let idx = ((t * 9.0) as usize).min(9);
261 print!("{}", DENSITY[idx]);
262 }
263 println!();
264 }
265
266 if state.colorbar {
267 let steps = 20_usize;
268 let gradient: String = (0..steps)
269 .map(|i| {
270 let t = i as f64 / (steps - 1).max(1) as f64;
271 let idx = ((t * 9.0) as usize).min(9);
272 DENSITY[idx]
273 })
274 .collect();
275 println!("{z_min:.4} [{gradient}] {z_max:.4}");
276 }
277 if let Some(xl) = &state.xlabel {
278 println!("x: {xl}");
279 }
280 if let Some(yl) = &state.ylabel {
281 println!("y: {yl}");
282 }
283}
284
285pub fn compute_luminance(r: &[f64], g: &[f64], b: &[f64]) -> Vec<f64> {
293 r.iter()
294 .zip(g.iter())
295 .zip(b.iter())
296 .map(|((&rv, &gv), &bv)| (0.299 * rv + 0.587 * gv + 0.114 * bv).clamp(0.0, 1.0))
297 .collect()
298}
299
300#[cfg(feature = "plot")]
306pub fn render_imshow_gray_ascii(z: &[f64], nrows: usize, ncols: usize, state: &FigureState) {
307 const DENSITY: [char; 10] = [' ', '.', ':', '-', '=', '+', '*', '#', '@', '█'];
308
309 if nrows == 0 || ncols == 0 {
310 return;
311 }
312 if let Some(t) = &state.title {
313 println!("{t}");
314 }
315 for r in 0..nrows {
316 for c in 0..ncols {
317 let v = z[r * ncols + c].clamp(0.0, 1.0);
318 let idx = ((v * 9.0) as usize).min(9);
319 print!("{}", DENSITY[idx]);
320 }
321 println!();
322 }
323 if let Some(xl) = &state.xlabel {
324 println!("x: {xl}");
325 }
326 if let Some(yl) = &state.ylabel {
327 println!("y: {yl}");
328 }
329}
330
331#[cfg(feature = "plot")]
337pub fn render_imshow_rgb_ascii(
338 r: &[f64],
339 g: &[f64],
340 b: &[f64],
341 nrows: usize,
342 ncols: usize,
343 state: &FigureState,
344) {
345 let lum = compute_luminance(r, g, b);
346 render_imshow_gray_ascii(&lum, nrows, ncols, state);
347}
348
349#[cfg(feature = "plot-svg")]
353const CB_WIDTH: u32 = 80;
354
355#[cfg(feature = "plot-svg")]
362pub fn render_imagesc_file(
363 z: &[f64],
364 nrows: usize,
365 ncols: usize,
366 path: &str,
367 state: FigureState,
368) -> Result<(), String> {
369 let (width, height) = state.canvas_size();
370 if path.ends_with(".svg") {
371 let root = SVGBackend::new(path, (width, height)).into_drawing_area();
372 draw_imagesc(z, nrows, ncols, &state, root, width)
373 } else if path.ends_with(".png") {
374 let root = BitMapBackend::new(path, (width, height)).into_drawing_area();
375 draw_imagesc(z, nrows, ncols, &state, root, width)
376 } else {
377 Err(format!("imagesc: unsupported format '{path}'"))
378 }
379}
380
381#[cfg(feature = "plot-svg")]
382fn draw_imagesc<DB: DrawingBackend>(
383 z: &[f64],
384 nrows: usize,
385 ncols: usize,
386 state: &FigureState,
387 root: DrawingArea<DB, plotters::coord::Shift>,
388 width: u32,
389) -> Result<(), String>
390where
391 DB::ErrorType: std::fmt::Display,
392{
393 let (r, g, b) = state.effective_bg_rgb();
394 root.fill(&RGBColor(r, g, b)).map_err(|e| e.to_string())?;
395
396 if nrows == 0 || ncols == 0 {
397 return root.present().map_err(|e| e.to_string());
398 }
399
400 let default_spec = ColormapSpec::Named("viridis".to_string());
401 let cmap_spec = state.colormap.as_ref().unwrap_or(&default_spec);
402 let (z_min, z_max) = data_range(z);
403 let range = z_max - z_min;
404
405 if state.colorbar {
406 let split = (width.saturating_sub(CB_WIDTH)) as i32;
407 let (img_area, cb_area) = root.split_horizontally(split);
408 draw_imagesc_cells(&img_area, z, nrows, ncols, state, cmap_spec, z_min, range)?;
409 draw_colorbar(&cb_area, z_min, z_max, cmap_spec)?;
410 } else {
411 draw_imagesc_cells(&root, z, nrows, ncols, state, cmap_spec, z_min, range)?;
412 }
413
414 root.present().map_err(|e| e.to_string())?;
415 Ok(())
416}
417
418#[cfg(feature = "plot-svg")]
419#[allow(clippy::too_many_arguments)]
420fn draw_imagesc_cells<DB: DrawingBackend>(
421 area: &DrawingArea<DB, plotters::coord::Shift>,
422 z: &[f64],
423 nrows: usize,
424 ncols: usize,
425 state: &FigureState,
426 spec: &ColormapSpec,
427 z_min: f64,
428 range: f64,
429) -> Result<(), String>
430where
431 DB::ErrorType: std::fmt::Display,
432{
433 let title = state.title.as_deref().unwrap_or("");
434 let xlabel = state.xlabel.as_deref().unwrap_or("");
435 let ylabel = state.ylabel.as_deref().unwrap_or("");
436
437 let mut chart = ChartBuilder::on(area)
438 .caption(title, ("sans-serif", 20))
439 .margin(30)
440 .x_label_area_size(40)
441 .y_label_area_size(50)
442 .build_cartesian_2d(0.0..(ncols as f64), 0.0..(nrows as f64))
443 .map_err(|e| e.to_string())?;
444
445 chart
446 .configure_mesh()
447 .x_desc(xlabel)
448 .y_desc(ylabel)
449 .disable_mesh()
450 .draw()
451 .map_err(|e| e.to_string())?;
452
453 for r in 0..nrows {
455 let y_lo = (nrows - 1 - r) as f64;
456 let y_hi = y_lo + 1.0;
457 for c in 0..ncols {
458 let v = z[r * ncols + c];
459 let t = if range > 0.0 {
460 ((v - z_min) / range).clamp(0.0, 1.0)
461 } else {
462 0.5
463 };
464 let (rr, gg, bb) = apply_colormap_spec(t, spec);
465 chart
466 .draw_series(std::iter::once(Rectangle::new(
467 [(c as f64, y_lo), ((c + 1) as f64, y_hi)],
468 RGBColor(rr, gg, bb).filled(),
469 )))
470 .map_err(|e| e.to_string())?;
471 }
472 }
473 Ok(())
474}
475
476#[cfg(feature = "plot-svg")]
477fn draw_colorbar<DB: DrawingBackend>(
478 area: &DrawingArea<DB, plotters::coord::Shift>,
479 z_min: f64,
480 z_max: f64,
481 spec: &ColormapSpec,
482) -> Result<(), String>
483where
484 DB::ErrorType: std::fmt::Display,
485{
486 let n_steps: usize = 64;
487 let step_h = (z_max - z_min) / n_steps as f64;
488
489 let mut chart = ChartBuilder::on(area)
492 .margin_top(30)
493 .margin_bottom(30)
494 .margin_left(0)
495 .margin_right(4)
496 .x_label_area_size(0)
497 .y_label_area_size(40)
498 .build_cartesian_2d(0.0..1.0, z_min..z_max)
499 .map_err(|e| e.to_string())?;
500
501 chart
503 .configure_mesh()
504 .disable_x_mesh()
505 .disable_y_mesh()
506 .draw()
507 .map_err(|e| e.to_string())?;
508
509 chart
511 .draw_series((0..n_steps).map(|i| {
512 let t = i as f64 / (n_steps - 1).max(1) as f64;
513 let y_lo = z_min + i as f64 * step_h;
514 let y_hi = (y_lo + step_h).min(z_max);
515 let (r, g, b) = apply_colormap_spec(t, spec);
516 Rectangle::new([(0.0, y_lo), (1.0, y_hi)], RGBColor(r, g, b).filled())
517 }))
518 .map_err(|e| e.to_string())?;
519
520 Ok(())
521}
522
523#[cfg(feature = "plot-svg")]
531pub fn render_imshow_gray_file(
532 z: &[f64],
533 nrows: usize,
534 ncols: usize,
535 path: &str,
536 state: FigureState,
537) -> Result<(), String> {
538 let (width, height) = state.canvas_size();
539 if path.ends_with(".svg") {
540 let root = SVGBackend::new(path, (width, height)).into_drawing_area();
541 draw_imshow_gray(z, nrows, ncols, &state, root)
542 } else if path.ends_with(".png") {
543 let root = BitMapBackend::new(path, (width, height)).into_drawing_area();
544 draw_imshow_gray(z, nrows, ncols, &state, root)
545 } else {
546 Err(format!("imshow: unsupported format '{path}'"))
547 }
548}
549
550#[cfg(feature = "plot-svg")]
551fn draw_imshow_gray<DB: DrawingBackend>(
552 z: &[f64],
553 nrows: usize,
554 ncols: usize,
555 state: &FigureState,
556 root: DrawingArea<DB, plotters::coord::Shift>,
557) -> Result<(), String>
558where
559 DB::ErrorType: std::fmt::Display,
560{
561 let (rb, gb, bb) = state.effective_bg_rgb();
562 root.fill(&RGBColor(rb, gb, bb))
563 .map_err(|e| e.to_string())?;
564
565 if nrows == 0 || ncols == 0 {
566 return root.present().map_err(|e| e.to_string());
567 }
568
569 let title = state.title.as_deref().unwrap_or("");
570 let xlabel = state.xlabel.as_deref().unwrap_or("");
571 let ylabel = state.ylabel.as_deref().unwrap_or("");
572
573 let mut chart = ChartBuilder::on(&root)
574 .caption(title, ("sans-serif", 20))
575 .margin(30)
576 .x_label_area_size(40)
577 .y_label_area_size(50)
578 .build_cartesian_2d(0.0..(ncols as f64), 0.0..(nrows as f64))
579 .map_err(|e| e.to_string())?;
580
581 chart
582 .configure_mesh()
583 .x_desc(xlabel)
584 .y_desc(ylabel)
585 .disable_mesh()
586 .draw()
587 .map_err(|e| e.to_string())?;
588
589 for r in 0..nrows {
590 let y_lo = (nrows - 1 - r) as f64;
591 let y_hi = y_lo + 1.0;
592 for c in 0..ncols {
593 let v = z[r * ncols + c].clamp(0.0, 1.0);
594 let gray = (v * 255.0).round() as u8;
595 chart
596 .draw_series(std::iter::once(Rectangle::new(
597 [(c as f64, y_lo), ((c + 1) as f64, y_hi)],
598 RGBColor(gray, gray, gray).filled(),
599 )))
600 .map_err(|e| e.to_string())?;
601 }
602 }
603
604 root.present().map_err(|e| e.to_string())?;
605 Ok(())
606}
607
608#[cfg(feature = "plot-svg")]
614pub fn render_imshow_rgb_file(
615 r: &[f64],
616 g: &[f64],
617 b: &[f64],
618 nrows: usize,
619 ncols: usize,
620 path: &str,
621 state: FigureState,
622) -> Result<(), String> {
623 let (width, height) = state.canvas_size();
624 if path.ends_with(".svg") {
625 let root = SVGBackend::new(path, (width, height)).into_drawing_area();
626 draw_imshow_rgb(r, g, b, nrows, ncols, &state, root)
627 } else if path.ends_with(".png") {
628 let root = BitMapBackend::new(path, (width, height)).into_drawing_area();
629 draw_imshow_rgb(r, g, b, nrows, ncols, &state, root)
630 } else {
631 Err(format!("imshow: unsupported format '{path}'"))
632 }
633}
634
635#[cfg(feature = "plot-svg")]
636#[allow(clippy::too_many_arguments)]
637fn draw_imshow_rgb<DB: DrawingBackend>(
638 r_ch: &[f64],
639 g_ch: &[f64],
640 b_ch: &[f64],
641 nrows: usize,
642 ncols: usize,
643 state: &FigureState,
644 root: DrawingArea<DB, plotters::coord::Shift>,
645) -> Result<(), String>
646where
647 DB::ErrorType: std::fmt::Display,
648{
649 let (rb, gb, bb) = state.effective_bg_rgb();
650 root.fill(&RGBColor(rb, gb, bb))
651 .map_err(|e| e.to_string())?;
652
653 if nrows == 0 || ncols == 0 {
654 return root.present().map_err(|e| e.to_string());
655 }
656
657 let title = state.title.as_deref().unwrap_or("");
658 let xlabel = state.xlabel.as_deref().unwrap_or("");
659 let ylabel = state.ylabel.as_deref().unwrap_or("");
660
661 let mut chart = ChartBuilder::on(&root)
662 .caption(title, ("sans-serif", 20))
663 .margin(30)
664 .x_label_area_size(40)
665 .y_label_area_size(50)
666 .build_cartesian_2d(0.0..(ncols as f64), 0.0..(nrows as f64))
667 .map_err(|e| e.to_string())?;
668
669 chart
670 .configure_mesh()
671 .x_desc(xlabel)
672 .y_desc(ylabel)
673 .disable_mesh()
674 .draw()
675 .map_err(|e| e.to_string())?;
676
677 let clamp_u8 = |v: f64| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
678
679 for r in 0..nrows {
680 let y_lo = (nrows - 1 - r) as f64;
681 let y_hi = y_lo + 1.0;
682 for c in 0..ncols {
683 let idx = r * ncols + c;
684 let rc = clamp_u8(r_ch[idx]);
685 let gc = clamp_u8(g_ch[idx]);
686 let bc = clamp_u8(b_ch[idx]);
687 chart
688 .draw_series(std::iter::once(Rectangle::new(
689 [(c as f64, y_lo), ((c + 1) as f64, y_hi)],
690 RGBColor(rc, gc, bc).filled(),
691 )))
692 .map_err(|e| e.to_string())?;
693 }
694 }
695
696 root.present().map_err(|e| e.to_string())?;
697 Ok(())
698}
699
700#[cfg(test)]
703mod tests {
704 use super::*;
705
706 #[test]
707 fn test_apply_colormap_gray_extremes() {
708 assert_eq!(apply_colormap(0.0, "gray"), (0, 0, 0));
709 assert_eq!(apply_colormap(1.0, "gray"), (255, 255, 255));
710 }
711
712 #[test]
713 fn test_colormap_custom_2pt() {
714 let lut = vec![(0u8, 0, 0), (255u8, 255, 255)];
715 let spec = ColormapSpec::Custom(lut);
716 assert_eq!(apply_colormap_spec(0.0, &spec), (0, 0, 0));
717 assert_eq!(apply_colormap_spec(1.0, &spec), (255, 255, 255));
718 }
719
720 #[test]
721 fn test_colormap_custom_midpt() {
722 let lut = vec![(0u8, 0, 0), (200u8, 100, 50)];
723 let spec = ColormapSpec::Custom(lut);
724 let (r, g, b) = apply_colormap_spec(0.5, &spec);
725 assert_eq!(r, 100);
726 assert_eq!(g, 50);
727 assert_eq!(b, 25);
728 }
729
730 #[test]
731 fn test_colormap_custom_too_short() {
732 let spec = ColormapSpec::Custom(vec![(128u8, 0, 0)]);
733 assert!(validate_colormap_spec(&spec).is_err());
734 }
735
736 #[test]
737 fn test_colormap_spec_named_viridis() {
738 let spec = ColormapSpec::Named("viridis".to_string());
739 assert!(validate_colormap_spec(&spec).is_ok());
740 assert_eq!(
741 apply_colormap_spec(0.0, &spec),
742 apply_colormap(0.0, "viridis")
743 );
744 assert_eq!(
745 apply_colormap_spec(1.0, &spec),
746 apply_colormap(1.0, "viridis")
747 );
748 }
749
750 #[test]
751 fn test_apply_colormap_clamp() {
752 let lo = apply_colormap(-1.0, "hot");
754 let hi = apply_colormap(2.0, "hot");
755 assert_eq!(lo, apply_colormap(0.0, "hot"));
756 assert_eq!(hi, apply_colormap(1.0, "hot"));
757 }
758
759 #[test]
760 fn test_apply_colormap_fallback() {
761 let _ = apply_colormap(0.5, "unknown_colormap_xyz");
763 }
764
765 #[test]
766 fn test_validate_colormap_valid() {
767 for name in VALID_COLORMAPS {
768 assert!(validate_colormap(name).is_ok(), "'{name}' should be valid");
769 }
770 }
771
772 #[test]
773 fn test_validate_colormap_invalid() {
774 let result = validate_colormap("rainbow");
775 assert!(result.is_err());
776 let msg = result.unwrap_err();
777 assert!(
778 msg.contains("colormap"),
779 "error should mention colormap: {msg}"
780 );
781 }
782
783 #[cfg(any(feature = "plot", feature = "plot-svg"))]
784 #[test]
785 fn test_data_range_normal() {
786 let (lo, hi) = data_range(&[3.0, 1.0, 4.0, 1.5]);
787 assert!((lo - 1.0).abs() < 1e-9);
788 assert!((hi - 4.0).abs() < 1e-9);
789 }
790
791 #[cfg(any(feature = "plot", feature = "plot-svg"))]
792 #[test]
793 fn test_data_range_all_nan() {
794 let (lo, hi) = data_range(&[f64::NAN]);
795 assert_eq!((lo, hi), (0.0, 1.0));
796 }
797
798 #[cfg(any(feature = "plot", feature = "plot-svg"))]
799 #[test]
800 fn test_data_range_constant() {
801 let (lo, hi) = data_range(&[5.0, 5.0, 5.0]);
803 assert!(hi > lo);
804 }
805
806 #[test]
809 fn test_compute_luminance_known() {
810 let lum = compute_luminance(&[1.0], &[0.0], &[0.0]);
812 assert!((lum[0] - 0.299).abs() < 1e-9);
813 }
814
815 #[test]
816 fn test_compute_luminance_clamps_above_1() {
817 let lum = compute_luminance(&[2.0], &[2.0], &[2.0]);
819 assert_eq!(lum[0], 1.0);
820 }
821
822 #[test]
823 fn test_compute_luminance_clamps_below_0() {
824 let lum = compute_luminance(&[-1.0], &[-1.0], &[-1.0]);
825 assert_eq!(lum[0], 0.0);
826 }
827
828 #[test]
829 fn test_imshow_gray_clamp_vs_imagesc_scale() {
830 let v: f64 = 2.0;
835 let gray = (v.clamp(0.0, 1.0) * 255.0).round() as u8;
836 assert_eq!(gray, 255);
837 let v2: f64 = -0.5;
840 let gray2 = (v2.clamp(0.0, 1.0) * 255.0).round() as u8;
841 assert_eq!(gray2, 0);
842 }
843}