1use crate::aes::Aes;
12use crate::data::GGData;
13use crate::plot::{GGError, GGPlot};
14
15fn base(data: impl GGData, mut aes: Aes, group: Option<&str>, as_fill: bool) -> (GGPlot, Aes) {
16 if let Some(g) = group {
17 aes = if as_fill { aes.fill(g) } else { aes.color(g) };
18 }
19 (GGPlot::new(data), aes)
20}
21
22pub fn ggscatter(data: impl GGData, x: &str, y: &str, color: Option<&str>) -> GGPlot {
25 let (plot, aes) = base(data, Aes::new().x(x).y(y), color, false);
26 plot.aes(aes).geom_point().theme_pubr()
27}
28
29pub fn ggline(data: impl GGData, x: &str, y: &str, color: Option<&str>) -> GGPlot {
32 let (plot, aes) = base(data, Aes::new().x(x).y(y), color, false);
33 plot.aes(aes).geom_line().theme_pubr()
34}
35
36pub fn ggboxplot(data: impl GGData, x: &str, y: &str, fill: Option<&str>) -> GGPlot {
39 let (plot, aes) = base(data, Aes::new().x(x).y(y), fill, true);
40 plot.aes(aes).geom_boxplot().theme_pubr()
41}
42
43pub fn ggviolin(data: impl GGData, x: &str, y: &str, fill: Option<&str>) -> GGPlot {
46 let (plot, aes) = base(data, Aes::new().x(x).y(y), fill, true);
47 plot.aes(aes).geom_violin().theme_pubr()
48}
49
50pub fn gghistogram(data: impl GGData, x: &str, fill: Option<&str>) -> GGPlot {
53 let (plot, aes) = base(data, Aes::new().x(x), fill, true);
54 plot.aes(aes).geom_histogram().theme_pubr()
55}
56
57pub fn ggdensity(data: impl GGData, x: &str, color: Option<&str>) -> GGPlot {
60 let (plot, aes) = base(data, Aes::new().x(x), color, false);
61 plot.aes(aes).geom_density().theme_pubr()
62}
63
64pub fn ggarrange(
69 plots: Vec<GGPlot>,
70 ncol: usize,
71 cell_w: u32,
72 cell_h: u32,
73) -> Result<String, GGError> {
74 let n = plots.len();
75 let ncol = ncol.max(1);
76 let nrow = n.div_ceil(ncol);
77 let total_w = ncol as u32 * cell_w;
78 let total_h = nrow.max(1) as u32 * cell_h;
79
80 let mut children = String::new();
81 for (i, plot) in plots.into_iter().enumerate() {
82 let inner = plot.render_svg_native_with_size(cell_w, cell_h)?;
83 let x = (i % ncol) as u32 * cell_w;
84 let y = (i / ncol) as u32 * cell_h;
85 let positioned = inner.replacen("<svg ", &format!("<svg x=\"{x}\" y=\"{y}\" "), 1);
88 children.push_str(&positioned);
89 }
90
91 Ok(format!(
92 "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{total_w}\" height=\"{total_h}\" \
93 viewBox=\"0 0 {total_w} {total_h}\">{children}</svg>"
94 ))
95}
96
97pub fn ggarrange_save(
99 plots: Vec<GGPlot>,
100 ncol: usize,
101 cell_w: u32,
102 cell_h: u32,
103 path: &str,
104) -> Result<(), GGError> {
105 let svg = ggarrange(plots, ncol, cell_w, cell_h)?;
106 std::fs::write(path, svg).map_err(GGError::Io)
107}
108
109#[cfg(all(feature = "plotters", not(target_arch = "wasm32")))]
113pub fn ggarrange_png(
114 plots: Vec<GGPlot>,
115 ncol: usize,
116 cell_w: u32,
117 cell_h: u32,
118) -> Result<Vec<u8>, GGError> {
119 use crate::render::RenderError;
120 let n = plots.len();
121 let ncol = ncol.max(1);
122 let nrow = n.div_ceil(ncol).max(1);
123 let mut canvas = image::RgbaImage::from_pixel(
124 ncol as u32 * cell_w,
125 nrow as u32 * cell_h,
126 image::Rgba([255, 255, 255, 255]),
127 );
128 for (i, plot) in plots.into_iter().enumerate() {
129 let png = plot.render_png_with_size(cell_w, cell_h)?;
130 let cell = image::load_from_memory(&png)
131 .map_err(|e| GGError::Render(RenderError::BackendError(format!("decode: {e}"))))?
132 .to_rgba8();
133 let x = ((i % ncol) as u32 * cell_w) as i64;
134 let y = ((i / ncol) as u32 * cell_h) as i64;
135 image::imageops::overlay(&mut canvas, &cell, x, y);
136 }
137 let mut out = std::io::Cursor::new(Vec::new());
138 canvas
139 .write_to(&mut out, image::ImageOutputFormat::Png)
140 .map_err(|e| GGError::Render(RenderError::BackendError(format!("encode: {e}"))))?;
141 Ok(out.into_inner())
142}
143
144#[cfg(all(feature = "plotters", not(target_arch = "wasm32")))]
146pub fn ggarrange_save_png(
147 plots: Vec<GGPlot>,
148 ncol: usize,
149 cell_w: u32,
150 cell_h: u32,
151 path: &str,
152) -> Result<(), GGError> {
153 let png = ggarrange_png(plots, ncol, cell_w, cell_h)?;
154 std::fs::write(path, png).map_err(GGError::Io)
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::data::Value;
161
162 fn xy() -> Vec<(String, Vec<Value>)> {
163 let x: Vec<Value> = (0..12).map(|i| Value::Float(i as f64)).collect();
164 let y: Vec<Value> = (0..12).map(|i| Value::Float((i as f64).sin())).collect();
165 let g: Vec<Value> = (0..12)
166 .map(|i| Value::Str(["a", "b"][i % 2].to_string()))
167 .collect();
168 vec![
169 ("x".to_string(), x),
170 ("y".to_string(), y),
171 ("g".to_string(), g),
172 ]
173 }
174
175 #[test]
176 fn constructors_build_and_render() {
177 assert!(ggscatter(xy(), "x", "y", Some("g"))
178 .render_svg_native()
179 .is_ok());
180 assert!(ggline(xy(), "x", "y", None).render_svg_native().is_ok());
181 assert!(ggboxplot(xy(), "g", "y", Some("g"))
182 .render_svg_native()
183 .is_ok());
184 assert!(ggviolin(xy(), "g", "y", Some("g"))
185 .render_svg_native()
186 .is_ok());
187 assert!(gghistogram(xy(), "y", None).render_svg_native().is_ok());
188 assert!(ggdensity(xy(), "y", Some("g")).render_svg_native().is_ok());
189 }
190
191 #[test]
192 fn ggarrange_composes_a_grid() {
193 let plots = vec![
194 ggscatter(xy(), "x", "y", None),
195 ggline(xy(), "x", "y", None),
196 ggboxplot(xy(), "g", "y", Some("g")),
197 ggdensity(xy(), "y", None),
198 ];
199 let svg = ggarrange(plots, 2, 300, 220).expect("arrange");
200 assert!(svg.contains("width=\"600\" height=\"440\""), "outer size");
202 assert_eq!(svg.matches("<svg ").count(), 5, "outer + 4 nested svgs");
203 assert!(svg.contains("x=\"0\" y=\"0\""));
205 assert!(svg.contains("x=\"300\" y=\"0\""));
206 assert!(svg.contains("x=\"0\" y=\"220\""));
207 assert!(svg.contains("x=\"300\" y=\"220\""));
208 }
209
210 #[test]
211 #[cfg(all(feature = "plotters", not(target_arch = "wasm32")))]
212 fn ggarrange_png_composes_a_grid() {
213 let plots = vec![
214 ggscatter(xy(), "x", "y", None),
215 ggline(xy(), "x", "y", None),
216 ggboxplot(xy(), "g", "y", Some("g")),
217 ];
218 let png = ggarrange_png(plots, 2, 200, 160).expect("png");
219 assert_eq!(&png[..8], &[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]);
221 assert!(png.len() > 1000);
222 }
223
224 #[test]
225 fn constructors_are_chainable() {
226 let svg = ggscatter(xy(), "x", "y", None)
229 .title("chained")
230 .render_svg_native()
231 .unwrap();
232 assert!(svg.contains("chained"));
233 }
234}