om nom nom nom
DESCRIPTION
Om nom nom nom Talk given at Clojure/conj 2014 in Washington DC Video available here: https://www.youtube.com/watch?v=4-oyZpLRQ20 Have you ever needed an easily customisable dashboard? Or needed to visualise data in a browser but was overwhelmed by d3.js? This talk will cover basics of React and Om, some data visualisation libraries and techniques, ways to handle live data and combining all that into an easily customisable dashboard. Expect demos, code and maybe, just maybe, om nom nom nom cookies.TRANSCRIPT
![Page 1: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/1.jpg)
Om Nom Nom NomAnna PawlickaData Engineer
@AnnaPawlicka
![Page 2: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/2.jpg)
Dashboard
• Communication tool
• See progress and compare values
• Show danger and success
• Share results
• Scan quickly
![Page 3: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/3.jpg)
Why build your own?
• We can never make a good dashboard that everyone will like
• Backend and front-end written in Clojure
• Clojurians like to build out of small components
• Mix and match & reuse
• It’s a good UI programming exercise
![Page 4: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/4.jpg)
A few principles
![Page 5: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/5.jpg)
Tables & Charts• Tables for values:
• Charts for trends and overall comparison:
Month Additons Deletions
1 29 14
2 68 34
Month Additions Deletions
1 29 14
2 68 34
vs.
0
25
50
75
100
April May June July0
50
100
150
200
April May June July0
5
10
15
20
0 3 6 9 12
![Page 6: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/6.jpg)
Don’t visualise!
Last week This week Variance
14 8 - 57%
0
3.5
7
10.5
14
2014-10-27 2014-11-03
8
14
0
3.5
7
10.5
14
2014-10-27 2014-11-03
vs.
![Page 7: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/7.jpg)
Avoid pie charts
Favourite Films
18
21
26
28
76
91
Sci-Fi Drama Romance Action Comedy Horror
Sci-Fi
Drama
Romance
Action
Comedy
Horror
0 25 50 75 100
18
21
26
28
76
91
vs.
![Page 8: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/8.jpg)
3D - why??Region 1 Region 2 Region 3
![Page 9: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/9.jpg)
Data-Ink Ratio
0
25
50
75
100
April May June July0
25
50
75
100
April May June July
![Page 10: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/10.jpg)
Step 1: Components
![Page 11: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/11.jpg)
Facebook’s React
• Solves complex UI rendering
• Declarative framework
• One way data binding
• Maintains virtual DOM
• Diffs between previous and next renders of a UI
• Less code
• Shorter time to update
![Page 12: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/12.jpg)
Om
• Entire state of the UI in a single piece of data
• Immutable data structures = Reference equality check
• No need to worry about optimisation
• Snapshot table & free undo
![Page 13: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/13.jpg)
Global state• Application state
• Global state stored in an atom
• Accessed and modified through cursors:
(def app-state (atom {:films {:data [{:v 1} {:v 2}]}}))
(-> cursor :films :data first)
:value {:v 1} :path [:films :data 0] :state #<Atom: {:films ..}>
Cursor
• Updated using:(om/update! cursor [:films :data] data)
(om/transact! cursor [:films :data] #(conj % data))
![Page 14: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/14.jpg)
Local & shared state
• Local state
• Local to single component
• Best for transient state, flags
• Updated by using:
• Shared state
• Accessible by the entire tree
• Does not trigger renders
(om/set-state! owner :value value)
![Page 15: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/15.jpg)
Components <-> Widgets• D3.js
• Data bound to DOM
• Interactive - transformations driven by data
• Huge community
• Higher level libraries available
• Dimple.js
• Higher level library powered by d3
• Interactive
• Simple API
• Access to underlying D3 functions
![Page 16: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/16.jpg)
Step 2: Data
![Page 17: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/17.jpg)
How fresh is fresh data?
• You could refresh for new data
• You could schedule to pull data at some intervals
• Or you could use:
• Long polling / HTTP streaming
• WebSockets
![Page 18: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/18.jpg)
WebSockets / Long polling• Http-kit:
• Clojure HTTP server
• Great out-of-the box support for WebSockets
• HTTP streaming / Long polling
• Sente:
• Clojure + ClojureScript + core.async + ajax + WebSockets
• Simple API
• Client & server
• There are also: Timbre, Jetty 7 websockets async
![Page 19: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/19.jpg)
Step 3: Combine them into dashboard a.k.a. demo
![Page 20: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/20.jpg)
Dev setup
• Chestnut:
• Figwheel - instant reloading ClojureScript, and CSS
• Weasel - browser-connected REPL is also included
• Heroku support
![Page 21: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/21.jpg)
Communication between server and client
• Server:
(let [{:keys [ch-recv send-fn ajax-get-or-ws-handshake-fn]} (s/make-channel-socket {})] (def ring-ajax-get-ws ajax-get-or-ws-handshake-fn) (def ch-chsk ch-recv) ; ChannelSocket's receive channel (def chsk-send! send-fn)) ; ChannelSocket's send API fn
• Client:
(let [{:keys [chsk ch-recv send-fn]} (s/make-channel-socket! "/ws" {} {:type :auto})] (def chsk chsk) (def ch-chsk ch-recv) ; ChannelSocket's receive channel (def chsk-send! send-fn)) ; ChannelSocket's send API fn
![Page 22: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/22.jpg)
• Server:
(defmethod handle-event :dashboard/github-issues [{:keys [?data ring-req]}] (let [{:keys [url refresh-rate]} ?data] (while true (http/get url {} (fn [{:keys [status headers body error]}] (chsk-send! uid [:dashboard/github-issues body]))) (Thread/sleep refresh-rate))))
(defn event-loop [] (go (loop [ev-msg (<! ch-chsk)] (thread (handle-event ev-msg)) (recur (<! ch-chsk)))))
• Client:
(defmethod handle-event :dashboard/github-issues [[_ msg] app] (om/update! app [:repository :issues :updated-at] (new js/Date)) (om/update! app [:repository :issues :value] (count msg)))
(defn event-loop [app] (go (loop [{:keys [event]} (<! ch-chsk)] … (handle-event payload app)))) (recur (<! ch-chsk)))))
![Page 23: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/23.jpg)
(defonce app-state (atom {:repository {:header {:selected-week nil} :code-frequency {:div {} :event-toggle {:current :hover} :data [] :updated-at nil} :contributors {:view {:current :charts} :data [] :updated-at nil}}}))
(defn main [] (let [event-chan (chan (sliding-buffer 100))] (om/root application app-state {:target (. js/document (getElementById "app")) :shared {:event-chan event-chan :mult-chan (mult event-chan)}})))
Shared channels
App model
![Page 24: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/24.jpg)
(defn dashboard [app owner] (reify om/IRender (render [_] (html [:div (r/jumbotron {} (html [:h1 "Dashboard" [:small (om/build clock (:clock app))]])) (om/build repository-view (-> app :repository) {:opts {:url “https://api.github.com/repos/mastodonc/kixi.hecuba"}})]))))
(defn application [app owner] (reify om/IWillMount (will-mount [_] (event-loop app owner)) om/IRender (render [_] (html [:div (case (:state app) :open (om/build dashboard app) :unknown [:div "Loading dashboard…"])]))))
Event loop
![Page 25: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/25.jpg)
(defn repository-view [cursor owner {:keys [url]}] (reify om/IDidMount (did-mount [_] (send-messages [{:k :dashboard/github-issues :url url}])) om/IRender (render [_] (let [{:keys [code-frequency contributors issues pulls]} cursor] (html [:div
(p/panel {:header (om/build toggles (-> code-frequency :event-toggle))} (om/build charts/stacked-bar-chart code-frequency))
(p/panel {:header (om/build toggles (-> contributors :view)} (om/build code-frequency-stats contributors))
(om/build numbers/simple-stats-card issues) (om/build numbers/simple-stats-card pulls)])))))
![Page 26: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/26.jpg)
(defmulti code-frequency-stats (fn [cursor owner] (-> cursor :view :current)))
(defmethod code-frequency-stats :charts [cursor owner] (om/component (html [:div (when (seq (:data cursor)) (om/build chart-stats-view {:data (:data cursor) :div (:div cursor)} {:fn (parse-contributors :charts)}))])))
(defmethod code-frequency-stats :table [cursor owner] (om/component (html [:div (om/build table-stats-view (:data cursor) {:fn (parse-contributors :table)})])))
(defmethod code-frequency-stats :cards [cursor owner] (om/component (html [:div (om/build team-members-stats-view (:data cursor) {:fn (parse-contributors :table)})])))
Transform data before rendering it, without losing benefits of a
cursor
![Page 27: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/27.jpg)
(defn chart-stats [cursor owner {:keys [y-axis color]}] (reify om/IInitState (init-state [_] {:c (chan (sliding-buffer 10)) :value (-> cursor :data first :week)}) om/IWillMount (will-mount [_] (let [c (om/get-state owner :c) mult-chan (om/get-shared owner :mult-chan) week (header)] (tap mult-chan c) (go-loop [] (let [event-chan (om/get-state owner :c) {:keys [event v]} (<! event-chan)] (om/set-state! owner :value v) (om/update! week :selected-week (common/unparse-date v "yyyy-MM-dd"))) (recur)))) om/IRenderState … om/IWillUnmount
(will-unmount [_] (untap (om/get-shared owner :mult-chan) (om/get-state owner :c))) …))
New channel that we tap to to our shared channel
![Page 28: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/28.jpg)
(defn chart-stats [cursor owner {:keys [y-axis color]}] (reify om/IInitState … om/IWillMount … om/IRenderState (render-state [_ state] (html [:div (let [value (common/timestamp->value (:value state) "week" (:data cursor)) selected-event (om/observe owner (event-type))] (om/build charts/bar-chart {:data value :div (:div cursor)} {:opts {:y-axis y-axis :event-type (:current selected-event)}}))])) om/IWillUnmount …))
Reference cursor(defn event-type [] (om/ref-cursor (-> (om/root-cursor app-state) :repository :code-frequency :event-toggle)))
![Page 29: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/29.jpg)
dimple chart(defn bar-chart [{:keys [data div]} owner {:keys [id] :as opts}] (reify om/IWillMount (will-mount [_] (.addEventListener js/window "resize" (fn [] (let [e (.getElementById js/document id) x (.-clientWidth e) y (.-clientHeight e)] (om/update! div :size {:width x :height y}))))) om/IRender (render [_] (html [:div {:id id}])) om/IDidMount (did-mount [_] (let [n (.getElementById js/document id)] (while (.hasChildNodes n) (.removeChild n (.-lastChild n)))) (draw-chart data div opts)) om/IDidUpdate (did-update [_ _ _] (let [n (.getElementById js/document id)] (while (.hasChildNodes n) (.removeChild n (.-lastChild n)))) (draw-chart data div opts))))
![Page 30: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/30.jpg)
dimple chart(defn- draw-chart [data div {:keys [id bounds x-axis y-axis plot series event-type]}]
(let [width (:width div) height (:height div) Chart (.-chart js/dimple) svg (.newSvg js/dimple (str "#" id)
width height) dimple-chart (.setBounds (Chart. svg) (:x bounds) (:y bounds) (:width bounds) (:height bounds)) x (.addCategoryAxis dimple-chart "x" x-axis) y (.addMeasureAxis dimple-chart "y" y-axis) s (.addSeries dimple-chart series plot (clj->js [x y]))]
(aset s "data" (clj->js data)) (.draw dimple-chart (when (= event-type :click) 1000))))
![Page 31: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/31.jpg)
stacked bar chart(defn- draw [data hover-chan size id event-type] ... (doto (.selectAll svg ".frequency") (-> (.data stacked) (.enter) … ;; Event listeners (cond-> (= event-type :click) (.on "click" (fn [d] (put! hover-chan {:event :click :v (.-x d) :d d})))) (cond-> (= event-type :hover) (.on "mouseover" (fn [_] (put! hover-chan {:event :mouseover :v (.-x d) :d d})))))))
![Page 32: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/32.jpg)
That’s all folks!
• Try out all the amazing libraries out there
• Don’t be afraid of JavaScript interop
• Use Om with data visualisation libraries - it’s easy
• Share your components
• Share your tips and tricks
• Don’t create 3D pie charts
![Page 33: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/33.jpg)
Thank you!
![Page 34: Om nom nom nom](https://reader034.vdocuments.net/reader034/viewer/2022042501/5594456c1a28abfa2f8b4823/html5/thumbnails/34.jpg)
Useful links
• Om: https://github.com/swannodette/om
• Sente: https://github.com/ptaoussanis/sente
• Chestnut: https://github.com/plexus/chestnut
• Demo: https://github.com/annapawlicka/pumpkin