Download - Om nom nom nom

Transcript
Page 1: Om nom nom nom

Om Nom Nom NomAnna PawlickaData Engineer

@AnnaPawlicka

Page 2: Om nom nom nom

Dashboard

• Communication tool

• See progress and compare values

• Show danger and success

• Share results

• Scan quickly

Page 3: Om nom nom nom

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

A few principles

Page 5: Om nom nom nom

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

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

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

3D - why??Region 1 Region 2 Region 3

Page 9: Om nom nom nom

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

Step 1: Components

Page 11: Om nom nom nom

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

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

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

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

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

Step 2: Data

Page 17: Om nom nom nom

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

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

Step 3: Combine them into dashboard a.k.a. demo

Page 20: Om nom nom nom

Dev setup

• Chestnut:

• Figwheel - instant reloading ClojureScript, and CSS

• Weasel - browser-connected REPL is also included

• Heroku support

Page 21: Om nom nom nom

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

• 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

(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

(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

(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

(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

(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

(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

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

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

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

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

Thank you!

Page 34: Om nom nom nom

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