Introduction to Rid3

Matthew Jaoudi, @gadfly361

August 8th, 2017

The Goal

To make pretty visualizations in clojurescript SPA

D3.js

  • visualization library
  • SVG
  • DOM manipulation

Reagent

Outline

  • Static example w/o Rid3
  • Dynamic example w/o Rid3
  • Describe the problem
  • Introduce Rid3
  • Dynamic example w/ Rid3
  • GUP example w/o Rid3
  • GUP example w/ Rid3

Static example w/o Rid3

Objectives

  • create svg with grey background
  • put green circle in center of svg


Source Code

(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}))

React Component Lifecycle

Mounting

  • componentWillMount
  • render
  • componentDidMount

Updating

  • componentWillReceiveProps
  • shouldComponentUpdate
  • render
  • componentDidUpdate

Unmounting

  • componentWillUnmount

Output

Dynamic example w/o Rid3

Objectives

  • create svg with grey background
  • put green circle in center of svg
  • update radius dynamically


Source Code

;; 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
    }))

Output

React Component Lifecycle

Mounting

  • componentWillMount
  • render
  • componentDidMount

Updating

  • componentWillReceiveProps
  • shouldComponentUpdate
  • render
  • componentDidUpdate

Unmounting

  • componentWillUnmount

Dynamic example w/o Rid3 (attempt 2)

Objectives

  • create svg with grey background
  • put green circle in center of svg
  • update radius dynamically


Source Code

(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
    }))

Output

Dynamic example w/o Rid3 (attempt 3)

Objectives

  • create svg with grey background
  • put green circle in center of svg
  • update radius dynamically


Source Code

(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
    }))

Output

So … what's the problem?

Manually deref ratom

Manually append elements

Manually select nodes

Not DRY

Manually create Form-3 component

Let's see it in the code

(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
    }))

Ok, but is this really a problem?

Yes

Visualizations grow quickly w.r.t number and complexity of elements

Enter Rid3

What is it Rid3?

  • Reagent interface to d3


Ok … not really an interface, but I liked the acronym

API

Rid3 exposes just one thing, a reagent component: viz

Argument to viz is a hashmap

Using 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"))))}]}])

Output

Rid3 Benefits

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

What is D3's General Update Pattern?

  • Data joins
  • Enter
  • Update
  • Exit

General Update Pattern w/o Rid3

Objectives

  • create svg with grey background
  • create dynamic barchart inside svg
  • add margin around barchart


Source

;; 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)}))

Output

General Update Pattern w/ Rid3

Objectives

  • create svg with grey background
  • create dynamic barchart inside svg
  • add margin around barchart


Source

(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")))))))}]}])

Output

Outro

Thanks for listening!



Any Questions?