textplots/
lib.rs

1//! Terminal plotting library for using in CLI applications.
2//! Should work well in any unicode terminal with monospaced font.
3//!
4//! It is inspired by [TextPlots.jl](https://github.com/sunetos/TextPlots.jl) which is inspired by [Drawille](https://github.com/asciimoo/drawille).
5//!
6//! Currently it features only drawing line plots on Braille canvas, but could be extended
7//! to support other canvas and chart types just like [UnicodePlots.jl](https://github.com/Evizero/UnicodePlots.jl)
8//! or any other cool terminal plotting library.
9//!
10//! Contributions are very much welcome!
11//!
12//! # Usage
13//! ```toml
14//! [dependencies]
15//! textplots = "0.8"
16//! ```
17//!
18//! ```rust
19//! use textplots::{Chart, Plot, Shape};
20//!
21//! println!("y = sin(x) / x");
22//!
23//! Chart::default()
24//!     .lineplot(&Shape::Continuous(Box::new(|x| x.sin() / x)))
25//!     .display();
26//! ```
27//!
28//! It will display something like this:
29//!
30//! <img src="https://github.com/loony-bean/textplots-rs/blob/master/doc/demo.png?raw=true"/>
31//!
32//! Default viewport size is 120 x 60 points, with X values ranging from -10 to 10.
33//! You can override the defaults calling `new`.
34//!
35//! ```rust
36//! use textplots::{Chart, Plot, Shape};
37//!
38//! println!("y = cos(x), y = sin(x) / 2");
39//!
40//! Chart::new(180, 60, -5.0, 5.0)
41//!     .lineplot(&Shape::Continuous(Box::new(|x| x.cos())))
42//!     .lineplot(&Shape::Continuous(Box::new(|x| x.sin() / 2.0)))
43//!     .display();
44//! ```
45//!
46//! <img src="https://github.com/loony-bean/textplots-rs/blob/master/doc/demo2.png?raw=true"/>
47//!
48//! You could also plot series of points. See [Shape](enum.Shape.html) and [examples](https://github.com/loony-bean/textplots-rs/tree/master/examples) for more details.
49//!
50//! <img src="https://github.com/loony-bean/textplots-rs/blob/master/doc/demo3.png?raw=true"/>
51
52pub mod scale;
53pub mod utils;
54
55use drawille::Canvas as BrailleCanvas;
56use drawille::PixelColor;
57use rgb::RGB8;
58use scale::Scale;
59use std::cmp;
60use std::default::Default;
61use std::f32;
62use std::fmt::{Display, Formatter, Result};
63
64/// How the chart will do the ranging on axes
65#[derive(PartialEq)]
66enum ChartRangeMethod {
67    /// Automatically ranges based on input data
68    AutoRange,
69    /// Has a fixed range between the given min & max
70    FixedRange,
71}
72
73/// Controls the drawing.
74pub struct Chart<'a> {
75    /// Canvas width in points.
76    width: u32,
77    /// Canvas height in points.
78    height: u32,
79    /// X-axis start value.
80    xmin: f32,
81    /// X-axis end value.
82    xmax: f32,
83    /// Y-axis start value (potentially calculated automatically).
84    ymin: f32,
85    /// Y-axis end value (potentially calculated automatically).
86    ymax: f32,
87    /// The type of y axis ranging we'll do
88    y_ranging: ChartRangeMethod,
89    /// Collection of shapes to be presented on the canvas.
90    shapes: Vec<(&'a Shape<'a>, Option<RGB8>)>,
91    /// Underlying canvas object.
92    canvas: BrailleCanvas,
93    /// X-axis style.
94    x_style: LineStyle,
95    /// Y-axis style.
96    y_style: LineStyle,
97    /// X-axis label format.
98    x_label_format: LabelFormat,
99    /// Y-axis label format.
100    y_label_format: LabelFormat,
101    /// Y-axis tick label density
102    y_tick_display: TickDisplay,
103}
104
105/// Specifies different kinds of plotted data.
106pub enum Shape<'a> {
107    /// Real value function.
108    Continuous(Box<dyn Fn(f32) -> f32 + 'a>),
109    /// Points of a scatter plot.
110    Points(&'a [(f32, f32)]),
111    /// Points connected with lines.
112    Lines(&'a [(f32, f32)]),
113    /// Points connected in step fashion.
114    Steps(&'a [(f32, f32)]),
115    /// Points represented with bars.
116    Bars(&'a [(f32, f32)]),
117}
118
119/// Provides an interface for drawing plots.
120pub trait Plot<'a> {
121    /// Draws a [line chart](https://en.wikipedia.org/wiki/Line_chart) of points connected by straight line segments.
122    fn lineplot(&'a mut self, shape: &'a Shape) -> &'a mut Chart;
123}
124
125/// Provides an interface for drawing colored plots.
126pub trait ColorPlot<'a> {
127    /// Draws a [line chart](https://en.wikipedia.org/wiki/Line_chart) of points connected by straight line segments using the specified color
128    fn linecolorplot(&'a mut self, shape: &'a Shape, color: RGB8) -> &'a mut Chart;
129}
130
131/// Provides a builder interface for styling axis.
132pub trait AxisBuilder<'a> {
133    /// Specifies the style of x-axis.
134    fn x_axis_style(&'a mut self, style: LineStyle) -> &'a mut Chart<'a>;
135
136    /// Specifies the style of y-axis.
137    fn y_axis_style(&'a mut self, style: LineStyle) -> &'a mut Chart<'a>;
138}
139
140pub trait LabelBuilder<'a> {
141    /// Specifies the label format of x-axis.
142    fn x_label_format(&'a mut self, format: LabelFormat) -> &'a mut Chart<'a>;
143
144    /// Specifies the label format of y-axis.
145    fn y_label_format(&'a mut self, format: LabelFormat) -> &'a mut Chart<'a>;
146}
147
148/// Provides an interface for adding tick labels to the y-axis
149pub trait TickDisplayBuilder<'a> {
150    // Horizontal labels don't allow for support of x-axis tick labels
151    /// Specifies the tick label density of y-axis.
152    /// TickDisplay::Sparse will change the canvas height to the nearest multiple of 16
153    /// TickDisplay::Dense will change the canvas height to the nearest multiple of 8
154    fn y_tick_display(&'a mut self, density: TickDisplay) -> &'a mut Chart<'a>;
155}
156
157impl<'a> Default for Chart<'a> {
158    fn default() -> Self {
159        Self::new(120, 60, -10.0, 10.0)
160    }
161}
162
163/// Specifies line style.
164/// Default value is `LineStyle::Dotted`.
165#[derive(Clone, Copy)]
166pub enum LineStyle {
167    /// Line is not displayed.
168    None,
169    /// Line is solid  (⠤⠤⠤).
170    Solid,
171    /// Line is dotted (⠄⠠⠀).
172    Dotted,
173    /// Line is dashed (⠤⠀⠤).
174    Dashed,
175}
176
177/// Specifies label format.
178/// Default value is `LabelFormat::Value`.
179pub enum LabelFormat {
180    /// Label is not displayed.
181    None,
182    /// Label is shown as a value.
183    Value,
184    /// Label is shown as a custom string.
185    Custom(Box<dyn Fn(f32) -> String>),
186}
187
188/// Specifies density of labels on the Y axis between ymin and ymax.
189/// Default value is `TickDisplay::None`.
190pub enum TickDisplay {
191    /// Tick labels are not displayed.
192    None,
193    /// Tick labels are sparsely shown (every 4th row)
194    Sparse,
195    /// Tick labels are densely shown (every 2nd row)
196    Dense,
197}
198
199impl TickDisplay {
200    fn get_row_spacing(&self) -> u32 {
201        match self {
202            TickDisplay::None => u32::MAX, // Unused
203            TickDisplay::Sparse => 4,
204            TickDisplay::Dense => 2,
205        }
206    }
207}
208
209impl<'a> Display for Chart<'a> {
210    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
211        // get frame and replace space with U+2800 (BRAILLE PATTERN BLANK)
212        let mut frame = self.canvas.frame().replace(' ', "\u{2800}");
213
214        if let Some(idx) = frame.find('\n') {
215            let xmin = self.format_x_axis_tick(self.xmin);
216            let xmax = self.format_x_axis_tick(self.xmax);
217
218            frame.insert_str(idx, &format!(" {0}", self.format_y_axis_tick(self.ymax)));
219
220            // Display y-axis ticks if requested
221            match self.y_tick_display {
222                TickDisplay::None => {}
223                TickDisplay::Sparse | TickDisplay::Dense => {
224                    let row_spacing: u32 = self.y_tick_display.get_row_spacing(); // Rows between ticks
225                    let num_steps: u32 = (self.height / 4) / row_spacing; // 4 dots per row of text
226                    let step_size = (self.ymax - self.ymin) / (num_steps) as f32;
227                    for i in 1..(num_steps) {
228                        if let Some(index) = frame
229                            .match_indices('\n')
230                            .collect::<Vec<(usize, &str)>>()
231                            .get((i * row_spacing) as usize)
232                        {
233                            frame.insert_str(
234                                index.0,
235                                &format!(
236                                    " {0}",
237                                    self.format_y_axis_tick(self.ymax - (step_size * i as f32))
238                                ),
239                            );
240                        }
241                    }
242                }
243            }
244
245            frame.push_str(&format!(
246                " {0}\n{1: <width$}{2}\n",
247                self.format_y_axis_tick(self.ymin),
248                xmin,
249                xmax,
250                width = (self.width as usize) / 2 - xmax.len()
251            ));
252        }
253        write!(f, "{}", frame)
254    }
255}
256
257impl<'a> Chart<'a> {
258    /// Creates a new `Chart` object.
259    ///
260    /// # Panics
261    ///
262    /// Panics if `width` is less than 32 or `height` is less than 3.
263    pub fn new(width: u32, height: u32, xmin: f32, xmax: f32) -> Self {
264        if width < 32 {
265            panic!("width should be at least 32");
266        }
267
268        if height < 3 {
269            panic!("height should be at least 3");
270        }
271
272        Self {
273            xmin,
274            xmax,
275            ymin: f32::INFINITY,
276            ymax: f32::NEG_INFINITY,
277            y_ranging: ChartRangeMethod::AutoRange,
278            width,
279            height,
280            shapes: Vec::new(),
281            canvas: BrailleCanvas::new(width, height),
282            x_style: LineStyle::Dotted,
283            y_style: LineStyle::Dotted,
284            x_label_format: LabelFormat::Value,
285            y_label_format: LabelFormat::Value,
286            y_tick_display: TickDisplay::None,
287        }
288    }
289
290    /// Creates a new `Chart` object with fixed y axis range.
291    ///
292    /// # Panics
293    ///
294    /// Panics if `width` is less than 32 or `height` is less than 3.
295    pub fn new_with_y_range(
296        width: u32,
297        height: u32,
298        xmin: f32,
299        xmax: f32,
300        ymin: f32,
301        ymax: f32,
302    ) -> Self {
303        if width < 32 {
304            panic!("width should be at least 32");
305        }
306
307        if height < 3 {
308            panic!("height should be at least 3");
309        }
310
311        Self {
312            xmin,
313            xmax,
314            ymin,
315            ymax,
316            y_ranging: ChartRangeMethod::FixedRange,
317            width,
318            height,
319            shapes: Vec::new(),
320            canvas: BrailleCanvas::new(width, height),
321            x_style: LineStyle::Dotted,
322            y_style: LineStyle::Dotted,
323            x_label_format: LabelFormat::Value,
324            y_label_format: LabelFormat::Value,
325            y_tick_display: TickDisplay::None,
326        }
327    }
328
329    /// Displays bounding rect.
330    pub fn borders(&mut self) {
331        let w = self.width;
332        let h = self.height;
333
334        self.vline(0, LineStyle::Dotted);
335        self.vline(w, LineStyle::Dotted);
336        self.hline(0, LineStyle::Dotted);
337        self.hline(h, LineStyle::Dotted);
338    }
339
340    /// Draws vertical line of the specified style.
341    fn vline(&mut self, i: u32, mode: LineStyle) {
342        match mode {
343            LineStyle::None => {}
344            LineStyle::Solid => {
345                if i <= self.width {
346                    for j in 0..=self.height {
347                        self.canvas.set(i, j);
348                    }
349                }
350            }
351            LineStyle::Dotted => {
352                if i <= self.width {
353                    for j in 0..=self.height {
354                        if j % 3 == 0 {
355                            self.canvas.set(i, j);
356                        }
357                    }
358                }
359            }
360            LineStyle::Dashed => {
361                if i <= self.width {
362                    for j in 0..=self.height {
363                        if j % 4 == 0 {
364                            self.canvas.set(i, j);
365                            self.canvas.set(i, j + 1);
366                        }
367                    }
368                }
369            }
370        }
371    }
372
373    /// Draws horizontal line of the specified style.
374    fn hline(&mut self, j: u32, mode: LineStyle) {
375        match mode {
376            LineStyle::None => {}
377            LineStyle::Solid => {
378                if j <= self.height {
379                    for i in 0..=self.width {
380                        self.canvas.set(i, self.height - j);
381                    }
382                }
383            }
384            LineStyle::Dotted => {
385                if j <= self.height {
386                    for i in 0..=self.width {
387                        if i % 3 == 0 {
388                            self.canvas.set(i, self.height - j);
389                        }
390                    }
391                }
392            }
393            LineStyle::Dashed => {
394                if j <= self.height {
395                    for i in 0..=self.width {
396                        if i % 4 == 0 {
397                            self.canvas.set(i, self.height - j);
398                            self.canvas.set(i + 1, self.height - j);
399                        }
400                    }
401                }
402            }
403        }
404    }
405
406    /// Prints canvas content.
407    pub fn display(&mut self) {
408        self.axis();
409        self.figures();
410
411        println!("{}", self);
412    }
413
414    /// Prints canvas content with some additional visual elements (like borders).
415    pub fn nice(&mut self) {
416        self.borders();
417        self.display();
418    }
419
420    /// Shows axis.
421    pub fn axis(&mut self) {
422        self.x_axis();
423        self.y_axis();
424    }
425
426    /// Shows x-axis.
427    pub fn x_axis(&mut self) {
428        let y_scale = Scale::new(self.ymin..self.ymax, 0.0..self.height as f32);
429
430        if self.ymin <= 0.0 && self.ymax >= 0.0 {
431            self.hline(y_scale.linear(0.0) as u32, self.x_style);
432        }
433    }
434
435    /// Shows y-axis.
436    pub fn y_axis(&mut self) {
437        let x_scale = Scale::new(self.xmin..self.xmax, 0.0..self.width as f32);
438
439        if self.xmin <= 0.0 && self.xmax >= 0.0 {
440            self.vline(x_scale.linear(0.0) as u32, self.y_style);
441        }
442    }
443
444    /// Performs formatting of the x axis.
445    fn format_x_axis_tick(&self, value: f32) -> String {
446        match &self.x_label_format {
447            LabelFormat::None => "".to_owned(),
448            LabelFormat::Value => format!("{:.1}", value),
449            LabelFormat::Custom(f) => f(value),
450        }
451    }
452
453    /// Performs formatting of the y axis.
454    fn format_y_axis_tick(&self, value: f32) -> String {
455        match &self.y_label_format {
456            LabelFormat::None => "".to_owned(),
457            LabelFormat::Value => format!("{:.1}", value),
458            LabelFormat::Custom(f) => f(value),
459        }
460    }
461
462    // Shows figures.
463    pub fn figures(&mut self) {
464        for (shape, color) in &self.shapes {
465            let x_scale = Scale::new(self.xmin..self.xmax, 0.0..self.width as f32);
466            let y_scale = Scale::new(self.ymin..self.ymax, 0.0..self.height as f32);
467
468            // translate (x, y) points into screen coordinates
469            let points: Vec<_> = match shape {
470                Shape::Continuous(f) => (0..self.width)
471                    .filter_map(|i| {
472                        let x = x_scale.inv_linear(i as f32);
473                        let y = f(x);
474                        if y.is_normal() {
475                            let j = y_scale.linear(y).round();
476                            Some((i, self.height - j as u32))
477                        } else {
478                            None
479                        }
480                    })
481                    .collect(),
482                Shape::Points(dt) | Shape::Lines(dt) | Shape::Steps(dt) | Shape::Bars(dt) => dt
483                    .iter()
484                    .filter_map(|(x, y)| {
485                        let i = x_scale.linear(*x).round() as u32;
486                        let j = y_scale.linear(*y).round() as u32;
487                        if i <= self.width && j <= self.height {
488                            Some((i, self.height - j))
489                        } else {
490                            None
491                        }
492                    })
493                    .collect(),
494            };
495
496            // display segments
497            match shape {
498                Shape::Continuous(_) | Shape::Lines(_) => {
499                    for pair in points.windows(2) {
500                        let (x1, y1) = pair[0];
501                        let (x2, y2) = pair[1];
502                        if let Some(color) = color {
503                            let color = rgb_to_pixelcolor(color);
504                            self.canvas.line_colored(x1, y1, x2, y2, color);
505                        } else {
506                            self.canvas.line(x1, y1, x2, y2);
507                        }
508                    }
509                }
510                Shape::Points(_) => {
511                    for (x, y) in points {
512                        if let Some(color) = color {
513                            let color = rgb_to_pixelcolor(color);
514                            self.canvas.set_colored(x, y, color);
515                        } else {
516                            self.canvas.set(x, y);
517                        }
518                    }
519                }
520                Shape::Steps(_) => {
521                    for pair in points.windows(2) {
522                        let (x1, y1) = pair[0];
523                        let (x2, y2) = pair[1];
524
525                        if let Some(color) = color {
526                            let color = rgb_to_pixelcolor(color);
527                            self.canvas.line_colored(x1, y2, x2, y2, color);
528                            self.canvas.line_colored(x1, y1, x1, y2, color);
529                        } else {
530                            self.canvas.line(x1, y2, x2, y2);
531                            self.canvas.line(x1, y1, x1, y2);
532                        }
533                    }
534                }
535                Shape::Bars(_) => {
536                    for pair in points.windows(2) {
537                        let (x1, y1) = pair[0];
538                        let (x2, y2) = pair[1];
539
540                        if let Some(color) = color {
541                            let color = rgb_to_pixelcolor(color);
542                            self.canvas.line_colored(x1, y2, x2, y2, color);
543                            self.canvas.line_colored(x1, y1, x1, y2, color);
544                            self.canvas.line_colored(x1, self.height, x1, y1, color);
545                            self.canvas.line_colored(x2, self.height, x2, y2, color);
546                        } else {
547                            self.canvas.line(x1, y2, x2, y2);
548                            self.canvas.line(x1, y1, x1, y2);
549                            self.canvas.line(x1, self.height, x1, y1);
550                            self.canvas.line(x2, self.height, x2, y2);
551                        }
552                    }
553                }
554            }
555        }
556    }
557
558    /// Returns the frame.
559    pub fn frame(&self) -> String {
560        self.canvas.frame()
561    }
562
563    fn rescale(&mut self, shape: &Shape) {
564        // rescale ymin and ymax
565        let x_scale = Scale::new(self.xmin..self.xmax, 0.0..self.width as f32);
566
567        let ys: Vec<_> = match shape {
568            Shape::Continuous(f) => (0..self.width)
569                .filter_map(|i| {
570                    let x = x_scale.inv_linear(i as f32);
571                    let y = f(x);
572                    if y.is_normal() {
573                        Some(y)
574                    } else {
575                        None
576                    }
577                })
578                .collect(),
579            Shape::Points(dt) | Shape::Lines(dt) | Shape::Steps(dt) | Shape::Bars(dt) => dt
580                .iter()
581                .filter_map(|(x, y)| {
582                    if *x >= self.xmin && *x <= self.xmax {
583                        Some(*y)
584                    } else {
585                        None
586                    }
587                })
588                .collect(),
589        };
590
591        let ymax = *ys
592            .iter()
593            .max_by(|x, y| x.partial_cmp(y).unwrap_or(cmp::Ordering::Equal))
594            .unwrap_or(&0.0);
595        let ymin = *ys
596            .iter()
597            .min_by(|x, y| x.partial_cmp(y).unwrap_or(cmp::Ordering::Equal))
598            .unwrap_or(&0.0);
599
600        self.ymin = f32::min(self.ymin, ymin);
601        self.ymax = f32::max(self.ymax, ymax);
602    }
603}
604
605impl<'a> ColorPlot<'a> for Chart<'a> {
606    fn linecolorplot(&'a mut self, shape: &'a Shape, color: RGB8) -> &'a mut Chart {
607        self.shapes.push((shape, Some(color)));
608        if self.y_ranging == ChartRangeMethod::AutoRange {
609            self.rescale(shape);
610        }
611        self
612    }
613}
614
615impl<'a> Plot<'a> for Chart<'a> {
616    fn lineplot(&'a mut self, shape: &'a Shape) -> &'a mut Chart {
617        self.shapes.push((shape, None));
618        if self.y_ranging == ChartRangeMethod::AutoRange {
619            self.rescale(shape);
620        }
621        self
622    }
623}
624
625fn rgb_to_pixelcolor(rgb: &RGB8) -> PixelColor {
626    PixelColor::TrueColor {
627        r: rgb.r,
628        g: rgb.g,
629        b: rgb.b,
630    }
631}
632
633impl<'a> AxisBuilder<'a> for Chart<'a> {
634    fn x_axis_style(&'a mut self, style: LineStyle) -> &'a mut Chart {
635        self.x_style = style;
636        self
637    }
638
639    fn y_axis_style(&'a mut self, style: LineStyle) -> &'a mut Chart {
640        self.y_style = style;
641        self
642    }
643}
644
645impl<'a> LabelBuilder<'a> for Chart<'a> {
646    /// Specifies a formater for the x-axis label.
647    fn x_label_format(&mut self, format: LabelFormat) -> &mut Self {
648        self.x_label_format = format;
649        self
650    }
651
652    /// Specifies a formater for the y-axis label.
653    fn y_label_format(&mut self, format: LabelFormat) -> &mut Self {
654        self.y_label_format = format;
655        self
656    }
657}
658
659impl<'a> TickDisplayBuilder<'a> for Chart<'a> {
660    /// Specifies the density of y-axis tick labels
661    fn y_tick_display(&mut self, density: TickDisplay) -> &mut Self {
662        // Round the canvas height to the nearest multiple using integer division
663        match density {
664            TickDisplay::None => {}
665            TickDisplay::Sparse => {
666                // Round to the nearest 16
667                self.height = if self.height < 16 {
668                    16
669                } else {
670                    ((self.height + 8) / 16) * 16
671                }
672            }
673            TickDisplay::Dense => {
674                // Round to the nearest 8
675                self.height = if self.height < 8 {
676                    8
677                } else {
678                    ((self.height + 4) / 8) * 8
679                }
680            }
681        }
682        self.y_tick_display = density;
683        self
684    }
685}