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(
72 plots: Vec<GGPlot>,
73 ncol: usize,
74 cell_w: u32,
75 cell_h: u32,
76) -> Result<String, GGError> {
77 let n = plots.len();
78 let ncol = ncol.max(1);
79 let nrow = n.div_ceil(ncol).max(1);
80 crate::compose::PlotGrid::new()
81 .add_all(plots)
82 .ncol(ncol)
83 .render_svg_native_with_size(
84 (ncol as u32).saturating_mul(cell_w),
85 (nrow as u32).saturating_mul(cell_h),
86 )
87}
88
89pub fn ggarrange_save(
91 plots: Vec<GGPlot>,
92 ncol: usize,
93 cell_w: u32,
94 cell_h: u32,
95 path: &str,
96) -> Result<(), GGError> {
97 let svg = ggarrange(plots, ncol, cell_w, cell_h)?;
98 std::fs::write(path, svg).map_err(GGError::Io)
99}
100
101#[cfg(all(feature = "plotters", not(target_arch = "wasm32")))]
105pub fn ggarrange_png(
106 plots: Vec<GGPlot>,
107 ncol: usize,
108 cell_w: u32,
109 cell_h: u32,
110) -> Result<Vec<u8>, GGError> {
111 use crate::render::RenderError;
112 let n = plots.len();
113 let ncol = ncol.max(1);
114 let nrow = n.div_ceil(ncol).max(1);
115 let mut canvas = image::RgbaImage::from_pixel(
116 ncol as u32 * cell_w,
117 nrow as u32 * cell_h,
118 image::Rgba([255, 255, 255, 255]),
119 );
120 for (i, plot) in plots.into_iter().enumerate() {
121 let png = plot.render_png_with_size(cell_w, cell_h)?;
122 let cell = image::load_from_memory(&png)
123 .map_err(|e| GGError::Render(RenderError::BackendError(format!("decode: {e}"))))?
124 .to_rgba8();
125 let x = ((i % ncol) as u32 * cell_w) as i64;
126 let y = ((i / ncol) as u32 * cell_h) as i64;
127 image::imageops::overlay(&mut canvas, &cell, x, y);
128 }
129 let mut out = std::io::Cursor::new(Vec::new());
130 canvas
131 .write_to(&mut out, image::ImageOutputFormat::Png)
132 .map_err(|e| GGError::Render(RenderError::BackendError(format!("encode: {e}"))))?;
133 Ok(out.into_inner())
134}
135
136#[cfg(all(feature = "plotters", not(target_arch = "wasm32")))]
138pub fn ggarrange_save_png(
139 plots: Vec<GGPlot>,
140 ncol: usize,
141 cell_w: u32,
142 cell_h: u32,
143 path: &str,
144) -> Result<(), GGError> {
145 let png = ggarrange_png(plots, ncol, cell_w, cell_h)?;
146 std::fs::write(path, png).map_err(GGError::Io)
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::data::Value;
153
154 fn xy() -> Vec<(String, Vec<Value>)> {
155 let x: Vec<Value> = (0..12).map(|i| Value::Float(i as f64)).collect();
156 let y: Vec<Value> = (0..12).map(|i| Value::Float((i as f64).sin())).collect();
157 let g: Vec<Value> = (0..12)
158 .map(|i| Value::Str(["a", "b"][i % 2].to_string()))
159 .collect();
160 vec![
161 ("x".to_string(), x),
162 ("y".to_string(), y),
163 ("g".to_string(), g),
164 ]
165 }
166
167 #[test]
168 fn constructors_build_and_render() {
169 assert!(ggscatter(xy(), "x", "y", Some("g"))
170 .render_svg_native()
171 .is_ok());
172 assert!(ggline(xy(), "x", "y", None).render_svg_native().is_ok());
173 assert!(ggboxplot(xy(), "g", "y", Some("g"))
174 .render_svg_native()
175 .is_ok());
176 assert!(ggviolin(xy(), "g", "y", Some("g"))
177 .render_svg_native()
178 .is_ok());
179 assert!(gghistogram(xy(), "y", None).render_svg_native().is_ok());
180 assert!(ggdensity(xy(), "y", Some("g")).render_svg_native().is_ok());
181 }
182
183 #[test]
184 fn ggarrange_composes_a_grid() {
185 let plots = vec![
186 ggscatter(xy(), "x", "y", None),
187 ggline(xy(), "x", "y", None),
188 ggboxplot(xy(), "g", "y", Some("g")),
189 ggdensity(xy(), "y", None),
190 ];
191 let svg = ggarrange(plots, 2, 300, 220).expect("arrange");
192 assert!(svg.contains("width=\"600\" height=\"440\""), "outer size");
194 assert_eq!(svg.matches("<svg ").count(), 5, "outer + 4 nested svgs");
195 assert!(svg.contains("x=\"0.00\" y=\"0.00\" width=\"300\" height=\"220\""));
197 assert!(svg.contains("x=\"300.00\" y=\"0.00\""));
198 assert!(svg.contains("x=\"0.00\" y=\"220.00\""));
199 assert!(svg.contains("x=\"300.00\" y=\"220.00\""));
200 assert!(svg.contains("data-panel=\"4\""));
201 }
202
203 #[test]
204 #[cfg(all(feature = "plotters", not(target_arch = "wasm32")))]
205 fn ggarrange_png_composes_a_grid() {
206 let plots = vec![
207 ggscatter(xy(), "x", "y", None),
208 ggline(xy(), "x", "y", None),
209 ggboxplot(xy(), "g", "y", Some("g")),
210 ];
211 let png = ggarrange_png(plots, 2, 200, 160).expect("png");
212 assert_eq!(&png[..8], &[0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a]);
214 assert!(png.len() > 1000);
215 }
216
217 #[test]
218 fn constructors_are_chainable() {
219 let svg = ggscatter(xy(), "x", "y", None)
222 .title("chained")
223 .render_svg_native()
224 .unwrap();
225 assert!(svg.contains("chained"));
226 }
227}