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 | ![](https://cdn.jsdelivr.net/gh/facorread/plotters-doc-data@a617d37/apidoc/histogram_vertical.svg) |
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 | ![](https://cdn.jsdelivr.net/gh/facorread/plotters-doc-data@a617d37/apidoc/histogram_horizontal.svg) |
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 | ![](https://cdn.jsdelivr.net/gh/facorread/plotters-doc-data@a617d37/apidoc/histogram_margin20.svg) |
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 | ![](https://cdn.jsdelivr.net/gh/facorread/plotters-doc-data@a617d37/apidoc/histogram_not_segmented.svg) |
57 | |
58 | [`Histogram::style()`] sets the style of the bars. Here is a histogram without `.filled()`: |
59 | |
60 | ![](https://cdn.jsdelivr.net/gh/facorread/plotters-doc-data@a617d37/apidoc/histogram_hollow.svg) |
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 | ![](https://cdn.jsdelivr.net/gh/facorread/plotters-doc-data@a617d37/apidoc/histogram_style_func.svg) |
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 | ![](https://cdn.jsdelivr.net/gh/facorread/plotters-doc-data@a617d37/apidoc/histogram_baseline.svg) |
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 | ![](https://cdn.jsdelivr.net/gh/facorread/plotters-doc-data@a617d37/apidoc/histogram_baseline_func.svg) |
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 | |