use crate::aes::Aes;
use crate::data::GGData;
use crate::plot::{GGError, GGPlot};
fn base(data: impl GGData, mut aes: Aes, group: Option<&str>, as_fill: bool) -> (GGPlot, Aes) {
if let Some(g) = group {
aes = if as_fill { aes.fill(g) } else { aes.color(g) };
}
(GGPlot::new(data), aes)
}
pub fn ggscatter(data: impl GGData, x: &str, y: &str, color: Option<&str>) -> GGPlot {
let (plot, aes) = base(data, Aes::new().x(x).y(y), color, false);
plot.aes(aes).geom_point().theme_pubr()
}
pub fn ggline(data: impl GGData, x: &str, y: &str, color: Option<&str>) -> GGPlot {
let (plot, aes) = base(data, Aes::new().x(x).y(y), color, false);
plot.aes(aes).geom_line().theme_pubr()
}
pub fn ggboxplot(data: impl GGData, x: &str, y: &str, fill: Option<&str>) -> GGPlot {
let (plot, aes) = base(data, Aes::new().x(x).y(y), fill, true);
plot.aes(aes).geom_boxplot().theme_pubr()
}
pub fn ggviolin(data: impl GGData, x: &str, y: &str, fill: Option<&str>) -> GGPlot {
let (plot, aes) = base(data, Aes::new().x(x).y(y), fill, true);
plot.aes(aes).geom_violin().theme_pubr()
}
pub fn gghistogram(data: impl GGData, x: &str, fill: Option<&str>) -> GGPlot {
let (plot, aes) = base(data, Aes::new().x(x), fill, true);
plot.aes(aes).geom_histogram().theme_pubr()
}
pub fn ggdensity(data: impl GGData, x: &str, color: Option<&str>) -> GGPlot {
let (plot, aes) = base(data, Aes::new().x(x), color, false);
plot.aes(aes).geom_density().theme_pubr()
}
pub fn ggarrange(
plots: Vec<GGPlot>,
ncol: usize,
cell_w: u32,
cell_h: u32,
) -> Result<String, GGError> {
let n = plots.len();
let ncol = ncol.max(1);
let nrow = n.div_ceil(ncol);
let total_w = ncol as u32 * cell_w;
let total_h = nrow.max(1) as u32 * cell_h;
let mut children = String::new();
for (i, plot) in plots.into_iter().enumerate() {
let inner = plot.render_svg_native_with_size(cell_w, cell_h)?;
let x = (i % ncol) as u32 * cell_w;
let y = (i / ncol) as u32 * cell_h;
let positioned = inner.replacen("<svg ", &format!("<svg x=\"{x}\" y=\"{y}\" "), 1);
children.push_str(&positioned);
}
Ok(format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{total_w}\" height=\"{total_h}\" \
viewBox=\"0 0 {total_w} {total_h}\">{children}</svg>"
))
}
pub fn ggarrange_save(
plots: Vec<GGPlot>,
ncol: usize,
cell_w: u32,
cell_h: u32,
path: &str,
) -> Result<(), GGError> {
let svg = ggarrange(plots, ncol, cell_w, cell_h)?;
std::fs::write(path, svg).map_err(GGError::Io)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn ggarrange_png(
plots: Vec<GGPlot>,
ncol: usize,
cell_w: u32,
cell_h: u32,
) -> Result<Vec<u8>, GGError> {
use crate::render::RenderError;
let n = plots.len();
let ncol = ncol.max(1);
let nrow = n.div_ceil(ncol).max(1);
let mut canvas = image::RgbaImage::from_pixel(
ncol as u32 * cell_w,
nrow as u32 * cell_h,
image::Rgba([255, 255, 255, 255]),
);
for (i, plot) in plots.into_iter().enumerate() {
let png = plot.render_png_with_size(cell_w, cell_h)?;
let cell = image::load_from_memory(&png)
.map_err(|e| GGError::Render(RenderError::BackendError(format!("decode: {e}"))))?
.to_rgba8();
let x = ((i % ncol) as u32 * cell_w) as i64;
let y = ((i / ncol) as u32 * cell_h) as i64;
image::imageops::overlay(&mut canvas, &cell, x, y);
}
let mut out = std::io::Cursor::new(Vec::new());
canvas
.write_to(&mut out, image::ImageOutputFormat::Png)
.map_err(|e| GGError::Render(RenderError::BackendError(format!("encode: {e}"))))?;
Ok(out.into_inner())
}
#[cfg(not(target_arch = "wasm32"))]
pub fn ggarrange_save_png(
plots: Vec<GGPlot>,
ncol: usize,
cell_w: u32,
cell_h: u32,
path: &str,
) -> Result<(), GGError> {
let png = ggarrange_png(plots, ncol, cell_w, cell_h)?;
std::fs::write(path, png).map_err(GGError::Io)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::Value;
fn xy() -> Vec<(String, Vec<Value>)> {
let x: Vec<Value> = (0..12).map(|i| Value::Float(i as f64)).collect();
let y: Vec<Value> = (0..12).map(|i| Value::Float((i as f64).sin())).collect();
let g: Vec<Value> = (0..12)
.map(|i| Value::Str(["a", "b"][i % 2].to_string()))
.collect();
vec![
("x".to_string(), x),
("y".to_string(), y),
("g".to_string(), g),
]
}
#[test]
fn constructors_build_and_render() {
assert!(ggscatter(xy(), "x", "y", Some("g")).render_svg().is_ok());
assert!(ggline(xy(), "x", "y", None).render_svg().is_ok());
assert!(ggboxplot(xy(), "g", "y", Some("g")).render_svg().is_ok());
assert!(ggviolin(xy(), "g", "y", Some("g")).render_svg().is_ok());
assert!(gghistogram(xy(), "y", None).render_svg().is_ok());
assert!(ggdensity(xy(), "y", Some("g")).render_svg().is_ok());
}
#[test]
fn ggarrange_composes_a_grid() {
let plots = vec![
ggscatter(xy(), "x", "y", None),
ggline(xy(), "x", "y", None),
ggboxplot(xy(), "g", "y", Some("g")),
ggdensity(xy(), "y", None),
];
let svg = ggarrange(plots, 2, 300, 220).expect("arrange");
assert!(svg.contains("width=\"600\" height=\"440\""), "outer size");
assert_eq!(svg.matches("<svg ").count(), 5, "outer + 4 nested svgs");
assert!(svg.contains("x=\"0\" y=\"0\""));
assert!(svg.contains("x=\"300\" y=\"0\""));
assert!(svg.contains("x=\"0\" y=\"220\""));
assert!(svg.contains("x=\"300\" y=\"220\""));
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn ggarrange_png_composes_a_grid() {
let plots = vec![
ggscatter(xy(), "x", "y", None),
ggline(xy(), "x", "y", None),
ggboxplot(xy(), "g", "y", Some("g")),
];
let png = ggarrange_png(plots, 2, 200, 160).expect("png");
assert_eq!(&png[..8], &[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]);
assert!(png.len() > 1000);
}
#[test]
fn constructors_are_chainable() {
let svg = ggscatter(xy(), "x", "y", None)
.title("chained")
.render_svg()
.unwrap();
assert!(svg.contains("chained"));
}
}