use super::PlotUtils;
use crate::AudioSampleResult;
use base64::Engine;
use plotly::Plot;
use std::fmt::Write;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CompositeLayout {
Vertical,
Horizontal,
Grid {
rows: usize,
cols: usize,
},
}
pub trait PlotComponent {
fn get_plot(&self) -> &Plot;
fn get_plot_mut(&mut self) -> &mut Plot;
fn requires_shared_x_axis(&self) -> bool;
}
pub struct CompositePlot {
html_plots: Vec<String>,
layout: CompositeLayout,
}
impl CompositePlot {
#[inline]
#[must_use]
pub const fn new() -> Self {
Self {
html_plots: Vec::new(),
layout: CompositeLayout::Vertical,
}
}
#[inline]
#[must_use]
pub fn add_plot<P: PlotComponent + 'static>(mut self, plot: P) -> Self {
self.html_plots.push(plot.get_plot().to_html());
self
}
#[inline]
#[must_use]
pub const fn layout(mut self, layout: CompositeLayout) -> Self {
self.layout = layout;
self
}
#[inline]
pub fn build(self) -> AudioSampleResult<Self> {
if self.html_plots.is_empty() {
return Err(crate::AudioSampleError::Parameter(
crate::ParameterError::InvalidValue {
parameter: "plots".to_string(),
reason: "Cannot build composite plot with no plots".to_string(),
},
));
}
Ok(self)
}
#[inline]
fn generate_composite_html(&self) -> AudioSampleResult<String> {
let container_style = match self.layout {
CompositeLayout::Vertical => "flex-direction: column;",
CompositeLayout::Horizontal => "flex-direction: row;",
CompositeLayout::Grid { rows: _, cols: _ } => "flex-direction: row; flex-wrap: wrap;",
};
let mut html: String = String::with_capacity(4096);
write!(
html,
r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Composite Plot</title>
<style>
body {{ margin: 0; padding: 0; }}
.composite-container {{
display: flex;
{container_style}
width: 100%;
height: 100vh;
}}
.plot-item {{
flex: 1;
min-height: 0;
min-width: 0;
}}
.plot-item iframe {{
width: 100%;
height: 100%;
border: none;
}}
</style>
</head>
<body>
<div class="composite-container">
"#
)?;
for plot_html in &self.html_plots {
let encoded = base64::engine::general_purpose::STANDARD.encode(plot_html.as_bytes());
write!(
html,
r#" <div class="plot-item">
<iframe src="data:text/html;base64,{encoded}"></iframe>
</div>
"#
)?;
}
html.push_str(
r" </div>
</body>
</html>",
);
Ok(html)
}
}
impl Default for CompositePlot {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl PlotUtils for CompositePlot {
#[inline]
fn html(&self) -> AudioSampleResult<String> {
self.generate_composite_html()
}
#[cfg(feature = "html_view")]
#[inline]
fn show(&self) -> AudioSampleResult<()> {
let html = self.html()?;
html_view::show(html).map_err(|e| {
crate::AudioSampleError::unsupported(format!("Failed to show plot: {}", e))
})?;
Ok(())
}
#[inline]
fn save<P: AsRef<Path>>(&self, path: P) -> AudioSampleResult<()> {
let path = path.as_ref();
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("html");
match extension.to_lowercase().as_str() {
"html" => {
let html = self.html()?;
std::fs::write(path, html).map_err(|e| {
crate::AudioSampleError::unsupported(format!("Failed to write HTML file: {e}"))
})?;
Ok(())
}
_ => Err(crate::AudioSampleError::Parameter(
crate::ParameterError::InvalidValue {
parameter: "file_extension".to_string(),
reason: format!("Composite plots only support HTML output. Got: {extension}"),
},
)),
}
}
}