4  Composable Plotting

Napkinsketch is a composable plotting library inspired by Wilkinson’s Grammar of Graphics and Julia’s AlgebraOfGraphics.jl. Its operators are shaped by Clojure idioms β€” threading, merge, plain maps β€” rather than a custom DSL.

The API has two verbs and one noun:

Phase Verb What it does Example
What to plot sk/view Describe your views of the data (sk/view data :x :y)
How to plot sk/lay-* Choose a drawing method sk/lay-point, sk/lay-histogram

The noun is a sketch β€” the composable result of both verbs. Sketches auto-render in notebooks, compose through threading (->), and are plain inspectable data.

(ns napkinsketch-book.composability
  (:require
   ;; Shared datasets
   [napkinsketch-book.datasets :as data]
   ;; Kindly β€” notebook rendering protocol
   [scicloj.kindly.v4.kind :as kind]
   ;; Napkinsketch β€” composable plotting
   [scicloj.napkinsketch.api :as sk]))

One Layer

The simplest sketch: data, columns, and a chart type.

(def scatter
  (-> data/iris
      (sk/lay-point :sepal_length :sepal_width)))

sk/lay-point returns a sketch β€” a lightweight wrapper that auto-renders in notebooks. But it is also plain Clojure data. Inside, a sketch wraps one or more views (maps describing what to plot) together with options:

(sk/sketch? scatter)
true
(kind/pprint (sk/views-of scatter))
[{:data
  https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv [150 5]:

| :sepal_length | :sepal_width | :petal_length | :petal_width |  :species |
|--------------:|-------------:|--------------:|-------------:|-----------|
|           5.1 |          3.5 |           1.4 |          0.2 |    setosa |
|           4.9 |          3.0 |           1.4 |          0.2 |    setosa |
|           4.7 |          3.2 |           1.3 |          0.2 |    setosa |
|           4.6 |          3.1 |           1.5 |          0.2 |    setosa |
|           5.0 |          3.6 |           1.4 |          0.2 |    setosa |
|           5.4 |          3.9 |           1.7 |          0.4 |    setosa |
|           4.6 |          3.4 |           1.4 |          0.3 |    setosa |
|           5.0 |          3.4 |           1.5 |          0.2 |    setosa |
|           4.4 |          2.9 |           1.4 |          0.2 |    setosa |
|           4.9 |          3.1 |           1.5 |          0.1 |    setosa |
|           ... |          ... |           ... |          ... |       ... |
|           6.9 |          3.1 |           5.4 |          2.1 | virginica |
|           6.7 |          3.1 |           5.6 |          2.4 | virginica |
|           6.9 |          3.1 |           5.1 |          2.3 | virginica |
|           5.8 |          2.7 |           5.1 |          1.9 | virginica |
|           6.8 |          3.2 |           5.9 |          2.3 | virginica |
|           6.7 |          3.3 |           5.7 |          2.5 | virginica |
|           6.7 |          3.0 |           5.2 |          2.3 | virginica |
|           6.3 |          2.5 |           5.0 |          1.9 | virginica |
|           6.5 |          3.0 |           5.2 |          2.0 | virginica |
|           6.2 |          3.4 |           5.4 |          2.3 | virginica |
|           5.9 |          3.0 |           5.1 |          1.8 | virginica |
,
  :x :sepal_length,
  :y :sepal_width,
  :mark :point,
  :stat :identity,
  :accepts [:size :shape :jitter :text :nudge-x :nudge-y],
  :doc "Scatter β€” individual data points.",
  :__base
  {:data
   https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv [150 5]:

| :sepal_length | :sepal_width | :petal_length | :petal_width |  :species |
|--------------:|-------------:|--------------:|-------------:|-----------|
|           5.1 |          3.5 |           1.4 |          0.2 |    setosa |
|           4.9 |          3.0 |           1.4 |          0.2 |    setosa |
|           4.7 |          3.2 |           1.3 |          0.2 |    setosa |
|           4.6 |          3.1 |           1.5 |          0.2 |    setosa |
|           5.0 |          3.6 |           1.4 |          0.2 |    setosa |
|           5.4 |          3.9 |           1.7 |          0.4 |    setosa |
|           4.6 |          3.4 |           1.4 |          0.3 |    setosa |
|           5.0 |          3.4 |           1.5 |          0.2 |    setosa |
|           4.4 |          2.9 |           1.4 |          0.2 |    setosa |
|           4.9 |          3.1 |           1.5 |          0.1 |    setosa |
|           ... |          ... |           ... |          ... |       ... |
|           6.9 |          3.1 |           5.4 |          2.1 | virginica |
|           6.7 |          3.1 |           5.6 |          2.4 | virginica |
|           6.9 |          3.1 |           5.1 |          2.3 | virginica |
|           5.8 |          2.7 |           5.1 |          1.9 | virginica |
|           6.8 |          3.2 |           5.9 |          2.3 | virginica |
|           6.7 |          3.3 |           5.7 |          2.5 | virginica |
|           6.7 |          3.0 |           5.2 |          2.3 | virginica |
|           6.3 |          2.5 |           5.0 |          1.9 | virginica |
|           6.5 |          3.0 |           5.2 |          2.0 | virginica |
|           6.2 |          3.4 |           5.4 |          2.3 | virginica |
|           5.9 |          3.0 |           5.1 |          1.8 | virginica |
,
   :x :sepal_length,
   :y :sepal_width}}]

Each view is a map with :data, :x, :y, and :mark. No rendering has happened yet β€” the sketch is just a description. When displayed, the notebook renders it:

scatter
sepal widthsepal length4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

Adding Color

Pass :color to group the data. Each group gets its own color and a legend appears automatically.

(-> data/iris
    (sk/lay-point :sepal_length :sepal_width {:color :species}))
sepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

Multiple Layers

sk/view describes what to plot β€” which columns and aesthetics to use. Then sk/lay-* functions add how to draw them. Multiple layers share the same views.

(def scatter-with-regression
  (-> data/iris
      (sk/view :sepal_length :sepal_width {:color :species})
      sk/lay-point
      sk/lay-lm))

The sketch now has two methods merged into its views β€” scatter points and regression lines share the same columns and color:

(kind/pprint (sk/views-of scatter-with-regression))
[{:data
  https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv [150 5]:

| :sepal_length | :sepal_width | :petal_length | :petal_width |  :species |
|--------------:|-------------:|--------------:|-------------:|-----------|
|           5.1 |          3.5 |           1.4 |          0.2 |    setosa |
|           4.9 |          3.0 |           1.4 |          0.2 |    setosa |
|           4.7 |          3.2 |           1.3 |          0.2 |    setosa |
|           4.6 |          3.1 |           1.5 |          0.2 |    setosa |
|           5.0 |          3.6 |           1.4 |          0.2 |    setosa |
|           5.4 |          3.9 |           1.7 |          0.4 |    setosa |
|           4.6 |          3.4 |           1.4 |          0.3 |    setosa |
|           5.0 |          3.4 |           1.5 |          0.2 |    setosa |
|           4.4 |          2.9 |           1.4 |          0.2 |    setosa |
|           4.9 |          3.1 |           1.5 |          0.1 |    setosa |
|           ... |          ... |           ... |          ... |       ... |
|           6.9 |          3.1 |           5.4 |          2.1 | virginica |
|           6.7 |          3.1 |           5.6 |          2.4 | virginica |
|           6.9 |          3.1 |           5.1 |          2.3 | virginica |
|           5.8 |          2.7 |           5.1 |          1.9 | virginica |
|           6.8 |          3.2 |           5.9 |          2.3 | virginica |
|           6.7 |          3.3 |           5.7 |          2.5 | virginica |
|           6.7 |          3.0 |           5.2 |          2.3 | virginica |
|           6.3 |          2.5 |           5.0 |          1.9 | virginica |
|           6.5 |          3.0 |           5.2 |          2.0 | virginica |
|           6.2 |          3.4 |           5.4 |          2.3 | virginica |
|           5.9 |          3.0 |           5.1 |          1.8 | virginica |
,
  :x :sepal_length,
  :y :sepal_width,
  :color :species,
  :mark :point,
  :stat :identity,
  :accepts [:size :shape :jitter :text :nudge-x :nudge-y],
  :doc "Scatter β€” individual data points.",
  :__base
  {:data
   https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv [150 5]:

| :sepal_length | :sepal_width | :petal_length | :petal_width |  :species |
|--------------:|-------------:|--------------:|-------------:|-----------|
|           5.1 |          3.5 |           1.4 |          0.2 |    setosa |
|           4.9 |          3.0 |           1.4 |          0.2 |    setosa |
|           4.7 |          3.2 |           1.3 |          0.2 |    setosa |
|           4.6 |          3.1 |           1.5 |          0.2 |    setosa |
|           5.0 |          3.6 |           1.4 |          0.2 |    setosa |
|           5.4 |          3.9 |           1.7 |          0.4 |    setosa |
|           4.6 |          3.4 |           1.4 |          0.3 |    setosa |
|           5.0 |          3.4 |           1.5 |          0.2 |    setosa |
|           4.4 |          2.9 |           1.4 |          0.2 |    setosa |
|           4.9 |          3.1 |           1.5 |          0.1 |    setosa |
|           ... |          ... |           ... |          ... |       ... |
|           6.9 |          3.1 |           5.4 |          2.1 | virginica |
|           6.7 |          3.1 |           5.6 |          2.4 | virginica |
|           6.9 |          3.1 |           5.1 |          2.3 | virginica |
|           5.8 |          2.7 |           5.1 |          1.9 | virginica |
|           6.8 |          3.2 |           5.9 |          2.3 | virginica |
|           6.7 |          3.3 |           5.7 |          2.5 | virginica |
|           6.7 |          3.0 |           5.2 |          2.3 | virginica |
|           6.3 |          2.5 |           5.0 |          1.9 | virginica |
|           6.5 |          3.0 |           5.2 |          2.0 | virginica |
|           6.2 |          3.4 |           5.4 |          2.3 | virginica |
|           5.9 |          3.0 |           5.1 |          1.8 | virginica |
,
   :x :sepal_length,
   :y :sepal_width,
   :color :species}}
 {:data
  https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv [150 5]:

| :sepal_length | :sepal_width | :petal_length | :petal_width |  :species |
|--------------:|-------------:|--------------:|-------------:|-----------|
|           5.1 |          3.5 |           1.4 |          0.2 |    setosa |
|           4.9 |          3.0 |           1.4 |          0.2 |    setosa |
|           4.7 |          3.2 |           1.3 |          0.2 |    setosa |
|           4.6 |          3.1 |           1.5 |          0.2 |    setosa |
|           5.0 |          3.6 |           1.4 |          0.2 |    setosa |
|           5.4 |          3.9 |           1.7 |          0.4 |    setosa |
|           4.6 |          3.4 |           1.4 |          0.3 |    setosa |
|           5.0 |          3.4 |           1.5 |          0.2 |    setosa |
|           4.4 |          2.9 |           1.4 |          0.2 |    setosa |
|           4.9 |          3.1 |           1.5 |          0.1 |    setosa |
|           ... |          ... |           ... |          ... |       ... |
|           6.9 |          3.1 |           5.4 |          2.1 | virginica |
|           6.7 |          3.1 |           5.6 |          2.4 | virginica |
|           6.9 |          3.1 |           5.1 |          2.3 | virginica |
|           5.8 |          2.7 |           5.1 |          1.9 | virginica |
|           6.8 |          3.2 |           5.9 |          2.3 | virginica |
|           6.7 |          3.3 |           5.7 |          2.5 | virginica |
|           6.7 |          3.0 |           5.2 |          2.3 | virginica |
|           6.3 |          2.5 |           5.0 |          1.9 | virginica |
|           6.5 |          3.0 |           5.2 |          2.0 | virginica |
|           6.2 |          3.4 |           5.4 |          2.3 | virginica |
|           5.9 |          3.0 |           5.1 |          1.8 | virginica |
,
  :x :sepal_length,
  :y :sepal_width,
  :color :species,
  :mark :line,
  :stat :lm,
  :accepts [:se :size :nudge-x :nudge-y],
  :doc
  "Linear model (lm) β€” ordinary least squares (OLS) regression line.",
  :__base
  {:data
   https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv [150 5]:

| :sepal_length | :sepal_width | :petal_length | :petal_width |  :species |
|--------------:|-------------:|--------------:|-------------:|-----------|
|           5.1 |          3.5 |           1.4 |          0.2 |    setosa |
|           4.9 |          3.0 |           1.4 |          0.2 |    setosa |
|           4.7 |          3.2 |           1.3 |          0.2 |    setosa |
|           4.6 |          3.1 |           1.5 |          0.2 |    setosa |
|           5.0 |          3.6 |           1.4 |          0.2 |    setosa |
|           5.4 |          3.9 |           1.7 |          0.4 |    setosa |
|           4.6 |          3.4 |           1.4 |          0.3 |    setosa |
|           5.0 |          3.4 |           1.5 |          0.2 |    setosa |
|           4.4 |          2.9 |           1.4 |          0.2 |    setosa |
|           4.9 |          3.1 |           1.5 |          0.1 |    setosa |
|           ... |          ... |           ... |          ... |       ... |
|           6.9 |          3.1 |           5.4 |          2.1 | virginica |
|           6.7 |          3.1 |           5.6 |          2.4 | virginica |
|           6.9 |          3.1 |           5.1 |          2.3 | virginica |
|           5.8 |          2.7 |           5.1 |          1.9 | virginica |
|           6.8 |          3.2 |           5.9 |          2.3 | virginica |
|           6.7 |          3.3 |           5.7 |          2.5 | virginica |
|           6.7 |          3.0 |           5.2 |          2.3 | virginica |
|           6.3 |          2.5 |           5.0 |          1.9 | virginica |
|           6.5 |          3.0 |           5.2 |          2.0 | virginica |
|           6.2 |          3.4 |           5.4 |          2.3 | virginica |
|           5.9 |          3.0 |           5.1 |          1.8 | virginica |
,
   :x :sepal_length,
   :y :sepal_width,
   :color :species}}]

Both layers render together β€” each species gets its own fitted line:

scatter-with-regression
sepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

Inference

When you omit something, napkinsketch infers it from the data. The principle is simple:

resolved-value = (or your-explicit-choice (inferred-from-data))

sk/view without a sk/lay-* infers the drawing method from the column types. Two numerical columns produce a scatter:

(-> data/iris
    (sk/view :sepal_length :sepal_width))
sepal widthsepal length4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

A single numerical column produces a histogram:

(-> data/iris
    (sk/view :sepal_length))
sepal length4.55.05.56.06.57.07.58.00510152025

Multiple Views

sk/view accepts multiple column pairs. Each pair becomes a separate panel:

(def two-panels
  (-> data/iris
      (sk/view [[:sepal_length :sepal_width]
                [:petal_length :petal_width]])))

The sketch wraps two views β€” one per column pair:

(kind/pprint (sk/views-of two-panels))
[{:data
  https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv [150 5]:

| :sepal_length | :sepal_width | :petal_length | :petal_width |  :species |
|--------------:|-------------:|--------------:|-------------:|-----------|
|           5.1 |          3.5 |           1.4 |          0.2 |    setosa |
|           4.9 |          3.0 |           1.4 |          0.2 |    setosa |
|           4.7 |          3.2 |           1.3 |          0.2 |    setosa |
|           4.6 |          3.1 |           1.5 |          0.2 |    setosa |
|           5.0 |          3.6 |           1.4 |          0.2 |    setosa |
|           5.4 |          3.9 |           1.7 |          0.4 |    setosa |
|           4.6 |          3.4 |           1.4 |          0.3 |    setosa |
|           5.0 |          3.4 |           1.5 |          0.2 |    setosa |
|           4.4 |          2.9 |           1.4 |          0.2 |    setosa |
|           4.9 |          3.1 |           1.5 |          0.1 |    setosa |
|           ... |          ... |           ... |          ... |       ... |
|           6.9 |          3.1 |           5.4 |          2.1 | virginica |
|           6.7 |          3.1 |           5.6 |          2.4 | virginica |
|           6.9 |          3.1 |           5.1 |          2.3 | virginica |
|           5.8 |          2.7 |           5.1 |          1.9 | virginica |
|           6.8 |          3.2 |           5.9 |          2.3 | virginica |
|           6.7 |          3.3 |           5.7 |          2.5 | virginica |
|           6.7 |          3.0 |           5.2 |          2.3 | virginica |
|           6.3 |          2.5 |           5.0 |          1.9 | virginica |
|           6.5 |          3.0 |           5.2 |          2.0 | virginica |
|           6.2 |          3.4 |           5.4 |          2.3 | virginica |
|           5.9 |          3.0 |           5.1 |          1.8 | virginica |
,
  :x :sepal_length,
  :y :sepal_width}
 {:data
  https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv [150 5]:

| :sepal_length | :sepal_width | :petal_length | :petal_width |  :species |
|--------------:|-------------:|--------------:|-------------:|-----------|
|           5.1 |          3.5 |           1.4 |          0.2 |    setosa |
|           4.9 |          3.0 |           1.4 |          0.2 |    setosa |
|           4.7 |          3.2 |           1.3 |          0.2 |    setosa |
|           4.6 |          3.1 |           1.5 |          0.2 |    setosa |
|           5.0 |          3.6 |           1.4 |          0.2 |    setosa |
|           5.4 |          3.9 |           1.7 |          0.4 |    setosa |
|           4.6 |          3.4 |           1.4 |          0.3 |    setosa |
|           5.0 |          3.4 |           1.5 |          0.2 |    setosa |
|           4.4 |          2.9 |           1.4 |          0.2 |    setosa |
|           4.9 |          3.1 |           1.5 |          0.1 |    setosa |
|           ... |          ... |           ... |          ... |       ... |
|           6.9 |          3.1 |           5.4 |          2.1 | virginica |
|           6.7 |          3.1 |           5.6 |          2.4 | virginica |
|           6.9 |          3.1 |           5.1 |          2.3 | virginica |
|           5.8 |          2.7 |           5.1 |          1.9 | virginica |
|           6.8 |          3.2 |           5.9 |          2.3 | virginica |
|           6.7 |          3.3 |           5.7 |          2.5 | virginica |
|           6.7 |          3.0 |           5.2 |          2.3 | virginica |
|           6.3 |          2.5 |           5.0 |          1.9 | virginica |
|           6.5 |          3.0 |           5.2 |          2.0 | virginica |
|           6.2 |          3.4 |           5.4 |          2.3 | virginica |
|           5.9 |          3.0 |           5.1 |          1.8 | virginica |
,
  :x :petal_length,
  :y :petal_width}]
two-panels
2345sepal lengthpetal lengthsepal widthpetal width

The SPLOM

sk/cross generates all combinations of columns β€” the same idea as Wilkinson’s cross operator (Γ—). Passing the result to sk/view produces a scatter plot matrix:

(def cols [:sepal_length :sepal_width :petal_length])
(-> data/iris
    (sk/view (sk/cross cols cols) {:color :species}))
speciessetosaversicolorvirginica051015234682462345sepal lengthsepal widthpetal lengthsepal lengthsepal widthpetal length

Nine panels β€” one per column pair. On the diagonal (where x = y), inference produces histograms instead of scatters. The color grouping applies to all views.

Options and Faceting

sk/options adds titles and configuration. sk/facet splits the data into panels by a column. Everything threads together β€” each function takes a sketch and returns a sketch:

(-> data/iris
    (sk/view :sepal_length :sepal_width {:color :species})
    (sk/facet :species)
    sk/lay-point
    sk/lay-lm
    (sk/options {:title "Iris by Species"}))
Iris by Speciessepal widthsepal lengthspeciessetosaversicolorvirginica682.02.53.03.54.04.56868setosaversicolorvirginica

Summary

Function Role Returns
sk/view Describe views (what to show) Sketch
sk/lay-* Add a method (how to show it) Sketch
sk/options Set title, labels, config Sketch
sk/facet Split into panels Sketch
sk/coord Set coordinate system Sketch
sk/scale Set axis scale type Sketch

Every function returns a sketch. Sketches compose through ->. They are plain data β€” vectors of maps you can inspect with sk/views-of and transform with ordinary Clojure functions.

The Core Concepts chapter covers each concept in detail.

What’s Next

source: notebooks/napkinsketch_book/composability.clj