Skip to main content

cgrustplot/plots/
scatter_plot.rs

1//! # Scatter Plot
2//! Displays scatter plot of a list of given points.
3//! 
4//! # Functions
5//! 
6//! * `scatter_plot` - Generates a RegionPlotBuilder from a predicate.
7//! * `list_as_points` - Enumerates a list to generate 2D points. (e.g. [8, 3, 4, 6] -> [(0, 8), (1, 3), (2, 4), (3, 6)]).
8//! 
9
10use num::ToPrimitive;
11use rayon::prelude::*;
12
13use crate::helper::{
14    arrays::{padded_vec_to, table_indices_to_counts, transpose_table},
15    axes::add_opt_axes_and_opt_titles,
16    charset::subdiv_chars::*,
17    mat_plot_lib::pyplot,
18    math::{bin_to_u8, ciel_div, max_always, min_always, pad_range},
19    file::save_to_file,
20    rendering::RenderableTextBuilder,
21};
22
23/// Pads a range by a ratio of it's width
24fn pad_point_range(points: &Vec<(f64, f64)>, padding: f64) -> ((f64, f64), (f64, f64)) {
25    (
26        pad_range((min_always(&points.iter().map(|i| i.0).collect(), 0.),
27            max_always(&points.iter().map(|i| i.0).collect(), 0.)), padding),
28        pad_range((min_always(&points.iter().map(|i| i.1).collect(), 0.),
29            max_always(&points.iter().map(|i| i.1).collect(), 0.)), padding)
30    )
31}
32
33pub(crate) fn padded_point_range<T: PartialOrd + Copy + ToPrimitive>(points: &Vec<(T, T)>, padding: f64) -> ((f64, f64), (f64, f64)) {
34    pad_point_range(
35        &points
36            .iter()
37            .map(|t|
38                (match t.0.to_f64() {Some(val) => val, None => 0.},
39                match t.1.to_f64() {Some(val) => val, None => 0.})
40            ).collect::<Vec<(f64, f64)>>(),
41        padding
42    )
43}
44
45pub(crate) fn determine_char_set<T: ToPrimitive + PartialEq>(points: &Vec<(T, T)>, range: ((f64, f64), (f64, f64)), size: (u32, u32)) -> (Vec<char>, (u32, u32)) {   
46    let pts: Vec<&(T, T)> = points.iter().filter(|i| i.0 == i.0 && i.1 == i.1).collect();
47
48    let v: Vec<f64> = table_indices_to_counts(&points, range, size).into_iter().flatten().map(|i| i as f64).collect();
49
50    let mean_v: f64 = v.iter().sum::<f64>() / v.len() as f64;
51    let max_v: f64 = max_always(&v, 0.);
52
53    if mean_v <= 1. && max_v * ciel_div(pts.len(), 20) as f64 <= 2. {
54        (dots_one_by_one(), (1, 1))
55    } else if mean_v <= 1.5 || max_v * ciel_div(pts.len(), 10) as f64 <= 4. {
56        (blocks_two_by_two(), (2, 2))
57    } else {
58        (dots_two_by_four(), (2, 4))
59    }
60}
61
62
63pub(crate) fn bool_arr_plot_string_custom_charset(arr: &Vec<Vec<bool>>, range: (u32, u32), charset: (Vec<char>, (u32, u32))) -> String {
64    // Dimensions of arr should be equal to (range.0, range.1)
65
66    let chrs = charset.0;
67    let chrsize = charset.1;
68    let x_size = ciel_div(range.0, chrsize.0);
69    let y_size = ciel_div(range.1, chrsize.1);
70
71    // Valid binary representing charachter set
72    assert_eq!(chrs.len() as u32, 1u32 << (chrsize.0 * chrsize.1));
73
74    (0..y_size).into_par_iter().map(|j|
75        (0..x_size).map(|i| {
76            // arr[y..yn][x..xn] defines the subarray for the character at (i, j)
77            let (x, y) = (chrsize.0 * i, chrsize.1 * j);
78            let (xn, yn) = (chrsize.0 * (i + 1), chrsize.1 * (j + 1));
79            
80            chrs[
81                // Determine the index of the charachter in chrs based on binary representation of points
82                bin_to_u8(
83                    // Transpose the subarray from (row, col) to (col, row), because charachters are stored in binary (col, row) order
84                transpose_table(
85                        // Padding the table to dimensions a multiple of the charset size
86                    &padded_vec_to(
87                            arr[y as usize..(yn as usize).clamp(0, arr.len())]
88                            .iter()
89                            .map(|row| padded_vec_to(
90                                row[x as usize..(xn as usize).clamp(0, row.len())].to_vec(),
91                                chrsize.0 as usize,
92                                false)
93                            )
94                            .collect::<Vec<Vec<bool>>>(),
95
96                            chrsize.1 as usize,
97                            vec![false; chrsize.0 as usize],
98                        )
99                    )
100                    // Flatten and extract into a single list of binary
101                    .into_iter()
102                    .flatten()
103                    .map(|i| *i)
104                    .collect::<Vec<bool>>()
105                ) as usize
106            ]
107        }).collect::<String>()
108    ).collect::<Vec<String>>()
109    .join("\n")
110}
111
112
113/// Builder for a Scatter Plot
114/// Set various options for plotting the points.
115/// 
116/// # Options
117/// 
118/// * `data` - Input points.
119/// * `domain_and_range` - Domain and range over which to plot the region. Default is computed.
120/// * `padding` - Proportion of domain and range to pad the plot with. Default is 0.1.
121/// * `size` - Dimensions (in characters) of the outputted plot. Default is (60, 30).
122/// * `title` - Optional title for the plot. Default is None.
123/// * `axes` - Whether or not to display axes and axes labels. Default is true.
124/// * `chars` - Charset to be used for plotting. Any set in `cgrustplot::helper::charset::subdiv_chars` works. Default is computed.
125/// 
126#[derive(Clone)]
127pub struct ScatterPlotBuilder<'a, T: PartialOrd + Copy + ToPrimitive + std::fmt::Debug> {
128    data: &'a Vec<(T, T)>,
129    domain_and_range: Option<((f64, f64), (f64, f64))>,
130    padding: Option<f64>,
131    size: Option<(u32, u32)>,
132    title: Option<&'a str>,
133    axes: Option<bool>,
134    chars: Option<(Vec<char>, (u32, u32))>,
135}
136
137/// Internal struct representing built values.
138struct ScatterPlot<'a, T: PartialOrd + Copy + ToPrimitive + std::fmt::Debug> {
139    data: &'a Vec<(T, T)>,
140    domain_and_range: ((f64, f64), (f64, f64)),
141    size: (u32, u32),
142    title: Option<&'a str>,
143    axes: bool,
144    chars: (Vec<char>, (u32, u32)),
145}
146
147impl<'a, T: PartialOrd + Copy + ToPrimitive + std::fmt::Debug> ScatterPlotBuilder<'a, T> {
148    /// Create an array plot from a table of data.
149    fn from<'b: 'a>(data: &'b Vec<(T, T)>) -> Self {
150        ScatterPlotBuilder {
151            data: data,
152            domain_and_range: None,
153            padding: None,
154            size: None,
155            title: None,
156            axes: None,
157            chars: None,
158        }
159    }
160
161    pub fn set_range(&mut self, range: ((f64, f64), (f64, f64))) -> &mut Self {
162        self.domain_and_range = Some(range);
163        self
164    }
165
166    pub fn set_padding(&mut self, padding: f64) -> &mut Self {
167        self.padding = Some(padding);
168        self
169    }
170
171    pub fn set_size(&mut self, size: (u32, u32)) -> &mut Self {
172        self.size = Some(size);
173        self
174    }
175
176    pub fn set_title<'b: 'a>(&mut self, title: &'b str) -> &mut Self {
177        self.title = Some(title);
178        self
179    }
180
181    pub fn set_axes(&mut self, do_axes: bool) -> &mut Self {
182        self.axes = Some(do_axes);
183        self
184    }
185
186    /// In addition to the chars, it also needs the dimensions of the charset.
187    /// If it's named "something_x_by_y", then set the dimensions to be (x, y).
188    /// 
189    /// e.g. dots_two_by_four should be input as .set_chars((dots_two_by_four(), (2, 4)))
190    pub fn set_chars(&mut self, chars: (Vec<char>, (u32, u32))) -> &mut Self {
191        self.chars = Some(chars);
192        self
193    }
194
195    fn build(&self) -> ScatterPlot<T> {
196        // Padding must go before range, as default arg for range is based on padding
197        let padding = self.padding.unwrap_or(0.1);
198        let domain_and_range = self.domain_and_range.unwrap_or_else(|| padded_point_range(&self.data, padding));
199        let size = self.size.unwrap_or((60, 30));
200        let chars = self.chars.clone().unwrap_or_else(|| determine_char_set(&self.data, domain_and_range, size));  // Cloned value is moved into built variant, so the clone would be needed anyway
201        
202        ScatterPlot {
203            data: self.data,
204            domain_and_range,
205            size: size,
206            title: self.title,
207            axes: self.axes.unwrap_or(true),
208            chars: chars,
209        }
210    }
211
212    /// Returns the plotted data as a string
213    pub fn as_string(&self) -> String {
214        self.build().as_string()
215    }
216
217    /// Displays the plotted data with println
218    pub fn print(&self) {
219        self.build().print();
220    }
221
222    /// Saves the text content of a plot to a file
223    pub fn save(&self, path: &str) {
224        save_to_file(&self.build().as_string(), path);
225    }
226
227    /// Returns a rendered text builder to render a string
228    pub fn as_image(&self) -> RenderableTextBuilder {
229        RenderableTextBuilder::from(self.build().as_string())
230    }
231
232    /// Displays the plot's data using pyplot
233    pub fn pyplot(&self) {
234        self.build().pyplot(None);
235    }
236
237    /// Saves the plot's data using pyplot
238    pub fn save_pyplot(&self, path: &str) {
239        self.build().pyplot(Some(path));
240    }
241
242    /// Returns the unformatted text content of a plot
243    #[allow(dead_code)]
244    pub(crate) fn plot(&self) -> String {
245        self.build().plot()
246    }
247
248}
249
250impl<'a, T: PartialOrd + Copy + ToPrimitive + std::fmt::Debug> ScatterPlot<'a, T> {
251    fn plot(&self) -> String {
252        let bool_arr: Vec<Vec<bool>> = table_indices_to_counts(&self.data, self.domain_and_range, (self.size.0 * self.chars.1.0, self.size.1 * self.chars.1.1))
253            .into_par_iter()
254            .map(|i| 
255                i.into_iter()
256                .map(|j| j != 0)
257                .collect()
258            ).collect();
259
260        bool_arr_plot_string_custom_charset(&bool_arr, (self.size.0 * self.chars.1.0, self.size.1 * self.chars.1.1), self.chars.clone())
261    }
262
263    fn as_string(&self) -> String {
264        add_opt_axes_and_opt_titles(&self.plot(), self.domain_and_range, self.axes, self.title)
265    }
266
267    fn print(&self) {
268        println!("{}", self.as_string());
269    }
270
271    fn pyplot(&self, path: Option<&str>) {
272        let x_data: Vec<T> = self.data.iter().map(|p| p.0).collect();
273        let y_data: Vec<T> = self.data.iter().map(|p| p.1).collect();
274        let command = format!("scatter({x_data:?}, {y_data:?})");
275
276        pyplot(&command, self.title, Some(self.axes), Some(self.domain_and_range), path);
277    }
278}
279
280/// Displays a 2D region which satisfies a given predicate.
281/// 
282/// # Example
283/// 
284/// ```
285/// use cgrustplot::plots::scatter_plot::scatter_plot;
286/// 
287/// let points = vec![(0., 0.), (1., 4.), (2., 8.), (1.2, 3.1)];
288/// scatter_plot(&points).set_size((30, 10)).print();
289/// 
290/// // Standard Output:
291/// //       │  ●                           
292/// // 7.360 ┼                              
293/// //       │                              
294/// // 5.440 ┼                              
295/// //       │              ●  ●            
296/// // 3.520 ┼                              
297/// //       │                              
298/// // 1.600 ┼                              
299/// //       │                              
300/// // -0.32 ┼                           ●  
301/// //       └┼──────┼──────┼──────┼────────
302/// //        -0.160 0.4000 0.9600 1.5200   
303/// ```
304/// 
305/// # Options
306/// 
307/// * `data` - Input points.
308/// * `domain_and_range` - Domain and range over which to plot the region. Default is computed.
309/// * `padding` - Proportion of domain and range to pad the plot with. Default is 0.1.
310/// * `size` - Dimensions (in characters) of the outputted plot. Default is (60, 30).
311/// * `title` - Optional title for the plot. Default is None.
312/// * `axes` - Whether or not to display axes and axes labels. Default is true.
313/// * `chars` - Charset to be used for plotting. Any set in `cgrustplot::helper::charset::subdiv_chars` works. Default is computed.
314/// 
315pub fn scatter_plot<'a, T: PartialOrd + Copy + ToPrimitive + std::fmt::Debug>(points: &'a Vec<(T, T)>) -> ScatterPlotBuilder<'a, T> {
316    ScatterPlotBuilder::from(points)
317}
318
319/// Enumerates a list to generate 2D points.
320/// 
321/// # Example
322/// ```
323/// use cgrustplot::plots::scatter_plot::list_as_points;
324/// 
325/// let list = vec![8, 3, 4, 6];
326/// assert_eq!(list_as_points(&list), vec![(0., 8.), (1., 3.), (2., 4.), (3., 6.)]);
327/// ``````
328pub fn list_as_points<T: ToPrimitive>(points: &Vec<T>) -> Vec<(f64, f64)> {
329    points.iter().enumerate().map(|(i, p)| (i as f64, p.to_f64().unwrap())).collect()
330}