This tutorial is a quick introduction to integrating Graphviz structures in Rust code.
Graphviz is a marvellous format and language for visualizing graph structures. The DOT notation gives you a lot out of the box, making it flexible enough for even the most demanding use cases. You can find plenty of examples here: https://graphviz.org/gallery/
To work with Graphviz from Rust, I created and use an integration library that strictly adheres to the DOT language by mirroring its structures in Rust code: https://crates.io/crates/graphviz-rust
The library provides three core functions to work with DOT sources:
- parsing DOT sources from a string.
- constructing and printing DOT sources from Rust code into a string.
- invoking the Graphviz command-line engine from Rust code.
To start working with the library, add it to your Cargo file:
graphviz-rust = "0.1.2"
Let’s take a simple graph like this one https://graphviz.org/Gallery/undirected/process.html and try to parse it:
#[cfg(test)]
mod tests {
extern crate graphviz_rust;
use graphviz_rust::dot_structures::*;
use graphviz_rust::parse;
#[test]
fn parse_test() {
let dot_graph =
r#"
graph G {
layout=neato
run -- intr;
intr -- runbl;
runbl -- run;
run -- kernel;
kernel -- zombie;
kernel -- sleep;
kernel -- runmem;
sleep -- swap;
swap -- runswap;
runswap -- new;
runswap -- runmem;
new -- runmem;
sleep -- runmem;
}
"#;
let graph: Result<Graph, String> = parse(dot_graph);
assert!(graph.is_ok())
}
}
To construct a similar graph from Rust code, the library provides a set of macros:
#[cfg(test)]
mod tests {
extern crate graphviz_rust;
use graphviz_rust::dot_structures::*;
use graphviz_rust::dot_generator::*;
use graphviz_rust::parse;
#[test]
fn create_test() {
let dot_graph =
r#"
graph G {
layout=neato
run -- intr;
intr -- runbl;
runbl -- run;
run -- kernel;
kernel -- zombie;
kernel -- sleep;
kernel -- runmem;
sleep -- swap;
swap -- runswap;
runswap -- new;
runswap -- runmem;
new -- runmem;
sleep -- runmem;
}
"#;
let graph: Result<Graph, String> = parse(dot_graph);
assert!(graph.is_ok());
let manual_graph = graph!(id!("G");
attr!("layout","neato"),
edge!(node_id!("run") => node_id!("intr")),
edge!(node_id!("intr") => node_id!("runbl")),
edge!(node_id!("runbl") => node_id!("run")),
edge!(node_id!("run") => node_id!("kernel")),
edge!(node_id!("kernel") => node_id!("zombie")),
edge!(node_id!("kernel") => node_id!("sleep")),
edge!(node_id!("kernel") => node_id!("runmem")),
edge!(node_id!("sleep") => node_id!("swap")),
edge!(node_id!("swap") => node_id!("runswap")),
edge!(node_id!("runswap") => node_id!("new")),
edge!(node_id!("runswap") => node_id!("runmem")),
edge!(node_id!("new") => node_id!("runmem")),
edge!(node_id!("sleep") => node_id!("runmem"))
);
assert_eq!(graph.unwrap(),manual_graph);
}
}
Don’t forget to import use graphviz_rust::dot_generator::*;.
The library also provides a builder that eases construction of the different types of attributes.
Let’s consider the following graph.
Attributes can be added to the graph either using the attr! macro or via graphviz_rust::attributes::{GraphAttributes, NodeAttributes, EdgeAttributes, SubgraphAttributes}:
#[cfg(test)]
mod tests {
extern crate graphviz_rust;
use graphviz_rust::dot_structures::*;
use graphviz_rust::dot_generator::*;
use graphviz_rust::parse;
use graphviz_rust::attributes::GraphAttributes as GAttributes;
use self::graphviz_rust::attributes::{EdgeAttributes, NodeAttributes, rankdir, shape};
#[test]
fn attribute_test() {
let graph_str =
r#"
digraph finite_state_machine {
rankdir=LR;
size=8.5
node [shape = doublecircle]; 0 3 4 8;
node [shape = circle];
0 -> 2 [label = "SS(B)"];
0 -> 1 [label = "SS(S)"];
1 -> 3 [label = "S($end)"];
2 -> 6 [label = "SS(b)"];
2 -> 5 [label = "SS(a)"];
2 -> 4 [label = "S(A)"];
5 -> 7 [label = "S(b)"];
5 -> 5 [label = "S(a)"];
6 -> 6 [label = "S(b)"];
6 -> 5 [label = "S(a)"];
7 -> 8 [label = "S(b)"];
7 -> 5 [label = "S(a)"];
8 -> 6 [label = "S(b)"];
8 -> 5 [label = "S(a)"];
}
"#;
let graph: Result<Graph, String> = parse(graph_str);
assert!(graph.is_ok());
let manual_graph =
graph!(di id!("finite_state_machine");
GAttributes::rankdir(rankdir::LR),
GAttributes::size(8.5),
GraphAttributes::Node(vec![NodeAttributes::shape(shape::doublecircle)]),
node!("0"),node!("3"),node!("4"),node!("8"),
GraphAttributes::Node(vec![NodeAttributes::shape(shape::circle)]),
edge!(node_id!("0") => node_id!("2");attr!("label",esc "\"SS(B)\"")),
edge!(node_id!("0") => node_id!("1");attr!("label",esc "\"SS(S)\"")),
edge!(node_id!("1") => node_id!("3");attr!("label",esc "\"S($end)\"")),
edge!(node_id!("2") => node_id!("6");attr!("label",esc "\"SS(b)\"")),
edge!(node_id!("2") => node_id!("5");attr!("label",esc "\"SS(a)\"")),
edge!(node_id!("2") => node_id!("4");attr!("label",esc "\"S(A)\"")),
edge!(node_id!("5") => node_id!("7");attr!("label",esc "\"S(b)\"")),
edge!(node_id!("5") => node_id!("5");attr!("label",esc "\"S(a)\"")),
edge!(node_id!("6") => node_id!("6");attr!("label",esc "\"S(b)\"")),
edge!(node_id!("6") => node_id!("5");attr!("label",esc "\"S(a)\"")),
edge!(node_id!("7") => node_id!("8");attr!("label",esc "\"S(b)\"")),
edge!(node_id!("7") => node_id!("5");attr!("label",esc "\"S(a)\"")),
edge!(node_id!("8") => node_id!("6");attr!("label",esc "\"S(b)\"")),
edge!(node_id!("8") => node_id!("5");attr!("label",esc "\"S(a)\""))
);
assert_eq!(graph.unwrap(), manual_graph);
}
}
The library also provides a way to print a graph into a DOT string natively, customizing the output with graphviz_rust::printer::PrinterContext:
#[cfg(test)]
mod tests {
extern crate graphviz_rust;
use graphviz_rust::dot_structures::*;
use graphviz_rust::dot_generator::*;
use graphviz_rust::parse;
use graphviz_rust::attributes::GraphAttributes as GAttributes;
use self::graphviz_rust::attributes::{EdgeAttributes, NodeAttributes, rankdir, shape};
use self::graphviz_rust::printer::{DotPrinter, PrinterContext};
fn print_test(){
let mut g = graph!(id!("id"));
for el in (1..10).into_iter() {
if el % 2 == 0 {
g.add_stmt(stmt!(node!(el)))
} else {
g.add_stmt(stmt!(subgraph!(el)))
}
}
println!("{}",g.print(&mut PrinterContext::default()));
}
}
An even more flexible alternative is to use the Graphviz command-line engine directly:
#[cfg(test)]
mod tests {
extern crate graphviz_rust;
use std::fs;
use graphviz_rust::dot_structures::*;
use graphviz_rust::dot_generator::*;
use graphviz_rust::parse;
use graphviz_rust::attributes::GraphAttributes as GAttributes;
use self::graphviz_rust::attributes::{EdgeAttributes, NodeAttributes, rankdir, shape};
use self::graphviz_rust::cmd::{CommandArg, Format};
use self::graphviz_rust::exec;
use self::graphviz_rust::printer::{DotPrinter, PrinterContext};
#[test]
fn exec_test() {
let mut g = graph!(id!("id");
node!("nod"),
subgraph!("sb";
edge!(node_id!("a") => subgraph!(;
node!("n";
NodeAttributes::color(color_name::black), NodeAttributes::shape(shape::egg))
))
),
edge!(node_id!("a1") => node_id!(esc "a2"))
);
let p = "1.svg";
let out = exec(g.clone(), PrinterContext::default(), vec![
CommandArg::Format(Format::Svg),
CommandArg::Output(p.to_string())
]).unwrap();
assert_eq!("",out);
}
}
The source files can be found here: https://github.com/besok/test-graphviz-rust