Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Library Usage

The unicode-plot crate is a standalone Rust library for rendering Unicode terminal plots. You can use it independently in any Rust project without the CLI or DSV parsing layers. This chapter shows how to create common plot types programmatically.

Adding the dependency

Add unicode-plot to your Cargo.toml:

[dependencies]
unicode-plot = "0.1"

Creating a line plot

The simplest way to create a line plot is to provide x and y vectors and use the default options:

use unicode_plot::{lineplot, LineplotOptions};

fn main() {
    let x: Vec<f64> = (0..100).map(|i| f64::from(i) / 10.0).collect();
    let y: Vec<f64> = x.iter().map(|v| v.sin()).collect();

    let plot = lineplot(&x, &y, LineplotOptions::default()).unwrap();

    let mut buf = Vec::new();
    plot.render(&mut buf, true).unwrap();
    print!("{}", String::from_utf8(buf).unwrap());
}

The render method takes any impl Write and a boolean indicating whether to emit ANSI color codes. Pass true for terminal output and false when writing to a file or a context that does not support ANSI escapes.

Customizing plot options

Each plot constructor accepts an options struct for controlling layout and appearance. The fields are concrete types rather than Option wrappers, so you can use struct update syntax with ..Default::default() to override only the fields you care about:

use unicode_plot::{lineplot, LineplotOptions};
use unicode_plot::{CanvasType, BorderType, TermColor, NamedColor};

fn main() {
    let x: Vec<f64> = (0..50).map(|i| f64::from(i)).collect();
    let y: Vec<f64> = x.iter().map(|v| v.sqrt()).collect();

    let opts = LineplotOptions {
        title: Some("Square root".into()),
        xlabel: Some("x".into()),
        ylabel: Some("sqrt(x)".into()),
        width: 60,
        height: 20,
        canvas: CanvasType::Block,
        border: BorderType::Corners,
        color: Some(TermColor::Named(NamedColor::Cyan)),
        name: Some("sqrt".into()),
        ..LineplotOptions::default()
    };

    let plot = lineplot(&x, &y, opts).unwrap();

    let mut buf = Vec::new();
    plot.render(&mut buf, true).unwrap();
    print!("{}", String::from_utf8(buf).unwrap());
}

Creating a bar chart

Bar charts take parallel slices of labels and values:

use unicode_plot::{barplot, BarplotOptions};

fn main() {
    let labels = ["Rust", "Python", "Go", "Java"];
    let values = [95.0, 82.0, 74.0, 68.0];

    let plot = barplot(&labels, &values, BarplotOptions::default()).unwrap();

    let mut buf = Vec::new();
    plot.render(&mut buf, true).unwrap();
    print!("{}", String::from_utf8(buf).unwrap());
}

Creating a scatter plot

Scatter plots use the same options structure as line plots but plot individual points instead of connected segments:

use unicode_plot::{scatterplot, LineplotOptions};

fn main() {
    let x: Vec<f64> = (0..200).map(|i| f64::from(i) / 20.0).collect();
    let y: Vec<f64> = x.iter().map(|v| v.sin() + 0.1 * v).collect();

    let opts = LineplotOptions {
        title: Some("Scatter".into()),
        ..LineplotOptions::default()
    };

    let plot = scatterplot(&x, &y, opts).unwrap();

    let mut buf = Vec::new();
    plot.render(&mut buf, false).unwrap();
    print!("{}", String::from_utf8(buf).unwrap());
}

Creating a histogram

Histograms take a single slice of numeric data and compute bins automatically:

use unicode_plot::{histogram, HistogramOptions};

fn main() {
    let data: Vec<f64> = (0..500)
        .map(|i| ((i as f64) * 0.1).sin() * 3.0 + 5.0)
        .collect();

    let opts = HistogramOptions {
        title: Some("Distribution".into()),
        ..HistogramOptions::default()
    };

    let plot = histogram(&data, opts).unwrap();

    let mut buf = Vec::new();
    plot.render(&mut buf, true).unwrap();
    print!("{}", String::from_utf8(buf).unwrap());
}

Creating a box plot

Box plots accept parallel slices of labels and data series. Each series is a slice of optional string values that are parsed as numbers:

use unicode_plot::{boxplot, BoxplotOptions};

fn main() {
    let labels = ["group_a", "group_b"];
    let series_a: Vec<Option<String>> =
        ["1.0", "2.0", "3.0", "4.0", "5.0"]
            .iter().map(|s| Some(s.to_string())).collect();
    let series_b: Vec<Option<String>> =
        ["2.5", "3.5", "4.5", "5.5", "6.5"]
            .iter().map(|s| Some(s.to_string())).collect();
    let series = [series_a.as_slice(), series_b.as_slice()];

    let plot = boxplot(&labels, &series, BoxplotOptions::default()).unwrap();

    let mut buf = Vec::new();
    plot.render(&mut buf, true).unwrap();
    print!("{}", String::from_utf8(buf).unwrap());
}

The boxplot API takes string values rather than floats because it shares parsing behavior with the CLI layer, which receives all input as text. Each None entry is skipped during summary computation.

Overlaying multiple series

For line plots and scatter plots, you can add additional series to an existing plot:

use unicode_plot::{lineplot, lineplot_add, LineplotOptions, LineplotSeriesOptions};
use unicode_plot::{TermColor, NamedColor};

fn main() {
    let x: Vec<f64> = (0..100).map(|i| f64::from(i) / 10.0).collect();
    let sin_y: Vec<f64> = x.iter().map(|v| v.sin()).collect();
    let cos_y: Vec<f64> = x.iter().map(|v| v.cos()).collect();

    let opts = LineplotOptions {
        title: Some("Trig functions".into()),
        ..LineplotOptions::default()
    };

    let mut plot = lineplot(&x, &sin_y, opts).unwrap();

    let series_opts = LineplotSeriesOptions {
        color: Some(TermColor::Named(NamedColor::Red)),
        name: Some("cos".into()),
    };
    lineplot_add(&mut plot, &x, &cos_y, series_opts).unwrap();

    let mut buf = Vec::new();
    plot.render(&mut buf, true).unwrap();
    print!("{}", String::from_utf8(buf).unwrap());
}

Rendering to different targets

All the examples above render to a Vec<u8> buffer and then convert to a string. You can also write directly to stdout, stderr, a file, or any other impl Write:

use unicode_plot::{lineplot_y, LineplotOptions};
use std::io;

fn main() {
    let y: Vec<f64> = (0..50).map(|i| (f64::from(i) / 5.0).sin()).collect();
    let plot = lineplot_y(&y, LineplotOptions::default()).unwrap();

    let stderr = io::stderr();
    let mut handle = stderr.lock();
    plot.render(&mut handle, true).unwrap();
}

Color mode control

For more fine-grained color control, use render_with_mode which accepts a ColorMode enum:

use unicode_plot::{lineplot_y, LineplotOptions, ColorMode};

fn main() {
    let y: Vec<f64> = (0..50).map(|i| f64::from(i * i)).collect();
    let plot = lineplot_y(&y, LineplotOptions::default()).unwrap();

    let mut buf = Vec::new();
    // ColorMode::Auto checks if the writer is a terminal
    // ColorMode::Always forces ANSI output
    // ColorMode::Never forces plain output
    plot.render_with_mode(&mut buf, ColorMode::Always, false).unwrap();
    print!("{}", String::from_utf8(buf).unwrap());
}

The third argument to render_with_mode indicates whether the writer is a terminal, which matters when the color mode is Auto.