changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > core / lisp/lib/dat/dot.lisp

changeset 698: 96958d3eb5b0
parent: 5bde4fedc5c1
author: Richard Westhaver <ellis@rwest.io>
date: Fri, 04 Oct 2024 22:04:59 -0400
permissions: -rw-r--r--
description: fixes
1 ;;; dot.lisp --- Graphviz DOT format
2 
3 ;;
4 
5 ;;; Commentary:
6 
7 ;; adapted from eschulte's graph library
8 
9 ;; ref: https://github.com/eschulte/graph/blob/master/dot.lisp
10 
11 ;;; Code:
12 (in-package :dat/dot)
13 
14 (in-readtable :std) ;; uses curry macros
15 
16 ;;; Visualization
17 (defstruct rank
18  "The information needed to specify a DOT rank statement. VALUE
19  expects a string and NODE-LIST expects a list."
20  value
21  node-list)
22 
23 (defun rank-print (r)
24  "Returns a string containing a DOT rank statement. R is a RANK structure."
25  (when (rank-p r))
26  (with-output-to-string (out)
27  (when (and (rank-value r) (rank-node-list r))
28  (format out "{rank=~a;" (rank-value r))
29  (mapc (lambda (n)
30  (format out " ~s;" n))
31  (rank-node-list r))
32  (format out " }~%"))))
33 
34 (defstruct subgraph
35  "The information needed to specify a DOT subgraph. NODE-ATTRIBUTES,
36 EDGE-ATTRIBUTES, and ATTRIBUTES expect assoc lists, and NODE-LIST
37 expects a list."
38  node-attributes
39  edge-attributes
40  attributes
41  ranks
42  node-list)
43 
44 (defun subgraph-print (s)
45  "Returns a string containing a DOT subgraph statement. S is a
46 SUBGRAPH structure."
47  (when (subgraph-p s)
48  (with-output-to-string (out)
49  (format out "subgraph ~a {~%" (string (gensym "cluster_")))
50  (when (subgraph-node-attributes s)
51  (format out " node [~a];~%"
52  (mapc (lambda (pair)
53  (format out "~a=~a, " (car pair) (cdr pair)))
54  (subgraph-node-attributes s))))
55  (when (subgraph-edge-attributes s)
56  (format out " edge [~a];~%"
57  (mapc (lambda (pair)
58  (format out "~a=~a, " (car pair) (cdr pair)))
59  (subgraph-edge-attributes s))))
60  (when (subgraph-attributes s)
61  (mapc (lambda (pair)
62  (format out " ~a=\"~a\";~%" (car pair) (cdr pair)))
63  (subgraph-attributes s)))
64  (when (subgraph-ranks s)
65  (mapcar #'rank-print (subgraph-ranks s)))
66  (when (subgraph-node-list s)
67  (mapc (lambda (n)
68  (format out " ~a;~%" n))
69  (subgraph-node-list s)))
70  (format out " }~%"))))
71 
72 (defun edge-to-dot (edge graph attrs &optional stream)
73  (format stream " \"~a\" ~a \"~a\" ~{~a~^ ~};~%"
74  (first edge)
75  (etypecase graph
76  (directed-graph "->")
77  (graph "--"))
78  (second edge)
79  (mapcar (lambda (l)
80  (destructuring-bind (attr . fn) l
81  (let ((val (funcall fn edge)))
82  (if val
83  (if (search "URL" (string attr))
84  (format nil "[~a=~a]"
85  (string-downcase
86  (string attr)
87  :end (- (length (string attr)) 3))
88  val)
89  (format nil "[~(~a~)=~a]" attr val)) ""))))
90  attrs)))
91 
92 (defun node-to-dot (node attrs &optional stream)
93  (format stream " \"~a\" ~{~a~^ ~};~%" node
94  (mapcar (lambda (l) (destructuring-bind (attr . fn) l
95  (let ((val (funcall fn node)))
96  (if val (if (search "URL" (string attr))
97  (format nil "[~a=~a]" attr val)
98  (format nil "[~(~a~)=~a]" attr val)) ""))))
99  attrs)))
100 
101 (defgeneric to-dot (graph
102  &key stream attributes node-attrs edge-attrs
103  subgraphs ranks)
104  (:documentation "Print the dot code representing GRAPH. The keyword
105 argument ATTRIBUTES takes an assoc list with DOT graph attribute (name
106 . value) pairs. NODE-ATTRS and EDGE-ATTRS also take assoc lists of DOT
107 graph attributes and functions taking nodes or edges respectively and
108 returning values. The DOT graph, node, and edge attributes are
109 described at http://www.graphviz.org/doc/info/attrs.html. SUBGRAPHS is
110 a list of SUBGRAPH structures. RANKS is a list of RANK structures."))
111 
112 (defmethod to-dot ((graph graph)
113  &key stream attributes node-attrs edge-attrs
114  subgraphs ranks)
115  ;; by default edges are labeled with their values
116  (unless (assoc :label edge-attrs)
117  (push (cons :label
118  (lambda (edge)
119  (let ((value (edge-value graph edge)))
120  (when value
121  (format nil "\"~A\"" value)))))
122  edge-attrs))
123  (format stream "~a to_dot {~%~{~a~}}~%"
124  (etypecase graph
125  (directed-graph "digraph")
126  (graph "graph"))
127  (append
128  (mapcar (lambda (l)
129  (destructuring-bind (a . b) l
130  (if (search "URL" (string a))
131  (format nil " ~a=~a;~%" a b)
132  (format nil " ~(~a~)=~a;~%" a b))))
133  attributes)
134  (mapcar {node-to-dot _ node-attrs} (hash-table-keys (nodes graph)))
135  (mapcar {edge-to-dot _ graph edge-attrs} (hash-table-keys (edges graph)))
136  (mapcar #'subgraph-print subgraphs)
137  (mapcar #'rank-print ranks))))
138 
139 (defgeneric to-dot-file (graph path &key attributes node-attrs edge-attrs
140  subgraphs ranks)
141  (:documentation "Write a dot representation of GRAPH to PATH."))
142 
143 (defmethod to-dot-file
144  ((object graph) path &key attributes node-attrs edge-attrs
145  subgraphs ranks)
146  (with-open-file (out path :direction :output :if-exists :supersede)
147  (to-dot object :stream out :attributes attributes :node-attrs node-attrs
148  :edge-attrs edge-attrs :subgraphs subgraphs :ranks ranks)))
149 
150 (defun from-dot (dot-string)
151  "Parse the DOT format string DOT-STRING into a graph.
152 More robust behavior may be achieved through parsing the output of the
153 dot executable."
154  (flet ((string->symbol (string) (intern (string-upcase string))))
155  (let* ((graph-type-re "^ *((di)?graph)")
156  (spec-re "[\\s]*(\\[([^]]+)\\])?;")
157  (node-name-re "[\\s]*\"?([a-zA-Z0-9_]+)\"?")
158  (node-spec-re (concatenate 'string node-name-re spec-re))
159  (edge-spec-re (concatenate 'string
160  node-name-re "[\\s]+([->]+)" node-name-re spec-re))
161  (label-name-re "label=(\"([^\"]+)\"|([^, ]+))[,\\]]")
162  (number-re "[0-9.\/e]+")
163  (graph (multiple-value-bind (string matches)
164  (cl-ppcre:scan-to-strings graph-type-re dot-string)
165  (declare (ignorable string))
166  (make-instance (string->symbol (aref matches 0))))))
167  ;; add nodes
168  (cl-ppcre:do-register-groups (node spec) (node-spec-re dot-string)
169  (declare (ignorable spec))
170  (unless (member node '("node" "graph") :test 'string=)
171  (add-node graph (symbolicate node))))
172  ;; add edges
173  (cl-ppcre:do-register-groups (left arrow right spec) (edge-spec-re dot-string)
174  (declare (ignorable arrow))
175  (multiple-value-bind (matchp regs) (cl-ppcre:scan-to-strings label-name-re spec)
176  (add-edge graph
177  (mapcar #'symbolicate (list left right))
178  (when matchp
179  (if (cl-ppcre:scan number-re (aref regs 1))
180  (read-from-string (aref regs 1)))))))
181  graph)))
182 
183 ;; (defun write-dot-stream (object stream)
184 ;; "Write OBJECT to STREAM in Graphviz DOT format.")
185 
186 ;; (defun write-dot-file (path object)
187 ;; "Write OBJECT to file PATH in Graphviz DOT format.")
188 
189 ;; (defun read-dot-stream (stream)
190 ;; "Read from STREAM in Graphviz DOT format.")
191 
192 ;; (defun read-dot-file (path)
193 ;; "Read from file PATH in Graphviz DOT format.")
194