cgrustplot/plots/
function_plot.rs

1//! # Function Plot
2//! Displays a graph of the given function.
3//! 
4//! # Functions
5//! 
6//! * `function_plot` - Generates a FunctionPlotBuilder from a function.
7//! * `as_float_func` - Creates a float-valued function (Fn(f64) -> f64) from a generalal numerical function.
8//! 
9
10use num::{FromPrimitive, ToPrimitive};
11
12use crate::helper::{
13    axes::add_opt_axes_and_opt_titles,
14    charset::{line_chars::*, NULL_CHR},
15    func_plot_domain::determine_plot_domain,
16    mat_plot_lib::pyplot,
17    math::{max_always, min_always, pad_range, subdivide},
18    file::save_to_file,
19    rendering::RenderableTextBuilder,
20};
21
22/// Builder for a Function Plot
23/// Set various options for plotting the function.
24/// 
25/// # Options
26///  
27/// * `func` - Input function.
28/// * `domain` - Specified domain to plot the function over. Default is computed.
29/// * `range` - Specified range to display the function over. Default is computed.
30/// * `domain_padding` - Proportion of the width of the domain to be padded with. Default is 0.1.
31/// * `range_padding` - Proportion of the height of the range to be padded with. Default is 0.1.
32/// * `size` - Dimensions (in characters) of the outputted plot. Default is (60, 10).
33/// * `title` - Optional title for the plot. Default is None.
34/// * `axes` - Whether or not to display axes and axes labels. Default is true.
35/// 
36/// # Notes
37/// 
38/// Use `.precompute()` to generate and save values to minimize future function calls when plotting.
39/// 
40#[derive(Clone)]
41pub struct FuncPlotBuilder<'a> {
42    func: Box<&'a dyn Fn(f64) -> f64>,
43    domain: Option<(f64, f64)>,
44    range: Option<(f64, f64)>,
45    domain_padding: Option<f64>,
46    range_padding: Option<f64>,
47    size: Option<(u32, u32)>,
48    title: Option<&'a str>,
49    axes: Option<bool>,
50    precomputed: Option<Vec<(f64, f64)>>,
51}
52
53/// Internal struct representing built values.
54struct FuncPlot<'a> {
55    func: Box<&'a dyn Fn(f64) -> f64>,
56    domain_and_range: ((f64, f64), (f64, f64)),
57    size: (u32, u32),
58    title: Option<&'a str>,
59    axes: bool,
60    precomputed: &'a Option<Vec<(f64, f64)>>
61}
62
63impl<'a> FuncPlotBuilder<'a> {
64    /// Create an array plot from a table of data.
65    fn from<'b: 'a>(func: &'b impl Fn(f64) -> f64) -> Self {
66        FuncPlotBuilder {
67            func: Box::new(func),
68            domain: None,
69            range: None,
70            domain_padding: None,
71            range_padding: None,
72            size: None,
73            title: None,
74            axes: None,
75            precomputed: None,
76        }
77    }
78
79    pub fn set_domain(&mut self, domain: (f64, f64)) -> &mut Self {
80        self.domain = Some(domain);
81        self
82    }
83
84    pub fn set_range(&mut self, range: (f64, f64)) -> &mut Self {
85        self.range = Some(range);
86        self
87    }
88
89    pub fn set_domain_padding(&mut self, padding: f64) -> &mut Self {
90        self.domain_padding = Some(padding);
91        self
92    }
93    
94    pub fn set_range_padding(&mut self, padding: f64) -> &mut Self {
95        self.range_padding = Some(padding);
96        self
97    }
98
99    pub fn set_size(&mut self, size: (u32, u32)) -> &mut Self {
100        self.size = Some(size);
101        self
102    }
103
104    pub fn set_title<'b : 'a>(&mut self, title: &'b str) -> &mut Self {
105        self.title = Some(title);
106        self
107    }
108
109    pub fn set_axes(&mut self, do_axes: bool) -> &mut Self {
110        self.axes = Some(do_axes);
111        self
112    }
113
114    pub fn enable_precomputation(&mut self) -> &mut Self {
115        self.precomputed = Some(vec![]);
116        self
117    }
118
119    /// Generate values before other computations so that f is called as few times as possible.
120    /// 
121    /// `resolution` parameter sets the number of datapoints precompute is allowed to generate.
122    /// The higher the resolution, the more detail will be in the output graph.
123    pub fn precompute(&mut self, resolution: u32) {
124        if self.precomputed.is_some() {
125            assert!(self.domain.is_some());
126
127            self.precomputed = Some(
128                subdivide(self.domain.unwrap().0, self.domain.unwrap().1, resolution)
129                    .into_iter()
130                    .map(|x| (x, (self.func)(x)))
131                    .collect::<Vec<(f64, f64)>>()
132            );
133        }
134    }
135
136    fn determine_range(&self, resolution: u32, domain: (f64, f64)) -> (f64, f64) {
137        let y_vals: Vec<f64>;
138        
139        match &self.precomputed {
140            Some(vals) => {
141                y_vals = vals
142                    .iter()
143                    .map(|p| p.1)
144                    .collect();
145            }
146            None => {
147                y_vals = subdivide(domain.0, domain.1, resolution)
148                    .into_iter()
149                    .map(|i| (self.func)(i))
150                    .collect();
151            }
152        }
153
154        (min_always(&y_vals,0.), max_always(&y_vals,0.))
155    }
156    
157    // It is reccomended to precompute for expensive functions before building
158    fn build(&self) -> FuncPlot {
159        let size = self.size.unwrap_or((60, 10));
160        let resolution = size.0;
161
162        let domain = self.domain.unwrap_or_else(|| determine_plot_domain(&*self.func));
163        let range = self.range.unwrap_or_else(|| self.determine_range(resolution, domain));
164
165        // With padding
166        let domain = pad_range(domain, self.domain_padding.unwrap_or(0.1));
167        let range = pad_range(range, self.range_padding.unwrap_or(0.1));
168        
169        FuncPlot {
170            func: self.func.clone(),
171            domain_and_range: (domain, range),
172            size: size,
173            title: self.title,
174            axes: self.axes.unwrap_or(true),
175            precomputed: &self.precomputed,
176        }
177    }
178
179    /// Returns the plotted data as a string
180    pub fn as_string(&self) -> String {
181        self.build().as_string()
182    }
183
184    /// Displays the plotted data with println
185    pub fn print(&self) {
186        self.build().print();
187    }
188
189    /// Saves the text content of a plot to a file
190    pub fn save(&self, path: &str) {
191        save_to_file(&self.build().as_string(), path);
192    }
193
194    /// Returns a rendered text builder to render a string
195    pub fn as_image(&self) -> RenderableTextBuilder {
196        RenderableTextBuilder::from(self.build().as_string())
197    }
198
199    /// Displays the plot's data using pyplot
200    pub fn pyplot(&self) {
201        self.build().pyplot(None);
202    }
203
204    /// Saves the plot's data using pyplot
205    pub fn save_pyplot(&self, path: &str) {
206        self.build().pyplot(Some(path));
207    }
208
209    /// Returns the unformatted text content of a plot
210    #[allow(dead_code)]
211    pub(crate) fn plot(&self) -> String {
212        self.build().plot()
213    }
214}
215
216impl<'a> FuncPlot<'a> {
217    fn plot(&self) -> String {
218        use rayon::prelude::*;
219
220        // charachters per unit
221        let cpux = self.size.0 as f64 / (self.domain_and_range.0.1 - self.domain_and_range.0.0);
222        let cpuy = self.size.1 as f64 / (self.domain_and_range.1.1 - self.domain_and_range.1.0);
223        let ctux = |c: i32| self.domain_and_range.0.0 + (c as f64 + 0.5) / cpux;
224        let utcy = |u: f64| ((self.domain_and_range.1.1 - u) * cpuy - 0.5) as i32;
225
226        // xc_vals includes one extra padding value on each side for derivative checks
227        let xc_vals: Vec<i32> = (-1..(1 + self.size.0 as i32)).collect();
228        let xu_vals: Vec<f64> = xc_vals.iter().map(|xc| ctux(*xc)).collect();
229        let yu_vals: Vec<f64> = xu_vals.iter().map(|xu| (self.func)(*xu)).collect();
230        let yc_vals: Vec<i32> = yu_vals.iter().map(|yu| utcy(*yu)).collect();
231
232        let mut o = (0..self.size.1).map(|_| (0..self.size.0).map(|_| ' ').collect::<Vec<char>>()).collect::<Vec<Vec<char>>>();
233
234        let mut set_o_char = |x: i32, y: i32, c: char| if 0 <= x && x < self.size.0 as i32 && 0 <= y && y < self.size.1 as i32 {o[y as usize][x as usize] = c};
235
236        for i in 0..self.size.0 as i32 {
237            let xc = xc_vals[(i + 1) as usize];
238            let (ycl, yc, ycr) = (yc_vals[i as usize], yc_vals[(i + 1) as usize], yc_vals[(i + 2) as usize]);
239
240            let rycl = yc - ycl;
241            let rycr = yc - ycr;
242
243            // Vertical Lines
244            let lowest_surrounding = std::cmp::min(rycl, rycr);
245            if lowest_surrounding < -1 {
246                for char_height_diff in (lowest_surrounding + 1)..0 {
247                    set_o_char(xc, yc - char_height_diff, VERTICAL);
248                }
249            }
250
251            // Match for Continuous lines
252            let chr =
253            match (rycl.clamp(-1, 1), rycr.clamp(-1, 1)) {
254                (-1, -1) => FLAT_LOW,
255                (0, 0) => FLAT_MED,
256                (1, 1) => FLAT_HIGH,
257
258                (-1, 1) => UP_TWO,
259                (1, -1) => DOWN_TWO,
260
261                (-1, 0) => FLAT_LOW,
262                (0, 1) => FLAT_HIGH,
263                (0, -1) => FLAT_LOW,
264                (1, 0) => FLAT_HIGH,
265
266                (_, _) => NULL_CHR,
267            };
268
269            set_o_char(xc, yc, chr);
270        }
271
272        o.into_par_iter().map(|l| l.into_iter().collect::<String>()).collect::<Vec<String>>().join("\n")
273    }
274
275    fn as_string(&self) -> String {
276        add_opt_axes_and_opt_titles(&self.plot(), self.domain_and_range, self.axes, self.title)
277    }
278
279    fn print(&self) {
280        println!("{}", self.as_string());
281    }
282
283    fn pyplot(&self, path: Option<&str>) {
284        let x_vals: Vec<f64>;
285        let y_vals: Vec<f64>;
286
287        match self.precomputed {
288            Some(vals) => {
289                x_vals = vals.iter().map(|p| p.0).collect();
290                y_vals = vals.iter().map(|p| p.1).collect();
291            }
292            None => {
293                x_vals = subdivide(self.domain_and_range.0.0, self.domain_and_range.0.1, 10 * self.size.0);
294                y_vals = x_vals.iter().map(|x| (self.func)(*x)).collect();
295            }
296        }
297
298        let command = format!("plot({x_vals:?}, {y_vals:?})");
299        pyplot(&command, self.title, Some(self.axes), Some(self.domain_and_range), path);
300    }
301}
302
303/// Displays a graph of the given function.
304/// 
305/// The domain to plot can be set within the builder, or a
306/// domain will be generated automatically.
307/// 
308/// # Example
309/// 
310/// ```
311/// use cgrustplot::plots::function_plot::function_plot;
312/// 
313/// let f = |x: f64| x * x * (x - 3.);
314/// function_plot(&f).print();
315/// 
316/// // Standard Output:
317/// //       │              _――――――_                                 /    
318/// // -0.32 ┼            _‾        ‾―_                             /     
319/// //       │          _‾             ‾_                          /      
320/// // -1.28 ┼         /                 ‾_                       /       
321/// //       │        /                    ‾_                    /        
322/// // -2.24 ┼       /                       ‾_                 /         
323/// //       │      /                          ‾_             _‾          
324/// // -3.20 ┼     /                             ‾―_        _‾            
325/// //       │     |                                ‾――――――‾              
326/// // -4.16 ┼    /                                                       
327/// //       └┼──────┼──────┼──────┼──────┼──────┼──────┼──────┼──────────
328/// //        -1.360 -0.800 -0.240 0.3200 0.8800 1.4400 2.0000 2.5600     
329/// ```
330/// 
331/// # Options
332///  
333/// * `func` - Input function.
334/// * `domain` - Specified domain to plot the function over. Default is computed.
335/// * `range` - Specified range to display the function over. Default is computed.
336/// * `domain_padding` - Proportion of the width of the domain to be padded with. Default is 0.1.
337/// * `range_padding` - Proportion of the height of the range to be padded with. Default is 0.1.
338/// * `size` - Dimensions (in characters) of the outputted plot. Default is (60, 10).
339/// * `title` - Optional title for the plot. Default is None.
340/// * `axes` - Whether or not to display axes and axes labels. Default is true.
341/// 
342/// # Notes
343/// 
344/// Use `.precompute()` to generate and save values to minimize future function calls when plotting.
345/// 
346pub fn function_plot<'a>(func: &'a impl Fn(f64) -> f64) -> FuncPlotBuilder<'a> {
347    FuncPlotBuilder::from(func)
348}
349
350/// Converts a numerical function `func` to a `Fn(f64) -> f64`
351pub fn as_float_function<'a, U, V>(func: impl Fn(U) -> V) -> impl Fn(f64) -> f64
352where
353    U: FromPrimitive,
354    V: ToPrimitive,
355{
356    move |x: f64| func(U::from_f64(x).unwrap()).to_f64().unwrap()
357}