1use std::collections::HashMap;
2
3use ndarray::Array2;
4use num_traits::ToPrimitive;
5use plotters::prelude::*;
6
7use crate::symbolic::core::DagOp;
8use crate::symbolic::core::Expr;
9
10#[derive(Clone, Debug)]
12pub struct PlotConfig {
13 pub width: u32,
15 pub height: u32,
17 pub caption: String,
19 pub line_color: RGBAColor,
21 pub mesh_color: RGBAColor,
23 pub samples: usize,
25 pub pitch: f64,
27 pub yaw: f64,
29 pub scale: f64,
31 pub label_font_size: u32,
33 pub caption_font_size: u32,
35}
36
37impl Default for PlotConfig {
38 fn default() -> Self {
39 Self {
40 width: 800,
41 height: 600,
42 caption: "Plot".to_string(),
43 line_color: RED.to_rgba(),
44 mesh_color: BLACK.mix(0.1),
45 samples: 500,
46 pitch: 0.5,
47 yaw: 0.5,
48 scale: 0.7,
49 label_font_size: 20,
50 caption_font_size: 40,
51 }
52 }
53}
54
55pub(crate) fn eval_expr(
59 root_expr: &Expr,
60 vars: &HashMap<String, f64>,
61) -> Result<f64, String> {
62 let mut results: HashMap<Expr, f64> = HashMap::new();
63
64 let mut stack: Vec<Expr> = vec![root_expr.clone()];
65
66 let mut visited = std::collections::HashSet::new();
67
68 while let Some(expr) = stack.last() {
69 if results.contains_key(expr) {
70 stack.pop();
71
72 continue;
73 }
74
75 let children = expr.children();
76
77 if children.is_empty() || visited.contains(expr) {
78 let current_expr = stack.pop().expect("Expr present");
79
80 let children = current_expr.children();
81
82 let get_child_val = |i: usize| -> f64 { results[&children[i]] };
83
84 let val_result = match current_expr.op() {
85 | DagOp::Constant(c) => Ok(c.into_inner()),
86 | DagOp::BigInt(i) => {
87 i.to_f64()
88 .ok_or_else(|| "BigInt conversion to f64 failed".to_string())
89 },
90 | DagOp::Rational(r) => {
91 Ok(r.numer().to_f64().unwrap() / r.denom().to_f64().unwrap())
92 },
93 | DagOp::Variable(v) => {
94 vars.get(&v)
95 .copied()
96 .ok_or_else(|| format!("Variable '{v}' not found"))
97 },
98 | DagOp::Add => Ok(get_child_val(0) + get_child_val(1)),
99 | DagOp::Sub => Ok(get_child_val(0) - get_child_val(1)),
100 | DagOp::Mul => Ok(get_child_val(0) * get_child_val(1)),
101 | DagOp::Div => Ok(get_child_val(0) / get_child_val(1)),
102 | DagOp::Power => Ok(get_child_val(0).powf(get_child_val(1))),
103 | DagOp::Neg => Ok(-get_child_val(0)),
104 | DagOp::Sqrt => Ok(get_child_val(0).sqrt()),
105 | DagOp::Abs => Ok(get_child_val(0).abs()),
106 | DagOp::Sin => Ok(get_child_val(0).sin()),
107 | DagOp::Cos => Ok(get_child_val(0).cos()),
108 | DagOp::Tan => Ok(get_child_val(0).tan()),
109 | DagOp::Csc => Ok(1.0 / get_child_val(0).sin()),
110 | DagOp::Sec => Ok(1.0 / get_child_val(0).cos()),
111 | DagOp::Cot => Ok(1.0 / get_child_val(0).tan()),
112 | DagOp::ArcSin => Ok(get_child_val(0).asin()),
113 | DagOp::ArcCos => Ok(get_child_val(0).acos()),
114 | DagOp::ArcTan => Ok(get_child_val(0).atan()),
115 | DagOp::Sinh => Ok(get_child_val(0).sinh()),
116 | DagOp::Cosh => Ok(get_child_val(0).cosh()),
117 | DagOp::Tanh => Ok(get_child_val(0).tanh()),
118 | DagOp::Log => Ok(get_child_val(0).ln()),
119 | DagOp::LogBase => Ok(get_child_val(1).log(get_child_val(0))),
120 | DagOp::Exp => Ok(get_child_val(0).exp()),
121 | DagOp::Floor => Ok(get_child_val(0).floor()),
122 | DagOp::Pi => Ok(std::f64::consts::PI),
123 | DagOp::E => Ok(std::f64::consts::E),
124 | _ => {
125 Err(format!(
126 "Numerical evaluation for operation {:?} is not implemented",
127 current_expr.op()
128 ))
129 },
130 };
131
132 let val = val_result?;
133
134 results.insert(current_expr, val);
135 } else {
136 visited.insert(expr.clone());
137
138 for child in children.iter().rev() {
139 stack.push(child.clone());
140 }
141 }
142 }
143
144 Ok(results[root_expr])
145}
146
147pub fn plot_function_2d(
153 expr: &Expr,
154 var: &str,
155 range: (f64, f64),
156 path: &str,
157 config: Option<PlotConfig>,
158) -> Result<(), String> {
159 let conf = config.unwrap_or_default();
160
161 let root = BitMapBackend::new(path, (conf.width, conf.height)).into_drawing_area();
162
163 root.fill(&WHITE).map_err(|e| e.to_string())?;
164
165 let y_min = (0..100)
166 .map(|i| {
167 let x = (range.1 - range.0).mul_add(f64::from(i) / 99.0, range.0);
168
169 eval_expr(expr, &HashMap::from([(var.to_string(), x)]))
170 })
171 .filter_map(Result::ok)
172 .fold(f64::INFINITY, f64::min);
173
174 let y_max = (0..100)
175 .map(|i| {
176 let x = (range.1 - range.0).mul_add(f64::from(i) / 99.0, range.0);
177
178 eval_expr(expr, &HashMap::from([(var.to_string(), x)]))
179 })
180 .filter_map(Result::ok)
181 .fold(f64::NEG_INFINITY, f64::max);
182
183 let mut chart = ChartBuilder::on(&root)
184 .caption(&conf.caption, ("sans-serif", 40).into_font())
185 .margin(5)
186 .x_label_area_size(30)
187 .y_label_area_size(30)
188 .build_cartesian_2d(range.0..range.1, y_min..y_max)
189 .map_err(|e| e.to_string())?;
190
191 chart
192 .configure_mesh()
193 .light_line_style(conf.mesh_color)
194 .draw()
195 .map_err(|e| e.to_string())?;
196
197 chart
198 .draw_series(LineSeries::new(
199 (0..=conf.samples).map(|i| {
200 let x = (range.1 - range.0).mul_add((i as f64) / conf.samples as f64, range.0);
201 let y = eval_expr(expr, &HashMap::from([(var.to_string(), x)])).unwrap_or(0.0);
202 (x, y)
203 }),
204 &conf.line_color,
205 ))
206 .map_err(|e| e.to_string())?;
207
208 root.present().map_err(|e| e.to_string())?;
209
210 Ok(())
211}
212
213pub fn plot_series_2d(
219 series: &[(String, Vec<(f64, f64)>)],
220 path: &str,
221 config: Option<PlotConfig>,
222) -> Result<(), String> {
223 let conf = config.unwrap_or_default();
224
225 let root = BitMapBackend::new(path, (conf.width, conf.height)).into_drawing_area();
226
227 root.fill(&WHITE).map_err(|e| e.to_string())?;
228
229 if series.is_empty() {
230 return Err("No data series \
231 provided"
232 .to_string());
233 }
234
235 let mut x_min = f64::INFINITY;
237
238 let mut x_max = f64::NEG_INFINITY;
239
240 let mut y_min = f64::INFINITY;
241
242 let mut y_max = f64::NEG_INFINITY;
243
244 for (_, data) in series {
245 for &(x, y) in data {
246 x_min = x_min.min(x);
247
248 x_max = x_max.max(x);
249
250 y_min = y_min.min(y);
251
252 y_max = y_max.max(y);
253 }
254 }
255
256 let x_pad = (x_max - x_min) * 0.05;
258
259 let y_pad = (y_max - y_min) * 0.1;
260
261 let x_range = (x_min - x_pad)..(x_max + x_pad);
262
263 let y_range = (y_min - y_pad)..(y_max + y_pad);
264
265 let mut chart = ChartBuilder::on(&root)
266 .caption(
267 &conf.caption,
268 ("sans-serif", conf.caption_font_size).into_font(),
269 )
270 .margin(conf.label_font_size / 2)
271 .x_label_area_size(conf.label_font_size * 2)
272 .y_label_area_size(conf.label_font_size * 2)
273 .build_cartesian_2d(x_range, y_range)
274 .map_err(|e| e.to_string())?;
275
276 chart
277 .configure_mesh()
278 .light_line_style(conf.mesh_color)
279 .label_style(("sans-serif", conf.label_font_size).into_font())
280 .draw()
281 .map_err(|e| e.to_string())?;
282
283 let colors = [&RED, &BLUE, &GREEN, &CYAN, &MAGENTA, &YELLOW, &BLACK];
284
285 for (i, (label, data)) in series.iter().enumerate() {
286 let color = colors[i % colors.len()];
287
288 chart
289 .draw_series(LineSeries::new(data.clone(), color))
290 .map_err(|e| e.to_string())?
291 .label(label)
292 .legend(move |(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], color));
293 }
294
295 chart
296 .configure_series_labels()
297 .background_style(WHITE.mix(0.8))
298 .border_style(BLACK)
299 .label_font(("sans-serif", conf.label_font_size).into_font())
300 .draw()
301 .map_err(|e| e.to_string())?;
302
303 root.present().map_err(|e| e.to_string())?;
304
305 Ok(())
306}
307
308pub fn plot_vector_field_2d(
314 comps: (&Expr, &Expr),
315 vars: (&str, &str),
316 x_range: (f64, f64),
317 y_range: (f64, f64),
318 path: &str,
319 config: Option<PlotConfig>,
320) -> Result<(), String> {
321 let conf = config.unwrap_or_default();
322
323 let root = BitMapBackend::new(path, (conf.width, conf.height)).into_drawing_area();
324
325 root.fill(&WHITE).map_err(|e| e.to_string())?;
326
327 let mut chart = ChartBuilder::on(&root)
328 .caption(&conf.caption, ("sans-serif", 40).into_font())
329 .build_cartesian_2d(x_range.0..x_range.1, y_range.0..y_range.1)
330 .map_err(|e| e.to_string())?;
331
332 chart
333 .configure_mesh()
334 .light_line_style(conf.mesh_color)
335 .draw()
336 .map_err(|e| e.to_string())?;
337
338 let (vx_expr, vy_expr) = comps;
339
340 let (x_var, y_var) = vars;
341
342 let mut arrows = Vec::new();
343
344 let steps: usize = ((conf.samples as f64).sqrt().round() as i64)
345 .try_into()
346 .unwrap_or(0);
347
348 for i in 0..steps {
349 for j in 0..steps {
350 let x = (x_range.1 - x_range.0).mul_add((i as f64) / (steps - 1) as f64, x_range.0);
351
352 let y = (y_range.1 - y_range.0).mul_add((j as f64) / (steps - 1) as f64, y_range.0);
353
354 let mut vars_map = HashMap::new();
355
356 vars_map.insert(x_var.to_string(), x);
357
358 vars_map.insert(y_var.to_string(), y);
359
360 if let (Ok(vx), Ok(vy)) = (eval_expr(vx_expr, &vars_map), eval_expr(vy_expr, &vars_map))
361 {
362 let magnitude = vx.hypot(vy);
363
364 if magnitude > 1e-9 {
365 let scale = (x_range.1 - x_range.0) * 0.05;
366
367 let end_x = (vx / magnitude).mul_add(scale, x);
368
369 let end_y = (vy / magnitude).mul_add(scale, y);
370
371 arrows.push(PathElement::new(
372 vec![(x, y), (end_x, end_y)],
373 conf.line_color,
374 ));
375 }
376 }
377 }
378 }
379
380 chart.draw_series(arrows).map_err(|e| e.to_string())?;
381
382 root.present().map_err(|e| e.to_string())?;
383
384 Ok(())
385}
386
387pub fn plot_surface_3d(
393 expr: &Expr,
394 vars: (&str, &str),
395 x_range: (f64, f64),
396 y_range: (f64, f64),
397 path: &str,
398 config: Option<PlotConfig>,
399) -> Result<(), String> {
400 let conf = config.unwrap_or_default();
401
402 let root = BitMapBackend::new(path, (conf.width, conf.height)).into_drawing_area();
403
404 root.fill(&WHITE).map_err(|e| e.to_string())?;
405
406 let mut chart = ChartBuilder::on(&root)
407 .caption(&conf.caption, ("sans-serif", 40).into_font())
408 .build_cartesian_3d(x_range.0..x_range.1, -1.0..1.0, y_range.0..y_range.1)
409 .map_err(|e| e.to_string())?;
410
411 chart.configure_axes().draw().map_err(|e| e.to_string())?;
412
413 let (x_var, y_var) = vars;
414
415 let steps: usize = ((conf.samples as f64).sqrt().round() as i64)
416 .try_into()
417 .unwrap_or(0);
418
419 let _ = chart.draw_series(
420 SurfaceSeries::xoz(
421 (0..steps)
422 .map(|i| x_range.0 + (x_range.1 - x_range.0) * (i as f64) / (steps - 1) as f64),
423 (0..steps)
424 .map(|i| y_range.0 + (y_range.1 - y_range.0) * (i as f64) / (steps - 1) as f64),
425 |x, z| {
426 let mut vars_map = HashMap::new();
427
428 vars_map.insert(x_var.to_string(), x);
429
430 vars_map.insert(y_var.to_string(), z);
431
432 eval_expr(expr, &vars_map).unwrap_or(0.0)
433 },
434 )
435 .style(conf.line_color.mix(0.5).filled()),
436 );
437
438 root.present().map_err(|e| e.to_string())?;
439
440 Ok(())
441}
442
443pub fn plot_surface_2d(
449 data: &Array2<f64>,
450 path: &str,
451 config: Option<PlotConfig>,
452) -> Result<(), String> {
453 let conf = config.unwrap_or_default();
454
455 let root = BitMapBackend::new(path, (conf.width, conf.height)).into_drawing_area();
456
457 root.fill(&WHITE).map_err(|e| e.to_string())?;
458
459 let (height, width) = data.dim();
460
461 let mut min_val = f64::INFINITY;
462
463 let mut max_val = f64::NEG_INFINITY;
464
465 for &val in data {
466 min_val = min_val.min(val);
467
468 max_val = max_val.max(val);
469 }
470
471 let z_min = if (max_val - min_val).abs() < 1e-9 {
473 min_val - 1.0
474 } else {
475 (max_val - min_val).mul_add(-0.1, min_val)
476 };
477
478 let z_max = if (max_val - min_val).abs() < 1e-9 {
479 max_val + 1.0
480 } else {
481 (max_val - min_val).mul_add(0.1, max_val)
482 };
483
484 let mut chart = ChartBuilder::on(&root)
485 .caption(&conf.caption, ("sans-serif", 40).into_font())
486 .build_cartesian_3d(0.0..width as f64, z_min..z_max, 0.0..height as f64)
487 .map_err(|e| e.to_string())?;
488
489 chart.configure_axes().draw().map_err(|e| e.to_string())?;
490
491 chart
492 .draw_series(
493 SurfaceSeries::xoz(
494 (0..width).map(|x| x as f64),
495 (0..height).map(|y| y as f64),
496 |x, y| {
497 let ix = usize::try_from(x.round() as isize)
498 .unwrap_or(0)
499 .min(width - 1);
500
501 let iy = usize::try_from(y.round() as isize)
502 .unwrap_or(0)
503 .min(height - 1);
504
505 data[[iy, ix]]
506 },
507 )
508 .style(conf.line_color.mix(0.5).filled()),
509 )
510 .map_err(|e| e.to_string())?;
511
512 root.present().map_err(|e| e.to_string())?;
513
514 Ok(())
515}
516
517pub fn plot_parametric_curve_3d(
523 comps: (&Expr, &Expr, &Expr),
524 var: &str,
525 range: (f64, f64),
526 path: &str,
527 config: Option<PlotConfig>,
528) -> Result<(), String> {
529 let conf = config.unwrap_or_default();
530
531 let root = BitMapBackend::new(path, (conf.width, conf.height)).into_drawing_area();
532
533 root.fill(&WHITE).map_err(|e| e.to_string())?;
534
535 let mut chart = ChartBuilder::on(&root)
536 .caption(&conf.caption, ("sans-serif", 40).into_font())
537 .build_cartesian_3d(-3.0..3.0, -3.0..3.0, -3.0..3.0)
538 .map_err(|e| e.to_string())?;
539
540 chart.configure_axes().draw().map_err(|e| e.to_string())?;
541
542 let (x_expr, y_expr, z_expr) = comps;
543
544 chart
545 .draw_series(LineSeries::new(
546 (0..=conf.samples).map(|i| {
547 let t = (range.1 - range.0).mul_add((i as f64) / conf.samples as f64, range.0);
548 let mut vars_map = HashMap::new();
549 vars_map.insert(var.to_string(), t);
550
551 let x = eval_expr(x_expr, &vars_map).unwrap_or(0.0);
552 let y = eval_expr(y_expr, &vars_map).unwrap_or(0.0);
553 let z = eval_expr(z_expr, &vars_map).unwrap_or(0.0);
554 (x, y, z)
555 }),
556 &conf.line_color,
557 ))
558 .map_err(|e| e.to_string())?;
559
560 root.present().map_err(|e| e.to_string())?;
561
562 Ok(())
563}
564
565pub fn plot_vector_field_3d(
571 comps: (&Expr, &Expr, &Expr),
572 vars: (&str, &str, &str),
573 ranges: ((f64, f64), (f64, f64), (f64, f64)),
574 path: &str,
575 config: Option<PlotConfig>,
576) -> Result<(), String> {
577 let conf = config.unwrap_or_default();
578
579 let root = BitMapBackend::new(path, (conf.width, conf.height)).into_drawing_area();
580
581 root.fill(&WHITE).map_err(|e| e.to_string())?;
582
583 let (x_range, y_range, z_range) = ranges;
584
585 let mut chart = ChartBuilder::on(&root)
586 .caption(&conf.caption, ("sans-serif", 40).into_font())
587 .build_cartesian_3d(
588 x_range.0..x_range.1,
589 y_range.0..y_range.1,
590 z_range.0..z_range.1,
591 )
592 .map_err(|e| e.to_string())?;
593
594 chart.configure_axes().draw().map_err(|e| e.to_string())?;
595
596 let (vx_expr, vy_expr, vz_expr) = comps;
597
598 let (x_var, y_var, z_var) = vars;
599
600 let mut arrows = Vec::new();
601
602 let steps: usize = ((conf.samples as f64).cbrt().round() as i64)
603 .try_into()
604 .unwrap_or(0);
605
606 for i in 0..steps {
607 for j in 0..steps {
608 for k in 0..steps {
609 let x = (x_range.1 - x_range.0).mul_add((i as f64) / (steps - 1) as f64, x_range.0);
610
611 let y = (y_range.1 - y_range.0).mul_add((j as f64) / (steps - 1) as f64, y_range.0);
612
613 let z = (z_range.1 - z_range.0).mul_add((k as f64) / (steps - 1) as f64, z_range.0);
614
615 let mut vars_map = HashMap::new();
616
617 vars_map.insert(x_var.to_string(), x);
618
619 vars_map.insert(y_var.to_string(), y);
620
621 vars_map.insert(z_var.to_string(), z);
622
623 if let (Ok(vx), Ok(vy), Ok(vz)) = (
624 eval_expr(vx_expr, &vars_map),
625 eval_expr(vy_expr, &vars_map),
626 eval_expr(vz_expr, &vars_map),
627 ) {
628 let magnitude = vz.mul_add(vz, vx.mul_add(vx, vy * vy)).sqrt();
629
630 if magnitude > 1e-6 {
631 let scale = (x_range.1 - x_range.0) * 0.05;
632
633 let end_x = (vx / magnitude).mul_add(scale, x);
634
635 let end_y = (vy / magnitude).mul_add(scale, y);
636
637 let end_z = (vz / magnitude).mul_add(scale, z);
638
639 arrows.push(PathElement::new(
640 vec![(x, y, z), (end_x, end_y, end_z)],
641 conf.line_color,
642 ));
643 }
644 }
645 }
646 }
647 }
648
649 chart.draw_series(arrows).map_err(|e| e.to_string())?;
650
651 root.present().map_err(|e| e.to_string())?;
652
653 Ok(())
654}
655
656#[cfg(test)]
657mod tests {
658
659 use std::collections::HashMap;
660
661 use super::*;
662 use crate::prelude::Expr;
663
664 #[test]
665 fn test_eval_basic() {
666 let x = Expr::Variable("x".to_string());
667
668 let expr = Expr::new_add(x, Expr::Constant(2.0));
669
670 let mut vars = HashMap::new();
671
672 vars.insert("x".to_string(), 3.0);
673
674 let res = eval_expr(&expr, &vars).unwrap();
675
676 assert_eq!(res, 5.0);
677 }
678
679 #[test]
680 fn test_eval_trig() {
681 let x = Expr::Variable("x".to_string());
682
683 let expr = Expr::new_sin(x);
684
685 let mut vars = HashMap::new();
686
687 vars.insert("x".to_string(), std::f64::consts::PI / 2.0);
688
689 let res = eval_expr(&expr, &vars).unwrap();
690
691 assert!((res - 1.0).abs() < 1e-10);
692 }
693
694 #[test]
695 fn test_eval_log_power() {
696 let x = Expr::Variable("x".to_string());
697
698 let expr = Expr::new_exp(Expr::new_log(x.clone()));
700
701 let mut vars = HashMap::new();
702
703 vars.insert("x".to_string(), 5.0);
704
705 let res = eval_expr(&expr, &vars).unwrap();
706
707 assert!((res - 5.0).abs() < 1e-10);
708 }
709
710 #[test]
711 fn test_eval_error() {
712 let x = Expr::Variable("x".to_string());
713
714 let vars = HashMap::new(); let res = eval_expr(&x, &vars);
716
717 assert!(res.is_err());
718 }
719
720 use proptest::prelude::*;
721
722 proptest! {
723 #[test]
724 fn prop_no_panic_eval(
725 val in -100.0..100.0f64,
726 depth in 1..4usize,
727 ) {
728 let x = Expr::Variable("x".to_string());
729 let mut expr = x.clone();
730 for _ in 0..depth {
731 expr = Expr::new_add(expr.clone(), Expr::Constant(1.0));
732 expr = Expr::new_mul(expr.clone(), Expr::Constant(0.5));
733 }
734 let mut vars = HashMap::new();
735 vars.insert("x".to_string(), val);
736 let _ = eval_expr(&expr, &vars);
737 }
738 }
739}
740
741pub fn plot_3d_path_from_points(
747 data: &[Vec<f64>],
748 path: &str,
749 config: Option<PlotConfig>,
750) -> Result<(), String> {
751 let conf = config.unwrap_or_default();
752
753 let root = BitMapBackend::new(path, (conf.width, conf.height)).into_drawing_area();
754
755 root.fill(&WHITE).map_err(|e| e.to_string())?;
756
757 let (mut x_min, mut x_max) = (f64::INFINITY, f64::NEG_INFINITY);
759
760 let (mut y_min, mut y_max) = (f64::INFINITY, f64::NEG_INFINITY);
761
762 let (mut z_min, mut z_max) = (f64::INFINITY, f64::NEG_INFINITY);
763
764 for point in data {
765 if point.len() >= 3 {
766 x_min = x_min.min(point[0]);
767
768 x_max = x_max.max(point[0]);
769
770 y_min = y_min.min(point[1]);
771
772 y_max = y_max.max(point[1]);
773
774 z_min = z_min.min(point[2]);
775
776 z_max = z_max.max(point[2]);
777 }
778 }
779
780 let mut chart = ChartBuilder::on(&root)
781 .caption(&conf.caption, ("sans-serif", 40).into_font())
782 .build_cartesian_3d(x_min..x_max, z_min..z_max, y_min..y_max) .map_err(|e| e.to_string())?;
784
785 chart.configure_axes().draw().map_err(|e| e.to_string())?;
786
787 chart
788 .draw_series(LineSeries::new(
789 data.iter().filter_map(|p| {
790 if p.len() >= 3 {
791 Some((p[0], p[2], p[1]))
792 } else {
793 None
794 }
795 }), &conf.line_color,
797 ))
798 .map_err(|e| e.to_string())?;
799
800 root.present().map_err(|e| e.to_string())?;
801
802 Ok(())
803}
804
805pub fn plot_heatmap_2d(
811 data: &Array2<f64>,
812 path: &str,
813 config: Option<PlotConfig>,
814) -> Result<(), String> {
815 let conf = config.unwrap_or_default();
816
817 let root = BitMapBackend::new(path, (conf.width, conf.height)).into_drawing_area();
818
819 root.fill(&WHITE).map_err(|e| e.to_string())?;
820
821 let (height, width) = data.dim();
822
823 let mut min_val = f64::INFINITY;
825
826 let mut max_val = f64::NEG_INFINITY;
827
828 for &val in data {
829 min_val = min_val.min(val);
830
831 println!("min_val: {min_val}");
832
833 max_val = max_val.max(val);
834
835 println!("max_val: {max_val}");
836 }
837
838 let mut chart = ChartBuilder::on(&root)
839 .caption(&conf.caption, ("sans-serif", 40).into_font())
840 .build_cartesian_2d(0..width as u32, 0..height as u32)
841 .map_err(|e| e.to_string())?;
842
843 chart.configure_mesh().draw().map_err(|e| e.to_string())?;
844
845 chart
846 .draw_series((0..width).flat_map(move |x| {
847 (0..height).map(move |y| {
848 let val = data[[y, x]]; let normalized = if max_val - min_val > 0.0 {
850 (val - min_val) / (max_val - min_val)
851 } else {
852 0.5 };
854 let color = HSLColor(240.0 * normalized, 1.0, 0.5);
857 Rectangle::new(
858 [(x as u32, y as u32), (x as u32 + 1, y as u32 + 1)],
859 color.filled(),
860 )
861 })
862 }))
863 .map_err(|e| e.to_string())?;
864
865 root.present().map_err(|e| e.to_string())?;
866
867 Ok(())
868}