On this page:
1.1 Overall Design
1.2 Library Functions
1.2.1 Creating Graphs
create-graph
create-subgraph
add-node
add-nodes
add-edge
add-edges
add-subgraph
graph->bitmap
graph->svg
graph->png
graph->dot
fsa->graph
machine->graph
1.2.2 Custom Formatters
formatters
create-formatters
1.3 Dealing with the DOT executable
find-dot-executable
has-dot-executable?
find-tmp-dir
1.4 Examples
1.4.1 Creating Basic Graphs
1.4.2 Adding Attributes
1.4.3 Creating Graphs with Formatters
1.4.4 Using Complex Data for Edges
1.4.5 Dealing with Subgraphs
8.13

1 FSM Graphviz Library🔗

Joshua Schappel
and Marco T. Morazán

 (require "interface.rkt") package: fsm

A library for creating Graphviz graphs that can be converted to the Dot Language or supported image types.

    1.1 Overall Design

    1.2 Library Functions

      1.2.1 Creating Graphs

      1.2.2 Custom Formatters

    1.3 Dealing with the DOT executable

    1.4 Examples

      1.4.1 Creating Basic Graphs

      1.4.2 Adding Attributes

      1.4.3 Creating Graphs with Formatters

      1.4.4 Using Complex Data for Edges

      1.4.5 Dealing with Subgraphs

1.1 Overall Design🔗

The graphviz library is designed to be its own entity. This means that both fsm-core and fsm-gui can work without the graphviz library. Below is a diagram of how the library interfaces with the rest of fsm.

1.2 Library Functions🔗

Below are all the exported library functions from "interface.rkt".

1.2.1 Creating Graphs🔗

procedure

(create-graph name    
  [#:fmtrs custom-formatters    
  #:atb graph-attributes])  graph?
  name : string?
  custom-formatters : formatters? = DEFAULT-FORMATTERS
  graph-attributes : (hash/c symbol? any/c)
   = (hash 'rankdir "LR")
Creates a graph where the name is the name of the generated dot language graph.

graph-attributes are a hash where the key is a symbol representing a graph attribute and the value is the value for that attribute.

custom-formatters are attribute level transformer functions that can be subscribed to a graph in order to generate custom values for attributes. These are frequently used to format how many characters of a label can exist on a single line for a label. For more information on formatters see Custom Formatters. The DEFAULT-FORMATTERS are:
(define (DEFAULT-EDGE-LABEL-FMTR lst)
  (string-join (map (lambda (v) (format "~a" v)) (reverse lst)) ", "))
 
(formatters (hash)
            (hash)
            (hash 'label DEFAULT-EDGE-LABEL-FMTR))

procedure

(create-subgraph [#:name name 
  #:atb subgraph-attributes]) 
  (or/c graph? subgraph?)
  name : symbol? = null
  subgraph-attributes : (hash/c symbol? any/c) = (hash)

See Dealing with Subgraphs for an example of using subgraphs.

If you are going to have edges between clusters you need to add the following on the subgraph attributes:
#:atb (hash 'compound true)

Creates a subgraph representation of a dot language subgraph.

name is the name of the subgraph. If the name is not provided then an anonymous subgraph is created. If the name starts with cluster then a subgraph cluster is created. For more information on subgraphs see Subgraphs and Clusters.

subgraph-attributes are a hash where the key is a symbol representing a graph attribute or cluster attribute (if the subgraph is a cluster) and the value is the value for that attribute.

procedure

(add-node parent name [#:atb node-attributes])

  (or/c graph? subgraph?)
  parent : (or/c graph? subgraph?)
  name : symbol?
  node-attributes : (hash/c symbol? any/c) = DEFAULT-NODE
Adds a node to the provided parent where the name is the name of the generated DOT node and sets the nodes label attribute to the name. Note: Since the DOT language does not allow "-" characters in node names the dashes are omitted but are still provided for the label.

node-attributes are a hash where the key is a symbol representing a node attribute and the value is the value for that attribute. The DEFAULT-NODE attributes are:
(hash 'color "black" 'shape "circle")

procedure

(add-nodes parent    
  names    
  [#:atb node-attributes])  (or/c graph? subgraph?)
  parent : (or/c graph? subgraph?)
  names : (listof symbol?)
  node-attributes : (hash/c symbol? any/c) = DEFAULT-NODE
Adds the list of nodes to the provided parent where each value in the names is the name of the generated DOT node and sets the nodes label attribute to the name. Note: Since the DOT language does not allow "-" characters in node names the dashes are omitted, but are still provided for the label.

node-attributes are a hash where the key is a symbol representing a node attribute and the value is the value for that attribute. They are applied to each of the nodes in the list. The DEFAULT-NODE attributes are:
(hash 'color "black" 'shape "circle")
Example usage:
(add-nodes (create-graph 'test) '(A B C D E-1))

procedure

(add-edge parent    
  value    
  start-node    
  end-node    
  [#:atb edge-attributes])  (or/c graph? subgraph?)
  parent : (or/c graph? subgraph?)
  value : any/c
  start-node : symbol?
  end-node : symbol?
  edge-attributes : (hash/c symbol? any/c) = (hash 'fontsize 15)
Adds a edge to the provided parent with a directional arrow from the start-node to the end-node. The label for the arrow is the value that is supplied. The edge structure stores the value as a list since we squash all edges between the same nodes into a single edge. The start-node and end-node can be the name of a cluster subgraph. If this is the case then an arrow is drawn from/to the subgraph cluster instead of a node.

Note: Since the DOT language does not allow "-" characters in node names the dashes are omitted, but are still provided for the label.

edge-attributes are a hash where the key is a symbol representing a edge attribute and the value is the value for that attribute.

procedure

(add-edges parent    
  edges    
  [#:atb edge-attributes])  (or/c graph? subgraph?)
  parent : (or/c graph? subgraph?)
  edges : (listof (list/c symbol? any/c symbol?))
  edge-attributes : (hash/c symbol? any/c) = (hash 'fontsize 15)
Adds this list of edges to the provided graph with a directional arrow from the start-node to the end-node. Note: Since the DOT language does not allow "-" characters in node names the dashes are omitted, but are still provided for the label. The start-node and end-node can be the name of a cluster subgraph. If this is the case then an arrow is drawn from/to the subgraph cluster instead of a node.

edges is a list of triples with the structure (list start-node end-node value)

edge-attributes are a hash where the key is a symbol representing a edge attribute and the value is the value for that attribute. It is applied to every value in the list.

Example usage:
(add-edges (add-nodes (create-graph 'test) '(A B C D))
           '((A a B) (B b B) (B c-1 D)))

procedure

(add-subgraph parent subgraph)  (or/c graph? subgraph?)

  parent : (or/c graph? subgraph?)
  subgraph : subgraph?
Adds the subgraph the parent. When adding a subgraph if the subgraph being added is not an anonymous subgraph then the name must be unique.

Example usage:
(define sg (create-subgraph #:name 'cluster1))
(add-subgraph (add-nodes (create-graph 'test) '(A B C D)) sg)

procedure

(graph->bitmap graph    
  [#:directory save-directory    
  #:filename filename    
  #:clean delete-files])  image?
  graph : graph?
  save-directory : path? = "system tmp directory"
  filename : string? = "__tmp__"
  delete-files : boolean? = #t
Converts the provided graph to a bitmap using htdp2-lib’s bitmap/file function. The file is saved in the provided save-directory using the provided filename.

When save-directory is not specified then the systems tmp directory is used if read and write permissions exist, otherwise it defaults to the current-directory.

When delete-files is false, then the generated ".dot" and ".png" files are not deleted.

Note: In order for the function to work one must have the DOT Complier downloaded on their machine and have a link to the DOT executable on there PATH or have the binary saved in one of the searched directories (see Dealing with the DOT executable for more details).
(define my-graph (create-graph 'test))
 
;; generate and cleanup files in the systems tmp directory using the default name
(graph->bitmap my-graph)
 
;; generate and cleanup files in using specified directory and filename
(graph->bitmap my-graph #:directory (current-directory) #:filename "test")
 
;; test.dot and test.png are not deleted
(graph->bitmap my-graph #:filename "test" #:clean #f)

procedure

(graph->svg graph    
  save-directory    
  filename    
  [#:clean delete-files])  path?
  graph : graph?
  save-directory : path?
  filename : string?
  delete-files : boolean? = #t
Converts the provided graph to a svg file and returns the path to the newly created file. The file is saved in the provided save-directory using the provided filename.

When delete-files is false the generated ".dot" file is deleted.

Note: In order for the function to work one must have the DOT Complier downloaded on their machine and have a link to the DOT executable on there PATH or have the binary saved in one of the searched directories (see Dealing with the DOT executable for more details).

procedure

(graph->png graph    
  save-directory    
  filename    
  [#:clean delete-files])  path?
  graph : graph?
  save-directory : path?
  filename : string?
  delete-files : boolean? = #t
Converts the provided graph to a png file and returns the path to the newly created file. The file is saved in the provided save-directory using the provided filename.

When delete-files is false the generated ".dot" file is deleted.

In order for the function to work one must have the DOT Complier downloaded on their machine and have a link to the DOT executable on there PATH.

procedure

(graph->dot graph save-directory filename)  path?

  graph : graph?
  save-directory : path?
  filename : string?
Converts the provided graph to a DOT language representation and returns the path to the newly created file. The file is saved in the provided save-directory using the provided filename.

procedure

(fsa->graph fsa color-blind-mode)  image?

  fsa : fsa?
  color-blind-mode : (and/c (>= n 0) (<= n 2))
Converts the provided fsa to a bitmap using htdp2-lib’s bitmap/file function. The file is saved in the users tmp directory. If the that directory is not found then it is saved in the users current directory.

color-blind-mode is a integer between 0 and 2 that represents a different set of colors to be used for the image.

procedure

(machine->graph machine    
  color-blind-mode    
  current-rule    
  current-state    
  invariant-state)  image?
  machine : machine?
  color-blind-mode : (and/c (>= n 0) (<= n 2))
  current-rule : (or/c symbol? boolean?)
  current-state : (or/c symbol? boolean?)
  invariant-state : (or/c 'pass 'fail 'none)
Converts the provided GUI machine to a bitmap using htdp2-lib’s bitmap/file function. The file is saved in the users tmp directory. If the that directory is not found then it is saved in the users current directory.

color-blind-mode is a integer between 0 and 2 that represents a different set of colors to be used for the image.

current-rule is the current rule to be highlighted. If #f is supplied then no rule is highlighted.

current-state is the current state that the machine is in. If #f is supplied then no state is highlighted.

invariant-state is a symbol representing is the invariant passed. If 'pass is supplied then the state is highlighted green. If 'fail is supplied then the state is highlighted red. If 'none is supplied then the state is not highlighted.

1.2.2 Custom Formatters🔗

Formatters are ways for implementers to customize how attribute data is generated to DOT code. If a formatter is provided for an attribute then it is used anytime DOT code is generated for that attribute.

struct

(struct formatters (graph node edge))

  graph : (hash/c symbol? (-> any/c string?))
  node : (hash/c symbol? (-> any/c string?))
  edge : (hash/c symbol? (-> any/c string?))
A structure type for formatters with the following fields:
  • graph formatters to be applied to graph level attributes.

  • node formatters to be applied to node level attributes.

  • edge formatters to be applied to edge level attributes.

For example one might add the following formatters to set one rule per line for a edge label.
;; one-rule-per-line :: listof(string) -> string
;; prints 1 rule per line
(define (one-rule-per-line rules)
  (string-join rules "\n"))
 
(define graph-formatters (formatters
                          (hash) ; graph level formatters
                          (hash) ; node level formatters
                          (hash 'label one-rule-per-line))) ; edge level formatters

procedure

(create-formatters [#:graph graph-fmtrs    
  #:node node-fmtrs    
  #:edge edge-fmtrs])  formatters?
  graph-fmtrs : (hash/c symbol? (-> any/c string?)) = (hash)
  node-fmtrs : (hash/c symbol? (-> any/c string?)) = (hash)
  edge-fmtrs : (hash/c symbol? (-> any/c string?)) = (hash)
Creates a formatters struct with the given arguments. For examples the above could be simplified to:
;; one-rule-per-line :: listof(string) -> string
;; prints 1 rule per line
(define (one-rule-per-line rules)
  (string-join rules "\n"))
 
(define fmtrs (create-formatters #:edge (hash 'label one-rule-per-line)))

1.3 Dealing with the DOT executable🔗

procedure

(find-dot-executable)  path?

procedure

(has-dot-executable?)  boolean?

Looks for the DOT executable on the system by looking at the PATH (Windows, MacOS, Linux) and specified directories (MacOS, Linux). If the executable is found, then the path to the executable is returned. The specified directories are:
  • /bin

  • /usr/bin

  • /usr/local/bin

  • /opt/local/bin

  • /opt/homebrew/bin

procedure

(find-tmp-dir)  path?

Looks for the systems tmp directory, if it exists then returns the path to that directory. If the tmp dir is not found then the current-directory from which the program is ran is used instead. Note: The directory that is returned must have read and write access.

1.4 Examples🔗

Below are examples of how to use the library.

1.4.1 Creating Basic Graphs🔗

The simplest way to create a graph is work in steps. First create the graph using create-graph. Then add the nodes to the graph using either add-node or add-nodes. Then add the edges to the graph using add-edges or add-edge. Last, convert it to the dot language.
(define init-graph (create-graph 'cgraph #:atb (hash 'rankdir "LR")))
 
(define nodes '(A-1 A-2))
 
(graph->bitmap
 (add-edge (foldr (lambda (name graph) (add-node graph name)) init-graph nodes)
           'a-name
           'A-1
           'A-2)
 #:directory (current-directory)
 #:filename "test")
produces

1.4.2 Adding Attributes🔗

Often you will want to add attributes to the graph. This is done using the #:atb keyword on the graph, edge, and node functions. Below is an example of using attributes for a graph.
(struct character (name mother father side))
 
;; character->node :: character -> graph
;; adds a character to the graph
(define (character->node c g)
  (add-node g (character-name c) #:atb (hash 'color (match (character-side c)
                                                      ['light 'green]
                                                      ['dark 'red]
                                                      ['unknown ""])
                                             'shape 'box
                                             'style 'filled)))
;; character->edge :: character -> graph
;; adds an edge to the graph
(define (character->edge c g)
  (match (cons (character-father c) (character-mother c))
    [(cons 'unknown 'unknown) g]
    [(cons 'unknown mother) (add-edge g "" mother (character-name c))]
    [(cons father 'unknown) (add-edge g "" father (character-name c))]
    [(cons father mother) (add-edge (add-edge g "" father (character-name c)) "" mother (character-name c))]))
 
 
(define skywalkers (list (character 'Shmi 'unknown 'unknown 'unknown)
                         (character 'Anakin 'Shmi 'unknown 'dark)
                         (character 'Ruwee 'unknown 'unknown 'unknown)
                         (character 'Jobal 'unknown 'unknown 'unknown)
                         (character 'Padme 'Jobal 'Ruwee 'light)
                         (character 'Luke 'Padme 'Anakin 'light)
                         (character 'Leia 'Padme 'Anakin 'light)
                         (character 'Han 'unknown 'unknown 'light)
                         (character 'Ben 'Leia 'Han 'dark)))
 
 
 
(define init-graph (create-graph 'test #:atb (hash 'label "Skywalker Family Tree")))
(define graph-with-nodes (foldl character->node init-graph skywalkers))
(define graph-with-edges (foldl character->edge graph-with-nodes skywalkers))
(graph->bitmap graph-with-edges)
produces

1.4.3 Creating Graphs with Formatters🔗

Formatters can be used to specify the output for an attribute value. For example we can use formatters to format the edge labels on a graph so that only one rule is displayed per line.
#lang racket
(require "interface.rkt")
 
;; one-rule-per-line :: listof(string) -> string
;; prints 1 rule per line
(define (one-rule-per-line rules)
  (string-join rules "\n"))
 
(define fmtrs (create-formatters #:edge (hash 'label one-rule-per-line)))
 
(graph->bitmap (add-edges (add-nodes (create-graph 'test  #:fmtrs fmtrs) '(A B C D))
                          '((A "(A a B)" B)
                            (A "(A b B)" B)
                            (B "(B a B)" C)
                            (B "(B b B)" C)
                            (B "(B c-1 B)" C)
                            (C "c-1" D)
                            (C "c-2" D))))
produces

1.4.4 Using Complex Data for Edges🔗

Sometimes it can be more intuitive to use non string data for edge labels. Edges are allowed to store any datatype for a edge label by defualt we just need to make sure that the label has a formatter to convert it to a string.
#lang racket
(require "interface.rkt")
 
;; one-rule-per-line :: listof(rules) -> string
;; prints 1 rule per line
(define (one-rule-per-line rules)
  (define string-rules (map (curry format "~a") rules))
  (string-join rules "\n"))
 
(define fmtrs (create-formatters #:edge (hash 'label one-rule-per-line)))
 
(graph->bitmap (add-edges (add-nodes (create-graph 'test  #:fmtrs fmtrs) '(A B C D))
                          '((A (A a B) B)
                            (A (A b B) B)
                            (B (B a B) C)
                            (B (B b B) C)
                            (B (B c-1 B) C)
                            (C c-1 D)
                            (C c-2 D))))
produces

1.4.5 Dealing with Subgraphs🔗

Subgraphs, specifically cluster graphs, offer a unique way of relating data. Below is an example of a subset of the Haskell Typeclass Hierarchy.
;; name: symbol
;; types: listof(symbol)
;; childs: listof(symbol)
(struct typeclass (name types childs))
 
;; subgraph-name: string -> string
;; Creates a subgraph cluster name by appending `cluster`
(define (subgraph-name s)
  (string->symbol (string-append "cluster" (symbol->string s))))
 
;; typeclass->subgraph: typeclass -> subgraph
;; Creates a subgraph for the typeclass and appends the `types` as nodes
(define (typeclass->subgraph tc)
  (foldr (lambda (n g)
           (add-node g (gensym) #:atb(hash 'label n 'fontsize 10)))
         (create-subgraph
          #:name (subgraph-name (typeclass-name tc))
          #:atb (hash 'label (typeclass-name tc)))
         (typeclass-types tc)))
 
 
;; typeclass-edges: typeclass -> listof(string string string)
;; creates a list of edges of the form: `(from label to)`
(define (typeclass-edges tc)
  (define (fmt-edge v)
    (list (subgraph-name (typeclass-name tc)) "" (subgraph-name v)))
  (map fmt-edge (typeclass-childs tc)))
 
 
 
(define data (list (typeclass 'Eq '(|All Except IO| |(->)|) '(Ord))
                   (typeclass 'Show '(|All Except IO| |(->)|) null)
                   (typeclass 'Read '(|All Except IO| |(->)|) null)
                   (typeclass 'Ord '(|All Except (->)| IO IOError) '(Real))
                   (typeclass 'Num '(Int Integer Float Double) '(Real Fractional))
                   (typeclass 'Bounded '(Int Char Bool |()| Ordering tuples) null)
                   (typeclass 'Enum '(|()| Bool Char Ordering Int Integer Float Double) '(Integral))
                   (typeclass 'Real '(Int Integer Float Double) '(RealFrac Integral))
                   (typeclass 'Fractional '(Float  Double) '(Floating Real))
                   (typeclass 'Integral '(Int Integer) null)
                   (typeclass 'RealFrac '(Float Double) '(RealFloat))
                   (typeclass 'Floating '(Float Double) '(RealFloat))
                   (typeclass 'RealFloat '(Float Double) null)
                   (typeclass 'Functor '(IO Maybe |[]|) '(Applicative Traversable))
                   (typeclass 'Applicative '(IO Maybe |[]|) '(Monad))
                   (typeclass 'Foldable '(Maybe |[]|) '(Traversable))
                   (typeclass 'Monad '(IO Maybe |[]|) null)
                   (typeclass 'Traversable '(Maybe |[]|) null)))
 
;; Create the initial graph
(define init-graph
  (create-graph 'typeclasses
                #:atb(hash 'compound true 'label "Haskell Typeclass Hierarchy")))
 
;; Add the subgraphs, nodes, and edges to the graph. Then render the image
(graph->bitmap
 (add-edges (foldl (lambda (tc g) (add-subgraph g (typeclass->subgraph tc)))
                   init-graph
                   data)
            #:atb (hash 'minlen 2)
            (append-map typeclass-edges data)))
produces the following