1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
//! Terminal plotting library for using in CLI applications.
//! Should work well in any unicode terminal with monospaced font.
//!
//! It is inspired by [TextPlots.jl](https://github.com/sunetos/TextPlots.jl) which is inspired by [Drawille](https://github.com/asciimoo/drawille).
//!
//! Currently it features only drawing line plots on Braille canvas, but could be extended
//! to support other canvas and chart types just like [UnicodePlots.jl](https://github.com/Evizero/UnicodePlots.jl)
//! or any other cool terminal plotting library.
//!
//! Contributions are very much welcome!
//!
//! # Usage
//! ```toml
//! [dependencies]
//! textplots = "0.3"
//! ```
//!
//! ```rust
//! extern crate textplots;
//!
//! use textplots::{Chart, Plot, Shape};
//!
//! fn main() {
//! println!("y = sin(x) / x");
//! Chart::default().lineplot( Shape::Continuous( |x| x.sin() / x )).display();
//! }
//! ```
//! It will display something like this:
//!
//! <img src="https://github.com/loony-bean/textplots-rs/blob/master/doc/demo.png?raw=true"/>
//!
//! Default viewport size is 120 x 60 points, with X values ranging from -10 to 10.
//! You can override the defaults calling `new`.
//!
//! ```rust
//! use textplots::{Chart, Plot, Shape};
//!
//! println!("y = cos(x), y = sin(x) / 2");
//! Chart::new(180, 60, -5.0, 5.0)
//! .lineplot( Shape::Continuous( |x| x.cos() ))
//! .lineplot( Shape::Continuous( |x| x.sin() / 2.0 ))
//! .display();
//! ```
//! <img src="https://github.com/loony-bean/textplots-rs/blob/master/doc/demo2.png?raw=true"/>
//!
//! 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.
//!
//! <img src="https://github.com/loony-bean/textplots-rs/blob/master/doc/demo3.png?raw=true"/>
//!
extern crate drawille;
pub mod utils;
pub mod scale;
use drawille::{Canvas as BrailleCanvas};
use scale::Scale;
use std::cmp;
use std::default::Default;
/// Controls the drawing.
pub struct Chart {
/// Canvas width in points
width: u32,
/// Canvas height in points
height: u32,
/// X-axis start value
xmin: f32,
/// X-axis end value
xmax: f32,
/// Y-axis start value (calculated automatically to display all the domain values)
ymin: f32,
/// Y-axis end value (calculated automatically to display all the domain values)
ymax: f32,
/// Underlying canvas object
canvas: BrailleCanvas,
}
/// Specifies different kinds of plotted data.
pub enum Shape<'a> {
/// Real value function
Continuous(fn(f32) -> f32),
/// Points connected with lines.
Lines(&'a [(f32, f32)]),
/// Points connected in step fashion.
Steps(&'a [(f32, f32)]),
/// Points represented with bars.
Bars(&'a [(f32, f32)]),
}
/// Provides an interface for drawing plots.
pub trait Plot {
/// Draws a [line chart](https://en.wikipedia.org/wiki/Line_chart) of points connected by straight line segments.
fn lineplot(&mut self, shape: Shape) -> &mut Chart;
}
impl Default for Chart {
fn default() -> Self {
Self::new(120, 60, -10.0, 10.0)
}
}
impl Chart {
/// Creates a new `Chart` object.
///
/// # Panics
///
/// Panics if `width` or `height` is less than 32.
pub fn new(width: u32, height: u32, xmin: f32, xmax: f32) -> Self {
if width < 32 {
panic!("width should be more then 32, {} is provided", width);
}
if height < 32 {
panic!("height should be more then 32, {} is provided", height);
}
Self {
xmin,
xmax,
ymin: 10.0,
ymax: -10.0,
width,
height,
canvas: BrailleCanvas::new(width, height)
}
}
/// Displays bounding rect.
fn borders(&mut self) {
let w = self.width;
let h = self.height;
self.vline(0);
self.vline(w);
self.hline(0);
self.hline(h);
}
/// Draws vertical line.
fn vline(&mut self, i: u32) {
if i <= self.width {
for j in 0..=self.height {
if j % 3 == 0 {
self.canvas.set(i, j);
}
}
}
}
/// Draws horisontal line.
fn hline(&mut self, j: u32) {
if j <= self.height {
for i in 0..=self.width {
if i % 3 == 0 {
self.canvas.set(i, self.height - j);
}
}
}
}
/// Prints canvas content.
pub fn display(&self) {
let frame = self.canvas.frame();
let rows = frame.split('\n').into_iter().count();
for (i, row) in frame.split('\n').into_iter().enumerate() {
if i == 0 {
println!("{0} {1:.1}", row, self.ymax);
} else if i == (rows - 1) {
println!("{0} {1:.1}", row, self.ymin);
} else {
println!("{}", row);
}
}
println!("{0: <width$.1}{1:.1}", self.xmin, self.xmax, width=(self.width as usize) / 2 - 3);
}
/// Prints canvas content with some additional visual elements (like borders).
pub fn nice(&mut self) {
self.borders();
// self.axis();
self.display();
}
/// Return the frame
pub fn frame(&self) -> String {
self.canvas.frame()
}
}
impl Plot for Chart {
fn lineplot(&mut self, shape: Shape) -> &mut Chart {
let x_scale = Scale::new(self.xmin..self.xmax, 0.0..self.width as f32);
let ys: Vec<_> = match shape {
Shape::Continuous(f) => {
(0..self.width)
.into_iter()
.filter_map(|i| {
let x = x_scale.inv_linear(i as f32);
let y = f(x);
if y.is_normal() {
Some(y)
} else {
None
}
})
.collect()
},
| Shape::Lines(dt)
| Shape::Steps(dt)
| Shape::Bars(dt) => {
dt.iter()
.filter_map(|(x, y)| {
if *x >= self.xmin && *x <= self.xmax {
Some(*y)
} else {
None
}
}).collect()
},
};
let ymax = *ys.iter().max_by( |x, y| x.partial_cmp(y).unwrap_or(cmp::Ordering::Equal) ).unwrap_or(&0.0);
let ymin = *ys.iter().min_by( |x, y| x.partial_cmp(y).unwrap_or(cmp::Ordering::Equal) ).unwrap_or(&0.0);
self.ymin = f32::min(self.ymin, ymin);
self.ymax = f32::max(self.ymax, ymax);
let y_scale = Scale::new(self.ymin..self.ymax, 0.0..self.height as f32);
// show axis
self.vline(x_scale.linear(0.0) as u32);
self.hline(y_scale.linear(0.0) as u32);
// translate (x, y) points into screen coordinates
let points: Vec<_> = match shape {
Shape::Continuous(f) => {
(0..self.width)
.into_iter()
.filter_map(|i| {
let x = x_scale.inv_linear(i as f32);
let y = f(x);
if y.is_normal() {
let j = y_scale.linear(y).round();
Some((i, self.height - j as u32))
} else {
None
}
})
.collect()
},
| Shape::Lines(dt)
| Shape::Steps(dt)
| Shape::Bars(dt) => {
dt
.into_iter()
.filter_map(|(x, y)| {
let i = x_scale.linear(*x).round() as u32;
let j = y_scale.linear(*y).round() as u32;
if i <= self.width && j <= self.height {
Some( (i, self.height - j) )
} else {
None
}
})
.collect()
},
};
// display segments
for pair in points.windows(2) {
let (x1, y1) = pair[0];
let (x2, y2) = pair[1];
match shape {
Shape::Continuous(_) => {
self.canvas.line(x1, y1, x2, y2);
},
Shape::Lines(_) => {
self.canvas.line(x1, y1, x2, y2);
},
Shape::Steps(_) => {
self.canvas.line(x1, y2, x2, y2);
self.canvas.line(x1, y1, x1, y2);
},
Shape::Bars(_) => {
self.canvas.line(x1, y2, x2, y2);
self.canvas.line(x1, y1, x1, y2);
self.canvas.line(x1, self.height, x1, y1);
self.canvas.line(x2, self.height, x2, y2);
},
}
}
self
}
}