1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
|
;;; graph.lisp --- because its easier to write than to learn such a library
;; Copyright (C) Eric Schulte 2013
;; Licensed under the Gnu Public License Version 3 or later
;;; Commentary
;; Helper function for serializing Graphs to JSON objects and reading
;; JSON objects back into graphs. The JSON syntax for a graph is
;; compatible with the JavaScript d3 visualization library, allowing
;; for interactive viewing of graphs in the browser.
;; See [d3](http://d3js.org/)
;; (specifically [d3-force](http://bl.ocks.org/4062045)) for more.
;;; Code:
(defpackage #:graph/json
(:use
:common-lisp
:alexandria
:metabang-bind
:named-readtables
:curry-compose-reader-macros
:graph
:yason)
(:export
:to-json
:from-json
:to-d3
:from-d3
:to-html))
(in-package :graph/json)
(in-readtable :curry-compose-reader-macros)
(defun json-to-plist (input)
"Parse string or stream INPUT into a plist."
(let ((yason:*parse-object-key-fn*
(lambda (el) (intern (string-upcase el) "KEYWORD")))
(yason:*parse-object-as* :plist))
(yason:parse input)))
;;; JSON import and export
(defmethod yason:encode ((symbol symbol) &optional (stream *standard-output*))
(yason:encode (string-downcase (symbol-name symbol)) stream))
(defgeneric to-json (graph &key stream node-fn edge-fn)
(:documentation "Write a JSON encoding of GRAPH to STREAM."))
(defmethod to-json
((graph graph) &key (stream *standard-output*) node-fn edge-fn)
(let ((plist (to-plist graph :node-fn node-fn :edge-fn edge-fn)))
(yason:encode
(plist-hash-table
(list :nodes (mapcar #'plist-hash-table (getf plist :nodes))
:edges (mapcar #'plist-hash-table (getf plist :edges))))
stream)))
(defun intern-string-nodes (plist)
(list :nodes (mapcar [{list :name} #'intern #'string-upcase {getf _ :name}]
(getf plist :nodes))
:edges (getf plist :edges)))
(defmethod from-json ((graph graph) input)
"Parse string or stream INPUT into GRAPH."
(from-plist graph (intern-string-nodes (json-to-plist input))))
;;; plist and D3 conversion
(defun plist-to-d3 (plist)
"Convert plist graph encoding PLIST to D3 format.
Note that D3 only handles 2-node edges, so extra nodes in edges will
be silently dropped."
(list :nodes (getf plist :nodes)
:links (mapcar (lambda (edge)
(let ((edge (getf edge :edge))
(value (getf edge :value)))
(list :source (first edge)
:target (second edge)
:value value)))
(getf plist :edges))))
(defun d3-to-plist (plist)
"Convert D3 format PLIST to graph encoding."
(list :nodes (mapcar [{list :name} #'intern #'string-upcase {getf _ :name}]
(getf plist :nodes))
:edges (mapcar (lambda (edge)
(list :edge (list (getf edge :source)
(getf edge :target))
:value (getf edge :value)))
(getf plist :links))))
;;; D3 format JSON import and export
(defgeneric to-d3 (graph &key stream group-fn)
(:documentation "Return a JSON encoding of GRAPH formatted for D3.
Edges should have numeric values which d3 will translate into their
width. Optional keyword argument GROUP-FN should be a function from
nodes to group numbers."))
(defmethod to-d3 ((graph graph) &key (stream *standard-output*) group-fn)
(let* ((plist (plist-to-d3
(if group-fn
(to-plist graph :node-fn [{list :group} group-fn])
(to-plist graph))))
(hash (plist-hash-table
(list :nodes (mapcar #'plist-hash-table (getf plist :nodes))
:links (mapcar #'plist-hash-table (getf plist :links))))))
(if stream
(yason:encode hash stream)
(with-output-to-string (out)
(yason:encode hash out)))))
(defgeneric from-d3 (graph input)
(:documentation "Parse a D3 format string or stream INPUT into GRAPH."))
(defmethod from-d3 ((graph graph) input)
(from-plist graph (d3-to-plist (json-to-plist input))))
(defgeneric to-html (graph &key stream group-fn)
(:documentation "Write GRAPH to an HTML file.
The resulting HTML file will display an interactive version of GRAPH.
Uses `to-d3' to first encode the graph as JSON which is embedded in
the HTML page."))
(defmethod to-html ((graph graph) &key (stream *standard-output*) group-fn)
(format stream d3-html (to-d3 graph :stream nil :group-fn group-fn)))
(defvar d3-html
"<!DOCTYPE html>
<!-- Taken from http://bl.ocks.org/4062045 -->
<html>
<style>
.node { stroke: #fff; stroke-width: 1.5px; }
.link { stroke: #999; stroke-opacity: .6; }
</style>
<body>
<script src=\"http://d3js.org/d3.v3.min.js\"></script>
<script>
var width = \"innerWidth\" in window
? window.innerWidth
: document.documentElement.offsetWidth;
var height = \"innerHeight\" in window
? window.innerHeight
: document.documentElement.offsetHeight;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.size([width, height]);
var svg = d3.select(\"body\").append(\"svg\")
.attr(\"width\", width)
.attr(\"height\", height);
var ingest = function(graph) {
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = svg.selectAll(\".link\")
.data(graph.links)
.enter().append(\"line\")
.attr(\"class\", \"link\")
.style(\"stroke-width\", function(d) { return Math.sqrt(d.value); });
var node = svg.selectAll(\".node\")
.data(graph.nodes)
.enter().append(\"circle\")
.attr(\"class\", \"node\")
.attr(\"r\", 5)
.style(\"fill\", function(d) { return color(d.group); })
.call(force.drag);
node.append(\"title\")
.text(function(d) { return d.name; });
force.on(\"tick\", function() {
link.attr(\"x1\", function(d) { return d.source.x; })
.attr(\"y1\", function(d) { return d.source.y; })
.attr(\"x2\", function(d) { return d.target.x; })
.attr(\"y2\", function(d) { return d.target.y; });
node.attr(\"cx\", function(d) { return d.x; })
.attr(\"cy\", function(d) { return d.y; });
});
};
ingest(~a);
</script>
</body>
</html>
")
|