reactive data visualisations with om

36
Reactive data visualisations with Om Anna Pawlicka Data Engineer @AnnaPawlicka Saturday, 28 June 14

Upload: anna-pawlicka

Post on 06-May-2015

7.958 views

Category:

Technology


2 download

DESCRIPTION

Talk presented at EuroClojure 2014

TRANSCRIPT

Page 1: Reactive data visualisations with Om

Reactive data visualisations with OmAnna PawlickaData Engineer

@AnnaPawlicka

Saturday, 28 June 14

Page 2: Reactive data visualisations with Om

Technologies

Saturday, 28 June 14

Page 3: Reactive data visualisations with Om

D3 (Data-Driven Documents)[to visualise data]

• Data bound to DOM

• Interactive - transformations driven by data

• Huge community

• Higher level libraries available

Saturday, 28 June 14

Page 4: Reactive data visualisations with Om

Leaflet.js & Dimple.js[higher level libraries]

• Open-source Java-Script libraries

• Interactive

• Simple API

• Access to underlying D3 functions

Saturday, 28 June 14

Page 5: Reactive data visualisations with Om

Facebook’s React[interface components]

• Solves complex UI rendering

• Declarative framework

• No to “two-way data binding”

• Re-renders the entire UI

Saturday, 28 June 14

Page 6: Reactive data visualisations with Om

U can’t touch this[a.k.a. Virtual DOM]

• Developer describes the document tree

• React :

• Maintains virtual DOM

• Diffs between previous and next renders of a UI

• Less code

• Shorter time to update

Saturday, 28 June 14

Page 7: Reactive data visualisations with Om

Om Nom Nom Nom[because we prefer Clojure]

• Entire state of the UI in a single piece of data

• Immutable data structures = Reference equality check

• No need to worry about optimisation

• Snapshottable

• Free undo

Saturday, 28 June 14

Page 8: Reactive data visualisations with Om

Component life cycle protocols

IWillMount

IRenderState

IShouldUpdateIInitState

IRender

Saturday, 28 June 14

Page 9: Reactive data visualisations with Om

Liberator & core.async[component interaction]

• Provide API to access external components (e.g. database):

(defresource hello-world :available-media-types ["text/plain"] :allowed-methods [:get] :handle-ok (fn [_] "Hello, world.”))

• Send/receive messages between components using core.async channels:

(let [ch (chan)] (go (while true (let [v (<! ch)] (prn "Vader: " v)))) (go (>! ch "No, I am your father") (<! (timeout 5000)) (>! ch "Search your feelings; you know it to be true!")))

Saturday, 28 June 14

Page 10: Reactive data visualisations with Om

Pretty charts

Saturday, 28 June 14

Page 11: Reactive data visualisations with Om

device_id | type | timestamp | value------------------------------------------+------------------------+--------------------------------- 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 00:00:00+0000 | 8 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 00:05:00+0000 | 46 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 00:10:00+0000 | 23 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 00:15:00+0000 | 20 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 00:20:00+0000 | 67 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 00:25:00+0000 | 70 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 00:30:00+0000 | 10 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 00:35:00+0000 | 42 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 00:40:00+0000 | 95 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 00:45:00+0000 | 16 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 00:50:00+0000 | 79 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 00:55:00+0000 | 33 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 01:00:00+0000 | 45 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 01:05:00+0000 | 85 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 01:10:00+0000 | 32 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 01:15:00+0000 | 7 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 01:20:00+0000 | 92 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 01:25:00+0000 | 15 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 01:30:00+0000 | 9 8c077c2c3eac472d153886244e7b8aa6cad6a7e7 | electricityConsumption | 2014-01-01 01:35:00+0000 | 73

Saturday, 28 June 14

Page 12: Reactive data visualisations with Om

Chart & API

Saturday, 28 June 14

Page 13: Reactive data visualisations with Om

(defresource measurements-resource [id type ctx] :allowed-methods #{:get} :available-media-types ["application/edn"] :handle-ok (partial retrieve-measurements id type))

(defresource devices-resource [_] :allowed-methods #{:get} :known-content-type? #{"application/edn"} :available-media-types #{"application/edn"} :handle-ok retrieve-devices)

(defroutes app-routes (ANY "/devices/" [] devices-resource) (ANY "/device/:id/type/:type/measurements/" [id type] (measurements-resource id type)) (route/not-found "Not Found"))

(def app (handler/site app-routes))

Saturday, 28 June 14

Page 14: Reactive data visualisations with Om

(def app-model (atom {:devices {:all []} :chart {:data []}}))

(om/root measurements-chart app-model {:target (.getElementById js/document "app") :shared {:url "http://localhost:3000/"}})

Saturday, 28 June 14

Page 15: Reactive data visualisations with Om

(defn measurements-chart [cursor owner] (reify om/IInitState (init-state [_] {:chans {:event-chan (chan (sliding-buffer 1))}}) om/IRenderState (render-state [_ {:keys [chans]}] (dom/div nil (om/build device-form (:devices cursor) {:init-state chans}) (om/build chart/chart-figure (:chart cursor) {:init-state chans :opts {:event-fn get-measurements :chart {:div {:id "chart" :width "100%" :height 600} :bounds {:x "5%" :y "15%" :width "80%" :height "50%"} :x-axis "timestamp" :y-axis "value" :plot js/dimple.plot.line}}})))))

Initialise core.async channel

Saturday, 28 June 14

Page 16: Reactive data visualisations with Om

(defn measurements-chart [cursor owner] (reify om/IInitState (init-state [_] {:chans {:event-chan (chan (sliding-buffer 1))}}) om/IRenderState (render-state [_ {:keys [chans]}] (dom/div nil (om/build device-form (:devices cursor) {:init-state chans}) (om/build chart/chart-figure (:chart cursor) {:init-state chans :opts {:event-fn get-measurements :chart {:div {:id "chart" :width "100%" :height 600} :bounds {:x "5%" :y "15%" :width "80%" :height "50%"} :x-axis "timestamp" :y-axis "value" :plot js/dimple.plot.line}}})))))

This is how you construct components

Triggered on arrival of a new message

Saturday, 28 June 14

Page 17: Reactive data visualisations with Om

(defn device-form [cursor owner] (reify om/IWillMount (will-mount [_] (let [host (:url (om/get-shared owner)) url (str host "devices/")] (GET url {:handler #(om/update! cursor [:all] %)}))) om/IRenderState (render-state [_ {:keys [event-chan]}] (let [devices (:all cursor)] (dom/div nil (dom/table nil (dom/thead nil (dom/tr nil (dom/th nil "Select") (dom/th nil "ID") (dom/th nil "Type") (dom/th nil "Description") (dom/th nil "Unit"))) (apply dom/tbody nil (om/build-all (form-row event-chan) devices))))))))

Sequence of components

Saturday, 28 June 14

Page 18: Reactive data visualisations with Om

(defn form-row [event-chan] (fn [the-item owner] (om/component (let [{:keys [id type description unit]} the-item] (dom/tr nil (dom/td nil (dom/input #js {:type "radio" :name "type" :value name :onChange (fn [e] (put! event-chan {:id id :type type}))})) (dom/td nil id) (dom/td nil type) (dom/td nil description) (dom/td nil unit))))))

Send message down the queue

Saturday, 28 June 14

Page 19: Reactive data visualisations with Om

(defn chart-figure [cursor owner {:keys [chart] :as opts}] (reify om/IWillMount (will-mount [_] (let [event-chan (om/get-state owner [:event-chan]) event-fn (:event-fn opts)] (go (while true (let [v (<! event-chan)] (event-fn cursor owner v)))))) om/IRender (render [_] (let [{:keys [id width height]} (:div chart)] (dom/div #js {:id id :width width :height height}))) om/IDidUpdate (did-update [_ _ _] (let [n (.getElementById js/document "chart")] (while (.hasChildNodes n) (.removeChild n (.-lastChild n)))) (when (:data cursor) (draw-chart cursor chart)))))

Reads the message from the queue

Saturday, 28 June 14

Page 20: Reactive data visualisations with Om

(defn get-measurements [cursor owner message]

(let [host (:url (om/get-shared owner)) {:keys [id type]} message url (str host "device/" id "/type/" type "/measurements/")]

(GET url {:handler #(om/update! cursor [:data] %)})))

Saturday, 28 June 14

Page 21: Reactive data visualisations with Om

(defn draw-chart [cursor {:keys [div bounds x-axis y-axis plot]}]

(let [{:keys [id width height]} div Chart (.-chart js/dimple) svg (.newSvg js/dimple (str "#" id) width height) data (get-in cursor [:data]) 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 nil plot (clj->js [x y]))]

(aset s "data" (clj->js data)) (.addLegend dimple-chart "5%" "10%" "20%" "10%" "right") (.draw dimple-chart)))

Saturday, 28 June 14

Page 22: Reactive data visualisations with Om

Last.fm chart

Saturday, 28 June 14

Page 23: Reactive data visualisations with Om

(def app-model (atom {:username-box {:username ""} :chart {:data []}}))

(om/root lastfm-chart app-model {:target (.getElementById js/document "app") :shared {:api-root "http://ws.audioscrobbler.com/2.0/"}})

Saturday, 28 June 14

Page 24: Reactive data visualisations with Om

(defn lastfm-chart [cursor owner] (reify om/IInitState (init-state [_] {:chans {:event-chan (chan (sliding-buffer 1))}}) om/IRenderState (render-state [_ {:keys [chans]}] (dom/div nil (dom/div #js {:className "container"} (dom/h3 nil "Last.fm chart") (om/build forms/input-box (:username-box cursor) {:init-state chans})

(dom/div #js {:className "well" :style #js {:width "100%" :height 600}} (om/build chart/chart-figure (:chart cursor) {:init-state chans :opts {:event-fn get-all-artists :chart {:div {:id "chart" :width "100%" :height 600} :bounds {:x "5%" :y "15%" :width "80%" :height "50%"} :x-axis "name" :y-axis "playcount" :plot js/dimple.plot.bar}}})))))))

Username input and chart components

Saturday, 28 June 14

Page 25: Reactive data visualisations with Om

(defn get-all-artists [cursor owner username]

(let [api-root (:api-root (om/get-shared owner)) url (str api-root "?method=user.gettopartists&user=" username "&api_key=" api-key "&format=json")]

(GET url {:handler #(om/update! cursor [:data] (get-in % ["topartists" "artist"]))})))

Saturday, 28 June 14

Page 26: Reactive data visualisations with Om

(defn send-value [owner event-chan] (let [value (om/get-state owner :value)] (put! event-chan value)))

(defn input-box [cursor owner] (reify om/IRenderState (render-state [_ {:keys [event-chan]}] (dom/div #js {:className "form-inline" :role "form"} (dom/div #js {:className "form-group"} (dom/input #js {:type "text" :className "form-control" :style #js {:width "100%"} :onChange (fn [e] (om/set-state! owner :value (.-value (.-target e)))) :onKeyPress (fn [e] (when (= (.-keyCode e) 13) (send-value owner event-chan)))})) (dom/button #js {:type "button" :className "btn btn-primary" :onClick (fn [e] (send-value owner event-chan)} "Go")))))

Saturday, 28 June 14

Page 27: Reactive data visualisations with Om

Interactive maps

Saturday, 28 June 14

Page 28: Reactive data visualisations with Om

Leaflet map & geocoding

Saturday, 28 June 14

Page 29: Reactive data visualisations with Om

(def app-model (atom {:map {:leaflet-map nil :map {:lat 50.06297958283694 :lng 19.94705200195313}} :panel {:coordinates nil}}))

(om/root geocoded-map app-model {:target (. js/document (getElementById "app"))})

Saturday, 28 June 14

Page 30: Reactive data visualisations with Om

(defn geocoded-map [cursor owner] (reify om/IInitState (init-state [_] {:chans {:event-chan (chan (sliding-buffer 1)) :pin-chan (chan (sliding-buffer 1))}}) om/IRenderState (render-state [_ {:keys [chans]}] (dom/div nil (om/build map-component (:map cursor) {:init-state chans}) (om/build panel-component (:panel cursor) {:init-state chans})))))

Saturday, 28 June 14

Page 31: Reactive data visualisations with Om

(defn map-component [cursor owner] (reify om/IWillMount (will-mount [_] (let [event-chan (om/get-state owner [:event-chan])] (go (while true (let [v (<! event-chan)] (pan-to-postcode cursor owner v)))))) om/IRender (render [this] (dom/div #js {:id "map"})) om/IDidMount (did-mount [this] (let [node (om/get-node owner) {:keys [leaflet-map] :as map} (create-map (:map cursor) node) loc {:lng (get-in cursor [:map :lng]) :lat (get-in cursor [:map :lat])}] (.on leaflet-map "click" (fn [e] (let [latlng (.-latlng e)] (drop-pin owner leaflet-map latlng)))) (.panTo leaflet-map (clj->js loc)) (om/update! cursor :leaflet-map leaflet-map)))))

Creates map and stores it in app state

Saturday, 28 June 14

Page 32: Reactive data visualisations with Om

(defn pan-to-postcode [cursor owner postcode] (let [postcode (.toUpperCase (string/replace postcode #"[\s]+" "")) url (str geocoding-api-root postcode)] (GET url {:handler (fn [body] (let [map (:leaflet-map @cursor) {:keys [lat lng]} (location-from-response body)] (.panTo map (clj->js {:lat (js/parseFloat lat) :lng (js/parseFloat lng)}))))})))

(defn drop-pin [owner map latlng] (let [marker (-> (.addTo (.marker js/L (clj->js latlng)) map)) pin-chan (om/get-state owner [:pin-chan])]

(put! pin-chan {:action :put :coordinates latlng})

(.on marker "click" (fn [e] (.removeLayer map marker) (put! pin-chan {:action :remove})))))

Saturday, 28 June 14

Page 33: Reactive data visualisations with Om

(defn panel-component [cursor owner] (reify om/IWillMount (will-mount [_] (let [pin-chan (om/get-state owner [:pin-chan])] (go (while true (let [{:keys [action coordinates]} (<! pin-chan)] (if (= action :put) (om/update! cursor [:coordinates] coordinates) (om/update! cursor [:coordinates] nil))))))) om/IRender (render [_] (let [event-chan (om/get-state owner [:event-chan])] (dom/div #js {:id "panel"} (dom/h3 nil "Postcode lookup") (om/build forms/input-box cursor {:init-state {:event-chan event-chan}}) (om/build coordinates-component (:coordinates cursor)))))))

Saturday, 28 June 14

Page 34: Reactive data visualisations with Om

(defn coordinates-component [cursor owner] (om/component (dom/section nil (dom/h3 nil "Coordinates") (dom/p nil "(Click anywhere on a map)") (when cursor (dom/div nil (dom/label nil (str "Lat: " (.-lat cursor))) (dom/label nil (str "Lng: " (.-lng cursor))))))))

Saturday, 28 June 14

Page 35: Reactive data visualisations with Om

Summary• You can leverage all of JavaScript and ClojureScript functionality

and combine them with Om

• Fast rendering and interactivity

• Immutability = efficiency

• Sane application structure

• Reusability

Saturday, 28 June 14

Page 36: Reactive data visualisations with Om

Thank you!

Saturday, 28 June 14