use super::*;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExportFormat {
Png,
Svg,
Html,
Tikz,
}
impl ExportFormat {
pub fn from_extension(path: &Path) -> VizResult<Self> {
match path.extension().and_then(|s| s.to_str()) {
Some("png") => Ok(ExportFormat::Png),
Some("svg") => Ok(ExportFormat::Svg),
Some("html") | Some("htm") => Ok(ExportFormat::Html),
Some("tex") | Some("tikz") => Ok(ExportFormat::Tikz),
Some(ext) => Err(VizError::ExportError(format!(
"Unsupported format: {}",
ext
))),
None => Err(VizError::ExportError("No file extension found".to_string())),
}
}
pub fn extension(&self) -> &'static str {
match self {
ExportFormat::Png => "png",
ExportFormat::Svg => "svg",
ExportFormat::Html => "html",
ExportFormat::Tikz => "tex",
}
}
}
#[derive(Debug, Clone)]
pub struct ExportConfig {
pub format: ExportFormat,
pub width: u32,
pub height: u32,
pub dpi: u32,
pub quality: u8,
}
impl Default for ExportConfig {
fn default() -> Self {
Self {
format: ExportFormat::Png,
width: 800,
height: 600,
dpi: 96,
quality: 90,
}
}
}
impl ExportConfig {
pub fn new(format: ExportFormat) -> Self {
Self {
format,
..Default::default()
}
}
pub fn with_size(mut self, width: u32, height: u32) -> VizResult<Self> {
if width == 0 || height == 0 {
return Err(VizError::InvalidConfig(
"Width and height must be positive".to_string(),
));
}
self.width = width;
self.height = height;
Ok(self)
}
pub fn with_dpi(mut self, dpi: u32) -> VizResult<Self> {
if dpi == 0 {
return Err(VizError::InvalidConfig("DPI must be positive".to_string()));
}
self.dpi = dpi;
Ok(self)
}
pub fn with_quality(mut self, quality: u8) -> VizResult<Self> {
if quality > 100 {
return Err(VizError::InvalidConfig(
"Quality must be between 0 and 100".to_string(),
));
}
self.quality = quality;
Ok(self)
}
}
pub struct Exporter;
impl Exporter {
pub fn to_tikz(
x: &[f64],
y: &[f64],
title: &str,
xlabel: &str,
ylabel: &str,
path: &Path,
) -> VizResult<()> {
if x.len() != y.len() {
return Err(VizError::DimensionMismatch(format!(
"x and y must have same length: {} != {}",
x.len(),
y.len()
)));
}
let mut tikz = String::new();
tikz.push_str("\\begin{tikzpicture}\n");
tikz.push_str("\\begin{axis}[\n");
tikz.push_str(&format!(" title={{{}}},\n", title));
tikz.push_str(&format!(" xlabel={{{}}},\n", xlabel));
tikz.push_str(&format!(" ylabel={{{}}},\n", ylabel));
tikz.push_str(" grid=major,\n");
tikz.push_str(" legend pos=north west,\n");
tikz.push_str("]\n");
tikz.push_str("\\addplot coordinates {\n");
for (xi, yi) in x.iter().zip(y.iter()) {
tikz.push_str(&format!(" ({}, {})\n", xi, yi));
}
tikz.push_str("};\n");
tikz.push_str("\\end{axis}\n");
tikz.push_str("\\end{tikzpicture}\n");
std::fs::write(path, tikz)?;
Ok(())
}
pub fn to_tikz_standalone(
x: &[f64],
y: &[f64],
title: &str,
xlabel: &str,
ylabel: &str,
path: &Path,
) -> VizResult<()> {
if x.len() != y.len() {
return Err(VizError::DimensionMismatch(format!(
"x and y must have same length: {} != {}",
x.len(),
y.len()
)));
}
let mut latex = String::new();
latex.push_str("\\documentclass{standalone}\n");
latex.push_str("\\usepackage{pgfplots}\n");
latex.push_str("\\pgfplotsset{compat=1.18}\n");
latex.push_str("\\begin{document}\n");
latex.push_str("\\begin{tikzpicture}\n");
latex.push_str("\\begin{axis}[\n");
latex.push_str(&format!(" title={{{}}},\n", title));
latex.push_str(&format!(" xlabel={{{}}},\n", xlabel));
latex.push_str(&format!(" ylabel={{{}}},\n", ylabel));
latex.push_str(" grid=major,\n");
latex.push_str(" legend pos=north west,\n");
latex.push_str("]\n");
latex.push_str("\\addplot coordinates {\n");
for (xi, yi) in x.iter().zip(y.iter()) {
latex.push_str(&format!(" ({}, {})\n", xi, yi));
}
latex.push_str("};\n");
latex.push_str("\\end{axis}\n");
latex.push_str("\\end{tikzpicture}\n");
latex.push_str("\\end{document}\n");
std::fs::write(path, latex)?;
Ok(())
}
pub fn batch_export<F>(
base_path: &Path,
formats: &[ExportFormat],
plot_fn: F,
) -> VizResult<Vec<std::path::PathBuf>>
where
F: Fn(&Path) -> VizResult<()>,
{
let mut output_paths = Vec::new();
for format in formats {
let path = base_path.with_extension(format.extension());
plot_fn(&path)?;
output_paths.push(path);
}
Ok(output_paths)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_format_from_extension() {
let path = PathBuf::from("test.png");
let format = ExportFormat::from_extension(&path);
assert!(format.is_ok());
assert_eq!(format.unwrap_or(ExportFormat::Svg), ExportFormat::Png);
let path = PathBuf::from("test.svg");
let format = ExportFormat::from_extension(&path);
assert_eq!(format.unwrap_or(ExportFormat::Png), ExportFormat::Svg);
let path = PathBuf::from("test.unknown");
let format = ExportFormat::from_extension(&path);
assert!(format.is_err());
}
#[test]
fn test_export_config() {
let config = ExportConfig::new(ExportFormat::Png)
.with_size(1024, 768)
.expect("Failed to set size")
.with_dpi(150)
.expect("Failed to set DPI")
.with_quality(95)
.expect("Failed to set quality");
assert_eq!(config.width, 1024);
assert_eq!(config.height, 768);
assert_eq!(config.dpi, 150);
assert_eq!(config.quality, 95);
}
#[test]
fn test_invalid_config() {
let result = ExportConfig::new(ExportFormat::Png).with_size(0, 600);
assert!(result.is_err());
let result = ExportConfig::new(ExportFormat::Png).with_quality(150);
assert!(result.is_err());
}
#[test]
fn test_tikz_dimension_mismatch() {
let x = vec![1.0, 2.0, 3.0];
let y = vec![1.0, 2.0];
let path = PathBuf::from("/tmp/test.tex");
let result = Exporter::to_tikz(&x, &y, "Test", "X", "Y", &path);
assert!(result.is_err());
}
}