(ns jahoo.core
(:require [reagent.core :as r]
[reagent.dom :as rd]
[clojure.zip :as z]
;[clojure.pprint :refer [pprint]]
;[clojure.string :refer [index-of]]
;[clojure.string :as str]
))
(enable-console-print!)
(defn log [a-thing]
(.log js/console a-thing))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; hidden preamble material for Vega (in addition to Vega-Lite) rendering
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn render-vega [spec elem]
(when spec
(let [spec (clj->js spec)
opts {:renderer "canvas"
:mode "vega"
:actions {
:export true,
:source true,
:compiled true,
:editor true}}]
(-> (js/vegaEmbed elem spec (clj->js opts))
(.then (fn [res]
(. js/vegaTooltip (vega (.-view res) spec))))
(.catch (fn [err]
(log err)))))))
(defn vega
"Reagent component that renders vega"
[spec]
(r/create-class
{:display-name "vega"
:component-did-mount (fn [this]
(render-vega spec (rd/dom-node this)))
:component-will-update (fn [this [_ new-spec]]
(render-vega new-spec (rd/dom-node this)))
:reagent-render (fn [spec]
[:div#vis])}))
;making a histogram from a list of observations
(defn list-to-hist-data-lite [l]
""" takes a list and returns a record
in the right format for vega data,
with each list element the label to a field named 'x'"""
(defrecord rec [category])
{:values (into [] (map ->rec l))})
(defn makehist-lite [data]
{
:$schema "https://vega.github.io/schema/vega-lite/v4.json",
:data data,
:mark "bar",
:encoding {
:x {:field "category",
:type "ordinal"},
:y {:aggregate "count",
:type "quantitative"}
}
})
(defn list-to-hist-data [l]
""" takes a list and returns a record
in the right format for vega data,
with each list element the label to a field named 'x'"""
(defrecord rec [category])
[{:name "raw",
:values (into [] (map ->rec l))}
{:name "aggregated"
:source "raw"
:transform
[{:as ["count"]
:type "aggregate"
:groupby ["category"]}]}
{:name "agg-sorted"
:source "aggregated"
:transform
[{:type "collect"
:sort {:field "category"}}]}
])
(defn makehist [data]
(let [n (count (distinct ((data 0) :values)))
h 200
pad 5
w (if (< n 20) (* n 35) (- 700 (* 2 pad)))]
{
:$schema "https://vega.github.io/schema/vega/v5.json",
:width w,
:height h,
:padding pad,
:data data,
:signals [
{:name "tooltip",
:value {},
:on [{:events "rect:mouseover", :update "datum"},
{:events "rect:mouseout", :update "{}"}]}
],
:scales [
{:name "xscale",
:type "band",
:domain {:data "agg-sorted", :field "category"},
:range "width",
:padding 0.05,
:round true},
{:name "yscale",
:domain {:data "agg-sorted", :field "count"},
:nice true,
:range "height"}
],
:axes [
{ :orient "bottom", :scale "xscale" },
{ :orient "left", :scale "yscale" }
],
:marks [
{:type "rect",
:from {:data "agg-sorted"},
:encode {
:enter {
:x {:scale "xscale", :field "category"},
:width {:scale "xscale", :band 1},
:y {:scale "yscale", :field "count"},
:y2 {:scale "yscale", :value 0}
},
:update {:fill {:value "steelblue"}},
:hover {:fill {:value "green"}}
}
},
{:type "text",
:encode {
:enter {
:align {:value "center"},
:baseline {:value "bottom"},
:fill {:value "#333"}
},
:update {
:x {:scale "xscale", :signal "tooltip.category", :band 0.5},
:y {:scale "yscale", :signal "tooltip.count", :offset -2},
:text {:signal "tooltip.count"},
:fillOpacity [
{:test "isNaN(tooltip.count)", :value 0},
{:value 1}
]
}
}
}
]
}))
(defn hist [l]
(vega (makehist (list-to-hist-data l))))
; for making bar plots
(defn list-to-barplot-data-lite [l m]
""" takes a list and returns a record
in the right format for vega data,
with each list element the label to a field named 'x'"""
(defrecord rec [category amount])
{:values (into [] (map ->rec l m))})
(defn makebarplot-lite [data]
{
:$schema "https://vega.github.io/schema/vega-lite/v4.json",
:data data,
:mark "bar",
:encoding {
:x {:field "element", :type "ordinal"},
:y {:field "value", :type "quantitative"}
}
})
(defn list-to-barplot-data [l m]
""" takes a list and returns a record
in the right format for vega data,
with each list element the label to a field named 'x'"""
(defrecord rec [category amount])
{:name "table",
:values (into [] (map ->rec l m))})
(defn makebarplot [data]
(let [n (count (data :values))
h 200
pad 5
w (if (< n 20) (* n 35) (- 700 (* 2 pad)))]
{
:$schema "https://vega.github.io/schema/vega/v5.json",
:width w,
:height h,
:padding pad,
:data data,
:signals [
{:name "tooltip",
:value {},
:on [{:events "rect:mouseover", :update "datum"},
{:events "rect:mouseout", :update "{}"}]}
],
:scales [
{:name "xscale",
:type "band",
:domain {:data "table", :field "category"},
:range "width",
:padding 0.05,
:round true},
{:name "yscale",
:domain {:data "table", :field "amount"},
:nice true,
:range "height"}
],
:axes [
{ :orient "bottom", :scale "xscale" },
{ :orient "left", :scale "yscale" }
],
:marks [
{:type "rect",
:from {:data "table"},
:encode {
:enter {
:x {:scale "xscale", :field "category"},
:width {:scale "xscale", :band 1},
:y {:scale "yscale", :field "amount"},
:y2 {:scale "yscale", :value 0}
},
:update {:fill {:value "steelblue"}},
:hover {:fill {:value "green"}}
}
},
{:type "text",
:encode {
:enter {
:align {:value "center"},
:baseline {:value "bottom"},
:fill {:value "#333"}
},
:update {
:x {:scale "xscale", :signal "tooltip.category", :band 0.5},
:y {:scale "yscale", :signal "tooltip.amount", :offset -2},
:text {:signal "tooltip.amount"},
:fillOpacity [
{:test "isNaN(tooltip.amount)", :value 0},
{:value 1}
]
}
}
}
]
}))
(defn barplot [l m]
(vega (makebarplot (list-to-barplot-data l m))))
; now, for tree making
;(thanks to Taylor Wood's answer in this thread on stackoverflow:
; https://stackoverflow.com/questions/57911965)
(defn count-up-to-right [loc]
(if (z/up loc)
(loop [x loc, pops 0]
(if (z/right x)
pops
(recur (z/up x) (inc pops))))
0))
(defn list-to-tree-spec
"take a list and walk through it (with clojure.zip library)
return a tree spec record in right format to pass to vega"
[l]
(loop [loc (z/seq-zip l), next-id 0, parent-ids [], acc []]
(cond
(z/end? loc) acc
(z/end? (z/next loc))
(conj acc
{:id (str next-id)
:name (str (z/node loc))
:parent (when (seq parent-ids)
(str (peek parent-ids)))})
(and (z/node loc) (not (z/branch? loc)))
(recur
(z/next loc)
(inc next-id)
(cond
(not (z/right loc))
(let [n (count-up-to-right loc)
popn (apply comp (repeat n pop))]
(some-> parent-ids not-empty popn))
(not (z/left loc))
(conj parent-ids next-id)
:else parent-ids)
(conj acc
{:id (str next-id)
:name (str (z/node loc))
:parent (when (seq parent-ids)
(str (peek parent-ids)))}))
:else
(recur (z/next loc) next-id parent-ids acc))))
(defn maketree [w h tree-spec]
""" makes vega spec for a tree given tree-spec in the right json-like format """
{:$schema "https://vega.github.io/schema/vega/v5.json"
:data [{:name "tree"
:transform [{:key "id" :parentKey "parent" :type "stratify"}
{:as ["x" "y" "depth" "children"]
:method {:signal "layout"}
:size [{:signal "width"} {:signal "height"}]
:type "tree"}]
:values tree-spec
}
{:name "links"
:source "tree"
:transform [{:type "treelinks"}
{:orient "horizontal"
:shape {:signal "links"}
:type "linkpath"}]}]
:height h
:marks [{:encode {:update {:path {:field "path"} :stroke {:value "#ccc"}}}
:from {:data "links"}
:type "path"}
{:encode {:enter {:size {:value 50} :stroke {:value "#fff"}}
:update {:fill {:field "depth" :scale "color"}
:x {:field "x"}
:y {:field "y"}}}
:from {:data "tree"}
:type "symbol"}
{:encode {:enter {:baseline {:value "bottom"}
:font {:value "Courier"}
:fontSize {:value 14}
:angle {:value 0}
:text {:field "name"}}
:update {:align {:signal "datum.children ? 'center' : 'center'"}
:dy {:signal "datum.children ? -6 : -6"}
:opacity {:signal "labels ? 1 : 0"}
:x {:field "x"}
:y {:field "y"}}}
:from {:data "tree"}
:type "text"}]
:padding 5
:scales [{:domain {:data "tree" :field "depth"}
:name "color"
:range {:scheme "magma"}
:type "linear"
:zero true}]
:signals [{:bind {:input "checkbox"} :name "labels" :value true}
{:bind {:input "radio" :options ["tidy" "cluster"]}
:name "layout"
:value "tidy"}
{:name "links"
:value "line"}]
:width w}
)
(defn tree-depth
"get the depth of a tree (list)"
[list]
(if (seq? list)
(inc (apply max 0 (map tree-depth list)))
0))
(defn tree
"plot tree using vega"
[list]
(let [spec (list-to-tree-spec list)
h (* 30 (tree-depth list))]
(vega (maketree 700 h spec))))
The Clojure code for rendering the visualizations (with Vega) in this post is here.
Using Klipse for code blocks
Here is an html code block that will run Clojure interactively in the browser, using Klipse:
<pre><code class="language-eval-clojure">
(+ 1 1)
</code></pre>
In markdown, specify the language in a fenced code block as eval-clojure
after the opening fence (the ‘selector’ name for clojure, which can be set in window.klipse_settings
in the head of the html file), like this:
```eval-clojure
(clojure code)
```
and the interpreter will make this into the same HTML as the above (note, the return of the last function call will be printed, if you want more to print you can always use print
or println
). Here’s an example:
(defn flip
([]
(if (< (rand 1) 0.5)
true
false))
([p]
(if (< (rand 1) p)
true
false)))
(println "Result of virtual coin flip:" (if (flip) "HEADS" "TAILS"))
(repeatedly 10 flip)
The code will be auto-evaluated with every change. Note that you also can use ⌘+return / CTRL+⏎ to re-run.
It’s useful to put some definitions in a hidden preamble, so I use a class <pre class="hidden">...</pre>
around the code block to make a Klipse clojure box that will be invisible, but will still run. Any definitions or side-effects made in in a hidden box will be available in later boxes.
For visualizations, rather than eval-clojure
, specify the language reagent
for a given code box if you want to do some output that involves rendering html elements. This is what we have to use to render vega graphics:
[:div [:button
{:on-click
(fn [e]
(js/alert "Now you have to close this pop-up!"))}
"Don't press this button."]]
Visualizations:
There’s some code in the preamble, which I have set to load, hidden, when this page is rendered (see the clojure-preamble.html file used to render this page) that gets Vega to work with Klipse. It is copied from Oz (thanks for the help on that, Alex!), and defines the useful function vega
, which will take in a vega spec vector, and output a diagram.
Vega for plotting
Note the visualizations need to be in a reagent
code box to be rendered. In an eval-clojure
box they don’t render.
Histograms:
Here is hist
called on a list of numbers (must be in a reagent
code box).
(hist [0 5 2 1 2 3 4 3 3 3 4 5 19 20 20 21 20 20 19 18])
or truth values
(hist (repeatedly 100 flip))
or lists…
(defn sample-kleene-ab []
(if (flip) '() (cons (if (flip) 'a 'b) (sample-kleene-ab))))
(hist (repeatedly 2000 sample-kleene-ab))
hist
is defined in the preamble.
Vega for drawing trees
You can define some tree data in clojure’s record data format like format:
(def example-tree-data
[{:id 0 :name "a"}
{:id 1 :name "b" :parent 0}
{:id 2 :name "c" :parent 0}
{:id 3 :name "d" :parent 2}])
(maketree w h tree-data)
(defined in the preamble) will take that data and make the spec for a Vega tree diagram of size w
px-by-h
px out of that. Then, in a reagent box you can call vega
on the result to visualize:
[:div
[:h4 "Here's a tree"]
[vega (maketree 200 100 example-tree-data)]]
Then here’s a function list-to-tree-spec
which will walk through a list (using the clojure.zip library) and output that format required:
(list-to-tree-spec '(a (b c)))
To be easier to use, there’s the function tree
, which should be called in a reagent
codebox, to plot a tree that will autosize a bit.
(defn tree
"plot tree using vega"
[list]
(let [spec (list-to-tree-spec list)
h (* 30 (tree-depth list))]
(vega (maketree 700 h spec)))))
Pass it a nested list list
, and you’ll get a visualization.
For example: a clojure function computation tree
(def fib3-tree
'("(fib 3)"
("(+ (fib 1) (fib 2))"
("(fib 1)" "1")
("(fib 2)"
("(+ (fib 0) (fib 1))"
("(fib 0)" "0")
("(fib 1)" "1")))))
)
(tree fib3-tree)
Or a natural language syntax/derivation tree:
(def j-tree
'(CP
(TP
(NP_i (N (Aoi-ga "\"Aoi-NOM\"")))
(T' (VP
(PP (NP (daidokoro "\"kitchen\"")) (P (de "\"in\"")))
(VP "(t_i)" (V'
(NP (N (hon-o "\"book-ACC\"")))
(V (yon- "\"read\"")))))
(T (da "\"PST\""))))
(C (to "\"COMP\"")))
)
(tree j-tree)