Reactive data visualisations with Om
Upcoming SlideShare
Loading in...5
×
 

Reactive data visualisations with Om

on

  • 4,109 views

Talk presented at EuroClojure 2014

Talk presented at EuroClojure 2014

Statistics

Views

Total Views
4,109
Views on SlideShare
3,796
Embed Views
313

Actions

Likes
12
Downloads
40
Comments
0

5 Embeds 313

https://twitter.com 287
https://www.linkedin.com 11
http://stage.sl.pt 10
http://www.slideee.com 4
http://presentz.org 1

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Reactive data visualisations with Om Reactive data visualisations with Om Presentation Transcript

  • Reactive data visualisations with Om Anna Pawlicka Data Engineer @AnnaPawlicka Saturday, 28 June 14
  • Technologies Saturday, 28 June 14
  • 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
  • Leaflet.js & Dimple.js [higher level libraries] • Open-source Java-Script libraries • Interactive • Simple API • Access to underlying D3 functions Saturday, 28 June 14
  • 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
  • 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
  • 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
  • Component life cycle protocols IWillMount IRenderState IShouldUpdateIInitState IRender Saturday, 28 June 14
  • 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
  • Pretty charts Saturday, 28 June 14
  • 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
  • Chart & API Saturday, 28 June 14
  • (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
  • (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
  • (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
  • (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
  • (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
  • (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
  • (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
  • (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
  • (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
  • Last.fm chart Saturday, 28 June 14
  • (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
  • (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
  • (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
  • (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
  • Interactive maps Saturday, 28 June 14
  • Leaflet map & geocoding Saturday, 28 June 14
  • (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
  • (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
  • (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
  • (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
  • (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
  • (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
  • 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
  • Thank you! Saturday, 28 June 14