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:
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:
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:
- [LispWorks’ main web page introducing CAPI
- LispWorks’ comprehensive CAPI documentation for LispWorks version 7.1
- An older web site (last updated in 2011 but I find it useful for ideas): CAPI Cookbook
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:
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.