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 draw(
53 &self,
54 data: &DataFrame,
55 coord: &dyn Coord,
56 scales: &ScaleSet,
57 _theme: &Theme,
58 backend: &mut dyn DrawBackend,
59 ) -> Result<(), RenderError> {
60 let xmin_col = data.column("xmin");
61 let xmax_col = data.column("xmax");
62 let y_col = data
63 .column("y")
64 .ok_or(RenderError::MissingAesthetic("y".into()))?;
65 let ymin_col = data.column("ymin");
69 let fill_col = data.column("fill");
73
74 let plot_area = backend.plot_area();
75 let x_scale = scales.get(&Aesthetic::X);
76 let y_scale = scales.get(&Aesthetic::Y);
77
78 for i in 0..data.nrows() {
79 let (left_n, right_n) = match (xmin_col, xmax_col) {
80 (Some(xmin), Some(xmax)) => {
81 let ln = x_scale.map(|s| s.map(&xmin[i])).unwrap_or(0.0);
82 let rn = x_scale.map(|s| s.map(&xmax[i])).unwrap_or(1.0);
83 (ln, rn)
84 }
85 _ => {
86 let x_col = data
88 .column("x")
89 .ok_or(RenderError::MissingAesthetic("x".into()))?;
90 let nx = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.0);
91 (nx - 0.02, nx + 0.02)
92 }
93 };
94
95 let fill = fill_col
96 .and_then(|c| scales.map_color(&Aesthetic::Fill, &c[i]))
97 .unwrap_or(self.fill);
98
99 let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
100 let ny_base = ymin_col
101 .and_then(|c| c[i].as_f64())
102 .and_then(|v| y_scale.map(|s| s.map(&crate::data::Value::Float(v))))
103 .unwrap_or_else(|| {
104 y_scale
105 .map(|s| s.map(&crate::data::Value::Float(0.0)))
106 .unwrap_or(0.0)
107 });
108
109 let (left_px, top_px) = coord.transform((left_n, ny), &plot_area);
110 let (right_px, bottom_px) = coord.transform((right_n, ny_base), &plot_area);
111
112 let cnt = super::tip_value(&y_col[i]);
114 let tip = match (xmin_col, xmax_col) {
115 (Some(lo), Some(hi)) => format!(
116 "[{}, {}): {cnt}",
117 super::tip_value(&lo[i]),
118 super::tip_value(&hi[i])
119 ),
120 _ => format!("count: {cnt}"),
121 };
122 super::set_mark(
123 backend,
124 Some(tip),
125 data.column("x").map(|c| super::tip_value(&c[i])),
126 super::series_key(data, i),
127 super::measured_value(data, i),
128 );
129 super::set_mark_formatted(
130 backend,
131 scales,
132 data.column("x").map(|c| &c[i]),
133 &Aesthetic::Y,
134 super::measured_raw(data, i),
135 );
136 super::set_row_extras(backend, data, i);
137
138 backend.draw_rect(
139 (left_px, top_px.min(bottom_px)),
140 (right_px, top_px.max(bottom_px)),
141 &RectStyle {
142 fill: Some(fill),
143 stroke: Some(self.color),
144 stroke_width: 0.5,
145 alpha: self.alpha,
146 clip: true,
147 corner_radius: 0.0,
148 },
149 )?;
150 }
151 super::clear_mark(backend);
152 super::clear_row_extras(backend);
153
154 Ok(())
155 }
156
157 fn required_aes(&self) -> Vec<Aesthetic> {
158 vec![Aesthetic::X]
159 }
160
161 fn default_stat(&self) -> Box<dyn Stat> {
162 let mut stat = StatBin {
163 bins: self.bins,
164 binwidth: self.binwidth,
165 };
166 if let Some(bw) = self.binwidth {
167 stat = stat.with_binwidth(bw);
168 }
169 Box::new(stat)
170 }
171
172 fn default_position(&self) -> Box<dyn Position> {
173 Box::new(PositionIdentity)
174 }
175
176 fn default_params(&self) -> GeomParams {
177 GeomParams::default()
178 }
179
180 fn name(&self) -> &str {
181 "histogram"
182 }
183
184 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
185 if let (true, Some(ink)) = (self.color == (50, 50, 50), el.ink) {
186 self.color = ink;
187 }
188 }
189
190 fn consumes_tooltips(&self) -> bool {
191 true
192 }
193
194 fn include_zero_baseline(&self) -> bool {
195 true
196 }
197
198 fn set_series_color(&mut self, color: (u8, u8, u8)) {
199 self.fill = color;
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use crate::aes::{AesMapping, Aesthetic, MappingStage};
206 use crate::data::Value;
207 use crate::prelude::*;
208
209 #[test]
216 fn after_stat_data_attr_works_on_a_histogram() {
217 let n = 50;
218 let x: Vec<Value> = (0..n).map(|i| Value::Float(i as f64 * 0.2)).collect();
219 let mut aes = Aes::new().x("x");
220 aes.mappings.push(AesMapping {
221 column: "count".to_string(),
222 aesthetic: Aesthetic::DataAttr("data-n".to_string()),
223 stage: MappingStage::AfterStat,
224 });
225
226 let svg = GGPlot::new(vec![("x".into(), x)])
227 .aes(aes)
228 .geom_histogram()
229 .render_svg_native_with_size(400, 300)
230 .unwrap();
231
232 assert!(svg.contains("data-n=\""));
233 }
234
235 fn histogram_fills(svg: &str) -> Vec<String> {
237 let mut out = Vec::new();
238 let mut rest = svg;
239 while let Some(i) = rest.find("<rect ") {
240 let end = i + rest[i..].find('>').unwrap();
241 let tag = &rest[i..end];
242 if tag.contains("data-geom=\"histogram\"") {
243 let k = tag.find(" fill=\"").unwrap() + 7;
244 out.push(tag[k..k + tag[k..].find('"').unwrap()].to_string());
245 }
246 rest = &rest[end + 1..];
247 }
248 out
249 }
250
251 fn grouped_data() -> Vec<(String, Vec<Value>)> {
252 let n = 40;
253 let v: Vec<Value> = (0..n).map(|i| Value::Float((i % 10) as f64)).collect();
254 let g: Vec<Value> = (0..n)
255 .map(|i| {
256 Value::Str(if i < n / 2 {
257 "ref".into()
258 } else {
259 "cur".into()
260 })
261 })
262 .collect();
263 vec![("v".into(), v), ("g".into(), g)]
264 }
265
266 #[test]
270 fn grouped_fill_draws_distinct_colours_with_a_legend() {
271 let svg = GGPlot::new(grouped_data())
272 .aes(Aes::new().x("v").fill("g"))
273 .geom_histogram_with(GeomHistogram {
274 alpha: 0.6,
275 ..Default::default()
276 })
277 .render_svg_native_with_size(500, 350)
278 .unwrap();
279 let fills: std::collections::BTreeSet<String> = histogram_fills(&svg).into_iter().collect();
280 assert!(fills.len() >= 2, "expected ≥2 fill colours: {fills:?}");
281 assert!(
282 svg.contains(">ref<") && svg.contains(">cur<"),
283 "legend: {svg}"
284 );
285 }
286
287 #[test]
291 fn grouped_fill_shares_bin_edges() {
292 let svg = GGPlot::new(grouped_data())
293 .aes(Aes::new().x("v").fill("g"))
294 .geom_histogram()
295 .render_svg_native_with_size(500, 350)
296 .unwrap();
297 let mut widths = std::collections::BTreeSet::new();
298 let mut rest = svg.as_str();
299 while let Some(i) = rest.find("<rect ") {
300 let end = i + rest[i..].find('>').unwrap();
301 let tag = &rest[i..end];
302 if tag.contains("data-geom=\"histogram\"") {
303 let k = tag.find(" width=\"").unwrap() + 8;
304 let w: f64 = tag[k..k + tag[k..].find('"').unwrap()].parse().unwrap();
305 widths.insert((w * 100.0).round() as i64);
306 }
307 rest = &rest[end + 1..];
308 }
309 assert_eq!(widths.len(), 1, "every bar the same width: {widths:?}");
310 }
311
312 #[test]
316 fn grouped_fill_can_be_stacked() {
317 let svg = GGPlot::new(grouped_data())
318 .aes(Aes::new().x("v").fill("g"))
319 .geom_histogram()
320 .position(crate::position::stack::PositionStack)
321 .render_svg_native_with_size(500, 350)
322 .unwrap();
323 let mut tops = Vec::new();
326 let mut rest = svg.as_str();
327 while let Some(i) = rest.find("<rect ") {
328 let end = i + rest[i..].find('>').unwrap();
329 let tag = &rest[i..end];
330 if tag.contains("data-geom=\"histogram\"") {
331 let k = tag.find(" y=\"").unwrap() + 4;
332 let y: f64 = tag[k..k + tag[k..].find('"').unwrap()].parse().unwrap();
333 tops.push(y);
334 }
335 rest = &rest[end + 1..];
336 }
337 let distinct: std::collections::BTreeSet<i64> =
338 tops.iter().map(|t| (t * 10.0).round() as i64).collect();
339 assert!(
340 distinct.len() > 1,
341 "stacked bars should start at different heights: {tops:?}"
342 );
343 }
344
345 #[test]
349 fn grouped_fill_can_be_dodged() {
350 let svg = GGPlot::new(grouped_data())
355 .aes(Aes::new().x("v").fill("g"))
356 .geom_histogram_with(GeomHistogram {
357 binwidth: Some(1.0),
358 ..Default::default()
359 })
360 .position(crate::position::dodge2::PositionDodge2::default().with_width(1.0))
361 .render_svg_native_with_size(500, 350)
362 .unwrap();
363 let mut spans: Vec<(f64, f64)> = Vec::new();
364 let mut rest = svg.as_str();
365 while let Some(i) = rest.find("<rect ") {
366 let end = i + rest[i..].find('>').unwrap();
367 let tag = &rest[i..end];
368 if tag.contains("data-geom=\"histogram\"") {
369 let attr = |a: &str| -> f64 {
370 let k = tag.find(&format!(" {a}=\"")).unwrap() + a.len() + 3;
371 tag[k..k + tag[k..].find('"').unwrap()].parse().unwrap()
372 };
373 let x = attr("x");
374 spans.push((x, x + attr("width")));
375 }
376 rest = &rest[end + 1..];
377 }
378 for i in 0..spans.len() {
380 for j in (i + 1)..spans.len() {
381 let (a, b) = (spans[i], spans[j]);
382 let overlap = a.0 < b.1 - 0.01 && b.0 < a.1 - 0.01;
383 assert!(!overlap, "bars {a:?} and {b:?} overlap after dodge2");
384 }
385 }
386 }
387}