Skip to main content

gallery/
gallery.rs

1//! Generates the plot gallery shown in the README.
2//!
3//! Run with: `cargo run --example gallery`
4//! Writes PNGs to `assets/gallery/`.
5
6use ggplot_rs::prelude::*;
7use polars::prelude::*;
8use std::f64::consts::PI;
9
10const W: u32 = 640;
11const H: u32 = 480;
12// Smaller thumbnails for the theme gallery.
13const TW: u32 = 480;
14const TH: u32 = 340;
15
16fn out(name: &str) -> String {
17    format!("assets/gallery/{name}.png")
18}
19
20fn main() -> Result<(), Box<dyn std::error::Error>> {
21    std::fs::create_dir_all("assets/gallery")?;
22
23    scatter()?;
24    smooth()?;
25    histogram()?;
26    bar()?;
27    boxplot()?;
28    violin()?;
29    continuous_color()?;
30    facet()?;
31    density()?;
32    contour_filled()?;
33    hexbin()?;
34    heatmap()?;
35    jitter()?;
36    ribbon()?;
37    area_stack()?;
38    themes()?;
39
40    println!("Gallery written to assets/gallery/");
41    Ok(())
42}
43
44/// Grouped scatter with a qualitative Brewer palette.
45fn scatter() -> Result<(), Box<dyn std::error::Error>> {
46    let n = 150;
47    let x: Vec<f64> = (0..n).map(|i| 4.5 + i as f64 * 0.02).collect();
48    let y: Vec<f64> = (0..n)
49        .map(|i| 2.5 + (i as f64 * 0.15).sin() + (i % 3) as f64 * 0.6)
50        .collect();
51    let species: Vec<&str> = (0..n)
52        .map(|i| ["setosa", "versicolor", "virginica"][i % 3])
53        .collect();
54
55    let df = df! { "x" => x, "y" => y, "species" => species }?;
56    GGPlot::new(df)
57        .aes(Aes::new().x("x").y("y").color("species"))
58        .geom_point()
59        .scale_color_brewer(PaletteName::Set1)
60        .title("Grouped Scatter")
61        .xlab("Sepal Length")
62        .ylab("Sepal Width")
63        .theme_minimal()
64        .save_with_size(&out("scatter"), W, H)?;
65    Ok(())
66}
67
68/// Points overlaid with a LOESS trend line and confidence band.
69fn smooth() -> Result<(), Box<dyn std::error::Error>> {
70    let n = 120;
71    let x: Vec<f64> = (0..n).map(|i| i as f64 * 0.1).collect();
72    let y: Vec<f64> = (0..n)
73        .map(|i| {
74            let t = i as f64 * 0.1;
75            (t * 0.6).sin() * 3.0 + t * 0.2 + ((i * 7919 % 100) as f64 / 100.0 - 0.5) * 1.5
76        })
77        .collect();
78
79    let df = df! { "x" => x, "y" => y }?;
80    GGPlot::new(df)
81        .aes(Aes::new().x("x").y("y"))
82        .geom_point()
83        .geom_smooth_with(GeomSmooth {
84            method: SmoothMethod::Loess { span: 0.5 },
85            ..Default::default()
86        })
87        .title("LOESS Smoothing")
88        .xlab("x")
89        .ylab("y")
90        .theme_bw()
91        .save_with_size(&out("smooth"), W, H)?;
92    Ok(())
93}
94
95/// Histogram of an approximately-normal sample.
96fn histogram() -> Result<(), Box<dyn std::error::Error>> {
97    let values: Vec<f64> = (0..1500)
98        .map(|i: i32| {
99            let r: f64 = (0..6)
100                .map(|k| ((i * (1237 + k * 311) + 5678) % 1000) as f64 / 1000.0)
101                .sum();
102            (r - 3.0) * 2.0
103        })
104        .collect();
105
106    let df = df! { "measurement" => values }?;
107    GGPlot::new(df)
108        .aes(Aes::new().x("measurement"))
109        .geom_histogram_with(GeomHistogram {
110            bins: 30,
111            ..Default::default()
112        })
113        .title("Histogram")
114        .xlab("Value")
115        .ylab("Count")
116        .theme_minimal()
117        .save_with_size(&out("histogram"), W, H)?;
118    Ok(())
119}
120
121/// Bar chart of category counts with a fill palette.
122fn bar() -> Result<(), Box<dyn std::error::Error>> {
123    let mut fruit: Vec<&str> = Vec::new();
124    for (f, c) in [
125        ("Apple", 8),
126        ("Banana", 5),
127        ("Cherry", 11),
128        ("Date", 3),
129        ("Elder", 7),
130    ] {
131        for _ in 0..c {
132            fruit.push(f);
133        }
134    }
135    let df = df! { "fruit" => fruit }?;
136    GGPlot::new(df)
137        .aes(Aes::new().x("fruit").fill("fruit"))
138        .geom_bar()
139        .scale_fill_brewer(PaletteName::Set2)
140        .title("Bar Chart")
141        .xlab("Fruit")
142        .ylab("Count")
143        .theme_minimal()
144        .save_with_size(&out("bar"), W, H)?;
145    Ok(())
146}
147
148/// Grouped boxplots.
149fn boxplot() -> Result<(), Box<dyn std::error::Error>> {
150    let n = 240;
151    let group: Vec<&str> = (0..n).map(|i| ["A", "B", "C", "D"][i % 4]).collect();
152    let value: Vec<f64> = (0..n)
153        .map(|i| {
154            let base = (i % 4) as f64 * 1.5;
155            base + (i as f64 * 0.4).sin() * 1.2 + ((i * 6151 % 100) as f64 / 100.0 - 0.5) * 2.0
156        })
157        .collect();
158
159    let df = df! { "group" => group, "value" => value }?;
160    GGPlot::new(df)
161        .aes(Aes::new().x("group").y("value"))
162        .geom_boxplot_with(GeomBoxplot {
163            fill: (70, 130, 180),
164            ..Default::default()
165        })
166        .title("Boxplot")
167        .xlab("Group")
168        .ylab("Value")
169        .theme_bw()
170        .save_with_size(&out("boxplot"), W, H)?;
171    Ok(())
172}
173
174/// Violin plots of grouped distributions.
175fn violin() -> Result<(), Box<dyn std::error::Error>> {
176    let n = 360;
177    let group: Vec<&str> = (0..n).map(|i| ["X", "Y", "Z"][i % 3]).collect();
178    let value: Vec<f64> = (0..n)
179        .map(|i| {
180            let g = (i % 3) as f64;
181            g * 2.0 + (i as f64 * 0.5).sin() * 1.5 + ((i * 4231 % 100) as f64 / 100.0 - 0.5) * 2.5
182        })
183        .collect();
184
185    let df = df! { "group" => group, "value" => value }?;
186    GGPlot::new(df)
187        .aes(Aes::new().x("group").y("value").fill("group"))
188        .geom_violin()
189        .scale_fill_brewer(PaletteName::Accent)
190        .title("Violin")
191        .xlab("Group")
192        .ylab("Value")
193        .theme_minimal()
194        .save_with_size(&out("violin"), W, H)?;
195    Ok(())
196}
197
198/// Spiral scatter coloured by a continuous variable (viridis).
199fn continuous_color() -> Result<(), Box<dyn std::error::Error>> {
200    let n = 400;
201    let x: Vec<f64> = (0..n)
202        .map(|i| {
203            let t = i as f64 * 0.05;
204            t.cos() * (1.0 + t * 0.12)
205        })
206        .collect();
207    let y: Vec<f64> = (0..n)
208        .map(|i| {
209            let t = i as f64 * 0.05;
210            t.sin() * (1.0 + t * 0.12)
211        })
212        .collect();
213    let z: Vec<f64> = (0..n).map(|i| i as f64 * 0.05).collect();
214
215    let df = df! { "x" => x, "y" => y, "z" => z }?;
216    GGPlot::new(df)
217        .aes(Aes::new().x("x").y("y").color("z"))
218        .geom_point()
219        .scale_color_viridis_c()
220        .title("Continuous Color (viridis)")
221        .xlab("x")
222        .ylab("y")
223        .theme_minimal()
224        .save_with_size(&out("continuous_color"), W, H)?;
225    Ok(())
226}
227
228/// Faceted scatter, one panel per group.
229fn facet() -> Result<(), Box<dyn std::error::Error>> {
230    let n = 180;
231    let x: Vec<f64> = (0..n).map(|i| (i as f64 * 0.1).cos() * 3.0).collect();
232    let y: Vec<f64> = (0..n)
233        .map(|i| (i as f64 * 0.1).sin() * 3.0 + (i % 3) as f64)
234        .collect();
235    let species: Vec<&str> = (0..n)
236        .map(|i| ["setosa", "versicolor", "virginica"][i % 3])
237        .collect();
238
239    let df = df! { "x" => x, "y" => y, "species" => species }?;
240    GGPlot::new(df)
241        .aes(Aes::new().x("x").y("y").color("species"))
242        .geom_point()
243        .facet_wrap("species", Some(3))
244        .scale_color_brewer(PaletteName::Set1)
245        .title("Facet Wrap")
246        .xlab("x")
247        .ylab("y")
248        .theme_bw()
249        .save_with_size(&out("facet"), W, H)?;
250    Ok(())
251}
252
253/// Overlapping density curves by group.
254fn density() -> Result<(), Box<dyn std::error::Error>> {
255    let n = 600;
256    let group: Vec<&str> = (0..n).map(|i| ["Group 1", "Group 2"][i % 2]).collect();
257    let value: Vec<f64> = (0..n)
258        .map(|i| {
259            let shift = (i % 2) as f64 * 2.5;
260            let t = i as f64 * 0.05;
261            shift + (t.sin() + (t * 1.7).cos()) + ((i * 3319 % 100) as f64 / 100.0 - 0.5) * PI
262        })
263        .collect();
264
265    let df = df! { "value" => value, "group" => group }?;
266    GGPlot::new(df)
267        .aes(Aes::new().x("value").fill("group").color("group"))
268        .geom_density()
269        .scale_fill_brewer(PaletteName::Set1)
270        .scale_color_brewer(PaletteName::Set1)
271        .title("Density by Group")
272        .xlab("Value")
273        .ylab("Density")
274        .theme_minimal()
275        .save_with_size(&out("density"), W, H)?;
276    Ok(())
277}
278
279/// Filled contour bands from a gridded surface.
280fn contour_filled() -> Result<(), Box<dyn std::error::Error>> {
281    let (mut x, mut y, mut z) = (Vec::new(), Vec::new(), Vec::new());
282    for i in 0..40 {
283        for j in 0..40 {
284            let xv = i as f64 * 0.25 - 5.0;
285            let yv = j as f64 * 0.25 - 5.0;
286            x.push(xv);
287            y.push(yv);
288            z.push((xv * 0.6).sin() * (yv * 0.6).cos() + (-(xv * xv + yv * yv) / 30.0).exp());
289        }
290    }
291    let df = df! { "x" => x, "y" => y, "z" => z }?;
292    GGPlot::new(df)
293        .aes(Aes::new().x("x").y("y"))
294        .geom_contour_filled()
295        .scale_fill_viridis_c()
296        .title("Filled Contours")
297        .theme_minimal()
298        .save_with_size(&out("contour_filled"), W, H)?;
299    Ok(())
300}
301
302/// Hexagonal binning of a 2-D point cloud.
303fn hexbin() -> Result<(), Box<dyn std::error::Error>> {
304    let n = 4000;
305    let x: Vec<f64> = (0..n)
306        .map(|i| {
307            let t = i as f64;
308            (t * 0.017).sin() * 2.0 + ((i * 7919 % 1000) as f64 / 1000.0 - 0.5) * 3.0
309        })
310        .collect();
311    let y: Vec<f64> = (0..n)
312        .map(|i| {
313            let t = i as f64;
314            (t * 0.017).cos() * 2.0 + ((i * 6323 % 1000) as f64 / 1000.0 - 0.5) * 3.0
315        })
316        .collect();
317    let df = df! { "x" => x, "y" => y }?;
318    GGPlot::new(df)
319        .aes(Aes::new().x("x").y("y"))
320        .geom_hex()
321        .scale_fill_viridis_c()
322        .title("Hex Binning")
323        .theme_minimal()
324        .save_with_size(&out("hexbin"), W, H)?;
325    Ok(())
326}
327
328/// Heatmap of a gridded value with `geom_tile`.
329fn heatmap() -> Result<(), Box<dyn std::error::Error>> {
330    let (mut x, mut y, mut fill) = (Vec::new(), Vec::new(), Vec::new());
331    for i in 0..14 {
332        for j in 0..14 {
333            x.push(i as f64);
334            y.push(j as f64);
335            fill.push((i as f64 * 0.5).sin() * (j as f64 * 0.5).cos());
336        }
337    }
338    let df = df! { "x" => x, "y" => y, "fill" => fill }?;
339    GGPlot::new(df)
340        .aes(Aes::new().x("x").y("y").fill("fill"))
341        .geom_tile()
342        .scale_fill_viridis_c()
343        .title("Heatmap")
344        .theme_minimal()
345        .save_with_size(&out("heatmap"), W, H)?;
346    Ok(())
347}
348
349/// Jittered categorical scatter (`geom_jitter`).
350fn jitter() -> Result<(), Box<dyn std::error::Error>> {
351    let n = 300;
352    let group: Vec<&str> = (0..n).map(|i| ["Control", "Low", "High"][i % 3]).collect();
353    let value: Vec<f64> = (0..n)
354        .map(|i| {
355            let base = (i % 3) as f64 * 1.5;
356            base + ((i * 5701 % 1000) as f64 / 1000.0 - 0.5) * 2.0
357        })
358        .collect();
359    let df = df! { "group" => group, "value" => value }?;
360    GGPlot::new(df)
361        .aes(Aes::new().x("group").y("value").color("group"))
362        .geom_jitter()
363        .scale_color_brewer(PaletteName::Dark2)
364        .title("Jittered Points")
365        .theme_minimal()
366        .save_with_size(&out("jitter"), W, H)?;
367    Ok(())
368}
369
370/// A confidence band (`geom_ribbon`) under a line.
371fn ribbon() -> Result<(), Box<dyn std::error::Error>> {
372    let n = 80;
373    let x: Vec<f64> = (0..n).map(|i| i as f64 * 0.15).collect();
374    let y: Vec<f64> = x.iter().map(|v| v.sin() + v * 0.1).collect();
375    let ymin: Vec<f64> = y.iter().map(|v| v - 0.4).collect();
376    let ymax: Vec<f64> = y.iter().map(|v| v + 0.4).collect();
377    let df = df! { "x" => x, "y" => y, "ymin" => ymin, "ymax" => ymax }?;
378    GGPlot::new(df)
379        .aes(Aes::new().x("x").y("y").ymin("ymin").ymax("ymax"))
380        .geom_ribbon()
381        .geom_line()
382        .primary_color((49, 130, 189))
383        .title("Ribbon + Line")
384        .theme_minimal()
385        .save_with_size(&out("ribbon"), W, H)?;
386    Ok(())
387}
388
389/// Stacked areas by group.
390fn area_stack() -> Result<(), Box<dyn std::error::Error>> {
391    let n = 40;
392    let mut x = Vec::new();
393    let mut y = Vec::new();
394    let mut g = Vec::new();
395    for grp in ["North", "South", "East"] {
396        for i in 0..n {
397            x.push(i as f64);
398            let base = match grp {
399                "North" => 2.0,
400                "South" => 3.0,
401                _ => 1.5,
402            };
403            y.push(base + (i as f64 * 0.2).sin().abs() * base);
404            g.push(grp);
405        }
406    }
407    let df = df! { "x" => x, "y" => y, "g" => g }?;
408    GGPlot::new(df)
409        .aes(Aes::new().x("x").y("y").fill("g"))
410        .geom_area()
411        .scale_fill_brewer(PaletteName::Set2)
412        .title("Stacked Area")
413        .theme_minimal()
414        .save_with_size(&out("area"), W, H)?;
415    Ok(())
416}
417
418/// The same plot rendered under every built-in theme.
419fn themes() -> Result<(), Box<dyn std::error::Error>> {
420    let n = 90;
421    let x: Vec<f64> = (0..n).map(|i| i as f64 * 0.1).collect();
422    let y: Vec<f64> = (0..n)
423        .map(|i| (i as f64 * 0.1).sin() + (i % 3) as f64 * 0.5)
424        .collect();
425    let g: Vec<&str> = (0..n).map(|i| ["A", "B", "C"][i % 3]).collect();
426    let df = df! { "x" => x, "y" => y, "g" => g }?;
427
428    type ThemeFn = fn() -> Theme;
429    let variants: [(&str, ThemeFn); 8] = [
430        ("gray", theme_gray),
431        ("bw", theme_bw),
432        ("minimal", theme_minimal),
433        ("classic", theme_classic),
434        ("light", theme_light),
435        ("dark", theme_dark),
436        ("linedraw", theme_linedraw),
437        ("void", theme_void),
438    ];
439    for (name, make) in variants {
440        GGPlot::new(df.clone())
441            .aes(Aes::new().x("x").y("y").color("g"))
442            .geom_point()
443            .theme(make())
444            .scale_color_brewer(PaletteName::Dark2)
445            .title(&format!("theme_{name}"))
446            .save_with_size(&out(&format!("theme_{name}")), TW, TH)?;
447    }
448    Ok(())
449}