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:
scatterAdding 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}))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-regressionInference
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))A single numerical column produces a histogram:
(-> data/iris
(sk/view :sepal_length))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-panelsThe 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}))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"}))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
- Core Concepts β data formats, marks, stats, color, grouping, coordinates
- Inference Rules β how napkinsketch chooses defaults
- Scatter Plots β chart type examples to explore