Post on 02-Jul-2015
description
Om Nom Nom NomAnna PawlickaData Engineer
@AnnaPawlicka
Dashboard
• Communication tool
• See progress and compare values
• Show danger and success
• Share results
• Scan quickly
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
A few principles
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
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.
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.
3D - why??Region 1 Region 2 Region 3
Data-Ink Ratio
0
25
50
75
100
April May June July0
25
50
75
100
April May June July
Step 1: Components
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
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
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))
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)
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
Step 2: Data
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
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
Step 3: Combine them into dashboard a.k.a. demo
Dev setup
• Chestnut:
• Figwheel - instant reloading ClojureScript, and CSS
• Weasel - browser-connected REPL is also included
• Heroku support
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
• 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)))))
(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
(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
(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)])))))
(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
(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
(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)))
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))))
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))))
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})))))))
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
Thank you!
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