/workspace/Dropbox/projects/scicloj/clay/notebooks/index.clj

Clay

...
...

(credit: Wikimedia Commons)

What is it?

Clay is a minimalistic Clojure tool for data visualization and literate programming, compatible with the Kindly convention. It is one of the fruits of our explorations at the visual-tools-group.

Goals

  • Easily explore & share things for others to easily pick & use.
  • Encourage writing Kindly-compatible notes for future compatiblity with other tools.
  • Flow with the REPL: encourage user interactions that flow naturally with the typical use of Clojure in editors and REPLs.

Setup

For rendering documents like this one with Clay, you need to:

  • add kindly-default and Clay as dependencies
  • call (kindly-default/setup!) and (clay/start!) (see below)

To enjoy Clay's dynamic interaction, you also need to inform it about code evaluations. This requires some setup at the your editor.

See the suggested setup for popular editors below. If your favourite editor is not supported yet, let us talk and make it work.

VSCode Calva

(to be updated soon)

Please add the following command to your keybindings.json file at the VScode setup (you may pick another key, of course). This command would evaluate a piece of code and send the result to be visualized in Clay.

{
 "key": "ctrl+shift+enter",
 "command": "calva.runCustomREPLCommand",
 "args": "(tap> {:clay-tap? true :form (quote $current-form) :code (str (quote $current-form)) :value $current-form})"
 }

Emacs CIDER

(to be updated soon)

Please add the following to your Emacs configuration. It will make sure to inform Clay about all user evaluations of Clojure code.

;; (inspired by: https://github.com/clojure-emacs/cider/issues/3094)
(require 'cider-mode)

(defun cider-tap (&rest r) ; inspired by https://github.com/clojure-emacs/cider/issues/3094
  (cons (concat "(let [__value "
                (caar r)
                "] (tap> {:clay-tap? true :form (quote " (caar r) ") :value __value}) __value)")
        (cdar r)))

(advice-add 'cider-nrepl-request:eval
:filter-args #'cider-tap)

Starting a Clay namespace

Now, we can write a namespace and play with Clay.

(ns index
  (:require [scicloj.clay.v2.api :as clay]
            [scicloj.kindly.v3.api :as kindly]
            [scicloj.kindly.v3.kind :as kind]
            [scicloj.kindly-default.v1.api :as kindly-default]))
...

Initialize Kindly's default.

(kindly-default/setup!)
...

Let us start Clay.

(clay/start!)
...

These initializations can also be done in a user.clj file, making them available for all namespaces in the project. The browser view should open automatically.

A few useful actions

Showing the whole namespace:

(comment
  (clay/show-doc! "notebooks/index.clj"))
...

Writing the document:

(comment
  (clay/show-doc-and-write-html!
   "notebooks/index.clj"
   {:toc? true}))
...

These can be conveniently bound to functions and keys at your editor (to b documented soon).

Interaction

Clay responds to user evaluations by displaying the result visually.

(+ 1 2)
...

In Emacs CIDER, after evaluation of a form (or a region), the browser view should show the evaluation result. In VSCode Calva, a similar effect can be achieved using the dedicated command and keybinding defined above.

Kinds

The way things should be visualized is determined by the advice of Kindly. In this namespace we demonstrate Kindly's default advice. User-defined Kindly advices should work as well. Kindly advises tools (like Clay) about the kind of way a given context should be displayed, by assigning to it a so-called kind. Please refer to the Kindly documentation for details about specifying and using kinds.

Examples

Plain values

By default, when there is no kind information provided by Kindly, values are pretty-printed.

(def people-as-maps
  (->> (range 29)
       (mapv (fn [i]
               {:preferred-language (["clojure" "clojurescript" "babashka"]
                                     (rand-int 3))
                :age (rand-int 100)}))))
...
(def people-as-vectors
  (->> people-as-maps
       (mapv (juxt :preferred-language :age))))
...
people-as-maps
...
people-as-vectors
...

Pretty printing

With the the :kind/pprint kind, this can behaviour can be made explicit (overriding other inferred kinds if necessary).

(kind/pprint people-as-maps)
...
(kind/pprint people-as-vectors)
...

Tables

The :kind/table kind can be handy for an interactive table view.

(kind/table
 {:column-names [:preferred-language :age]
  :row-vectors people-as-vectors})
...
(kind/table
 {:column-names [:preferred-language :age]
  :row-maps people-as-maps})
...

Hiccup

Hiccup, a popular Clojure way to represent HTML, can be specified by kind:

(kind/hiccup
 [:big [:big [:p {:style ; https://www.htmlcsscolor.com/hex/7F5F3F
                  {:color "#7F5F3F"}}
              "hello"]]])
...

Images

(import javax.imageio.ImageIO
        java.net.URL)
...
(->  "https://upload.wikimedia.org/wikipedia/commons/2/2c/Clay-ss-2005.jpg"
     (URL.)
     (ImageIO/read))
...

Datasets

tech.ml.dataset datasets currently use the default printing of the library. Let us create such a dataset using Tablecloth.

(require '[tablecloth.api :as tc])
...
(-> {:x (range 6)
     :y [:A :B :C :A :B :C]}
    tc/dataset)
...
(-> {:x [1 [2 3] 4]
     :y [:A :B :C]}
    tc/dataset)
...
(defn vega-point-plot [data]
  (-> {:data {:values data},
       :mark "point"
       :encoding
       {:size {:field "w" :type "quantitative"}
        :x {:field "x", :type "quantitative"},
        :y {:field "y", :type "quantitative"},
        :fill {:field "z", :type "nominal"}}}
      kind/vega))
...
(defn random-data [n]
  (->> (repeatedly n #(- (rand) 0.5))
       (reductions +)
       (map-indexed (fn [x y]
                      {:w (rand-int 9)
                       :z (rand-int 9)
                       :x x
                       :y y}))))
...
(defn random-vega-plot [n]
  (-> n
      random-data
      vega-point-plot))
...
(random-vega-plot 9)
...
(def cytoscape-example
  {:elements {:nodes [{:data {:id "a" :parent "b"} :position {:x 215 :y 85}}
                      {:data {:id "b"}}
                      {:data {:id "c" :parent "b"} :position {:x 300 :y 85}}
                      {:data {:id "d"} :position {:x 215 :y 175}}
                      {:data {:id "e"}}
                      {:data {:id "f" :parent "e"} :position {:x 300 :y 175}}]
              :edges [{:data {:id "ad" :source "a" :target "d"}}
                      {:data {:id "eb" :source "e" :target "b"}}]}
   :style [{:selector "node"
            :css {:content "data(id)"
                  :text-valign "center"
                  :text-halign "center"}}
           {:selector "parent"
            :css {:text-valign "top"
                  :text-halign "center"}}
           {:selector "edge"
            :css {:curve-style "bezier"
                  :target-arrow-shape "triangle"}}]
   :layout {:name "preset"
            :padding 5}})
...
(kind/cytoscape cytoscape-example)
...
(kind/cytoscape [cytoscape-example
                 {:style {:height 100
                          :width 100}}])
...
(kind/echarts
 {:xAxis {:data ["Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun"]}
  :yAxis {}
  :series [{:type "bar"
            :color ["#7F5F3F"]
            :data [23 24 18 25 27 28 25]}]})
...

Delays

Clojure Delays are a common way to define computations that do not take place immediately. The computation takes place when dereferencing the value for the first time. Clay makes sure to dererence Delays when passing values for visualization. This is handy for slow example snippets and explorations, that one would typically not like to slow down the evaluation of the whole namespace, but would like to visualize them on demand and also include in them in the final document.

(delay
  (Thread/sleep 500)
  (+ 1 2))
...
(delay
  [:div [:big "hi......."]])
...

Tests

The clay/check function allows to define tests that render accordingly, marking their failure or success.

(-> 2
    (+ 3)
    (clay/check = 4))
...
(-> 2
    (+ 3)
    (clay/check = 5))
...

We are considering a so-called "doctest" setup involving such checks, so that actual Clojure tests would be derived automatically from them. This would open the way for literate testing / testable documentation solutions, such as those we have been using in the past (e.g., in tutorials of ClojisR using Notespace v2).

:bye
...