scirs2_ndimage/visualization/
plotting.rs

1//! Plotting and Visualization Functions
2//!
3//! This module provides comprehensive plotting functionality for creating
4//! various types of visualizations including histograms, line plots, surface plots,
5//! contour plots, and gradient vector field visualizations.
6
7use 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
15/// Generate a histogram plot representation
16pub 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    // Find min and max values
25    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    // Create histogram bins
35    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    // Generate plot representation
46    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
128/// Generate a profile plot (line plot) representation
129pub 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            // Plot data points and lines
160            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                // Add grid if requested
195                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
246/// Generate a 3D surface plot representation of a 2D array
247pub 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    // Find min and max values for scaling
259    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            // Create a simplified 3D representation using CSS transforms
275            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; // Scale to percentage
285
286                    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                    // Color based on height
290                    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            // Simple ASCII art representation
327            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 representation
367            add_ascii_surface(&mut plot, data, 20, 60)?;
368        }
369    }
370
371    Ok(plot)
372}
373
374/// Generate a contour plot representation of a 2D array
375pub 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    // Find min and max values for level calculation
391    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    // Calculate contour levels
401    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            // Simple contour approximation by drawing level sets
419            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                // Find points close to this level
429                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)?; // 2% tolerance
433
434                        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
496/// Visualize gradient information as a vector field
497pub 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            // Sample gradient vectors at regular intervals
525            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 arrowhead
548                        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            // Compute some statistics
564            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            // Show a text-based representation
589            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
618/// Create a scatter plot of two data arrays
619pub 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    // Find data ranges
640    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            // Use the same logic as profile plot for text formats
686            return plot_profile(x_data, y_data, config);
687        }
688    }
689
690    Ok(plot)
691}
692
693/// Helper function to add SVG grid
694fn 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    // Vertical grid lines
700    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    // Horizontal grid lines
710    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
722/// Helper function to add SVG arrowhead
723fn 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
747/// Helper function to add ASCII surface representation
748fn 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
789/// Generate a heatmap visualization of a 2D array
790pub 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    // Find min and max values for scaling
802    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            // Create a simple HTML table representation
817            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            // Simple ASCII art heatmap
867            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            // Simple ASCII representation for text mode
912            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
941/// Alias for plot_gradient for backward compatibility
942pub fn plot_gradient<T>(data: &ArrayView2<T>, config: &PlotConfig) -> NdimageResult<String>
943where
944    T: Float + FromPrimitive + ToPrimitive + Debug + Clone,
945{
946    // For now, this is an alias to the existing gradient plotting functionality
947    // that was extracted from the original code
948    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}