1use std::f64::consts::{FRAC_PI_2, TAU};
8
9use crate::aes::Aesthetic;
10use crate::coord::Coord;
11use crate::data::DataFrame;
12use crate::position::identity::PositionIdentity;
13use crate::position::Position;
14use crate::render::backend::{DrawBackend, FontFace, RectStyle, TextAnchor, TextStyle};
15use crate::render::RenderError;
16use crate::scale::ScaleSet;
17use crate::stat::identity::StatIdentity;
18use crate::stat::Stat;
19use crate::theme::Theme;
20
21use super::{Geom, GeomParams};
22
23pub struct GeomVariablePie {
26 pub inner: f64,
28 pub min_radius: f64,
31 pub fill: (u8, u8, u8),
33 pub border: (u8, u8, u8),
34 pub labels: bool,
36 pub label_size: f64,
37 pub label_color: (u8, u8, u8),
38}
39
40impl Default for GeomVariablePie {
41 fn default() -> Self {
42 GeomVariablePie {
43 inner: 0.0,
44 min_radius: 0.35,
45 fill: (97, 156, 255),
46 border: (255, 255, 255),
47 labels: true,
48 label_size: 10.0,
49 label_color: (40, 40, 40),
50 }
51 }
52}
53
54pub(crate) fn radius_fraction(v: f64, lo: f64, hi: f64, min_r: f64) -> f64 {
57 if hi > lo {
58 min_r + (1.0 - min_r) * ((v - lo) / (hi - lo)).clamp(0.0, 1.0)
59 } else {
60 1.0
61 }
62}
63
64impl Geom for GeomVariablePie {
65 fn draw(
66 &self,
67 data: &DataFrame,
68 _coord: &dyn Coord,
69 scales: &ScaleSet,
70 theme: &Theme,
71 backend: &mut dyn DrawBackend,
72 ) -> Result<(), RenderError> {
73 let y_col = data
74 .column("y")
75 .ok_or(RenderError::MissingAesthetic("y".into()))?;
76 let r_col = data
77 .column("radius")
78 .ok_or(RenderError::MissingAesthetic("radius".into()))?;
79 let (fill_col, label_col) = (data.column("fill"), data.column("label"));
80 let rows: Vec<(usize, f64, f64)> = (0..data.nrows())
81 .filter_map(|i| {
82 let (y, r) = (y_col[i].as_f64()?, r_col[i].as_f64()?);
83 (y.is_finite() && y > 0.0 && r.is_finite()).then_some((i, y, r))
84 })
85 .collect();
86 let total: f64 = rows.iter().map(|r| r.1).sum();
87 if total <= 0.0 {
88 return Ok(());
89 }
90 let (lo, hi) = rows
91 .iter()
92 .fold((f64::MAX, f64::MIN), |(a, b), r| (a.min(r.2), b.max(r.2)));
93 let area = backend.plot_area();
94 let c = (area.x + area.width / 2.0, area.y + area.height / 2.0);
95 let room = if self.labels { 0.8 } else { 0.95 };
96 let full = room * area.width.min(area.height) / 2.0;
97 let pt = |a: f64, r: f64| (c.0 + r * a.cos(), c.1 + r * a.sin());
98 let family = super::resolve_font_family("", theme);
99 let mut labels = Vec::new();
100 let mut start = -FRAC_PI_2;
101 for (i, y, rv) in rows {
102 let sweep = TAU * y / total;
103 let outer = full * radius_fraction(rv, lo, hi, self.min_radius);
104 let inner = full * self.inner.clamp(0.0, 0.95);
105 let steps = ((sweep / 0.04).ceil() as usize).max(2);
106 let mut poly: Vec<(f64, f64)> = (0..=steps)
107 .map(|k| pt(start + sweep * k as f64 / steps as f64, outer))
108 .collect();
109 if inner > 0.0 {
110 poly.extend(
111 (0..=steps)
112 .rev()
113 .map(|k| pt(start + sweep * k as f64 / steps as f64, inner)),
114 );
115 } else {
116 poly.push(c);
117 }
118 let fill = fill_col
119 .and_then(|f| scales.map_color(&Aesthetic::Fill, &f[i]))
120 .unwrap_or(self.fill);
121 let name = label_col
122 .or(fill_col)
123 .map(|l| super::tip_value(&l[i]))
124 .unwrap_or_default();
125 let tip = format!(
126 "{name}: {} (radius {})",
127 super::tip_value(&y_col[i]),
128 super::tip_value(&r_col[i])
129 );
130 super::set_mark(
131 backend,
132 Some(tip),
133 Some(name.clone()),
134 super::series_key(data, i),
135 super::raw_value(&y_col[i]),
136 );
137 super::set_row_extras(backend, data, i);
138 backend.draw_polygon(
139 &poly,
140 &RectStyle {
141 fill: Some(fill),
142 stroke: Some(self.border),
143 stroke_width: 1.0,
144 alpha: 1.0,
145 clip: false,
146 corner_radius: 0.0,
147 },
148 )?;
149 if self.labels && !name.is_empty() {
150 let mid = start + sweep / 2.0;
151 let p = pt(mid, outer + 8.0);
152 let anchor = if mid.cos().abs() < 0.15 {
153 TextAnchor::Middle
154 } else if mid.cos() > 0.0 {
155 TextAnchor::Start
156 } else {
157 TextAnchor::End
158 };
159 labels.push((name, p, anchor));
160 }
161 start += sweep;
162 }
163 super::clear_mark(backend);
164 super::clear_row_extras(backend);
165 for (text, pos, anchor) in labels {
166 backend.draw_text(
167 &text,
168 pos,
169 &TextStyle {
170 color: self.label_color,
171 size: self.label_size,
172 anchor,
173 angle: 0.0,
174 family: family.clone(),
175 face: FontFace::Plain,
176 bg_colour: None,
177 bg_r: 0.0,
178 },
179 )?;
180 }
181 Ok(())
182 }
183
184 fn required_aes(&self) -> Vec<Aesthetic> {
185 vec![Aesthetic::Y, Aesthetic::Radius]
186 }
187 fn default_stat(&self) -> Box<dyn Stat> {
188 Box::new(StatIdentity)
189 }
190 fn default_position(&self) -> Box<dyn Position> {
191 Box::new(PositionIdentity)
192 }
193 fn default_params(&self) -> GeomParams {
194 GeomParams::default()
195 }
196 fn name(&self) -> &str {
197 "variable_pie"
198 }
199 fn consumes_tooltips(&self) -> bool {
200 true
201 }
202 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
203 if let Some(ink) = el.ink {
204 self.label_color = ink;
205 }
206 if let (true, Some(paper)) = (self.border == (255, 255, 255), el.paper) {
207 self.border = paper;
208 }
209 }
210 fn set_series_color(&mut self, color: (u8, u8, u8)) {
211 self.fill = color;
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn radius_maps_linearly() {
221 assert_eq!(radius_fraction(10.0, 10.0, 20.0, 0.4), 0.4);
222 assert_eq!(radius_fraction(20.0, 10.0, 20.0, 0.4), 1.0);
223 assert!((radius_fraction(15.0, 10.0, 20.0, 0.4) - 0.7).abs() < 1e-12);
224 assert_eq!(radius_fraction(5.0, 5.0, 5.0, 0.4), 1.0);
225 }
226}