├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── doc ├── demo.png ├── demo2.png ├── demo3.png ├── demo4.png └── demo5.png ├── examples ├── color.rs ├── hist.rs ├── label.rs ├── liveplot.rs ├── multiple.rs ├── plot.rs └── sparse_color.rs ├── run_examples.sh └── src ├── lib.rs ├── main.rs ├── scale.rs └── utils.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Rust 3 | 4 | on: 5 | push: 6 | branches: [ master, main ] 7 | pull_request: 8 | branches: [ master, main ] 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Install Rust 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | profile: minimal 23 | toolchain: stable 24 | override: true 25 | components: clippy 26 | - name: Checkout Sources 27 | uses: actions/checkout@v2 28 | - name: Check formatting 29 | uses: actions-rs/cargo@v1 30 | with: 31 | command: fmt 32 | args: -- --check 33 | - name: Lints 34 | uses: actions-rs/cargo@v1 35 | with: 36 | command: clippy 37 | args: -- -D warnings 38 | - name: Build & Test Project 39 | run: | 40 | ./run_examples.sh 41 | cargo test 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target/ 3 | **/*.rs.bk 4 | Cargo.lock 5 | .idea 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "textplots" 3 | description = "Terminal plotting library." 4 | version = "0.8.7" 5 | authors = ["Alexey Suslov "] 6 | license = "MIT" 7 | repository = "https://github.com/loony-bean/textplots-rs" 8 | keywords = ["plotting", "unicode", "charts", "cli"] 9 | categories = ["visualization", "command-line-interface"] 10 | readme = "README.md" 11 | edition = "2021" 12 | 13 | [lib] 14 | name = "textplots" 15 | path = "src/lib.rs" 16 | 17 | [[bin]] 18 | name = "textplots" 19 | path = "src/main.rs" 20 | required-features = ["tool"] 21 | 22 | [badges] 23 | travis-ci = { repository = "loony-bean/textplots-rs", branch = "master" } 24 | 25 | [features] 26 | tool = [ 27 | "meval", 28 | "structopt", 29 | ] 30 | 31 | [dependencies] 32 | drawille = "0.3.0" 33 | structopt = { version = "0.3", optional = true } 34 | meval = { version = "0.2", optional = true } 35 | rgb = "0.8.27" 36 | 37 | [dev-dependencies] 38 | ctrlc = "3" 39 | console = "0.15.7" 40 | chrono = "0.4.30" 41 | 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # textplots [![Crates.io](https://img.shields.io/crates/v/textplots.svg)](https://crates.io/crates/textplots) 2 | 3 | Terminal plotting library for using in Rust CLI applications. 4 | Should work well in any unicode terminal with monospaced font. 5 | 6 | It is inspired by [TextPlots.jl](https://github.com/sunetos/TextPlots.jl) which is inspired by [Drawille](https://github.com/asciimoo/drawille). 7 | 8 | Currently it features only drawing line charts on Braille canvas, but could be extended 9 | to support other canvas and chart types just like [UnicodePlots.jl](https://github.com/Evizero/UnicodePlots.jl) 10 | or another cool terminal plotting library. 11 | 12 | Contributions are very much welcome! 13 | 14 | # Usage 15 | 16 | ## Using as a library 17 | 18 | ```rust 19 | use textplots::{Chart, Plot, Shape}; 20 | 21 | fn main() { 22 | println!("y = sin(x) / x"); 23 | 24 | Chart::default() 25 | .lineplot(&Shape::Continuous(Box::new(|x| x.sin() / x))) 26 | .display(); 27 | } 28 | ``` 29 | 30 | 31 | 32 | ## Using as a binary 33 | 34 | ```sh 35 | textplots '10*x + x^2 + 10*sin(x)*abs(x)' --xmin=-20 --xmax=20 36 | ``` 37 | 38 | 39 | 40 | ## Bonus! Colored plots (see examples) 41 | 42 | 43 | 44 | # Building 45 | 46 | ## Library 47 | 48 | ```sh 49 | cargo build 50 | ``` 51 | 52 | ## Binary 53 | 54 | ```sh 55 | cargo build --bin --release textplots --features="tool" 56 | ``` 57 | -------------------------------------------------------------------------------- /doc/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loony-bean/textplots-rs/49051a1a921ebe11348996cd95381929ece26e4d/doc/demo.png -------------------------------------------------------------------------------- /doc/demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loony-bean/textplots-rs/49051a1a921ebe11348996cd95381929ece26e4d/doc/demo2.png -------------------------------------------------------------------------------- /doc/demo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loony-bean/textplots-rs/49051a1a921ebe11348996cd95381929ece26e4d/doc/demo3.png -------------------------------------------------------------------------------- /doc/demo4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loony-bean/textplots-rs/49051a1a921ebe11348996cd95381929ece26e4d/doc/demo4.png -------------------------------------------------------------------------------- /doc/demo5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loony-bean/textplots-rs/49051a1a921ebe11348996cd95381929ece26e4d/doc/demo5.png -------------------------------------------------------------------------------- /examples/color.rs: -------------------------------------------------------------------------------- 1 | use rgb::RGB8; 2 | use textplots::{Chart, ColorPlot, Shape}; 3 | 4 | fn main() { 5 | // You can plot several functions on the same chart. 6 | // However the resolution of text displays is low, and the result might not be great. 7 | println!("\ny = cos(x), y = sin(x) / 2"); 8 | Chart::new(180, 60, -5.0, 5.0) 9 | .linecolorplot( 10 | &Shape::Continuous(Box::new(|x| x.cos())), 11 | RGB8 { 12 | r: 255_u8, 13 | g: 0, 14 | b: 0, 15 | }, 16 | ) 17 | .linecolorplot( 18 | &Shape::Continuous(Box::new(|x| x.sin() / 2.0)), 19 | RGB8 { r: 0, g: 0, b: 255 }, 20 | ) 21 | .display(); 22 | 23 | println!("\nRainbow"); 24 | let min_radius = 5.0; 25 | let ring_gap = 0.5; 26 | let segment_count = 20; 27 | let num_rings = 6; 28 | 29 | let mut rings = vec![]; 30 | for _ in 0..num_rings { 31 | rings.push(vec![]); 32 | } 33 | 34 | for i in 0..segment_count { 35 | let angle: f32 = ((std::f64::consts::PI as f32 / 2.0) / segment_count as f32) * i as f32; 36 | let angle_sin = angle.sin(); 37 | let angle_cos = angle.cos(); 38 | 39 | (0..num_rings).for_each(|j| { 40 | rings[j].push(( 41 | (min_radius + (ring_gap * j as f32)) * angle_cos, 42 | (min_radius + (ring_gap * j as f32)) * angle_sin, 43 | )); 44 | }); 45 | } 46 | 47 | (0..num_rings).for_each(|i| { 48 | let mut ring_copy = rings[i].clone(); 49 | ring_copy.reverse(); 50 | for coord in ring_copy.iter_mut() { 51 | coord.0 = 0.0 - coord.0; 52 | } 53 | rings[i].append(&mut ring_copy); 54 | }); 55 | 56 | let max_radius = min_radius + ((num_rings as f32 + 1.0) * ring_gap); 57 | Chart::new(180, 60, 0.0 - max_radius, max_radius) 58 | .linecolorplot( 59 | &Shape::Lines(rings[5].as_ref()), 60 | RGB8 { 61 | // Red 62 | r: 255, 63 | g: 0, 64 | b: 0, 65 | }, 66 | ) 67 | .linecolorplot( 68 | &Shape::Lines(rings[4].as_ref()), 69 | RGB8 { 70 | // Orange 71 | r: 255, 72 | g: 165, 73 | b: 0, 74 | }, 75 | ) 76 | .linecolorplot( 77 | &Shape::Lines(rings[3].as_ref()), 78 | RGB8 { 79 | // Yellow 80 | r: 255, 81 | g: 255, 82 | b: 0, 83 | }, 84 | ) 85 | .linecolorplot( 86 | &Shape::Lines(rings[2].as_ref()), 87 | RGB8 { 88 | // Green 89 | r: 0, 90 | g: 255, 91 | b: 0, 92 | }, 93 | ) 94 | .linecolorplot( 95 | &Shape::Lines(rings[1].as_ref()), 96 | RGB8 { 97 | // Blue 98 | r: 0, 99 | g: 0, 100 | b: 255, 101 | }, 102 | ) 103 | .linecolorplot( 104 | &Shape::Lines(rings[0].as_ref()), 105 | RGB8 { 106 | // Violet 107 | r: 136, 108 | g: 43, 109 | b: 226, 110 | }, 111 | ) 112 | .display(); 113 | } 114 | -------------------------------------------------------------------------------- /examples/hist.rs: -------------------------------------------------------------------------------- 1 | use textplots::{utils, Chart, Plot, Shape}; 2 | 3 | fn main() { 4 | let points = [ 5 | (0.0, 11.677842), 6 | (1.0, 9.36832), 7 | (2.0, 9.44862), 8 | (3.0, 8.933405), 9 | (4.0, 11.812017), 10 | (5.0, 9.225881), 11 | (6.0, 11.498754), 12 | (7.0, 8.048191), 13 | (8.0, 11.141933), 14 | (9.0, 8.908442), 15 | (10.0, 9.795515), 16 | (11.0, 9.115709), 17 | (12.0, 8.907767), 18 | (13.0, 11.153717), 19 | (14.0, 10.893343), 20 | (15.0, 9.299543), 21 | (16.0, 9.701611), 22 | (17.0, 10.5055685), 23 | (18.0, 9.29626), 24 | (19.0, 9.14646), 25 | (20.0, 8.892315), 26 | (21.0, 10.202528), 27 | (22.0, 9.776375), 28 | (23.0, 9.106522), 29 | (24.0, 10.017476), 30 | (25.0, 9.344163), 31 | (26.0, 10.087107), 32 | (27.0, 10.595626), 33 | (28.0, 10.26935), 34 | (29.0, 9.619124), 35 | (30.0, 9.759572), 36 | (31.0, 10.563857), 37 | (32.0, 10.254505), 38 | (33.0, 10.876704), 39 | (34.0, 10.487561), 40 | (35.0, 9.672023), 41 | (36.0, 11.139632), 42 | (37.0, 11.562245), 43 | (38.0, 12.311694), 44 | (39.0, 8.636497), 45 | (40.0, 10.127292), 46 | (41.0, 10.867212), 47 | (42.0, 10.285108), 48 | (43.0, 8.08108), 49 | (44.0, 10.198147), 50 | (45.0, 10.746926), 51 | (46.0, 9.746353), 52 | (47.0, 9.767584), 53 | (48.0, 10.165949), 54 | (49.0, 10.08595), 55 | (50.0, 10.08878), 56 | (51.0, 11.137325), 57 | (52.0, 9.596683), 58 | (53.0, 9.3425045), 59 | (54.0, 9.627313), 60 | (55.0, 10.010254), 61 | (56.0, 9.526915), 62 | (57.0, 11.239794), 63 | (58.0, 8.913055), 64 | (59.0, 10.136724), 65 | (60.0, 10.72442), 66 | (61.0, 9.044553), 67 | (62.0, 8.657727), 68 | (63.0, 10.284623), 69 | (64.0, 10.32074), 70 | (65.0, 8.713137), 71 | (66.0, 9.928085), 72 | (67.0, 8.439049), 73 | (68.0, 9.942111), 74 | (69.0, 9.212654), 75 | (70.0, 8.224474), 76 | (71.0, 11.252406), 77 | (72.0, 8.816701), 78 | (73.0, 10.853656), 79 | (74.0, 8.788404), 80 | (75.0, 10.526451), 81 | (76.0, 10.779287), 82 | (77.0, 9.357046), 83 | (78.0, 10.788815), 84 | (79.0, 10.013228), 85 | (80.0, 10.859512), 86 | (81.0, 10.734754), 87 | (82.0, 10.504648), 88 | (83.0, 10.104772), 89 | (84.0, 10.20058), 90 | (85.0, 10.663727), 91 | (86.0, 11.040146), 92 | (87.0, 12.313308), 93 | (88.0, 10.41382), 94 | (89.0, 9.867012), 95 | (90.0, 9.984057), 96 | (91.0, 8.8879595), 97 | (92.0, 9.459296), 98 | (93.0, 9.00407), 99 | (94.0, 10.469272), 100 | (95.0, 9.79327), 101 | (96.0, 12.317585), 102 | (97.0, 8.190812), 103 | (98.0, 12.709852), 104 | (99.0, 9.720502), 105 | ]; 106 | 107 | println!("\ny = line plot"); 108 | Chart::new(180, 60, 0.0, 100.0) 109 | .lineplot(&Shape::Lines(&points)) 110 | .display(); 111 | 112 | println!("\ny = steps"); 113 | Chart::new(180, 60, 0.0, 100.0) 114 | .lineplot(&Shape::Steps(&points)) 115 | .display(); 116 | 117 | println!("\ny = bars"); 118 | Chart::new(180, 60, 0.0, 100.0) 119 | .lineplot(&Shape::Bars(&points)) 120 | .display(); 121 | 122 | let hist = utils::histogram(&points, 6.0, 15.0, 16); 123 | println!("\ny = histogram bars"); 124 | Chart::new(180, 60, 6.0, 14.0) 125 | .lineplot(&Shape::Bars(&hist)) 126 | .nice(); 127 | } 128 | -------------------------------------------------------------------------------- /examples/label.rs: -------------------------------------------------------------------------------- 1 | use chrono::{Duration, NaiveDate}; 2 | use textplots::{ 3 | Chart, ColorPlot, LabelBuilder, LabelFormat, Shape, TickDisplay, TickDisplayBuilder, 4 | }; 5 | 6 | fn main() { 7 | // Specify how labels are displayed. 8 | let start = NaiveDate::from_ymd_opt(2023, 6, 1).unwrap(); 9 | 10 | let end = NaiveDate::from_ymd_opt(2023, 9, 1).unwrap(); 11 | 12 | println!("My step count over 3 months: "); 13 | Chart::new_with_y_range(200, 50, 0.0, (end - start).num_days() as f32, 0.0, 25_000.0) 14 | .linecolorplot( 15 | &Shape::Continuous(Box::new(|x| { 16 | 1000.0 * (5.0 * (0.5 * x).sin() + 0.05 * x) + 9000.0 17 | })), 18 | rgb::RGB { 19 | r: 10, 20 | g: 100, 21 | b: 200, 22 | }, 23 | ) 24 | .x_label_format(LabelFormat::Custom(Box::new(move |val| { 25 | format!("{}", start + Duration::days(val as i64)) 26 | }))) 27 | .y_label_format(LabelFormat::Value) 28 | .y_tick_display(TickDisplay::Sparse) 29 | .nice(); 30 | } 31 | -------------------------------------------------------------------------------- /examples/liveplot.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{ 2 | atomic::{AtomicBool, Ordering}, 3 | Arc, 4 | }; 5 | use textplots::{Chart, ColorPlot, Shape}; 6 | 7 | const PRINT_LEN: usize = 100; 8 | const PURPLE: rgb::RGB8 = rgb::RGB8::new(0xE0, 0x80, 0xFF); 9 | const RED: rgb::RGB8 = rgb::RGB8::new(0xFF, 0x00, 0x00); 10 | const GREEN: rgb::RGB8 = rgb::RGB8::new(0x00, 0xFF, 0x00); 11 | 12 | const RUN_DURATION_S: u64 = 5; 13 | 14 | fn main() { 15 | let should_run = Arc::new(AtomicBool::new(true)); 16 | let should_run_ctrlc_ref = should_run.clone(); 17 | 18 | let mut x: [(f32, f32); PRINT_LEN] = [(0., 0.); PRINT_LEN]; 19 | let mut y: [(f32, f32); PRINT_LEN] = [(0., 0.); PRINT_LEN]; 20 | let mut z: [(f32, f32); PRINT_LEN] = [(0., 0.); PRINT_LEN]; 21 | 22 | // hide the cursor so we don't see it flying all over 23 | let term = console::Term::stdout(); 24 | term.hide_cursor().unwrap(); 25 | term.clear_screen().unwrap(); 26 | 27 | // On ctrl+C, reset terminal settings and let the thread know to stop 28 | ctrlc::set_handler(move || { 29 | should_run_ctrlc_ref 30 | .as_ref() 31 | .store(false, Ordering::Relaxed); 32 | }) 33 | .unwrap(); 34 | 35 | // run until we get ctrl+C or timeout 36 | let mut time: f32 = 0.; 37 | let start_time = std::time::SystemTime::now(); 38 | while should_run.as_ref().load(Ordering::Acquire) 39 | && start_time.elapsed().unwrap().as_secs() < RUN_DURATION_S 40 | { 41 | // update our plotting data 42 | let x_val = time.sin(); 43 | let y_val = (time + std::f32::consts::FRAC_PI_3).sin(); 44 | let z_val = (time + 2. * std::f32::consts::FRAC_PI_3).sin(); 45 | x.copy_within(1..PRINT_LEN, 0); 46 | y.copy_within(1..PRINT_LEN, 0); 47 | z.copy_within(1..PRINT_LEN, 0); 48 | x[PRINT_LEN - 1] = (0., x_val as f32); 49 | y[PRINT_LEN - 1] = (0., y_val as f32); 50 | z[PRINT_LEN - 1] = (0., z_val as f32); 51 | for index in 0..PRINT_LEN { 52 | x[index].0 += 1.; 53 | y[index].0 += 1.; 54 | z[index].0 += 1.; 55 | } 56 | 57 | time += std::f32::consts::PI / 50.; 58 | 59 | // update our plot 60 | term.move_cursor_to(0, 0).unwrap(); 61 | Chart::new_with_y_range(200, 100, 0., PRINT_LEN as f32, -1.5, 1.5) 62 | .linecolorplot(&Shape::Lines(&x), RED) 63 | .linecolorplot(&Shape::Lines(&y), GREEN) 64 | .linecolorplot(&Shape::Lines(&z), PURPLE) 65 | .display(); 66 | 67 | std::thread::sleep(std::time::Duration::from_millis(10)); 68 | } 69 | 70 | // re-reveal the cursor 71 | let term = console::Term::stdout(); 72 | term.show_cursor().unwrap(); 73 | } 74 | -------------------------------------------------------------------------------- /examples/multiple.rs: -------------------------------------------------------------------------------- 1 | use textplots::{Chart, Plot, Shape}; 2 | 3 | fn main() { 4 | // Display multiple plots. 5 | // https://github.com/loony-bean/textplots-rs/issues/8 6 | println!("y = -x^2; y = x^2"); 7 | Chart::default() 8 | .lineplot(&Shape::Continuous(Box::new(|x| (-x.powf(2.0))))) 9 | .lineplot(&Shape::Continuous(Box::new(|x| (x.powf(2.0))))) 10 | .display(); 11 | 12 | // https://github.com/loony-bean/textplots-rs/issues/15 13 | let (mut l1, mut l2, mut l3) = (vec![], vec![], vec![]); 14 | for n in -2..=2 { 15 | l1.push((n as f32, n as f32)); 16 | l2.push((n as f32, n as f32 - 1.)); 17 | l3.push((n as f32, n as f32 - 2.)); 18 | } 19 | 20 | println!("\nf(x)=x; f(x)=x-1; f(x)=x-2"); 21 | Chart::new(120, 80, -2., 2.) 22 | .lineplot(&Shape::Lines(l1.as_slice())) 23 | .lineplot(&Shape::Lines(l2.as_slice())) 24 | .lineplot(&Shape::Lines(l3.as_slice())) 25 | .nice(); 26 | 27 | let (mut l4, mut l5, mut l6) = (vec![], vec![], vec![]); 28 | for n in -2..=2 { 29 | l4.push((n as f32, n as f32)); 30 | l5.push((n as f32, n as f32 + 1.)); 31 | l6.push((n as f32, n as f32 + 2.)); 32 | } 33 | 34 | println!("\nf(x)=x; f(x)=x+1; f(x)=x+2"); 35 | Chart::new(120, 80, -2., 2.) 36 | .lineplot(&Shape::Lines(l4.as_slice())) 37 | .lineplot(&Shape::Lines(l5.as_slice())) 38 | .lineplot(&Shape::Lines(l6.as_slice())) 39 | .nice(); 40 | } 41 | -------------------------------------------------------------------------------- /examples/plot.rs: -------------------------------------------------------------------------------- 1 | use textplots::{Chart, Plot, Shape}; 2 | 3 | fn main() { 4 | // You can pass any real value function. 5 | println!("y = atan(x)"); 6 | Chart::default() 7 | .lineplot(&Shape::Continuous(Box::new(|x| x.atan()))) 8 | .display(); 9 | 10 | // The plot try to display everything that is a `normal` float, skipping NaN's and friends. 11 | println!("\ny = sin(x) / x"); 12 | Chart::default() 13 | .lineplot(&Shape::Continuous(Box::new(|x| x.sin() / x))) 14 | .display(); 15 | 16 | // Default viewport size is 120 x 60 points, with X values ranging from -10 to 10. 17 | println!("\ny = ln(x)"); 18 | Chart::default() 19 | .lineplot(&Shape::Continuous(Box::new(f32::ln))) 20 | .display(); 21 | 22 | // You can plot several functions on the same chart. 23 | // However the resolution of text displays is low, and the result might not be great. 24 | println!("\ny = cos(x), y = sin(x) / 2"); 25 | Chart::new(180, 60, -5.0, 5.0) 26 | .lineplot(&Shape::Continuous(Box::new(|x| x.cos()))) 27 | .lineplot(&Shape::Continuous(Box::new(|x| x.sin() / 2.0))) 28 | .display(); 29 | 30 | let points = [ 31 | (-10.0, -1.0), 32 | (0.0, 0.0), 33 | (1.0, 1.0), 34 | (2.0, 0.0), 35 | (3.0, 3.0), 36 | (4.0, 4.0), 37 | (5.0, 3.0), 38 | (9.0, 1.0), 39 | (10.0, -1.0), 40 | ]; 41 | 42 | println!("\ny = interpolated points"); 43 | Chart::default().lineplot(&Shape::Lines(&points)).display(); 44 | 45 | println!("\ny = staircase points"); 46 | Chart::default().lineplot(&Shape::Steps(&points)).display(); 47 | 48 | println!("\ny = scatter plot"); 49 | Chart::default().lineplot(&Shape::Points(&points)).display(); 50 | 51 | // You can instead get the raw string value 52 | println!("\nRender to string (and then print that string)"); 53 | let mut chart = Chart::default(); 54 | let binding = Shape::Continuous(Box::new(|x| x.atan())); 55 | let chart = chart.lineplot(&binding); 56 | 57 | chart.axis(); 58 | chart.figures(); 59 | 60 | let chart_string = chart.to_string(); 61 | 62 | println!("{}", chart_string); 63 | } 64 | -------------------------------------------------------------------------------- /examples/sparse_color.rs: -------------------------------------------------------------------------------- 1 | use rgb::RGB8; 2 | use textplots::{Chart, ColorPlot, Shape}; 3 | 4 | fn main() { 5 | // Plot a sparse set of points with colors 6 | 7 | let points = vec![ 8 | (2.3, 2.1), 9 | (1.5, 2.1), 10 | (3.6, 0.20), 11 | (2.4, 0.22), 12 | (1.9, 2.9), 13 | (3.2, 3.0), 14 | (0.38, 1.2), 15 | (2.6, 0.45), 16 | (0.040, 2.9), 17 | (1.5, 1.8), 18 | (1.4, 0.41), 19 | (3.6, 3.3), 20 | (0.80, 0.83), 21 | (2.7, 1.5), 22 | (1.6, 1.8), 23 | (2.6, 0.70), 24 | (3.8, 0.64), 25 | (2.7, 2.7), 26 | (1.7, 2.3), 27 | (1.9, 0.87), 28 | (1.8, 2.4), 29 | (1.2, 2.1), 30 | (3.7, 1.6), 31 | (0.32, 3.7), 32 | (0.26, 3.2), 33 | (3.9, 2.4), 34 | (2.0, 1.6), 35 | (0.37, 1.6), 36 | (0.53, 0.71), 37 | (1.2, 3.4), 38 | ]; 39 | let sparse_points = Shape::Points(points.as_slice()); 40 | 41 | let mut plot = Chart::new(60, 40, 0., 3.); 42 | let point_color = RGB8 { 43 | // Red 44 | r: 255, 45 | g: 0, 46 | b: 0, 47 | }; 48 | let dots = plot.linecolorplot(&sparse_points, point_color); 49 | dots.display(); 50 | } 51 | -------------------------------------------------------------------------------- /run_examples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | for d in examples/* 5 | do 6 | example=$(echo $d | sed 's/examples\/\(.*\).rs/\1/') 7 | echo "Running example $example..." 8 | cargo run --example $example 9 | done 10 | 11 | -------------------------------------------------------------------------------- /src/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 | //! 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 | //! 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 | //! 51 | 52 | pub mod scale; 53 | pub mod utils; 54 | 55 | use drawille::Canvas as BrailleCanvas; 56 | use drawille::PixelColor; 57 | use rgb::RGB8; 58 | use scale::Scale; 59 | use std::cmp; 60 | use std::default::Default; 61 | use std::f32; 62 | use std::fmt::{Display, Formatter, Result}; 63 | 64 | /// How the chart will do the ranging on axes 65 | #[derive(PartialEq)] 66 | enum 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. 74 | pub 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)>, 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. 106 | pub enum Shape<'a> { 107 | /// Real value function. 108 | Continuous(Box 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. 120 | pub 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. 126 | pub 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. 132 | pub 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 | 140 | pub 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 149 | pub 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 | 157 | impl<'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)] 166 | pub 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`. 179 | pub 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 String>), 186 | } 187 | 188 | /// Specifies density of labels on the Y axis between ymin and ymax. 189 | /// Default value is `TickDisplay::None`. 190 | pub 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 | 199 | impl 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 | 209 | impl<'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::>() 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: 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 | 605 | impl<'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 | 615 | impl<'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 | 625 | fn rgb_to_pixelcolor(rgb: &RGB8) -> PixelColor { 626 | PixelColor::TrueColor { 627 | r: rgb.r, 628 | g: rgb.g, 629 | b: rgb.b, 630 | } 631 | } 632 | 633 | impl<'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 | 645 | impl<'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 | 659 | impl<'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 | } 686 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::process::exit; 2 | use structopt::StructOpt; 3 | use textplots::{Chart, Plot, Shape}; 4 | 5 | #[derive(StructOpt)] 6 | struct Opt { 7 | /// Formula to plot 8 | #[structopt(name = "FORMULA")] 9 | formula: String, 10 | /// X-axis start value. 11 | #[structopt(long, default_value = "-10.0")] 12 | xmin: f32, 13 | /// X-axis end value. 14 | #[structopt(long, default_value = "10.0")] 15 | xmax: f32, 16 | /// Y-axis start value. 17 | #[structopt(long)] 18 | ymin: Option, 19 | /// X-axis end value. 20 | #[structopt(long)] 21 | ymax: Option, 22 | /// Canvas width in points. 23 | #[structopt(short, long, default_value = "180")] 24 | width: u32, 25 | /// Canvas height in points. 26 | #[structopt(short, long, default_value = "60")] 27 | height: u32, 28 | } 29 | 30 | fn main() { 31 | let opt = Opt::from_args(); 32 | 33 | let res = opt 34 | .formula 35 | .parse() 36 | .and_then(|expr: meval::Expr| expr.bind("x")); 37 | let func = match res { 38 | Ok(func) => func, 39 | Err(err) => { 40 | // if there was an error with parsing 41 | // or binding "x", exit with error 42 | 43 | eprintln!("{}", err); 44 | exit(1); 45 | } 46 | }; 47 | 48 | // check for invalid ymin/ymax 49 | if (opt.ymax.is_none() && opt.ymin.is_some()) || (opt.ymax.is_some() && opt.ymin.is_none()) { 50 | eprintln!("both ymin and ymax must be specified"); 51 | exit(2); 52 | } 53 | 54 | println!("y = {}", opt.formula); 55 | let mut chart = if opt.ymin.is_none() { 56 | Chart::new(opt.width, opt.height, opt.xmin, opt.xmax) 57 | } else { 58 | Chart::new_with_y_range( 59 | opt.width, 60 | opt.height, 61 | opt.xmin, 62 | opt.xmax, 63 | opt.ymin.unwrap(), 64 | opt.ymax.unwrap(), 65 | ) 66 | }; 67 | chart 68 | .lineplot(&Shape::Continuous(Box::new(|x| func(x.into()) as f32))) 69 | .display(); 70 | } 71 | -------------------------------------------------------------------------------- /src/scale.rs: -------------------------------------------------------------------------------- 1 | //! Transformations between domain and range. 2 | 3 | use std::ops::Range; 4 | 5 | /// Holds mapping between domain and range of the function. 6 | pub struct Scale { 7 | domain: Range, 8 | range: Range, 9 | } 10 | 11 | impl Scale { 12 | /// Translates value from domain to range scale. 13 | /// ``` 14 | /// # use textplots::scale::Scale; 15 | /// assert_eq!(-0.8, Scale::new(0_f32..10_f32, -1_f32..1_f32).linear(1.0)); 16 | /// ``` 17 | pub fn linear(&self, x: f32) -> f32 { 18 | let p = (x - self.domain.start) / (self.domain.end - self.domain.start); 19 | let r = self.range.start + p * (self.range.end - self.range.start); 20 | r.max(self.range.start).min(self.range.end) 21 | } 22 | 23 | /// Translates value from range to domain scale. 24 | /// ``` 25 | /// # use textplots::scale::Scale; 26 | /// assert_eq!(5.5, Scale::new(0_f32..10_f32, -1_f32..1_f32).inv_linear(0.1)); 27 | /// ``` 28 | pub fn inv_linear(&self, i: f32) -> f32 { 29 | let p = (i - self.range.start) / (self.range.end - self.range.start); 30 | let d = self.domain.start + p * (self.domain.end - self.domain.start); 31 | d.max(self.domain.start).min(self.domain.end) 32 | } 33 | 34 | pub fn new(domain: Range, range: Range) -> Self { 35 | Scale { domain, range } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | //! Helpers for passing the data into plots. 2 | //! 3 | //! Merely a bunch of functions hanging around while the library API is taking shape. 4 | 5 | /// Transforms points into frequency distribution (for using in histograms). 6 | /// Values outside of [`min`, `max`] interval are ignored, and everything that 7 | /// falls into the specified interval is grouped into `bins` number of buckets of equal width. 8 | /// 9 | /// ``` 10 | /// # use textplots::utils::histogram; 11 | /// assert_eq!(vec![(0.0, 1.0), (5.0, 1.0)], histogram( &[ (0.0, 0.0), (9.0, 9.0), (10.0, 10.0) ], 0.0, 10.0, 2 )); 12 | /// ``` 13 | pub fn histogram(data: &[(f32, f32)], min: f32, max: f32, bins: usize) -> Vec<(f32, f32)> { 14 | let mut output = vec![0; bins]; 15 | 16 | let step = (max - min) / bins as f32; 17 | 18 | for &(_x, y) in data.iter() { 19 | if y < min || y > max { 20 | continue; 21 | } 22 | 23 | let bucket_id = ((y - min) / step) as usize; 24 | if bucket_id < output.len() { 25 | output[bucket_id] += 1; 26 | } 27 | } 28 | 29 | output 30 | .into_iter() 31 | .enumerate() 32 | .map(|(x, y)| ((min + (x as f32) * step), y as f32)) 33 | .collect() 34 | } 35 | --------------------------------------------------------------------------------