Plotters -- Rust Drawing Library
About This Module
Prework
Prework Reading
Pre-lecture Reflections
Lecture
Learning Objectives
From the website:
Plotters is a drawing library designed for rendering figures, plots, and charts, in pure Rust.
Full documentation is at https://docs.rs/plotters/latest/plotters/.
Installation and Configuration
Rust Project
In Cargo.toml add the following dependency
[dependencies]
plotters="0.3.6"
Or inside Jupyter notebook
For 2021 edition:
:dep plotters = { version = "^0.3.6", default_features = false, features = ["evcxr", "all_series"] }
For 2024 edition, default_features became default-features (dash instead of underscore):
:dep plotters = { version = "^0.3.6", default-features = false, features = ["evcxr", "all_series"] }
Plotters Tutorial
We'll go through the interactive tutorial, reproduced here.
Import everything defined in prelude which includes evcxr_figure().
// Rust 2021 edition syntax
//:dep plotters = { version = "^0.3.6", default_features = false, features = ["evcxr", "all_series"] }
// Rust 2024 edition syntax: changed the syntax to 'default-feature'
:dep plotters = { version = "^0.3.6", default-features = false, features = ["evcxr", "all_series"] }
extern crate plotters;
// Import all the plotters prelude functions
use plotters::prelude::*;
// To create a figure that can be displayed in Jupyter notebook, use evcxr_figure function.
// The first param is the resolution of the figure.
// The second param is the closure that performes the drawing.
evcxr_figure((300, 100), |name| {
// Do the drawings
name.fill(&BLUE)?;
// Tell plotters that everything is ok
Ok(())
})
evcxr_figure((xsize, ysize), |name| { your code })is how you make a figure of a certain size and put things in it.- name is the handle for accessing the figure
//:dep plotters = { version = "^0.3.0", default_features = false, features = ["evcxr", "all_series"] }
extern crate plotters;
use plotters::prelude::*;
evcxr_figure((320,50), |root| {
root.fill(&GREEN)?;
root.draw(&Text::new("Hello World from Plotters!", (15, 15), ("Arial", 20).into_font()))?;
Ok(())
})
Sub-Drawing Areas
The object created by evcxr_figure is a DrawingArea
DrawingArea is a key concept and represents the handle into which things will be actually drawn. Plotters supports different types of drawing areas depending on context.
- Inside jupyter notebook the type of drawing area is an SVG (Scalable Vector Graphics) area
- When used from the termina the most common type is a BitMapBackend
For full documentation on what you can do with DrawingArea see https://docs.rs/plotters/latest/plotters/drawing/struct.DrawingArea.html
Key capabilities:
- fill: Fill it with a background color
- draw_mesh: Draw a mesh on it
- draw_text: Add some text in graphic form
- present: make it visible (may not be neeed in all backend types)
- titled: Add a title and return the remaining area
- split_*: Split it into subareas in a variety of ways
Split Drawing Areas Example
We can make a Sierpiński carpet by splitting the drawing areas and recursion function.
The Sierpiński carpet is a plane fractal first described by Wacław Sierpiński in 1916. The carpet is a generalization of the Cantor set to two dimensions...
//:dep plotters = { version = "^0.3.0", default_features = false, features = ["evcxr", "all_series"] }
extern crate plotters;
use plotters::prelude::*;
use plotters::coord::Shift;
pub fn sierpinski_carpet(
depth: u32,
drawing_area: &DrawingArea<SVGBackend, Shift>) -> Result<(), Box<dyn std::error::Error>> {
if depth > 0 {
// Split the drawing area into 9 equal parts
let sub_areas = drawing_area.split_evenly((3,3));
// Iterate over the sub-areas
for (idx, sub_area) in (0..).zip(sub_areas.iter()) {
if idx == 4 { // idx == 4 is the center sub-area
// If the sub-area is the center one, fill it with white
sub_area.fill(&WHITE)?;
} else {
sierpinski_carpet(depth - 1, sub_area)?;
}
}
}
Ok(())
}
evcxr_figure((480,480), |root| {
root.fill(&BLACK)?;
sierpinski_carpet(5, &root)
}).style("width: 600px") /* You can add CSS style to the result */
/* Note: doesn't work in VSCode/Cursor */
Charts
Drawing areas are too basic for scientific drawings so the next important concept is a chart
Charts can be used to plot functions, datasets, bargraphs, scatterplots, 3D Objects and other stuff.
Full documentation at https://docs.rs/plotters/latest/plotters/chart/struct.ChartBuilder.html and https://docs.rs/plotters/latest/plotters/chart/struct.ChartContext.html
//:dep plotters = { version = "^0.3.0", default_features = false, features = ["evcxr", "all_series"] }
extern crate plotters;
use plotters::prelude::*;
evcxr_figure((640, 240), |root| {
// The following code will create a chart context
let mut chart = ChartBuilder::on(&root)
// the caption for the chart
.caption("Hello Plotters Chart Context!", ("Arial", 20).into_font())
// the X and Y coordinates spaces for the chart
.build_cartesian_2d(0f32..1f32, 0f32..1f32)?;
// Then we can draw a series on it!
chart.draw_series((1..10).map(|x|{
let x = x as f32/10.0;
Circle::new((x,x), 5, &RED)
}))?;
Ok(())
}).style("width:60%")
Common chart components
Adding a mesh, and X and Y labels
//:dep plotters = { version = "^0.3.0", default_features = false, features = ["evcxr", "all_series"] }
extern crate plotters;
use plotters::prelude::*;
evcxr_figure((640, 480), |root| {
// The following code will create a chart context
let mut chart = ChartBuilder::on(&root)
.caption("Chart with Axis Label", ("Arial", 20).into_font())
.x_label_area_size(80)
.y_label_area_size(80)
.build_cartesian_2d(0f32..1f32, 0f32..1f32)?;
chart.configure_mesh()
.x_desc("Here's the label for X")
.y_desc("Here's the label for Y")
.draw()?;
// Then we can draw a series on it!
chart.draw_series((1..10).map(|x|{
let x = x as f32/10.0;
Circle::new((x,x), 5, &RED)
}))?;
Ok(())
}).style("width: 60%")
Then let's disable mesh lines for the X axis
//:dep plotters = { version = "^0.3.0", default_features = false, features = ["evcxr", "all_series"] }
extern crate plotters;
use plotters::prelude::*;
evcxr_figure((640, 480), |root| {
// The following code will create a chart context
let mut chart = ChartBuilder::on(&root)
.caption("Chart Context with Mesh and Axis", ("Arial", 20).into_font())
.x_label_area_size(40)
.y_label_area_size(40)
.build_cartesian_2d(0f32..1f32, 0f32..1f32)?;
chart.configure_mesh()
.y_labels(10)
.light_line_style(&TRANSPARENT)
.disable_x_mesh()
.draw()?;
// Then we can draw a series on it!
chart.draw_series((1..10).map(|x|{
let x = x as f32/10.0;
Circle::new((x,x), 5, &RED)
}))?;
Ok(())
}).style("width: 60%")
Adding subcharts
Simple. Split your drawing area and then add a chart in each of the split portions
//:dep plotters = { version = "^0.3.0", default_features = false, features = ["evcxr", "all_series"] }
extern crate plotters;
use plotters::prelude::*;
evcxr_figure((640, 480), |root| {
let sub_areas = root.split_evenly((2,2));
for (idx, area) in (1..).zip(sub_areas.iter()) {
// The following code will create a chart context
let mut chart = ChartBuilder::on(&area)
.caption(format!("Subchart #{}", idx), ("Arial", 15).into_font())
.x_label_area_size(40)
.y_label_area_size(40)
.build_cartesian_2d(0f32..1f32, 0f32..1f32)?;
chart.configure_mesh()
.y_labels(10)
.light_line_style(&TRANSPARENT)
.disable_x_mesh()
.draw()?;
// Then we can draw a series on it!
chart.draw_series((1..10).map(|x|{
let x = x as f32/10.0;
Circle::new((x,x), 5, &RED)
}))?;
}
Ok(())
}).style("width: 60%")
Drawing on Charts with the Series Abstraction
-
Unlike most of the plotting libraries,
Plottersdoesn't actually define any types of chart. -
All the charts are abstracted to a concept of series.
- By doing so, you can put a histgoram series and a line plot series into the same chart context.
-
The series is actually defined as an iterator of elements.
This gives Plotters a huge flexibility on drawing charts. You can implement your own types of series and uses the coordinate translation and chart elements.
There are few types of predefined series, just for convenience:
- Line Series
- Histogram
- Point Series
Scatter Plot
First, generate random numbers
:dep rand = { version = "0.6.5" }
//:dep plotters = { version = "^0.3.0", default_features = false, features = ["evcxr", "all_series"] }
extern crate rand;
use rand::distributions::Normal;
use rand::distributions::Distribution;
use rand::thread_rng;
let sd = 0.13;
let random_points:Vec<(f64,f64)> = {
let mut norm_dist = Normal::new(0.5, sd);
let (mut x_rand, mut y_rand) = (thread_rng(), thread_rng());
let x_iter = norm_dist.sample_iter(&mut x_rand);
let y_iter = norm_dist.sample_iter(&mut y_rand);
x_iter.zip(y_iter).take(1000).collect()
};
println!("{}", random_points.len());
1000
To draw the series, we provide an iterator on the elements and then map a closure.
extern crate plotters;
use plotters::prelude::*;
evcxr_figure((480, 480), |root| {
// The following code will create a chart context
let mut chart = ChartBuilder::on(&root)
.caption("Normal Distribution w/ 2 sigma", ("Arial", 20).into_font())
.x_label_area_size(40)
.y_label_area_size(40)
.build_ranged(0f64..1f64, 0f64..1f64)?;
chart.configure_mesh()
.disable_x_mesh()
.disable_y_mesh()
.draw()?;
// Draw little green circles. Remember that closures can capture variables from the enclosing scope
chart.draw_series(random_points.iter().map(|(x,y)| Circle::new((*x,*y), 3, GREEN.filled())));
// You can always freely draw on the drawing backend. So we can add background after the fact
let area = chart.plotting_area();
let two_sigma = sd * 2.0;
let chart_width = 480;
let radius = two_sigma * chart_width as f64; // circle radius is in pixels not chart coords
area.draw(&Circle::new((0.5, 0.5), radius, RED.mix(0.3).filled()))?;
area.draw(&Cross::new((0.5, 0.5), 5, &RED))?;
Ok(())
}).style("width:60%")
Histograms
We can also have histograms. For histograms, we can use the predefined histogram series struct to build the histogram easily. The following code demonstrate how to create both histogram for X and Y value of random_points.
// Rust 2021
//:dep plotters = { version = "^0.3.0", default_features = false, features = ["evcxr", "all_series", "all_elements"] }
// Rust 2024
:dep plotters = { version = "^0.3.0", default-features = false, features = ["evcxr", "all_series", "all_elements"] }
extern crate plotters;
use plotters::prelude::*;
evcxr_figure((640, 480), |root| {
let areas = root.split_evenly((2,1));
let mut charts = vec![];
// The following code will create a chart context
for (area, name) in areas.iter().zip(["X", "Y"].into_iter()) {
let mut chart = ChartBuilder::on(&area)
.caption(format!("Histogram for {}", name), ("Arial", 20).into_font())
.x_label_area_size(40)
.y_label_area_size(40)
.build_cartesian_2d(0u32..100u32, 0f64..0.5f64)?;
chart.configure_mesh()
.disable_x_mesh()
.disable_y_mesh()
.y_labels(5)
.x_label_formatter(&|x| format!("{:.1}", *x as f64 / 100.0))
.y_label_formatter(&|y| format!("{}%", (*y * 100.0) as u32))
.draw()?;
charts.push(chart);
}
// Histogram is just another series but a nicely encapsulated one
let hist_x = Histogram::vertical(&charts[0])
.style(RED.filled())
.margin(0)
.data(random_points.iter().map(|(x,_)| ((x*100.0) as u32, 0.01)));
let hist_y = Histogram::vertical(&charts[0])
.style(GREEN.filled())
.margin(0)
.data(random_points.iter().map(|(_,y)| ((y*100.0) as u32, 0.01)));
charts[0].draw_series(hist_x);
charts[1].draw_series(hist_y);
Ok(())
}).style("width:60%")
Fancy combination of histogram and scatter
Split the drawing area in 3 parts and draw two histograms and a scatter plot
//:dep plotters = { version = "^0.3.0", default_features = false, features = ["evcxr", "all_series", "all_elements"] }
use plotters::prelude::*;
evcxr_figure((640, 480), |root| {
let root = root.titled("Scatter with Histogram Example", ("Arial", 20).into_font())?;
// Split the drawing area into a grid with specified X and Y breakpoints
let areas = root.split_by_breakpoints([560], [80]);
let mut x_hist_ctx = ChartBuilder::on(&areas[0])
.y_label_area_size(40)
.build_cartesian_2d(0u32..100u32, 0f64..0.5f64)?;
let mut y_hist_ctx = ChartBuilder::on(&areas[3])
.x_label_area_size(40)
.build_cartesian_2d(0f64..0.5f64, 0..100u32)?;
let mut scatter_ctx = ChartBuilder::on(&areas[2])
.x_label_area_size(40)
.y_label_area_size(40)
.build_cartesian_2d(0f64..1f64, 0f64..1f64)?;
scatter_ctx.configure_mesh()
.disable_x_mesh()
.disable_y_mesh()
.draw()?;
scatter_ctx.draw_series(random_points.iter().map(|(x,y)| Circle::new((*x,*y), 3, GREEN.filled())))?;
let x_hist = Histogram::vertical(&x_hist_ctx)
.style(RED.filled())
.margin(0)
.data(random_points.iter().map(|(x,_)| ((x*100.0) as u32, 0.01)));
let y_hist = Histogram::horizontal(&y_hist_ctx)
.style(GREEN.filled())
.margin(0)
.data(random_points.iter().map(|(_,y)| ((y*100.0) as u32, 0.01)));
x_hist_ctx.draw_series(x_hist)?;
y_hist_ctx.draw_series(y_hist)?;
Ok(())
}).style("width:60%")
Drawing Lines
It's stil using the draw_series call with the convenient wrapper of LineSeries.
//:dep plotters = { version = "^0.3.0", default_features = false, features = ["evcxr", "all_series", "all_elements"] }
use plotters::prelude::*;
evcxr_figure((640, 480), |root_area| {
root_area.fill(&WHITE)?;
let root_area = root_area.titled("Line Graph", ("sans-serif", 60))?;
let x_axis = (-3.4f32..3.4).step(0.1);
let mut cc = ChartBuilder::on(&root_area)
.margin(5)
.set_all_label_area_size(50)
.caption("Sine and Cosine", ("sans-serif", 40))
.build_cartesian_2d(-3.4f32..3.4, -1.2f32..1.2f32)?;
cc.configure_mesh()
.x_labels(20)
.y_labels(10)
.disable_mesh()
.x_label_formatter(&|v| format!("{:.1}", v))
.y_label_formatter(&|v| format!("{:.1}", v))
.draw()?;
cc.draw_series(LineSeries::new(x_axis.values().map(|x| (x, x.sin())), &RED))?
.label("Sine")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED));
cc.draw_series(LineSeries::new(x_axis.values().map(|x| (x, x.cos())), &BLUE,))?
.label("Cosine")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], BLUE));
cc.configure_series_labels().border_style(BLACK).draw()?;
Ok(())
}).style("width:60%")
3D Plotting
Big difference is in the ChartBuilder call. Instead of build_cartesian_2d we use build_cartesian_3d.
Unlike the 2D plots, 3D plots use the function configure_axes to configure the chart components.
//:dep plotters = { version = "^0.3.0", default_features = false, features = ["evcxr", "all_series", "all_elements"] }
use plotters::prelude::*;
evcxr_figure((640, 480), |root| {
let root = root.titled("3D Plotting", ("Arial", 20).into_font())?;
let mut chart = ChartBuilder::on(&root)
.build_cartesian_3d(-10.0..10.0, -10.0..10.0, -10.0..10.0)?;
chart.configure_axes().draw()?;
// Draw a red circle parallel to XOZ panel
chart.draw_series(LineSeries::new(
(-314..314).map(|a| a as f64 / 100.0).map(|a| (8.0 * a.cos(), 0.0, 8.0 *a.sin())),
&RED,
))?;
// Draw a green circle parallel to YOZ panel
chart.draw_series(LineSeries::new(
(-314..314).map(|a| a as f64 / 100.0).map(|a| (0.0, 8.0 * a.cos(), 8.0 *a.sin())),
&GREEN,
))?;
Ok(())
})
For more examples check
https://plotters-rs.github.io/plotters-doc-data/evcxr-jupyter-integration.html
What about using it from the terminal?
The key difference is in how you define your drawing area.
-
Inside Jupyter notebook we create a drawing area using evcxr_figure
-
In the terminal context we create a drawing area using
#![allow(unused)] fn main() { let root = BitMapBackend::new("0.png", (640, 480)).into_drawing_area(); // or let root = SVGBackend::new("0.svg", (1024, 768)).into_drawing_area(); // or let root = BitMapBackend::gif("0.gif", (600, 400), 100)?.into_drawing_area(); }
Let's take a look on the terminal example (demo).
What if you don't want output to a file or a browser but standalone application?
Things get very messy and machine specific there. You need to integrate with the underlying OS graphics terminal libraries. For MacOS and Linux this is the the CairoBackend library but I don't know what it is for Windows
Here's an example from the terminal using GTK.
On MacOS, install these dependencies first:
brew install gtk4
brew install pkg-config
Then cargo run in (plotters-gtk-demo).