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, LineStyle, Linetype, PointStyle, RectStyle};
11use crate::render::RenderError;
12use crate::scale::ScaleSet;
13use crate::stat::pareto::StatPareto;
14use crate::stat::Stat;
15use crate::theme::Theme;
16
17use super::{Geom, GeomParams};
18
19pub struct GeomPareto {
22 pub fill: (u8, u8, u8),
24 pub color: (u8, u8, u8),
26 pub line_color: (u8, u8, u8),
28 pub line_width: f64,
29 pub point_size: f64,
31 pub width: f64,
33 pub alpha: f64,
34 pub sort: bool,
36 pub threshold: Option<f64>,
39}
40
41impl Default for GeomPareto {
42 fn default() -> Self {
43 GeomPareto {
44 fill: (97, 156, 255),
45 color: (50, 50, 50),
46 line_color: (0xe8, 0x59, 0x0c),
47 line_width: 1.5,
48 point_size: 3.0,
49 width: 0.8,
50 alpha: 1.0,
51 sort: true,
52 threshold: None,
53 }
54 }
55}
56
57impl GeomPareto {
58 pub fn with_threshold(mut self, share: f64) -> Self {
60 self.threshold = Some(share);
61 self
62 }
63}
64
65impl Geom for GeomPareto {
66 fn draw(
67 &self,
68 data: &DataFrame,
69 coord: &dyn Coord,
70 scales: &ScaleSet,
71 _theme: &Theme,
72 backend: &mut dyn DrawBackend,
73 ) -> Result<(), RenderError> {
74 let col = |n: &str| {
75 data.column(n)
76 .ok_or_else(|| RenderError::MissingAesthetic(n.into()))
77 };
78 let (x_col, y_col, cum_col) = (col("x")?, col("y")?, col("cum")?);
79 let pct_col = data.column("cum_pct");
80 let area = backend.plot_area();
81 let x_scale = scales.get(&Aesthetic::X);
82 let y_scale = scales.get(&Aesthetic::Y);
83 let my = |v: f64| y_scale.map(|s| s.map(&Value::Float(v))).unwrap_or(0.0);
84 let half = if x_scale.map(|s| s.is_discrete()).unwrap_or(false) {
85 let n = x_scale.map(|s| s.breaks().len()).unwrap_or(1).max(1) as f64;
86 let pad = if coord.discrete_expand() { 1.1 } else { 1.0 };
87 self.width / (n * pad) / 2.0
88 } else {
89 super::continuous_bar_half_width(
90 x_col.iter().filter_map(|v| x_scale.map(|s| s.map(v))),
91 self.width,
92 0.02,
93 )
94 };
95 let pct_txt = |i: usize| {
96 pct_col
97 .and_then(|c| c[i].as_f64())
98 .map(|p| {
99 format!(
100 "{}%",
101 super::tip_value(&Value::Float((p * 10.0).round() / 10.0))
102 )
103 })
104 .unwrap_or_default()
105 };
106
107 let mut line = Vec::with_capacity(data.nrows());
108 for i in 0..data.nrows() {
109 let (Some(v), Some(cum)) = (y_col[i].as_f64(), cum_col[i].as_f64()) else {
110 continue;
111 };
112 let nx = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.5);
113 let x_txt = super::tip_value(&x_col[i]);
114 super::set_mark(
115 backend,
116 Some(format!(
117 "{x_txt}: {} (cumulative {})",
118 super::tip_value(&y_col[i]),
119 pct_txt(i)
120 )),
121 Some(x_txt),
122 super::series_key(data, i),
123 super::raw_value(&y_col[i]),
124 );
125 super::set_mark_formatted(
126 backend,
127 scales,
128 Some(&x_col[i]),
129 &Aesthetic::Y,
130 Some(&y_col[i]),
131 );
132 super::set_row_extras(backend, data, i);
133 let (x0, y0) = coord.transform((nx - half, my(v)), &area);
134 let (x1, y1) = coord.transform((nx + half, my(0.0)), &area);
135 backend.draw_rect(
136 (x0.min(x1), y0.min(y1)),
137 (x0.max(x1), y0.max(y1)),
138 &RectStyle {
139 fill: Some(self.fill),
140 stroke: Some(self.color),
141 stroke_width: 0.5,
142 alpha: self.alpha,
143 clip: true,
144 corner_radius: 0.0,
145 },
146 )?;
147 line.push((i, nx, my(cum)));
148 }
149 super::clear_row_extras(backend);
150 line.sort_by(|a, b| a.1.total_cmp(&b.1));
151
152 if let (Some(share), Some(total)) = (
153 self.threshold,
154 cum_col.iter().filter_map(|v| v.as_f64()).reduce(f64::max),
155 ) {
156 super::clear_mark(backend);
157 let ny = my(share * total);
158 backend.draw_line(
159 &[
160 coord.transform((0.0, ny), &area),
161 coord.transform((1.0, ny), &area),
162 ],
163 &LineStyle {
164 color: self.line_color,
165 alpha: 0.7 * self.alpha,
166 width: 1.0,
167 linetype: Linetype::Dashed,
168 ..Default::default()
169 },
170 )?;
171 }
172
173 let pts: Vec<(f64, f64)> = line
174 .iter()
175 .map(|&(_, nx, ny)| coord.transform((nx, ny), &area))
176 .collect();
177 if pts.len() > 1 {
178 super::set_mark(backend, None, None, Some("cumulative".into()), None);
179 backend.draw_line(
180 &pts,
181 &LineStyle {
182 color: self.line_color,
183 alpha: self.alpha,
184 width: self.line_width,
185 linetype: Linetype::Solid,
186 ..Default::default()
187 },
188 )?;
189 }
190 if self.point_size > 0.0 {
191 for (&(i, _, _), &p) in line.iter().zip(&pts) {
192 let x_txt = super::tip_value(&x_col[i]);
193 super::set_mark(
194 backend,
195 Some(format!("{x_txt}: cumulative {}", pct_txt(i))),
196 Some(x_txt),
197 Some("cumulative".into()),
198 pct_col.and_then(|c| super::raw_value(&c[i])),
199 );
200 backend.draw_circle(
201 p,
202 self.point_size,
203 &PointStyle {
204 color: self.line_color,
205 alpha: self.alpha,
206 filled: true,
207 ..Default::default()
208 },
209 )?;
210 }
211 }
212 super::clear_mark(backend);
213 Ok(())
214 }
215
216 fn required_aes(&self) -> Vec<Aesthetic> {
217 vec![Aesthetic::X, Aesthetic::Y]
218 }
219
220 fn default_stat(&self) -> Box<dyn Stat> {
221 Box::new(StatPareto { sort: self.sort })
222 }
223
224 fn default_position(&self) -> Box<dyn Position> {
225 Box::new(PositionIdentity)
226 }
227
228 fn default_params(&self) -> GeomParams {
229 GeomParams::default()
230 }
231
232 fn name(&self) -> &str {
233 "pareto"
234 }
235
236 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
237 if let (true, Some(ink)) = (self.color == (50, 50, 50), el.ink) {
238 self.color = ink;
239 }
240 }
241
242 fn consumes_tooltips(&self) -> bool {
243 true
244 }
245
246 fn include_zero_baseline(&self) -> bool {
247 true
248 }
249
250 fn set_series_color(&mut self, color: (u8, u8, u8)) {
251 self.fill = color;
252 }
253}