All Manuals > CLIM 2.0 User Guide > 17 Formatted Output

17.2 Formatting Graphs in CLIM

17.2.1 Conceptual Overview of Formatting Graphs

When you need to format a graph, you specify the nodes to be in the graph and the scheme for organizing them. The CLIM graph formatter does the layout automatically, obeying any constraints that you supply.

You can format any graph in CLIM. The CLIM graph formatter is most successful with directed acyclic graphs (DAG). "Directed" means that the arcs on the graph have a direction. "Acyclic" means that there are no loops in the graph.

Here is an example of such a graph:

A Directed Acyclic Graph

To specify the elements and the organization of the graph, you provide CLIM with the following information:

Based on that information, CLIM lays out the graph for you. You can specify a number of options that control the appearance of the graph. For example, you can specify whether you want the graph to grow vertically (downward) or horizontally (to the right). Note that CLIM's algorithm does the best layout it can, but complicated graphs can be difficult to lay out in a readable way.

See 17.5 Advanced Topics for the graph formatting protocol.

17.2.2 CLIM Operators for Graph Formatting

format-graph-from-roots Function

format-graph-from-roots root-objects object-printer inferior-producer &key stream orientation cutoff-depth merge-duplicates duplicate-key duplicate-test generation-separation within-generation-separation center-nodes arc-drawer arc-drawing-options graph-type (move-cursor t)

Summary: Draws a graph whose roots are specified by the sequence root-objects. The nodes of the graph are displayed by calling the function object-printer, which takes two arguments, the node to display and a stream. inferior-producer is a function of one argument that is called on each node to produce a sequence of inferiors (or nil if there are none). Both object-printer and inferior-producer have dynamic extent.

The output from graph formatting takes place in a normalized +y-downward coordinate system. The graph is placed so that the upper left corner of its bounding rectangle is at the current text cursor position of stream. If the boolean move-cursor is t (the default), then the text cursor will be moved so that it immediately follows the lower right corner of the graph.

The returned value is the output record corresponding to the graph.

stream is an output recording stream to which output will be done. It defaults to *standard-output*.

orientation specifies the direction from root to leaves in the graph. orientation may be either :horizontal (the default) or :vertical. In LispWorks, it may also be :down or :up; :right is a synonym for :horizontal and :down is a synonym for :vertical.

cutoff-depth specifies the maximum depth of the graph. It defaults to nil, meaning that there is no cutoff depth. Otherwise it must be an integer, meaning that no nodes deeper than cutoff-depth will be formatted or displayed.

If the boolean merge-duplicates is t, then duplicate objects in the graph will share the same node in the display of the graph. That is, when merge-duplicates is t, the resulting graph will be a tree. If merge-duplicates is nil (the default), then duplicate objects will be displayed in separate nodes. duplicate-key is a function of one argument that is used to extract the node object component used for duplicate comparison; the default is identity. duplicate-test is a function of two arguments that is used to compare two objects to see if they are duplicates; the default is eql. duplicate-key and duplicate-test have dynamic extent.

generation-separation is the amount of space to leave between successive generations of the graph; the default is 20. within-generation-separation is the amount of space to leave between nodes in the same generation of the graph; the default is 10. generation-separation and within-generation-separation are specified in the same way as the y-spacing argument to formatting-table.

When center-nodes is t, each node of the graph is centered with respect to the widest node in the same generation. The default is nil.

arc-drawer is a function of seven positional and some unspecified keyword arguments that is responsible for drawing the arcs from one node to another; it has dynamic extent. The positional arguments are the stream, the "from" node, the "to" node, the "from" x and y position, and the "to" x and y position. The keyword arguments gotten from arc-drawing-options are typically line drawing options, such as for draw-line*. If arc-drawer is unsupplied, the default behavior is to draw a thin line from the "from" node to the "to" node using draw-line*.

graph-type is a keyword that specifies the type of graph to draw. CLIM supports graphs of type :tree, :directed-graph (and its synonym :digraph), and :directed-acyclic-graph (and its synonym :dag). graph-type defaults to :tree when merge-duplicates is t; otherwise, it defaults to :digraph.

The following is an example demonstrating the use of format-graph-from-roots to draw an arrow. Note that draw-arrow* is available internally.

(define-application-frame graph-it ()
  ((root-node :initform (find-class 'clim:design)
              :initarg :root-node
              :accessor root-node)
   (app-stream :initform nil :accessor app-stream))
  (:panes  (display :application
                    :display-function 'draw-display
                    :display-after-commands :no-clear))
  (:layouts
   (:defaults
    (horizontally () display))))
 
(defmethod draw-display ((frame graph-it) stream)
  (format-graph-from-roots (root-node *application-frame*)
                           #'draw-node
                           #'clos:class-direct-subclasses
                           :stream stream
                           :arc-drawer 
                           #'(lambda (stream from-object
                                             to-object x1 y1
                                             x2 y2 
                                             &rest
                                             drawing-options)
                               (declare (dynamic-extent
                                         drawing-options))
                               (declare (ignore from-object
                                                to-object))
                               (apply #'draw-arrow* stream
                                      x1 y1 x2 y2 drawing-options))
                           :merge-duplicates t)
  (setf (app-stream frame) stream))
 
(define-presentation-type node ())
 
(defun draw-node (object stream)
  (with-output-as-presentation (stream object 'node)
                               (surrounding-output-with-border
                                (stream :shape :rectangle)
                                (format stream "~A"
                                        (class-name object)))))
 
(define-graph-it-command (exit :menu "Exit") ()
  (frame-exit *application-frame*))
 
(defun graph-it (&optional (root-node (find-class 'basic-sheet))
                           (port (find-port)))
  (if (atom root-node) (setf root-node (list root-node))) 
  (let ((graph-it (make-application-frame 'graph-it
                                          :frame-manager
                                          (find-frame-manager
                                           :port port)
                                          :width 800
                                          :height 600
                                          :root-node root-node)))
    (run-frame-top-level graph-it)))

17.2.3 Examples of CLIM Graph Formatting

(defstruct node (name "") (children nil))
 
(defvar g1 (let* ((2a (make-node :name "2A"))
                  (2b (make-node :name "2B"))
                  (2c (make-node :name "2C"))
                  (1a (make-node :name "1A" :children (list 2a 2b)))
                  (1b (make-node :name "1B" :children (list 2b 2c)))) 
             (make-node :name "0" :children (list 1a 1b))))
 
(defun test-graph (root-node &rest keys) 
  (apply #'clim:format-graph-from-root root-node 
         #'(lambda (node s)
             (write-string (node-name node) s)) 
         #'node-children keys)) 

Evaluating (test-graph g1 :stream *my-window*) results in the following graph:

A Horizontal Graph

In A Horizontal Graph, the graph has a horizontal orientation and grows toward the right by default. We can supply the :orientation keyword to control this. Evaluating (test-graph g1 :stream *my-window* :orientation :vertical) results in the following graph:

A Vertical Graph

The following example uses format-graph-from-roots to create a graph with multiple parents, that is, a graph in which node D is a child of both nodes B and C. Note that it interprets its first argument as a list of top-level graph nodes, so we have wrapped the root node inside a list.

(defun test-graph (win)
  (window-clear win)
  (format-graph-from-roots '((a (b (d)) (c (d))))
                           #'(lambda (x s) (princ (car x) s))
                           #'cdr
                           :stream win
                           :orientation :vertical
                           :merge-duplicates t
                           :duplicate-key #'car)
  (force-output win))

CLIM 2.0 User Guide - 01 Dec 2021 19:39:01