Skip to main content

ggplot_rs/geom/
partition.rs

1//! Hierarchical partition charts: `geom_sunburst` (under `coord_polar`) and
2//! `geom_icicle` (Cartesian). One band per hierarchy level; each node's span
3//! is proportional to its value and nested inside its parent's. Layout by
4//! [`StatPartition`].
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::partition::StatPartition;
15use crate::stat::Stat;
16use crate::theme::Theme;
17
18use super::treemap::{contrast_text, fit_size};
19use super::{Geom, GeomParams};
20
21/// Partition chart over the `levels` columns (outermost first), sized by
22/// `weight` (1 per row when unmapped). Drawn as sectors under a polar coord
23/// (sunburst) and as rectangles otherwise (icicle).
24pub struct GeomPartition {
25    /// Hierarchy columns, outermost level first.
26    pub levels: Vec<String>,
27    /// Centre hole of a sunburst, in level widths (`geom_sunburst` turns it
28    /// into the polar coord's inner radius; unused in Cartesian coords).
29    pub hole: f64,
30    /// Order siblings by descending value.
31    pub sort: bool,
32    /// Grow downward with the root on top (icicle).
33    pub downward: bool,
34    /// Fill when no fill scale applies.
35    pub fill: (u8, u8, u8),
36    /// Blend each level this much further toward white (0 = flat colours).
37    pub lighten_per_level: f64,
38    pub border: (u8, u8, u8),
39    pub border_width: f64,
40    pub alpha: f64,
41    /// Label size (shrunk to fit, hidden below `min_label_size`; 0 = none).
42    pub label_size: f64,
43    pub min_label_size: f64,
44}
45
46impl GeomPartition {
47    /// A sunburst over `levels` (pair with `coord_polar`; `geom_sunburst`
48    /// does that for you).
49    pub fn sunburst<S: Into<String>>(levels: impl IntoIterator<Item = S>) -> Self {
50        GeomPartition {
51            levels: levels.into_iter().map(Into::into).collect(),
52            hole: 0.6,
53            sort: true,
54            downward: false,
55            fill: (97, 156, 255),
56            lighten_per_level: 0.18,
57            border: (255, 255, 255),
58            border_width: 1.0,
59            alpha: 1.0,
60            label_size: 10.0,
61            min_label_size: 6.5,
62        }
63    }
64
65    /// An icicle over `levels`: the root level on top, children below.
66    pub fn icicle<S: Into<String>>(levels: impl IntoIterator<Item = S>) -> Self {
67        GeomPartition {
68            hole: 0.0,
69            downward: true,
70            ..Self::sunburst(levels)
71        }
72    }
73}
74
75fn lighten(c: (u8, u8, u8), t: f64) -> (u8, u8, u8) {
76    let t = t.clamp(0.0, 1.0);
77    let m = |v: u8| (v as f64 + (255.0 - v as f64) * t).round() as u8;
78    (m(c.0), m(c.1), m(c.2))
79}
80
81impl Geom for GeomPartition {
82    fn draw(
83        &self,
84        data: &DataFrame,
85        coord: &dyn Coord,
86        scales: &ScaleSet,
87        theme: &Theme,
88        backend: &mut dyn DrawBackend,
89    ) -> Result<(), RenderError> {
90        let col = |n: &str| {
91            data.column(n)
92                .ok_or_else(|| RenderError::MissingAesthetic(n.into()))
93        };
94        let (x0c, x1c, y0c, y1c) = (col("xmin")?, col("xmax")?, col("ymin")?, col("ymax")?);
95        let (node, path, value) = (col("node")?, col("path")?, col("value")?);
96        let (depth, share) = (data.column("depth"), data.column("share"));
97        let fill_col = data.column("fill");
98        let area = backend.plot_area();
99        let nx = |v: &Value| scales.get(&Aesthetic::X).map(|s| s.map(v)).unwrap_or(0.0);
100        let ny = |v: &Value| scales.get(&Aesthetic::Y).map(|s| s.map(v)).unwrap_or(0.0);
101        let dist = |a: (f64, f64), b: (f64, f64)| (a.0 - b.0).hypot(a.1 - b.1);
102        let family = super::resolve_font_family("", theme);
103
104        let mut labels = Vec::new();
105        for i in 0..data.nrows() {
106            let (x0, x1, y0, y1) = (nx(&x0c[i]), nx(&x1c[i]), ny(&y0c[i]), ny(&y1c[i]));
107            if ![x0, x1, y0, y1].iter().all(|v| v.is_finite()) || x1 <= x0 {
108                continue;
109            }
110            let base = fill_col
111                .and_then(|c| scales.map_color(&Aesthetic::Fill, &c[i]))
112                .unwrap_or(self.fill);
113            let d = depth.and_then(|c| c[i].as_f64()).unwrap_or(0.0);
114            let fill = lighten(base, d * self.lighten_per_level);
115            let pct = share
116                .and_then(|c| c[i].as_f64())
117                .map(|s| {
118                    format!(
119                        " ({}%)",
120                        super::tip_value(&Value::Float((s * 1000.0).round() / 10.0))
121                    )
122                })
123                .unwrap_or_default();
124            super::set_mark(
125                backend,
126                Some(format!(
127                    "{}: {}{pct}",
128                    super::tip_value(&path[i]),
129                    super::tip_value(&value[i])
130                )),
131                Some(super::tip_value(&path[i])),
132                super::series_key(data, i),
133                super::raw_value(&value[i]),
134            );
135            let style = RectStyle {
136                fill: Some(fill),
137                stroke: Some(self.border),
138                stroke_width: self.border_width,
139                alpha: self.alpha,
140                clip: !coord.is_polar(),
141                corner_radius: 0.0,
142            };
143            if coord.is_polar() {
144                let pts = super::col::polar_sector(coord, &area, x0, x1, y0, y1);
145                backend.draw_polygon(&pts, &style)?;
146            } else {
147                let (a, b) = (
148                    coord.transform((x0, y1), &area),
149                    coord.transform((x1, y0), &area),
150                );
151                backend.draw_rect(
152                    (a.0.min(b.0), a.1.min(b.1)),
153                    (a.0.max(b.0), a.1.max(b.1)),
154                    &style,
155                )?;
156            }
157            if self.label_size > 0.0 {
158                let (xm, ym) = ((x0 + x1) / 2.0, (y0 + y1) / 2.0);
159                let centre = coord.transform((xm, ym), &area);
160                // Room along the band (chord at mid-radius) and across it.
161                let along = dist(
162                    coord.transform((x0, ym), &area),
163                    coord.transform((x1, ym), &area),
164                );
165                let across = dist(
166                    coord.transform((xm, y0), &area),
167                    coord.transform((xm, y1), &area),
168                );
169                let name = super::tip_value(&node[i]);
170                if let Some(size) =
171                    fit_size(&name, along, across, self.label_size, self.min_label_size)
172                {
173                    labels.push((name, centre, size, contrast_text(fill)));
174                }
175            }
176        }
177        super::clear_mark(backend);
178        for (text, pos, size, color) in labels {
179            backend.draw_text(
180                &text,
181                pos,
182                &TextStyle {
183                    color,
184                    size,
185                    anchor: TextAnchor::Middle,
186                    angle: 0.0,
187                    family: family.clone(),
188                    face: FontFace::Plain,
189                    bg_colour: None,
190                    bg_r: 0.0,
191                },
192            )?;
193        }
194        Ok(())
195    }
196
197    fn required_aes(&self) -> Vec<Aesthetic> {
198        Vec::new()
199    }
200
201    fn default_stat(&self) -> Box<dyn Stat> {
202        Box::new(StatPartition {
203            levels: self.levels.clone(),
204            sort: self.sort,
205            downward: self.downward,
206        })
207    }
208
209    fn default_position(&self) -> Box<dyn Position> {
210        Box::new(PositionIdentity)
211    }
212
213    fn default_params(&self) -> GeomParams {
214        GeomParams::default()
215    }
216
217    fn name(&self) -> &str {
218        "partition"
219    }
220
221    fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
222        if let (true, Some(paper)) = (self.border == (255, 255, 255), el.paper) {
223            self.border = paper;
224        }
225    }
226}