1#[cfg(feature = "plotting")]
8use plotters::prelude::*;
9use scirs2_core::numeric::Complex64;
10use std::error::Error;
11use std::path::Path;
12
13#[derive(Debug, Clone)]
15pub struct PlotConfig {
16 pub width: u32,
18 pub height: u32,
20 pub dpi: u32,
22 pub title: String,
24 pub x_label: String,
26 pub y_label: String,
28 pub show_grid: bool,
30 pub show_legend: bool,
32 pub color_scheme: ColorScheme,
34}
35
36impl Default for PlotConfig {
37 fn default() -> Self {
38 Self {
39 width: 800,
40 height: 600,
41 dpi: 100,
42 title: String::new(),
43 x_label: "x".to_string(),
44 y_label: "f(x)".to_string(),
45 show_grid: true,
46 show_legend: true,
47 color_scheme: ColorScheme::default(),
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
54pub enum ColorScheme {
55 Default,
56 Viridis,
57 Plasma,
58 Inferno,
59 Magma,
60 ColorBlind,
61}
62
63impl Default for ColorScheme {
64 fn default() -> Self {
65 ColorScheme::Default
66 }
67}
68
69pub trait Visualizable {
71 fn plot_2d(&self, config: &PlotConfig) -> Result<Vec<u8>, Box<dyn Error>>;
73
74 fn plot_3d(&self, config: &PlotConfig) -> Result<Vec<u8>, Box<dyn Error>>;
76
77 fn animate(&self, config: &PlotConfig) -> Result<Vec<Vec<u8>>, Box<dyn Error>>;
79}
80
81pub struct MultiPlot {
83 functions: Vec<Box<dyn Fn(f64) -> f64>>,
84 labels: Vec<String>,
85 x_range: (f64, f64),
86 config: PlotConfig,
87}
88
89impl MultiPlot {
90 pub fn new(config: PlotConfig) -> Self {
91 Self {
92 functions: Vec::new(),
93 labels: Vec::new(),
94 x_range: (-10.0, 10.0),
95 config,
96 }
97 }
98
99 pub fn add_function(mut self, f: Box<dyn Fn(f64) -> f64>, label: &str) -> Self {
100 self.functions.push(f);
101 self.labels.push(label.to_string());
102 self
103 }
104
105 pub fn set_x_range(mut self, min: f64, max: f64) -> Self {
106 self.x_range = (min, max);
107 self
108 }
109
110 #[cfg(feature = "plotting")]
111 pub fn plot<P: AsRef<Path>>(&self, path: P) -> Result<(), Box<dyn Error>> {
112 let root = BitMapBackend::new(path.as_ref(), (self.config.width, self.config.height))
113 .into_drawing_area();
114 root.fill(&WHITE)?;
115
116 let mut chart = ChartBuilder::on(&root)
117 .caption(&self.config.title, ("sans-serif", 40))
118 .margin(10)
119 .x_label_area_size(30)
120 .y_label_area_size(40)
121 .build_cartesian_2d(self.x_range.0..self.x_range.1, -2f64..2f64)?;
122
123 if self.config.show_grid {
124 chart
125 .configure_mesh()
126 .x_desc(&self.config.x_label)
127 .y_desc(&self.config.y_label)
128 .draw()?;
129 }
130
131 let colors = [&RED, &BLUE, &GREEN, &MAGENTA, &CYAN];
132
133 for (i, (f, label)) in self.functions.iter().zip(&self.labels).enumerate() {
134 let color = colors[i % colors.len()];
135 let data: Vec<(f64, f64)> = ((self.x_range.0 * 100.0) as i32
136 ..(self.x_range.1 * 100.0) as i32)
137 .map(|x| x as f64 / 100.0)
138 .map(|x| (x, f(x)))
139 .collect();
140
141 chart
142 .draw_series(LineSeries::new(data, color))?
143 .label(label)
144 .legend(move |(x, y)| PathElement::new(vec![(x, y), (x + 10, y)], color));
145 }
146
147 if self.config.show_legend {
148 chart
149 .configure_series_labels()
150 .background_style(&WHITE.mix(0.8))
151 .border_style(&BLACK)
152 .draw()?;
153 }
154
155 root.present()?;
156 Ok(())
157 }
158}
159
160pub mod gamma_plots {
162 use super::*;
163 use crate::{digamma, gamma, gammaln};
164
165 pub fn plot_gamma_family<P: AsRef<Path>>(path: P) -> Result<(), Box<dyn Error>> {
167 let config = PlotConfig {
168 title: "Gamma Function Family".to_string(),
169 x_label: "x".to_string(),
170 y_label: "f(x)".to_string(),
171 ..Default::default()
172 };
173
174 MultiPlot::new(config)
175 .add_function(Box::new(|x| gamma(x)), "Γ(x)")
176 .add_function(Box::new(|x| gammaln(x)), "ln Γ(x)")
177 .add_function(Box::new(|x| digamma(x)), "ψ(x)")
178 .set_x_range(0.1, 5.0)
179 .plot(path)
180 }
181
182 #[cfg(feature = "plotting")]
184 pub fn plot_gamma_complex<P: AsRef<Path>>(path: P) -> Result<(), Box<dyn Error>> {
185 use crate::gamma::complex::gamma_complex;
186
187 let root = BitMapBackend::new(path.as_ref(), (800, 600)).into_drawing_area();
188 root.fill(&WHITE)?;
189
190 let mut chart = ChartBuilder::on(&root)
191 .caption("Complex Gamma Function |Γ(z)|", ("sans-serif", 40))
192 .margin(10)
193 .x_label_area_size(30)
194 .y_label_area_size(40)
195 .build_cartesian_2d(-5f64..5f64, -5f64..5f64)?;
196
197 chart
198 .configure_mesh()
199 .x_desc("Re(z)")
200 .y_desc("Im(z)")
201 .draw()?;
202
203 let n = 100;
205 let mut data = vec![];
206
207 for i in 0..n {
208 for j in 0..n {
209 let x = -5.0 + 10.0 * i as f64 / n as f64;
210 let y = -5.0 + 10.0 * j as f64 / n as f64;
211 let z = Complex64::new(x, y);
212 let gamma_z = gamma_complex(z);
213 let magnitude = gamma_z.norm().ln(); data.push(Rectangle::new(
216 [(x, y), (x + 0.1, y + 0.1)],
217 HSLColor(240.0 - magnitude * 30.0, 0.7, 0.5).filled(),
218 ));
219 }
220 }
221
222 chart.draw_series(data)?;
223
224 root.present()?;
225 Ok(())
226 }
227}
228
229pub mod bessel_plots {
231 use super::*;
232 use crate::bessel::{j0, j1, jn};
233
234 pub fn plot_bessel_j<P: AsRef<Path>>(path: P) -> Result<(), Box<dyn Error>> {
236 let config = PlotConfig {
237 title: "Bessel Functions of the First Kind".to_string(),
238 ..Default::default()
239 };
240
241 MultiPlot::new(config)
242 .add_function(Box::new(|x| j0(x)), "J₀(x)")
243 .add_function(Box::new(|x| j1(x)), "J₁(x)")
244 .add_function(Box::new(|x| jn(2, x)), "J₂(x)")
245 .add_function(Box::new(|x| jn(3, x)), "J₃(x)")
246 .set_x_range(0.0, 20.0)
247 .plot(path)
248 }
249
250 pub fn plot_bessel_zeros<P: AsRef<Path>>(path: P) -> Result<(), Box<dyn Error>> {
252 use crate::bessel_zeros::j0_zeros;
253
254 #[cfg(feature = "plotting")]
255 {
256 let root = BitMapBackend::new(path.as_ref(), (800, 600)).into_drawing_area();
257 root.fill(&WHITE)?;
258
259 let mut chart = ChartBuilder::on(&root)
260 .caption("Bessel Function Zeros", ("sans-serif", 40))
261 .margin(10)
262 .x_label_area_size(30)
263 .y_label_area_size(40)
264 .build_cartesian_2d(0f64..30f64, -0.5f64..1f64)?;
265
266 chart.configure_mesh().x_desc("x").y_desc("J_n(x)").draw()?;
267
268 let j0_data: Vec<(f64, f64)> = (0..3000)
270 .map(|i| i as f64 / 100.0)
271 .map(|x| (x, j0(x)))
272 .collect();
273 chart
274 .draw_series(LineSeries::new(j0_data, &BLUE))?
275 .label("J₀(x)")
276 .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 10, y)], &BLUE));
277
278 for k in 1..=10 {
280 if let Ok(zero) = j0_zeros::<f64>(k) {
281 chart.draw_series(PointSeries::of_element(
282 vec![(zero, 0.0)],
283 5,
284 &RED,
285 &|c, s, st| {
286 return EmptyElement::at(c)
287 + Circle::new((0, 0), s, st.filled())
288 + Text::new(format!("{:.3}", zero), (10, 0), ("sans-serif", 15));
289 },
290 ))?;
291 }
292 }
293
294 chart
295 .configure_series_labels()
296 .background_style(&WHITE.mix(0.8))
297 .border_style(&BLACK)
298 .draw()?;
299
300 root.present()?;
301 }
302
303 Ok(())
304 }
305}
306
307pub mod error_function_plots {
309 use super::*;
310 use crate::{erf, erfc, erfinv};
311
312 pub fn plot_error_functions<P: AsRef<Path>>(path: P) -> Result<(), Box<dyn Error>> {
314 let config = PlotConfig {
315 title: "Error Functions".to_string(),
316 ..Default::default()
317 };
318
319 MultiPlot::new(config)
320 .add_function(Box::new(|x| erf(x)), "erf(x)")
321 .add_function(Box::new(|x| erfc(x)), "erfc(x)")
322 .add_function(
323 Box::new(|x| if x.abs() < 0.999 { erfinv(x) } else { f64::NAN }),
324 "erfinv(x)",
325 )
326 .set_x_range(-3.0, 3.0)
327 .plot(path)
328 }
329}
330
331pub mod polynomial_plots {
333 use super::*;
334 use crate::legendre;
335
336 pub fn plot_legendre<P: AsRef<Path>>(path: P, maxn: usize) -> Result<(), Box<dyn Error>> {
338 let config = PlotConfig {
339 title: format!("Legendre Polynomials P_n(x) for _n = 0..{}", maxn),
340 ..Default::default()
341 };
342
343 let mut plot = MultiPlot::new(config).set_x_range(-1.0, 1.0);
344
345 for _n in 0..=maxn {
346 plot = plot.add_function(Box::new(move |x| legendre(_n, x)), &format!("P_{}", _n));
347 }
348
349 plot.plot(path)
350 }
351
352 pub fn animate_polynomials() -> Result<Vec<Vec<u8>>, Box<dyn Error>> {
354 Ok(vec![])
357 }
358}
359
360pub mod surface_plots {
362 use super::*;
363
364 #[cfg(feature = "plotting")]
366 pub fn plot_3d_surface<P, F>(path: P, f: F, title: &str) -> Result<(), Box<dyn Error>>
367 where
368 P: AsRef<Path>,
369 F: Fn(f64, f64) -> f64,
370 {
371 let root = BitMapBackend::new(path.as_ref(), (800, 600)).into_drawing_area();
372 root.fill(&WHITE)?;
373
374 let mut chart = ChartBuilder::on(&root)
375 .caption(title, ("sans-serif", 40))
376 .margin(10)
377 .x_label_area_size(30)
378 .y_label_area_size(40)
379 .build_cartesian_3d(-5.0..5.0, -5.0..5.0, -2.0..2.0)?;
380
381 chart.configure_axes().draw()?;
382
383 let n = 50;
385 let mut data = vec![];
386
387 for i in 0..n {
388 for j in 0..n {
389 let x = -5.0 + 10.0 * i as f64 / n as f64;
390 let y = -5.0 + 10.0 * j as f64 / n as f64;
391 let z = f(x, y);
392
393 if z.is_finite() {
394 data.push((x, y, z));
395 }
396 }
397 }
398
399 let x_range: Vec<f64> = (0..51).map(|i| -5.0 + i as f64 * 0.2).collect();
401 let y_range: Vec<f64> = (0..51).map(|i| -5.0 + i as f64 * 0.2).collect();
402
403 chart.draw_series(
404 SurfaceSeries::xoz(x_range.into_iter(), y_range.into_iter(), |x, y| f(x, y))
405 .style(&BLUE.mix(0.5)),
406 )?;
407
408 root.present()?;
409 Ok(())
410 }
411}
412
413#[cfg(feature = "interactive")]
415pub mod interactive {
416 #[allow(unused_imports)]
417 use super::*;
418
419 pub struct InteractivePlotConfig {
421 pub enable_zoom: bool,
422 pub enable_pan: bool,
423 pub enable_tooltips: bool,
424 pub enable_export: bool,
425 }
426
427 pub fn create_interactive_plot<F>(
429 f: F,
430 config: InteractivePlotConfig,
431 x_range: (f64, f64),
432 function_name: &str,
433 ) -> String
434 where
435 F: Fn(f64) -> f64,
436 {
437 let n_points = 1000;
439 let step = (x_range.1 - x_range.0) / n_points as f64;
440 let mut data_points = Vec::new();
441
442 for i in 0..=n_points {
443 let x = x_range.0 + i as f64 * step;
444 let y = f(x);
445 if y.is_finite() {
446 data_points.push(format!("[{}, {}]", x, y));
447 }
448 }
449
450 let mut x_values = Vec::new();
452 let mut y_values = Vec::new();
453
454 for i in 0..=n_points {
455 let x = x_range.0 + i as f64 * step;
456 let y = f(x);
457 if y.is_finite() {
458 x_values.push(x);
459 y_values.push(y);
460 }
461 }
462
463 let x_json = format!(
464 "[{}]",
465 x_values
466 .iter()
467 .map(|x| format!("{x}"))
468 .collect::<Vec<_>>()
469 .join(", ")
470 );
471 let y_json = format!(
472 "[{}]",
473 y_values
474 .iter()
475 .map(|y| format!("{y}"))
476 .collect::<Vec<_>>()
477 .join(", ")
478 );
479
480 format!(
482 r#"
483<!DOCTYPE html>
484<html>
485<head>
486 <title>Interactive Plot - {}</title>
487 <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
488 <style>
489 body {{
490 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
491 margin: 0;
492 padding: 20px;
493 background-color: #f5f5f5;
494 }}
495 .container {{
496 max-width: 1200px;
497 margin: 0 auto;
498 background: white;
499 padding: 20px;
500 border-radius: 8px;
501 box-shadow: 0 2px 10px rgba(0,0,0,0.1);
502 }}
503 h1 {{
504 color: #333;
505 text-align: center;
506 margin-bottom: 30px;
507 }}
508 .controls {{
509 display: flex;
510 gap: 15px;
511 margin-bottom: 20px;
512 flex-wrap: wrap;
513 align-items: center;
514 }}
515 .control-group {{
516 display: flex;
517 flex-direction: column;
518 gap: 5px;
519 }}
520 label {{
521 font-weight: 600;
522 color: #555;
523 font-size: 14px;
524 }}
525 input, select, button {{
526 padding: 8px 12px;
527 border: 1px solid #ddd;
528 border-radius: 4px;
529 font-size: 14px;
530 }}
531 button {{
532 background-color: #007bff;
533 color: white;
534 border: none;
535 cursor: pointer;
536 transition: background-color 0.2s;
537 }}
538 button:hover {{
539 background-color: #0056b3;
540 }}
541 #plot {{
542 width: 100%;
543 height: 600px;
544 }}
545 .info-panel {{
546 margin-top: 20px;
547 padding: 15px;
548 background-color: #f8f9fa;
549 border-radius: 6px;
550 border-left: 4px solid #007bff;
551 }}
552 .tooltip-info {{
553 margin-top: 10px;
554 font-size: 14px;
555 color: #666;
556 }}
557 </style>
558</head>
559<body>
560 <div class="container">
561 <h1>Interactive Visualization: {}</h1>
562
563 <div class="controls">
564 <div class="control-group">
565 <label for="xMin">X Min:</label>
566 <input type="number" id="xMin" value="{}" step="0.1">
567 </div>
568 <div class="control-group">
569 <label for="xMax">X Max:</label>
570 <input type="number" id="xMax" value="{}" step="0.1">
571 </div>
572 <div class="control-group">
573 <label for="points">Points:</label>
574 <select id="points">
575 <option value="500">500</option>
576 <option value="1000" selected>1000</option>
577 <option value="2000">2000</option>
578 <option value="5000">5000</option>
579 </select>
580 </div>
581 <button onclick="updatePlot()">Update Plot</button>
582 <button onclick="resetZoom()">Reset Zoom</button>
583 <button onclick="exportData()">Export CSV</button>
584 {}
585 </div>
586
587 <div id="plot"></div>
588
589 <div class="info-panel">
590 <h3>Interactive Features:</h3>
591 <ul>
592 <li><strong>Zoom:</strong> Click and drag to zoom into a region</li>
593 <li><strong>Pan:</strong> Hold shift and drag to pan around</li>
594 <li><strong>Hover:</strong> Move mouse over the curve to see coordinates</li>
595 <li><strong>Double-click:</strong> Reset zoom to fit all data</li>
596 </ul>
597 <div class="tooltip-info" id="tooltip-info">
598 Hover over the plot to see coordinate information here.
599 </div>
600 </div>
601 </div>
602
603 <script>
604 let currentData = {};
605
606 // JavaScript implementations of special functions
607 function gamma(x) {{
608 if (x < 0) return NaN;
609 if (x === 0) return Infinity;
610 if (x === 1 || x === 2) return 1;
611
612 // Stirling's approximation for x > 1
613 if (x > 1) {{
614 return Math.sqrt(2 * Math.PI / x) * Math.pow(x / Math.E, x);
615 }}
616 return gamma(x + 1) / x;
617 }}
618
619 function besselJ0(x) {{
620 const ax = Math.abs(x);
621 if (ax < 8) {{
622 const y = x * x;
623 return ((-0.0000000000000000015 * y + 0.000000000000000176) * y +
624 (-0.0000000000000156) * y + 0.0000000000164) * y +
625 (-0.00000000106) * y + 0.000000421) * y +
626 (-0.0000103) * y + 0.00015625) * y +
627 (-0.015625) * y + 1;
628 }} else {{
629 const z = 8 / ax;
630 const y = z * z;
631 const xx = ax - 0.785398164;
632 return Math.sqrt(0.636619772 / ax) *
633 (Math.cos(xx) * (1 + y * (-0.0703125 + y * 0.1121520996)) +
634 z * Math.sin(xx) * (-0.0390625 + y * 0.0444479255));
635 }}
636 }}
637
638 function erf(x) {{
639 const a1 = 0.254829592;
640 const a2 = -0.284496736;
641 const a3 = 1.421413741;
642 const a4 = -1.453152027;
643 const a5 = 1.061405429;
644 const p = 0.3275911;
645
646 const sign = x >= 0 ? 1 : -1;
647 x = Math.abs(x);
648
649 const t = 1.0 / (1.0 + p * x);
650 const y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
651
652 return sign * y;
653 }}
654
655 function airyAi(x) {{
656 // Simplified Airy Ai function approximation
657 if (x > 5) return 0; // Exponentially decaying for positive x
658 if (x < -5) {{
659 // Oscillatory behavior for negative x
660 const arg = (2/3) * Math.pow(Math.abs(x), 1.5);
661 return (1 / (Math.sqrt(Math.PI) * Math.pow(Math.abs(x), 0.25))) * Math.sin(arg + Math.PI/4);
662 }}
663 // Rough approximation for the intermediate region
664 return Math.exp(-Math.abs(x)) * Math.cos(x);
665 }}
666
667 function getSpecialFunction(functionName) {{
668 const _name = functionName.toLowerCase();
669 if (_name.includes('gamma')) return gamma;
670 if (_name.includes('bessel') && name.includes('j0')) return besselJ0;
671 if (_name.includes('error') || name.includes('erf')) return erf;
672 if (_name.includes('airy')) return airyAi;
673 // Default fallback - could add more functions as needed
674 return Math.sin;
675 }}
676
677 function initializePlot() {{
678 const data = [{{
679 x: {},
680 y: {},
681 type: 'scatter',
682 mode: 'lines',
683 _name: '{}',
684 line: {{
685 color: '#1f77b4',
686 width: 2
687 }},
688 hovertemplate: '<b>x:</b> %{{x:.6f}}<br><b>f(x):</b> %{{y:.6f}}<extra></extra>'
689 }}];
690
691 const layout = {{
692 title: {{
693 text: '{} Function',
694 font: {{ size: 20 }}
695 }},
696 xaxis: {{
697 title: 'x',
698 showgrid: true,
699 zeroline: true,
700 showspikes: true,
701 spikethickness: 1,
702 spikecolor: '#999',
703 spikemode: 'across'
704 }},
705 yaxis: {{
706 title: 'f(x)',
707 showgrid: true,
708 zeroline: true,
709 showspikes: true,
710 spikethickness: 1,
711 spikecolor: '#999',
712 spikemode: 'across'
713 }},
714 hovermode: 'closest',
715 showlegend: true,
716 plot_bgcolor: 'white',
717 paper_bgcolor: 'white'
718 }};
719
720 const plotConfig = {{
721 responsive: true,
722 displayModeBar: true,
723 modeBarButtonsToAdd: [
724 'pan2d',
725 'zoomin2d',
726 'zoomout2d',
727 'autoScale2d',
728 'hoverClosestCartesian',
729 'hoverCompareCartesian'
730 ],
731 toImageButtonOptions: {{
732 format: 'png',
733 filename: '{}_plot',
734 height: 600,
735 width: 800,
736 scale: 1
737 }}
738 }};
739
740 Plotly.newPlot('plot', data, layout, plotConfig);
741
742 // Add hover event listener for tooltip info
743 document.getElementById('plot').on('plotly_hover', function(data) {{
744 const point = data.points[0];
745 document.getElementById('tooltip-info').innerHTML =
746 `<strong>Coordinates:</strong> x = ${{point.x.toFixed(6)}}, f(x) = ${{point.y.toFixed(6)}}`;
747 }});
748
749 currentData = {{ x: {}, y: {} }};
750 }}
751
752 function updatePlot() {{
753 const xMin = parseFloat(document.getElementById('xMin').value);
754 const xMax = parseFloat(document.getElementById('xMax').value);
755 const nPoints = parseInt(document.getElementById('points').value);
756
757 // Generate data points using actual special function implementations
758 const step = (xMax - xMin) / nPoints;
759 const x = [];
760 const y = [];
761
762 for (let i = 0; i <= nPoints; i++) {{
763 const xVal = xMin + i * step;
764 x.push(xVal);
765 // Use appropriate special function based on function _name
766 const func = getSpecialFunction('{}');
767 const yVal = func(xVal);
768 y.push(isFinite(yVal) ? yVal : NaN);
769 }}
770
771 Plotly.restyle('plot', {{'x': [x], 'y': [y]}});
772 currentData = {{ x: x, y: y }};
773 }}
774
775 function resetZoom() {{
776 Plotly.relayout('plot', {{
777 'xaxis.autorange': true,
778 'yaxis.autorange': true
779 }});
780 }}
781
782 function exportData() {{
783 let csv = 'x,f(x)\\n';
784 for (let i = 0; i < currentData.x.length; i++) {{
785 csv += `${{currentData.x[i]}},${{currentData.y[i]}}\\n`;
786 }}
787
788 const blob = new Blob([csv], {{ type: 'text/csv' }});
789 const url = window.URL.createObjectURL(blob);
790 const a = document.createElement('a');
791 a.style.display = 'none';
792 a.href = url;
793 a.download = '{}_data.csv';
794 document.body.appendChild(a);
795 a.click();
796 window.URL.revokeObjectURL(url);
797 document.body.removeChild(a);
798 }}
799
800 // Initialize the plot when the page loads
801 window.onload = initializePlot;
802 </script>
803</body>
804</html>
805 "#,
806 function_name,
807 function_name,
808 x_range.0,
809 x_range.1,
810 if config.enable_tooltips {
811 r#"<button onclick="toggleTooltips()">Toggle Info</button>"#
812 } else {
813 ""
814 },
815 x_json,
816 y_json,
817 function_name,
818 function_name,
819 function_name,
820 function_name, x_json,
822 y_json,
823 function_name,
824 function_name )
826 }
827
828 pub fn create_gamma_plot() -> String {
830 use crate::gamma::gamma;
831 let config = InteractivePlotConfig {
832 enable_zoom: true,
833 enable_pan: true,
834 enable_tooltips: true,
835 enable_export: true,
836 };
837 create_interactive_plot(gamma, config, (0.1, 5.0), "Gamma")
838 }
839
840 pub fn create_bessel_j0_plot() -> String {
841 use crate::bessel::j0;
842 let config = InteractivePlotConfig {
843 enable_zoom: true,
844 enable_pan: true,
845 enable_tooltips: true,
846 enable_export: true,
847 };
848 create_interactive_plot(j0, config, (-10.0, 10.0), "Bessel J0")
849 }
850
851 pub fn create_erf_plot() -> String {
852 use crate::erf::erf;
853 let config = InteractivePlotConfig {
854 enable_zoom: true,
855 enable_pan: true,
856 enable_tooltips: true,
857 enable_export: true,
858 };
859 create_interactive_plot(erf, config, (-3.0, 3.0), "Error Function")
860 }
861
862 pub fn create_comparison_plot() -> String {
864 let template = r#"
866<!DOCTYPE html>
867<html>
868<head>
869 <title>Special Functions Comparison</title>
870 <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
871 <style>
872 body { font-family: Arial, sans-serif; margin: 20px; background-color: #f8f9fa; }
873 .container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
874 h1 { text-align: center; color: #333; }
875 #plot { width: 100%; height: 700px; }
876 .controls { margin-bottom: 20px; text-align: center; }
877 button { margin: 5px; padding: 10px 20px; border: none; border-radius: 4px; background-color: #007bff; color: white; cursor: pointer; }
878 button:hover { background-color: #0056b3; }
879 </style>
880</head>
881<body>
882 <div class="container">
883 <h1>Special Functions Comparison</h1>
884 <div class="controls">
885 <button onclick="showGamma()">Gamma Function</button>
886 <button onclick="showBessel()">Bessel J0</button>
887 <button onclick="showErf()">Error Function</button>
888 <button onclick="showAll()">Show All</button>
889 </div>
890 <div id="plot"></div>
891 </div>
892
893 <script>
894 function generateData(func, range, nPoints, name) {
895 const step = (range[1] - range[0]) / nPoints;
896 const x = [];
897 const y = [];
898
899 for (let i = 0; i <= nPoints; i++) {
900 const xVal = range[0] + i * step;
901 x.push(xVal);
902 y.push(func(xVal));
903 }
904
905 return {
906 x: x,
907 y: y,
908 type: 'scatter',
909 mode: 'lines',
910 name: name,
911 line: { width: 2 }
912 };
913 }
914
915 function gamma(x) {
916 // Simplified gamma function approximation for demo
917 if (x < 0) return NaN;
918 if (x === 0) return Infinity;
919 if (x === 1 || x === 2) return 1;
920
921 // Stirling's approximation for simplicity
922 if (x > 1) {
923 return Math.sqrt(2 * Math.PI / x) * Math.pow(x / Math.E, x);
924 }
925 return gamma(x + 1) / x;
926 }
927
928 function besselJ0(x) {
929 // Simplified Bessel J0 approximation
930 const ax = Math.abs(x);
931 if (ax < 8) {
932 const y = x * x;
933 return ((-0.0000000000000000015 * y + 0.000000000000000176) * y +
934 (-0.0000000000000156) * y + 0.0000000000164) * y +
935 (-0.00000000106) * y + 0.000000421) * y +
936 (-0.0000103) * y + 0.00015625) * y +
937 (-0.015625) * y + 1;
938 } else {
939 const z = 8 / ax;
940 const y = z * z;
941 const xx = ax - 0.785398164;
942 return Math.sqrt(0.636619772 / ax) *
943 (Math.cos(xx) * (1 + y * (-0.0703125 + y * 0.1121520996)) +
944 z * Math.sin(xx) * (-0.0390625 + y * 0.0444479255));
945 }
946 }
947
948 function erf(x) {
949 // Simplified error function approximation
950 const a1 = 0.254829592;
951 const a2 = -0.284496736;
952 const a3 = 1.421413741;
953 const a4 = -1.453152027;
954 const a5 = 1.061405429;
955 const p = 0.3275911;
956
957 const sign = x >= 0 ? 1 : -1;
958 x = Math.abs(x);
959
960 const t = 1.0 / (1.0 + p * x);
961 const y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
962
963 return sign * y;
964 }
965
966 function showGamma() {
967 const data = [generateData(gamma, [0.1, 5], 1000, 'Gamma(x)')];
968 Plotly.newPlot('plot', data, {
969 title: 'Gamma Function',
970 xaxis: { title: 'x' },
971 yaxis: { title: 'Γ(x)' }
972 });
973 }
974
975 function showBessel() {
976 const data = [generateData(besselJ0, [-15, 15], 1000, 'J₀(x)')];
977 Plotly.newPlot('plot', data, {
978 title: 'Bessel Function of the First Kind (J₀)',
979 xaxis: { title: 'x' },
980 yaxis: { title: 'J₀(x)' }
981 });
982 }
983
984 function showErf() {
985 const data = [generateData(erf, [-3, 3], 1000, 'erf(x)')];
986 Plotly.newPlot('plot', data, {
987 title: 'Error Function',
988 xaxis: { title: 'x' },
989 yaxis: { title: 'erf(x)' }
990 });
991 }
992
993 function showAll() {
994 const data = [
995 generateData(x => gamma(x) / 10, [0.1, 3], 500, 'Γ(x)/10'),
996 generateData(besselJ0, [-10, 10], 500, 'J₀(x)'),
997 generateData(erf, [-3, 3], 500, 'erf(x)')
998 ];
999 Plotly.newPlot('plot', data, {
1000 title: 'Special Functions Comparison',
1001 xaxis: { title: 'x' },
1002 yaxis: { title: 'f(x)' }
1003 });
1004 }
1005
1006 // Initialize with gamma function
1007 window.onload = showGamma;
1008 </script>
1009</body>
1010</html>
1011 "#;
1012
1013 template.to_string()
1014 }
1015}
1016
1017pub mod export {
1019 use super::*;
1020
1021 pub enum ExportFormat {
1023 PNG,
1024 SVG,
1025 PDF,
1026 LaTeX,
1027 CSV,
1028 }
1029
1030 pub fn export_plot_data<F>(
1032 f: F,
1033 x_range: (f64, f64),
1034 n_points: usize,
1035 format: ExportFormat,
1036 ) -> Result<Vec<u8>, Box<dyn Error>>
1037 where
1038 F: Fn(f64) -> f64,
1039 {
1040 match format {
1041 ExportFormat::CSV => {
1042 let mut csv_data = String::from("x,y\n");
1043 let step = (x_range.1 - x_range.0) / n_points as f64;
1044
1045 for i in 0..=n_points {
1046 let x = x_range.0 + i as f64 * step;
1047 let y = f(x);
1048 csv_data.push_str(&format!("{},{}\n", x, y));
1049 }
1050
1051 Ok(csv_data.into_bytes())
1052 }
1053 ExportFormat::LaTeX => {
1054 let mut latex = String::from("\\begin{tikzpicture}\n\\begin{axis}[\n");
1056 latex.push_str(" xlabel=$x$,\n ylabel=$f(x)$,\n]\n");
1057 latex.push_str("\\addplot[blue,thick] coordinates {\n");
1058
1059 let step = (x_range.1 - x_range.0) / n_points as f64;
1060 for i in 0..=n_points {
1061 let x = x_range.0 + i as f64 * step;
1062 let y = f(x);
1063 if y.is_finite() {
1064 latex.push_str(&format!(" ({},{})\n", x, y));
1065 }
1066 }
1067
1068 latex.push_str("};\n\\end{axis}\n\\end{tikzpicture}\n");
1069 Ok(latex.into_bytes())
1070 }
1071 ExportFormat::PDF => {
1072 Err("PDF export is not yet implemented".to_string().into())
1074 }
1075 ExportFormat::PNG => {
1076 let mut png_data = Vec::new();
1078 {
1079 let backend =
1080 plotters::backend::BitMapBackend::with_buffer(&mut png_data, (800, 600))
1081 .into_drawing_area();
1082 backend
1083 .fill(&plotters::style::colors::WHITE)
1084 .map_err(|e| format!("Failed to fill background: {}", e))?;
1085
1086 let mut chart = plotters::chart::ChartBuilder::on(&backend)
1087 .caption("Special Function Plot", ("sans-serif", 30))
1088 .margin(10)
1089 .x_label_area_size(30)
1090 .y_label_area_size(40)
1091 .build_cartesian_2d(x_range.0..x_range.1, -2f64..2f64)
1092 .map_err(|e| format!("Failed to build chart: {}", e))?;
1093
1094 chart
1095 .configure_mesh()
1096 .x_desc("x")
1097 .y_desc("f(x)")
1098 .draw()
1099 .map_err(|e| format!("Failed to draw mesh: {}", e))?;
1100
1101 let data: Vec<(f64, f64)> = (0..=n_points)
1103 .map(|i| {
1104 let x =
1105 x_range.0 + i as f64 * (x_range.1 - x_range.0) / n_points as f64;
1106 let y = f(x);
1107 (x, y)
1108 })
1109 .filter(|(_, y)| y.is_finite())
1110 .collect();
1111
1112 chart
1113 .draw_series(plotters::series::LineSeries::new(
1114 data,
1115 &plotters::style::colors::BLUE,
1116 ))
1117 .map_err(|e| format!("Failed to draw series: {}", e))?;
1118
1119 backend
1120 .present()
1121 .map_err(|e| format!("Failed to present plot: {}", e))?;
1122 }
1123 Ok(png_data)
1126 }
1127 ExportFormat::SVG => {
1128 let mut svg_data = String::new();
1130 {
1131 let backend =
1132 plotters::backend::SVGBackend::with_string(&mut svg_data, (800, 600));
1133 let root = backend.into_drawing_area();
1134 root.fill(&plotters::style::colors::WHITE)
1135 .map_err(|e| format!("Failed to fill background: {}", e))?;
1136
1137 let mut chart = plotters::chart::ChartBuilder::on(&root)
1138 .caption("Special Function Plot", ("sans-serif", 30))
1139 .margin(10)
1140 .x_label_area_size(30)
1141 .y_label_area_size(40)
1142 .build_cartesian_2d(x_range.0..x_range.1, -2f64..2f64)
1143 .map_err(|e| format!("Failed to build chart: {}", e))?;
1144
1145 chart
1146 .configure_mesh()
1147 .x_desc("x")
1148 .y_desc("f(x)")
1149 .draw()
1150 .map_err(|e| format!("Failed to draw mesh: {}", e))?;
1151
1152 let data: Vec<(f64, f64)> = (0..=n_points)
1154 .map(|i| {
1155 let x =
1156 x_range.0 + i as f64 * (x_range.1 - x_range.0) / n_points as f64;
1157 let y = f(x);
1158 (x, y)
1159 })
1160 .filter(|(_, y)| y.is_finite())
1161 .collect();
1162
1163 chart
1164 .draw_series(plotters::series::LineSeries::new(
1165 data,
1166 &plotters::style::colors::BLUE,
1167 ))
1168 .map_err(|e| format!("Failed to draw series: {}", e))?;
1169
1170 root.present()
1171 .map_err(|e| format!("Failed to present plot: {}", e))?;
1172 }
1173 Ok(svg_data.into_bytes())
1174 }
1175 }
1176 }
1177}
1178
1179#[cfg(test)]
1180mod tests {
1181 use super::*;
1182
1183 #[test]
1184 fn test_plot_config() {
1185 let config = PlotConfig::default();
1186 assert_eq!(config.width, 800);
1187 assert_eq!(config.height, 600);
1188 assert!(config.show_grid);
1189 }
1190
1191 #[test]
1192 fn test_export_csv() {
1193 let data = export::export_plot_data(|x| x * x, (0.0, 1.0), 10, export::ExportFormat::CSV)
1194 .expect("Operation failed");
1195
1196 let csv = String::from_utf8(data).expect("Operation failed");
1197 assert!(csv.contains("x,y\n"));
1198 assert!(csv.contains("0,0\n"));
1199 assert!(csv.contains("1,1\n"));
1200 }
1201}