16 Configuration
Napkinsketch has three kinds of options:
Layer options control individual layers β aesthetics like
:color, statistical parameters like:bandwidth. See the Methods chapter.Plot options set per-plot text β
:title,:subtitle,:caption, axis labels.Configuration controls everything else β dimensions, theme, palette, color scale, and more β via a layered precedence chain.
This chapter covers configuration and plot options.
(ns napkinsketch-book.configuration
(:require
;; Shared datasets for these docs
[napkinsketch-book.datasets :as data]
;; Kindly β notebook rendering protocol
[scicloj.kindly.v4.kind :as kind]
;; Napkinsketch β composable plotting
[scicloj.napkinsketch.api :as sk]))We use the iris dataset throughout.
We define base-plot as a function because sketches render at display time β calling the function produces a fresh specification that picks up the current configuration.
(defn base-plot
[] (-> data/iris
(sk/lay-point :sepal_length :sepal_width {:color :species})))Inspecting the Current Configuration
sk/config returns the resolved configuration as a plain map. It merges all active configuration layers into one map.
(sk/config){:margin-multi 30,
:validate true,
:point-stroke "none",
:title-offset 18,
:tick-spacing-y 40,
:panel-size 200,
:label-offset 18,
:label-font-size 13,
:default-color "#333",
:width 600,
:point-stroke-width 0,
:annotation-dash [4 3],
:legend-width 100,
:theme {:bg "#E8E8E8", :grid "#F5F5F5", :font-size 11},
:bin-method :sturges,
:domain-padding 0.05,
:strip-height 16,
:point-opacity 0.75,
:line-width 2.5,
:grid-stroke-width 0.6,
:tick-spacing-x 60,
:strip-font-size 10,
:title-font-size 15,
:band-opacity 0.15,
:bar-opacity 0.85,
:annotation-stroke "#333",
:height 400,
:margin 30,
:point-radius 3.0}Configuration Keys
Each key, its default value, and a description.
(def category-order
["Layout" "Theme" "Typography" "Points" "Bars & Lines"
"Annotations" "Ticks" "Statistics" "Labels" "Behavior"
"Color" "Interaction" "Output"])(kind/table
{:column-names ["Key" "Default" "Category" "Description"]
:row-maps
(let [cfg (sk/config)]
(->> sk/config-key-docs
(sort-by (fn [[k [cat]]]
[(.indexOf ^java.util.List category-order cat) (name k)]))
(mapv (fn [[k [cat desc]]]
{"Key" (kind/code (pr-str k))
"Default" (kind/code (pr-str (get cfg k)))
"Category" cat
"Description" desc}))))})| Key | Default | Category | Description |
|---|---|---|---|
|
|
Layout | Plot height in pixels |
|
|
Layout | Legend placement β :right, :bottom, :top, or :none |
|
|
Layout | Width reserved for the legend column |
|
|
Layout | Margin around single-panel plots (pixels) |
|
|
Layout | Margin around multi-panel plots (pixels) |
|
|
Layout | Default panel size for faceted/multi-variable grids |
|
|
Layout | Plot width in pixels |
|
|
Theme | Nested map {:bg :grid :font-size} β visual identity |
|
|
Typography | Font size for axis labels |
|
|
Typography | Font size for facet strip labels |
|
|
Typography | Font size for the plot title |
|
|
Points | Default point opacity (0.0β1.0) |
|
|
Points | Default point radius |
|
|
Points | Point border stroke color ("none" to disable) |
|
|
Points | Point border stroke width |
|
|
Bars & Lines | Default bar fill opacity |
|
|
Bars & Lines | Grid line stroke width |
|
|
Bars & Lines | Default line stroke width |
|
|
Annotations | Dash pattern [dash gap] for annotation lines |
|
|
Annotations | Stroke color for annotation marks |
|
|
Annotations | Opacity for confidence bands |
|
|
Ticks | Minimum pixel spacing between x-axis ticks |
|
|
Ticks | Minimum pixel spacing between y-axis ticks |
|
|
Statistics | Histogram bin count method (:sturges, :sqrt, :rice, :fd) |
|
|
Statistics | Fractional padding added to numeric domains |
|
|
Labels | Pixel offset for axis labels from the axis |
|
|
Labels | Height of facet strip label bars |
|
|
Labels | Pixel offset for the title from the top |
|
|
Behavior | Fallback color when no color mapping is set |
|
|
Behavior | When true, validate plans against Malli schema |
|
|
Color | Center value for diverging color scales |
|
|
Color | Continuous color scale β :sequential, :diverging, or keyword |
|
|
Color | Categorical palette β keyword, vector, or map |
|
|
Interaction | Enable drag-to-select brush (truthy value) |
|
|
Interaction | Enable hover tooltips (truthy value) |
|
|
Output | Render format β :svg (default) |
Plot Options
These options are accepted by sk/options, sk/plan, and sk/plot but are inherently per-plot β text content or nested config override.
(kind/table
{:column-names ["Key" "Category" "Description"]
:row-maps
(->> sk/plot-option-docs
(sort-by (fn [[k [cat]]] [cat (name k)]))
(mapv (fn [[k [cat desc]]]
{"Key" (kind/code (pr-str k))
"Category" cat
"Description" desc})))})| Key | Category | Description |
|---|---|---|
|
Config | Nested config map merged into resolved config |
|
Content | Plot caption string (bottom) |
|
Content | Plot subtitle string |
|
Content | Plot title string |
|
Content | X-axis label (overrides inferred) |
|
Content | Y-axis label (overrides inferred) |
Using Plot Options
Pass an options map to sk/options to override any setting for a single plot. Plot options have the highest priority β they override everything else.
Custom dimensions β the defaults are:
(select-keys (sk/config) [:width :height]){:width 600, :height 400}(-> (base-plot)
(sk/options {:width 900 :height 250}))Theme deep-merge β only the specified keys change. Here we set a white background; :grid and :font-size keep their defaults:
(-> (base-plot)
(sk/options {:theme {:bg "#FFFFFF"}}))Named palette:
(-> (base-plot)
(sk/options {:palette :dark2}))Global Overrides with set-config!
sk/set-config! sets overrides that persist across calls β useful when you want a consistent style for an entire session or notebook.
Set a global width override and render:
(sk/set-config! {:width 800}){:width 800}(select-keys (sk/config) [:width :height]){:width 800, :height 400}(-> (base-plot))Reset to library defaults by passing nil:
(sk/set-config! nil)nil(select-keys (sk/config) [:width :height]){:width 600, :height 400}Thread-Local Overrides with with-config
sk/with-config is a macro that binds configuration overrides for the duration of its body, then automatically reverts. This is ideal for one-off experiments or when different sections of a notebook need different settings.
A dark theme, scoped to this block:
(sk/with-config {:theme {:bg "#1a1a2e" :grid "#16213e" :font-size 8}}
(-> (base-plot)
(sk/options {:title "Dark Theme via with-config"})))Partial theme override β only :bg changes; :grid and :font-size are deep-merged from the defaults:
(sk/with-config {:theme {:bg "#F5F5DC"}}
(-> (base-plot)
(sk/options {:title "Partial Theme Override"})))Outside the body, the default theme is back:
(select-keys (sk/config) [:width :height]){:width 600, :height 400}The Precedence Chain
When multiple configuration layers are active, the highest-priority layer wins for each key. The chain from highest to lowest:
plot options > with-config > set-config! > napkinsketch.edn > library defaults
Letβs demonstrate all three programmatic levels at once, using a different key at each level so we can see each one win.
Before overriding, the library default for point-radius is:
(:point-radius (sk/config))3.0Now set a global override for width, height, and point-radius:
(sk/set-config! {:width 800 :height 350 :point-radius 5.0}){:width 800, :height 350, :point-radius 5.0}Layer a thread-local override on top for width and height (but not point-radius):
(def precedence-result
(sk/with-config {:width 1200 :height 500}
;; Pass plot options for width only:
(let [plan (sk/plan (base-plot) {:width 900})]
{:plan-width (:width plan)
:plan-height (:height plan)})))precedence-result{:plan-width 900, :plan-height 500}We can verify point-radius too β only set-config! touched it, so it wins over the library default (2.5):
(def precedence-point-radius
(sk/with-config {:width 1200 :height 500}
(:point-radius (sk/config))))precedence-point-radius5.0The rendered plot reflects the same precedence:
(def precedence-plot
(sk/with-config {:width 1200 :height 500}
(-> (base-plot)
(sk/options {:width 900}))))precedence-plotClean up the global override.
(sk/set-config! nil)nil(select-keys (sk/config) [:width :height :point-radius]){:width 600, :height 400, :point-radius 3.0}To summarize what happened:
| Key | Library default | set-config! | with-config | plot options | Winner |
|---|---|---|---|---|---|
:width |
600 | 800 | 1200 | 900 | 900 (plot options) |
:height |
400 | 350 | 500 | β | 500 (with-config) |
:point-radius |
2.5 | 5.0 | β | β | 5.0 (set-config!) |
Project-Level Defaults with napkinsketch.edn
For team-wide consistency, create a napkinsketch.edn file in your project root (or anywhere on the classpath). It is read automatically with a 1-second cache.
Example napkinsketch.edn:
{:width 800
:height 500
:theme {:bg "#FFFFFF" :grid "#F0F0F0" :font-size 10}
:palette :tableau10
:point-radius 3}This layer sits between library defaults and set-config! in the precedence chain β it overrides defaults but is overridden by any programmatic configuration.
Theme Customization
The :theme key is a nested map with three entries:
:bgβ panel background color (hex string):gridβ grid line color (hex string):font-sizeβ tick label font size (number)
(count (:theme (sk/config)))3All configuration merging uses deep-merge β nested maps like :theme are merged recursively at every level (sk/options, sk/with-config, sk/set-config!, and napkinsketch.edn). You only need to specify the keys you want to change.
Override only :bg, keep default :grid and :font-size:
(-> (base-plot)
(sk/options {:theme {:bg "#F5F5DC"}})
sk/plot)Override all three for a dark theme:
(-> (base-plot)
(sk/options {:title "Full Dark Theme"
:theme {:bg "#2d2d2d" :grid "#444444" :font-size 10}})
sk/plot)Comparing Two Themes Side by Side
sk/arrange composes independent plots in a CSS grid. Each plot can have its own theme.
(sk/arrange
[(-> (base-plot)
(sk/options {:title "Light"
:theme {:bg "#FFFFFF" :grid "#EEEEEE" :font-size 8}
:width 350 :height 250}))
(-> (base-plot)
(sk/options {:title "Dark"
:theme {:bg "#2d2d2d" :grid "#444444" :font-size 8}
:width 350 :height 250}))])Palette Configuration
The :palette key controls the color cycle for categorical color mappings. It accepts:
a keyword β any palette name from the clojure2d color library (hundreds available: ColorBrewer, Wes Anderson, thi.ng, paletteer, etc.)
a vector of hex strings:
["#E74C3C" "#3498DB" "#2ECC71"]a map of
{category-value "#hex"}for explicit assignment
Common palette names: :set1, :set2, :dark2, :tableau10, :category10, :pastel1, :accent, :paired. Use (clojure2d.color/find-palette #"pattern") to discover more.
Palette works at every configuration level.
Named palette via plot options:
(-> (base-plot)
(sk/options {:palette :tableau10}))Custom vector palette:
(-> (base-plot)
(sk/options {:palette ["#E74C3C" "#3498DB" "#2ECC71"]}))Explicit map palette:
(-> (base-plot)
(sk/options {:palette {"setosa" "#FF6B6B"
"versicolor" "#4ECDC4"
"virginica" "#45B7D1"}}))Global palette via set-config!:
(sk/set-config! {:palette :pastel1}){:palette :pastel1}(-> (base-plot))(sk/set-config! nil)nilThread-local palette via with-config:
(sk/with-config {:palette :accent}
(-> (base-plot)))Color Scale Configuration
When a numeric column is mapped to :color, Napkinsketch uses a continuous gradient (dark-to-light blue by default). The :color-scale option controls which gradient is used.
Default continuous color (dark blue to light blue):
(-> {:x (range 50) :y (range 50) :c (range 50)}
(sk/lay-point :x :y {:color :c}))Color scale override via plot options β inferno gradient:
(-> {:x (range 50) :y (range 50) :c (range 50)}
(sk/lay-point :x :y {:color :c})
(sk/options {:color-scale :inferno}))Thread-local color scale via with-config:
(sk/with-config {:color-scale :plasma}
(-> {:x (range 50) :y (range 50) :c (range 50)}
(sk/lay-point :x :y {:color :c})))The plan records :color-scale in its legend. The renderer uses the pre-computed gradient stops, or resolves a fresh gradient if the render-time configuration specifies a different color scale.
(-> {:x (range 50) :y (range 50) :c (range 50)}
(sk/lay-point :x :y {:color :c})
(sk/plan {:color-scale :inferno})
:legend
(select-keys [:color-scale :type])){:color-scale :inferno, :type :continuous}Validation Control
By default, sk/plan validates the output against a Malli schema and throws if the plan is malformed. This is controlled by the :validate key.
Two helper functions let you inspect plans manually:
sk/valid-plan?β returns true or falsesk/explain-planβ returns nil if valid, or a Malli explanation map
Default behavior (validate = true) β a valid plan passes silently:
(sk/plan (base-plot)){:panels
[{:coord :cartesian,
:y-domain [1.88 4.5200000000000005],
:x-scale {:type :linear},
:x-domain [4.12 8.08],
:x-ticks
{:values [4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0],
:labels ["4.5" "5.0" "5.5" "6.0" "6.5" "7.0" "7.5" "8.0"],
:categorical? false},
:col 0,
:layers
[{:mark :point,
:style {:opacity 0.75, :radius 3.0},
:groups
[{:color
[0.8941176470588236
0.10196078431372549
0.10980392156862745
1.0],
:xs #tech.v3.dataset.column<float64>[50]
:sepal_length
[5.100, 4.900, 4.700, 4.600, 5.000, 5.400, 4.600, 5.000, 4.400, 4.900, 5.400, 4.800, 4.800, 4.300, 5.800, 5.700, 5.400, 5.100, 5.700, 5.100...],
:ys #tech.v3.dataset.column<float64>[50]
:sepal_width
[3.500, 3.000, 3.200, 3.100, 3.600, 3.900, 3.400, 3.400, 2.900, 3.100, 3.700, 3.400, 3.000, 3.000, 4.000, 4.400, 3.900, 3.500, 3.800, 3.800...],
:label "setosa",
:row-indices #tech.v3.dataset.column<int64>[50]
:__row-idx
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19...]}
{:color
[0.21568627450980393
0.49411764705882355
0.7215686274509804
1.0],
:xs #tech.v3.dataset.column<float64>[50]
:sepal_length
[7.000, 6.400, 6.900, 5.500, 6.500, 5.700, 6.300, 4.900, 6.600, 5.200, 5.000, 5.900, 6.000, 6.100, 5.600, 6.700, 5.600, 5.800, 6.200, 5.600...],
:ys #tech.v3.dataset.column<float64>[50]
:sepal_width
[3.200, 3.200, 3.100, 2.300, 2.800, 2.800, 3.300, 2.400, 2.900, 2.700, 2.000, 3.000, 2.200, 2.900, 2.900, 3.100, 3.000, 2.700, 2.200, 2.500...],
:label "versicolor",
:row-indices #tech.v3.dataset.column<int64>[50]
:__row-idx
[50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69...]}
{:color
[0.30196078431372547 0.6862745098039216 0.2901960784313726 1.0],
:xs #tech.v3.dataset.column<float64>[50]
:sepal_length
[6.300, 5.800, 7.100, 6.300, 6.500, 7.600, 4.900, 7.300, 6.700, 7.200, 6.500, 6.400, 6.800, 5.700, 5.800, 6.400, 6.500, 7.700, 7.700, 6.000...],
:ys #tech.v3.dataset.column<float64>[50]
:sepal_width
[3.300, 2.700, 3.000, 2.900, 3.000, 3.000, 2.500, 2.900, 2.500, 3.600, 3.200, 2.700, 3.000, 2.500, 2.800, 3.200, 3.000, 3.800, 2.600, 2.200...],
:label "virginica",
:row-indices #tech.v3.dataset.column<int64>[50]
:__row-idx
[100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119...]}],
:y-domain [2.0 4.4],
:x-domain [4.3 7.9]}],
:y-scale {:type :linear},
:y-ticks
{:values [2.0 2.5 3.0 3.5 4.0 4.5],
:labels ["2.0" "2.5" "3.0" "3.5" "4.0" "4.5"],
:categorical? false},
:row 0}],
:width 600,
:height 400,
:caption nil,
:total-width 722.5,
:legend-position :right,
:layout-type :single,
:layout
{:subtitle-pad 0,
:legend-w 100,
:caption-pad 0,
:y-label-pad 22.5,
:legend-h 0,
:title-pad 0,
:strip-h 0,
:x-label-pad 18,
:strip-w 0},
:grid {:rows 1, :cols 1},
:legend
{:title :species,
:entries
[{:label "setosa",
:color
[0.8941176470588236 0.10196078431372549 0.10980392156862745 1.0]}
{:label "versicolor",
:color
[0.21568627450980393 0.49411764705882355 0.7215686274509804 1.0]}
{:label "virginica",
:color
[0.30196078431372547 0.6862745098039216 0.2901960784313726 1.0]}]},
:panel-height 400.0,
:title nil,
:y-label "sepal width",
:alpha-legend nil,
:x-label "sepal length",
:subtitle nil,
:panel-width 600.0,
:size-legend nil,
:total-height 418.0,
:margin 30}The rendered plot works normally:
(-> (base-plot))What Validation Catches
To see what happens when a plan is malformed, we can build one with validation disabled, then corrupt it. First, create a valid plan and confirm it passes:
(def good-plan (sk/plan (base-plot) {:validate false}))(sk/valid-plan? good-plan)trueNow corrupt the :width to a string β this violates the schema, which requires a positive integer:
(def bad-plan (assoc good-plan :width "not-a-number"))(sk/valid-plan? bad-plan)falsesk/explain-plan pinpoints the problem. The :errors key in the returned map shows exactly which path failed and why:
(-> (sk/explain-plan bad-plan)
:errors
first
(select-keys [:path :in :value])){:path [:width], :in [:width], :value "not-a-number"}With validation enabled (the default), sk/plan would throw an exception for such a malformed plan. We can verify this by catching the exception:
(try
(let [plan (sk/plan (base-plot) {:validate false})
bad (assoc plan :width "not-a-number")]
(when-let [explanation (sk/explain-plan bad)]
(throw (ex-info "Plan does not conform to schema"
{:explanation explanation})))
:no-error)
(catch Exception e
{:caught true
:message (.getMessage e)})){:caught true, :message "Plan does not conform to schema"}Disabling Validation
Disable validation with :validate false:
(sk/plan (base-plot) {:validate false}){:panels
[{:coord :cartesian,
:y-domain [1.88 4.5200000000000005],
:x-scale {:type :linear},
:x-domain [4.12 8.08],
:x-ticks
{:values [4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0],
:labels ["4.5" "5.0" "5.5" "6.0" "6.5" "7.0" "7.5" "8.0"],
:categorical? false},
:col 0,
:layers
[{:mark :point,
:style {:opacity 0.75, :radius 3.0},
:groups
[{:color
[0.8941176470588236
0.10196078431372549
0.10980392156862745
1.0],
:xs #tech.v3.dataset.column<float64>[50]
:sepal_length
[5.100, 4.900, 4.700, 4.600, 5.000, 5.400, 4.600, 5.000, 4.400, 4.900, 5.400, 4.800, 4.800, 4.300, 5.800, 5.700, 5.400, 5.100, 5.700, 5.100...],
:ys #tech.v3.dataset.column<float64>[50]
:sepal_width
[3.500, 3.000, 3.200, 3.100, 3.600, 3.900, 3.400, 3.400, 2.900, 3.100, 3.700, 3.400, 3.000, 3.000, 4.000, 4.400, 3.900, 3.500, 3.800, 3.800...],
:label "setosa",
:row-indices #tech.v3.dataset.column<int64>[50]
:__row-idx
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19...]}
{:color
[0.21568627450980393
0.49411764705882355
0.7215686274509804
1.0],
:xs #tech.v3.dataset.column<float64>[50]
:sepal_length
[7.000, 6.400, 6.900, 5.500, 6.500, 5.700, 6.300, 4.900, 6.600, 5.200, 5.000, 5.900, 6.000, 6.100, 5.600, 6.700, 5.600, 5.800, 6.200, 5.600...],
:ys #tech.v3.dataset.column<float64>[50]
:sepal_width
[3.200, 3.200, 3.100, 2.300, 2.800, 2.800, 3.300, 2.400, 2.900, 2.700, 2.000, 3.000, 2.200, 2.900, 2.900, 3.100, 3.000, 2.700, 2.200, 2.500...],
:label "versicolor",
:row-indices #tech.v3.dataset.column<int64>[50]
:__row-idx
[50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69...]}
{:color
[0.30196078431372547 0.6862745098039216 0.2901960784313726 1.0],
:xs #tech.v3.dataset.column<float64>[50]
:sepal_length
[6.300, 5.800, 7.100, 6.300, 6.500, 7.600, 4.900, 7.300, 6.700, 7.200, 6.500, 6.400, 6.800, 5.700, 5.800, 6.400, 6.500, 7.700, 7.700, 6.000...],
:ys #tech.v3.dataset.column<float64>[50]
:sepal_width
[3.300, 2.700, 3.000, 2.900, 3.000, 3.000, 2.500, 2.900, 2.500, 3.600, 3.200, 2.700, 3.000, 2.500, 2.800, 3.200, 3.000, 3.800, 2.600, 2.200...],
:label "virginica",
:row-indices #tech.v3.dataset.column<int64>[50]
:__row-idx
[100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119...]}],
:y-domain [2.0 4.4],
:x-domain [4.3 7.9]}],
:y-scale {:type :linear},
:y-ticks
{:values [2.0 2.5 3.0 3.5 4.0 4.5],
:labels ["2.0" "2.5" "3.0" "3.5" "4.0" "4.5"],
:categorical? false},
:row 0}],
:width 600,
:height 400,
:caption nil,
:total-width 722.5,
:legend-position :right,
:layout-type :single,
:layout
{:subtitle-pad 0,
:legend-w 100,
:caption-pad 0,
:y-label-pad 22.5,
:legend-h 0,
:title-pad 0,
:strip-h 0,
:x-label-pad 18,
:strip-w 0},
:grid {:rows 1, :cols 1},
:legend
{:title :species,
:entries
[{:label "setosa",
:color
[0.8941176470588236 0.10196078431372549 0.10980392156862745 1.0]}
{:label "versicolor",
:color
[0.21568627450980393 0.49411764705882355 0.7215686274509804 1.0]}
{:label "virginica",
:color
[0.30196078431372547 0.6862745098039216 0.2901960784313726 1.0]}]},
:panel-height 400.0,
:title nil,
:y-label "sepal width",
:alpha-legend nil,
:x-label "sepal length",
:subtitle nil,
:panel-width 600.0,
:size-legend nil,
:total-height 418.0,
:margin 30}You can also disable validation globally for a debugging session:
(sk/set-config! {:validate false})
;; ... work freely ...
(sk/set-config! nil) ;; re-enableSummary
| Mechanism | Scope | Persistence | Example |
|---|---|---|---|
| plot options | single call | none | (sk/options {...}) or (sk/plot views {...}) |
with-config |
lexical body | until body exits | (sk/with-config {:width 800} ...) |
set-config! |
global | until reset | (sk/set-config! {:width 800}) |
napkinsketch.edn |
project | file on disk | {:width 800} in project root |
| library defaults | everywhere | built-in | resources/napkinsketch-defaults.edn |
Precedence: plot options > with-config > set-config! > napkinsketch.edn > library defaults.
Use sk/config at any time to see the resolved configuration.
Whatβs Next
- Customization β annotations, color scales, tooltips, and brush selection
- Faceting β split plots into panels by category