1use crate::errors::{SVMError, SVMResult};
14use scirs2_core::ndarray::{Array1, Array2};
15
16pub type MarginBoundaries = (Vec<(f64, f64)>, Vec<(f64, f64)>);
18
19#[derive(Debug, Clone)]
21pub enum ColorPalette {
22 Default,
24 HighContrast,
26 ColorblindFriendly,
28 Grayscale,
30 Custom(Vec<String>),
32}
33
34impl ColorPalette {
35 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#[derive(Debug, Clone)]
67pub struct VisualizationConfig {
68 pub color_palette: ColorPalette,
70 pub figure_size: (usize, usize),
72 pub resolution: usize,
74 pub show_support_vectors: bool,
76 pub show_decision_boundary: bool,
78 pub show_margins: bool,
80 pub support_vector_size: f64,
82 pub point_size: f64,
84 pub line_width: f64,
86 pub alpha: f64,
88 pub title: Option<String>,
90 pub axis_labels: Option<(String, String)>,
92 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#[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>, pub margin_type: MarginType,
125}
126
127#[derive(Debug, Clone, PartialEq)]
129pub enum MarginType {
130 Outside,
132 OnMargin,
134 Inside,
136 Misclassified,
138}
139
140#[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>, pub config: VisualizationConfig,
147 pub x_range: (f64, f64),
148 pub y_range: (f64, f64),
149}
150
151impl Plot2D {
152 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 pub fn add_points(&mut self, x: &Array2<f64>, y: &Array1<f64>, support_vectors: &[usize]) {
166 if x.ncols() < 2 {
167 return; }
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, });
183 }
184
185 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 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 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 if value.abs() < 0.1 {
225 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 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 if (value - 1.0).abs() < 0.1 {
257 positive_margin.push((x, y));
258 }
259 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 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 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 if self.config.show_grid {
298 self.add_grid_to_svg(&mut svg);
299 }
300
301 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 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 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 self.add_points_to_svg(&mut svg, transform_x, transform_y);
332
333 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 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 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 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 #[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#[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 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 pub fn to_heatmap_svg(&self) -> String {
538 let (rows, cols) = self.matrix.dim();
539 let cell_size = 4; 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 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 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 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 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 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 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#[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 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 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 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 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 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 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 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 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
778pub 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 pub fn new(config: VisualizationConfig) -> Self {
792 Self { config }
793 }
794
795 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 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 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 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}