Skip to main content

sklears_svm/
visualization.rs

1//! Support Vector Machine Visualization Tools
2//!
3//! This module provides comprehensive visualization capabilities for SVM models including:
4//! - Support vector plotting and highlighting
5//! - Decision boundary visualization in 2D and 3D
6//! - Margin visualization
7//! - Kernel matrix heatmaps
8//! - Training convergence plots
9//! - Feature importance visualization for linear SVMs
10//! - Multi-class decision regions
11//! - Probability contours for calibrated models
12
13use crate::errors::{SVMError, SVMResult};
14use scirs2_core::ndarray::{Array1, Array2};
15
16/// Type alias for margin boundaries (positive, negative)
17pub type MarginBoundaries = (Vec<(f64, f64)>, Vec<(f64, f64)>);
18
19/// Color palette for visualizations
20#[derive(Debug, Clone)]
21pub enum ColorPalette {
22    /// Default matplotlib-like colors
23    Default,
24    /// High contrast colors for accessibility
25    HighContrast,
26    /// Colorblind-friendly palette
27    ColorblindFriendly,
28    /// Grayscale palette
29    Grayscale,
30    /// Custom color specifications
31    Custom(Vec<String>),
32}
33
34impl ColorPalette {
35    /// Get color for a given class index
36    pub fn get_color(&self, class_index: usize) -> String {
37        match self {
38            ColorPalette::Default => {
39                let colors = [
40                    "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2",
41                    "#7f7f7f", "#bcbd22", "#17becf",
42                ];
43                colors[class_index % colors.len()].to_string()
44            }
45            ColorPalette::HighContrast => {
46                let colors = ["#000000", "#FFFFFF", "#FF0000", "#00FF00", "#0000FF"];
47                colors[class_index % colors.len()].to_string()
48            }
49            ColorPalette::ColorblindFriendly => {
50                let colors = [
51                    "#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7",
52                    "#000000",
53                ];
54                colors[class_index % colors.len()].to_string()
55            }
56            ColorPalette::Grayscale => {
57                let intensity = (class_index * 40) % 200 + 40;
58                format!("#{:02x}{:02x}{:02x}", intensity, intensity, intensity)
59            }
60            ColorPalette::Custom(colors) => colors[class_index % colors.len()].clone(),
61        }
62    }
63}
64
65/// Configuration for SVM visualizations
66#[derive(Debug, Clone)]
67pub struct VisualizationConfig {
68    /// Color palette to use
69    pub color_palette: ColorPalette,
70    /// Figure size (width, height)
71    pub figure_size: (usize, usize),
72    /// Resolution for contour plots
73    pub resolution: usize,
74    /// Show support vectors
75    pub show_support_vectors: bool,
76    /// Show decision boundary
77    pub show_decision_boundary: bool,
78    /// Show margins
79    pub show_margins: bool,
80    /// Support vector marker size
81    pub support_vector_size: f64,
82    /// Regular point marker size
83    pub point_size: f64,
84    /// Line width for boundaries
85    pub line_width: f64,
86    /// Transparency level (0.0 to 1.0)
87    pub alpha: f64,
88    /// Title for the plot
89    pub title: Option<String>,
90    /// Labels for axes
91    pub axis_labels: Option<(String, String)>,
92    /// Grid visibility
93    pub show_grid: bool,
94}
95
96impl Default for VisualizationConfig {
97    fn default() -> Self {
98        Self {
99            color_palette: ColorPalette::Default,
100            figure_size: (800, 600),
101            resolution: 100,
102            show_support_vectors: true,
103            show_decision_boundary: true,
104            show_margins: true,
105            support_vector_size: 8.0,
106            point_size: 4.0,
107            line_width: 2.0,
108            alpha: 0.7,
109            title: None,
110            axis_labels: None,
111            show_grid: true,
112        }
113    }
114}
115
116/// Point data for visualization
117#[derive(Debug, Clone)]
118pub struct PlotPoint {
119    pub x: f64,
120    pub y: f64,
121    pub class: i32,
122    pub is_support_vector: bool,
123    pub alpha_value: Option<f64>, // Lagrange multiplier
124    pub margin_type: MarginType,
125}
126
127/// Type of margin for a point
128#[derive(Debug, Clone, PartialEq)]
129pub enum MarginType {
130    /// Point is outside the margin (correctly classified)
131    Outside,
132    /// Point is on the margin boundary
133    OnMargin,
134    /// Point is inside the margin (margin violation)
135    Inside,
136    /// Point is misclassified
137    Misclassified,
138}
139
140/// 2D visualization data structure
141#[derive(Debug, Clone)]
142pub struct Plot2D {
143    pub points: Vec<PlotPoint>,
144    pub decision_boundary: Option<Vec<(f64, f64)>>,
145    pub margin_boundaries: Option<MarginBoundaries>, // positive, negative margins
146    pub config: VisualizationConfig,
147    pub x_range: (f64, f64),
148    pub y_range: (f64, f64),
149}
150
151impl Plot2D {
152    /// Create a new 2D plot
153    pub fn new(config: VisualizationConfig) -> Self {
154        Self {
155            points: Vec::new(),
156            decision_boundary: None,
157            margin_boundaries: None,
158            config,
159            x_range: (0.0, 1.0),
160            y_range: (0.0, 1.0),
161        }
162    }
163
164    /// Add points to the plot
165    pub fn add_points(&mut self, x: &Array2<f64>, y: &Array1<f64>, support_vectors: &[usize]) {
166        if x.ncols() < 2 {
167            return; // Need at least 2 dimensions for 2D plot
168        }
169
170        self.points.clear();
171
172        for (i, (&x1, &x2)) in x.column(0).iter().zip(x.column(1).iter()).enumerate() {
173            let is_sv = support_vectors.contains(&i);
174
175            self.points.push(PlotPoint {
176                x: x1,
177                y: x2,
178                class: y[i] as i32,
179                is_support_vector: is_sv,
180                alpha_value: None,
181                margin_type: MarginType::Outside, // Will be calculated later
182            });
183        }
184
185        // Update plot ranges
186        let x_values: Vec<f64> = self.points.iter().map(|p| p.x).collect();
187        let y_values: Vec<f64> = self.points.iter().map(|p| p.y).collect();
188
189        if !x_values.is_empty() {
190            let x_min = x_values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
191            let x_max = x_values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
192            let y_min = y_values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
193            let y_max = y_values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
194
195            let x_range = x_max - x_min;
196            let y_range = y_max - y_min;
197            let margin = 0.1;
198
199            self.x_range = (x_min - margin * x_range, x_max + margin * x_range);
200            self.y_range = (y_min - margin * y_range, y_max + margin * y_range);
201        }
202    }
203
204    /// Generate decision boundary points
205    pub fn generate_decision_boundary<F>(&mut self, decision_function: F)
206    where
207        F: Fn(f64, f64) -> f64,
208    {
209        let resolution = self.config.resolution;
210        let x_step = (self.x_range.1 - self.x_range.0) / resolution as f64;
211        let y_step = (self.y_range.1 - self.y_range.0) / resolution as f64;
212
213        let mut boundary_points = Vec::new();
214
215        // Find decision boundary using contour-following algorithm
216        for i in 0..resolution {
217            for j in 0..resolution {
218                let x = self.x_range.0 + i as f64 * x_step;
219                let y = self.y_range.0 + j as f64 * y_step;
220
221                let value = decision_function(x, y);
222
223                // Check for sign changes (decision boundary crossings)
224                if value.abs() < 0.1 {
225                    // Close to decision boundary
226                    boundary_points.push((x, y));
227                }
228            }
229        }
230
231        if !boundary_points.is_empty() {
232            self.decision_boundary = Some(boundary_points);
233        }
234    }
235
236    /// Generate margin boundaries
237    pub fn generate_margin_boundaries<F>(&mut self, decision_function: F)
238    where
239        F: Fn(f64, f64) -> f64,
240    {
241        let resolution = self.config.resolution;
242        let x_step = (self.x_range.1 - self.x_range.0) / resolution as f64;
243        let y_step = (self.y_range.1 - self.y_range.0) / resolution as f64;
244
245        let mut positive_margin = Vec::new();
246        let mut negative_margin = Vec::new();
247
248        for i in 0..resolution {
249            for j in 0..resolution {
250                let x = self.x_range.0 + i as f64 * x_step;
251                let y = self.y_range.0 + j as f64 * y_step;
252
253                let value = decision_function(x, y);
254
255                // Positive margin boundary (value ≈ +1)
256                if (value - 1.0).abs() < 0.1 {
257                    positive_margin.push((x, y));
258                }
259                // Negative margin boundary (value ≈ -1)
260                else if (value + 1.0).abs() < 0.1 {
261                    negative_margin.push((x, y));
262                }
263            }
264        }
265
266        if !positive_margin.is_empty() || !negative_margin.is_empty() {
267            self.margin_boundaries = Some((positive_margin, negative_margin));
268        }
269    }
270
271    /// Generate SVG output for the plot
272    pub fn to_svg(&self) -> String {
273        let mut svg = format!(
274            r#"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg">
275<style>
276    .point {{ stroke: #000; stroke-width: 0.5; }}
277    .support-vector {{ stroke: #000; stroke-width: 2; }}
278    .decision-boundary {{ fill: none; stroke: #ff0000; stroke-width: {}; }}
279    .positive-margin {{ fill: none; stroke: #00ff00; stroke-width: 1; stroke-dasharray: 5,5; }}
280    .negative-margin {{ fill: none; stroke: #0000ff; stroke-width: 1; stroke-dasharray: 5,5; }}
281    .grid {{ stroke: #ddd; stroke-width: 0.5; }}
282</style>
283"#,
284            self.config.figure_size.0, self.config.figure_size.1, self.config.line_width
285        );
286
287        // Add title
288        if let Some(ref title) = self.config.title {
289            svg.push_str(&format!(
290                r#"<text x="{}" y="20" text-anchor="middle" font-family="Arial" font-size="16">{}</text>"#,
291                self.config.figure_size.0 / 2,
292                title
293            ));
294        }
295
296        // Add grid if requested
297        if self.config.show_grid {
298            self.add_grid_to_svg(&mut svg);
299        }
300
301        // Transform coordinates to SVG space
302        let x_scale = (self.config.figure_size.0 - 80) as f64 / (self.x_range.1 - self.x_range.0);
303        let y_scale = (self.config.figure_size.1 - 80) as f64 / (self.y_range.1 - self.y_range.0);
304
305        let transform_x = |x: f64| ((x - self.x_range.0) * x_scale + 40.0) as i32;
306        let transform_y = |y: f64| {
307            (self.config.figure_size.1 as f64 - (y - self.y_range.0) * y_scale - 40.0) as i32
308        };
309
310        // Draw margin boundaries
311        if self.config.show_margins {
312            if let Some((ref pos_margin, ref neg_margin)) = self.margin_boundaries {
313                self.add_margin_boundaries_to_svg(
314                    &mut svg,
315                    pos_margin,
316                    neg_margin,
317                    transform_x,
318                    transform_y,
319                );
320            }
321        }
322
323        // Draw decision boundary
324        if self.config.show_decision_boundary {
325            if let Some(ref boundary) = self.decision_boundary {
326                self.add_decision_boundary_to_svg(&mut svg, boundary, transform_x, transform_y);
327            }
328        }
329
330        // Draw points
331        self.add_points_to_svg(&mut svg, transform_x, transform_y);
332
333        // Add axes labels
334        if let Some((ref x_label, ref y_label)) = self.config.axis_labels {
335            svg.push_str(&format!(
336                r#"<text x="{}" y="{}" text-anchor="middle" font-family="Arial" font-size="12">{}</text>"#,
337                self.config.figure_size.0 / 2,
338                self.config.figure_size.1 - 10,
339                x_label
340            ));
341            svg.push_str(&format!(
342                r#"<text x="15" y="{}" text-anchor="middle" transform="rotate(-90 15 {})" font-family="Arial" font-size="12">{}</text>"#,
343                self.config.figure_size.1 / 2,
344                self.config.figure_size.1 / 2,
345                y_label
346            ));
347        }
348
349        svg.push_str("</svg>");
350        svg
351    }
352
353    fn add_grid_to_svg(&self, svg: &mut String) {
354        // Add simple grid lines (implementation depends on requirements)
355        let grid_lines = 10;
356        let width = self.config.figure_size.0 - 80;
357        let height = self.config.figure_size.1 - 80;
358
359        for i in 1..grid_lines {
360            let x = 40 + (width * i) / grid_lines;
361            let y = 40 + (height * i) / grid_lines;
362
363            svg.push_str(&format!(
364                r#"<line x1="{}" y1="40" x2="{}" y2="{}" class="grid"/>"#,
365                x,
366                x,
367                height + 40
368            ));
369            svg.push_str(&format!(
370                r#"<line x1="40" y1="{}" x2="{}" y2="{}" class="grid"/>"#,
371                y,
372                width + 40,
373                y
374            ));
375        }
376    }
377
378    fn add_points_to_svg<F1, F2>(&self, svg: &mut String, transform_x: F1, transform_y: F2)
379    where
380        F1: Fn(f64) -> i32,
381        F2: Fn(f64) -> i32,
382    {
383        for point in &self.points {
384            let x = transform_x(point.x);
385            let y = transform_y(point.y);
386            let color = self.config.color_palette.get_color(point.class as usize);
387            let size = if point.is_support_vector {
388                self.config.support_vector_size
389            } else {
390                self.config.point_size
391            };
392            let class_name = if point.is_support_vector {
393                "support-vector"
394            } else {
395                "point"
396            };
397
398            svg.push_str(&format!(
399                r#"<circle cx="{}" cy="{}" r="{}" fill="{}" opacity="{}" class="{}"/>"#,
400                x, y, size, color, self.config.alpha, class_name
401            ));
402        }
403    }
404
405    fn add_decision_boundary_to_svg<F1, F2>(
406        &self,
407        svg: &mut String,
408        boundary: &[(f64, f64)],
409        transform_x: F1,
410        transform_y: F2,
411    ) where
412        F1: Fn(f64) -> i32,
413        F2: Fn(f64) -> i32,
414    {
415        if boundary.is_empty() {
416            return;
417        }
418
419        svg.push_str(r#"<path d=""#);
420        for (i, (x, y)) in boundary.iter().enumerate() {
421            let svg_x = transform_x(*x);
422            let svg_y = transform_y(*y);
423
424            if i == 0 {
425                svg.push_str(&format!("M {} {}", svg_x, svg_y));
426            } else {
427                svg.push_str(&format!(" L {} {}", svg_x, svg_y));
428            }
429        }
430        svg.push_str(r#"" class="decision-boundary"/>"#);
431    }
432
433    fn add_margin_boundaries_to_svg<F1, F2>(
434        &self,
435        svg: &mut String,
436        pos_margin: &[(f64, f64)],
437        neg_margin: &[(f64, f64)],
438        transform_x: F1,
439        transform_y: F2,
440    ) where
441        F1: Fn(f64) -> i32 + Copy,
442        F2: Fn(f64) -> i32 + Copy,
443    {
444        // Draw positive margin
445        if !pos_margin.is_empty() {
446            svg.push_str(r#"<path d=""#);
447            for (i, (x, y)) in pos_margin.iter().enumerate() {
448                let svg_x = transform_x(*x);
449                let svg_y = transform_y(*y);
450
451                if i == 0 {
452                    svg.push_str(&format!("M {} {}", svg_x, svg_y));
453                } else {
454                    svg.push_str(&format!(" L {} {}", svg_x, svg_y));
455                }
456            }
457            svg.push_str(r#"" class="positive-margin"/>"#);
458        }
459
460        // Draw negative margin
461        if !neg_margin.is_empty() {
462            svg.push_str(r#"<path d=""#);
463            for (i, (x, y)) in neg_margin.iter().enumerate() {
464                let svg_x = transform_x(*x);
465                let svg_y = transform_y(*y);
466
467                if i == 0 {
468                    svg.push_str(&format!("M {} {}", svg_x, svg_y));
469                } else {
470                    svg.push_str(&format!(" L {} {}", svg_x, svg_y));
471                }
472            }
473            svg.push_str(r#"" class="negative-margin"/>"#);
474        }
475    }
476
477    /// Export plot data to JSON format for web visualization
478    #[cfg(feature = "visualization")]
479    pub fn to_json(&self) -> serde_json::Value {
480        use serde_json::json;
481
482        let points: Vec<serde_json::Value> = self
483            .points
484            .iter()
485            .map(|p| {
486                json!({
487                    "x": p.x,
488                    "y": p.y,
489                    "class": p.class,
490                    "is_support_vector": p.is_support_vector,
491                    "alpha_value": p.alpha_value,
492                    "margin_type": format!("{:?}", p.margin_type)
493                })
494            })
495            .collect();
496
497        json!({
498            "points": points,
499            "decision_boundary": self.decision_boundary,
500            "margin_boundaries": self.margin_boundaries,
501            "x_range": self.x_range,
502            "y_range": self.y_range,
503            "config": {
504                "figure_size": self.config.figure_size,
505                "resolution": self.config.resolution,
506                "show_support_vectors": self.config.show_support_vectors,
507                "show_decision_boundary": self.config.show_decision_boundary,
508                "show_margins": self.config.show_margins
509            }
510        })
511    }
512}
513
514/// Kernel matrix visualization
515#[derive(Debug, Clone)]
516pub struct KernelMatrixPlot {
517    pub matrix: Array2<f64>,
518    pub labels: Option<Array1<f64>>,
519    pub config: VisualizationConfig,
520}
521
522impl KernelMatrixPlot {
523    /// Create a new kernel matrix plot
524    pub fn new(
525        matrix: Array2<f64>,
526        labels: Option<Array1<f64>>,
527        config: VisualizationConfig,
528    ) -> Self {
529        Self {
530            matrix,
531            labels,
532            config,
533        }
534    }
535
536    /// Generate heatmap visualization of the kernel matrix
537    pub fn to_heatmap_svg(&self) -> String {
538        let (rows, cols) = self.matrix.dim();
539        let cell_size = 4; // pixels per matrix cell
540        let width = cols * cell_size;
541        let height = rows * cell_size;
542
543        let mut svg = format!(
544            r#"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg">"#,
545            width + 100,
546            height + 100
547        );
548
549        // Add title
550        if let Some(ref title) = self.config.title {
551            svg.push_str(&format!(
552                r#"<text x="{}" y="20" text-anchor="middle" font-family="Arial" font-size="14">{}</text>"#,
553                (width + 100) / 2,
554                title
555            ));
556        }
557
558        // Find matrix value range for color mapping
559        let min_val = self.matrix.iter().fold(f64::INFINITY, |a, &b| a.min(b));
560        let max_val = self.matrix.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
561        let range = max_val - min_val;
562
563        // Draw heatmap cells
564        for i in 0..rows {
565            for j in 0..cols {
566                let value = self.matrix[[i, j]];
567                let normalized = if range > 0.0 {
568                    ((value - min_val) / range).clamp(0.0, 1.0)
569                } else {
570                    0.5
571                };
572
573                // Color mapping (blue to red)
574                let red = (255.0 * normalized) as u8;
575                let blue = (255.0 * (1.0 - normalized)) as u8;
576                let color = format!("#{:02x}00{:02x}", red, blue);
577
578                let x = 50 + j * cell_size;
579                let y = 50 + i * cell_size;
580
581                svg.push_str(&format!(
582                    r#"<rect x="{}" y="{}" width="{}" height="{}" fill="{}"/>"#,
583                    x, y, cell_size, cell_size, color
584                ));
585            }
586        }
587
588        // Add color bar legend
589        self.add_colorbar_to_svg(&mut svg, width + 70, 50, 20, height, min_val, max_val);
590
591        svg.push_str("</svg>");
592        svg
593    }
594
595    #[allow(clippy::too_many_arguments)]
596    fn add_colorbar_to_svg(
597        &self,
598        svg: &mut String,
599        x: usize,
600        y: usize,
601        width: usize,
602        height: usize,
603        min_val: f64,
604        max_val: f64,
605    ) {
606        let steps = 50;
607        let step_height = height / steps;
608
609        for i in 0..steps {
610            let normalized = i as f64 / (steps - 1) as f64;
611            let red = (255.0 * normalized) as u8;
612            let blue = (255.0 * (1.0 - normalized)) as u8;
613            let color = format!("#{:02x}00{:02x}", red, blue);
614
615            svg.push_str(&format!(
616                r#"<rect x="{}" y="{}" width="{}" height="{}" fill="{}"/>"#,
617                x,
618                y + (steps - 1 - i) * step_height,
619                width,
620                step_height,
621                color
622            ));
623        }
624
625        // Add value labels
626        svg.push_str(&format!(
627            r#"<text x="{}" y="{}" font-family="Arial" font-size="10">{:.3}</text>"#,
628            x + width + 5,
629            y + 5,
630            max_val
631        ));
632        svg.push_str(&format!(
633            r#"<text x="{}" y="{}" font-family="Arial" font-size="10">{:.3}</text>"#,
634            x + width + 5,
635            y + height,
636            min_val
637        ));
638    }
639}
640
641/// Training convergence visualization
642#[derive(Debug, Clone)]
643pub struct ConvergencePlot {
644    pub objective_values: Vec<f64>,
645    pub iteration_numbers: Vec<usize>,
646    pub tolerance: f64,
647    pub config: VisualizationConfig,
648}
649
650impl ConvergencePlot {
651    /// Create a new convergence plot
652    pub fn new(
653        objective_values: Vec<f64>,
654        iteration_numbers: Vec<usize>,
655        tolerance: f64,
656        config: VisualizationConfig,
657    ) -> Self {
658        Self {
659            objective_values,
660            iteration_numbers,
661            tolerance,
662            config,
663        }
664    }
665
666    /// Generate SVG line plot of convergence
667    pub fn to_svg(&self) -> String {
668        let width = self.config.figure_size.0;
669        let height = self.config.figure_size.1;
670        let margin = 50;
671        let plot_width = width - 2 * margin;
672        let plot_height = height - 2 * margin;
673
674        let mut svg = format!(
675            r#"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg">
676<style>
677    .axis {{ stroke: #000; stroke-width: 1; }}
678    .grid {{ stroke: #ddd; stroke-width: 0.5; }}
679    .line {{ fill: none; stroke: #1f77b4; stroke-width: 2; }}
680    .tolerance-line {{ fill: none; stroke: #ff0000; stroke-width: 1; stroke-dasharray: 5,5; }}
681</style>
682"#,
683            width, height
684        );
685
686        // Add title
687        if let Some(ref title) = self.config.title {
688            svg.push_str(&format!(
689                r#"<text x="{}" y="20" text-anchor="middle" font-family="Arial" font-size="16">{}</text>"#,
690                width / 2,
691                title
692            ));
693        }
694
695        if self.objective_values.is_empty() {
696            svg.push_str("</svg>");
697            return svg;
698        }
699
700        // Find data ranges
701        let max_iter = *self.iteration_numbers.iter().max().unwrap_or(&1);
702        let min_obj = self
703            .objective_values
704            .iter()
705            .fold(f64::INFINITY, |a, &b| a.min(b));
706        let max_obj = self
707            .objective_values
708            .iter()
709            .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
710
711        let x_scale = plot_width as f64 / max_iter as f64;
712        let y_scale = plot_height as f64 / (max_obj - min_obj).max(1e-10);
713
714        // Draw axes
715        svg.push_str(&format!(
716            r#"<line x1="{}" y1="{}" x2="{}" y2="{}" class="axis"/>"#,
717            margin,
718            margin + plot_height,
719            margin + plot_width,
720            margin + plot_height
721        ));
722        svg.push_str(&format!(
723            r#"<line x1="{}" y1="{}" x2="{}" y2="{}" class="axis"/>"#,
724            margin,
725            margin,
726            margin,
727            margin + plot_height
728        ));
729
730        // Draw convergence line
731        if self.objective_values.len() > 1 {
732            svg.push_str(r#"<path d=""#);
733            for (i, (&iter, &obj)) in self
734                .iteration_numbers
735                .iter()
736                .zip(self.objective_values.iter())
737                .enumerate()
738            {
739                let x = margin + (iter as f64 * x_scale) as usize;
740                let y = margin + plot_height - ((obj - min_obj) * y_scale) as usize;
741
742                if i == 0 {
743                    svg.push_str(&format!("M {} {}", x, y));
744                } else {
745                    svg.push_str(&format!(" L {} {}", x, y));
746                }
747            }
748            svg.push_str(r#"" class="line"/>"#);
749        }
750
751        // Draw tolerance line
752        let tolerance_y = margin + plot_height - ((self.tolerance - min_obj) * y_scale) as usize;
753        svg.push_str(&format!(
754            r#"<line x1="{}" y1="{}" x2="{}" y2="{}" class="tolerance-line"/>"#,
755            margin,
756            tolerance_y,
757            margin + plot_width,
758            tolerance_y
759        ));
760
761        // Add labels
762        svg.push_str(&format!(
763            r#"<text x="{}" y="{}" text-anchor="middle" font-family="Arial" font-size="12">Iteration</text>"#,
764            margin + plot_width / 2,
765            height - 10
766        ));
767        svg.push_str(&format!(
768            r#"<text x="15" y="{}" text-anchor="middle" transform="rotate(-90 15 {})" font-family="Arial" font-size="12">Objective Value</text>"#,
769            margin + plot_height / 2,
770            margin + plot_height / 2
771        ));
772
773        svg.push_str("</svg>");
774        svg
775    }
776}
777
778/// Comprehensive SVM visualizer
779pub struct SVMVisualizer {
780    config: VisualizationConfig,
781}
782
783impl Default for SVMVisualizer {
784    fn default() -> Self {
785        Self::new(VisualizationConfig::default())
786    }
787}
788
789impl SVMVisualizer {
790    /// Create a new SVM visualizer
791    pub fn new(config: VisualizationConfig) -> Self {
792        Self { config }
793    }
794
795    /// Visualize 2D SVM classification
796    pub fn plot_2d_classification<F>(
797        &self,
798        x: &Array2<f64>,
799        y: &Array1<f64>,
800        support_vectors: &[usize],
801        decision_function: F,
802    ) -> SVMResult<Plot2D>
803    where
804        F: Fn(f64, f64) -> f64,
805    {
806        if x.ncols() < 2 {
807            return Err(SVMError::invalid_input(
808                "Need at least 2 features for 2D visualization",
809            ));
810        }
811
812        let mut plot = Plot2D::new(self.config.clone());
813        plot.add_points(x, y, support_vectors);
814
815        if self.config.show_decision_boundary {
816            plot.generate_decision_boundary(&decision_function);
817        }
818
819        if self.config.show_margins {
820            plot.generate_margin_boundaries(&decision_function);
821        }
822
823        Ok(plot)
824    }
825
826    /// Visualize kernel matrix
827    pub fn plot_kernel_matrix(
828        &self,
829        kernel_matrix: &Array2<f64>,
830        labels: Option<&Array1<f64>>,
831    ) -> KernelMatrixPlot {
832        KernelMatrixPlot::new(kernel_matrix.clone(), labels.cloned(), self.config.clone())
833    }
834
835    /// Visualize training convergence
836    pub fn plot_convergence(
837        &self,
838        objective_values: Vec<f64>,
839        iteration_numbers: Vec<usize>,
840        tolerance: f64,
841    ) -> ConvergencePlot {
842        ConvergencePlot::new(
843            objective_values,
844            iteration_numbers,
845            tolerance,
846            self.config.clone(),
847        )
848    }
849
850    /// Create interactive HTML visualization
851    pub fn create_interactive_html(&self, _plot_data: &Plot2D, include_controls: bool) -> String {
852        #[cfg(feature = "visualization")]
853        let json_data = _plot_data.to_json();
854        #[cfg(not(feature = "visualization"))]
855        let json_data = "{}".to_string();
856
857        let controls = if include_controls {
858            r#"
859            <div id="controls">
860                <label><input type="checkbox" id="show-sv" checked> Show Support Vectors</label>
861                <label><input type="checkbox" id="show-boundary" checked> Show Decision Boundary</label>
862                <label><input type="checkbox" id="show-margins" checked> Show Margins</label>
863            </div>
864            "#
865        } else {
866            ""
867        };
868
869        format!(
870            r#"<!DOCTYPE html>
871<html>
872<head>
873    <title>SVM Visualization</title>
874    <script src="https://d3js.org/d3.v7.min.js"></script>
875    <style>
876        body {{ font-family: Arial, sans-serif; }}
877        #controls {{ margin: 10px; }}
878        #controls label {{ margin-right: 15px; }}
879        .point {{ stroke: #000; stroke-width: 0.5; }}
880        .support-vector {{ stroke: #000; stroke-width: 2; }}
881    </style>
882</head>
883<body>
884    <h1>SVM Visualization</h1>
885    {}
886    <div id="plot"></div>
887
888    <script>
889        const data = {};
890
891        // D3.js visualization code would go here
892        // This is a placeholder for the interactive visualization
893        const svg = d3.select('#plot')
894            .append('svg')
895            .attr('width', 800)
896            .attr('height', 600);
897
898        // Add visualization implementation
899        console.log('Plot data:', data);
900    </script>
901</body>
902</html>"#,
903            controls, json_data
904        )
905    }
906}
907
908#[allow(non_snake_case)]
909#[cfg(test)]
910mod tests {
911    use super::*;
912    use approx::assert_abs_diff_eq;
913
914    #[test]
915    fn test_color_palette() {
916        let palette = ColorPalette::Default;
917        let color0 = palette.get_color(0);
918        let color1 = palette.get_color(1);
919
920        assert_ne!(color0, color1);
921        assert!(color0.starts_with('#'));
922    }
923
924    #[test]
925    fn test_plot_creation() {
926        let config = VisualizationConfig::default();
927        let plot = Plot2D::new(config);
928
929        assert!(plot.points.is_empty());
930        assert_eq!(plot.x_range, (0.0, 1.0));
931        assert_eq!(plot.y_range, (0.0, 1.0));
932    }
933
934    #[test]
935    fn test_add_points() {
936        let config = VisualizationConfig::default();
937        let mut plot = Plot2D::new(config);
938
939        let x = Array2::from_shape_vec((3, 2), vec![0.0, 0.0, 1.0, 1.0, 0.5, 0.5])
940            .expect("array shape mismatch");
941        let y = Array1::from_vec(vec![-1.0, 1.0, 1.0]);
942        let support_vectors = vec![0, 2];
943
944        plot.add_points(&x, &y, &support_vectors);
945
946        assert_eq!(plot.points.len(), 3);
947        assert!(plot.points[0].is_support_vector);
948        assert!(!plot.points[1].is_support_vector);
949        assert!(plot.points[2].is_support_vector);
950    }
951
952    #[test]
953    fn test_visualizer_creation() {
954        let visualizer = SVMVisualizer::default();
955        assert_eq!(visualizer.config.resolution, 100);
956    }
957
958    #[test]
959    fn test_convergence_plot() {
960        let config = VisualizationConfig::default();
961        let objectives = vec![10.0, 5.0, 2.0, 1.0, 0.5];
962        let iterations = vec![0, 1, 2, 3, 4];
963
964        let plot = ConvergencePlot::new(objectives, iterations, 0.1, config);
965
966        assert_eq!(plot.objective_values.len(), 5);
967        assert_abs_diff_eq!(plot.tolerance, 0.1);
968    }
969
970    #[test]
971    fn test_svg_generation() {
972        let config = VisualizationConfig::default();
973        let plot = Plot2D::new(config);
974
975        let svg = plot.to_svg();
976        assert!(svg.contains("<svg"));
977        assert!(svg.contains("</svg>"));
978    }
979}