Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Reactive data visualisations with Om

10,252 views

Published on

Talk presented at EuroClojure 2014

Published in: Technology
  • Be the first to comment

Reactive data visualisations with Om

  1. 1. Reactive data visualisations with Om Anna Pawlicka Data Engineer @AnnaPawlicka Saturday, 28 June 14
  2. 2. Technologies Saturday, 28 June 14
  3. 3. 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
  4. 4. Leaflet.js & Dimple.js [higher level libraries] • Open-source Java-Script libraries • Interactive • Simple API • Access to underlying D3 functions Saturday, 28 June 14
  5. 5. 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
  6. 6. 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
  7. 7. 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
  8. 8. Component life cycle protocols IWillMount IRenderState IShouldUpdateIInitState IRender Saturday, 28 June 14
  9. 9. 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
  10. 10. Pretty charts Saturday, 28 June 14
  11. 11. 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
  12. 12. Chart & API Saturday, 28 June 14
  13. 13. (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
  14. 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
  15. 15. (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
  16. 16. (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
  17. 17. (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
  18. 18. (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
  19. 19. (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
  20. 20. (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
  21. 21. (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
  22. 22. Last.fm chart Saturday, 28 June 14
  23. 23. (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
  24. 24. (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
  25. 25. (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
  26. 26. (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
  27. 27. Interactive maps Saturday, 28 June 14
  28. 28. Leaflet map & geocoding Saturday, 28 June 14
  29. 29. (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
  30. 30. (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
  31. 31. (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
  32. 32. (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
  33. 33. (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
  34. 34. (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
  35. 35. 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
  36. 36. Thank you! Saturday, 28 June 14

×