besok

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:

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

#Rust #Graphviz #Visualization