use std::{fs, path::Path};
pub(crate) static PLOTLY_JS: &str = include_str!("assets/js/plotly-3.3.0.min.js");
pub(crate) static TAILWIND_CSS: &str = include_str!("assets/js/tailwindcss-3.4.17.js");
pub(crate) fn get_plotly_js() -> &'static str {
PLOTLY_JS
}
pub(crate) fn get_tailwind_css() -> &'static str {
TAILWIND_CSS
}
pub(crate) fn write_plot_html(file_path: &str, html_content: &str) -> std::io::Result<()> {
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
let path = Path::new(file_path);
if path.exists() {
let _ = fs::remove_file(path);
}
let mut file = File::create(path)?;
file.write_all(html_content.as_bytes())?;
Ok(())
}
struct TwoPortPlotHtmlData<'a> {
network_names: &'a [String],
frequency_data: &'a [String],
s11_data: &'a [String],
s21_data: &'a [String],
s12_data: &'a [String],
s22_data: &'a [String],
s11_complex_data: &'a [String],
s22_complex_data: &'a [String],
}
fn generate_two_port_plot_html(
output_path: &str,
data: TwoPortPlotHtmlData<'_>,
) -> std::io::Result<()> {
let folder_path = Path::new(output_path)
.parent()
.map(|p| {
if p.as_os_str().is_empty() {
Path::new(".")
} else {
p
}
})
.unwrap_or(Path::new("."));
std::fs::create_dir_all(folder_path)?;
let mut html_content = include_str!("assets/template_2port.html").to_string();
let format_js_string_array = |arr: &[String]| -> String {
let items: Vec<String> = arr.iter().map(|s| format!("'{}'", s)).collect();
format!("[{}]", items.join(", "))
};
let format_js_data_array = |arr: &[String]| -> String { format!("[{}]", arr.join(", ")) };
html_content = html_content.replace(
"{{ network_names }}",
&format_js_string_array(data.network_names),
);
html_content = html_content.replace(
"{{ frequency_data }}",
&format_js_data_array(data.frequency_data),
);
html_content = html_content.replace("{{ s11_data }}", &format_js_data_array(data.s11_data));
html_content = html_content.replace("{{ s21_data }}", &format_js_data_array(data.s21_data));
html_content = html_content.replace("{{ s12_data }}", &format_js_data_array(data.s12_data));
html_content = html_content.replace("{{ s22_data }}", &format_js_data_array(data.s22_data));
html_content = html_content.replace(
"{{ s11_complex_data }}",
&format_js_data_array(data.s11_complex_data),
);
html_content = html_content.replace(
"{{ s22_complex_data }}",
&format_js_data_array(data.s22_complex_data),
);
write_plot_html(output_path, &html_content)?;
let js_assets_path = format!(
"{}/js",
std::path::Path::new(output_path)
.parent()
.unwrap()
.to_str()
.unwrap()
);
std::fs::create_dir_all(&js_assets_path)?;
let plotly_js_path = format!("{}/plotly-3.3.0.min.js", js_assets_path);
let tailwind_js_path = format!("{}/tailwindcss-3.4.17.js", js_assets_path);
std::fs::write(plotly_js_path, get_plotly_js())?;
std::fs::write(tailwind_js_path, get_tailwind_css())?;
Ok(())
}
pub fn generate_one_port_plot_html(
output_path: &str,
network_names: &[String],
frequency_data: &[String],
s11_data: &[String],
) -> std::io::Result<()> {
let folder_path = Path::new(output_path)
.parent()
.map(|p| {
if p.as_os_str().is_empty() {
Path::new(".")
} else {
p
}
})
.unwrap_or(Path::new("."));
std::fs::create_dir_all(folder_path)?;
let mut html_content = include_str!("assets/template_1port.html").to_string();
let format_js_string_array = |arr: &[String]| -> String {
let items: Vec<String> = arr.iter().map(|s| format!("'{}'", s)).collect();
format!("[{}]", items.join(", "))
};
let format_js_data_array = |arr: &[String]| -> String { format!("[{}]", arr.join(", ")) };
html_content = html_content.replace(
"{{ network_names }}",
&format_js_string_array(network_names),
);
html_content = html_content.replace(
"{{ frequency_data }}",
&format_js_data_array(frequency_data),
);
html_content = html_content.replace("{{ s11_data }}", &format_js_data_array(s11_data));
write_plot_html(output_path, &html_content)?;
let js_assets_path = format!(
"{}/js",
std::path::Path::new(output_path)
.parent()
.unwrap()
.to_str()
.unwrap()
);
std::fs::create_dir_all(&js_assets_path)?;
let plotly_js_path = format!("{}/plotly-3.3.0.min.js", js_assets_path);
let tailwind_js_path = format!("{}/tailwindcss-3.4.17.js", js_assets_path);
std::fs::write(plotly_js_path, get_plotly_js())?;
std::fs::write(tailwind_js_path, get_tailwind_css())?;
Ok(())
}
pub fn generate_plot_from_networks(
networks: &[crate::Network],
output_path: &str,
) -> std::io::Result<()> {
if networks.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"No networks provided for plotting",
));
}
let rank = networks[0].rank;
tracing::debug!(
num_networks = networks.len(),
rank,
output_path,
"Generating plot"
);
for network in networks {
if network.rank != rank {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"All networks must have the same rank. Found {} and {}",
rank, network.rank
),
));
}
}
match rank {
1 => {
let mut network_names = Vec::new();
let mut frequency_data_list = Vec::new();
let mut s11_data_list = Vec::new();
for network in networks {
network_names.push(network.name.clone());
let freq = network
.f
.iter()
.map(|f| f.to_string())
.collect::<Vec<String>>()
.join(", ");
frequency_data_list.push(format!("[{}]", freq));
let s11 = network
.s_db(1, 1)
.iter()
.map(|s| s.s_db.decibel().to_string())
.collect::<Vec<String>>()
.join(", ");
s11_data_list.push(format!("[{}]", s11));
}
generate_one_port_plot_html(
output_path,
&network_names,
&frequency_data_list,
&s11_data_list,
)
}
2 => {
let mut network_names = Vec::new();
let mut frequency_data_list = Vec::new();
let mut s11_data_list = Vec::new();
let mut s21_data_list = Vec::new();
let mut s12_data_list = Vec::new();
let mut s22_data_list = Vec::new();
let mut s11_complex_data_list = Vec::new();
let mut s22_complex_data_list = Vec::new();
for network in networks {
network_names.push(network.name.clone());
let freq = network
.f
.iter()
.map(|f| f.to_string())
.collect::<Vec<String>>()
.join(", ");
frequency_data_list.push(format!("[{}]", freq));
let s11 = network
.s_db(1, 1)
.iter()
.map(|s| s.s_db.decibel().to_string())
.collect::<Vec<String>>()
.join(", ");
s11_data_list.push(format!("[{}]", s11));
let s11_complex = network
.s_ri(1, 1)
.iter()
.map(|s| format!("{{ real: {}, imaginary: {} }}", s.s_ri.0, s.s_ri.1))
.collect::<Vec<String>>()
.join(", ");
s11_complex_data_list.push(format!("[{}]", s11_complex));
let s21 = network
.s_db(2, 1)
.iter()
.map(|s| s.s_db.decibel().to_string())
.collect::<Vec<String>>()
.join(", ");
s21_data_list.push(format!("[{}]", s21));
let s12 = network
.s_db(1, 2)
.iter()
.map(|s| s.s_db.decibel().to_string())
.collect::<Vec<String>>()
.join(", ");
s12_data_list.push(format!("[{}]", s12));
let s22 = network
.s_db(2, 2)
.iter()
.map(|s| s.s_db.decibel().to_string())
.collect::<Vec<String>>()
.join(", ");
s22_data_list.push(format!("[{}]", s22));
let s22_complex = network
.s_ri(2, 2)
.iter()
.map(|s| format!("{{ real: {}, imaginary: {} }}", s.s_ri.0, s.s_ri.1))
.collect::<Vec<String>>()
.join(", ");
s22_complex_data_list.push(format!("[{}]", s22_complex));
}
generate_two_port_plot_html(
output_path,
TwoPortPlotHtmlData {
network_names: &network_names,
frequency_data: &frequency_data_list,
s11_data: &s11_data_list,
s21_data: &s21_data_list,
s12_data: &s12_data_list,
s22_data: &s22_data_list,
s11_complex_data: &s11_complex_data_list,
s22_complex_data: &s22_complex_data_list,
},
)
}
_ => {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
format!(
"Plotting for {}-port networks is not yet supported. \
Currently only 1-port and 2-port networks can be plotted. \
For {}-port networks, you can still parse and access S-parameters programmatically, \
but interactive HTML plots are not available yet.",
rank, rank
),
))
}
}
}
#[cfg(test)]
mod tests {
use std::fs;
use crate::Network;
use super::*;
use std::path::PathBuf;
fn setup_test_dir(name: &str) -> PathBuf {
let mut path = std::env::temp_dir();
path.push("touchstone_tests");
path.push(name);
path.push(format!(
"{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&path).unwrap();
path
}
#[test]
fn test_get_plotly_js_not_empty() {
let js = get_plotly_js();
assert!(!js.is_empty());
assert!(js.len() > 1000); }
#[test]
fn test_get_tailwind_css_not_empty() {
let css = get_tailwind_css();
assert!(!css.is_empty());
assert!(css.len() > 1000);
}
#[test]
fn test_write_plot_html() {
let test_dir = setup_test_dir("test_write_plot_html");
let output_path = test_dir.join("output.html");
let output_str = output_path.to_str().unwrap();
let content = "<html><body>test</body></html>";
write_plot_html(output_str, content).unwrap();
let read_back = fs::read_to_string(&output_path).unwrap();
assert_eq!(read_back, content);
}
#[test]
fn test_generate_one_port_plot_html() {
let test_dir = setup_test_dir("test_generate_one_port_plot_html");
let s1p_path = test_dir.join("test.s1p");
fs::copy("files/hfss_oneport.s1p", &s1p_path).unwrap();
let network = Network::new(s1p_path.to_str().unwrap()).unwrap();
let output_path = test_dir.join("oneport_plot.html");
let output_str = output_path.to_str().unwrap().to_string();
let freq_data = vec![format!(
"[{}]",
network
.f
.iter()
.map(|f| f.to_string())
.collect::<Vec<String>>()
.join(", ")
)];
let s11_data: Vec<String> = vec![format!(
"[{}]",
network
.s_db(1, 1)
.iter()
.map(|s| s.s_db.decibel().to_string())
.collect::<Vec<String>>()
.join(", ")
)];
generate_one_port_plot_html(
&output_str,
std::slice::from_ref(&network.name),
&freq_data,
&s11_data,
)
.unwrap();
assert!(output_path.exists());
let html = fs::read_to_string(&output_path).unwrap();
assert!(html.contains("plotly"));
assert!(test_dir.join("js").exists());
}
#[test]
fn test_generate_plot_from_networks_empty() {
let test_dir = setup_test_dir("test_empty_networks");
let output_path = test_dir.join("empty.html");
let result = generate_plot_from_networks(&[], output_path.to_str().unwrap());
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::InvalidInput);
}
#[test]
fn test_generate_plot_from_networks_rank_mismatch() {
let n1 = Network::new("files/hfss_oneport.s1p").unwrap();
let n2 = Network::new("files/ntwk1.s2p").unwrap();
let test_dir = setup_test_dir("test_rank_mismatch");
let output_path = test_dir.join("mismatch.html");
let result = generate_plot_from_networks(&[n1, n2], output_path.to_str().unwrap());
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
assert!(err.to_string().contains("same rank"));
}
#[test]
fn test_generate_plot_from_networks_one_port() {
let network = Network::new("files/hfss_oneport.s1p").unwrap();
let test_dir = setup_test_dir("test_plot_one_port");
let output_path = test_dir.join("oneport.html");
generate_plot_from_networks(&[network], output_path.to_str().unwrap()).unwrap();
assert!(output_path.exists());
assert!(test_dir.join("js").exists());
}
#[test]
fn test_generate_plot_from_networks_multi_two_port() {
let n1 = Network::new("files/ntwk1.s2p").unwrap();
let n2 = Network::new("files/ntwk2.s2p").unwrap();
let test_dir = setup_test_dir("test_plot_multi_two_port");
let output_path = test_dir.join("overlay.html");
generate_plot_from_networks(&[n1, n2], output_path.to_str().unwrap()).unwrap();
assert!(output_path.exists());
let html = fs::read_to_string(&output_path).unwrap();
assert!(html.contains("ntwk1"));
assert!(html.contains("ntwk2"));
let card_s11 = html.find("id=\"card-s11\"").unwrap();
let card_s21 = html.find("id=\"card-s21\"").unwrap();
let card_s12 = html.find("id=\"card-s12\"").unwrap();
let card_s22 = html.find("id=\"card-s22\"").unwrap();
let card_smith_s11 = html.find("id=\"card-smith-s11\"").unwrap();
let card_smith_s22 = html.find("id=\"card-smith-s22\"").unwrap();
assert!(card_s11 < card_s21);
assert!(card_s21 < card_s12);
assert!(card_s12 < card_s22);
assert!(card_s22 < card_smith_s11);
assert!(card_smith_s11 < card_smith_s22);
let top_right_card = &html[card_s21..card_s12];
assert!(top_right_card.contains("S21 (Insertion Loss)"));
assert!(top_right_card.contains("downloadCSV('plot-s21', 's21_data')"));
assert!(top_right_card.contains("downloadImage('plot-s21', 's21_plot')"));
assert!(!top_right_card.contains("S12"));
let bottom_left_card = &html[card_s12..card_s22];
assert!(bottom_left_card.contains("S12 (Reverse"));
assert!(bottom_left_card.contains("downloadCSV('plot-s12', 's12_data')"));
assert!(bottom_left_card.contains("downloadImage('plot-s12', 's12_plot')"));
let s21_plot_call = html
.find("Plotly.newPlot('plot-s21', createTraces(freqData, s21Data")
.unwrap();
let s12_plot_call = html
.find("Plotly.newPlot('plot-s12', createTraces(freqData, s12Data")
.unwrap();
assert!(s21_plot_call < s12_plot_call);
let smith_s11_card = &html[card_smith_s11..card_smith_s22];
assert!(smith_s11_card.contains("S11 Smith Chart"));
assert!(smith_s11_card.contains("downloadCSV('plot-smith-s11', 'smith_s11_data')"));
assert!(smith_s11_card.contains("downloadImage('plot-smith-s11', 'smith_s11_plot')"));
assert!(html.contains("const s11ComplexData = [[{ real:"));
assert!(html.contains("const s22ComplexData = [[{ real:"));
assert!(!html.contains("{{ s11_complex_data }}"));
assert!(!html.contains("{{ s22_complex_data }}"));
assert!(html.contains("Plotly.newPlot('plot-smith-s11', createSmithTraces(s11ComplexData"));
assert!(html.contains("Plotly.newPlot('plot-smith-s22', createSmithTraces(s22ComplexData"));
assert!(html.contains("function constrainYAxisRange(plotId, range)"));
assert!(html.contains("min = Math.min(min, 0);"));
assert!(html.contains("if (min > 0) min = 0;"));
}
#[test]
fn test_generate_two_port_plot_html() {
let test_dir = setup_test_dir("test_generate_two_port_plot_html");
let s2p_path = test_dir.join("test_plot.s2p");
fs::copy("files/test_plot/test_plot.s2p", &s2p_path).unwrap();
let network = Network::new(s2p_path.to_str().unwrap()).unwrap();
let output_path = s2p_path.with_extension("s2p.html");
let output_path_str = output_path.to_str().unwrap().to_string();
println!("{}", output_path_str);
let output_path_as_path = Path::new(&output_path_str);
if output_path_as_path.exists() {
let _ = fs::remove_file(output_path_as_path);
}
let _ = generate_plot_from_networks(&[network], &output_path_str);
assert!(std::path::Path::new(&output_path_str).exists());
assert!(test_dir.join("js").exists());
}
}