21  Exploring Plans

When Napkinsketch renders a plot, it builds an intermediate data structure called a plan before rendering anything. This notebook walks through the plan step by step, building intuition for the data model by looking at what sk/plan produces for different plots.

(ns napkinsketch-book.exploring-sketches
  (: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]
   ;; Method registry β€” lookup mark/stat/position by keyword
   [scicloj.napkinsketch.method :as method]))

A Minimal Scatter Plot

Let’s start with the simplest possible plot: 5 points, no color, no title.

(def tiny {:x [1 2 3 4 5]
           :y [2 4 1 5 3]})

Here is the rendered plot:

(-> tiny
    (sk/lay-point :x :y))
yx1.01.52.02.53.03.54.04.55.01.01.52.02.53.03.54.04.55.0

And here is the plan β€” the data structure that drives the rendering. We’ll use sk/plan with the same arguments:

(def tiny-pl (-> tiny
                 (sk/lay-point :x :y)
                 sk/plan))

What’s in a plan?

At the top level, a plan describes dimensions and layout. Here is the entire plan β€” a plain Clojure map:

tiny-pl
{:panels
 [{:coord :cartesian,
   :y-domain [0.8 5.2],
   :x-scale {:type :linear},
   :x-domain [0.8 5.2],
   :x-ticks
   {:values [1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0],
    :labels ["1.0" "1.5" "2.0" "2.5" "3.0" "3.5" "4.0" "4.5" "5.0"],
    :categorical? false},
   :col 0,
   :layers
   [{:mark :point,
     :style {:opacity 0.75, :radius 3.0},
     :groups
     [{:color [0.2 0.2 0.2 1.0], :xs #tech.v3.dataset.column<int64>[5]
:x
[1, 2, 3, 4, 5], :ys #tech.v3.dataset.column<int64>[5]
:y
[2, 4, 1, 5, 3], :row-indices #tech.v3.dataset.column<int64>[5]
:__row-idx
[0, 1, 2, 3, 4]}],
     :y-domain [1 5],
     :x-domain [1 5]}],
   :y-scale {:type :linear},
   :y-ticks
   {:values [1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0],
    :labels ["1.0" "1.5" "2.0" "2.5" "3.0" "3.5" "4.0" "4.5" "5.0"],
    :categorical? false},
   :row 0}],
 :width 600,
 :height 400,
 :caption nil,
 :total-width 622.5,
 :legend-position :right,
 :layout-type :single,
 :layout
 {:subtitle-pad 0,
  :legend-w 0,
  :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 nil,
 :panel-height 400.0,
 :title nil,
 :y-label "y",
 :alpha-legend nil,
 :x-label "x",
 :subtitle nil,
 :panel-width 600.0,
 :size-legend nil,
 :total-height 418.0,
 :margin 30}

Notice:

  • Dimensions are 600Γ—400 with a 25-pixel margin
  • Labels "x" and "y" are inferred from column names
  • No legend (we didn’t map a column to color)
  • One panel with :x-domain, :y-domain, ticks, and layers

The panel

The plan contains one or more panels. A simple plot has one panel; faceting and SPLOM (scatter plot matrix) produce multiple. Each panel holds its own data space:

(def tiny-panel (first (:panels tiny-pl)))
(keys tiny-panel)
(:coord
 :y-domain
 :x-scale
 :x-domain
 :x-ticks
 :col
 :layers
 :y-scale
 :y-ticks
 :row)

Domains β€” the numeric range of the data, with a small padding:

(:x-domain tiny-panel)
[0.8 5.2]
(:y-domain tiny-panel)
[0.8 5.2]

Scale specs β€” what kind of scale to use:

(:x-scale tiny-panel)
{:type :linear}

Ticks β€” pre-computed tick positions and their text labels:

(:x-ticks tiny-panel)
{:values [1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0],
 :labels ["1.0" "1.5" "2.0" "2.5" "3.0" "3.5" "4.0" "4.5" "5.0"],
 :categorical? false}

These are the actual numbers that will appear on the axis. They are in data space β€” not pixel positions.

The layer

Each method in the plot produces one layer. Our scatter has a single point layer:

(def tiny-layer (first (:layers tiny-panel)))
tiny-layer
{:mark :point,
 :style {:opacity 0.75, :radius 3.0},
 :groups
 [{:color [0.2 0.2 0.2 1.0], :xs #tech.v3.dataset.column<int64>[5]
:x
[1, 2, 3, 4, 5], :ys #tech.v3.dataset.column<int64>[5]
:y
[2, 4, 1, 5, 3], :row-indices #tech.v3.dataset.column<int64>[5]
:__row-idx
[0, 1, 2, 3, 4]}],
 :y-domain [1 5],
 :x-domain [1 5]}

The style gives rendering hints (opacity, radius) but the geometry is in the groups. Without a color mapping, there is one group:

(count (:groups tiny-layer))
1

The group contains the actual data β€” x/y coordinates in data space, plus a resolved RGBA color:

(first (:groups tiny-layer))
{:color [0.2 0.2 0.2 1.0], :xs #tech.v3.dataset.column<int64>[5]
:x
[1, 2, 3, 4, 5], :ys #tech.v3.dataset.column<int64>[5]
:y
[2, 4, 1, 5, 3], :row-indices #tech.v3.dataset.column<int64>[5]
:__row-idx
[0, 1, 2, 3, 4]}

These are the original data values β€” not pixel positions. The renderer maps them through scales to get pixel coordinates.

In other words, the plan describes geometry in data space.

Adding Color

When we map a column to color, the plan splits data into groups and adds a legend.

(-> 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
(def iris-pl (-> data/iris
                 (sk/lay-point :sepal_length :sepal_width {:color :species})
                 sk/plan))

Here is the full plan β€” notice the legend and three groups:

iris-pl
{: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}

Now we have three groups β€” one per species:

(def iris-layer (first (:layers (first (:panels iris-pl)))))
(count (:groups iris-layer))
3

Each group has its own resolved color and a subset of the data:

(mapv (fn [g]
        {:color (:color g)
         :n-points (count (:xs g))})
      (:groups iris-layer))
[{:color
  [0.8941176470588236 0.10196078431372549 0.10980392156862745 1.0],
  :n-points 50}
 {:color
  [0.21568627450980393 0.49411764705882355 0.7215686274509804 1.0],
  :n-points 50}
 {:color
  [0.30196078431372547 0.6862745098039216 0.2901960784313726 1.0],
  :n-points 50}]

The legend describes the color mapping:

(:legend iris-pl)
{: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]}]}

Colors are resolved to [r g b a] vectors β€” no symbolic references. The same color appears in both the layer groups and the legend entries.

Continuous Color

When :color maps to a numeric column, the plan stores per-point colors and a continuous gradient legend.

(def cont-pl (-> data/iris
                 (sk/lay-point :sepal_length :sepal_width {:color :petal_length})
                 sk/plan))
(:legend cont-pl)
{:title :petal_length,
 :type :continuous,
 :min 1.0,
 :max 6.9,
 :color-scale nil,
 :stops
 [{:t 0.0,
   :color
   [0.07450980392156863 0.16862745098039217 0.2627450980392157 1.0]}
  {:t 0.05263157894736842,
   :color
   [0.08833849329205366 0.19628482972136224 0.2998968008255934 1.0]}
  {:t 0.10526315789473684,
   :color
   [0.1021671826625387 0.22394220846233232 0.3370485036119711 1.0]}
  {:t 0.15789473684210525,
   :color
   [0.11599587203302374 0.2515995872033024 0.3742002063983488 1.0]}
  {:t 0.21052631578947367,
   :color
   [0.1298245614035088 0.27925696594427246 0.4113519091847265 1.0]}
  {:t 0.2631578947368421,
   :color
   [0.14365325077399382 0.30691434468524253 0.4485036119711042 1.0]}
  {:t 0.3157894736842105,
   :color
   [0.15748194014447883 0.33457172342621255 0.4856553147574819 1.0]}
  {:t 0.3684210526315789,
   :color
   [0.17131062951496387 0.3622291021671826 0.5228070175438597 1.0]}
  {:t 0.42105263157894735,
   :color
   [0.1851393188854489 0.3898864809081527 0.5599587203302373 1.0]}
  {:t 0.47368421052631576,
   :color
   [0.19896800825593394 0.41754385964912283 0.597110423116615 1.0]}
  {:t 0.5263157894736842,
   :color
   [0.21279669762641898 0.4452012383900929 0.6342621259029928 1.0]}
  {:t 0.5789473684210527,
   :color
   [0.22662538699690402 0.472858617131063 0.6714138286893705 1.0]}
  {:t 0.631578947368421,
   :color
   [0.24045407636738905 0.500515995872033 0.7085655314757482 1.0]}
  {:t 0.6842105263157895,
   :color
   [0.25428276573787406 0.5281733746130031 0.7457172342621259 1.0]}
  {:t 0.7368421052631579,
   :color
   [0.2681114551083591 0.5558307533539731 0.7828689370485036 1.0]}
  {:t 0.7894736842105263,
   :color
   [0.28194014447884413 0.5834881320949432 0.8200206398348814 1.0]}
  {:t 0.8421052631578947,
   :color
   [0.29576883384932917 0.6111455108359133 0.857172342621259 1.0]}
  {:t 0.8947368421052632,
   :color
   [0.3095975232198142 0.6388028895768834 0.8943240454076368 1.0]}
  {:t 0.9473684210526315,
   :color
   [0.3234262125902993 0.6664602683178534 0.9314757481940144 1.0]}
  {:t 1.0,
   :color
   [0.33725490196078434 0.6941176470588235 0.9686274509803922 1.0]}]}

The legend has pre-computed gradient stops β€” no functions:

(select-keys (:legend cont-pl) [:title :type :min :max :color-scale])
{:title :petal_length,
 :type :continuous,
 :min 1.0,
 :max 6.9,
 :color-scale nil}

Twenty evenly spaced stops store the gradient colors:

(count (:stops (:legend cont-pl)))
20

Histograms

A histogram computes bins from the data. The plan stores the bin edges and counts β€” still in data space.

(-> data/iris
    (sk/lay-histogram :sepal_length))
sepal length4.55.05.56.06.57.07.58.00510152025
(def hist-pl (-> data/iris
                 (sk/lay-histogram :sepal_length)
                 sk/plan))
hist-pl
{:panels
 [{:coord :cartesian,
   :y-domain [-1.4000000000000001 29.4],
   :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 :bar,
     :style {:opacity 0.85},
     :groups
     [{:color [0.2 0.2 0.2 1.0],
       :bars
       [{:lo 4.3, :hi 4.7, :count 9}
        {:lo 4.7, :hi 5.1, :count 23}
        {:lo 5.1, :hi 5.5, :count 20}
        {:lo 5.5, :hi 5.9, :count 28}
        {:lo 5.9, :hi 6.300000000000001, :count 28}
        {:lo 6.300000000000001, :hi 6.7, :count 14}
        {:lo 6.7, :hi 7.1, :count 16}
        {:lo 7.1, :hi 7.5, :count 6}
        {:lo 7.5, :hi 7.9, :count 6}]}],
     :y-domain [0 28],
     :x-domain [4.3 7.9]}],
   :y-scale {:type :linear},
   :y-ticks
   {:values [0.0 5.0 10.0 15.0 20.0 25.0],
    :labels ["0" "5" "10" "15" "20" "25"],
    :categorical? false},
   :row 0}],
 :width 600,
 :height 400,
 :caption nil,
 :total-width 600.0,
 :legend-position :right,
 :layout-type :single,
 :layout
 {:subtitle-pad 0,
  :legend-w 0,
  :caption-pad 0,
  :y-label-pad 0,
  :legend-h 0,
  :title-pad 0,
  :strip-h 0,
  :x-label-pad 18,
  :strip-w 0},
 :grid {:rows 1, :cols 1},
 :legend nil,
 :panel-height 400.0,
 :title nil,
 :y-label nil,
 :alpha-legend nil,
 :x-label "sepal length",
 :subtitle nil,
 :panel-width 600.0,
 :size-legend nil,
 :total-height 418.0,
 :margin 30}
(def hist-layer (first (:layers (first (:panels hist-pl)))))
(:mark hist-layer)
:bar

The geometry is in :bars β€” each bin has a lo edge, hi edge, and count:

(let [g (first (:groups hist-layer))]
  (:bars g))
[{:lo 4.3, :hi 4.7, :count 9}
 {:lo 4.7, :hi 5.1, :count 23}
 {:lo 5.1, :hi 5.5, :count 20}
 {:lo 5.5, :hi 5.9, :count 28}
 {:lo 5.9, :hi 6.300000000000001, :count 28}
 {:lo 6.300000000000001, :hi 6.7, :count 14}
 {:lo 6.7, :hi 7.1, :count 16}
 {:lo 7.1, :hi 7.5, :count 6}
 {:lo 7.5, :hi 7.9, :count 6}]

The renderer will draw a rectangle from (lo, 0) to (hi, count) in data space, then map through scales to pixels.

Categorical Bars

A bar chart counts occurrences of each category. The plan records the categories and counts per group.

(-> data/penguins
    (sk/lay-bar :island {:color :species}))
islandspeciesAdelieChinstrapGentooTorgersenBiscoeDream020406080100120
(def bar-pl (-> data/penguins
                (sk/lay-bar :island {:color :species})
                sk/plan))
(def bar-layer (first (:layers (first (:panels bar-pl)))))

The mark type is :rect and the layer knows the categories:

bar-layer
{:mark :rect,
 :style {:opacity 0.85},
 :position :dodge,
 :categories ["Torgersen" "Biscoe" "Dream"],
 :groups
 [{:color
   [0.8941176470588236 0.10196078431372549 0.10980392156862745 1.0],
   :label "Adelie",
   :counts
   [{:category "Torgersen", :count 52}
    {:category "Biscoe", :count 44}
    {:category "Dream", :count 56}],
   :dodge-idx 0}
  {:color
   [0.21568627450980393 0.49411764705882355 0.7215686274509804 1.0],
   :label "Chinstrap",
   :counts
   [{:category "Torgersen", :count 0}
    {:category "Biscoe", :count 0}
    {:category "Dream", :count 68}],
   :dodge-idx 1}
  {:color
   [0.30196078431372547 0.6862745098039216 0.2901960784313726 1.0],
   :label "Gentoo",
   :counts
   [{:category "Torgersen", :count 0}
    {:category "Biscoe", :count 124}
    {:category "Dream", :count 0}],
   :dodge-idx 2}],
 :y-domain [0 124],
 :x-domain ("Torgersen" "Biscoe" "Dream"),
 :dodge-ctx {:n-groups 3}}

Each group (one per color) has counts for every category:

(mapv (fn [g]
        {:label (:label g)
         :counts (:counts g)})
      (:groups bar-layer))
[{:label "Adelie",
  :counts
  [{:category "Torgersen", :count 52}
   {:category "Biscoe", :count 44}
   {:category "Dream", :count 56}]}
 {:label "Chinstrap",
  :counts
  [{:category "Torgersen", :count 0}
   {:category "Biscoe", :count 0}
   {:category "Dream", :count 68}]}
 {:label "Gentoo",
  :counts
  [{:category "Torgersen", :count 0}
   {:category "Biscoe", :count 124}
   {:category "Dream", :count 0}]}]

The :position field (:dodge or :stack) tells the renderer how to arrange multiple groups within each category.

Stacked Bars

Stacking changes the position field:

(def stacked-pl (-> data/penguins
                    (sk/lay-stacked-bar :island {:color :species})
                    sk/plan))
(def stacked-layer (first (:layers (first (:panels stacked-pl)))))
(:position stacked-layer)
:stack

The counts are the same β€” only the rendering instruction differs. The plan describes what to draw; the renderer decides how.

Regression Lines

A regression produces line segments in data space.

(-> data/iris
    (sk/lay-point :sepal_length :sepal_width)
    sk/lay-lm)
sepal widthsepal length4.55.05.56.06.57.07.58.02.02.53.03.54.04.5
(def lm-pl (-> data/iris
               (sk/lay-point :sepal_length :sepal_width)
               sk/lay-lm
               sk/plan))

Two layers β€” points and line:

(mapv :mark (:layers (first (:panels lm-pl))))
[:point :line]
(def lm-layer (second (:layers (first (:panels lm-pl)))))

Its group has endpoints β€” a line segment in data space:

(first (:groups lm-layer))
{:color [0.2 0.2 0.2 1.0],
 :label "",
 :x1 4.3,
 :y1 3.1528422048579974,
 :x2 7.9,
 :y2 2.930056932187078}

The renderer maps these two points through scales to get a pixel-space line segment.

Per-Group Regression

When both points and regression have a color mapping, the line layer gets one segment per group:

(-> data/iris
    (sk/view :petal_length :petal_width {:color :species})
    sk/lay-point
    sk/lay-lm)
petal widthpetal lengthspeciessetosaversicolorvirginica12345670.00.51.01.52.02.5
(def grp-pl (-> data/iris
                (sk/view :petal_length :petal_width {:color :species})
                sk/lay-point
                sk/lay-lm
                sk/plan))
(let [line-layer (second (:layers (first (:panels grp-pl))))]
  (mapv (fn [g]
          {:color (:color g)
           :x1 (some-> (:x1 g) (Math/round) int)
           :x2 (some-> (:x2 g) (Math/round) int)})
        (:groups line-layer)))
[{:color
  [0.8941176470588236 0.10196078431372549 0.10980392156862745 1.0],
  :x1 1,
  :x2 2}
 {:color
  [0.21568627450980393 0.49411764705882355 0.7215686274509804 1.0],
  :x1 3,
  :x2 5}
 {:color
  [0.30196078431372547 0.6862745098039216 0.2901960784313726 1.0],
  :x1 5,
  :x2 7}]

Three line segments, each with its own color β€” one per species.

Connected Lines (Polylines)

Line marks from identity data (not regression) store xs/ys vectors:

(def wave {:x (range 30)
           :y (map #(Math/sin (* % 0.3)) (range 30))})
(-> wave
    (sk/lay-line :x :y))
yx051015202530-1.0-0.8-0.6-0.4-0.20.00.20.40.60.81.0
(def wave-pl (-> wave
                 (sk/lay-line :x :y)
                 sk/plan))
(def wave-group (first (:groups (first (:layers (first (:panels wave-pl)))))))
{:n-points (count (:xs wave-group))
 :first-x (first (:xs wave-group))
 :last-x (last (:xs wave-group))}
{:n-points 30, :first-x 0, :last-x 29}

The renderer connects these points in order to draw a polyline.

Value Bars

Value bars map categorical x to numeric y without any counting. The plan stores the raw x/y pairs:

(def sales {:product [:widget :gadget :gizmo :doohickey]
            :revenue [120 340 210 95]})
(-> sales
    (sk/lay-value-bar :product :revenue))
revenueproduct:widget:gadget:gizmo:doohickey050100150200250300350
(def sales-pl (-> sales
                  (sk/lay-value-bar :product :revenue)
                  sk/plan))
(let [g (first (:groups (first (:layers (first (:panels sales-pl))))))]
  {:xs (:xs g)
   :ys (:ys g)})
{:xs #tech.v3.dataset.column<string>[4]
:product
[:widget, :gadget, :gizmo, :doohickey],
 :ys #tech.v3.dataset.column<int64>[4]
:revenue
[120, 340, 210, 95]}

Flipped Coordinates

Setting :coord :flip swaps x and y in the plan’s panel:

(def flip-pl (-> data/iris
                 (sk/lay-bar :species)
                 (sk/coord :flip)
                 sk/plan))
(:coord (first (:panels flip-pl)))
:flip

The domains are swapped β€” the categorical axis is now y:

(let [p (first (:panels flip-pl))]
  {:x-domain-type (if (number? (first (:x-domain p))) :numeric :categorical)
   :y-domain-type (if (number? (first (:y-domain p))) :numeric :categorical)})
{:x-domain-type :numeric, :y-domain-type :categorical}

The layer data is unchanged β€” the coord type tells the renderer to swap axes during mapping.

Options Affect the Plan

Title, labels, and dimensions are recorded in the plan:

(def opts-pl (-> data/iris
                 (sk/lay-point :sepal_length :sepal_width)
                 (sk/plan {:title "My Custom Title"
                             :x-label "Length (cm)"
                             :y-label "Width (cm)"
                             :width 800
                             :height 300})))
opts-pl
{: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.2 0.2 0.2 1.0],
       :xs #tech.v3.dataset.column<float64>[150]
: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>[150]
: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...],
       :row-indices #tech.v3.dataset.column<int64>[150]
:__row-idx
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19...]}],
     :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 800,
 :height 300,
 :caption nil,
 :total-width 822.5,
 :legend-position :right,
 :layout-type :single,
 :layout
 {:subtitle-pad 0,
  :legend-w 0,
  :caption-pad 0,
  :y-label-pad 22.5,
  :legend-h 0,
  :title-pad 18,
  :strip-h 0,
  :x-label-pad 18,
  :strip-w 0},
 :grid {:rows 1, :cols 1},
 :legend nil,
 :panel-height 300.0,
 :title "My Custom Title",
 :y-label "Width (cm)",
 :alpha-legend nil,
 :x-label "Length (cm)",
 :subtitle nil,
 :panel-width 800.0,
 :size-legend nil,
 :total-height 336.0,
 :margin 30}

The layout records how much space to reserve for each label:

(:layout opts-pl)
{:subtitle-pad 0,
 :legend-w 0,
 :caption-pad 0,
 :y-label-pad 22.5,
 :legend-h 0,
 :title-pad 18,
 :strip-h 0,
 :x-label-pad 18,
 :strip-w 0}

Plan vs Plot β€” Side by Side

sk/plan and sk/plot accept the same arguments. sk/plan returns the intermediate data map; sk/plot returns the final SVG.

The plan (a plain Clojure map):

(def final-views
  (-> data/iris
      (sk/view :petal_length :petal_width {:color :species})
      sk/lay-point
      sk/lay-lm))
(def final-pl (sk/plan final-views {:title "Iris Petals"}))
final-pl
{:panels
 [{:coord :cartesian,
   :y-domain [-0.01999999999999999 2.62],
   :x-scale {:type :linear},
   :x-domain [0.705 7.195],
   :x-ticks
   {:values [1.0 2.0 3.0 4.0 5.0 6.0 7.0],
    :labels ["1" "2" "3" "4" "5" "6" "7"],
    :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]
:petal_length
[1.400, 1.400, 1.300, 1.500, 1.400, 1.700, 1.400, 1.500, 1.400, 1.500, 1.500, 1.600, 1.400, 1.100, 1.200, 1.500, 1.300, 1.400, 1.700, 1.500...],
       :ys #tech.v3.dataset.column<float64>[50]
:petal_width
[0.2000, 0.2000, 0.2000, 0.2000, 0.2000, 0.4000, 0.3000, 0.2000, 0.2000, 0.1000, 0.2000, 0.2000, 0.1000, 0.1000, 0.2000, 0.4000, 0.4000, 0.3000, 0.3000, 0.3000...],
       :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]
:petal_length
[4.700, 4.500, 4.900, 4.000, 4.600, 4.500, 4.700, 3.300, 4.600, 3.900, 3.500, 4.200, 4.000, 4.700, 3.600, 4.400, 4.500, 4.100, 4.500, 3.900...],
       :ys #tech.v3.dataset.column<float64>[50]
:petal_width
[1.400, 1.500, 1.500, 1.300, 1.500, 1.300, 1.600, 1.000, 1.300, 1.400, 1.000, 1.500, 1.000, 1.400, 1.300, 1.400, 1.500, 1.000, 1.500, 1.100...],
       :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]
:petal_length
[6.000, 5.100, 5.900, 5.600, 5.800, 6.600, 4.500, 6.300, 5.800, 6.100, 5.100, 5.300, 5.500, 5.000, 5.100, 5.300, 5.500, 6.700, 6.900, 5.000...],
       :ys #tech.v3.dataset.column<float64>[50]
:petal_width
[2.500, 1.900, 2.100, 1.800, 2.200, 2.100, 1.700, 1.800, 1.800, 2.500, 2.000, 1.900, 2.100, 2.000, 2.400, 2.300, 1.800, 2.200, 2.300, 1.500...],
       :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 [0.1 2.5],
     :x-domain [1.0 6.9]}
    {:mark :line,
     :style {:stroke-width 2.5},
     :groups
     [{:color
       [0.8941176470588236
        0.10196078431372549
        0.10980392156862745
        1.0],
       :label "setosa",
       :x1 1.0,
       :y1 0.15302476654486402,
       :x2 1.9,
       :y2 0.33414535119772626}
      {:color
       [0.21568627450980393
        0.49411764705882355
        0.7215686274509804
        1.0],
       :label "versicolor",
       :x1 3.0,
       :y1 0.9088724584103528,
       :x2 5.1,
       :y2 1.6040850277264331}
      {:color
       [0.30196078431372547 0.6862745098039216 0.2901960784313726 1.0],
       :label "virginica",
       :x1 4.5,
       :y1 1.8573676029159527,
       :x2 6.9,
       :y2 2.2420802958833654}],
     :y-domain [0.1 2.5],
     :x-domain [1.0 6.9]}],
   :y-scale {:type :linear},
   :y-ticks
   {:values [-0.0 0.5 1.0 1.5 2.0 2.5],
    :labels ["0.0" "0.5" "1.0" "1.5" "2.0" "2.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 18,
  :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 "Iris Petals",
 :y-label "petal width",
 :alpha-legend nil,
 :x-label "petal length",
 :subtitle nil,
 :panel-width 600.0,
 :size-legend nil,
 :total-height 436.0,
 :margin 30}

Layer summary:

(mapv (fn [l]
        {:mark (:mark l)
         :n-groups (count (:groups l))})
      (:layers (first (:panels final-pl))))
[{:mark :point, :n-groups 3} {:mark :line, :n-groups 3}]

The rendered plot (SVG):

(-> final-views (sk/options {:title "Iris Petals"}))
Iris Petalspetal widthpetal lengthspeciessetosaversicolorvirginica12345670.00.51.01.52.02.5

Multi-Panel Plans

Faceting produces plans with multiple panels. Each panel has its own domains, ticks, and layers, plus grid positioning.

(def faceted-pl
  (-> data/iris
      (sk/lay-point :sepal_length :sepal_width {:color :species})
      (sk/facet :species)
      sk/plan))

The grid tells us the layout:

(:grid faceted-pl)
{:rows 1, :cols 3}

Three panels β€” one per species:

(count (:panels faceted-pl))
3

Each panel has a grid position and strip label:

(:panels faceted-pl)
[{:coord :cartesian,
  :y-domain [1.88 4.5200000000000005],
  :col-label "setosa",
  :x-scale {:type :linear},
  :x-domain [4.12 8.08],
  :x-ticks {:values [6.0 8.0], :labels ["6" "8"], :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...]}],
    :y-domain [2.3 4.4],
    :x-domain [4.3 5.8]}],
  :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}
 {:coord :cartesian,
  :y-domain [1.88 4.5200000000000005],
  :col-label "versicolor",
  :x-scale {:type :linear},
  :x-domain [4.12 8.08],
  :x-ticks {:values [6.0 8.0], :labels ["6" "8"], :categorical? false},
  :col 1,
  :layers
  [{:mark :point,
    :style {:opacity 0.75, :radius 3.0},
    :groups
    [{: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
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19...]}],
    :y-domain [2.0 3.4],
    :x-domain [4.9 7.0]}],
  :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}
 {:coord :cartesian,
  :y-domain [1.88 4.5200000000000005],
  :col-label "virginica",
  :x-scale {:type :linear},
  :x-domain [4.12 8.08],
  :x-ticks {:values [6.0 8.0], :labels ["6" "8"], :categorical? false},
  :col 2,
  :layers
  [{:mark :point,
    :style {:opacity 0.75, :radius 3.0},
    :groups
    [{: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
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19...]}],
    :y-domain [2.2 3.8],
    :x-domain [4.9 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}]

Panel-level domains show the data range for each subset:

(:panels faceted-pl)
[{:coord :cartesian,
  :y-domain [1.88 4.5200000000000005],
  :col-label "setosa",
  :x-scale {:type :linear},
  :x-domain [4.12 8.08],
  :x-ticks {:values [6.0 8.0], :labels ["6" "8"], :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...]}],
    :y-domain [2.3 4.4],
    :x-domain [4.3 5.8]}],
  :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}
 {:coord :cartesian,
  :y-domain [1.88 4.5200000000000005],
  :col-label "versicolor",
  :x-scale {:type :linear},
  :x-domain [4.12 8.08],
  :x-ticks {:values [6.0 8.0], :labels ["6" "8"], :categorical? false},
  :col 1,
  :layers
  [{:mark :point,
    :style {:opacity 0.75, :radius 3.0},
    :groups
    [{: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
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19...]}],
    :y-domain [2.0 3.4],
    :x-domain [4.9 7.0]}],
  :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}
 {:coord :cartesian,
  :y-domain [1.88 4.5200000000000005],
  :col-label "virginica",
  :x-scale {:type :linear},
  :x-domain [4.12 8.08],
  :x-ticks {:values [6.0 8.0], :labels ["6" "8"], :categorical? false},
  :col 2,
  :layers
  [{:mark :point,
    :style {:opacity 0.75, :radius 3.0},
    :groups
    [{: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
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19...]}],
    :y-domain [2.2 3.8],
    :x-domain [4.9 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}]

With shared scales (the default), all panels have the same domains. With :scales :free-y, each panel gets its own y-domain.

The plan also records per-panel pixel dimensions:

(select-keys faceted-pl [:layout-type :grid :total-width :total-height])
{:layout-type :facet-grid,
 :grid {:rows 1, :cols 3},
 :total-width 722.5,
 :total-height 434.0}

Multi-panel plans validate against the same Malli schema:

(sk/valid-plan? faceted-pl)
true

Malli Validation

Every plan conforms to a Malli schema. Validation runs automatically when sk/plan is called (default :validate true). Pass {:validate false} to skip it.

You can also check manually with sk/valid-plan?:

(sk/valid-plan? tiny-pl)
true
(sk/valid-plan? iris-pl)
true
(sk/valid-plan? hist-pl)
true
(sk/valid-plan? bar-pl)
true
(sk/valid-plan? lm-pl)
true
(sk/valid-plan? final-pl)
true

When a plan is invalid, sk/explain-plan shows which part failed:

(sk/explain-plan (assoc tiny-pl :width "not-a-number"))
{:schema
 [:map [:width pos-int?] [:height pos-int?] [:margin number?] [:total-width number?] [:total-height number?] [:panel-width number?] [:panel-height number?] [:grid [:map [:rows pos-int?] [:cols pos-int?]]] [:layout-type [:enum :single :facet-grid :multi-variable]] [:title {:optional true} [:maybe string?]] [:subtitle {:optional true} [:maybe string?]] [:caption {:optional true} [:maybe string?]] [:x-label {:optional true} [:maybe string?]] [:y-label {:optional true} [:maybe string?]] [:legend {:optional true} [:maybe [:or [:map [:title keyword?] [:entries [:vector [:map [:label string?] [:color [:vector {:min 3, :max 4} number?]]]]]] [:map [:title keyword?] [:type [:= :continuous]] [:min number?] [:max number?] [:color-scale {:optional true} [:maybe keyword?]] [:stops [:vector [:map [:t number?] [:color [:vector {:min 3, :max 4} number?]]]]]]]]] [:size-legend {:optional true} [:maybe [:map [:title keyword?] [:type [:= :size]] [:min number?] [:max number?] [:entries [:vector [:map [:value number?] [:radius number?]]]]]]] [:alpha-legend {:optional true} [:maybe [:map [:title keyword?] [:type [:= :alpha]] [:min number?] [:max number?] [:entries [:vector [:map [:value number?] [:alpha number?]]]]]]] [:legend-position [:enum :right :bottom :top :none]] [:panels [:vector [:map [:x-domain [:vector any?]] [:y-domain [:vector any?]] [:x-scale [:map [:type [:enum :linear :log :categorical]]]] [:y-scale [:map [:type [:enum :linear :log :categorical]]]] [:coord [:enum :cartesian :flip :polar :fixed]] [:x-ticks [:map [:values [:vector any?]] [:labels [:vector string?]] [:categorical? boolean?]]] [:y-ticks [:map [:values [:vector any?]] [:labels [:vector string?]] [:categorical? boolean?]]] [:layers [:vector [:map [:mark keyword?] [:style [:map [:opacity {:optional true} number?] [:radius {:optional true} number?] [:stroke-width {:optional true} number?] [:font-size {:optional true} number?] [:box-width {:optional true} number?] [:cap-width {:optional true} number?] [:length {:optional true} number?] [:jitter {:optional true} [:or boolean? number?]]]] [:groups {:optional true} [:vector [:or [:map [:color [:vector {:min 3, :max 4} number?]] [:xs [:sequential number?]] [:ys [:sequential number?]] [:colors {:optional true} [:sequential [:vector {:min 3, :max 4} number?]]] [:sizes {:optional true} [:sequential number?]] [:alphas {:optional true} [:sequential number?]] [:shapes {:optional true} [:vector any?]] [:row-indices {:optional true} [:sequential int?]]] [:map [:color [:vector {:min 3, :max 4} number?]] [:bars [:vector [:map [:lo number?] [:hi number?] [:count number?]]]]] [:map [:color [:vector {:min 3, :max 4} number?]] [:label string?] [:counts [:vector [:map [:category any?] [:count number?]]]]] [:map [:color [:vector {:min 3, :max 4} number?]] [:xs [:sequential any?]] [:ys [:sequential number?]]] [:map [:color [:vector {:min 3, :max 4} number?]] [:x1 number?] [:y1 number?] [:x2 number?] [:y2 number?]] [:map [:color [:vector {:min 3, :max 4} number?]] [:xs [:sequential number?]] [:ys [:sequential number?]]] [:map [:color [:vector {:min 3, :max 4} number?]] [:xs [:sequential number?]] [:ys [:sequential number?]] [:labels {:optional true} [:vector string?]]] [:map [:color [:vector {:min 3, :max 4} number?]] [:xs [:sequential number?]] [:ys [:sequential number?]] [:ymins [:sequential number?]] [:ymaxs [:sequential number?]]]]]] [:boxes {:optional true} [:vector [:map [:category any?] [:color [:vector {:min 3, :max 4} number?]] [:color-category {:optional true} any?] [:median number?] [:q1 number?] [:q3 number?] [:whisker-lo number?] [:whisker-hi number?] [:outliers {:optional true} [:sequential number?]]]]] [:violins {:optional true} [:vector [:map [:category any?] [:color [:vector {:min 3, :max 4} number?]] [:color-category {:optional true} any?] [:ys [:sequential number?]] [:densities [:sequential number?]]]]] [:tiles {:optional true} [:vector [:map [:x-lo number?] [:x-hi number?] [:y-lo number?] [:y-hi number?] [:color [:vector {:min 3, :max 4} number?]]]]] [:ridges {:optional true} [:vector [:map [:category any?] [:color [:vector {:min 3, :max 4} number?]] [:ys [:sequential number?]] [:densities [:sequential number?]]]]] [:color-categories {:optional true} [:maybe [:vector any?]]] [:position {:optional true} keyword?] [:categories {:optional true} [:vector any?]] [:side {:optional true} [:enum :x :y :both]]]]] [:row int?] [:col int?] [:annotations {:optional true} [:vector [:map [:mark [:enum :rule-v :rule-h :band-v :band-h]] [:intercept {:optional true} number?] [:lo {:optional true} number?] [:hi {:optional true} number?]]]] [:row-label {:optional true} [:maybe string?]] [:col-label {:optional true} [:maybe string?]]]]] [:layout [:map [:x-label-pad number?] [:y-label-pad number?] [:title-pad number?] [:subtitle-pad number?] [:caption-pad number?] [:legend-w number?] [:legend-h number?] [:strip-h number?] [:strip-w number?]]]],
 :value
 {:panels
  [{:coord :cartesian,
    :y-domain [0.8 5.2],
    :x-scale {:type :linear},
    :x-domain [0.8 5.2],
    :x-ticks
    {:values [1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0],
     :labels ["1.0" "1.5" "2.0" "2.5" "3.0" "3.5" "4.0" "4.5" "5.0"],
     :categorical? false},
    :col 0,
    :layers
    [{:mark :point,
      :style {:opacity 0.75, :radius 3.0},
      :groups
      [{:color [0.2 0.2 0.2 1.0], :xs #tech.v3.dataset.column<int64>[5]
:x
[1, 2, 3, 4, 5], :ys #tech.v3.dataset.column<int64>[5]
:y
[2, 4, 1, 5, 3], :row-indices #tech.v3.dataset.column<int64>[5]
:__row-idx
[0, 1, 2, 3, 4]}],
      :y-domain [1 5],
      :x-domain [1 5]}],
    :y-scale {:type :linear},
    :y-ticks
    {:values [1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0],
     :labels ["1.0" "1.5" "2.0" "2.5" "3.0" "3.5" "4.0" "4.5" "5.0"],
     :categorical? false},
    :row 0}],
  :width "not-a-number",
  :height 400,
  :caption nil,
  :total-width 622.5,
  :legend-position :right,
  :layout-type :single,
  :layout
  {:subtitle-pad 0,
   :legend-w 0,
   :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 nil,
  :panel-height 400.0,
  :title nil,
  :y-label "y",
  :alpha-legend nil,
  :x-label "x",
  :subtitle nil,
  :panel-width 600.0,
  :size-legend nil,
  :total-height 418.0,
  :margin 30},
 :errors
 ({:path [:width],
   :in [:width],
   :schema pos-int?,
   :value "not-a-number"})}

Data Types

Plans are plain inspectable data β€” maps, numbers, strings, keywords, and dtype-next buffers for numeric arrays (see Architecture) (:xs, :ys, etc.). The buffers support nth, count, seq, and all standard sequence operations.

(type (:xs (first (:groups (first (:layers (first (:panels tiny-pl))))))))
tech.v3.dataset.impl.column.Column

You can convert any numeric buffer to a plain vector with vec:

(vec (:xs (first (:groups (first (:layers (first (:panels tiny-pl))))))))
[1 2 3 4 5]

What’s Next

source: notebooks/napkinsketch_book/exploring_sketches.clj