Skip to content

Latest commit

 

History

History
242 lines (195 loc) · 10.8 KB

README.org

File metadata and controls

242 lines (195 loc) · 10.8 KB

Dynamic graphs in emacs

https://melpa.org/packages/dynamic-graphs-badge.svg

Motivations

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).

What this package does

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.

Filters

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.

Key bindings

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.

Examples

From function

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: ./images/full.png

Click on “A” box to open the example.com website from url.

Press `e n’ to switch to neato engine:

./images/neato.png

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.

./images/F-around.png

Press 3 to increase radius for displayed nodes and display node A as well:

./images/f-and-one.png

Type C to customize how the graph is displayed interactively: ./images/customize.png

From gv file/buffer

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

From image and imap

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.

Call graph (example)

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)} "))))

images/callers.png

Call graph (again)

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 ./images/dynamic-graphs.png Double click displays Emacs help for the functions.

Relation to other packages

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).

Some Known bugs

  • 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

Tested on

  • Windows 10 and graphviz 2.38 (old…)
  • Fedora 33 and graphviz from repositories