To make pretty visualizations in clojurescript SPA
Objectives
(defn viz-render []
[:div {:id "viz"}])
(defn viz-did-mount []
;; Select node
(let [node (js/d3.select "#viz")]
(-> node
(.append "svg") ;; Focus is on svg
(.attr "height" 100)
(.attr "width" 100)
(.style "background-color" "lightgrey")
(.append "g") ;; Focus is on g
(.append "circle") ;; Focus is on circle
(.attr "r" 25)
(.attr "cx" 50)
(.attr "cy" 50)
(.attr "fill" "green")
)))
(defn viz []
(reagent/create-class
{:reagent-render viz-render
:component-did-mount viz-did-mount}))
Mounting
Updating
Unmounting
Objectives
;; Add reagent atom (i.e., ratom)
(defonce radius-ratom (reagent/atom 25))
(defn viz-render[]
;; deref ratom to cause re-render
(let [_ @radius-ratom]
[:div {:id "viz"}]))
(defn viz-did-mount []
(let [node (js/d3.select "#viz")
radius @radius-ratom] ;; Depend on ratom
(-> node
(.append "svg")
(.attr "height" 100)
(.attr "width" 100)
(.style "background-color" "lightgrey")
(.append "g")
(.append "circle")
(.attr "r" radius)
(.attr "cx" 50)
(.attr "cy" 50)
(.attr "fill" "green"))))
(defn viz []
(reagent/create-class
{:reagent-render viz-render
:component-did-mount viz-did-mount
}))
Mounting
Updating
Unmounting
Objectives
(defonce radius-ratom (reagent/atom 25))
(defn viz-render[]
(let [_ @radius-ratom]
[:div {:id "viz"}]))
(defn viz-did-mount []
(let [node (js/d3.select "#viz")
radius @radius-ratom]
(-> node
(.append "svg")
(.attr "height" 100)
(.attr "width" 100)
(.style "background-color" "lightgrey")
(.append "g")
(.append "circle")
(.attr "r" radius)
(.attr "cx" 50)
(.attr "cy" 50)
(.attr "fill" "green"))))
(defn viz []
(reagent/create-class
{:reagent-render viz-render
:component-did-mount viz-did-mount
;; add component-did-update lifecycle
;; (using same fn as did-mount)
:component-did-update viz-did-mount
}))
Objectives
(defonce radius-ratom (reagent/atom 25))
(defn viz-render[]
(let [_ @radius-ratom]
[:div {:id "viz"}]))
(defn viz-did-mount []
(let [node (js/d3.select "#viz")
radius @radius-ratom]
(-> node
(.append "svg")
(.attr "height" 100)
(.attr "width" 100)
(.style "background-color" "lightgrey")
(.append "g")
(.append "circle")
(.attr "r" radius)
(.attr "cx" 50)
(.attr "cy" 50)
(.attr "fill" "green"))))
;; Add did-update fn that doesn't append elements
(defn viz-did-update [ratom]
(let [node (js/d3.select "#viz svg circle")
radius @radius-ratom]
(-> node
(.attr "r" radius)
(.attr "cx" 50)
(.attr "cy" 50)
(.attr "fill" "green"))))
(defn viz []
(reagent/create-class
{:reagent-render viz-render
:component-did-mount viz-did-mount
;; Use did-update fn
:component-did-update viz-did-update
}))
Manually deref ratom
Manually append elements
Manually select nodes
Not DRY
Manually create Form-3 component
(defonce radius-ratom (reagent/atom 25))
(defn viz-render[]
;; Manually deref ratom
(let [_ @radius-ratom]
[:div {:id "viz"}]))
(defn viz-did-mount []
(let [;; Manually select DOM node
node (js/d3.select "#viz")
radius @radius-ratom]
(-> node
;; Manually append svg
(.append "svg")
(.attr "height" 100)
(.attr "width" 100)
(.style "background-color" "lightgrey")
;; Manually append g
(.append "g")
;; Manually append circle
(.append "circle")
;; Code repeated in viz-did-update
(.attr "r" radius)
(.attr "cx" 50)
(.attr "cy" 50)
(.attr "fill" "green"))))
(defn viz-did-update [ratom]
(let [;; Manually select DOM node
node (js/d3.select "#viz svg circle")
radius @radius-ratom]
(-> node
(.attr "r" radius)
(.attr "cx" 50)
(.attr "cy" 50)
(.attr "fill" "green"))))
;; Manually create a Form-3 component
(defn viz []
(reagent/create-class
{:reagent-render viz-render
:component-did-mount viz-did-mount
:component-did-update viz-did-update
}))
Yes
Visualizations grow quickly w.r.t number and complexity of elements
What is it Rid3?
Ok … not really an interface, but I liked the acronym
Rid3 exposes just one thing, a reagent component: viz
(defonce radius-ratom (reagent/atom 25))
(defn viz []
[rid3/viz
{:id "viz"
:ratom radius-ratom
:svg {:did-mount
(fn [node ratom]
(-> node
(.attr "height" 100)
(.attr "width" 100)
(.style "background-color" "lightgrey")))}
:pieces
[{:kind :elem
:tag "circle"
:class "my-circle"
:did-mount
(fn [node ratom]
(let [radius @ratom]
(-> node
(.attr "r" radius)
(.attr "cx" 50)
(.attr "cy" 50)
(.attr "fill" "green"))))}]}])
Derefs ratom for you
Appends elements for you
Passes appropriate node as argument
DRY code
Avoids use of Form-3 component
Defaults did-mount function to did-update
Enforces classes on tags bringing visibility to svg structure
Implicitly uses D3's General Update Pattern
Objectives
;; Vars
(def width 160)
(def height 160)
(def margin {:top 16
:right 16
:bottom 16
:left 16})
(defonce app-state
(reagent/atom
{:data [{:x 5}
{:x 2}
{:x 3}]}))
;; svg (react lifecycle)
(defn svg-did-mount [ratom]
(-> (js/d3.select "#barchart")
(.append "svg")
(.attr "width" (+ width
(:left margin)
(:right margin)))
(.attr "height" (+ height
(:top margin)
(:bottom margin)))
(.style "background-color" "lightgrey")))
;; main-container (react lifecycle)
(defn main-container-did-mount [ratom]
(-> (js/d3.select "#barchart svg")
(.append "g")
(.attr "class" "main-container")
(.attr "transform"
(str "translate("
(:left margin)
","
(:top margin)
")"))))
;; bars (d3 general update pattern)
(defn bars-data-join->node [ratom]
(let [data (:data @ratom)]
(-> (js/d3.select "#barchart svg .main-container .bars")
(.selectAll "rect")
(.data (clj->js data)))))
(defn bars-enter [ratom]
(let [node (bars-data-join->node ratom)]
(-> node
.enter
(.append "rect"))))
(defn bars-update [ratom]
(let [node (bars-data-join->node ratom)
data (:data @ratom)
data-n (count data)
rect-height (/ height data-n)
x-scale (-> js/d3
.scaleLinear
(.domain #js [0 5])
(.range #js [0 width]))]
(-> node
(.attr "fill" "green")
(.attr "x" (x-scale 0))
(.attr "y" (fn [_ i]
(* i rect-height)))
(.attr "height" (- rect-height 1))
(.attr "width" (fn [d]
(x-scale (aget d "x")))))))
(defn bars-exit [ratom]
(let [node (bars-data-join->node ratom)]
(-> node
.exit
.remove)))
;; bars (react lifecycle)
(defn bars-did-update [ratom]
(bars-enter ratom)
(bars-update ratom)
(bars-exit ratom))
(defn bars-did-mount [ratom]
(-> (js/d3.select "#barchart svg .main-container")
(.append "g")
(.attr "class" "bars"))
(bars-did-update ratom))
;; Main
(defn viz-render [ratom]
(let [_ @ratom]
[:div
{:id "barchart"}]))
(defn viz-did-mount [ratom]
(svg-did-mount ratom)
(main-container-did-mount ratom)
(bars-did-mount ratom))
(defn viz-did-update [ratom]
(bars-did-update ratom))
(defn viz [ratom]
(reagent/create-class
{:reagent-render #(viz-render ratom)
:component-did-mount #(viz-did-mount ratom)
:component-did-update #(viz-did-update ratom)}))
Objectives
(defonce app-state
(reagent/atom
{:width 160
:height 160
:margin {:top 16
:right 16
:bottom 16
:left 16}
:dataset [{:x 5}
{:x 2}
{:x 3}]}))
(defn viz []
[rid3/viz
{:id "barchart"
:ratom app-state
:svg
{:did-mount
(fn [node ratom]
(let [{:keys [margin
width
height]} @ratom]
(-> node
(.attr "width" (+ width
(:left margin)
(:right margin)))
(.attr "height" (+ height
(:top margin)
(:bottom margin)))
(.style "background-color" "lightgrey"))))}
:main-container
{:did-mount
(fn [node ratom]
(let [margin (:margin @ratom)]
(-> node
(.attr "transform"
(str "translate("
(:left margin)
","
(:top margin)
")")))))}
:pieces
[{:kind :elem-with-data
:tag "rect"
:class "bars"
:did-mount
(fn [node ratom]
(let [{:keys [dataset
width
height]} @ratom
data-n (count dataset)
rect-height (/ height data-n)
x-scale (-> js/d3
.scaleLinear
(.domain #js [0 5])
(.range #js [0 width]))]
(-> node
(.attr "fill" "green")
(.attr "x" (x-scale 0))
(.attr "y" (fn [_ i]
(* i rect-height)))
(.attr "height" (- rect-height 1))
(.attr "width" (fn [d]
(x-scale (aget d "x")))))))}]}])
Thanks for listening!
Any Questions?