Cl-Dot Simple Example

The first example from the cl-dot manual was the following:

(defmethod cl-dot:graph-object-node ((graph (eql 'example)) (object cons))
  (make-instance 'cl-dot:node
                 :attributes '(:label "cell \\N"
                               :shape :box)))
(defmethod cl-dot:graph-object-points-to ((graph (eql 'example)) (object cons))
  (list (car object)
        (make-instance 'cl-dot:attributed
                       :object (cdr object)
                       :attributes '(:weight 3))))
;; Symbols
(defmethod cl-dot:graph-object-node ((graph (eql 'example)) (object symbol))
  (make-instance 'cl-dot:node
                 :attributes `(:label ,object
                               :shape :hexagon
                               :style :filled
                               :color :black
                               :fillcolor "#ccccff")))
(let* ((data '(a b c #1=(b z) c d #1#))
       (dgraph (cl-dot:generate-graph-from-roots 'example (list data)
                                                 '(:rankdir "LR"))))
  (cl-dot:dot-graph dgraph "test-lr.png" :format :png))

This resulted in this diagram:

How do we get from that data set to that diagram? Look at the graph-object-node definition above. We are looking at a diagram of the cons cells, not a diagram of the list elements. Lets look at it in slow motion and look at the dot language that was produced from just the data being '(a b).

(let* ((data '(a b))
       (dgraph (cl-dot:generate-graph-from-roots 'example (list data)
                                                 '(:rankdir "LR"))))
          (cl-dot:print-graph dgraph))
digraph dg {
rankdir=LR;
  "1" [label="cell \N",shape=box];
  "3" [label="cell \N",shape=box];
  "5" [label="NIL",shape=hexagon,style=filled,color="BLACK",fillcolor="#ccccff"];
  "4" [label="B",shape=hexagon,style=filled,color="BLACK",fillcolor="#ccccff"];
  "2" [label="A",shape=hexagon,style=filled,color="BLACK",fillcolor="#ccccff"];
  "1" -> "3" [weight=3];
  "1" -> "2" [];
  "3" -> "5" [weight=3];
  "3" -> "4" [];
}

We have 2 kinds of nodes (the con cell and the symbol). Each con cell node has two pointers. One will point to the next con cell in the list and the second and will point to the symbol node. The last con cell doesn't have a symbol to point to, so it points to NIL. In doubt, read chapter 12 in PCL.

Both Michael Weber, the current maintainer of Cl-dot and Juho Snellman, the original author, are way beyond me when it comes to Lisp. So I decided to write up this tutorial (and I ended up extending cl-dot along the way). Some people are going to view this tutorial as too simple. In that case, they can write their own.

Keeping it simple and avoiding all the database stuff, we will use two sets of sample information. The first set has class instances that point to a single other class instance in a list of instances. The second set has a list of class instances and a second list showing connections. You can think of the first set as manager reporting where each person only has one manager. The second set links people to projects and indicates how much time they spend on each project.CL-Dot Routines Cl-dot is written from a CLOS perspective and has generic methods for nodes and edges that you need to write methods for. Those methods are dependent on your data and how you want your data shown. The methods you write will be tied to creating a particular graph, in this case named manager-graph. Assuming you are doing more than one type of graph, you will obviously redefine the methods to create those other types of graphs.

It is important to understand that the instance of person class is your data, it is not an instance of the node class which represents that person. Each node will have an id which is automatically generated by cl-dot for that node and that id will have no relation to the id for the associated instance of the person class.

Graph the Management Structure

(defmethod cl-dot:graph-object-node ((graph (eql 'manager-graph)) (object person))
          (let ((name (name object)))
            (make-instance 'cl-dot:node
                           :attributes `(:label ,name
                                                :shape :box))))
(defmethod cl-dot:graph-object-pointed-to-by ((graph (eql 'manager-graph)) (object person))
;;Don't want people  pointing at themselves
  (unless (= (id object) (manager-id object))
    (list
     (make-instance 'cl-dot:attributed
                    :object (find-manager object)
                    :attributes '(:weight 1)))))

Now to try it out. First, let's just check and see if it generates

what we expect in a dot file. Note that this uses the patched cl-dot

version to generate a name for the digraph.

(let ((dgraph (cl-dot:generate-graph-from-roots 'manager-graph *person-list* )))
  (cl-dot:print-graph dgraph :graph-name "Mgr1"))
digraph Mgr1 {
  "6" [label="Chris",shape=box];
  "5" [label="Paul",shape=box];
  "4" [label="Steve",shape=box];
  "3" [label="Janet",shape=box];
  "2" [label="John",shape=box];
  "1" [label="Mark",shape=box];
  "3" -> "6" [weight=1];
  "3" -> "5" [weight=1];
  "2" -> "4" [weight=1];
  "1" -> "3" [weight=1];
  "1" -> "2" [weight=1];
 }

This looks good. So replace the path name for the file with your requirements. You can also change the format to jpg, png, ps, pdf etc.

(let ((dgraph (cl-dot:generate-graph-from-roots 'manager-graph *person-list* )))
  (cl-dot:dot-graph dgraph
   "/home/sc/projects/manager-test.png" :format :png))

Ok. I'm getting what I expected.

The current version of cl-dot only uses the dot and neato graphic layout engines. It is fairly easy to patch it so that you can get different graphs from the different graphviz engines by supplying the appropriate engine name to the engine keyword.

(let ((dgraph (cl-dot:generate-graph-from-roots 'manager-graph *person-list* )))
  (cl-dot:dot-graph dgraph
  "/home/sc/projects/cl-dot-sc/test1-twopi-l.png" :format :png :engine "twopi"))

At this point it is fairly basic and boring, but at least shows the nodes and edges. Before we start trying to make it more interesting, let's look at diagraming the project / people relationships.

Diagramming the Project Structure

Now let's see if we can come up with a sensible graph for pointing people at projects and indicating, on the edges, the percentage of time each person spends on the project.

First, we need another node method for persons, this time tied to a project-graph. Then we need a node method for projects and a "points to" method showing persons pointing at projects, again all tied to a project graph.

(defmethod cl-dot:graph-object-node ((graph (eql 'project-graph)) (object person))
          (let ((name (name object)))
            (make-instance 'cl-dot:node
                           :attributes `(:label ,name
                                                :shape :box))))
(defmethod cl-dot:graph-object-node ((graph (eql 'project-graph)) (object project))
          (let ((name (name object)))
            (make-instance 'cl-dot:node
                           :attributes `(:label ,name
                                                :shape :box))))
(defmethod cl-dot:graph-object-points-to ((graph (eql 'project-graph)) (object person))
          (mapcar #'(lambda (x)
                      (let ((edge-name (format nil "~a%" (find-project-percent object x))))
                        (make-instance 'cl-dot:attributed
                                       :object x
                                       :attributes `(:weight 1 :label ,edge-name))))
                  (find-projects-by-person object)))

At this point, calling something like:

(let ((dgraph (cl-dot:generate-graph-from-roots 'project-graph *person-list*)))
  (cl-dot:dot-graph dgraph  "/home/sc/projects/cl-dot-sc/project-test1-l.png" :format :png))

results in a graph, but not a particulary attractive one. It does, however, show the people and the edges have labels showing the percentag of time spent on the project. In addition, you will note that the persons are arranged in reverse order to the person list.

There are quite a few different attributes you could throw at the graph before we start looking at the attributes of the nodes and edges themselves. For example, trading in the curved edges for straight edges (:splines "false"), changing the orientation, inserting a legend for the graph and setting a color scheme, something like this:

(let ((dgraph (cl-dot:generate-graph-from-roots 'project-graph
*person-list*
    '(:splines "false" :colorscheme "set39" :bgcolor "/set39/1" :label
    "First Graph Attribute Test" :rankdir "LR"))))
  (cl-dot:dot-graph dgraph  "/home/sc/projects/cl-dot-sc/project-test2-l.png" :format :png))

Distinguishing Data

Right now we have very little data to differentiate our team members from each other except for who reports to who. Without using distinguishing data, there is no way for Graphviz (or any other diagramming software) to visually distinguish either. We could take a couple of approaches here.

One method would be to create new subclasses of person and create new graph-object-node methods for those new subclasses, e.g. depending on whether the team member had another team member reporting to them.

A second method would be to just write in conditional attributes into the graph-object-node methods

Under either method, we will need to know whether or not someone has subordinates. OK, that is easy, we already built a helper function

named (has-subordinates-p (x)).

Now we can go back to the graph-object-node and try to use that information

(defmethod cl-dot:graph-object-node ((graph (eql 'manager-graph)) (object person))
          (let* ((name (name object))
                (attributes (if (has-subordinates-p object)
                                `(:label ,name :shape :octagon :color
          "#a05220" :style :filled)
                                `(:label ,name :shape :box :color "#405220"))))
            (make-instance 'cl-dot:node
                           :attributes attributes)))
(let ((dgraph (cl-dot:generate-graph-from-roots 'manager-graph *person-list* )))
  (cl-dot:dot-graph dgraph
   "/home/sc/projects/cl-dot-sc/test3-l.png" :format :png))

Now our managers have octagon shapes, with a filled in color and we have just changed the border color for the subordinates.

If we add two people as subordinates under John,

(push (make-instance 'person :id 7 :manager-id 1 :name "Penny") *person-list*)
(push (make-instance 'person :id 8 :manager-id 1 :name "Geoff") *person-list*)
(let ((dgraph (cl-dot:generate-graph-from-roots 'manager-graph *person-list* )))
  (cl-dot:dot-graph dgraph
   "/home/sc/projects/cl-dot-sc/test4-l.png" :format :png))

the diagram changes appropriately. You can start playing with as many attributes and data distinguishing factors as you like, you can even

try out some of the other diagraming engines.

(let ((dgraph (cl-dot:generate-graph-from-roots 'manager-graph *person-list* )))
  (cl-dot:dot-graph dgraph
   "/home/sc/projects/cl-dot-sc/test6-l.png" :format :png :engine "neato"))
(let ((dgraph (cl-dot:generate-graph-from-roots 'manager-graph *person-list* )))
  (cl-dot:dot-graph dgraph
   "/home/sc/projects/cl-dot-sc/test7-l.png" :format :png :engine "fdp"))