3  Catalogue of visualization kinds

(ns kinds
  (:require [scicloj.kindly.v4.api :as kindly]
            [scicloj.kindly.v4.kind :as kind]
            [clojure.math :as math]
            [tablecloth.api :as tc]
            [tablecloth.column.api :as tcc]
            [clojure.java.io :as io]))

3.1 Plain values

Values with no kind are displayed the default way each tool would display them. In Clay, they are simply pretty-printed.

(+  4 5)
9
(str "abcd" "efgh")
"abcdefgh"

3.2 Plain data structures

By default (according to kindly/advice), plain Clojure data structures: vectors, other sequentials (lists/seqs/ranges/etc.), sets, and maps, are assigned the kinds kind/vector, kind/seq, , kind/set, and kind/map, respectively.

Each tool may have its own way to display these kinds. For example, Clay just uses text, while Portal has a hierarchical navigation UI.

(list 1 "A" :B 'C)
(1 "A" :B C)
(range 9)
(0 1 2 3 4 5 6 7 8)
[1 "A" :B 'C]
[1 "A" :B C]
#{1 "A" :B 'C}
#{1 :B C "A"}
{1 "A" :B 'C}
{1 "A", :B C}

More examples:

(def people-as-maps
  (->> (range 29)
       (mapv (fn [i]
               {:preferred-language (["clojure" "clojurescript" "babashka"]
                                     (rand-int 3))
                :age (rand-int 100)}))))
people-as-maps
[{:preferred-language "clojurescript", :age 94}
 {:preferred-language "clojure", :age 51}
 {:preferred-language "clojurescript", :age 18}
 {:preferred-language "clojure", :age 7}
 {:preferred-language "clojure", :age 15}
 {:preferred-language "clojurescript", :age 22}
 {:preferred-language "babashka", :age 68}
 {:preferred-language "clojurescript", :age 97}
 {:preferred-language "babashka", :age 51}
 {:preferred-language "clojurescript", :age 35}
 {:preferred-language "clojurescript", :age 19}
 {:preferred-language "babashka", :age 31}
 {:preferred-language "clojure", :age 81}
 {:preferred-language "babashka", :age 41}
 {:preferred-language "babashka", :age 15}
 {:preferred-language "clojure", :age 76}
 {:preferred-language "clojure", :age 73}
 {:preferred-language "babashka", :age 26}
 {:preferred-language "clojurescript", :age 59}
 {:preferred-language "clojure", :age 90}
 {:preferred-language "clojurescript", :age 56}
 {:preferred-language "babashka", :age 10}
 {:preferred-language "clojure", :age 55}
 {:preferred-language "clojurescript", :age 79}
 {:preferred-language "clojurescript", :age 59}
 {:preferred-language "clojure", :age 65}
 {:preferred-language "clojure", :age 44}
 {:preferred-language "clojure", :age 97}
 {:preferred-language "clojurescript", :age 24}]
(def people-as-vectors
  (->> people-as-maps
       (mapv (juxt :preferred-language :age))))
people-as-vectors
[["clojurescript" 94]
 ["clojure" 51]
 ["clojurescript" 18]
 ["clojure" 7]
 ["clojure" 15]
 ["clojurescript" 22]
 ["babashka" 68]
 ["clojurescript" 97]
 ["babashka" 51]
 ["clojurescript" 35]
 ["clojurescript" 19]
 ["babashka" 31]
 ["clojure" 81]
 ["babashka" 41]
 ["babashka" 15]
 ["clojure" 76]
 ["clojure" 73]
 ["babashka" 26]
 ["clojurescript" 59]
 ["clojure" 90]
 ["clojurescript" 56]
 ["babashka" 10]
 ["clojure" 55]
 ["clojurescript" 79]
 ["clojurescript" 59]
 ["clojure" 65]
 ["clojure" 44]
 ["clojure" 97]
 ["clojurescript" 24]]

These kinds have recursive kind semantics: if the values inside them have kind information, they should be handled accordingly.

Here is a vector of things of different kinds inside:

[(kind/hiccup
  [:div {:style
         {:background-color "floralwhite"}}
   [:p "hello"]])
 (kind/md
  "hello *hello* **hello**")
 (kind/code
  "(defn f [x] (+  x 9))")]

[

hello

hello hello hello

(defn f [x] (+  x 9))

]

And here is a map:

{:x  (kind/md
      "**hello**")
 (kind/md
  "**hello**") :x}

{

:x

hello

hello

:x

}

Here is a more detailed example:

{:vector-of-numbers [2 9 -1]
 :vector-of-different-things ["hi"
                              (kind/hiccup
                               [:big [:big "hello"]])]
 :map-of-different-things {:markdown (kind/md ["*hi*, **hi**"])
                           :number 9999}
 :hiccup (kind/hiccup
          [:big [:big "bye"]])
 :dataset (tc/dataset {:x (range 3)
                       :y [:A :B :C]})}

{

:vector-of-numbers [2 9 -1]
:vector-of-different-things

[

"hi"
hello

]

:map-of-different-things

{

:markdown

hi, hi

:number 9999

}

:hiccup
bye
:dataset

_unnamed [3 2]:

:x :y
0 :A
1 :B
2 :C

}

3.3 Hidden

Values of kind/hidden are simply not displayed.

(kind/hidden
 {:x 9})

3.4 Markdown

Values of kind/md are rendered as Markdown.

(kind/md
 ["

* This is [markdown](https://www.markdownguide.org/).
  * *Isn't it??*"
  "

* Here is **some more** markdown."])
  • This is markdown.
    • Isn’t it??
  • Here is some more markdown.

Ideally, tools should support TeX inside Markown.

(kind/md
 "If $x$ equals 9, then $$x^2+9=90$$")

If \(x\) equals 9, then \[x^2+9=90\]

3.5 TeX

(kind/tex "x^2=\\alpha")

\[x^2=\alpha\]

3.6 Mermaid

(kind/mermaid
 "
flowchart TD
    A[Christmas] -->|Get money| B(Go shopping)
    B --> C{Let me think}
    C -->|One| D[Laptop]
    C -->|Two| E[iPhone]
    C -->|Three| F[fa:fa-car Car]
  ")
flowchart TD A[Christmas] -->|Get money| B(Go shopping) B --> C{Let me think} C -->|One| D[Laptop] C -->|Two| E[iPhone] C -->|Three| F[fa:fa-car Car]

3.7 Graphviz

(kind/graphviz "digraph D {

  A [shape=diamond]
  B [shape=box]
  C [shape=circle]

  A -> B [style=dashed, color=grey]
  A -> C [color=\"black:invis:black\"]
  A -> D [penwidth=5, arrowhead=none]

}")

3.8 Code

Values of kind/code are rendered as Clojure code.

(kind/code "(update {:x 9} :x inc)")
(update {:x 9} :x inc)
(kind/code
 ["(update {:x 9} :x inc)"
  "(update {:x 9} :x dec)"])
(update {:x 9} :x inc)
(update {:x 9} :x dec)

3.9 Edn

(will be documented soon)

3.10 Hiccup

Values of kind/hiccup should be displayed as the HTML this value defines according to Hiccup notation.

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

  • hello

This kind has recursive semantics: if the values inside them have kind information, they should be handled accordingly.

Foe example:

(kind/hiccup
 [:div {:style
        {:background-color "#eeddee"
         :border-style "solid"}}
  [:p {:style {:background-color "#ccddcc"
               :border-style "solid"}}
   "hello"]
  (kind/md
   "hello *hello* **hello**")
  (kind/code
   "(defn f [x] (+  x 9))")])

hello

hello hello hello

(defn f [x] (+  x 9))

Scittle and Reagent kinds are recognized automatically inside Hiccup:

  • A list beginning with a symbol means kind/scittle.
  • A vector with a list beginning with a symbol means kind/reagent.
(kind/hiccup
 [:div
  ;; recognized as `kind/scittle`
  '(defn g [x]
     (+ x 9))
  ;; recognized as `kind/reagent`
  ['(fn []
      [:p (g 11)])]])

A more detailed nesting example:

(kind/hiccup
 [:div {:style {:background "#f5f3ff"
                :border "solid"}}

  [:hr]
  [:pre [:code "kind/md"]]
  (kind/md "*some text* **some more text**")

  [:hr]
  [:pre [:code "kind/code"]]
  (kind/code "{:x (1 2 [3 4])}")

  [:hr]
  [:pre [:code "kind/dataset"]]
  (tc/dataset {:x (range 13)
               :y (map inc (range 13))})
  [:hr]
  [:pre [:code "kind/table"]]
  (kind/table
   (tc/dataset {:x (range 13)
                :y (map inc (range 13))})
   {:style {:height "200px"}})
  [:hr]
  [:pre [:code "kind/vega-lite"]]
  (-> {:data {:values "x,y
1,1
2,4
3,9
-1,1
-2,4
-3,9"
              :format {:type :csv}},
       :mark "point"
       :encoding
       {:x {:field "x", :type "quantitative"}
        :y {:field "y", :type "quantitative"}}}
      kind/vega-lite)

  [:hr]
  [:pre [:code "kind/reagent"]
   [:p "(automatically recognized without annotation)"]]
  ;; Recognized as `kind/reagent`:
  ['(fn [numbers]
      [:p {:style {:background "#d4ebe9"}}
       (pr-str (map inc numbers))])
   (vec (range 40))]])

kind/md

some text some more text


kind/code
{:x (1 2 [3 4])}

kind/dataset

_unnamed [13 2]:

:x :y
0 1
1 2
2 3
3 4
4 5
5 6
6 7
7 8
8 9
9 10
10 11
11 12
12 13

kind/table
x y
0 1
1 2
2 3
3 4
4 5
5 6
6 7
7 8
8 9
9 10
10 11
11 12
12 13

kind/vega-lite

kind/reagent

(automatically recognized without annotation)

3.11 Hiccup2

kind/hiccup2 provides support for Hiccup version 2.

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

  • hello

3.12 Reagent

Values of kind/reagent express Reagent components.

(kind/reagent
 ['(fn [{:keys [initial-value
                background-color]}]
     (let [*click-count (reagent.core/atom initial-value)]
       (fn []
         [:div {:style {:background-color background-color}}
          "The atom " [:code "*click-count"] " has value: "
          @*click-count ". "
          [:input {:type "button" :value "Click me!"
                   :on-click #(swap! *click-count inc)}]])))
  {:initial-value 9
   :background-color "#d4ebe9"}])

The :html/deps option can be used to provide additional dependencies:

(kind/reagent
 ['(fn []
     [:div {:style {:height "200px"}
            :ref (fn [el]
                   (let [m (-> js/L
                               (.map el)
                               (.setView (clj->js [51.505 -0.09])
                                         13))]
                     (-> js/L
                         .-tileLayer
                         (.provider "OpenStreetMap.Mapnik")
                         (.addTo m))
                     (-> js/L
                         (.marker (clj->js [51.5 -0.09]))
                         (.addTo m)
                         (.bindPopup "A pretty CSS popup.<br> Easily customizable.")
                         (.openPopup))))}])]
 ;; Note we need to mention the dependency:
 {:html/deps [:leaflet]})

Possible ways to specify deps should be documented better soon.

3.13 Scittle

With kind/scittle, one may specify Clojurescript code to run through Scittle.

(kind/scittle
 '(.log js/console "hello"))
(kind/scittle
 '(defn f [x]
    (+ x 9)))
(kind/reagent
 ['(fn []
     [:p (f 11)])])

3.14 HTML

Values of kind/html are displayed as raw html.

(kind/html
 "<div style='height:40px; width:40px; background:purple'></div> ")
(kind/html
 "
<svg height=100 width=100>
<circle cx=50 cy=50 r=40 stroke='purple' stroke-width=3 fill='floralwhite' />
</svg> ")

3.14.1 Flares

Flares are inferred to be kind/html.

(tagged-literal
 'flare/html {:html "<div style='height:40px; width:40px; background:purple'></div> "
              :key "example"})

3.15 Vega-Lite

(def vega-lite-plot
  (kind/vega-lite
   {:encoding
    {:y {:field "y", :type "quantitative"},
     :size {:value 400},
     :x {:field "x", :type "quantitative"}},
    :mark {:type "circle", :tooltip true},
    :width 400,
    :background "floralwhite",
    :height 100,
    :data {:values "x,y\n1,1\n2,-4\n3,9\n", :format {:type "csv"}}}))
vega-lite-plot

3.16 Vega

Inspired by Let’s Make A Bar Chart Tutorial of the Vega docs.

(kind/vega
 {:$schema "https://vega.github.io/schema/vega/v5.json"
  :width 400
  :height 200
  :padding 5
  :data {:name "table"
         :values [{:category :A :amount 28}
                  {:category :B :amount 55}
                  {:category :C :amount 43}
                  {:category :D :amount 91}
                  {:category :E :amount 81}
                  {:category :F :amount 53}
                  {:category :G :amount 19}
                  {:category :H :amount 87}]}
  :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 :red}}}}})

3.17 Cytoscape

(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
  {:width "100px"
   :height "100px"
   :background "floralwhite"}})

3.18 ECharts

This example is taken from Apache ECharts’ Getting Started.

(def echarts-example
  {:title {:text "Echarts Example"}
   :tooltip {}
   :legend {:data ["sales"]}
   :xAxis {:data ["Shirts", "Cardigans", "Chiffons",
                  "Pants", "Heels", "Socks"]}
   :yAxis {}
   :series [{:name "sales"
             :type "bar"
             :data [5 20 36
                    10 10 20]}]})
(kind/echarts
 echarts-example)
(-> echarts-example
    (kind/echarts {:style
                   {:width "500px"
                    :height "200px"
                    :background "floralwhite"}}))

3.19 Plotly

(def plotly-example
  (let [n 20
        walk (fn [bias]
               (->> (repeatedly n #(-> (rand)
                                       (- 0.5)
                                       (+ bias)))
                    (reductions +)))]
    {:data [{:x (walk 1)
             :y (walk -1)
             :z (map #(* % %)
                     (walk 2))
             :type :scatter3d
             :mode :lines+markers
             :opacity 0.2
             :line {:width 10}
             :marker {:size 20
                      :colorscale :Viridis}}]}))
(kind/plotly
 plotly-example)
(-> plotly-example
    (kind/plotly {:style
                  {:width "300px"
                   :height "300px"}}))

3.20 htmlwidgets-ggplotly

kind/htmlwidgets-ggplotly supports rendering plots through the JS client side of ggplotly

  • an R package offering a Plotly fronted for ggplot2’s grammar of graphics implementation. This package is part of the htmlwidgets ecosystem, and we represent that in the kind’s name.

The following is a work-in-progress attempt to generate JSON specs of the kind consumed by ggplotly’s client side.

The following spec function was generaged by mimicking R’s ggplotly(ggplot(mtcars, aes(wt, mpg)) + geom_point()). Therefore, some parts are hard-coded and require generalization. Other parts are missing (e.g., specifying colours).

(defn ->ggplotly-spec [{:keys [layers labels]}]
  (kind/htmlwidgets-ggplotly
   (let [;; assuming a single layer for now:
         {:keys [data xmin xmax ymin ymax]} (first layers)
         ;; an auxiliary function to compute tick values:
         ->tickvals (fn [l r]
                      (let [jump (-> (- r l)
                                     (/ 6)
                                     math/floor
                                     int
                                     (max 1))]
                        (-> l
                            math/ceil
                            (range r jump))))]
     {:x
      {:config
       {:doubleClick "reset",
        :modeBarButtonsToAdd ["hoverclosest" "hovercompare"],
        :showSendToCloud false},
       :layout
       {:plot_bgcolor "rgba(235,235,235,1)",
        :paper_bgcolor "rgba(255,255,255,1)",
        :legend
        {:bgcolor "rgba(255,255,255,1)",
         :bordercolor "transparent",
         :borderwidth 1.88976377952756,
         :font {:color "rgba(0,0,0,1)", :family "", :size 11.689497716895}},
        :xaxis (let [tickvals (->tickvals xmin xmax)
                     ticktext (mapv str tickvals)
                     range-len (- xmax xmin)
                     range-expansion (* 0.1 range-len)
                     expanded-range [(- xmin range-expansion)
                                     (+ xmax range-expansion)]]
                 {:linewidth 0,
                  :nticks nil,
                  :linecolor nil,
                  :ticklen 3.65296803652968,
                  :tickcolor "rgba(51,51,51,1)",
                  :tickmode "array",
                  :gridcolor "rgba(255,255,255,1)",
                  :automargin true,
                  :type "linear",
                  :tickvals tickvals
                  :zeroline false,
                  :title
                  {:text (:x labels),
                   :font {:color "rgba(0,0,0,1)", :family "", :size 14.6118721461187}},
                  :tickfont {:color "rgba(77,77,77,1)", :family "", :size 11.689497716895},
                  :autorange false,
                  :showticklabels true,
                  :showline false,
                  :showgrid true,
                  :ticktext ticktext
                  :ticks "outside",
                  :gridwidth 0.66417600664176,
                  :anchor "y",
                  :domain [0 1],
                  :hoverformat ".2f",
                  :tickangle 0,
                  :tickwidth 0.66417600664176,
                  :categoryarray ticktext,
                  :categoryorder "array",
                  :range expanded-range},)
        :font {:color "rgba(0,0,0,1)", :family "", :size 14.6118721461187},
        :showlegend false,
        :barmode "relative",
        :yaxis (let [tickvals (->tickvals ymin ymax)
                     ticktext (mapv str tickvals)
                     range-len (- ymax ymin)
                     range-expansion (* 0.1 range-len)
                     expanded-range [(- ymin range-expansion)
                                     (+ ymax range-expansion)]]
                 {:linewidth 0,
                  :nticks nil,
                  :linecolor nil,
                  :ticklen 3.65296803652968,
                  :tickcolor "rgba(51,51,51,1)",
                  :tickmode "array",
                  :gridcolor "rgba(255,255,255,1)",
                  :automargin true,
                  :type "linear",
                  :tickvals tickvals,
                  :zeroline false,
                  :title
                  {:text (:y labels),
                   :font {:color "rgba(0,0,0,1)", :family "", :size 14.6118721461187}},
                  :tickfont {:color "rgba(77,77,77,1)", :family "", :size 11.689497716895},
                  :autorange false,
                  :showticklabels true,
                  :showline false,
                  :showgrid true,
                  :ticktext ticktext,
                  :ticks "outside",
                  :gridwidth 0.66417600664176,
                  :anchor "x",
                  :domain [0 1],
                  :hoverformat ".2f",
                  :tickangle 0,
                  :tickwidth 0.66417600664176,
                  :categoryarray ticktext,
                  :categoryorder "array",
                  :range expanded-range},)
        :hovermode "closest",
        :margin
        {:t 25.7412480974125,
         :r 7.30593607305936,
         :b 39.6955859969559,
         :l 37.2602739726027},
        :shapes
        [{:yref "paper",
          :fillcolor nil,
          :xref "paper",
          :y1 1,
          :type "rect",
          :line {:color nil, :width 0, :linetype []},
          :y0 0,
          :x1 1,
          :x0 0}]},
       :highlight
       {:on "plotly_click",
        :persistent false,
        :dynamic false,
        :selectize false,
        :opacityDim 0.2,
        :selected {:opacity 1},
        :debounce 0},
       :base_url "https://plot.ly",
       :cur_data "1f2fea5b54d146",
       :source "A",
       :shinyEvents
       ["plotly_hover"
        "plotly_click"
        "plotly_selected"
        "plotly_relayout"
        "plotly_brushed"
        "plotly_brushing"
        "plotly_clickannotation"
        "plotly_doubleclick"
        "plotly_deselect"
        "plotly_afterplot"
        "plotly_sunburstclick"],
       :attrs {:1f2fea5b54d146 {:x {}, :y {}, :type "scatter"}},
       :data
       [{:y (:y data)
         :hoveron "points",
         :frame nil,
         :hoverinfo "text",
         :marker
         {:autocolorscale false,
          :color "rgba(0,0,0,1)",
          :opacity 1,
          :size 5.66929133858268,
          :symbol "circle",
          :line {:width 1.88976377952756, :color "rgba(0,0,0,1)"}},
         :mode "markers"
         :type "scatter",
         :xaxis "x",
         :showlegend false,
         :yaxis "y",
         :x (:x data)
         :text (-> data
                   (tc/select-columns [:x :y])
                   (tc/rows :as-maps)
                   (->> (mapv pr-str)))}]},
      :evals [],
      :jsHooks []})))

A random walk example:

(let [n 100
      xs (range n)
      ys (reductions + (repeatedly n #(- (rand) 0.5)))
      xmin (tcc/reduce-min xs)
      xmax (tcc/reduce-max xs)
      ymin (tcc/reduce-min ys)
      ymax (tcc/reduce-max ys)
      data (tc/dataset {:x xs
                        :y ys})]
  (->ggplotly-spec
   {:layers [{:data data
              :xmin xmin :xmax xmax
              :ymin ymin :ymax ymax}]
    :labels {:x "wt"
             :y "mpg"}}))

3.21 Highcharts

(def highcharts-example
  {:title {:text "Line chart"}
   :subtitle {:text "By Job Category"}
   :yAxis {:title {:text "Number of Employees"}}
   :series [{:name "Installation & Developers"
             :data [43934, 48656, 65165, 81827, 112143, 142383,
                    171533, 165174, 155157, 161454, 154610]}

            {:name "Manufacturing",
             :data [24916, 37941, 29742, 29851, 32490, 30282,
                    38121, 36885, 33726, 34243, 31050]}

            {:name "Sales & Distribution",
             :data [11744, 30000, 16005, 19771, 20185, 24377,
                    32147, 30912, 29243, 29213, 25663]}

            {:name "Operations & Maintenance",
             :data [nil, nil, nil, nil, nil, nil, nil,
                    nil, 11164, 11218, 10077]}

            {:name "Other",
             :data [21908, 5548, 8105, 11248, 8989, 11816, 18274,
                    17300, 13053, 11906, 10073]}]

   :xAxis {:accessibility {:rangeDescription "Range: 2010 to 2020"}}

   :legend {:layout "vertical",
            :align "right",
            :verticalAlign "middle"}

   :plotOptions {:series {:label {:connectorAllowed false},
                          :pointStart 2010}}

   :responsive {:rules [{:condition {:maxWidth 500},
                         :chartOptions {:legend {:layout "horizontal",
                                                 :align "center",
                                                 :verticalAlign "bottom"}}}]}})
(kind/highcharts
 highcharts-example)
(kind/highcharts
 highcharts-example
 {:style {:height "300px"}})

3.22 Observable

Observable visualizations can be written as Javascript. Some of us are working on a Clojure DSL to express the same.

Examples from Quarto’s Observable documentation:

(kind/observable
 "athletes = FileAttachment('notebooks/datasets/athletes.csv').csv({typed: true})")
(kind/observable
 "athletes")
(kind/observable
 "Inputs.table(athletes)")
(kind/observable
 "
Plot.plot({
  grid: true,
  facet: {
    data: athletes,
    y: 'sex'
  },
  marks: [
    Plot.rectY(
      athletes,
      Plot.binX({y: 'count'}, {x: 'weight', fill: 'sex'})
    ),
    Plot.ruleY([0])
  ]
})
")
(kind/observable
 "population = FileAttachment('notebooks/datasets/population.json').json()")
(kind/observable
 "population")
(kind/observable
 " import { chart } with { population as data } from '@d3/zoomable-sunburst'
 chart")
(kind/observable
 "
//| panel: input
viewof bill_length_min = Inputs.range(
                                      [32, 50],
                                      {value: 35, step: 1, label: 'Bill length (min):'}
                                      )
viewof islands = Inputs.checkbox(
                                 ['Torgersen', 'Biscoe', 'Dream'],
                                 { value: ['Torgersen', 'Biscoe'],
                                  label: 'Islands:'
                                  }
                                 )

Plot.rectY(filtered,
            Plot.binX(
                      {y: 'count'},
                      {x: 'body_mass_g', fill: 'species', thresholds: 20}
                      ))
 .plot({
        facet: {
                data: filtered,
                x: 'sex',
                y: 'species',
                marginRight: 80
                },
        marks: [
                Plot.frame(),
                ]
        }
       )
Inputs.table(filtered)
data = FileAttachment('notebooks/datasets/palmer-penguins.csv').csv({ typed: true })
filtered = data.filter(function(penguin) {
                                           return bill_length_min < penguin.bill_length_mm &&
                                           islands.includes(penguin.island);
                                           })
")

3.23 Video

Values of kind/video are specifications for embedded videos.

URLs that serve video files can be specified using the :src key:

(kind/video {:src "https://www.sample-videos.com/video321/mp4/240/big_buck_bunny_240p_30mb.mp4"})

Youtube videos can be spscified using the :youtube-id key: See HTML Youtube Videos on w3schools, for the relevant options.

(kind/video
 {:youtube-id "DAQnvAgBma8"})
(kind/video
 {:youtube-id "DAQnvAgBma8"
  :allowfullscreen false})
(kind/video
 {:youtube-id "DAQnvAgBma8"
  :iframe-width 480
  :iframe-height 270})
(kind/video
 {:youtube-id "DAQnvAgBma8"
  :embed-options {:mute 1
                  :controls 0}})

3.24 Image

By default (according to kindly/advice), BufferedImage objects are inferred to be of kind/image.

(defonce tree-image
  (-> "notebooks/images/Kindly.svg.png"
      io/file
      (javax.imageio.ImageIO/read)))
(type tree-image)
java.awt.image.BufferedImage
tree-image

URLS can be annotated as images as well.

(kind/image
 {:src "https://raw.githubusercontent.com/scicloj/kindly/refs/heads/main/Kindly.svg"})

3.25 Audio

(experimental)

Playable audio can be specified with a sound file:

(kind/audio
 {:src "notebooks/sine_wave.wav"})

It can also be specified as a map containing a sampling rate value and a sequential of sampels - float values between -1 to 1.

(defn generate-sine-wave [{:keys [sample-rate duration frequency amplitude]
                           :or {amplitude 0.5}}]
  (let [num-samples (int (* sample-rate duration))
        ;; Generate samples as int16 reader
        samples (for [idx (range num-samples)]
                  (let [angle (* 2.0 Math/PI idx frequency (/ 1.0 sample-rate))]
                    (* amplitude (Math/sin angle))))]
    {:samples samples
     :sample-rate sample-rate}))
(kind/audio
 (generate-sine-wave {:sample-rate 44100.0
                      :duration 3
                      :frequency 440.0
                      :amplitude 0.3}))
(kind/audio
 (let [[c4 e4 g4] (for [freq [261.63 329.63 392.00]]
                    (-> {:sample-rate 44100.0
                         :duration 1
                         :frequency freq
                         :amplitude 0.3}
                        generate-sine-wave
                        :samples))]
   {:sample-rate 44100.0
    :samples (->> (concat c4
                          (map + c4 e4)
                          (map + c4 e4 g4)
                          (map + e4 g4)
                          g4))}))

3.26 Dataset

By default (according to kindly/advice), tech.ml.dataset / Tablecloth datasets are inferred to be of kind/dataset.

This kind should be printed and rendered as Markdown, possibly with some tool-specific table styling.

(def squares-dataset
  (-> {:x (range 25)}
      tc/dataset
      (tc/map-columns :y
                      [:x]
                      (fn [x]
                        (* x x)))))

Datasets can have various printable values inside:

(tc/dataset
 {:x [1 [2 3] 4]
  :y [:A :B :C]})

_unnamed [3 2]:

:x :y
1 :A
[2 3] :B
4 :C

Some elements might be missing:

(tc/dataset
 [{:x 1 :y 2 :z 3}
  {:y 4 :z 5}])

_unnamed [2 3]:

:x :y :z
1 2 3
4 5

Some kind options of kind/dataset control the way a dataset is printed.

(-> {:x (range 30)}
    tc/dataset
    (kind/dataset {:dataset/print-range 6}))

_unnamed [30 1]:

:x
0
1
2
…
26
27
28
29

3.27 Tables

The kind/table kind can be handy for an interactive table view. kind/table understands many structures which can be rendered as a table.

A map containing either :row-vectors (sequence of sequences) or row-maps (sequence of maps) keys with optional :column-names.

(kind/table
 {:column-names [:preferred-language :age]
  :row-vectors people-as-vectors})
preferred-language age
clojurescript 94
clojure 51
clojurescript 18
clojure 7
clojure 15
clojurescript 22
babashka 68
clojurescript 97
babashka 51
clojurescript 35
clojurescript 19
babashka 31
clojure 81
babashka 41
babashka 15
clojure 76
clojure 73
babashka 26
clojurescript 59
clojure 90
clojurescript 56
babashka 10
clojure 55
clojurescript 79
clojurescript 59
clojure 65
clojure 44
clojure 97
clojurescript 24

Lack of column names produces table without a header.

(kind/table
 {:row-vectors (take 5 people-as-vectors)})
clojurescript 94
clojure 51
clojurescript 18
clojure 7
clojure 15

Column names are inferred from a sequence of maps

(kind/table
 {:row-maps (take 5 people-as-maps)})
preferred-language age
clojurescript 94
clojure 51
clojurescript 18
clojure 7
clojure 15

We can limit displayed columns for sequence of maps case.

(kind/table
 {:column-names [:preferred-language]
  :row-maps (take 5 people-as-maps)})
preferred-language
clojurescript
clojure
clojurescript
clojure
clojure

Sequence of sequences and sequence of maps also work

(kind/table (take 5 people-as-vectors))
clojurescript 94
clojure 51
clojurescript 18
clojure 7
clojure 15
(kind/table (take 5 people-as-maps))
preferred-language age
clojurescript 94
clojure 51
clojurescript 18
clojure 7
clojure 15

Additionally map of sequences is supported (unless it contains :row-vectors or :row-maps key, see such case above).

(kind/table {:x (range 6)
             :y [:A :B :C :A :B :C]})
x y
0 A
1 B
2 C
3 A
4 B
5 C

A dataset can be also treated as a table input.

(def people-as-dataset
  (tc/dataset people-as-maps))
(-> people-as-dataset
    kind/table)
preferred-language age
clojurescript 94
clojure 51
clojurescript 18
clojure 7
clojure 15
clojurescript 22
babashka 68
clojurescript 97
babashka 51
clojurescript 35
clojurescript 19
babashka 31
clojure 81
babashka 41
babashka 15
clojure 76
clojure 73
babashka 26
clojurescript 59
clojure 90
clojurescript 56
babashka 10
clojure 55
clojurescript 79
clojurescript 59
clojure 65
clojure 44
clojure 97
clojurescript 24

Additional options may hint at way the table should be rendered.

(-> people-as-dataset
    (kind/table {:element/max-height "300px"}))
preferred-language age
clojurescript 94
clojure 51
clojurescript 18
clojure 7
clojure 15
clojurescript 22
babashka 68
clojurescript 97
babashka 51
clojurescript 35
clojurescript 19
babashka 31
clojure 81
babashka 41
babashka 15
clojure 76
clojure 73
babashka 26
clojurescript 59
clojure 90
clojurescript 56
babashka 10
clojure 55
clojurescript 79
clojurescript 59
clojure 65
clojure 44
clojure 97
clojurescript 24

Some tools support datatables for displaying tables. This can be expressed using the :use-datatables option.

(-> people-as-maps
    tc/dataset
    (kind/table {:use-datatables true}))
preferred-languageage
clojurescript94
clojure51
clojurescript18
clojure7
clojure15
clojurescript22
babashka68
clojurescript97
babashka51
clojurescript35
clojurescript19
babashka31
clojure81
babashka41
babashka15
clojure76
clojure73
babashka26
clojurescript59
clojure90
clojurescript56
babashka10
clojure55
clojurescript79
clojurescript59
clojure65
clojure44
clojure97
clojurescript24

In addition, the :datatables option can be used to control datatables options (see the full list).

(-> people-as-dataset
    (kind/table {:use-datatables true
                 :datatables {:scrollY 200}}))
preferred-languageage
clojurescript94
clojure51
clojurescript18
clojure7
clojure15
clojurescript22
babashka68
clojurescript97
babashka51
clojurescript35
clojurescript19
babashka31
clojure81
babashka41
babashka15
clojure76
clojure73
babashka26
clojurescript59
clojure90
clojurescript56
babashka10
clojure55
clojurescript79
clojurescript59
clojure65
clojure44
clojure97
clojurescript24

The kind/table has recursive semantics: if the values inside them have kind information, they should be handled accordingly.

(kind/table
 {:column-names [(kind/code ":x")
                 (kind/code ":y")]
  :row-vectors [[(kind/md "*some text* **some more text**")
                 (kind/code "{:x (1 2 [3 4])}")]
                [(tc/dataset {:x (range 3)
                              :y (map inc (range 3))})
                 vega-lite-plot]
                [(kind/hiccup [:div {:style {:height 200}}
                               tree-image])
                 (kind/md "$x^2$")]]})
:x
:y

some text some more text

{:x (1 2 [3 4])}

_unnamed [3 2]:

:x :y
0 1
1 2
2 3

3.28 Printing

Values of kind kind/pprint should be printed.

(->> (range 30)
     (apply array-map)
     kind/println)
{0 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}

3.29 Pretty printing

Values of kind kind/pprint should be pretty-printed.

(->> (range 30)
     (apply array-map)
     kind/pprint)
{0 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}

For some tool like Clay, this is the default when there is no kind information.

(->> (range 30)
     (apply array-map))
{0 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}

Still, it can be is useful to ensure the same behaviour across different tools.

It can also be useful to override other kinds previously specified or automatically inferred.

(kind/pprint
 hello-hiccup)
[:ul
 [:li [:p "hi"]]
 [:li [:big [:big [:p {:style {:color "#7F5F3F"}} "hello"]]]]]
(kind/pprint
 tree-image)
[#object[java.awt.image.BufferedImage 0x18f1421c "BufferedImage@18f1421c: type = 6 ColorModel: #pixelBits = 32 numComponents = 4 color space = java.awt.color.ICC_ColorSpace@26b1c010 transparency = 3 has alpha = true isAlphaPre = false ByteInterleavedRaster: width = 256 height = 256 #numDataElements 4 dataOff[0] = 3"]]
(kind/pprint
 (tc/dataset {:x (range 3)
              :y [:A :B :C]}))
_unnamed [3 2]:

| :x | :y |
|---:|----|
|  0 | :A |
|  1 | :B |
|  2 | :C |

3.30 emmy-viewers

The support for Emmy-viewers is documented at the πŸ“– Emmy-viewers chapterπŸ“– of this book.

3.31 Portal

Values of kind/portal are displayed using an embedded Portal viewer.

(kind/portal
 {:x (range 3)})

This kind has recursive semantics: if the values inside them have kind information, they should be handled accordingly.

Note that kind/portal applies the kind-portal adapter to nested kinds.

(kind/portal
 [(kind/hiccup [:img {:height 50 :width 50
                      :src "https://clojure.org/images/clojure-logo-120b.png"}])
  (kind/hiccup [:img {:height 50 :width 50
                      :src "https://raw.githubusercontent.com/djblue/portal/fbc54632adc06c6e94a3d059c858419f0063d1cf/resources/splash.svg"}])])
(kind/portal
 [(kind/hiccup [:big [:big "a plot"]])
  vega-lite-plot])
(kind/portal
 [(kind/hiccup [:p {:style {:background-color "#ccddcc"
                            :border-style "solid"}}
                "hello"])
  (kind/md
   "hello *hello* **hello**")
  (kind/code
   "(defn f [x] (+  x 9))")
  vega-lite-plot])

3.32 Fragment

kind/fragment is a special kind. It expects a sequential value and generates multiple items, of potentially multiple kinds, from its elements.

(->> ["purple" "darkgreen" "goldenrod"]
     (mapcat (fn [color]
               [(kind/md (str "### subsection: " color))
                (kind/hiccup [:div {:style {:background-color color
                                            :color "lightgrey"}}
                              [:big [:p color]]])]))
     kind/fragment)

3.32.1 subsection: purple

purple

3.32.2 subsection: darkgreen

darkgreen

3.32.3 subsection: goldenrod

goldenrod

3.33 Function

kind/fn is a special kind. It is displayed by first evaluating a given function and arguments, then proceeding recursively with the resulting value.

The function can be specified through the Kindly options.

(kind/fn {:x 1
          :y 2}
  {:kindly/f (fn [{:keys [x y]}]
               (+ x y))})
3
(kind/fn {:my-video-src "https://file-examples.com/storage/fe58a1f07d66f447a9512f1/2017/04/file_example_MP4_480_1_5MG.mp4"}
  {:kindly/f (fn [{:keys [my-video-src]}]
               (kind/video
                {:src my-video-src}))})

If the value is a vector, the function is the first element, and the arguments are the rest.

(kind/fn
  [+ 1 2])
3

If the value is a map, the function is held at the key :kindly/f, and the argument is the map.

(kind/fn
  {:kindly/f (fn [{:keys [x y]}]
               (+ x y))
   :x 1
   :y 2})
3

The kind of the value returned by the function is respected. For example, here are examples with a function returning kind/dataset.

(kind/fn
  {:x (range 3)
   :y (repeatedly 3 rand)}
  {:kindly/f tc/dataset})

_unnamed [3 2]:

:x :y
0 0.13585634
1 0.33618081
2 0.38114008
(kind/fn
  [tc/dataset
   {:x (range 3)
    :y (repeatedly 3 rand)}])

_unnamed [3 2]:

:x :y
0 0.33611106
1 0.56259421
2 0.15527656
(kind/fn
  {:kindly/f tc/dataset
   :x (range 3)
   :y (repeatedly 3 rand)})

_unnamed [3 2]:

:x :y
0 0.42247918
1 0.00479184
2 0.51202044

kind/fn is a special kind. It is displayed by first evaluating a given function and arguments, then proceeding recursively with the resulting value.

If the value is a vector, the function is the first element, and the arguments are the rest.

(kind/fn
  [+ 1 2])
3

If the value is a map, the function is held at the key :kindly/f, and the argument is the map.

(kind/fn
  {:kindly/f (fn [{:keys [x y]}]
               (+ x y))
   :x 1
   :y 2})
3

The kind of the value returned by the function is respected. For example, here are examples with a function returning kind/dataset.

(kind/fn
  [tc/dataset
   {:x (range 3)
    :y (repeatedly 3 rand)}])

_unnamed [3 2]:

:x :y
0 0.91306295
1 0.34601430
2 0.59254594
(kind/fn
  {:kindly/f tc/dataset
   :x (range 3)
   :y (repeatedly 3 rand)})

_unnamed [3 2]:

:x :y
0 0.70284352
1 0.79135992
2 0.78089413

3.34 test-last

kind/test-last allows to define a test over the previous form. This is still work-in-progress, and will be documented soon.

For now, pleaes see the documentation at the Clay tool.

If you are interested in this option, please reach out. We can test it with your project needs, and it will help stabilizing a useful API.

3.35 Documentation

(experimental πŸ§ͺ)

With kind/doc, one may create a documentation entries for a value. The entries will be level-3 headings (### in Markdown) and will thus appear in the table of contents when rendered through Quarto.

Typically, we interleave such documentation entries with free-form notebook examples.

(kind/doc :abcd/efgh)

3.35.1 :abcd/efgh

When the value is a var, the typical var metadata (arglists if present, and docstring) are added.

(kind/doc #'clojure.core/eduction)

3.35.2 eduction

[xform* coll]

Returns a reducible/iterable application of the transducers to the items in coll. Transducers are applied in order as if combined with comp. Note that these applications will be performed every time reduce/iterator is called.

source: notebooks/kinds.clj