criterion/plot/plotters_backend/
pdf.rs

1use super::*;
2use crate::measurement::ValueFormatter;
3use crate::report::{BenchmarkId, ComparisonData, MeasurementData, ReportContext};
4use plotters::data;
5use plotters::style::RGBAColor;
6use std::path::Path;
7
8pub(crate) fn pdf_comparison_figure(
9    path: &Path,
10    title: Option<&str>,
11    formatter: &dyn ValueFormatter,
12    measurements: &MeasurementData<'_>,
13    comparison: &ComparisonData,
14    size: Option<(u32, u32)>,
15) {
16    let base_avg_times = Sample::new(&comparison.base_avg_times);
17    let typical = base_avg_times.max().max(measurements.avg_times.max());
18    let mut scaled_base_avg_times: Vec<f64> = comparison.base_avg_times.clone();
19    let unit = formatter.scale_values(typical, &mut scaled_base_avg_times);
20    let scaled_base_avg_times = Sample::new(&scaled_base_avg_times);
21
22    let mut scaled_new_avg_times: Vec<f64> = (&measurements.avg_times as &Sample<f64>)
23        .iter()
24        .cloned()
25        .collect();
26    let _ = formatter.scale_values(typical, &mut scaled_new_avg_times);
27    let scaled_new_avg_times = Sample::new(&scaled_new_avg_times);
28
29    let base_mean = scaled_base_avg_times.mean();
30    let new_mean = scaled_new_avg_times.mean();
31
32    let (base_xs, base_ys, base_y_mean) =
33        kde::sweep_and_estimate(scaled_base_avg_times, KDE_POINTS, None, base_mean);
34    let (xs, ys, y_mean) =
35        kde::sweep_and_estimate(scaled_new_avg_times, KDE_POINTS, None, new_mean);
36
37    let x_range = data::fitting_range(base_xs.iter().chain(xs.iter()));
38    let y_range = data::fitting_range(base_ys.iter().chain(ys.iter()));
39
40    let size = size.unwrap_or(SIZE);
41    let root_area = SVGBackend::new(&path, (size.0, size.1)).into_drawing_area();
42
43    let mut cb = ChartBuilder::on(&root_area);
44
45    if let Some(title) = title {
46        cb.caption(title, (DEFAULT_FONT, 20));
47    }
48
49    let mut chart = cb
50        .margin((5).percent())
51        .set_label_area_size(LabelAreaPosition::Left, (5).percent_width().min(60))
52        .set_label_area_size(LabelAreaPosition::Bottom, (5).percent_height().min(40))
53        .build_cartesian_2d(x_range, y_range.clone())
54        .unwrap();
55
56    chart
57        .configure_mesh()
58        .disable_mesh()
59        .y_desc("Density (a.u.)")
60        .x_desc(format!("Average Time ({})", unit))
61        .x_label_formatter(&|&x| pretty_print_float(x, true))
62        .y_label_formatter(&|&y| pretty_print_float(y, true))
63        .x_labels(5)
64        .draw()
65        .unwrap();
66
67    chart
68        .draw_series(AreaSeries::new(
69            base_xs.iter().zip(base_ys.iter()).map(|(x, y)| (*x, *y)),
70            y_range.start,
71            DARK_RED.mix(0.5).filled(),
72        ))
73        .unwrap()
74        .label("Base PDF")
75        .legend(|(x, y)| Rectangle::new([(x, y - 5), (x + 20, y + 5)], DARK_RED.mix(0.5).filled()));
76
77    chart
78        .draw_series(AreaSeries::new(
79            xs.iter().zip(ys.iter()).map(|(x, y)| (*x, *y)),
80            y_range.start,
81            DARK_BLUE.mix(0.5).filled(),
82        ))
83        .unwrap()
84        .label("New PDF")
85        .legend(|(x, y)| {
86            Rectangle::new([(x, y - 5), (x + 20, y + 5)], DARK_BLUE.mix(0.5).filled())
87        });
88
89    chart
90        .draw_series(std::iter::once(PathElement::new(
91            vec![(base_mean, 0.0), (base_mean, base_y_mean)],
92            DARK_RED.filled().stroke_width(2),
93        )))
94        .unwrap()
95        .label("Base Mean")
96        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], DARK_RED));
97
98    chart
99        .draw_series(std::iter::once(PathElement::new(
100            vec![(new_mean, 0.0), (new_mean, y_mean)],
101            DARK_BLUE.filled().stroke_width(2),
102        )))
103        .unwrap()
104        .label("New Mean")
105        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], DARK_BLUE));
106
107    if title.is_some() {
108        chart.configure_series_labels().draw().unwrap();
109    }
110}
111
112pub(crate) fn pdf_small(
113    id: &BenchmarkId,
114    context: &ReportContext,
115    formatter: &dyn ValueFormatter,
116    measurements: &MeasurementData<'_>,
117    size: Option<(u32, u32)>,
118) {
119    let avg_times = &*measurements.avg_times;
120    let typical = avg_times.max();
121    let mut scaled_avg_times: Vec<f64> = (avg_times as &Sample<f64>).iter().cloned().collect();
122    let unit = formatter.scale_values(typical, &mut scaled_avg_times);
123    let scaled_avg_times = Sample::new(&scaled_avg_times);
124    let mean = scaled_avg_times.mean();
125
126    let (xs, ys, mean_y) = kde::sweep_and_estimate(scaled_avg_times, KDE_POINTS, None, mean);
127    let xs_ = Sample::new(&xs);
128    let ys_ = Sample::new(&ys);
129
130    let y_limit = ys_.max() * 1.1;
131
132    let path = context.report_path(id, "pdf_small.svg");
133
134    let size = size.unwrap_or(SIZE);
135    let root_area = SVGBackend::new(&path, (size.0, size.1)).into_drawing_area();
136
137    let mut chart = ChartBuilder::on(&root_area)
138        .margin((5).percent())
139        .set_label_area_size(LabelAreaPosition::Left, (5).percent_width().min(60))
140        .set_label_area_size(LabelAreaPosition::Bottom, (5).percent_height().min(40))
141        .build_cartesian_2d(xs_.min()..xs_.max(), 0.0..y_limit)
142        .unwrap();
143
144    chart
145        .configure_mesh()
146        .disable_mesh()
147        .y_desc("Density (a.u.)")
148        .x_desc(format!("Average Time ({})", unit))
149        .x_label_formatter(&|&x| pretty_print_float(x, true))
150        .y_label_formatter(&|&y| pretty_print_float(y, true))
151        .x_labels(5)
152        .draw()
153        .unwrap();
154
155    chart
156        .draw_series(AreaSeries::new(
157            xs.iter().zip(ys.iter()).map(|(x, y)| (*x, *y)),
158            0.0,
159            DARK_BLUE.mix(0.25).filled(),
160        ))
161        .unwrap();
162
163    chart
164        .draw_series(std::iter::once(PathElement::new(
165            vec![(mean, 0.0), (mean, mean_y)],
166            DARK_BLUE.filled().stroke_width(2),
167        )))
168        .unwrap();
169}
170
171pub(crate) fn pdf(
172    id: &BenchmarkId,
173    context: &ReportContext,
174    formatter: &dyn ValueFormatter,
175    measurements: &MeasurementData<'_>,
176    size: Option<(u32, u32)>,
177) {
178    let avg_times = &measurements.avg_times;
179    let typical = avg_times.max();
180    let mut scaled_avg_times: Vec<f64> = (avg_times as &Sample<f64>).iter().cloned().collect();
181    let unit = formatter.scale_values(typical, &mut scaled_avg_times);
182    let scaled_avg_times = Sample::new(&scaled_avg_times);
183
184    let mean = scaled_avg_times.mean();
185
186    let iter_counts = measurements.iter_counts();
187    let &max_iters = iter_counts
188        .iter()
189        .max_by_key(|&&iters| iters as u64)
190        .unwrap();
191    let exponent = (max_iters.log10() / 3.).floor() as i32 * 3;
192    let y_scale = 10f64.powi(-exponent);
193
194    let y_label = if exponent == 0 {
195        "Iterations".to_owned()
196    } else {
197        format!("Iterations (x 10^{})", exponent)
198    };
199
200    let (xs, ys) = kde::sweep(scaled_avg_times, KDE_POINTS, None);
201    let (lost, lomt, himt, hist) = avg_times.fences();
202    let mut fences = [lost, lomt, himt, hist];
203    let _ = formatter.scale_values(typical, &mut fences);
204    let [lost, lomt, himt, hist] = fences;
205
206    let path = context.report_path(id, "pdf.svg");
207
208    let xs_ = Sample::new(&xs);
209
210    let size = size.unwrap_or(SIZE);
211    let root_area = SVGBackend::new(&path, (size.0, size.1)).into_drawing_area();
212
213    let range = data::fitting_range(ys.iter());
214
215    let mut chart = ChartBuilder::on(&root_area)
216        .margin((5).percent())
217        .caption(id.as_title(), (DEFAULT_FONT, 20))
218        .set_label_area_size(LabelAreaPosition::Left, (5).percent_width().min(60))
219        .set_label_area_size(LabelAreaPosition::Right, (5).percent_width().min(60))
220        .set_label_area_size(LabelAreaPosition::Bottom, (5).percent_height().min(40))
221        .build_cartesian_2d(xs_.min()..xs_.max(), 0.0..max_iters)
222        .unwrap()
223        .set_secondary_coord(xs_.min()..xs_.max(), 0.0..range.end);
224
225    chart
226        .configure_mesh()
227        .disable_mesh()
228        .y_desc(y_label)
229        .x_desc(format!("Average Time ({})", unit))
230        .x_label_formatter(&|&x| pretty_print_float(x, true))
231        .y_label_formatter(&|&y| pretty_print_float(y * y_scale, true))
232        .draw()
233        .unwrap();
234
235    chart
236        .configure_secondary_axes()
237        .y_desc("Density (a.u.)")
238        .x_label_formatter(&|&x| pretty_print_float(x, true))
239        .y_label_formatter(&|&y| pretty_print_float(y, true))
240        .draw()
241        .unwrap();
242
243    chart
244        .draw_secondary_series(AreaSeries::new(
245            xs.iter().zip(ys.iter()).map(|(x, y)| (*x, *y)),
246            0.0,
247            DARK_BLUE.mix(0.5).filled(),
248        ))
249        .unwrap()
250        .label("PDF")
251        .legend(|(x, y)| {
252            Rectangle::new([(x, y - 5), (x + 20, y + 5)], DARK_BLUE.mix(0.5).filled())
253        });
254
255    chart
256        .draw_series(std::iter::once(PathElement::new(
257            vec![(mean, 0.0), (mean, max_iters)],
258            DARK_BLUE,
259        )))
260        .unwrap()
261        .label("Mean")
262        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], DARK_BLUE));
263
264    chart
265        .draw_series(vec![
266            PathElement::new(vec![(lomt, 0.0), (lomt, max_iters)], DARK_ORANGE),
267            PathElement::new(vec![(himt, 0.0), (himt, max_iters)], DARK_ORANGE),
268            PathElement::new(vec![(lost, 0.0), (lost, max_iters)], DARK_RED),
269            PathElement::new(vec![(hist, 0.0), (hist, max_iters)], DARK_RED),
270        ])
271        .unwrap();
272    use crate::stats::univariate::outliers::tukey::Label;
273
274    let mut draw_data_point_series =
275        |filter: &dyn Fn(&Label) -> bool, color: RGBAColor, name: &str| {
276            chart
277                .draw_series(
278                    avg_times
279                        .iter()
280                        .zip(scaled_avg_times.iter())
281                        .zip(iter_counts.iter())
282                        .filter_map(|(((_, label), t), i)| {
283                            if filter(&label) {
284                                Some(Circle::new((*t, *i), POINT_SIZE, color.filled()))
285                            } else {
286                                None
287                            }
288                        }),
289                )
290                .unwrap()
291                .label(name)
292                .legend(move |(x, y)| Circle::new((x + 10, y), POINT_SIZE, color.filled()));
293        };
294
295    draw_data_point_series(
296        &|l| !l.is_outlier(),
297        DARK_BLUE.to_rgba(),
298        "\"Clean\" sample",
299    );
300    draw_data_point_series(
301        &|l| l.is_mild(),
302        RGBColor(255, 127, 0).to_rgba(),
303        "Mild outliers",
304    );
305    draw_data_point_series(&|l| l.is_severe(), DARK_RED.to_rgba(), "Severe outliers");
306    chart.configure_series_labels().draw().unwrap();
307}