1use crate::error::{NdimageError, NdimageResult};
8use crate::utils::{safe_f64_to_float, safe_usize_to_float};
9use crate::visualization::colormap::create_colormap;
10use crate::visualization::types::{PlotConfig, ReportFormat};
11use scirs2_core::ndarray::{ArrayView1, ArrayView2};
12use scirs2_core::numeric::{Float, FromPrimitive, ToPrimitive, Zero};
13use std::fmt::{Debug, Write};
14
15pub fn plot_histogram<T>(data: &ArrayView1<T>, config: &PlotConfig) -> NdimageResult<String>
17where
18 T: Float + FromPrimitive + ToPrimitive + Debug + Clone,
19{
20 if data.is_empty() {
21 return Err(NdimageError::InvalidInput("Data array is empty".into()));
22 }
23
24 let min_val = data.iter().cloned().fold(T::infinity(), T::min);
26 let max_val = data.iter().cloned().fold(T::neg_infinity(), T::max);
27
28 if max_val <= min_val {
29 return Err(NdimageError::InvalidInput(
30 "All data values are the same".into(),
31 ));
32 }
33
34 let mut histogram = vec![0usize; config.num_bins];
36 let range = max_val - min_val;
37 let bin_size = range / safe_usize_to_float::<T>(config.num_bins)?;
38
39 for &value in data.iter() {
40 let normalized = (value - min_val) / bin_size;
41 let bin_idx = normalized.to_usize().unwrap_or(0).min(config.num_bins - 1);
42 histogram[bin_idx] += 1;
43 }
44
45 let max_count = *histogram.iter().max().unwrap_or(&1);
47 let mut plot = String::new();
48
49 match config.format {
50 ReportFormat::Html => {
51 writeln!(&mut plot, "<div class='histogram-plot'>")?;
52 writeln!(&mut plot, "<h3>{}</h3>", config.title)?;
53 writeln!(&mut plot, "<div class='histogram-bars'>")?;
54
55 for (i, &count) in histogram.iter().enumerate() {
56 let height_percent = (count as f64 / max_count as f64) * 100.0;
57 let bin_start = min_val + safe_usize_to_float::<T>(i)? * bin_size;
58 let bin_end = bin_start + bin_size;
59
60 writeln!(
61 &mut plot,
62 "<div class='bar' style='height: {:.1}%' title='[{:.3}, {:.3}): {}'></div>",
63 height_percent,
64 bin_start.to_f64().unwrap_or(0.0),
65 bin_end.to_f64().unwrap_or(0.0),
66 count
67 )?;
68 }
69
70 writeln!(&mut plot, "</div>")?;
71 writeln!(&mut plot, "<div class='axis-labels'>")?;
72 writeln!(&mut plot, "<span class='xlabel'>{}</span>", config.xlabel)?;
73 writeln!(&mut plot, "<span class='ylabel'>{}</span>", config.ylabel)?;
74 writeln!(&mut plot, "</div>")?;
75 writeln!(&mut plot, "</div>")?;
76 }
77 ReportFormat::Markdown => {
78 writeln!(&mut plot, "## {}", config.title)?;
79 writeln!(&mut plot)?;
80 writeln!(&mut plot, "```")?;
81
82 for (i, &count) in histogram.iter().enumerate() {
83 let bar_length = (count as f64 / max_count as f64 * 50.0) as usize;
84 let bin_center = min_val
85 + (safe_usize_to_float::<T>(i)? + safe_f64_to_float::<T>(0.5)?) * bin_size;
86
87 writeln!(
88 &mut plot,
89 "{:8.3} |{:<50} {}",
90 bin_center.to_f64().unwrap_or(0.0),
91 "*".repeat(bar_length),
92 count
93 )?;
94 }
95
96 writeln!(&mut plot, "```")?;
97 writeln!(&mut plot)?;
98 writeln!(&mut plot, "**{}** vs **{}**", config.xlabel, config.ylabel)?;
99 }
100 ReportFormat::Text => {
101 writeln!(&mut plot, "{}", config.title)?;
102 writeln!(&mut plot, "{}", "=".repeat(config.title.len()))?;
103 writeln!(&mut plot)?;
104
105 for (i, &count) in histogram.iter().enumerate() {
106 let bar_length = (count as f64 / max_count as f64 * 50.0) as usize;
107 let bin_center = min_val
108 + (safe_usize_to_float::<T>(i)? + safe_f64_to_float::<T>(0.5)?) * bin_size;
109
110 writeln!(
111 &mut plot,
112 "{:8.3} |{:<50} {}",
113 bin_center.to_f64().unwrap_or(0.0),
114 "*".repeat(bar_length),
115 count
116 )?;
117 }
118
119 writeln!(&mut plot)?;
120 writeln!(&mut plot, "X-axis: {}", config.xlabel)?;
121 writeln!(&mut plot, "Y-axis: {}", config.ylabel)?;
122 }
123 }
124
125 Ok(plot)
126}
127
128pub fn plot_profile<T>(
130 x_data: &ArrayView1<T>,
131 y_data: &ArrayView1<T>,
132 config: &PlotConfig,
133) -> NdimageResult<String>
134where
135 T: Float + FromPrimitive + ToPrimitive + Debug + Clone,
136{
137 if x_data.len() != y_data.len() {
138 return Err(NdimageError::InvalidInput(
139 "X and Y data must have the same length".into(),
140 ));
141 }
142
143 if x_data.is_empty() {
144 return Err(NdimageError::InvalidInput("Data arrays are empty".into()));
145 }
146
147 let mut plot = String::new();
148
149 match config.format {
150 ReportFormat::Html => {
151 writeln!(&mut plot, "<div class='profile-plot'>")?;
152 writeln!(&mut plot, "<h3>{}</h3>", config.title)?;
153 writeln!(
154 &mut plot,
155 "<svg width='{}' height='{}'>",
156 config.width, config.height
157 )?;
158
159 let x_min = x_data.iter().cloned().fold(T::infinity(), T::min);
161 let x_max = x_data.iter().cloned().fold(T::neg_infinity(), T::max);
162 let y_min = y_data.iter().cloned().fold(T::infinity(), T::min);
163 let y_max = y_data.iter().cloned().fold(T::neg_infinity(), T::max);
164
165 let x_range = x_max - x_min;
166 let y_range = y_max - y_min;
167
168 if x_range > T::zero() && y_range > T::zero() {
169 let mut path_data = String::new();
170
171 for (i, (&x, &y)) in x_data.iter().zip(y_data.iter()).enumerate() {
172 let px = ((x - x_min) / x_range * safe_usize_to_float(config.width - 100)?
173 + safe_f64_to_float::<T>(50.0)?)
174 .to_f64()
175 .unwrap_or(0.0);
176 let py = (config.height as f64 - 50.0)
177 - ((y - y_min) / y_range * safe_usize_to_float(config.height - 100)?)
178 .to_f64()
179 .unwrap_or(0.0);
180
181 if i == 0 {
182 write!(&mut path_data, "M {} {}", px, py)?;
183 } else {
184 write!(&mut path_data, " L {} {}", px, py)?;
185 }
186 }
187
188 writeln!(
189 &mut plot,
190 "<path d='{}' stroke='blue' stroke-width='2' fill='none'/>",
191 path_data
192 )?;
193
194 if config.show_grid {
196 add_svg_grid(&mut plot, config.width, config.height)?;
197 }
198 }
199
200 writeln!(&mut plot, "</svg>")?;
201 writeln!(&mut plot, "<div class='axis-labels'>")?;
202 writeln!(&mut plot, "<span class='xlabel'>{}</span>", config.xlabel)?;
203 writeln!(&mut plot, "<span class='ylabel'>{}</span>", config.ylabel)?;
204 writeln!(&mut plot, "</div>")?;
205 writeln!(&mut plot, "</div>")?;
206 }
207 ReportFormat::Markdown => {
208 writeln!(&mut plot, "## {}", config.title)?;
209 writeln!(&mut plot)?;
210 writeln!(&mut plot, "```")?;
211
212 for (&x, &y) in x_data.iter().zip(y_data.iter()) {
213 writeln!(
214 &mut plot,
215 "{:10.4} {:10.4}",
216 x.to_f64().unwrap_or(0.0),
217 y.to_f64().unwrap_or(0.0)
218 )?;
219 }
220
221 writeln!(&mut plot, "```")?;
222 writeln!(&mut plot)?;
223 writeln!(&mut plot, "**{}** vs **{}**", config.xlabel, config.ylabel)?;
224 }
225 ReportFormat::Text => {
226 writeln!(&mut plot, "{}", config.title)?;
227 writeln!(&mut plot, "{}", "=".repeat(config.title.len()))?;
228 writeln!(&mut plot)?;
229 writeln!(&mut plot, "{:>10} {:>10}", config.xlabel, config.ylabel)?;
230 writeln!(&mut plot, "{}", "-".repeat(22))?;
231
232 for (&x, &y) in x_data.iter().zip(y_data.iter()) {
233 writeln!(
234 &mut plot,
235 "{:10.4} {:10.4}",
236 x.to_f64().unwrap_or(0.0),
237 y.to_f64().unwrap_or(0.0)
238 )?;
239 }
240 }
241 }
242
243 Ok(plot)
244}
245
246pub fn plot_surface<T>(data: &ArrayView2<T>, config: &PlotConfig) -> NdimageResult<String>
248where
249 T: Float + FromPrimitive + ToPrimitive + Debug + Clone,
250{
251 let (height, width) = data.dim();
252 if height == 0 || width == 0 {
253 return Err(NdimageError::InvalidInput("Data array is empty".into()));
254 }
255
256 let mut plot = String::new();
257
258 let min_val = data.iter().cloned().fold(T::infinity(), T::min);
260 let max_val = data.iter().cloned().fold(T::neg_infinity(), T::max);
261
262 if max_val <= min_val {
263 return Err(NdimageError::InvalidInput(
264 "All data values are the same".into(),
265 ));
266 }
267
268 match config.format {
269 ReportFormat::Html => {
270 writeln!(&mut plot, "<div class='surface-plot'>")?;
271 writeln!(&mut plot, "<h3>{}</h3>", config.title)?;
272 writeln!(&mut plot, "<div class='surface-container'>")?;
273
274 let step_x = width.max(1) / (config.width / 20).max(1);
276 let step_y = height.max(1) / (config.height / 20).max(1);
277
278 for i in (0..height).step_by(step_y) {
279 for j in (0..width).step_by(step_x) {
280 let value = data[[i, j]];
281 let normalized = ((value - min_val) / (max_val - min_val))
282 .to_f64()
283 .unwrap_or(0.0);
284 let z_height = normalized * 100.0; let x_pos = (j as f64 / width as f64) * config.width as f64;
287 let y_pos = (i as f64 / height as f64) * config.height as f64;
288
289 let colormap = create_colormap(config.colormap, 256);
291 let color_idx = (normalized * 255.0) as usize;
292 let color = colormap.get(color_idx).unwrap_or(&colormap[0]);
293
294 writeln!(
295 &mut plot,
296 "<div class='surface-point' style='left: {:.1}px; top: {:.1}px; height: {:.1}%; background-color: {};'></div>",
297 x_pos, y_pos, z_height, color.to_hex()
298 )?;
299 }
300 }
301
302 writeln!(&mut plot, "</div>")?;
303 writeln!(&mut plot, "<div class='surface-info'>")?;
304 writeln!(
305 &mut plot,
306 "<p>Value range: [{:.3}, {:.3}]</p>",
307 min_val.to_f64().unwrap_or(0.0),
308 max_val.to_f64().unwrap_or(0.0)
309 )?;
310 writeln!(&mut plot, "</div>")?;
311 writeln!(&mut plot, "</div>")?;
312 }
313 ReportFormat::Markdown => {
314 writeln!(&mut plot, "## {} (3D Surface)", config.title)?;
315 writeln!(&mut plot)?;
316 writeln!(&mut plot, "```")?;
317 writeln!(&mut plot, "3D Surface Plot of {}×{} data", height, width)?;
318 writeln!(
319 &mut plot,
320 "Value range: [{:.3}, {:.3}]",
321 min_val.to_f64().unwrap_or(0.0),
322 max_val.to_f64().unwrap_or(0.0)
323 )?;
324 writeln!(&mut plot)?;
325
326 let ascii_height = 20;
328 let ascii_width = 60;
329 for i in 0..ascii_height {
330 for j in 0..ascii_width {
331 let data_i = (i * height) / ascii_height;
332 let data_j = (j * width) / ascii_width;
333 let value = data[[data_i, data_j]];
334 let normalized = ((value - min_val) / (max_val - min_val))
335 .to_f64()
336 .unwrap_or(0.0);
337
338 let char = match (normalized * 10.0) as u32 {
339 0..=1 => ' ',
340 2..=3 => '.',
341 4..=5 => ':',
342 6..=7 => '+',
343 8..=9 => '*',
344 _ => '#',
345 };
346 write!(&mut plot, "{}", char)?;
347 }
348 writeln!(&mut plot)?;
349 }
350
351 writeln!(&mut plot, "```")?;
352 }
353 ReportFormat::Text => {
354 writeln!(&mut plot, "{} (3D Surface)", config.title)?;
355 writeln!(&mut plot, "{}", "=".repeat(config.title.len() + 13))?;
356 writeln!(&mut plot)?;
357 writeln!(&mut plot, "Data dimensions: {}×{}", height, width)?;
358 writeln!(
359 &mut plot,
360 "Value range: [{:.3}, {:.3}]",
361 min_val.to_f64().unwrap_or(0.0),
362 max_val.to_f64().unwrap_or(0.0)
363 )?;
364 writeln!(&mut plot)?;
365
366 add_ascii_surface(&mut plot, data, 20, 60)?;
368 }
369 }
370
371 Ok(plot)
372}
373
374pub fn plot_contour<T>(
376 data: &ArrayView2<T>,
377 num_levels: usize,
378 config: &PlotConfig,
379) -> NdimageResult<String>
380where
381 T: Float + FromPrimitive + ToPrimitive + Debug + Clone,
382{
383 let (height, width) = data.dim();
384 if height == 0 || width == 0 {
385 return Err(NdimageError::InvalidInput("Data array is empty".into()));
386 }
387
388 let mut plot = String::new();
389
390 let min_val = data.iter().cloned().fold(T::infinity(), T::min);
392 let max_val = data.iter().cloned().fold(T::neg_infinity(), T::max);
393
394 if max_val <= min_val {
395 return Err(NdimageError::InvalidInput(
396 "All data values are the same".into(),
397 ));
398 }
399
400 let mut levels = Vec::new();
402 for i in 0..num_levels {
403 let t = i as f64 / (num_levels - 1).max(1) as f64;
404 let level = min_val + (max_val - min_val) * safe_f64_to_float::<T>(t)?;
405 levels.push(level);
406 }
407
408 match config.format {
409 ReportFormat::Html => {
410 writeln!(&mut plot, "<div class='contour-plot'>")?;
411 writeln!(&mut plot, "<h3>{}</h3>", config.title)?;
412 writeln!(
413 &mut plot,
414 "<svg width='{}' height='{}'>",
415 config.width, config.height
416 )?;
417
418 for (level_idx, &level) in levels.iter().enumerate() {
420 let color_intensity = (level_idx as f64 / num_levels as f64 * 255.0) as u8;
421 let color = format!(
422 "rgb({}, {}, {})",
423 color_intensity,
424 100,
425 255 - color_intensity
426 );
427
428 for i in 0..height.saturating_sub(1) {
430 for j in 0..width.saturating_sub(1) {
431 let val = data[[i, j]];
432 let threshold = (max_val - min_val) * safe_f64_to_float::<T>(0.02)?; if (val - level).abs() < threshold {
435 let x = (j as f64 / width as f64) * config.width as f64;
436 let y = (i as f64 / height as f64) * config.height as f64;
437
438 writeln!(
439 &mut plot,
440 "<circle cx='{:.1}' cy='{:.1}' r='1' fill='{}' opacity='0.7'/>",
441 x, y, color
442 )?;
443 }
444 }
445 }
446 }
447
448 writeln!(&mut plot, "</svg>")?;
449 writeln!(&mut plot, "<div class='contour-legend'>")?;
450 writeln!(&mut plot, "<h4>Contour Levels:</h4>")?;
451 for (i, &level) in levels.iter().enumerate() {
452 writeln!(
453 &mut plot,
454 "<span style='color: rgb({}, 100, {})'>Level {}: {:.3}</span><br/>",
455 (i as f64 / num_levels as f64 * 255.0) as u8,
456 255 - (i as f64 / num_levels as f64 * 255.0) as u8,
457 i + 1,
458 level.to_f64().unwrap_or(0.0)
459 )?;
460 }
461 writeln!(&mut plot, "</div>")?;
462 writeln!(&mut plot, "</div>")?;
463 }
464 ReportFormat::Markdown => {
465 writeln!(&mut plot, "## {} (Contour)", config.title)?;
466 writeln!(&mut plot)?;
467 writeln!(&mut plot, "Contour levels:")?;
468 for (i, &level) in levels.iter().enumerate() {
469 writeln!(
470 &mut plot,
471 "- Level {}: {:.3}",
472 i + 1,
473 level.to_f64().unwrap_or(0.0)
474 )?;
475 }
476 }
477 ReportFormat::Text => {
478 writeln!(&mut plot, "{} (Contour)", config.title)?;
479 writeln!(&mut plot, "{}", "=".repeat(config.title.len() + 10))?;
480 writeln!(&mut plot)?;
481 writeln!(&mut plot, "Contour levels:")?;
482 for (i, &level) in levels.iter().enumerate() {
483 writeln!(
484 &mut plot,
485 " Level {}: {:.3}",
486 i + 1,
487 level.to_f64().unwrap_or(0.0)
488 )?;
489 }
490 }
491 }
492
493 Ok(plot)
494}
495
496pub fn visualize_gradient<T>(
498 gradient_x: &ArrayView2<T>,
499 gradient_y: &ArrayView2<T>,
500 config: &PlotConfig,
501) -> NdimageResult<String>
502where
503 T: Float + FromPrimitive + ToPrimitive + Debug + Clone,
504{
505 if gradient_x.dim() != gradient_y.dim() {
506 return Err(NdimageError::DimensionError(
507 "Gradient components must have the same dimensions".into(),
508 ));
509 }
510
511 let (height, width) = gradient_x.dim();
512 let mut plot = String::new();
513
514 match config.format {
515 ReportFormat::Html => {
516 writeln!(&mut plot, "<div class='gradient-plot'>")?;
517 writeln!(&mut plot, "<h3>{}</h3>", config.title)?;
518 writeln!(
519 &mut plot,
520 "<svg width='{}' height='{}'>",
521 config.width, config.height
522 )?;
523
524 let step_x = width.max(1) / (config.width / 20).max(1);
526 let step_y = height.max(1) / (config.height / 20).max(1);
527
528 for i in (0..height).step_by(step_y) {
529 for j in (0..width).step_by(step_x) {
530 let gx = gradient_x[[i, j]].to_f64().unwrap_or(0.0);
531 let gy = gradient_y[[i, j]].to_f64().unwrap_or(0.0);
532
533 let magnitude = (gx * gx + gy * gy).sqrt();
534 if magnitude > 1e-6 {
535 let scale = 10.0 / magnitude.max(1e-6);
536 let start_x = j as f64 * config.width as f64 / width as f64;
537 let start_y = i as f64 * config.height as f64 / height as f64;
538 let end_x = start_x + gx * scale;
539 let end_y = start_y + gy * scale;
540
541 writeln!(
542 &mut plot,
543 "<line x1='{:.1}' y1='{:.1}' x2='{:.1}' y2='{:.1}' stroke='red' stroke-width='1'/>",
544 start_x, start_y, end_x, end_y
545 )?;
546
547 add_svg_arrowhead(&mut plot, start_x, start_y, end_x, end_y)?;
549 }
550 }
551 }
552
553 writeln!(&mut plot, "</svg>")?;
554 writeln!(&mut plot, "</div>")?;
555 }
556 ReportFormat::Markdown => {
557 writeln!(&mut plot, "## {}", config.title)?;
558 writeln!(&mut plot)?;
559 writeln!(&mut plot, "Gradient vector field visualization")?;
560 writeln!(&mut plot)?;
561 writeln!(&mut plot, "- Image dimensions: {}×{}", width, height)?;
562
563 let magnitude_sum: f64 = gradient_x
565 .iter()
566 .zip(gradient_y.iter())
567 .map(|(&gx, &gy)| {
568 let gx_f = gx.to_f64().unwrap_or(0.0);
569 let gy_f = gy.to_f64().unwrap_or(0.0);
570 (gx_f * gx_f + gy_f * gy_f).sqrt()
571 })
572 .sum();
573
574 let avg_magnitude = magnitude_sum / (width * height) as f64;
575 writeln!(
576 &mut plot,
577 "- Average gradient magnitude: {:.4}",
578 avg_magnitude
579 )?;
580 }
581 ReportFormat::Text => {
582 writeln!(&mut plot, "{}", config.title)?;
583 writeln!(&mut plot, "{}", "=".repeat(config.title.len()))?;
584 writeln!(&mut plot)?;
585 writeln!(&mut plot, "Gradient Vector Field")?;
586 writeln!(&mut plot, "Image dimensions: {}×{}", width, height)?;
587
588 writeln!(&mut plot)?;
590 writeln!(&mut plot, "Sample gradient vectors:")?;
591 writeln!(
592 &mut plot,
593 "{:>5} {:>5} {:>10} {:>10} {:>10}",
594 "Row", "Col", "Grad_X", "Grad_Y", "Magnitude"
595 )?;
596 writeln!(&mut plot, "{}", "-".repeat(50))?;
597
598 let step = height.max(width) / 10;
599 for i in (0..height).step_by(step.max(1)) {
600 for j in (0..width).step_by(step.max(1)) {
601 let gx = gradient_x[[i, j]].to_f64().unwrap_or(0.0);
602 let gy = gradient_y[[i, j]].to_f64().unwrap_or(0.0);
603 let magnitude = (gx * gx + gy * gy).sqrt();
604
605 writeln!(
606 &mut plot,
607 "{:5} {:5} {:10.4} {:10.4} {:10.4}",
608 i, j, gx, gy, magnitude
609 )?;
610 }
611 }
612 }
613 }
614
615 Ok(plot)
616}
617
618pub fn plot_scatter<T>(
620 x_data: &ArrayView1<T>,
621 y_data: &ArrayView1<T>,
622 config: &PlotConfig,
623) -> NdimageResult<String>
624where
625 T: Float + FromPrimitive + ToPrimitive + Debug + Clone,
626{
627 if x_data.len() != y_data.len() {
628 return Err(NdimageError::InvalidInput(
629 "X and Y data must have the same length".into(),
630 ));
631 }
632
633 if x_data.is_empty() {
634 return Err(NdimageError::InvalidInput("Data arrays are empty".into()));
635 }
636
637 let mut plot = String::new();
638
639 let x_min = x_data.iter().cloned().fold(T::infinity(), T::min);
641 let x_max = x_data.iter().cloned().fold(T::neg_infinity(), T::max);
642 let y_min = y_data.iter().cloned().fold(T::infinity(), T::min);
643 let y_max = y_data.iter().cloned().fold(T::neg_infinity(), T::max);
644
645 match config.format {
646 ReportFormat::Html => {
647 writeln!(&mut plot, "<div class='scatter-plot'>")?;
648 writeln!(&mut plot, "<h3>{}</h3>", config.title)?;
649 writeln!(
650 &mut plot,
651 "<svg width='{}' height='{}'>",
652 config.width, config.height
653 )?;
654
655 if config.show_grid {
656 add_svg_grid(&mut plot, config.width, config.height)?;
657 }
658
659 let x_range = x_max - x_min;
660 let y_range = y_max - y_min;
661
662 if x_range > T::zero() && y_range > T::zero() {
663 for (&x, &y) in x_data.iter().zip(y_data.iter()) {
664 let px = ((x - x_min) / x_range * safe_usize_to_float(config.width - 100)?
665 + safe_f64_to_float::<T>(50.0)?)
666 .to_f64()
667 .unwrap_or(0.0);
668 let py = (config.height as f64 - 50.0)
669 - ((y - y_min) / y_range * safe_usize_to_float(config.height - 100)?)
670 .to_f64()
671 .unwrap_or(0.0);
672
673 writeln!(
674 &mut plot,
675 "<circle cx='{:.1}' cy='{:.1}' r='3' fill='blue' opacity='0.7'/>",
676 px, py
677 )?;
678 }
679 }
680
681 writeln!(&mut plot, "</svg>")?;
682 writeln!(&mut plot, "</div>")?;
683 }
684 ReportFormat::Markdown | ReportFormat::Text => {
685 return plot_profile(x_data, y_data, config);
687 }
688 }
689
690 Ok(plot)
691}
692
693fn add_svg_grid(plot: &mut String, width: usize, height: usize) -> std::fmt::Result {
695 let grid_lines = 10;
696 let x_step = width as f64 / grid_lines as f64;
697 let y_step = height as f64 / grid_lines as f64;
698
699 for i in 0..=grid_lines {
701 let x = i as f64 * x_step;
702 writeln!(
703 plot,
704 "<line x1='{}' y1='0' x2='{}' y2='{}' stroke='#ddd' stroke-width='1'/>",
705 x, x, height
706 )?;
707 }
708
709 for i in 0..=grid_lines {
711 let y = i as f64 * y_step;
712 writeln!(
713 plot,
714 "<line x1='0' y1='{}' x2='{}' y2='{}' stroke='#ddd' stroke-width='1'/>",
715 y, width, y
716 )?;
717 }
718
719 Ok(())
720}
721
722fn add_svg_arrowhead(
724 plot: &mut String,
725 start_x: f64,
726 start_y: f64,
727 end_x: f64,
728 end_y: f64,
729) -> std::fmt::Result {
730 let arrow_len = 3.0;
731 let dx = end_x - start_x;
732 let dy = end_y - start_y;
733 let angle = dy.atan2(dx);
734
735 let arrow1_x = end_x - arrow_len * (angle - 0.5).cos();
736 let arrow1_y = end_y - arrow_len * (angle - 0.5).sin();
737 let arrow2_x = end_x - arrow_len * (angle + 0.5).cos();
738 let arrow2_y = end_y - arrow_len * (angle + 0.5).sin();
739
740 writeln!(
741 plot,
742 "<polygon points='{:.1},{:.1} {:.1},{:.1} {:.1},{:.1}' fill='red'/>",
743 end_x, end_y, arrow1_x, arrow1_y, arrow2_x, arrow2_y
744 )
745}
746
747fn add_ascii_surface<T>(
749 plot: &mut String,
750 data: &ArrayView2<T>,
751 ascii_height: usize,
752 ascii_width: usize,
753) -> std::fmt::Result
754where
755 T: Float + FromPrimitive + ToPrimitive + Debug + Clone,
756{
757 let (height, width) = data.dim();
758 let min_val = data.iter().cloned().fold(T::infinity(), T::min);
759 let max_val = data.iter().cloned().fold(T::neg_infinity(), T::max);
760
761 for i in 0..ascii_height {
762 for j in 0..ascii_width {
763 let data_i = (i * height) / ascii_height.max(1);
764 let data_j = (j * width) / ascii_width.max(1);
765 let value = data[[data_i, data_j]];
766 let normalized = if max_val > min_val {
767 ((value - min_val) / (max_val - min_val))
768 .to_f64()
769 .unwrap_or(0.0)
770 } else {
771 0.5
772 };
773
774 let char = match (normalized * 10.0) as u32 {
775 0..=1 => ' ',
776 2..=3 => '.',
777 4..=5 => ':',
778 6..=7 => '+',
779 8..=9 => '*',
780 _ => '#',
781 };
782 write!(plot, "{}", char)?;
783 }
784 writeln!(plot)?;
785 }
786 Ok(())
787}
788
789pub fn plot_heatmap<T>(data: &ArrayView2<T>, config: &PlotConfig) -> NdimageResult<String>
791where
792 T: Float + FromPrimitive + ToPrimitive + Debug + Clone,
793{
794 if data.is_empty() {
795 return Err(NdimageError::InvalidInput("Data array is empty".into()));
796 }
797
798 let (height, width) = data.dim();
799 let mut plot = String::new();
800
801 let min_val = data.iter().cloned().fold(T::infinity(), T::min);
803 let max_val = data.iter().cloned().fold(T::neg_infinity(), T::max);
804
805 if max_val <= min_val {
806 return Err(NdimageError::InvalidInput(
807 "All values in array are the same".into(),
808 ));
809 }
810
811 match config.format {
812 ReportFormat::Html => {
813 writeln!(&mut plot, "<div class='heatmap-plot'>")?;
814 writeln!(&mut plot, "<h3>{}</h3>", config.title)?;
815
816 writeln!(&mut plot, "<table style='border-collapse: collapse;'>")?;
818 let display_height = height.min(20);
819 let display_width = width.min(20);
820
821 for i in 0..display_height {
822 writeln!(&mut plot, "<tr>")?;
823 for j in 0..display_width {
824 let data_i = (i * height) / display_height;
825 let data_j = (j * width) / display_width;
826 let value = data[[data_i, data_j]];
827 let normalized = ((value - min_val) / (max_val - min_val))
828 .to_f64()
829 .unwrap_or(0.0);
830
831 let intensity = (normalized * 255.0) as u8;
832 let color = format!("rgb({}, {}, {})", intensity, intensity, intensity);
833
834 writeln!(
835 &mut plot,
836 "<td style='width: 15px; height: 15px; background-color: {}; border: 1px solid #ccc;'></td>",
837 color
838 )?;
839 }
840 writeln!(&mut plot, "</tr>")?;
841 }
842 writeln!(&mut plot, "</table>")?;
843
844 writeln!(&mut plot, "<p>Data dimensions: {}×{}</p>", height, width)?;
845 writeln!(
846 &mut plot,
847 "<p>Value range: [{:.3}, {:.3}]</p>",
848 min_val.to_f64().unwrap_or(0.0),
849 max_val.to_f64().unwrap_or(0.0)
850 )?;
851 writeln!(&mut plot, "</div>")?;
852 }
853 ReportFormat::Markdown => {
854 writeln!(&mut plot, "## {} (Heatmap)", config.title)?;
855 writeln!(&mut plot)?;
856 writeln!(&mut plot, "```")?;
857 writeln!(&mut plot, "Data dimensions: {}×{}", height, width)?;
858 writeln!(
859 &mut plot,
860 "Value range: [{:.3}, {:.3}]",
861 min_val.to_f64().unwrap_or(0.0),
862 max_val.to_f64().unwrap_or(0.0)
863 )?;
864 writeln!(&mut plot)?;
865
866 let display_height = height.min(30);
868 let display_width = width.min(60);
869
870 for i in 0..display_height {
871 for j in 0..display_width {
872 let data_i = (i * height) / display_height;
873 let data_j = (j * width) / display_width;
874 let value = data[[data_i, data_j]];
875 let normalized = ((value - min_val) / (max_val - min_val))
876 .to_f64()
877 .unwrap_or(0.0);
878
879 let char = match (normalized * 9.0) as u32 {
880 0 => ' ',
881 1 => '.',
882 2 => ':',
883 3 => '-',
884 4 => '=',
885 5 => '+',
886 6 => '*',
887 7 => '#',
888 8 => '@',
889 _ => '█',
890 };
891 write!(&mut plot, "{}", char)?;
892 }
893 writeln!(&mut plot)?;
894 }
895
896 writeln!(&mut plot, "```")?;
897 }
898 ReportFormat::Text => {
899 writeln!(&mut plot, "{} (Heatmap)", config.title)?;
900 writeln!(&mut plot, "{}", "=".repeat(config.title.len() + 10))?;
901 writeln!(&mut plot)?;
902 writeln!(&mut plot, "Data dimensions: {}×{}", height, width)?;
903 writeln!(
904 &mut plot,
905 "Value range: [{:.3}, {:.3}]",
906 min_val.to_f64().unwrap_or(0.0),
907 max_val.to_f64().unwrap_or(0.0)
908 )?;
909 writeln!(&mut plot)?;
910
911 let display_height = height.min(20);
913 let display_width = width.min(40);
914
915 for i in 0..display_height {
916 for j in 0..display_width {
917 let data_i = (i * height) / display_height;
918 let data_j = (j * width) / display_width;
919 let value = data[[data_i, data_j]];
920 let normalized = ((value - min_val) / (max_val - min_val))
921 .to_f64()
922 .unwrap_or(0.0);
923
924 let char = match (normalized * 4.0) as u32 {
925 0 => ' ',
926 1 => '.',
927 2 => 'o',
928 3 => 'O',
929 _ => '#',
930 };
931 write!(&mut plot, "{}", char)?;
932 }
933 writeln!(&mut plot)?;
934 }
935 }
936 }
937
938 Ok(plot)
939}
940
941pub fn plot_gradient<T>(data: &ArrayView2<T>, config: &PlotConfig) -> NdimageResult<String>
943where
944 T: Float + FromPrimitive + ToPrimitive + Debug + Clone,
945{
946 plot_heatmap(data, config)
949}
950
951#[cfg(test)]
952mod tests {
953 use super::*;
954 use crate::visualization::types::{ColorMap, PlotConfig, ReportFormat};
955 use scirs2_core::ndarray::Array1;
956
957 #[test]
958 fn test_plot_histogram() {
959 let data = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
960 let config = PlotConfig {
961 title: "Test Histogram".to_string(),
962 format: ReportFormat::Text,
963 num_bins: 5,
964 ..Default::default()
965 };
966
967 let result = plot_histogram(&data.view(), &config);
968 assert!(result.is_ok());
969
970 let plot_str = result.unwrap();
971 assert!(plot_str.contains("Test Histogram"));
972 }
973
974 #[test]
975 fn test_plot_profile() {
976 let x_data = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
977 let y_data = Array1::from_vec(vec![2.0, 4.0, 6.0, 8.0, 10.0]);
978 let config = PlotConfig {
979 title: "Test Profile".to_string(),
980 format: ReportFormat::Text,
981 ..Default::default()
982 };
983
984 let result = plot_profile(&x_data.view(), &y_data.view(), &config);
985 assert!(result.is_ok());
986
987 let plot_str = result.unwrap();
988 assert!(plot_str.contains("Test Profile"));
989 }
990
991 #[test]
992 fn test_plot_surface() {
993 let data = scirs2_core::ndarray::Array2::from_shape_fn((10, 10), |(i, j)| (i + j) as f64);
994 let config = PlotConfig {
995 title: "Test Surface".to_string(),
996 format: ReportFormat::Text,
997 ..Default::default()
998 };
999
1000 let result = plot_surface(&data.view(), &config);
1001 assert!(result.is_ok());
1002
1003 let plot_str = result.unwrap();
1004 assert!(plot_str.contains("Test Surface"));
1005 assert!(plot_str.contains("Data dimensions"));
1006 }
1007
1008 #[test]
1009 fn test_plot_scatter() {
1010 let x_data = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
1011 let y_data = Array1::from_vec(vec![2.0, 4.0, 6.0, 8.0, 10.0]);
1012 let config = PlotConfig {
1013 title: "Test Scatter".to_string(),
1014 format: ReportFormat::Html,
1015 ..Default::default()
1016 };
1017
1018 let result = plot_scatter(&x_data.view(), &y_data.view(), &config);
1019 assert!(result.is_ok());
1020
1021 let plot_str = result.unwrap();
1022 assert!(plot_str.contains("Test Scatter"));
1023 assert!(plot_str.contains("<svg"));
1024 }
1025
1026 #[test]
1027 fn test_empty_data_error() {
1028 let empty_data = Array1::<f64>::from_vec(vec![]);
1029 let config = PlotConfig::default();
1030
1031 let result = plot_histogram(&empty_data.view(), &config);
1032 assert!(result.is_err());
1033 }
1034
1035 #[test]
1036 fn test_mismatched_data_error() {
1037 let x_data = Array1::from_vec(vec![1.0, 2.0, 3.0]);
1038 let y_data = Array1::from_vec(vec![1.0, 2.0]);
1039 let config = PlotConfig::default();
1040
1041 let result = plot_profile(&x_data.view(), &y_data.view(), &config);
1042 assert!(result.is_err());
1043 }
1044}