1use crate::aes::Aesthetic;
2use crate::coord::Coord;
3use crate::data::DataFrame;
4use crate::position::identity::PositionIdentity;
5use crate::position::Position;
6use crate::render::backend::{DrawBackend, RectStyle};
7use crate::render::RenderError;
8use crate::scale::ScaleSet;
9use crate::stat::bin::StatBin;
10use crate::stat::Stat;
11use crate::theme::Theme;
12
13use super::{Geom, GeomParams};
14
15pub struct GeomHistogram {
17 pub bins: usize,
18 pub binwidth: Option<f64>,
19 pub fill: (u8, u8, u8),
20 pub color: (u8, u8, u8),
21 pub alpha: f64,
22}
23
24impl GeomHistogram {
25 pub fn with_binwidth(mut self, width: f64) -> Self {
27 self.binwidth = Some(width);
28 self
29 }
30
31 pub fn with_bins(mut self, bins: usize) -> Self {
33 self.bins = bins;
34 self.binwidth = None;
35 self
36 }
37}
38
39impl Default for GeomHistogram {
40 fn default() -> Self {
41 GeomHistogram {
42 bins: 30,
43 binwidth: None,
44 fill: (97, 156, 255),
45 color: (50, 50, 50),
46 alpha: 1.0,
47 }
48 }
49}
50
51impl Geom for GeomHistogram {
52 fn key_fill(&self) -> Option<((u8, u8, u8), f64)> {
53 Some((self.fill, self.alpha))
54 }
55
56 fn draw(
57 &self,
58 data: &DataFrame,
59 coord: &dyn Coord,
60 scales: &ScaleSet,
61 _theme: &Theme,
62 backend: &mut dyn DrawBackend,
63 ) -> Result<(), RenderError> {
64 let xmin_col = data.column("xmin");
65 let xmax_col = data.column("xmax");
66 let y_col = data
67 .column("y")
68 .ok_or(RenderError::MissingAesthetic("y".into()))?;
69 let ymin_col = data.column("ymin");
73 let fill_col = data.column("fill");
77
78 let plot_area = backend.plot_area();
79 let x_scale = scales.get(&Aesthetic::X);
80 let y_scale = scales.get(&Aesthetic::Y);
81
82 for i in 0..data.nrows() {
83 let (left_n, right_n) = match (xmin_col, xmax_col) {
84 (Some(xmin), Some(xmax)) => {
85 let ln = x_scale.map(|s| s.map(&xmin[i])).unwrap_or(0.0);
86 let rn = x_scale.map(|s| s.map(&xmax[i])).unwrap_or(1.0);
87 (ln, rn)
88 }
89 _ => {
90 let x_col = data
92 .column("x")
93 .ok_or(RenderError::MissingAesthetic("x".into()))?;
94 let nx = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.0);
95 (nx - 0.02, nx + 0.02)
96 }
97 };
98
99 let fill = fill_col
100 .and_then(|c| scales.map_color(&Aesthetic::Fill, &c[i]))
101 .unwrap_or(self.fill);
102
103 let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
104 let ny_base = ymin_col
105 .and_then(|c| c[i].as_f64())
106 .and_then(|v| y_scale.map(|s| s.map(&crate::data::Value::Float(v))))
107 .unwrap_or_else(|| {
108 y_scale
109 .map(|s| s.map(&crate::data::Value::Float(0.0)))
110 .unwrap_or(0.0)
111 });
112
113 let (left_px, top_px) = coord.transform((left_n, ny), &plot_area);
114 let (right_px, bottom_px) = coord.transform((right_n, ny_base), &plot_area);
115
116 let cnt = super::tip_value(&y_col[i]);
118 let tip = match (xmin_col, xmax_col) {
119 (Some(lo), Some(hi)) => {
122 let (a, b) = (lo[i].as_f64(), hi[i].as_f64());
123 let d = a.zip(b).map(|(a, b)| {
124 (1.0 - (b - a).abs().log10().floor()).clamp(0.0, 8.0) as usize
125 });
126 let e = |v: Option<f64>, raw: &crate::data::Value| match (v, d) {
127 (Some(v), Some(d)) if v.is_finite() => edge(v, d),
128 _ => super::tip_value(raw),
129 };
130 format!("[{}, {}): {cnt}", e(a, &lo[i]), e(b, &hi[i]))
131 }
132 _ => format!("count: {cnt}"),
133 };
134 super::set_mark(
135 backend,
136 Some(tip),
137 data.column("x").map(|c| super::tip_value(&c[i])),
138 super::series_key(data, i),
139 super::measured_value(data, i),
140 );
141 super::set_mark_formatted(
142 backend,
143 scales,
144 data.column("x").map(|c| &c[i]),
145 &Aesthetic::Y,
146 super::measured_raw(data, i),
147 );
148 super::set_row_extras(backend, data, i);
149
150 backend.draw_rect(
151 (left_px, top_px.min(bottom_px)),
152 (right_px, top_px.max(bottom_px)),
153 &RectStyle {
154 fill: Some(fill),
155 stroke: Some(self.color),
156 stroke_width: 0.5,
157 alpha: self.alpha,
158 clip: true,
159 corner_radius: 0.0,
160 },
161 )?;
162 }
163 super::clear_mark(backend);
164 super::clear_row_extras(backend);
165
166 Ok(())
167 }
168
169 fn required_aes(&self) -> Vec<Aesthetic> {
170 vec![Aesthetic::X]
171 }
172
173 fn default_stat(&self) -> Box<dyn Stat> {
174 let mut stat = StatBin {
175 bins: self.bins,
176 binwidth: self.binwidth,
177 };
178 if let Some(bw) = self.binwidth {
179 stat = stat.with_binwidth(bw);
180 }
181 Box::new(stat)
182 }
183
184 fn default_position(&self) -> Box<dyn Position> {
185 Box::new(PositionIdentity)
186 }
187
188 fn default_params(&self) -> GeomParams {
189 GeomParams::default()
190 }
191
192 fn name(&self) -> &str {
193 "histogram"
194 }
195
196 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
197 if let (true, Some(ink)) = (self.color == (50, 50, 50), el.ink) {
198 self.color = ink;
199 }
200 }
201
202 fn consumes_tooltips(&self) -> bool {
203 true
204 }
205
206 fn include_zero_baseline(&self) -> bool {
207 true
208 }
209
210 fn set_series_color(&mut self, color: (u8, u8, u8)) {
211 self.fill = color;
212 }
213}
214
215fn edge(v: f64, d: usize) -> String {
217 let s = format!("{v:.d$}");
218 if s.contains('.') {
219 s.trim_end_matches('0').trim_end_matches('.').to_string()
220 } else {
221 s
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use crate::aes::{AesMapping, Aesthetic, MappingStage};
228 use crate::data::Value;
229 use crate::prelude::*;
230
231 #[test]
238 fn after_stat_data_attr_works_on_a_histogram() {
239 let n = 50;
240 let x: Vec<Value> = (0..n).map(|i| Value::Float(i as f64 * 0.2)).collect();
241 let mut aes = Aes::new().x("x");
242 aes.mappings.push(AesMapping {
243 column: "count".to_string(),
244 aesthetic: Aesthetic::DataAttr("data-n".to_string()),
245 stage: MappingStage::AfterStat,
246 });
247
248 let svg = GGPlot::new(vec![("x".into(), x)])
249 .aes(aes)
250 .geom_histogram()
251 .render_svg_native_with_size(400, 300)
252 .unwrap();
253
254 assert!(svg.contains("data-n=\""));
255 }
256
257 #[test]
260 fn tooltip_edges_follow_the_bin_width() {
261 assert_eq!(super::edge(223.36, 1), "223.4");
262 assert_eq!(super::edge(230.0, 1), "230");
263 assert_eq!(super::edge(0.125, 3), "0.125");
264 let x: Vec<Value> = (0..200)
265 .map(|i| Value::Float(100.0 + i as f64 * 1.37))
266 .collect();
267 let svg = GGPlot::new(vec![("x".into(), x)])
268 .aes(Aes::new().x("x"))
269 .geom_histogram()
270 .render_svg_native_with_size(400, 300)
271 .unwrap();
272 let tip = svg
273 .split("<title>")
274 .nth(1)
275 .unwrap()
276 .split("</title>")
277 .next()
278 .unwrap();
279 let edges = tip.trim_start_matches('[').split(')').next().unwrap();
280 for e in edges.split(", ") {
281 let dec = e.split('.').nth(1).map_or(0, str::len);
282 assert!(dec <= 2, "{tip}");
283 }
284 }
285
286 fn histogram_fills(svg: &str) -> Vec<String> {
288 let mut out = Vec::new();
289 let mut rest = svg;
290 while let Some(i) = rest.find("<rect ") {
291 let end = i + rest[i..].find('>').unwrap();
292 let tag = &rest[i..end];
293 if tag.contains("data-geom=\"histogram\"") {
294 let k = tag.find(" fill=\"").unwrap() + 7;
295 out.push(tag[k..k + tag[k..].find('"').unwrap()].to_string());
296 }
297 rest = &rest[end + 1..];
298 }
299 out
300 }
301
302 fn grouped_data() -> Vec<(String, Vec<Value>)> {
303 let n = 40;
304 let v: Vec<Value> = (0..n).map(|i| Value::Float((i % 10) as f64)).collect();
305 let g: Vec<Value> = (0..n)
306 .map(|i| {
307 Value::Str(if i < n / 2 {
308 "ref".into()
309 } else {
310 "cur".into()
311 })
312 })
313 .collect();
314 vec![("v".into(), v), ("g".into(), g)]
315 }
316
317 #[test]
321 fn grouped_fill_draws_distinct_colours_with_a_legend() {
322 let svg = GGPlot::new(grouped_data())
323 .aes(Aes::new().x("v").fill("g"))
324 .geom_histogram_with(GeomHistogram {
325 alpha: 0.6,
326 ..Default::default()
327 })
328 .render_svg_native_with_size(500, 350)
329 .unwrap();
330 let fills: std::collections::BTreeSet<String> = histogram_fills(&svg).into_iter().collect();
331 assert!(fills.len() >= 2, "expected ≥2 fill colours: {fills:?}");
332 assert!(
333 svg.contains(">ref<") && svg.contains(">cur<"),
334 "legend: {svg}"
335 );
336 }
337
338 #[test]
342 fn grouped_fill_shares_bin_edges() {
343 let svg = GGPlot::new(grouped_data())
344 .aes(Aes::new().x("v").fill("g"))
345 .geom_histogram()
346 .render_svg_native_with_size(500, 350)
347 .unwrap();
348 let mut widths = std::collections::BTreeSet::new();
349 let mut rest = svg.as_str();
350 while let Some(i) = rest.find("<rect ") {
351 let end = i + rest[i..].find('>').unwrap();
352 let tag = &rest[i..end];
353 if tag.contains("data-geom=\"histogram\"") {
354 let k = tag.find(" width=\"").unwrap() + 8;
355 let w: f64 = tag[k..k + tag[k..].find('"').unwrap()].parse().unwrap();
356 widths.insert((w * 100.0).round() as i64);
357 }
358 rest = &rest[end + 1..];
359 }
360 assert_eq!(widths.len(), 1, "every bar the same width: {widths:?}");
361 }
362
363 #[test]
367 fn grouped_fill_can_be_stacked() {
368 let svg = GGPlot::new(grouped_data())
369 .aes(Aes::new().x("v").fill("g"))
370 .geom_histogram()
371 .position(crate::position::stack::PositionStack)
372 .render_svg_native_with_size(500, 350)
373 .unwrap();
374 let mut tops = Vec::new();
377 let mut rest = svg.as_str();
378 while let Some(i) = rest.find("<rect ") {
379 let end = i + rest[i..].find('>').unwrap();
380 let tag = &rest[i..end];
381 if tag.contains("data-geom=\"histogram\"") {
382 let k = tag.find(" y=\"").unwrap() + 4;
383 let y: f64 = tag[k..k + tag[k..].find('"').unwrap()].parse().unwrap();
384 tops.push(y);
385 }
386 rest = &rest[end + 1..];
387 }
388 let distinct: std::collections::BTreeSet<i64> =
389 tops.iter().map(|t| (t * 10.0).round() as i64).collect();
390 assert!(
391 distinct.len() > 1,
392 "stacked bars should start at different heights: {tops:?}"
393 );
394 }
395
396 #[test]
400 fn grouped_fill_can_be_dodged() {
401 let svg = GGPlot::new(grouped_data())
406 .aes(Aes::new().x("v").fill("g"))
407 .geom_histogram_with(GeomHistogram {
408 binwidth: Some(1.0),
409 ..Default::default()
410 })
411 .position(crate::position::dodge2::PositionDodge2::default().with_width(1.0))
412 .render_svg_native_with_size(500, 350)
413 .unwrap();
414 let mut spans: Vec<(f64, f64)> = Vec::new();
415 let mut rest = svg.as_str();
416 while let Some(i) = rest.find("<rect ") {
417 let end = i + rest[i..].find('>').unwrap();
418 let tag = &rest[i..end];
419 if tag.contains("data-geom=\"histogram\"") {
420 let attr = |a: &str| -> f64 {
421 let k = tag.find(&format!(" {a}=\"")).unwrap() + a.len() + 3;
422 tag[k..k + tag[k..].find('"').unwrap()].parse().unwrap()
423 };
424 let x = attr("x");
425 spans.push((x, x + attr("width")));
426 }
427 rest = &rest[end + 1..];
428 }
429 for i in 0..spans.len() {
431 for j in (i + 1)..spans.len() {
432 let (a, b) = (spans[i], spans[j]);
433 let overlap = a.0 < b.1 - 0.01 && b.0 < a.1 - 0.01;
434 assert!(!overlap, "bars {a:?} and {b:?} overlap after dodge2");
435 }
436 }
437 }
438}