chrono_probe/
plot.rs

1//! # Plot
2//!
3//! This module contains the functions for plotting the results of the measurements.
4//! This is done using the [`time_plot`] function which this module provides.
5//!
6//! The [`time_plot`] function takes as inputs:
7//! * A path to save the plot to
8//! * a [`Measurements`] struct, which contains the results of the measurements
9//! * a [`PlotConfig`] struct, which contains the configuration for the plot
10//!
11//! The [`PlotConfig`] struct can be created using the builder pattern, configurable option are:
12//! * [`PlotConfig::with_x_label`]: Sets the x label for the plot.
13//! * [`PlotConfig::with_y_label`]: Sets the y label for the plot.
14//! * [`PlotConfig::with_title`]: Sets the title for the plot.
15//! * [`PlotConfig::with_caption`]: Sets the caption for the plot.
16//! * [`PlotConfig::with_scale`]: Sets the scale for the plot.
17
18use std::fmt::{Debug, Formatter};
19use std::time::Duration;
20
21use plotters::prelude::*;
22
23use crate::measurements::{Measurements, Point};
24
25/// Configuration for plotting.
26///
27pub struct PlotConfig<'a> {
28    title: &'a str,
29    caption: &'a str,
30    x_label: &'a str,
31    y_label: &'a str,
32    scale: Scale,
33}
34
35/// The scale of the plot.
36pub enum Scale {
37    /// Linear scale
38    Linear,
39    /// Double logarithmic scale
40    LogLog,
41}
42
43impl<'a> PlotConfig<'a> {
44    /// Crate a new [`PlotConfig`].
45    ///
46    /// Prefer using [`PlotConfig::default`] and then setting the desired values.
47    pub fn new(
48        title: &'a str,
49        caption: &'a str,
50        x_label: &'a str,
51        y_label: &'a str,
52        scale: Scale,
53    ) -> PlotConfig<'a> {
54        PlotConfig {
55            title,
56            caption,
57            x_label,
58            y_label,
59            scale,
60        }
61    }
62
63    /// Sets the x label for the plot.
64    pub fn with_x_label(mut self, x_label: &'a str) -> PlotConfig<'a> {
65        self.x_label = x_label;
66        self
67    }
68
69    /// Sets the y label for the plot.
70    pub fn with_y_label(mut self, y_label: &'a str) -> PlotConfig<'a> {
71        self.y_label = y_label;
72        self
73    }
74
75    /// Sets the title for the plot.
76    pub fn with_title(mut self, title: &'a str) -> PlotConfig<'a> {
77        self.title = title;
78        self
79    }
80
81    /// Sets the caption for the plot.
82    pub fn with_caption(mut self, caption: &'a str) -> PlotConfig<'a> {
83        self.caption = caption;
84        self
85    }
86
87    /// Sets the scale for the plot.
88    pub fn with_scale(mut self, scale: Scale) -> PlotConfig<'a> {
89        self.scale = scale;
90        self
91    }
92}
93
94impl<'a> Default for PlotConfig<'a> {
95    fn default() -> PlotConfig<'a> {
96        PlotConfig::new(
97            "Measurements plot",
98            "Caption",
99            "Size",
100            "Time",
101            Scale::Linear,
102        )
103    }
104}
105
106enum Precision {
107    Nanoseconds,
108    Microseconds,
109    Milliseconds,
110    Seconds,
111}
112
113impl Precision {
114    const MAX_U32: u128 = u32::MAX as u128;
115
116    fn get_precision_u32(duration: Duration) -> Self {
117        if duration.as_nanos() < Self::MAX_U32 {
118            Precision::Nanoseconds
119        } else if duration.as_micros() < Self::MAX_U32 {
120            Precision::Microseconds
121        } else if duration.as_millis() < Self::MAX_U32 {
122            Precision::Milliseconds
123        } else {
124            Precision::Seconds
125        }
126    }
127
128    fn as_u32(&self, duration: Duration) -> u32 {
129        match self {
130            Precision::Nanoseconds => duration.as_nanos() as u32,
131            Precision::Microseconds => duration.as_micros() as u32,
132            Precision::Milliseconds => duration.as_millis() as u32,
133            Precision::Seconds => duration.as_secs() as u32,
134        }
135    }
136}
137
138impl Debug for Precision {
139    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
140        match self {
141            Precision::Nanoseconds => write!(f, "ns"),
142            Precision::Microseconds => write!(f, "μs"),
143            Precision::Milliseconds => write!(f, "ms"),
144            Precision::Seconds => write!(f, "s"),
145        }
146    }
147}
148
149/// Plots the data from the [`Measurements`] using [plotters].
150/// The plot is saved to the file specified by `file_name`, the file created will be an SVG file.
151///
152/// # Arguments
153///
154/// * `file_name` - The name of the file to save the plot to
155/// * `measurements` - The measurements to plot
156/// * `builder` - The builder that was used to generate the measurements
157///
158pub fn time_plot(file_name: &str, measurements: Measurements, config: &PlotConfig) {
159    let x_min = measurements.min_length() as u32;
160    let x_max = measurements.max_length() as u32;
161
162    let max_time = measurements.max_time();
163    let y_precision = Precision::get_precision_u32(max_time);
164    let y_min = y_precision.as_u32(measurements.min_time());
165    let y_max = y_precision.as_u32(max_time);
166
167    let mut measurements = measurements.measurements;
168
169    // plot setup
170    let root = SVGBackend::new(file_name, (1024, 768)).into_drawing_area();
171    root.fill(&WHITE).unwrap();
172
173    let (upper, lower) = root.split_vertically(750);
174
175    lower
176        .titled(
177            config.title,
178            ("sans-serif", 10).into_font().color(&BLACK.mix(0.5)),
179        )
180        .unwrap();
181
182    let caption = config.caption.to_string();
183
184    let mut binding = ChartBuilder::on(&upper);
185
186    let chart_builder = binding
187        .caption(&caption, ("sans-serif", (5).percent_height()))
188        .set_label_area_size(LabelAreaPosition::Left, (8).percent())
189        .set_label_area_size(LabelAreaPosition::Bottom, (4).percent())
190        .margin((1).percent());
191
192    match config.scale {
193        Scale::Linear => {
194            let mut chart = chart_builder
195                .build_cartesian_2d(x_min..x_max, y_min..y_max)
196                .unwrap();
197            chart
198                .configure_mesh()
199                .x_desc(config.x_label)
200                .y_desc(format!("{} ({:?})", config.x_label, y_precision))
201                .draw()
202                .unwrap();
203
204            // draw data for each algorithm
205            for (i, measurement) in measurements.iter_mut().enumerate() {
206                measurement.measurement.sort_by_key(|a| a.size);
207
208                let color = Palette99::pick(i).mix(0.9);
209                chart
210                    .draw_series(LineSeries::new(
211                        measurement
212                            .measurement
213                            .iter()
214                            .map(|&Point { size, time, .. }| {
215                                (size as u32, y_precision.as_u32(time))
216                            }),
217                        color.stroke_width(3),
218                    ))
219                    .unwrap()
220                    .label(&measurement.algorithm_name)
221                    .legend(move |(x, y)| {
222                        Rectangle::new([(x, y - 5), (x + 10, y + 5)], color.filled())
223                    });
224            }
225
226            chart
227                .configure_series_labels()
228                .border_style(BLACK)
229                .draw()
230                .unwrap();
231        }
232        Scale::LogLog => {
233            let mut chart = chart_builder
234                .build_cartesian_2d((x_min..x_max).log_scale(), (y_min..y_max).log_scale())
235                .unwrap();
236            chart
237                .configure_mesh()
238                .x_desc(config.x_label)
239                .y_desc(format!("{} ({:?})", config.x_label, y_precision))
240                .draw()
241                .unwrap();
242
243            // draw data for each algorithm
244            for (i, measurement) in measurements.iter_mut().enumerate() {
245                measurement.measurement.sort_by_key(|a| a.size);
246
247                let color = Palette99::pick(i).mix(0.9);
248                chart
249                    .draw_series(LineSeries::new(
250                        measurement
251                            .measurement
252                            .iter()
253                            .map(|&Point { size, time, .. }| {
254                                (size as u32, y_precision.as_u32(time))
255                            }),
256                        color.stroke_width(3),
257                    ))
258                    .unwrap()
259                    .label(&measurement.algorithm_name)
260                    .legend(move |(x, y)| {
261                        Rectangle::new([(x, y - 5), (x + 10, y + 5)], color.filled())
262                    });
263            }
264
265            chart
266                .configure_series_labels()
267                .border_style(BLACK)
268                .draw()
269                .unwrap();
270        }
271    };
272
273    // To avoid the IO failure being ignored silently, we manually call the present function
274    root.present().expect(
275        "Unable to write result to file, please make sure 'results' dir exists under current dir",
276    );
277    println!("Result has been saved to {file_name}");
278}