While it takes only few lines to make a graph in graph file language, it takes more effort to make it visually aligned with the rest of its presentation.
For me, the best way is to separate graph definition in a .gv file from definition of fonts, colors etc that can be kept in a .gvpr file that can be used as a style file.
Second motivation is peeking into parts of large graphs. Graphviz does nice job of presenting large graphs as whole, but in many case I want to be able to zoom near some particular node (original motivation was displaying close nodes in linked org files).
Make dynamic graphs: take a graph, apply some filters, and display it as an image.
The graph image to be inserted is created by a call to dynamic-graphs-display-graph. One of the parameters is a function that generates the graph description and inserts the graph code into the current buffer; Other parameters are the base name of the image buffer, identification of the root node, and filters to apply (see Filters).
(dynamic-graphs-display-graph "test" nil
(lambda ()
(insert "digraph Gr {A [URL=\"http://example.com\"] A -> B -> C -> A ->E->F->G}"))
'(2 "N {style=\"filled, rounded\",fillcolor=\"green\"}" node-refs boxize default))
In addition to the image itself, information about node positions is created (cmapx), stored in buffer-local variable, and used to map clicks to actions.
All the parameters are stored (locally) so that buffer can be reverted. It means that the function is re-evaluated on each buffer revert.
As a convenience, dynamic-graphs-display-graph-buffer uses content of the current buffer as graph code.
The filters can apply both enhancing operations (add colors, …) and more complicated operations coded in gvpr. As a special case there is a filter that removes all nodes that are more distant than a parameter from a root node.
Filters are passed as a list to the entry functions. Each list item can be:
- a string that “does not look like a path name”, that is interpreted as a gvpr string
- a path name that is interpreted as a gvpr file
- symbol remove-cycles to remove cycles (this may be dangerous)
- an integer, to remove nodes removed more than this number from the root node (if the root node is set).
In addition, named filters may be defined in dynamic-graphs-transformations, and the names defined there can be used. These predefined transformations can be used out-of-the-box are somewhat reasonable:
- boxize
- add shape=box to all nodes. Note that current imap interpretation handle only boxes
- node-refs
- for nodes without URL set, set URL to
id:name
.
See documentation string for dynamic-graphs-filters for details.
The graphs are claimed to be dynamic. As of now, it has two aspects:
- When the underlying function (buffer content, other data source, …) changes, the image itself changes.
- The engine, root node, as well as filters can be changed on fly. This can be done either by individual key bindings below, or in an interactive way using dynamic-graphs-customize-locals, bound to C. This uses widgets for local customization.
The image is displayed with a specialized minor mode. Predefined key bindings on the displayed image in this mode include:
- e (dynamic-graphs-set-engine) change grahviz engine (dot, circo, …)
- c (dynamic-graphs-remove-cycles) change whether cycles are removed
- 1-9 (dynamic-graphs-zoom-by-key) set maximum displayed distance from a root node
- mouse-1 (dynamic-graphs-shift-focus-or-follow-link) shift root node or follow link defined in imap file - that is, in URL attribute of the node. Link is followed by customizable function, by default `browse-url’ - but `org-link-open-from-string’ might be more useful. See docstring for details.
- ! saves current graph in a .gv file.
- p saves current graph in pdf format.
- / clears root (also available through C)
Subject to change (even more than the rest of package):
- ? dynamic-graphs-help shows on mouse ID alt or href of node.
- ds asks for a filter string and applies it,
- dd asks for a filter from dynamic-graphs-transformations and applies it,
- dp pops most recent filter added,
- d0 clears filters to the default value.
Run
(dynamic-graphs-display-graph "test" nil
(lambda ()
(insert "digraph Gr {A [URL=\"http://example.com\"] A -> B -> C -> A ->E->F->G}"))
'(2 "N {style=\"filled, rounded\",fillcolor=\"green\"}"
node-refs boxize default))
and you will get an image with the full graph:
Click on “A” box to open the example.com website from url.
Press `e n’ to switch to neato engine:
Click over box F to show it and nodes connected to it (radius 2
as in the filter list above). The default
filter causes the root node
to be highlighter in yellow.
Press 3 to increase radius for displayed nodes and display node A as well:
Type C to customize how the graph is displayed interactively:
See .gv and .gvpr file in examples subdirectory:
- running
dynamic-graphs-display-graph-buffer
in the example.gv buffer displays the graph - if you accepted the local variables, the style in style.gvpr is applied and single click leads changes root
- local variables can be set do define filters, root, etc
If you open a png file that has an imap file with same base name in
the same directory and turn on the dynamic-graphs-graph-mode
, the
clicks on nodes with corresponding record in the imap file are
interpreted by browse-url
command (or, in general, by current
dynamic-graphs-follow-link-fn
) and the link is open
You can try it on example.png in the examples directory; clicking to A or B should lead you to example web page or this repo on github.
Following function can prepare and display clickable call graph of an emacs lisp file.
(defun call-graph-file ()
(interactive)
(let ((byte-compile-generate-call-tree t)
(base (file-name-base (buffer-file-name))))
(save-window-excursion
(byte-compile-file (buffer-file-name)))
(dynamic-graphs-display-graph "callers" nil
(lambda ()
(insert "digraph calls {\n")
(cl-flet ((d-t-p (var)
(equal (cl-mismatch (symbol-name var) base) (length base)))
(shorten (var)
(concat ":" (substring (symbol-name var) (1+ (length base))))))
(dolist (item byte-compile-call-tree)
(when (d-t-p (car item))
(insert (format "%S [command=%S, label=%S]\n"
(symbol-name (car item))
(commandp (car item))
(shorten (car item))))
(dolist (calls (nth 2 item))
(when (d-t-p calls)
(insert (format "\"%s\" -> \"%s\"\n"
(symbol-name (car item))
calls)))))))
(insert "}\n"))
'(default boxize "N [command==\"t\"]{style=\"filled\"} N {URL=sprintf(\"help:%s\", name)} "))))
This is similar to the previous example, but a generic graph generating function is factored out:
(cl-defun dynamic-graphs-make-graph-from-list (name list filters &key params (name-fn #'car) (links-fn #'cadr) (props-fn #'cddr))
(dynamic-graphs-display-graph name nil
(lambda ()
(insert (format "digraph %S {\n" name))
(dolist (par params)
(insert (format "%s=%S\n" (car par) (cdr par))))
(dolist (item list)
(let ((open ?\[ )
(name (funcall name-fn item))
(props (funcall props-fn item)))
(insert (format "\n%S" name))
(when props
(insert " ")
(dolist (prop props)
(insert (format "%c%s=%S" open (car prop) (cdr prop)))
(setq open ?,))
(insert "]"))
(insert "\n")
(dolist (link (funcall links-fn item))
(insert (format "\t%S -> %S\n" name link)))))
(insert "}\n"))
filters))
(defun open-id-as-function-help (ref)
(when (= 3 (cl-mismatch ref "id:")))
(describe-function (intern (substring ref 3))))
and more is done via gvpr filter.
(defun call-graph-ii ()
(interactive)
(let ((byte-compile-generate-call-tree t)
(base (file-name-base (buffer-file-name))))
(save-window-excursion
(byte-compile-file (buffer-file-name)))
(dynamic-graphs-make-graph-from-list
base byte-compile-call-tree (list "filters/call-graph.gvpr" 4)
:params `((prefix . ,base))
:props-fn (lambda (a) (list (cons 'command (commandp (car a)))))
:name-fn (lambda (a) (symbol-name (car a)))
:links-fn (lambda (a) (mapcar #'symbol-name (caddr a))))
(setq dynamic-graphs-follow-link-fn #'open-id-as-function-help)))
Example output (after setting root node to :filter (single click) and limiting radius) is Double click displays Emacs help for the functions.
There is a graphviz-dot-mode package on Melpa that “helps you to create .dot or .gv files containing syntax compatible with Graphviz and use Graphviz to convert these files to diagrams”. This package does not compete on this; it tries to take existing .gv files (or buffers, or other, maybe large and generated sources) and visualize them inside Emacs with some styling and with interactive features.
There is a gvpr-mode package for editing gvpr files on Melpa.
One can use built-in `image-mode’ to view a gv file as an image (processed by dot, and no clickable links).
- The code to get URL from imap file works only for rectangles
- The code to get scale of image is too complicated and relies on undocumented, but I do not know how to do it better
- The code could use tests and then refactorization
- The way that the permament file local variables are used to maintain state between iterations does not seem satisfactory
- Does auto-revert-buffer work? If not, how to fix?
- Add useful compilers to gvpr snippet
- Windows 10 and graphviz 2.38 (old…)
- Fedora 33 and graphviz from repositories