Media

Graphviz

Graphs in Graphviz's DOT language laid out and drawn as SVG, with no Graphviz installed: flowcharts, org charts and state machines.

Version 1.0.0 · 4 functions · 1 crate

#diagrams#svg#graphs#docs

Functions

What use graphviz.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use graphviz_svg, graphviz_save from graphviz (Quillo 0.6).

graphviz_svg(dot: str) → stror an error

The graph in dot (DOT text such as digraph { a -> b; b -> c [label="ok"]; }) laid out and drawn as an SVG. Node shapes, colours, labels, records, rankdir=LR and edge styles are understood; subgraphs (clusters) and HTML labels are not.

graphviz_save(dot: str, out: str) → stror an error

The same drawing saved at out (folders made): the path.

graphviz_check(dot: str) → any

Whether dot reads as a graph: { ok, error }, error the parser's message (null when ok). For a form where people write their own graphs.

graphviz_edges_svg(edges: list, options: any) → stror an error

A graph drawn from edges, each [from, to] or { from, to, label }, without writing DOT: arrows unless options.directed is false, top to bottom unless options.direction is "LR" (left to right). Names are any text.

Get it

$ quillo install graphviz               # graphviz.qwer into this folder$ quillo install graphviz --example     # and its example, graphviz.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/graphviz.qwer$ curl -O https://quillolang.org/plugins/files/graphviz.qwe

Then, in your program, all of it or only what you call:

main.qwe
use graphviz.qweruse graphviz_svg, graphviz_save from graphviz      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
layout-rs = "0.1"

Example

graphviz.qwe
// An order's life drawn as a state machine, and a team's reporting lines drawn from a list.////   quillo run graphviz.qwe      then open http://localhost:8080/order.svguse "graphviz.qwer"let ORDER = `digraph {    rankdir=LR;    node [shape=box, style=rounded];    new -> paid [label="pays"];    paid -> shipped [label="leaves the roastery"];    shipped -> delivered;    new -> cancelled [label="no payment in 7 days"];    cancelled [color="red"];}`route GET /order.svg = respond(200, graphviz_svg(ORDER)?, { "content-type": "image/svg+xml" })route GET /team.svg {    let team = [{ name: "Ada", boss: null }, { name: "Grace", boss: "Ada" }, { name: "Alan", boss: "Ada" }, { name: "Edsger", boss: "Grace" }]    let edges = team | filter((p) => p.boss != null) | map((p) => [p.boss, p.name])    respond(200, graphviz_edges_svg(edges, null)?, { "content-type": "image/svg+xml" })}route GET /check = { good: graphviz_check(ORDER), bad: graphviz_check("digraph { a -> }") }
$ quillo run graphviz.qwe$ curl 'localhost:8080/order.svg'$ curl 'localhost:8080/team.svg'$ curl 'localhost:8080/check'

Source

graphviz.qwer
//! Graphs written in Graphviz's DOT language laid out and drawn as SVG, without Graphviz installed: flowcharts, org charts, dependencies and state machines.//! version: 1.0.0//!//! [dependencies]//! layout-rs = "0.1"use layout::backends::svg::SVGWriter;use layout::gv::{DotParser, GraphBuilder};use serde_json::{json, Value};const MAX_DOT: usize = 512 * 1024;fn render(dot: &str) -> Result<String, String> {    if dot.len() > MAX_DOT {        return Err("the graph is larger than 512 KB of DOT".into());    }    let dot = dot.to_string();    // The layout code can panic on graphs it does not handle: that is an error value here.    std::panic::catch_unwind(move || -> Result<String, String> {        let g = DotParser::new(&dot).process().map_err(|e| format!("the DOT cannot be read: {}", e))?;        let mut gb = GraphBuilder::new();        gb.visit_graph(&g);        let mut vg = gb.get();        let mut svg = SVGWriter::new();        vg.do_it(false, false, false, &mut svg);        Ok(svg.finalize())    })    .map_err(|_| "the graph could not be laid out".to_string())?}/// The graph in `dot` (DOT text such as `digraph { a -> b; b -> c [label="ok"]; }`) laid out and drawn as an SVG./// Node shapes, colours, labels, records, `rankdir=LR` and edge styles are understood; subgraphs (clusters) and HTML labels are not.pub fn graphviz_svg(dot: &str) -> Result<String, String> {    render(dot)}/// The same drawing saved at `out` (folders made): the path.pub fn graphviz_save(dot: &str, out: &str) -> Result<String, String> {    let svg = render(dot)?;    if let Some(dir) = std::path::Path::new(out).parent().filter(|d| !d.as_os_str().is_empty()) {        std::fs::create_dir_all(dir).map_err(|e| format!("{}: {}", dir.display(), e))?;    }    std::fs::write(out, svg).map_err(|e| format!("{}: {}", out, e))?;    Ok(out.to_string())}/// Whether `dot` reads as a graph: `{ ok, error }`, `error` the parser's message (null when ok). For a form where people write their own graphs.pub fn graphviz_check(dot: &str) -> Value {    match DotParser::new(dot).process() {        Ok(_) => json!({ "ok": true, "error": null }),        Err(e) => json!({ "ok": false, "error": format!("the DOT cannot be read: {}", e) }),    }}fn quote(s: &str) -> String {    format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n"))}/// A graph drawn from `edges`, each `[from, to]` or `{ from, to, label }`, without writing DOT: arrows unless `options.directed` is false,/// top to bottom unless `options.direction` is "LR" (left to right). Names are any text.pub fn graphviz_edges_svg(edges: Vec<serde_json::Value>, options: serde_json::Value) -> Result<String, String> {    if edges.len() > 5000 {        return Err("up to 5,000 edges".into());    }    let directed = options.get("directed").and_then(Value::as_bool).unwrap_or(true);    let lr = options.get("direction").and_then(Value::as_str).map_or(false, |d| d.eq_ignore_ascii_case("lr"));    let text = |v: Option<&Value>| match v {        Some(Value::String(s)) => Some(s.clone()),        Some(Value::Number(n)) => Some(n.to_string()),        _ => None,    };    let mut dot = format!("{} {{\n", if directed { "digraph" } else { "graph" });    if lr {        dot += "rankdir=LR;\n";    }    for e in &edges {        let (from, to, label) = match e {            Value::Array(a) => (text(a.first()), text(a.get(1)), text(a.get(2))),            Value::Object(_) => (text(e.get("from")), text(e.get("to")), text(e.get("label"))),            _ => (None, None, None),        };        let (Some(from), Some(to)) = (from, to) else { return Err(format!("an edge is [from, to] or {{ from, to, label }}, not {}", e)) };        dot += &format!("{} {} {}", quote(&from), if directed { "->" } else { "--" }, quote(&to));        if let Some(l) = label {            dot += &format!(" [label={}]", quote(&l));        }        dot += ";\n";    }    dot += "}\n";    render(&dot)}
Thread
78Useful? Like it, or add to its thread below.

Thread 0 posts

On Graphviz: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.