1use crate::aes::Aesthetic;
6use crate::coord::Coord;
7use crate::data::{DataFrame, Value};
8use crate::position::identity::PositionIdentity;
9use crate::position::Position;
10use crate::render::backend::{DrawBackend, FontFace, RectStyle, TextAnchor, TextStyle};
11use crate::render::RenderError;
12use crate::scale::ScaleSet;
13use crate::stat::funnel::{FunnelShape, StatFunnel};
14use crate::stat::Stat;
15use crate::theme::Theme;
16
17use super::treemap::contrast_text;
18use super::{Geom, GeomParams};
19
20#[derive(Clone, Copy, Debug, PartialEq)]
22pub enum FunnelPercent {
23 None,
25 OfFirst,
27 OfTotal,
29}
30
31pub struct GeomFunnel {
33 pub shape: FunnelShape,
34 pub gap: f64,
36 pub fill: (u8, u8, u8),
38 pub alpha: f64,
39 pub labels: bool,
41 pub percent: FunnelPercent,
42 pub label_size: f64,
43 pub label_color: (u8, u8, u8),
45}
46
47impl Default for GeomFunnel {
48 fn default() -> Self {
49 GeomFunnel {
50 shape: FunnelShape::Proportional,
51 gap: 0.012,
52 fill: (97, 156, 255),
53 alpha: 1.0,
54 labels: true,
55 percent: FunnelPercent::OfFirst,
56 label_size: 10.0,
57 label_color: (40, 40, 40),
58 }
59 }
60}
61
62impl GeomFunnel {
63 pub fn silhouette(neck_width: f64, neck_height: f64) -> Self {
66 GeomFunnel {
67 shape: FunnelShape::Silhouette {
68 neck_width,
69 neck_height,
70 },
71 percent: FunnelPercent::OfTotal,
72 ..Default::default()
73 }
74 }
75
76 pub fn pyramid() -> Self {
78 GeomFunnel {
79 shape: FunnelShape::Pyramid,
80 percent: FunnelPercent::OfTotal,
81 ..Default::default()
82 }
83 }
84}
85
86impl Geom for GeomFunnel {
87 fn draw(
88 &self,
89 data: &DataFrame,
90 coord: &dyn Coord,
91 scales: &ScaleSet,
92 theme: &Theme,
93 backend: &mut dyn DrawBackend,
94 ) -> Result<(), RenderError> {
95 let col = |n: &str| {
96 data.column(n)
97 .ok_or_else(|| RenderError::MissingAesthetic(n.into()))
98 };
99 let (xc, yc, stage, value) = (col("fx")?, col("fy")?, col("stage")?, col("value")?);
100 let (ly, pf, pt) = (
101 col("label_y")?,
102 data.column("pct_first"),
103 data.column("pct_total"),
104 );
105 let fill_col = data.column("fill");
106 let area = backend.plot_area();
107 let map = |x: f64, y: f64| coord.transform((0.5 + 0.96 * x, 0.02 + 0.96 * y), &area);
110 let family = super::resolve_font_family("", theme);
111
112 let mut i = 0;
114 let mut labels = Vec::new();
115 while i < data.nrows() {
116 let key = stage[i].key_str();
117 let mut j = i;
118 while j < data.nrows() && stage[j].key_str() == key {
119 j += 1;
120 }
121 let pts: Vec<(f64, f64)> = (i..j)
122 .filter_map(|k| Some(map(xc[k].as_f64()?, yc[k].as_f64()?)))
123 .collect();
124 let fill = fill_col
125 .and_then(|c| scales.map_color(&Aesthetic::Fill, &c[i]))
126 .unwrap_or(self.fill);
127 let pct = match self.percent {
128 FunnelPercent::None => None,
129 FunnelPercent::OfFirst => pf.and_then(|c| c[i].as_f64()),
130 FunnelPercent::OfTotal => pt.and_then(|c| c[i].as_f64()),
131 }
132 .map(|p| {
133 format!(
134 " ({}%)",
135 super::tip_value(&Value::Float((p * 1000.0).round() / 10.0))
136 )
137 })
138 .unwrap_or_default();
139 let name = super::tip_value(&stage[i]);
140 let text = format!("{name}: {}{pct}", super::tip_value(&value[i]));
141 super::set_mark(
142 backend,
143 Some(text.clone()),
144 Some(name),
145 super::series_key(data, i),
146 super::raw_value(&value[i]),
147 );
148 if pts.len() >= 3 {
149 backend.draw_polygon(
150 &pts,
151 &RectStyle {
152 fill: Some(fill),
153 stroke: None,
154 stroke_width: 0.0,
155 alpha: self.alpha,
156 clip: true,
157 corner_radius: 0.0,
158 },
159 )?;
160 }
161 if self.labels {
162 if let Some(y) = ly[i].as_f64() {
163 let half = (i..j)
166 .filter_map(|k| xc[k].as_f64())
167 .map(f64::abs)
168 .sum::<f64>()
169 / (j - i).max(1) as f64;
170 let (l, r) = (map(-half, y), map(half, y));
171 let need = text.chars().count() as f64 * self.label_size * 0.6 + 8.0;
172 if (r.0 - l.0).abs() >= need {
173 labels.push((text, map(0.0, y), contrast_text(fill), TextAnchor::Middle));
174 } else {
175 let edge = (i..j).filter_map(|k| xc[k].as_f64()).fold(0.0, f64::max);
176 let p = map(edge, y);
177 labels.push((text, (p.0 + 6.0, p.1), self.label_color, TextAnchor::Start));
178 }
179 }
180 }
181 i = j;
182 }
183 super::clear_mark(backend);
184 for (text, pos, color, anchor) in labels {
185 backend.draw_text(
186 &text,
187 pos,
188 &TextStyle {
189 color,
190 size: self.label_size,
191 anchor,
192 angle: 0.0,
193 family: family.clone(),
194 face: FontFace::Plain,
195 bg_colour: None,
196 bg_r: 0.0,
197 },
198 )?;
199 }
200 Ok(())
201 }
202
203 fn required_aes(&self) -> Vec<Aesthetic> {
204 vec![Aesthetic::X, Aesthetic::Y]
205 }
206
207 fn default_stat(&self) -> Box<dyn Stat> {
208 Box::new(StatFunnel {
209 shape: self.shape,
210 gap: self.gap,
211 })
212 }
213
214 fn default_position(&self) -> Box<dyn Position> {
215 Box::new(PositionIdentity)
216 }
217
218 fn default_params(&self) -> GeomParams {
219 GeomParams::default()
220 }
221
222 fn name(&self) -> &str {
223 "funnel"
224 }
225
226 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
227 if let Some(ink) = el.ink {
228 self.label_color = ink;
229 }
230 }
231}