Graphviz Examples in Lisp

Introduction

I wanted to automatically generate some diagrams from a database. I've done it before, in other languages, generating strings to pass on to Graphviz, and decided to look at the lisp alternatives.

Obviously I could write something to again just dump strings, but looking at the lisp libraries, there seemed to be two potential other alternatives: cl-dot and s-dot. One advantage for s-dot is that it can handle clusters and subgraphs and cl-dot cannot currently handle that type of additional granularity. Neither can currently handle tooltips and urls, but it is fairly easy to add those attributes to either library. One thing I did note with s-dot is that the dot language file it currently generates includes every attribute, regardless of whether the graph, node, edge, etc actually uses that attribute. Again, it is trivially easy to change this to have it include only the attributes which have values.

Keeping it simple and avoiding all the database stuff, I decided to 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.

We are going to be using exactly the same data for the s-dot examples and the cl-dot examples.

For the first example, we will look at an org chart and how those people are connected to multiple projects with time allocated per project. In order to generate a graph structure diagram, we need data to look at. For this example, we need people, we need projects, and we need something that connects people to projects. We'll assume that each person can have only one manager, but each person can be working on multiple projects and each project can have multiple people working on it.

Base Data

For illustrative purposes, we will use lists, but you can use arrays, hashes, etc.

(defclass person ()
  ((id :accessor id :initarg :id )
  (name :accessor name  :initarg :name )
  (manager-id :accessor manager-id  :initarg :manager-id)))
(defclass project ()
  ((id :accessor id :initarg :id )
  (name :accessor name  :initarg :name)))
(defclass person-project ()
  ((id :accessor id :initarg :id)
   (person-id :accessor person-id :initarg :person-id)
   (project-id :accessor project-id :initarg :project-id)
   (percent-allocated :accessor percent-allocated :initarg :percent-allocated)))
(defvar *person-list* (list
  (make-instance 'person :id 2 :manager-id 2 :name "Mark")
       (make-instance 'person :id 1 :manager-id 2 :name "John")
       (make-instance 'person :id 3 :manager-id 2 :name "Janet")
       (make-instance 'person :id 4 :manager-id 1 :name "Steve")
       (make-instance 'person :id 5 :manager-id 3 :name "Paul")
       (make-instance 'person :id 6 :manager-id 3 :name "Chris")))
(defvar *project-list*
        (list (make-instance 'project :id 1 :name
                                   "Project Alpha")
                    (make-instance 'project :id 2 :name
                                   "Project Beta")
                    (make-instance 'project :id 3 :name
                                   "Project Gamma")))
(defvar *person-project-list*
 (list
       (make-instance 'person-project :id 2 :person-id 1 :project-id 1 :percent-allocated 100)
       (make-instance 'person-project :id 1 :person-id 2 :project-id 1 :percent-allocated 70)
       (make-instance 'person-project :id 3 :person-id 3 :project-id 1 :percent-allocated 10)
       (make-instance 'person-project :id 4 :person-id 4 :project-id 2 :percent-allocated 40)
       (make-instance 'person-project :id 5 :person-id 5 :project-id 2 :percent-allocated 25)
       (make-instance 'person-project :id 6 :person-id 2 :project-id 3 :percent-allocated 30)
       (make-instance 'person-project :id 7 :person-id 3 :project-id 3 :percent-allocated 90)
       (make-instance 'person-project :id 8 :person-id 4 :project-id 1 :percent-allocated 60)
       (make-instance 'person-project :id 9 :person-id 5 :project-id 3 :percent-allocated 75)
       (make-instance 'person-project :id 10 :person-id 6 :project-id 2 :percent-allocated 100)))
(defvar *manager-list* ())

If you are wondering, yes, it was deliberate to not put the persons in id order, just to see how it would affect the diagrams.

Helper Functions

The following are just basic list handling functions to get information from the data. I could have set the data up in arrays, hashes, etc, but in any language, you still need the basic functions to query the data. These aren't necessarily the most efficient functions for your data set, but they are just helper functions to navigate our sample data set.

(defun find-item-by-id (id item-list)
  "Returns the item from the list with the specific id"
  (remove-if-not #'(lambda (x) (= id (id x))) item-list))
(defun find-person (id)
  "Returns an instance of person"
  (car (find-item-by-id id *person-list*)))
(defun find-project-by-id (id)
  "Returns the project with the specific id"
  (car (remove-if-not #'(lambda (x) (= id (id x))) *project-list*)))
(defun find-manager (person)
  "Returns the person's manager"
  (car  (remove-if-not #'(lambda (x) (= (manager-id person) (id x))) *person-list*)))
(defun find-projects-by-person (person)
  "Returns a list of project instances connected to the person"
 (defun find-item-by-id (id item-list)
  "Returns the item from the list with the specific id"
  (remove-if-not #'(lambda (x) (= id (id x))) item-list))
(defun find-person (id)
  "Returns an instance of person"
    (car (find-item-by-id id *person-list*)))
(defun find-project-by-id (id)
  "Returns the project with the specific id"
  (car (remove-if-not #'(lambda (x) (= id (id x))) *project-list*)))
(defun find-manager (person)
  "Returns the person's manager"
  (car  (remove-if-not #'(lambda (x) (= (manager-id person) (id x))) *person-list*)))
(defun find-projects-by-person (person)
  "Returns a list of project instances connected to the person"
  (mapcar #'(lambda (x) (find-project-by-id x))
          (mapcar #'(lambda (x) (project-id x))
                  (remove-if-not #'(lambda (x) (= (id person) (person-id x)))
                                 *person-project-list*))))
(defun find-project-percent (person project)
  "Takes an instance of a person, an instance of a project and looks
  at the person-project list and returns the number for that
  person/project combination."
  (percent-allocated
     (car
        (remove-if-not #'(lambda (x)
                            (and (= (person-id x) (id person))
                                 (= (project-id x) (id project))))
                       *person-project-list*))))
(defun has-subordinates-p (person)
  (let ((id (id person)) (mgr nil))
    (dolist (x *person-list*) 
      (when (equal id (manager-id x)) (setf mgr t) (return))) mgr))

Now you can look at either the Cl-dot example page or the s-dot example page.