| 1 | use std::collections::{hash_map::IntoIter as HashMapIter, HashMap}; |
| 2 | use std::marker::PhantomData; |
| 3 | use std::ops::AddAssign; |
| 4 | |
| 5 | use crate::chart::ChartContext; |
| 6 | use crate::coord::cartesian::Cartesian2d; |
| 7 | use crate::coord::ranged1d::{DiscreteRanged, Ranged}; |
| 8 | use crate::element::Rectangle; |
| 9 | use crate::style::{Color, ShapeStyle, GREEN}; |
| 10 | use plotters_backend::DrawingBackend; |
| 11 | |
| 12 | pub trait HistogramType {} |
| 13 | pub struct Vertical; |
| 14 | pub struct Horizontal; |
| 15 | |
| 16 | impl HistogramType for Vertical {} |
| 17 | impl HistogramType for Horizontal {} |
| 18 | |
| 19 | /** |
| 20 | Presents data in a histogram. Input data can be raw or aggregated. |
| 21 | |
| 22 | # Examples |
| 23 | |
| 24 | ``` |
| 25 | use plotters::prelude::*; |
| 26 | let data = [1, 1, 2, 2, 1, 3, 3, 2, 2, 1, 1, 2, 2, 2, 3, 3, 1, 2, 3]; |
| 27 | let drawing_area = SVGBackend::new("histogram_vertical.svg" , (300, 200)).into_drawing_area(); |
| 28 | drawing_area.fill(&WHITE).unwrap(); |
| 29 | let mut chart_builder = ChartBuilder::on(&drawing_area); |
| 30 | chart_builder.margin(5).set_left_and_bottom_label_area_size(20); |
| 31 | let mut chart_context = chart_builder.build_cartesian_2d((1..3).into_segmented(), 0..9).unwrap(); |
| 32 | chart_context.configure_mesh().draw().unwrap(); |
| 33 | chart_context.draw_series(Histogram::vertical(&chart_context).style(BLUE.filled()).margin(10) |
| 34 | .data(data.map(|x| (x, 1)))).unwrap(); |
| 35 | ``` |
| 36 | |
| 37 | The result is a histogram counting the occurrences of 1, 2, and 3 in `data`: |
| 38 | |
| 39 |  |
| 40 | |
| 41 | Here is a variation with [`Histogram::horizontal()`], replacing `(1..3).into_segmented(), 0..9` with |
| 42 | `0..9, (1..3).into_segmented()`: |
| 43 | |
| 44 |  |
| 45 | |
| 46 | The spacing between histogram bars is adjusted with [`Histogram::margin()`]. |
| 47 | Here is a version of the figure where `.margin(10)` has been replaced by `.margin(20)`; |
| 48 | the resulting bars are narrow and more spaced: |
| 49 | |
| 50 |  |
| 51 | |
| 52 | [`crate::coord::ranged1d::IntoSegmentedCoord::into_segmented()`] is useful for discrete data; it makes sure the histogram bars |
| 53 | are centered on each data value. Here is another variation with `(1..3).into_segmented()` |
| 54 | replaced by `1..4`: |
| 55 | |
| 56 |  |
| 57 | |
| 58 | [`Histogram::style()`] sets the style of the bars. Here is a histogram without `.filled()`: |
| 59 | |
| 60 |  |
| 61 | |
| 62 | The following version uses [`Histogram::style_func()`] for finer control. Let's replace `.style(BLUE.filled())` with |
| 63 | `.style_func(|x, _bar_height| if let SegmentValue::Exact(v) = x {[BLACK, RED, GREEN, BLUE][*v as usize].filled()} else {BLACK.filled()})`. |
| 64 | The resulting bars come in different colors: |
| 65 | |
| 66 |  |
| 67 | |
| 68 | [`Histogram::baseline()`] adjusts the base of the bars. The following figure adds `.baseline(1)` |
| 69 | to the right of `.margin(10)`. The lower portion of the bars are removed: |
| 70 | |
| 71 |  |
| 72 | |
| 73 | The following figure uses [`Histogram::baseline_func()`] for finer control. Let's add |
| 74 | `.baseline_func(|x| if let SegmentValue::Exact(v) = x {*v as i32} else {0})` |
| 75 | to the right of `.margin(10)`. The lower portion of the bars are removed; the removed portion is taller |
| 76 | to the right: |
| 77 | |
| 78 |  |
| 79 | */ |
| 80 | pub struct Histogram<'a, BR, A, Tag = Vertical> |
| 81 | where |
| 82 | BR: DiscreteRanged, |
| 83 | A: AddAssign<A> + Default, |
| 84 | Tag: HistogramType, |
| 85 | { |
| 86 | style: Box<dyn Fn(&BR::ValueType, &A) -> ShapeStyle + 'a>, |
| 87 | margin: u32, |
| 88 | iter: HashMapIter<usize, A>, |
| 89 | baseline: Box<dyn Fn(&BR::ValueType) -> A + 'a>, |
| 90 | br: BR, |
| 91 | _p: PhantomData<Tag>, |
| 92 | } |
| 93 | |
| 94 | impl<'a, BR, A, Tag> Histogram<'a, BR, A, Tag> |
| 95 | where |
| 96 | BR: DiscreteRanged + Clone, |
| 97 | A: AddAssign<A> + Default + 'a, |
| 98 | Tag: HistogramType, |
| 99 | { |
| 100 | fn empty(br: &BR) -> Self { |
| 101 | Self { |
| 102 | style: Box::new(|_, _| GREEN.filled()), |
| 103 | margin: 5, |
| 104 | iter: HashMap::new().into_iter(), |
| 105 | baseline: Box::new(|_| A::default()), |
| 106 | br: br.clone(), |
| 107 | _p: PhantomData, |
| 108 | } |
| 109 | } |
| 110 | /** |
| 111 | Sets the style of the histogram bars. |
| 112 | |
| 113 | See [`Histogram`] for more information and examples. |
| 114 | */ |
| 115 | pub fn style<S: Into<ShapeStyle>>(mut self, style: S) -> Self { |
| 116 | let style = style.into(); |
| 117 | self.style = Box::new(move |_, _| style); |
| 118 | self |
| 119 | } |
| 120 | |
| 121 | /** |
| 122 | Sets the style of histogram using a closure. |
| 123 | |
| 124 | The closure takes the position of the bar in guest coordinates as argument. |
| 125 | The argument may need some processing if the data range has been transformed by |
| 126 | [`crate::coord::ranged1d::IntoSegmentedCoord::into_segmented()`] as shown in the [`Histogram`] example. |
| 127 | */ |
| 128 | pub fn style_func( |
| 129 | mut self, |
| 130 | style_func: impl Fn(&BR::ValueType, &A) -> ShapeStyle + 'a, |
| 131 | ) -> Self { |
| 132 | self.style = Box::new(style_func); |
| 133 | self |
| 134 | } |
| 135 | |
| 136 | /** |
| 137 | Sets the baseline of the histogram. |
| 138 | |
| 139 | See [`Histogram`] for more information and examples. |
| 140 | */ |
| 141 | pub fn baseline(mut self, baseline: A) -> Self |
| 142 | where |
| 143 | A: Clone, |
| 144 | { |
| 145 | self.baseline = Box::new(move |_| baseline.clone()); |
| 146 | self |
| 147 | } |
| 148 | |
| 149 | /** |
| 150 | Sets the histogram bar baselines using a closure. |
| 151 | |
| 152 | The closure takes the bar position and height as argument. |
| 153 | The argument may need some processing if the data range has been transformed by |
| 154 | [`crate::coord::ranged1d::IntoSegmentedCoord::into_segmented()`] as shown in the [`Histogram`] example. |
| 155 | */ |
| 156 | pub fn baseline_func(mut self, func: impl Fn(&BR::ValueType) -> A + 'a) -> Self { |
| 157 | self.baseline = Box::new(func); |
| 158 | self |
| 159 | } |
| 160 | |
| 161 | /** |
| 162 | Sets the margin for each bar, in backend pixels. |
| 163 | |
| 164 | See [`Histogram`] for more information and examples. |
| 165 | */ |
| 166 | pub fn margin(mut self, value: u32) -> Self { |
| 167 | self.margin = value; |
| 168 | self |
| 169 | } |
| 170 | |
| 171 | /** |
| 172 | Specifies the input data for the histogram through an appropriate data iterator. |
| 173 | |
| 174 | See [`Histogram`] for more information and examples. |
| 175 | */ |
| 176 | pub fn data<TB: Into<BR::ValueType>, I: IntoIterator<Item = (TB, A)>>( |
| 177 | mut self, |
| 178 | iter: I, |
| 179 | ) -> Self { |
| 180 | let mut buffer = HashMap::<usize, A>::new(); |
| 181 | for (x, y) in iter.into_iter() { |
| 182 | if let Some(x) = self.br.index_of(&x.into()) { |
| 183 | *buffer.entry(x).or_insert_with(Default::default) += y; |
| 184 | } |
| 185 | } |
| 186 | self.iter = buffer.into_iter(); |
| 187 | self |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | impl<'a, BR, A> Histogram<'a, BR, A, Vertical> |
| 192 | where |
| 193 | BR: DiscreteRanged + Clone, |
| 194 | A: AddAssign<A> + Default + 'a, |
| 195 | { |
| 196 | /** |
| 197 | Creates a vertical histogram. |
| 198 | |
| 199 | See [`Histogram`] for more information and examples. |
| 200 | */ |
| 201 | pub fn vertical<ACoord, DB: DrawingBackend + 'a>( |
| 202 | parent: &ChartContext<DB, Cartesian2d<BR, ACoord>>, |
| 203 | ) -> Self |
| 204 | where |
| 205 | ACoord: Ranged<ValueType = A>, |
| 206 | { |
| 207 | let dp = parent.as_coord_spec().x_spec(); |
| 208 | |
| 209 | Self::empty(dp) |
| 210 | } |
| 211 | } |
| 212 | |
| 213 | impl<'a, BR, A> Histogram<'a, BR, A, Horizontal> |
| 214 | where |
| 215 | BR: DiscreteRanged + Clone, |
| 216 | A: AddAssign<A> + Default + 'a, |
| 217 | { |
| 218 | /** |
| 219 | Creates a horizontal histogram. |
| 220 | |
| 221 | See [`Histogram`] for more information and examples. |
| 222 | */ |
| 223 | pub fn horizontal<ACoord, DB: DrawingBackend>( |
| 224 | parent: &ChartContext<DB, Cartesian2d<ACoord, BR>>, |
| 225 | ) -> Self |
| 226 | where |
| 227 | ACoord: Ranged<ValueType = A>, |
| 228 | { |
| 229 | let dp = parent.as_coord_spec().y_spec(); |
| 230 | Self::empty(dp) |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | impl<'a, BR, A> Iterator for Histogram<'a, BR, A, Vertical> |
| 235 | where |
| 236 | BR: DiscreteRanged, |
| 237 | A: AddAssign<A> + Default, |
| 238 | { |
| 239 | type Item = Rectangle<(BR::ValueType, A)>; |
| 240 | fn next(&mut self) -> Option<Self::Item> { |
| 241 | while let Some((x, y)) = self.iter.next() { |
| 242 | if let Some((x, Some(nx))) = self |
| 243 | .br |
| 244 | .from_index(x) |
| 245 | .map(|v| (v, self.br.from_index(x + 1))) |
| 246 | { |
| 247 | let base = (self.baseline)(&x); |
| 248 | let style = (self.style)(&x, &y); |
| 249 | let mut rect = Rectangle::new([(x, y), (nx, base)], style); |
| 250 | rect.set_margin(0, 0, self.margin, self.margin); |
| 251 | return Some(rect); |
| 252 | } |
| 253 | } |
| 254 | None |
| 255 | } |
| 256 | } |
| 257 | |
| 258 | impl<'a, BR, A> Iterator for Histogram<'a, BR, A, Horizontal> |
| 259 | where |
| 260 | BR: DiscreteRanged, |
| 261 | A: AddAssign<A> + Default, |
| 262 | { |
| 263 | type Item = Rectangle<(A, BR::ValueType)>; |
| 264 | fn next(&mut self) -> Option<Self::Item> { |
| 265 | while let Some((y, x)) = self.iter.next() { |
| 266 | if let Some((y, Some(ny))) = self |
| 267 | .br |
| 268 | .from_index(y) |
| 269 | .map(|v| (v, self.br.from_index(y + 1))) |
| 270 | { |
| 271 | let base = (self.baseline)(&y); |
| 272 | let style = (self.style)(&y, &x); |
| 273 | let mut rect = Rectangle::new([(x, y), (base, ny)], style); |
| 274 | rect.set_margin(self.margin, self.margin, 0, 0); |
| 275 | return Some(rect); |
| 276 | } |
| 277 | } |
| 278 | None |
| 279 | } |
| 280 | } |
| 281 | |