Web Plotter - Plotting from Incanter to Websites

1 Introduction

This is my first try with literate programming. I am going to do some simple plotting with Incanter to HTML using NVD3 and following the Clojure Data Analysis Cookbook.

2 Project

2.1 Project definition

Project dependencies can be check for new versions via `lein ancient`. An upgrade can be done with `lein ancient upgrade`. lein-ancient is not part of the project and should be instaled in the user profiles `~/.lein/profiles.clj`.

(defproject web-plotter "0.1.0-SNAPSHOT"
  :description "plotting with Incanter and NVD3"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.6.0"]
                 [ring/ring-core "1.3.2"]
                 [ring/ring-jetty-adapter "1.3.2"]
                 [compojure "1.3.1"]
                 [hiccup "1.0.5"]
                 [org.clojure/clojurescript "0.0-2760"]]
  :plugins [[lein-ring "0.9.1"]
            [lein-cljsbuild "1.0.4"]]
  :ring {:handler web-plotter.server/app}
  :cljsbuild {:builds
              [{:source-paths ["src-cljs"]
                :compiler
                {:output-to "resources/js/main.js"
                 :optimizations :whitespace
                 :pretty-print true}}]})

2.2 README

# Web Plotter
This is a literate programming experiment, please check out the generated, woven documentation:

["Woven" Documentation](http://nblumoe.github.io/web-plotter/)

2.3 License note

3 Application

3.1 Server

The backend application.

3.1.1 Namespace setup

(ns web-plotter.server
  (:require [compojure.route :as route]
            [compojure.handler :as handler]
            [clojure.string :as str])
  (:use compojure.core
        ring.adapter.jetty
        [ring.middleware.content-type :only (wrap-content-type)]
        [ring.middleware.file :only (wrap-file)]
        [ring.middleware.file-info :only (wrap-file-info)]
        [ring.middleware.stacktrace :only (wrap-stacktrace)]
        [ring.util.response :only (redirect)]
        [hiccup core element page]
        [hiccup.middleware :only (wrap-base-url)]))

Ring is used via jetty for handling HTTP requests and Compojure for routing. (Ring concepts) Hiccup renders Clojure data structures to HTML.

3.1.2 Hiccup Templates

  1. Index

    Rendering an index page via Hiccup. The provided Clojure datastructure is mapped to HTML and should be easy to understand.

    (defn index-page []
      (html5
        [:head
         [:title "Web Plotter"]]
        [:body
         [:h1 {:id "web-plotter"} "Web Plotter"]
         [:ol
          [:li [:a {:href "scatter"} "Scatter plot"]]
          [:li [:a {:href "data/example.json"} "Some example data"]]]
         (include-js "js/main.js")]))
    
  2. NVD3 layout

    Page layout for rendering NVD3 plots.

    Title and HTML body contents will be inserted, as well as a JS snippet to initialize the plotting. Additional JS files can be included via the optional parameter.

    (defn d3-page
      [title js body & {:keys [extra-js] :or {extra-js []}}]
      (html5
        [:head
         [:title title]
         (include-css "/css/nv.d3.css")
         (include-css "/css/main.css")
         (include-css "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css")
         (include-css "https://cdnjs.cloudflare.com/ajax/libs/bootstrap-material-design/0.2.2/css/material-wfont.min.css")
         ]
        [:body
          (concat
            [body]
            [ (include-js "http://d3js.org/d3.v3.min.js")
              (include-js "https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.7.0/nv.d3.min.js")
              (include-js "https://code.jquery.com/jquery-2.1.3.min.js")
              (include-js "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js")
              (include-js "https://cdnjs.cloudflare.com/ajax/libs/bootstrap-material-design/0.2.2/js/material.min.js")]
            (map include-js extra-js)
            [(include-js "js/main.js")
             (javascript-tag js)])]))
    
  3. Scatter plot page

    An example page for a scatter plot. Uses the NVD3 page template.

    (defn scatter-plot-page []
      (d3-page "Scatter Plot"
        "web_plotter.scatter.scatter_plot();"
        [:div.container
         [:div.row
          [:div.col-md-12
           [:h1 "Scatter Plot"]]]
         [:div.row
          [:div.col-md-12
           [:div#scatter.chart [:svg]]]]]))
    

3.1.3 Routes

Define the routes of the server via Compojure.

The Compojure DSL allows to compose routes into a single Ring application handler. Common route definitions include the HTTP verb, a path, parameters and a response.

(defroutes
  site-routes
  (GET "/" [] (index-page))
  (GET "/scatter" [] (scatter-plot-page))
  (GET "/scatter/data.json" [] (redirect "/data/census-race.json"))
  (route/resources "/")
  (route/not-found "Page not found"))
  • The root path will be redirected to the json data.
  • The resources directory gets served as static files from the web server root path.
  • If no matching route is found, return an error message.

3.1.4 Request handling

Set up the app request handler which was build with Compojure. Middlewares for serving static files, adding file info and setting content-type headers are wrapped around the handler.

(def app
  (-> (handler/site site-routes)
    (wrap-file "resources")
    (wrap-file-info)
    (wrap-content-type)))

3.2 Client

Client side code.

3.2.1 Core NVD3 helper functions

Namespace definition.

(ns web-plotter.core)

Define two types for NVD3.

(deftype Group [key values])

(deftype Point [x y size])

A function to add a label to an axis.

(defn add-label
  [chart axis label]
  (if-not (nil? label)
    (.axisLabel (aget chart axis) label)))

Add labels to both axes.

(defn add-axes-labels
  [chart x-label y-label]
  (doto chart
    (add-label "xAxis" x-label)
    (add-label "yAxis" y-label)))

Populating a chart with data and plotting it.

(defn populate-node
  [selector chart groups transition continuation]
  (-> (.select js/d3 selector)
    (.datum groups)
    (.transition)
    (.duration 50000)
    (.call chart)
    (.call continuation)))

Get data from an URL and create a plot with labelled axes from the data.

(defn create-chart
  [data-url selector make-chart json->groups &
   {:keys [transition continuation x-label y-label]
    :or {transition false continuation (fn [_])
         x-label nil, y-label nil}}]
  (.json js/d3 data-url
    (fn [error data]
      (when data
        (.addGraph js/nv
          (fn [] (let [chart (make-chart)]
                  (add-axes-labels chart x-label y-label)
                  (populate-node selector chart (json->groups data)
                    transition continuation)
                  (.windowResize js/nv.utils #(.update chart)))))))))

3.2.2 Scatter plots

(ns web-plotter.scatter
  (:require [web-plotter.core :as web-plotter]))

(defn sum-by [key-fn coll]
  (reduce + 0 (map key-fn coll)))

(defn sum-values [key-fn coll]
  (reduce
    (fn [m [k vs]] (assoc m k (sum-by key-fn vs)))
    {}
    coll))


(defn sum-data-fields [json]
  (let [by-state (group-by #(.-state_name %) json)
        white-by-state (sum-values #(.-white %) by-state)
        afam-by-state (sum-values #(.-black %) by-state)
        total-by-state (sum-values #(.-total %) by-state)]
    (map #(hash-map :state %
            :white (white-by-state %)
            :black (afam-by-state %)
            :total (total-by-state %))
      (keys by-state))))

(defn ->nv [item]
  (let [{:keys [white black]} item]
    (web-plotter/Point. (/ white 1000) (/ black 1000) 1)))

(defn ->nv-data [key-name data]
  (->> data
    sum-data-fields
    (map ->nv)
    (apply array)
    (web-plotter/Group. key-name)
    (array)))

(defn make-chart []
  (let [c (-> (.scatterChart js/nv.models)
            (.showDistX true)
            (.showDistY true)
            (.useVoronoi false)
            (.color (.. js/d3 -scale category10 range)))]
    (.tickFormat (.-xAxis c) (.format js/d3 "d"))
    (.tickFormat (.-yAxis c) (.format js/d3 "d"))
    c))

(defn ^:export scatter-plot []
  (web-plotter/create-chart
    "/scatter/data.json"
    "#scatter svg"
    make-chart
    (partial ->nv-data "Racial Data")
    :x-label "Population, whites, by thousands"
    :y-label "Population, African-Americans, by thousands"
    :transition true))

4 Appendix

4.1 Ring concepts

Handlers
Ring handlers are Clojure functions taking a map representing the HTTP requests as an argument and return a map representing the HTTP response. Thus handlers transform requests into repsonses. (Request Map Reference, Response Map Reference).
Middleware

Middleware are higher order functions taking a handler as an argument and returning a new, transformed handler. Middleware extends the handling of raw HTTP requests to add functionalities like request parameters, sessions, file uploading etc.

Middleware example:

;; defining the middleware
(defn wrap-content-type [handler content-type]
  (fn[request]
    (let [response (handler request)]
      (assoc-in response [:headers "Content-Type"] content-type))))

;; wrapping the middleware around a handler
(def app (wrap-content-type handler "text/html"))
Adapters
Adapters connect Ring to web servers like Jetty, http-kit, etc.

Author: Nils Blum-Oeste

Created: 2015-02-08 So 10:38

Emacs 24.3.1 (Org mode 8.3beta)

Validate