Weitere ähnliche Inhalte Ähnlich wie Reactive data visualisations with Om (20) Kürzlich hochgeladen (20) Reactive data visualisations with Om3. 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
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
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
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
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
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