Skip to main content

ggplot_rs/geom/
marimekko.rs

1//! Variable-width column charts: `geom_variwide` (bar width ∝ `weight`,
2//! height = `y`) and `geom_marimekko` (columns ∝ their total, split into
3//! 100 % stacked segments by `fill`). Layout by [`StatMarimekko`]; the
4//! builders put the category names under the column centres.
5
6use crate::aes::Aesthetic;
7use crate::coord::Coord;
8use crate::data::{DataFrame, Value};
9use crate::position::identity::PositionIdentity;
10use crate::position::Position;
11use crate::render::backend::{DrawBackend, FontFace, RectStyle, TextAnchor, TextStyle};
12use crate::render::RenderError;
13use crate::scale::ScaleSet;
14use crate::stat::marimekko::StatMarimekko;
15use crate::stat::Stat;
16use crate::theme::Theme;
17
18use super::treemap::{contrast_text, fit_size};
19use super::{Geom, GeomParams};
20
21/// Variwide / Marimekko chart. Map `x` (category), `weight` (column width)
22/// and `y` (variwide height) or `fill` (Marimekko segments).
23pub struct GeomMarimekko {
24    /// Segments by `fill` (Marimekko) rather than one bar per column.
25    pub mosaic: bool,
26    /// Gap between columns as a fraction of the full width.
27    pub gap: f64,
28    /// Fill when `fill` is unmapped.
29    pub fill: (u8, u8, u8),
30    pub border: (u8, u8, u8),
31    pub alpha: f64,
32    /// Label Marimekko segments with their share of the column.
33    pub labels: bool,
34    pub label_size: f64,
35}
36
37impl GeomMarimekko {
38    /// Variwide: bar widths ∝ `weight`, heights = `y`.
39    pub fn variwide() -> Self {
40        GeomMarimekko {
41            mosaic: false,
42            gap: 0.0,
43            fill: (97, 156, 255),
44            border: (255, 255, 255),
45            alpha: 1.0,
46            labels: false,
47            label_size: 9.0,
48        }
49    }
50
51    /// Marimekko: column widths ∝ their total `weight`, segments by `fill`.
52    pub fn marimekko() -> Self {
53        GeomMarimekko {
54            mosaic: true,
55            gap: 0.006,
56            labels: true,
57            ..Self::variwide()
58        }
59    }
60}
61
62impl Geom for GeomMarimekko {
63    fn draw(
64        &self,
65        data: &DataFrame,
66        coord: &dyn Coord,
67        scales: &ScaleSet,
68        theme: &Theme,
69        backend: &mut dyn DrawBackend,
70    ) -> Result<(), RenderError> {
71        let col = |n: &str| {
72            data.column(n)
73                .ok_or_else(|| RenderError::MissingAesthetic(n.into()))
74        };
75        let (x0c, x1c, y0c, y1c) = (col("xmin")?, col("xmax")?, col("ymin")?, col("ymax")?);
76        let (column, value, share) = (col("column")?, col("value")?, col("share")?);
77        let (fill_col, y_col) = (data.column("fill"), data.column("y"));
78        let area = backend.plot_area();
79        let m = |aes: Aesthetic, v: &Value| scales.get(&aes).map(|s| s.map(v)).unwrap_or(0.0);
80        let family = super::resolve_font_family("", theme);
81        let pct = |v: &Value| {
82            v.as_f64()
83                .map(|s| {
84                    format!(
85                        "{}%",
86                        super::tip_value(&Value::Float((s * 1000.0).round() / 10.0))
87                    )
88                })
89                .unwrap_or_default()
90        };
91        let mut labels = Vec::new();
92        for i in 0..data.nrows() {
93            let a = coord.transform((m(Aesthetic::X, &x0c[i]), m(Aesthetic::Y, &y1c[i])), &area);
94            let b = coord.transform((m(Aesthetic::X, &x1c[i]), m(Aesthetic::Y, &y0c[i])), &area);
95            let ((l, t), (r, btm)) = ((a.0.min(b.0), a.1.min(b.1)), (a.0.max(b.0), a.1.max(b.1)));
96            if ![l, t, r, btm].iter().all(|v| v.is_finite()) {
97                continue;
98            }
99            let fill = fill_col
100                .and_then(|c| scales.map_color(&Aesthetic::Fill, &c[i]))
101                .unwrap_or(self.fill);
102            let name = super::tip_value(&column[i]);
103            let tip = if self.mosaic {
104                let seg = fill_col
105                    .map(|c| super::tip_value(&c[i]))
106                    .unwrap_or_default();
107                format!(
108                    "{name} / {seg}: {} ({})",
109                    super::tip_value(&value[i]),
110                    pct(&share[i])
111                )
112            } else {
113                format!(
114                    "{name}: {} (width {})",
115                    y_col.map(|c| super::tip_value(&c[i])).unwrap_or_default(),
116                    super::tip_value(&value[i])
117                )
118            };
119            super::set_mark(
120                backend,
121                Some(tip),
122                Some(name),
123                super::series_key(data, i),
124                super::raw_value(&value[i]),
125            );
126            super::set_row_extras(backend, data, i);
127            backend.draw_rect(
128                (l, t),
129                (r, btm),
130                &RectStyle {
131                    fill: Some(fill),
132                    stroke: Some(self.border),
133                    stroke_width: 0.8,
134                    alpha: self.alpha,
135                    clip: true,
136                    corner_radius: 0.0,
137                },
138            )?;
139            if self.labels {
140                let text = pct(&share[i]);
141                if let Some(size) = fit_size(&text, r - l, btm - t, self.label_size, 6.0) {
142                    labels.push((
143                        text,
144                        ((l + r) / 2.0, (t + btm) / 2.0),
145                        size,
146                        contrast_text(fill),
147                    ));
148                }
149            }
150        }
151        super::clear_mark(backend);
152        super::clear_row_extras(backend);
153        for (text, pos, size, color) in labels {
154            backend.draw_text(
155                &text,
156                pos,
157                &TextStyle {
158                    color,
159                    size,
160                    anchor: TextAnchor::Middle,
161                    angle: 0.0,
162                    family: family.clone(),
163                    face: FontFace::Plain,
164                    bg_colour: None,
165                    bg_r: 0.0,
166                },
167            )?;
168        }
169        Ok(())
170    }
171
172    fn required_aes(&self) -> Vec<Aesthetic> {
173        vec![Aesthetic::X]
174    }
175
176    fn default_stat(&self) -> Box<dyn Stat> {
177        Box::new(StatMarimekko {
178            mosaic: self.mosaic,
179            gap: self.gap,
180        })
181    }
182
183    fn default_position(&self) -> Box<dyn Position> {
184        Box::new(PositionIdentity)
185    }
186
187    fn default_params(&self) -> GeomParams {
188        GeomParams::default()
189    }
190
191    fn name(&self) -> &str {
192        if self.mosaic {
193            "marimekko"
194        } else {
195            "variwide"
196        }
197    }
198
199    fn consumes_tooltips(&self) -> bool {
200        true
201    }
202
203    fn include_zero_baseline(&self) -> bool {
204        true
205    }
206
207    fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
208        if let (true, Some(paper)) = (self.border == (255, 255, 255), el.paper) {
209            self.border = paper;
210        }
211    }
212
213    fn set_series_color(&mut self, color: (u8, u8, u8)) {
214        self.fill = color;
215    }
216}