Find file
Fetching contributors…
Cannot retrieve contributors at this time
266 lines (163 sloc) 8.61 KB

DRAFT: C2 will get its own git repository in March 2012

I'm just sketching out ideas for a D3-inspired Clojure data visualization library. If you've got ideas about how this kind of thing should look, definitely shoot me an email (or, if you're in Portland, OR, lets get a beer).

--Kevin September 2011


C2 is a declarative data visualization library that generates static SVG. It's like D3, but written in Clojure and less fancy!

Design Goals

The two eminent goals of C2 are composability and simplicity of expression.

Composability is achieved in the traditional functional-programming way: breaking things up into small pieces. In the case of C2, the fundamental visual pieces are SVG <g> elements, and the fundamental statistical pieces are closures. The <g> elements depict things like axes, parts of legends, sets of marks, &c.

// (pictures please!)

and the closures are different scales and statistical transforms that aggregate (e.g., histograms) or smooth / regress (e.g., best-fit models).

To make a C2 visualization, you explicitly apply transforms to your data, build fundamental visual elements, and then lay out these elements.

C2 provides a constraint-based layout system to place graphics according to both essential (i.e., data-driven) and incidental (e.g., margin + max-label-width) considerations.

Using declarative constraints between graphics rather than absolutely positioning low-level DOM elements prevents incidental layout concerns from tainting the essential visualization mappings, making graphics more reusable.

The simplicity of expression comes from Clojure; the C2 API embraces Clojure idioms like destructuring and laziness, and makes generous use of multi-arity functions. This gives C2 a concise, jQuery-like flavor compared to D3. Occasionally, C2 sacrifices absolute transparency for user-simplicity to sweep SVG DOM awkwardness under the rug and embrace best practices. For instance, while you can still say

(append "svg:svg") ;;=> <svg></svg>

you almost always want

(append :svg) ;;=> <svg xmlns=""
              ;;        xmlns:ev=""
              ;;        xmlns:xlink=""></svg>

(see SVG authoring guidelines)

Append also accepts a map of attributes, which is macro-expanded into a scope with a map-datum's values as locals. That is, you can write

(-> g-element
    (data [{:x 1 :y 2}, {:x 4 :y 0}])
    (append :circle {:cx x :cy y}))

which is equivalent to D3[{x: 1, y: 2}, {x: 4 y: 0}])
  .attr("cx", function(d){return d.x;})
  .attr("cy", function(d){return d.y;})

//How are we going to expand attribute-setter-maps when the data is just a scalar collection? //Is that magical scope going to be confusing for users? Maybe when they're doing subsets and binding vars in a (doseq)...

Unlike D3, C2 does not support nested looping via multiple calls to .data(). Rather, C2's (data) function can only be called once for a given selection; nested elements can be constructed by explict use of Clojure's looping constructs like (doseq):

(doseq [[something-val, subset] (group-by :something the-data)]
  (append svg (dotplot subset, :scale s))
  (append svg (label something-val)))


In D3 one tends to mix essential visualization data aesthetics with incidental layout code. To make a bar chart you might write something like"#my-svg")
.data([9, 2, 4, 11, 19, 13])
.attr("class", "bar")
.attr("height", function(x, i){return x;}) //essential mapping
.attr("x", function(x, i){return i*bar_spacing;}); //incidental mapping

In C2, you would write something like



;; A graphical timecard like

;;Factor it into the two guides and dataframe.

(let [hourcard-data [{:day-of-week 0 :hour 0 :val 0}
                     {:day-of-week 0 :hour 1 :val 1.3}
                     {:day-of-week 6 :hour 13 :val 2.1}
                     {:day-of-week 3 :hour 11 :val 2}]
      svg (append "#timecard-chart" :svg)

      ;;Make the dataframe
      df-g (append svg :g {:id "timecard-df"})
      ;;The day-of-week guide on the left
      dow-guide (append svg :g {:id "dow-guide" :class "guide"})
      ;;The hour guide on the right
      hour-guide (append svg :g {:id "hour-guide" :class "guide"})

      width 800, height 300, max-radius 10

      ;;Make the two positional scales; min-max returns [min, max]
      scale-hour (scale/linear :domain (min-max timecard-data :dimension :hour)
                               :range [0 width])
      scale-dow (scale/linear :domain (min-max timecard-data :dimension :day-of-week)
                              :range [0 height])
      ;;The radius scale has the same domain as the hour scale, so derive it.
      scale-radius (range scale-hour [0 max-radius])
      scale-colour (scale/linear :domain (min-max timecard-data :dimension :val)
                                 ;;The Raphael example uses green->red, which is a terrible color scale for the colourblind.
                                 :range ["white" "blue"])]
  ;;Fill the dataframe
  (-> df-g
      (data ds) (append :circle {:class "hour-circle"
                                 :fill  (scale-colour val)
                                 :cx    (scale-hour hour)
                                 :cy    (scale-dow day-of-week)
                                 :r     (scale-radius val)}))

  ;;Append the day-of-week guide
  (-> dow-guide
      (append (guide scale-dow
                     :direction :vertical
                     ;;A function that accepts tick values in the domain.
                     ;;In this case, we're using vectors as functions; (["A", "B"] 1) ;;=> "B".
                     :text ["Mon" "Tue" "Wed" "Thur" "Fri" "Sat" "Sun"]
                     :tick nil))) ;;no tickmarks please

  ;;And the hour guide
  (-> hour-guide
      (append (guide scale-hour
                     :direction :horizontal
                     :text #(condp = %
                                     0  "12am"
                                     12 "12pm"
                                     (mod % 12))
                     :tick nil)))) ;;no tickmarks please

Open questions

Should we abstract dealing with labels that are different than attribute values; {:dow 0} => "Sunday"?

How can we bind individual data to a hierarchy of DOM elements re-usably? (This might only be an issue when adding animation or update/binding support.) For instance, if we want a datum in a graph visualization to be represented by

<g class="node">
  <circle cx=d.x cy=d.y></circle>
  <text class="label"></text>

C2 rejects a custom scenegraph abstraction in favor of directly manipulating an existing standard (SVG). The rational is the same as D3's; while custom abstractions may be more efficient for specifying certain visualizations---a bar chart or a line chart---they ultimately limit expressiveness. See section one of the D3 paper (PDF link).

Differences from D3

D3 is a language for constructing bespoke visualizations on web, not generating plots (sayith Bostock: "D3 is not a charting library!"). While many people use D3 to create charts,

C2 is focused only on the construction of static statistical graphics; it does not support transitions or animations. This is because

1) It's easier to implement a static system. 2) Good information design is good graphic design: we want output suitable for print, as well as the web. 3) Interactivity in information software is very hard to do well anyway. We agree with Bret Victor's take in Magic Ink.

Theoretical Foundations

(would be nice to integrate...)

Hadly Wickham's thesis

Hadley Wickam's layered grammar (used by the R ggplot2 package): A dataset and set of mappings from variables to aesthetics. One or more layers, each consisting of geometric objects, statistical transformations, position adjustments, datasets & aesthetic mappings.

Leland Wilkinson's Grammar of Graphics

References Interface differences: operation vs. expression