Reactive data visualisations with Om

  • 7,235 views
Uploaded on

Talk presented at EuroClojure 2014

Talk presented at EuroClojure 2014

More in: Technology
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
No Downloads

Views

Total Views
7,235
On Slideshare
0
From Embeds
0
Number of Embeds
5

Actions

Shares
Downloads
63
Comments
0
Likes
15

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. Reactive data visualisations with Om Anna Pawlicka Data Engineer @AnnaPawlicka Saturday, 28 June 14
  • 2. Technologies Saturday, 28 June 14
  • 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. 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. 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. 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. 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. Component life cycle protocols IWillMount IRenderState IShouldUpdateIInitState IRender Saturday, 28 June 14
  • 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. Pretty charts Saturday, 28 June 14
  • 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. Chart & API Saturday, 28 June 14
  • 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. (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. (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. (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. (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. (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. (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. (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. (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. Last.fm chart Saturday, 28 June 14
  • 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. (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. (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. (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. Interactive maps Saturday, 28 June 14
  • 28. Leaflet map & geocoding Saturday, 28 June 14
  • 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. (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. (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. (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. (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. (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. 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. Thank you! Saturday, 28 June 14