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))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))1The 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}))(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))3Each 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)))20Histograms
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))(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):barThe 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}))(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):stackThe 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)(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)(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))(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))(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))):flipThe 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"}))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))3Each 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)trueMalli 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)trueWhen 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.ColumnYou 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
- Architecture β the four-stage pipeline in detail
- Extensibility β add custom marks, stats, and renderers