Knowledge Graph Navigator User Interface Using LispWorks CAPI

As we have seen in the last two chapters the Knowledge Graph Navigator (which I will often refer to as KGN) is a tool for processing a set of entity names and automatically exploring the public Knowledge Graph DBPedia using SPARQL queries. I started to write KGN for my own use, to automate some things I used to do manually when exploring Knowledge Graphs, and later thought that KGN might also be useful for educational purposes. KGN shows the user the auto-generated SPARQL queries so hopefully the user will learn by seeing examples. KGN uses NLP code developed in earlier chapters and we will reuse that code with a short review of using the APIs. Here is a screenshot showing the application we develop here:

UI for the Knowledge Graph Navigator

We will use the KGN common library developed earlier. This example replaces the text bases UI from the last chapter and requires either the free or professional version of LispWorks to run.

The code for the CAPI user interface is found in the GitHub repository https://github.com/mark-watson/kgn-capi-ui.

Project Configuration and Running the Application

The following listing of kgn.asd shows the five packages this example depends on in addition to #:kgn-common that was developed in an earlier chapter that is referenced in the file package.lisp:

 1 ;;;; knowledgegraphnavigator.asd
 2 
 3 (asdf:defsystem #:kgn-capi-ui
 4   :description "top level Knowledge Graph Navigator package"
 5   :author "Mark Watson <markw@markwatson.com>"
 6   :license "Apache 2"
 7   :depends-on (#:kgn-common #:sparql #:kbnlp #:lw-grapher #:trivial-open-browser)
 8   :components ((:file "package")
 9                 (:file "kgn-capi-ui")
10                 (:file "option-pane")
11                 (:file "colorize")
12                 (:file "user-interface")))

Other dependency libraries specified in project.lisp are trivial-open-browser which we will use to open a web browser to URIs for human readable information on DBPedia and sparql that was developed in an earlier chapter.

Listing of package.lisp:

1 ;;;; package.lisp
2 
3 (defpackage #:kgn-capi-ui
4   (:use #:cl)
5   (:use #:kgn-common #:sparql #:lw-grapher #:trivial-open-browser)
6   (:export #:kgn-capi-ui))

The free personal edition of LispWorks does not support initialization files so you must manually load Quicklisp from the Listener Window when you first start LispWorks Personal as seen in the following repl listing (edited to remove some output for brevity). Once Quicklisp is loaded we then use ql:quickload to load the example in this chapter (some output removed for brevity):

 1 CL-USER 1 > (load "~/quicklisp/setup.lisp")
 2 ; Loading text file /Users/markw/quicklisp/setup.lisp
 3 ; Loading /Applications/LispWorks Personal 7.1/...
 4 ;; Creating system "COMM"
 5 #P"/Users/markw/quicklisp/setup.lisp"
 6 
 7 CL-USER 2 > (ql:quickload "kgn")
 8 To load "kgn":
 9   Load 1 ASDF system:
10     kgn
11 ; Loading "kgn"
12 .
13 "Starting to load data...." 
14 "....done loading data." 
15 "#P\"/Users/markw/GITHUB/common-lisp/entity-uris/entity-uris.lisp\"" 
16 "current directory:" 
17 "/Users/markw/GITHUB/common-lisp/entity-uris" 
18 "Starting to load data...." 
19 "....done loading data."
20 [package kgn]
21 To load "sqlite":
22   Load 1 ASDF system:
23     sqlite
24 ; Loading "sqlite"
25 To load "cl-json":
26   Load 1 ASDF system:
27     cl-json
28 ; Loading "cl-json"
29 To load "drakma":
30   Load 1 ASDF system:
31     drakma
32 ; Loading "drakma"
33 .To load "entity-uris":
34   Load 1 ASDF system:
35     entity-uris
36 ; Loading "entity-uris"
37 ("kgn")
38 CL-USER 3 > (kgn:kgn)
39 #<KGN::KGN-INTERFACE "Knowledge Graph Navigator" 40201E91DB>

Please note that I assume you have configured all of the examples for this book for discoverability by Quicklisp as per the section Setup for Local Quicklisp Projects in Appendix A.

When the KGN application starts a sample query is randomly chosen. Queries with many entities can take a while to process, especially when you first start using this application. Every time KGN makes a web service call to DBPedia the query and response are cached in a SQLite database in ~/.kgn_cache.db which can greatly speed up the program, especially in development mode when testing a set of queries. This caching also takes some load off of the public DBPedia endpoint, which is a polite thing to do.

I use LispWorks Professional and add two utility functions to the bottom on my ~/.lispworks configuration file (you can’t do this with LispWorks Personal):

 1 ;;; The following lines added by ql:add-to-init-file:
 2 #-quicklisp
 3 (let ((quicklisp-init
 4          (merge-pathnames
 5            "quicklisp/setup.lisp"
 6            (user-homedir-pathname))))
 7   (when (probe-file quicklisp-init)
 8     (load quicklisp-init)))
 9 
10 (defun ql (x) (ql:quickload x))
11 (defun qlp (x)
12   (ql:quickload x)
13   (SYSTEM::%IN-PACKAGE (string-upcase x) :NEW T))

Function ql is just a short alias to avoid frequently typing ql:quickload and qlp loads a Quicklisp project and then performs an in-package of the Common Lisp package with the same name as the Quicklisp project.

Utilities to Colorize SPARQL and Generated Output

When I first had the basic functionality of KGN with a CAPI UI working, I was disappointed by how the application looked as all black text on a white background. Every editor and IDE I use colorizes text in an appropriate way so I took advantage of the function capi::write-string-with-properties to easily implement color hilting SPARQL queries.

The code in the following listing is in the file kgn/colorize.lisp. When I generate SPARQL queries to show the user I use the characters “@@” as placeholders for end of lines in the generated output. In line 5 I am ensuring that there are spaces around these characters so they get tokenized properly. In the loop starting at line 7 I process the tokens checking each one to see if it should have a color associated with it when it is written to the output stream.

 1 (in-package #:kgn)
 2 
 3 (defun colorize-sparql (s  &key (stream nil))
 4   (let ((tokens (tokenize-string-keep-uri
 5                     (replace-all s "@@" " @@ ")))
 6         in-var)
 7     (dolist (token tokens)
 8       (if (> (length token) 0)
 9           (if (or in-var (equal token "?"))
10               (capi::write-string-with-properties
11                 token
12                 '(:highlight :compiler-warning-highlight)
13                 stream)
14             (if (find token '("where" "select" "distinct" "option" "filter"
15                               "FILTER" "OPTION" "DISTINCT"
16                               "SELECT" "WHERE")
17                       :test #'equal)
18                 (capi::write-string-with-properties 
19                   token
20                   '(:highlight :compiler-note-highlight)
21                   stream)
22               (if (equal (subseq token 0 1) "<")
23                   (capi::write-string-with-properties
24                     token
25                     '(:highlight :bold)
26                     stream)
27                 (if (equal token "@@")
28                     (terpri stream)
29                   (if (not (equal token "~")) (write-string token stream)))))))
30       (if (equal token "?")
31           (setf in-var t)
32         (setf in-var nil))
33       (if (and
34            (not in-var)
35            (not (equal token "?")))
36           (write-string " " stream)))
37     (terpri stream)))

Here is an example call to function colorize-sparql:

1 KGN 25 > (colorize-sparql "select ?s ?p  where {@@  ?s ?p \"Microsoft\" } @@  FILTER (lang(?comment) = 'en')")
2 select ?s ?p where { 
3  ?s ?p "Microsoft" } 
4  FILTER ( lang ( ?comment ) = 'en' ) 

Main Implementation File kgn-capi-ui.lisp

  1 ;;----------------------------------------------------------------------------
  2 ;; To try it, compile and load this file and then execute:
  3 ;;
  4 ;;      (kgn::kgn)
  5 ;;
  6 ;;----------------------------------------------------------------------------
  7 ;; Copyright (c) 2020-2022 Mark Watson. All rights reserved.
  8 ;;----------------------------------------------------------------------------
  9 
 10 (in-package #:kgn-capi-ui)
 11 
 12 (defvar *width* 1370)
 13 (defvar *best-width* 1020)
 14 (defvar *show-info-pane* t)
 15 
 16 (defvar *pane2-message*
 17   "In order to process your query a series of SPARQL queries will be formed based on the query. These generated SPARQL queries will be shown here and the reuslts of the queries will be formatted and displayed in the results display pane below.")
 18 
 19 (defvar *pane3-message*
 20   "Enter a query containing entities like people's names, companys, places, etc. following by the RETURN key to start processing your query. You can also directly use a DBPedia URI for an entity, for example: <http://dbpedia.org/resource/Apple_Inc.> When you start this application, a sample query is randomly chosen to get you started.")
 21 
 22 (defun test-callback-click (selected-node-name)
 23   (ignore-errors
 24     (format nil "* user clicked on node: ~A~%" selected-node-name)))
 25 
 26 (defun test-callback-click-shift (selected-node-name)
 27   (ignore-errors
 28     (if (equal (subseq selected-node-name 0 5) "<http")
 29         (trivial-open-browser:open-browser 
 30          (subseq selected-node-name 1 (- (length selected-node-name) 1))))
 31     (format nil "* user shift-clicked on node: ~A - OPEN WEB BROWSER~%" selected-node-name)))
 32 
 33 (defun cache-callback (&rest x) (declare (ignore x))
 34   (if *USE-CACHING*
 35       (capi:display (make-instance 'options-panel-interface))))
 36 
 37 (defun website-callback (&rest x) (declare (ignore x)) (trivial-open-browser:open-browser "http://www.knowledgegraphnavigator.com/"))
 38 
 39 (defun toggle-grapher-visibility (&rest x)
 40   (declare (ignore x))
 41   (setf *show-info-pane* (not *show-info-pane*)))
 42 
 43 (defvar *examples*)
 44 (setf *examples* '("Bill Gates and Melinda Gates at Microsoft in Seattle"
 45                    "Bill Clinton <http://dbpedia.org/resource/Georgia_(U.S._state)>"
 46                    "Bill Gates and Steve Jobs visited IBM and Microsoft in Berlin, San Francisco, Toronto, Canada"
 47                    "Steve Jobs lived near San Francisco and was a founder of <http://dbpedia.org/resource/Apple_Inc.>"
 48                    "<http://dbpedia.org/resource/Bill_Gates> visited IBM"
 49                    "<http://dbpedia.org/resource/Bill_Gates> visited <http://dbpedia.org/resource/Apple_Inc.>"
 50                    "Bill Gates visited <http://dbpedia.org/resource/Apple_Inc.>"))
 51 
 52 (capi:define-interface kgn-interface ()
 53   ()
 54   (:menus
 55    (action-menu 
 56     "Actions"
 57     (
 58      ("Copy generated SPARQL to clipboard"
 59       :callback
 60       #'(lambda (&rest x) (declare (ignore x))
 61           (let ((messages (capi:editor-pane-text text-pane2)))
 62             (capi::set-clipboard text-pane2 (format nil "---- Generated SPARQL and comments:~%~%~A~%~%" messages) nil))))
 63      ("Copy results to clipboard"
 64       :callback
 65       #'(lambda (&rest x) (declare (ignore x))
 66           (let ((results (capi:editor-pane-text text-pane3)))
 67             (capi::set-clipboard text-pane2 (format nil "---- Results:~%~%~A~%" results) nil))))
 68      ("Copy generated SPARQL and results to clipboard"
 69       :callback
 70       #'(lambda (&rest x) (declare (ignore x))
 71           (let ((messages (capi:editor-pane-text text-pane2))
 72                 (results (capi:editor-pane-text text-pane3)))
 73             (capi::set-clipboard
 74              text-pane2
 75              (format nil "---- Generated SPARQL and comments:~%~%~A~%~%---- Results:~%~%~A~%" messages results) nil))))
 76      ("Visit Knowledge Graph Navigator Web Site" :callback 'website-callback)
 77      ("Clear query cache" :callback 'cache-callback)
 78      ((if *show-info-pane*
 79           "Stop showing Grapher window for new results"
 80         "Start showing Grapher window for new results")
 81       :callback 'toggle-grapher-visibility)
 82      )))
 83   (:menu-bar action-menu)
 84   (:panes
 85    (text-pane1
 86     capi:text-input-pane
 87     :text (nth (random (length *examples*)) *examples*)
 88     :title "Query"
 89     :min-height 80
 90     :max-height 100
 91     :max-width *width*
 92     ;;:min-width (- *width* 480)
 93     :width *best-width*
 94     :callback 'start-background-thread)
 95 
 96    (text-pane2
 97     capi:collector-pane
 98     :font "Courier"
 99     :min-height 210
100     :max-height 250
101     :title "Generated SPARQL queries to get results"
102     :text "Note: to answer queries, this app makes multipe SPARQL queries to DBPedia. These SPARQL queries will be shown here."
103     :vertical-scroll t
104     :create-callback #'(lambda (&rest x)
105                          (declare (ignore x))
106                          (setf (capi:editor-pane-text text-pane2) *pane2-message*))
107     :max-width *width*
108     :width *best-width*
109     :horizontal-scroll t)
110 
111    (text-pane3
112     capi:collector-pane ;; capi:display-pane ;; capi:text-input-pane
113     :text *pane3-message*
114     :font "Courier"
115     :line-wrap-marker nil
116     :wrap-style :split-on-space
117     :vertical-scroll :with-bar
118     :title "Results"
119     :horizontal-scroll t
120     :min-height 220
121     :width *best-width*
122     :create-callback #'(lambda (&rest x)
123                          (declare (ignore x))
124                          (setf (capi:editor-pane-text text-pane3) *pane3-message*))
125     :max-height 240
126     :max-width *width*)
127    (info
128     capi:title-pane
129     :text "Use natural language queries to generate SPARQL"))
130   (:layouts
131    (main-layout
132     capi:grid-layout
133     '(nil info
134       nil text-pane1
135       nil text-pane2
136       nil text-pane3)
137      :x-ratios '(1 99)
138     :has-title-column-p t))
139   (:default-initargs
140    :layout 'main-layout
141    :title "Knowledge Graph Navigator"
142    :best-width *best-width*
143    :max-width *width*))
144 
145 (defun start-background-thread (query-text self)
146   (format t "~%** ** entering start-progress-bar-test-from-background-thread:~%~%self=~S~%~%" self)
147   (with-slots (text-pane2 text-pane3) self
148     (print text-pane2)
149     (mp:process-run-function "progress-bar-test-from-background-thread"
150                               '()
151                               'run-and-monitor-progress-background-thread
152                               query-text text-pane2 text-pane3)))
153 
154 ;; This function runs in a separate thread.
155 
156 (defun run-and-monitor-progress-background-thread (text text-pane2 text-pane3)
157   (unwind-protect
158       (setf (capi:editor-pane-text text-pane2) "")
159     (setf (capi:editor-pane-text text-pane3) "")
160     ;;(capi:display-message "done")
161     (let ((message-stream (capi:collector-pane-stream text-pane2))
162           (results-stream (capi:collector-pane-stream text-pane3)))
163       (format message-stream "# Starting to process query....~%")
164       (format results-stream *pane3-message*)
165       (let ((user-selections (prompt-selection-list (get-entity-data-helper text :message-stream message-stream))))
166         (print "***** from prompt selection list:") (print user-selections)
167         (setf (capi:editor-pane-text text-pane3) "")
168         (dolist (ev user-selections)
169           (if (> (length (cadr ev)) 0)
170               (let ()
171                 (terpri results-stream)
172                 (capi::write-string-with-properties
173                  (format nil "- - - ENTITY TYPE: ~A - - -" (car ev))
174                  '(:highlight :compiler-error-highlight) results-stream)
175                 ;;(terpri results-stream)
176                 (dolist (uri (cadr ev))
177                   (setf uri (car uri))
178                   (case (car ev)
179                     (:people
180                      (pprint-results 
181                       (kgn-common:dbpedia-get-person-detail  uri :message-stream message-stream :colorize-sparql-function #'colorize-sparql)
182                       :stream results-stream))
183                     (:companies
184                      (pprint-results 
185                       (kgn-common:dbpedia-get-company-detail uri :message-stream message-stream :colorize-sparql-function #'colorize-sparql)
186                       :stream results-stream))
187                     (:countries
188                      (pprint-results
189                       (kgn-common:dbpedia-get-country-detail uri :message-stream message-stream :colorize-sparql-function #'colorize-sparql)
190                       :stream results-stream))
191                     (:cities
192                      (pprint-results
193                       (kgn-common:dbpedia-get-city-detail    uri :message-stream message-stream :colorize-sparql-function #'colorize-sparql)
194                       :stream results-stream))
195                     (:products
196                      (pprint-results
197                       (kgn-common:dbpedia-get-product-detail uri :message-stream message-stream :colorize-sparql-function #'colorize-sparql)
198                       :stream results-stream)))))))
199 
200         (let (links x)
201           (dolist (ev user-selections)
202             (dolist (uri (second ev))
203               (setf uri (car uri))
204               (if (> (length ev) 2)
205                   (setf x (caddr ev)))
206               (setf links (cons (list (symbol-name (first ev)) uri x) links)))
207 
208           (setf
209            links
210            (append
211             links
212             (entity-results->relationship-links
213              user-selections
214              :message-stream message-stream))))
215 
216           (if
217               *show-info-pane*
218               (lw-grapher:make-info-panel-grapher '("PEOPLE" "COMPANIES" "COUNTRIES" "CITIES" "PRODUCTS" "PLACES")
219                                                   links 'test-callback-click 'test-callback-click-shift))) ;; do  not use #' !!
220         (terpri results-stream)
221         (princ "** Done wih query **" results-stream)))))
222 
223    
224 
225 ;; MAIN entry point for application:
226 
227 (defun kgn-capi-ui ()
228   ;;(ignore-errors (create-dbpedia))
229   (capi:display (make-instance 'kgn-interface)))

User Interface Utilites File user-interface.lisp

In the previous chapter, the function prompt-selection-list was defined in the file kgn-text-ui/kgn-text-ui.lisp for text based (console) UIs. Here it is implemented in a separate file user-interface.lisp in the project directory kgn-capi-ui.

 1 (in-package #:kgn-capi-ui)
 2 
 3 ;; (use-package "CAPI")
 4 
 5 (defun prompt-selection-list (a-list-of-choices) 
 6   (let (ret)
 7     (dolist (choice a-list-of-choices)
 8       (setf choice (remove-if #'null choice))
 9       (let* ((topic-type (car choice))
10              (choice-list-full (rest choice))
11              (choice-list (remove-duplicates
12                            (map 'list #'(lambda (z)
13                                           (list
14                                            z ;; (first z)
15                                            (string-shorten
16                                             (kgn-common:clean-comment
17                                              (kgn-common:clean-comment (cadr z)))
18                                             140 :first-remove-stop-words t)))
19                                 (apply #'append choice-list-full))
20                            :test #'equal)))
21         (let ((dialog-results (alexandria:flatten
22                                (capi:prompt-with-list ;; SHOW SELECTION LIST
23                                    (map 'list #'second choice-list)
24                                    (symbol-name topic-type)
25                                    :interaction :multiple-selection
26                                    :choice-class 'capi:button-panel
27                                    :pane-args '(:visible-min-width 910
28                                    :layout-class capi:column-layout))))
29               (ret2))
30           (dolist (x choice-list)
31             (if (find (second x) dialog-results)
32                 (setf ret2 (cons (car x) ret2))))
33           (if (> (length ret2) 0)
34               (setf ret (cons (list topic-type (reverse ret2)) ret))))))
35     (reverse ret)))
36 
37 ;; (get-entity-data-helper "Bill Gates went to Seattle to Microsoft")
38 ;; (prompt-selection-list
39 ;;   (get-entity-data-helper
40 ;;     "Bill Gates went to Seattle to Microsoft"))

User Interface CAPI Options Panes Definition File option-pane.lisp

In the following listing we define functions to implement CAPI menus:

  1 (in-package #:kgn-capi-ui)
  2 
  3 ;; options for:
  4 ;;  1. programming language to generate code snippets for
  5 ;;  2. colorization options (do we really need this??)
  6 ;;  3. show disk space used by caching
  7 ;;  4. option to remove local disk cache
  8 
  9 (defvar *width-options-panel* 800)
 10 
 11 (defun get-cache-disk-space ()
 12   (let ((x (ignore-errors
 13              (floor
 14                (/
 15                 (with-open-file
 16                   (file "~/Downloads/knowledge_graph_navigator_cache.db")
 17                  (file-length file)) 1000)))))
 18     (or x  0))) ;; units in megabytes
 19 
 20 (defun clear-cache-callback (&rest val)
 21   (declare (ignore val))
 22   (ignore-errors (delete-file "~/Downloads/knowledge_graph_navigator_cache.db")))
 23 
 24 (defvar *code-snippet-language* nil)
 25 (defun set-prog-lang (&rest val)
 26   (format t "* set-prog-lang: val=~S~%" val)
 27   (setf *code-snippet-language* (first val)))
 28 
 29 (capi:define-interface options-panel-interface ()
 30   ()
 31   (:panes
 32    #|
 33    (prog-lang-pane
 34     capi:option-pane
 35     :items '("No language set" "Python" "Common Lisp")
 36     :visible-items-count 6
 37     :selection (if (equal *code-snippet-language* nil)
 38                    0
 39                  (if (equal *code-snippet-language* "No language set")
 40                      0
 41                    (if (equal *code-snippet-language* "Python")
 42                        1
 43                      (if (equal *code-snippet-language* "Common Lisp")
 44                          2
 45                        0))))
 46     :interaction :single-selection
 47     :selection-callback
 48     'set-prog-lang)|#
 49    (disk-space-pane
 50      capi:text-input-pane
 51     :text (format nil "~A (megabytes)"
 52                   (let ((x
 53                          (ignore-errors
 54                            (floor
 55                              (/
 56                                (with-open-file (file "~/.kgn_cache.db")
 57                                  (file-length file))
 58                                 1000)))))
 59                     (if x
 60                         x
 61                       0)))
 62     :title "Current size of cache:"
 63     :min-width 170
 64     :max-width *width-options-panel*)
 65    (clear-disk-cache-pane
 66     capi:push-button-panel
 67     ;;:title "Clear local query cache:"
 68     :items 
 69     '("Clear local query cache")
 70     :selection-callback 
 71     #'(lambda (&rest val)
 72         (declare (ignore val))
 73         (ignore-errors (delete-file "~/.kgn_cache.db"))
 74         (ignore-errors (setf (capi:text-input-pane-text disk-space-pane)
 75                              "0 (megabytes)"))))
 76    (toggle-graph-display
 77     capi:option-pane
 78     :items '("Show Graph Info Pane Browser" "Hide Graph Info Pane Browser")
 79     :selected-item (if *show-info-pane* 0 1)
 80     ;;:title ""
 81     :selection-callback 'toggle-grapher-visibility))
 82 
 83   (:layouts
 84    (main-layout
 85     capi:grid-layout
 86     '(nil disk-space-pane
 87     nil clear-disk-cache-pane)
 88     :x-ratios '(1 99)
 89     :has-title-column-p nil))
 90   (:default-initargs
 91    :layout 'main-layout
 92    :title "Knowledge Graph Navigator Options"
 93    :max-width *width-options-panel*))
 94 
 95 ;; MAIN entry point for application:
 96 
 97 
 98 ;;         (capi:display (make-instance 'options-panel-interface))
 99 
100 (defun ui2 () (capi:display (make-instance 'options-panel-interface)))

The popup list in the last example looks like:

Popup list shows the user possible entity resolutions for each entity found in the input query. The user selects the resolved entities to use.

In this example there were two “Bill Gates” entities, one an early American frontiersman, the other the founder of Microsoft and I chose the latter person to continue finding information about.

Using LispWorks CAPI UI Toolkit

You can use the free LispWorks Personal Edition for running KGN. Using other Common Lisp implementations like Clozure-CL and SBCL will not work because the CAPI user interface library is proprietary to LispWorks. I would like to direct you to three online resources for learning CAPI:

I am not going to spend too much time in this chapter explaining my CAPI-based code. If you use LispWorks (either the free Personal or the Professional editions) you are likely to use CAPI and spending time on the official documentation and especially the included example programs is strongly recommended.

In the next section I will review the KGN specific application parts of the CAPI-based UI.

The following figure shows a popup window displaying a graph of discovered entities and relationships:

UI for info-pane-grapher

Since I just showed the info-pane-grapher this is a good time to digress to its implementation. This is in a different package and you will find the source code in src/lw-grapher/info-pane-grapher.lisp. I used the graph layout algorithm from ISI-Grapher Manual (by Gabriel Robbins). There is another utility in src/lw-grapher/lw-grapher.lisp that also displays a graph without mouse support and an attached information pane that is not used here but you might prefer it for reuse in your projects if you don’t need mouse interactions.

The graph nodes are derived from the class capi:pinboard-object:

1 (defclass text-node (capi:pinboard-object)
2   ((text :initarg :text :reader text-node-text)
3    (string-x-offset :accessor text-node-string-x-offset)
4    (string-y-offset :accessor text-node-string-y-offset)))

I customized how my graph nodes are drawn in a graph pane (this is derived from LispWorks example code):

 1 (defmethod capi:draw-pinboard-object (pinboard (self text-node)
 2                                                &key &allow-other-keys)
 3   (multiple-value-bind (X Y  width height)
 4       (capi:static-layout-child-geometry self)
 5     (let* ((half-width  (floor (1- width)  2))
 6            (half-height (floor (1- height) 2))
 7            (circle-x (+ X half-width))
 8            (circle-y (+ Y half-height))
 9            (background :white)
10            (foreground (if background
11                            :black
12                          (capi:simple-pane-foreground pinboard)))
13            (text (text-node-text self)))
14         (gp:draw-ellipse pinboard
15                            circle-x circle-y
16                            half-width half-height
17                            :filled t
18                            :foreground background)
19         (gp:draw-ellipse pinboard
20                          circle-x circle-y
21                          half-width half-height
22                          :foreground foreground)
23         (gp:draw-string pinboard
24                         text
25                         (+ X (text-node-string-x-offset self))
26                         (+ Y (text-node-string-y-offset self))
27                         :foreground foreground))))

Most of the work is done in the graph layout method that uses Gabriel Robbins’ algorithm. Here I just show the signature and we won’t go into implementation. If you are interested in modifying the layout code, I include a screen shot from ISI-Grapher manual showing the algorithm in a single page; see the file src/lw-grapher/Algorithm from ISI-Grapher Manual.png.

The following code snippets show the method signature for the layout algorithm function in the file src/lw-grapher/grapher.lisp. I also include the call to capi:graph-pane-nodes that is the CLOS reader method for getting the list of node objects in a graph pane:

1 (defun graph-layout (self &key force)
2   (declare (ignore force))
3   (let* ((nodes (capi:graph-pane-nodes self))
4     ...

The CAPI graph node model uses a function that is passed a node object and returns a list of this node’s child node objects. There are several examples of this in the CAPI graph examples that are included with LispWorks (see the CAPI documentation).

In src/lw-grapher/lw-grapher.lisp I wrote a function that builds a graph layout and instead of passing in a “return children” function I found it more convenient to wrap this process, accepting a list of graph nodes and graph edges as function arguments:

  1 (in-package :lw-grapher)
  2 
  3 ;; A Grapher (using the layout algorithm from the ISI-Grapher
  4 ;; user guide) with an info panel
  5 
  6 (defun make-info-panel-grapher (h-root-name-list h-edge-list
  7                                 h-callback-function-click
  8                                 h-callback-function-shift-click)
  9   (let (edges roots last-selected-node node-callback-click
 10       node-callback-click-shift output-pane)
 11     (labels
 12         ((handle-mouse-click-on-pane (pane x y)
 13            (ignore-errors
 14              (let ((object (capi:pinboard-object-at-position pane x y)))
 15                (if object
 16                    (let ()
 17                      (if last-selected-node
 18                          (capi:unhighlight-pinboard-object pane
 19                              last-selected-node))
 20                      (setf last-selected-node object)
 21                      (capi:highlight-pinboard-object pane object)
 22                      (let ((c-stream (collector-pane-stream output-pane))) 
 23                        (format c-stream
 24                          (funcall node-callback-click
 25                            (text-node-full-text object)))
 26                        (terpri c-stream)))))))
 27          (handle-mouse-click-shift-on-pane (pane x y)
 28            (ignore-errors
 29              (let ((object
 30                     (capi:pinboard-object-at-position pane x y)))
 31                (if object
 32                    (let ()
 33                      (if last-selected-node
 34                          (capi:unhighlight-pinboard-object
 35                            pane last-selected-node))
 36                      (setf last-selected-node object)
 37                      (capi:highlight-pinboard-object pane object)
 38                      (let ((c-stream
 39                              (collector-pane-stream output-pane)))
 40                        (format c-stream
 41                          (funcall node-callback-click-shift
 42                            (text-node-full-text object)))
 43                        (terpri c-stream)))))))
 44          
 45          (info-panel-node-children-helper (node-text)
 46            (let (ret)
 47              (dolist (e edges)
 48                (if (equal (first e) node-text)
 49                    (setf ret (cons (second e) ret))))
 50              (reverse ret)))
 51          
 52          (make-info-panel-grapher-helper
 53            (root-name-list edge-list callback-function-click
 54             callback-function-click-shift)
 55            ;; example: root-name-list: '("n1") edge-list:
 56            ;;   '(("n1" "n2") ("n1" "n3"))
 57            (setf edges edge-list
 58                  roots root-name-list
 59                  node-callback-click callback-function-click
 60                  node-callback-click-shift callback-function-click-shift)
 61            (capi:contain 
 62 
 63             (make-instance
 64              'column-layout
 65              :title "Entity Browser"
 66              :description
 67              (list
 68               (make-instance 'capi:graph-pane
 69                              :min-height 330
 70                              :max-height 420
 71                              :roots roots
 72                              :layout-function 'graph-layout
 73                              :children-function #'info-panel-node-children-helper
 74                              :edge-pane-function 
 75                              #'(lambda(self from to)
 76                                  (declare (ignore self))
 77                                  (let ((prop-name ""))
 78                                    (dolist (edge edge-list)
 79                                      (if (and
 80                                           (equal from (first edge))
 81                                           (equal to (second edge)))
 82                                          (if (and (> (length edge) 2) (third edge))
 83                                              (let ((last-index
 84                                                      (search
 85                                                       "/" (third edge) 
 86                                                       :from-end t)))
 87                                                (if last-index
 88                                                    (setf prop-name 
 89                                                     (subseq (third edge) 
 90                                                     (1+ last-index)))
 91                                                  (setf prop-name (third edge)))))))
 92                                    (make-instance 
 93                                     'capi:labelled-arrow-pinboard-object
 94                                     :data (format nil "~A" prop-name))))
 95                              :node-pinboard-class 'text-node
 96                              :input-model `(((:button-1 :release)
 97                                              ,#'(lambda (pane x y)
 98                                                   (handle-mouse-click-on-pane 
 99                                                     pane x y)))
100                                             ((:button-1 :release :shift) ;; :press)
101                                              ,#'(lambda (pane x y)
102                                                   (handle-mouse-click-shift-on-pane 
103                                                     pane x y))))
104                              :node-pane-function 'make-text-node)
105               (setf
106                output-pane
107                (make-instance 'capi:collector-pane
108                               :min-height 130
109                               :max-height 220
110                               :title "Message collection pane"
111                               :text "..."
112                               :vertical-scroll t
113                               :horizontal-scroll t))))
114             :title 
115     "Info Pane Browser: mouse click for info, mouse click + shift for web browser"
116             
117             :best-width 550 :best-height 450)))
118       (make-info-panel-grapher-helper h-root-name-list
119         h-edge-list h-callback-function-click
120         h-callback-function-shift-click))))

Wrap-up

This is a long example application for a book so I did not discuss all of the code in the project. If you enjoy running and experimenting with this example and want to modify it for your own projects then I hope that I provided a sufficient road map for you to do so.

I got the idea for the KGN application because I was spending quite a bit of time manually setting up SPARQL queries for DBPedia (and other public sources like WikiData) and I wanted to experiment with partially automating this process. I wrote the CAPI user interface for fun since this example application could have had similar functionality as a command line tool.