Skip to content
Andrei

Plotting a line chart with Rust's GTK+3 bindings

Rust, GTK1 min read

Since I discovered that there was bindings of GTK+3 in Rust I got excited to build something with it. After looking some examples, I thought it would be fun to build a line chart plotter. I ended up learning a lot, so I'll guide you in this post how to plot a very basic line chart using GTK and Cairo.

Requirements

First things first.

Make sure you have GTK+3 installed, chances are that if you're in a linux distro with GNOME you're already good to go and don't need to do anything. I didn't test this tutorial at Windows or MacOS, but theoretically it should work.

Let's get started

We'll need to use gtk, gio, and cairo (the graphics library) crates in this project, so edit your cargo.toml file adding these dependencies:

1[dependencies]
2gio = "^0"
3gtk = "^0"
4cairo-rs = "^0"

Now with the setup done, let's create and run our GTK application that will hold the graphics. The draw function will handle the drawing of the chart and it receives the app, the x and y axis as parameters.

1use gio::prelude::*;
2use gtk::prelude::*;
3use std::env::args;
4
5fn draw(app: &gtk::Application, x_axis: Vec<i32>, y_axis: Vec<i32>) {
6 // some code here
7}
8
9fn main() {
10 // Initilize the application with the default config
11 let application = gtk::Application::new(Some("com.andrei.gtk-line-chart"), Default::default())
12 .expect("Initialization failed...");
13
14 // The data axis we'll plot a line chart
15 let x_axis = vec![0, 1, 2, 3, 4, 5, 6, 8, 9];
16 let y_axis = vec![0, 3, 5, 4, 3, 6, 6, 7, 14];
17
18 application.connect_activate(move |app| {
19 draw(app, x_axis.clone(), y_axis.clone());
20 });
21
22 application.run(&args().collect::<Vec<_>>());
23}

So now we have our application, but we need two things to start drawing our chart: some Window and a DrawingArea inside of it. For the sake of simplicity we'll set a fixed 800x400 size for the window, and the drawing area will have a 30px padding of the window size. So here's the code inside the draw function:

1let window = gtk::ApplicationWindow::new(app);
2let drawing_area = Box::new(DrawingArea::new)();
3let size = (800.0, 400.0);
4let padding = 30.0;
5let chart_area: (f64, f64) = (size.0 - padding * 2.0, size.1 - padding * 2.0);
6
7drawing_area.connect_draw(move |_, cr| {
8 // Here we draw using the given Context
9 Inhibit(false)
10});
11
12window.set_default_size(size.0 as i32, size.1 as i32);
13
14window.add(&drawing_area);
15window.show_all();

The connect_draw closure receives the cairo's Context as parameter (cr), that's the variable we'll use to draw the lines and labels, it's pretty much just like any other canvas. So let's set the background, the font and line width.

1cr.set_source_rgb(1.0 / 255.0, 46.0 / 255.0, 64.0 / 255.0); // Background color
2cr.paint();
3
4// Set a monospace font
5cr.select_font_face("monospace", FontSlant::Normal, FontWeight::Bold);
6cr.set_font_size(12.0);
7cr.set_line_width(1.0);

Before plotting our line chart we need to normalize our data, this code below basically finds every relative point in the charting area for every data point of our data:

1let max_x = x_axis.iter().max().unwrap();
2let max_y = y_axis.iter().max().unwrap();
3let size_x = chart_area.0 / *max_x as f64;
4let size_y = chart_area.1 / *max_y as f64;
5
6let data_points = x_axis.iter().zip(y_axis.iter());
7let normalized_data: Vec<(f64, f64, f64)> = data_points
8 .map(|(x, y)| {
9 (
10 padding + size_x * *x as f64,
11 padding + chart_area.1 - size_y * *y as f64,
12 *y as f64,
13 )
14 })
15 .collect();

Now we can start drawing the grid and its labels:

1cr.set_source_rgb(79.0 / 255.0, 134.0 / 255.0, 140.0 / 255.0); // Set the grid lines color
2
3for y_grid_line in 0..=(*max_y as i32) {
4 let y_line = y_grid_line as f64 * size_y + padding;
5 cr.move_to(padding, y_line);
6 cr.line_to(size.0 - padding, y_line);
7 cr.stroke();
8
9 cr.move_to(padding / 3.0, y_line);
10 cr.show_text((max_y - y_grid_line).to_string().as_ref());
11}
12
13for x_grid_line in 0..=(*max_x as i32) {
14 let x_line = x_grid_line as f64 * size_x + padding;
15 cr.move_to(x_line, padding);
16 cr.line_to(x_line, size.1 - padding);
17 cr.stroke();
18
19 cr.line_to(x_line - 2.0, size.1 - padding / 3.0);
20 cr.show_text(x_grid_line.to_string().as_ref());
21}

And finally, we draw the line iterating the data we a window of two, source and target. We're also priting the label of the y axis above every data point:

1cr.set_line_width(2.0);
2cr.set_source_rgb(191.0 / 255.0, 186.0 / 255.0, 159.0 / 255.0); // Chart line/label color
3
4let data_window = normalized_data.windows(2);
5for points in data_window {
6 let source = points[0];
7 let target = points[1];
8
9 // Draw the line
10 cr.move_to(source.0, source.1);
11 cr.line_to(target.0, target.1);
12 cr.stroke();
13
14 // Draw the label
15 cr.move_to(target.0 - 8.0, target.1 - 10.0);
16 cr.show_text(target.2.to_string().as_ref());
17}
18
19Inhibit(false)

Awesome, now just run cargo run and you'll be gifted with this beauty:

Our amazing line chart

Conclusion

Playing with GTK bindings in Rust is super simple and straightforward. It's exciting that developers now can build complex GUI interfaces with the power of GTK and the speed and correctness of the Rust programming language.