27 Architecture
Plotje has a five-stage pipeline. You compose a pose β a declarative description of data, mappings, and layers β that flattens into a draft automatically.
This notebook traces a small example through every stage, explains the plan boundary, and shows the namespace structure.
(ns plotje-book.architecture
(:require
;; Kindly -- notebook rendering protocol
[scicloj.kindly.v4.kind :as kind]
;; Rdatasets -- standard datasets
[scicloj.metamorph.ml.rdatasets :as rdatasets]
;; Plotje -- composable plotting
[scicloj.plotje.api :as pj]
;; Pose substrate -- leaf->draft, resolve-tree
[scicloj.plotje.impl.pose :as pose-impl]
;; Plan pipeline -- draft->plan, domains, ticks, legends, layout
[scicloj.plotje.impl.plan :as plan-impl]
;; Malli schema validation
[scicloj.plotje.impl.plan-schema :as ss]))Pipeline Overview
(composable API)"] -->|pose->draft| D["Draft
(flat maps)"] D -->|draft->plan| P["Plan
(data-space)"] P -->|scales + coords| M["Membrane
(drawing primitives)"] M -->|tree walk| F["Plot
(output)"] style B fill:#d1c4e9 style D fill:#e8f5e9 style P fill:#fff3e0 style M fill:#e3f2fd style F fill:#fce4ec
Pose β a composable description of data, mappings, and layers. Built by
pj/pose,pj/lay-*,pj/options,pj/facet,pj/arrange,pj/scale, andpj/coord. No computation has happened yet.Draft β a flat vector of maps, one per applicable layer-and-leaf combination with scope merged. Each map has
:data,:x,:y,:mark,:stat, and aesthetic keys. Produced bypj/draft.Plan β fully resolved geometry in data space (domains, ticks, legends, computed shapes), as plain Clojure maps and dtype-next buffers. Produced by
pj/plan. No rendering primitives yet.Membrane β positioned drawing primitives (Translate, WithColor, Path, Label, β¦). Produced by
pj/plan->membrane.Plot β final output (SVG hiccup or BufferedImage). Produced by
pj/membrane->plotwalking the membrane tree.
Most users only interact with the pose stage and never need to think about the others. The stages below matter when you are debugging unexpected output, building a custom renderer, or extending the library.
Pipeline Trace
Letβs trace a small example through all five stages, inspecting the intermediate values at each step.
(def trace-data
{:x [1 2 3 4 5]
:y [2 4 3 5 4]
:g [:a :a :b :b :b]})Pose
The user composes a pose by threading data through composable functions. The pose records what to plot without doing any computation.
(def trace-pose
(-> trace-data
(pj/lay-point :x :y {:color :g})))A pose is a plain Clojure map. The fields below are what you see while inspecting the threaded value:
:dataβ the dataset (coerced to Tablecloth):mappingβ mappings that flow into every layer on this pose:layersβ layers placed on this pose:posesβ sub-poses; a leaf has none:optsβ plot-level options (title, width, etc.)
(pj/pose? trace-pose)trueBecause a leaf pose has no sub-poses, this is a leaf:
(pose-impl/leaf? trace-pose)trueThe mapping carries the position aesthetics (from the positional :x / :y arguments); the color aesthetic (from the options map) rides on the layer so a subsequent pj/lay-* with different options does not disturb it:
(:mapping trace-pose){:x :x, :y :y}(get-in trace-pose [:layers 0 :layer-type]):pointThe :color mapping lives on the layerβs own :mapping:
(get-in trace-pose [:layers 0 :mapping :color]):gDraft
pj/draft flattens the pose into a vector of maps. Each map merges pose-level mappings, leaf mappings, and layer details into one flat map with :data, :x, :y, :mark, etc.
(def trace-draft
(pj/draft trace-pose))(count trace-draft)1(select-keys (first trace-draft) [:x :y :mark :color]){:x :x, :y :y, :mark :point, :color :g}Plan
draft->plan converts the draft into a plan β a pure-data map with data-space geometry, resolved colors, computed domains, and tick info. The values are still in data space.
(def trace-plan
(plan-impl/draft->plan trace-draft {}))trace-plan{:panels
[{:coord :cartesian,
:y-domain [1.85 5.15],
: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},
:size-scale nil,
:alpha-scale nil,
:groups
[{:color
[0.8941176470588236
0.10196078431372549
0.10980392156862745
1.0],
:xs #tech.v3.dataset.column<int64>[2]
:x
[1, 2],
:ys #tech.v3.dataset.column<int64>[2]
:y
[2, 4],
:label "a",
:row-indices #tech.v3.dataset.column<int64>[2]
:__row-idx
[0, 1]}
{:color
[0.21568627450980393
0.49411764705882355
0.7215686274509804
1.0],
:xs #tech.v3.dataset.column<int64>[3]
:x
[3, 4, 5],
:ys #tech.v3.dataset.column<int64>[3]
:y
[3, 5, 4],
:label "b",
:row-indices #tech.v3.dataset.column<int64>[3]
:__row-idx
[2, 3, 4]}],
:y-domain [2 5],
:x-domain [1 5]}],
:y-scale {:type :linear},
:y-ticks
{:values [2.0 2.5 3.0 3.5 4.0 4.5 5.0],
:labels ["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 600.0,
:legend-position :right,
:layout-type :single,
:layout
{:subtitle-pad 0,
:legend-w 100,
:caption-pad 0,
:y-label-pad 42.5,
:legend-h 0.0,
:title-pad 0,
:strip-h 0,
:x-label-pad 38,
:strip-w 0.0},
:grid {:rows 1, :cols 1},
:legend
{:title :g,
:entries
[{:label "a",
:color
[0.8941176470588236 0.10196078431372549 0.10980392156862745 1.0]}
{:label "b",
:color
[0.21568627450980393
0.49411764705882355
0.7215686274509804
1.0]}]},
:panel-height 362.0,
:title nil,
:y-label "y",
:alpha-legend nil,
:x-label "x",
:subtitle nil,
:panel-width 457.5,
:size-legend nil,
:total-height 400.0,
:tooltip nil,
:margin 10}The plan validates against a Malli schema:
(ss/valid? trace-plan)trueMembrane
plan->membrane converts the plan into a tree of membrane drawing primitives laid out for the rendered plot.
(def trace-membrane (pj/plan->membrane trace-plan))trace-membrane[{:x 12,
:y 181.0,
:drawable
{:degrees -90,
:drawable
{:color [0.2 0.2 0.2 1.0],
:drawables
({:text "y",
:font {:name nil, :size 13, :weight nil, :width nil, :slant nil},
:text-anchor "middle"})}}}
{:x 271.25,
:y 382.0,
:drawable
{:color [0.2 0.2 0.2 1.0],
:drawables
({:text "x",
:font {:name nil, :size 13, :weight nil, :width nil, :slant nil},
:text-anchor "middle"})}}
{:x 510.0,
:y 2,
:drawable
{:color [0.2 0.2 0.2 1.0],
:drawables
({:text "g",
:font
{:name nil, :size 11, :weight nil, :width nil, :slant nil}})}}
{:x 510.0,
:y 20,
:drawable
[{:x 0,
:y 0,
:drawable
{:color
[0.8941176470588236 0.10196078431372549 0.10980392156862745 1.0],
:drawables
({:style :membrane.ui/style-fill,
:drawables [{:width 8, :height 8, :border-radius 4.0}]})}}
{:x 12,
:y 0,
:drawable
{:color [0.2 0.2 0.2 1.0],
:drawables
({:text "a",
:font
{:name nil, :size 10, :weight nil, :width nil, :slant nil}})}}]}
{:x 510.0,
:y 36,
:drawable
[{:x 0,
:y 0,
:drawable
{:color
[0.21568627450980393 0.49411764705882355 0.7215686274509804 1.0],
:drawables
({:style :membrane.ui/style-fill,
:drawables [{:width 8, :height 8, :border-radius 4.0}]})}}
{:x 12,
:y 0,
:drawable
{:color [0.2 0.2 0.2 1.0],
:drawables
({:text "b",
:font
{:name nil, :size 10, :weight nil, :width nil, :slant nil}})}}]}
{:x 52.5,
:y 10.0,
:drawable
{:color
[0.9098039215686274 0.9098039215686274 0.9098039215686274 1.0],
:drawables
({:style :membrane.ui/style-fill,
:drawables [{:width 437.5, :height 342.0}]})}}
{:x 42.5,
:y 0.0,
:drawable
[{:color
[0.9607843137254902 0.9607843137254902 0.9607843137254902 1.0],
:drawables
({:stroke-width 0.6,
:drawables
[{:style :membrane.ui/style-stroke,
:drawables
[{:points
([29.88636363636363 10] [29.88636363636363 352.0])}]}]})}
{:color
[0.9607843137254902 0.9607843137254902 0.9607843137254902 1.0],
:drawables
({:stroke-width 0.6,
:drawables
[{:style :membrane.ui/style-stroke,
:drawables
[{:points
([79.60227272727272 10] [79.60227272727272 352.0])}]}]})}
{:color
[0.9607843137254902 0.9607843137254902 0.9607843137254902 1.0],
:drawables
({:stroke-width 0.6,
:drawables
[{:style :membrane.ui/style-stroke,
:drawables
[{:points
([129.3181818181818 10] [129.3181818181818 352.0])}]}]})}
{:color
[0.9607843137254902 0.9607843137254902 0.9607843137254902 1.0],
:drawables
({:stroke-width 0.6,
:drawables
[{:style :membrane.ui/style-stroke,
:drawables
[{:points
([179.03409090909088 10] [179.03409090909088 352.0])}]}]})}
{:color
[0.9607843137254902 0.9607843137254902 0.9607843137254902 1.0],
:drawables
({:stroke-width 0.6,
:drawables
[{:style :membrane.ui/style-stroke,
:drawables [{:points ([228.75 10] [228.75 352.0])}]}]})}
{:color
[0.9607843137254902 0.9607843137254902 0.9607843137254902 1.0],
:drawables
({:stroke-width 0.6,
:drawables
[{:style :membrane.ui/style-stroke,
:drawables
[{:points
([278.4659090909091 10] [278.4659090909091 352.0])}]}]})}
{:color
[0.9607843137254902 0.9607843137254902 0.9607843137254902 1.0],
:drawables
({:stroke-width 0.6,
:drawables
[{:style :membrane.ui/style-stroke,
:drawables
[{:points
([328.1818181818182 10] [328.1818181818182 352.0])}]}]})}
{:color
[0.9607843137254902 0.9607843137254902 0.9607843137254902 1.0],
:drawables
({:stroke-width 0.6,
:drawables
[{:style :membrane.ui/style-stroke,
:drawables
[{:points
([377.89772727272725 10] [377.89772727272725 352.0])}]}]})}
{:color
[0.9607843137254902 0.9607843137254902 0.9607843137254902 1.0],
:drawables
({:stroke-width 0.6,
:drawables
[{:style :membrane.ui/style-stroke,
:drawables
[{:points
([427.6136363636363 10] [427.6136363636363 352.0])}]}]})}
{:color
[0.9607843137254902 0.9607843137254902 0.9607843137254902 1.0],
:drawables
({:stroke-width 0.6,
:drawables
[{:style :membrane.ui/style-stroke,
:drawables
[{:points
([10 336.45454545454544] [447.5 336.45454545454544])}]}]})}
{:color
[0.9607843137254902 0.9607843137254902 0.9607843137254902 1.0],
:drawables
({:stroke-width 0.6,
:drawables
[{:style :membrane.ui/style-stroke,
:drawables
[{:points
([10 284.6363636363636] [447.5 284.6363636363636])}]}]})}
{:color
[0.9607843137254902 0.9607843137254902 0.9607843137254902 1.0],
:drawables
({:stroke-width 0.6,
:drawables
[{:style :membrane.ui/style-stroke,
:drawables
[{:points
([10 232.8181818181818] [447.5 232.8181818181818])}]}]})}
{:color
[0.9607843137254902 0.9607843137254902 0.9607843137254902 1.0],
:drawables
({:stroke-width 0.6,
:drawables
[{:style :membrane.ui/style-stroke,
:drawables
[{:points
([10 181.00000000000003] [447.5 181.00000000000003])}]}]})}
{:color
[0.9607843137254902 0.9607843137254902 0.9607843137254902 1.0],
:drawables
({:stroke-width 0.6,
:drawables
[{:style :membrane.ui/style-stroke,
:drawables
[{:points
([10 129.18181818181822] [447.5 129.18181818181822])}]}]})}
{:color
[0.9607843137254902 0.9607843137254902 0.9607843137254902 1.0],
:drawables
({:stroke-width 0.6,
:drawables
[{:style :membrane.ui/style-stroke,
:drawables
[{:points
([10 77.36363636363637] [447.5 77.36363636363637])}]}]})}
{:color
[0.9607843137254902 0.9607843137254902 0.9607843137254902 1.0],
:drawables
({:stroke-width 0.6,
:drawables
[{:style :membrane.ui/style-stroke,
:drawables
[{:points
([10 25.54545454545456] [447.5 25.54545454545456])}]}]})}
{:x 26.88636363636363,
:y 333.45454545454544,
:drawable
[{:color
[0.8941176470588236
0.10196078431372549
0.10980392156862745
0.75],
:drawables
({:style :membrane.ui/style-fill,
:drawables [{:width 6.0, :height 6.0, :border-radius 3.0}]})}
nil],
:row-idx 0}
{:x 126.31818181818181,
:y 126.18181818181822,
:drawable
[{:color
[0.8941176470588236
0.10196078431372549
0.10980392156862745
0.75],
:drawables
({:style :membrane.ui/style-fill,
:drawables [{:width 6.0, :height 6.0, :border-radius 3.0}]})}
nil],
:row-idx 1}
{:x 225.75,
:y 229.8181818181818,
:drawable
[{:color
[0.21568627450980393
0.49411764705882355
0.7215686274509804
0.75],
:drawables
({:style :membrane.ui/style-fill,
:drawables [{:width 6.0, :height 6.0, :border-radius 3.0}]})}
nil],
:row-idx 2}
{:x 325.1818181818182,
:y 22.54545454545456,
:drawable
[{:color
[0.21568627450980393
0.49411764705882355
0.7215686274509804
0.75],
:drawables
({:style :membrane.ui/style-fill,
:drawables [{:width 6.0, :height 6.0, :border-radius 3.0}]})}
nil],
:row-idx 3}
{:x 424.6136363636363,
:y 126.18181818181822,
:drawable
[{:color
[0.21568627450980393
0.49411764705882355
0.7215686274509804
0.75],
:drawables
({:style :membrane.ui/style-fill,
:drawables [{:width 6.0, :height 6.0, :border-radius 3.0}]})}
nil],
:row-idx 4}
{:x 29.88636363636363,
:y 364.0,
:drawable
{:color [0.4 0.4 0.4 1.0],
:drawables
({:text "1.0",
:font
{:name nil, :size 11, :weight nil, :width nil, :slant nil},
:text-anchor "middle"})}}
{:x 79.60227272727272,
:y 364.0,
:drawable
{:color [0.4 0.4 0.4 1.0],
:drawables
({:text "1.5",
:font
{:name nil, :size 11, :weight nil, :width nil, :slant nil},
:text-anchor "middle"})}}
{:x 129.3181818181818,
:y 364.0,
:drawable
{:color [0.4 0.4 0.4 1.0],
:drawables
({:text "2.0",
:font
{:name nil, :size 11, :weight nil, :width nil, :slant nil},
:text-anchor "middle"})}}
{:x 179.03409090909088,
:y 364.0,
:drawable
{:color [0.4 0.4 0.4 1.0],
:drawables
({:text "2.5",
:font
{:name nil, :size 11, :weight nil, :width nil, :slant nil},
:text-anchor "middle"})}}
{:x 228.75,
:y 364.0,
:drawable
{:color [0.4 0.4 0.4 1.0],
:drawables
({:text "3.0",
:font
{:name nil, :size 11, :weight nil, :width nil, :slant nil},
:text-anchor "middle"})}}
{:x 278.4659090909091,
:y 364.0,
:drawable
{:color [0.4 0.4 0.4 1.0],
:drawables
({:text "3.5",
:font
{:name nil, :size 11, :weight nil, :width nil, :slant nil},
:text-anchor "middle"})}}
{:x 328.1818181818182,
:y 364.0,
:drawable
{:color [0.4 0.4 0.4 1.0],
:drawables
({:text "4.0",
:font
{:name nil, :size 11, :weight nil, :width nil, :slant nil},
:text-anchor "middle"})}}
{:x 377.89772727272725,
:y 364.0,
:drawable
{:color [0.4 0.4 0.4 1.0],
:drawables
({:text "4.5",
:font
{:name nil, :size 11, :weight nil, :width nil, :slant nil},
:text-anchor "middle"})}}
{:x 427.6136363636363,
:y 364.0,
:drawable
{:color [0.4 0.4 0.4 1.0],
:drawables
({:text "5.0",
:font
{:name nil, :size 11, :weight nil, :width nil, :slant nil},
:text-anchor "middle"})}}
{:x 7.0,
:y 330.95454545454544,
:drawable
{:color [0.4 0.4 0.4 1.0],
:drawables
({:text "2.0",
:font
{:name nil, :size 11, :weight nil, :width nil, :slant nil},
:text-anchor "end"})}}
{:x 7.0,
:y 279.1363636363636,
:drawable
{:color [0.4 0.4 0.4 1.0],
:drawables
({:text "2.5",
:font
{:name nil, :size 11, :weight nil, :width nil, :slant nil},
:text-anchor "end"})}}
{:x 7.0,
:y 227.3181818181818,
:drawable
{:color [0.4 0.4 0.4 1.0],
:drawables
({:text "3.0",
:font
{:name nil, :size 11, :weight nil, :width nil, :slant nil},
:text-anchor "end"})}}
{:x 7.0,
:y 175.50000000000003,
:drawable
{:color [0.4 0.4 0.4 1.0],
:drawables
({:text "3.5",
:font
{:name nil, :size 11, :weight nil, :width nil, :slant nil},
:text-anchor "end"})}}
{:x 7.0,
:y 123.68181818181822,
:drawable
{:color [0.4 0.4 0.4 1.0],
:drawables
({:text "4.0",
:font
{:name nil, :size 11, :weight nil, :width nil, :slant nil},
:text-anchor "end"})}}
{:x 7.0,
:y 71.86363636363637,
:drawable
{:color [0.4 0.4 0.4 1.0],
:drawables
({:text "4.5",
:font
{:name nil, :size 11, :weight nil, :width nil, :slant nil},
:text-anchor "end"})}}
{:x 7.0,
:y 20.04545454545456,
:drawable
{:color [0.4 0.4 0.4 1.0],
:drawables
({:text "5.0",
:font
{:name nil, :size 11, :weight nil, :width nil, :slant nil},
:text-anchor "end"})}}]}]Plot
membrane->plot converts the membrane tree into SVG hiccup.
(def trace-plot
(pj/membrane->plot trace-membrane :svg
{:total-width (:total-width trace-plan)
:total-height (:total-height trace-plan)}))(kind/pprint trace-plot)[:svg
{:xmlns "http://www.w3.org/2000/svg",
:width 600.0,
:height 400.0,
:viewBox "0 0 600.0 400.0",
:role "img",
:font-family
"system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"}
[:g
[:g
{:transform "translate(12.00,181.00)"}
[:g
{:transform "rotate(-90.00)"}
[:g
[:text
{:fill "rgb(51,51,51)",
:fill-opacity 1.0,
:font-size 13,
:dominant-baseline "hanging",
:text-anchor "middle"}
"y"]]]]
[:g
{:transform "translate(271.25,382.00)"}
[:g
[:text
{:fill "rgb(51,51,51)",
:fill-opacity 1.0,
:font-size 13,
:dominant-baseline "hanging",
:text-anchor "middle"}
"x"]]]
[:g
{:transform "translate(510.00,2.00)"}
[:g
[:text
{:fill "rgb(51,51,51)",
:fill-opacity 1.0,
:font-size 11,
:dominant-baseline "hanging"}
"g"]]]
[:g
{:transform "translate(510.00,20.00)"}
[:g
[:g
{:transform "translate(0.00,0.00)"}
[:g
[:g
[:rect
{:y 0,
:rx 4.0,
:stroke "none",
:fill "rgb(228,26,28)",
:width 8,
:x 0,
:ry 4.0,
:fill-opacity 1.0,
:height 8}]]]]
[:g
{:transform "translate(12.00,0.00)"}
[:g
[:text
{:fill "rgb(51,51,51)",
:fill-opacity 1.0,
:font-size 10,
:dominant-baseline "hanging"}
"a"]]]]]
[:g
{:transform "translate(510.00,36.00)"}
[:g
[:g
{:transform "translate(0.00,0.00)"}
[:g
[:g
[:rect
{:y 0,
:rx 4.0,
:stroke "none",
:fill "rgb(55,126,184)",
:width 8,
:x 0,
:ry 4.0,
:fill-opacity 1.0,
:height 8}]]]]
[:g
{:transform "translate(12.00,0.00)"}
[:g
[:text
{:fill "rgb(51,51,51)",
:fill-opacity 1.0,
:font-size 10,
:dominant-baseline "hanging"}
"b"]]]]]
[:g
{:transform "translate(52.50,10.00)"}
[:g
[:g
[:rect
{:fill "rgb(232,232,232)",
:fill-opacity 1.0,
:stroke "none",
:x 0,
:y 0,
:width 437.5,
:height 342.0}]]]]
[:g
{:transform "translate(42.50,0.00)"}
[:g
[:g
[:g
[:g
[:polyline
{:fill "none",
:stroke "rgb(245,245,245)",
:stroke-opacity 1.0,
:stroke-width 0.6,
:points "29.89,10.00 29.89,352.00"}]]]]
[:g
[:g
[:g
[:polyline
{:fill "none",
:stroke "rgb(245,245,245)",
:stroke-opacity 1.0,
:stroke-width 0.6,
:points "79.60,10.00 79.60,352.00"}]]]]
[:g
[:g
[:g
[:polyline
{:fill "none",
:stroke "rgb(245,245,245)",
:stroke-opacity 1.0,
:stroke-width 0.6,
:points "129.32,10.00 129.32,352.00"}]]]]
[:g
[:g
[:g
[:polyline
{:fill "none",
:stroke "rgb(245,245,245)",
:stroke-opacity 1.0,
:stroke-width 0.6,
:points "179.03,10.00 179.03,352.00"}]]]]
[:g
[:g
[:g
[:polyline
{:fill "none",
:stroke "rgb(245,245,245)",
:stroke-opacity 1.0,
:stroke-width 0.6,
:points "228.75,10.00 228.75,352.00"}]]]]
[:g
[:g
[:g
[:polyline
{:fill "none",
:stroke "rgb(245,245,245)",
:stroke-opacity 1.0,
:stroke-width 0.6,
:points "278.47,10.00 278.47,352.00"}]]]]
[:g
[:g
[:g
[:polyline
{:fill "none",
:stroke "rgb(245,245,245)",
:stroke-opacity 1.0,
:stroke-width 0.6,
:points "328.18,10.00 328.18,352.00"}]]]]
[:g
[:g
[:g
[:polyline
{:fill "none",
:stroke "rgb(245,245,245)",
:stroke-opacity 1.0,
:stroke-width 0.6,
:points "377.90,10.00 377.90,352.00"}]]]]
[:g
[:g
[:g
[:polyline
{:fill "none",
:stroke "rgb(245,245,245)",
:stroke-opacity 1.0,
:stroke-width 0.6,
:points "427.61,10.00 427.61,352.00"}]]]]
[:g
[:g
[:g
[:polyline
{:fill "none",
:stroke "rgb(245,245,245)",
:stroke-opacity 1.0,
:stroke-width 0.6,
:points "10.00,336.45 447.50,336.45"}]]]]
[:g
[:g
[:g
[:polyline
{:fill "none",
:stroke "rgb(245,245,245)",
:stroke-opacity 1.0,
:stroke-width 0.6,
:points "10.00,284.64 447.50,284.64"}]]]]
[:g
[:g
[:g
[:polyline
{:fill "none",
:stroke "rgb(245,245,245)",
:stroke-opacity 1.0,
:stroke-width 0.6,
:points "10.00,232.82 447.50,232.82"}]]]]
[:g
[:g
[:g
[:polyline
{:fill "none",
:stroke "rgb(245,245,245)",
:stroke-opacity 1.0,
:stroke-width 0.6,
:points "10.00,181.00 447.50,181.00"}]]]]
[:g
[:g
[:g
[:polyline
{:fill "none",
:stroke "rgb(245,245,245)",
:stroke-opacity 1.0,
:stroke-width 0.6,
:points "10.00,129.18 447.50,129.18"}]]]]
[:g
[:g
[:g
[:polyline
{:fill "none",
:stroke "rgb(245,245,245)",
:stroke-opacity 1.0,
:stroke-width 0.6,
:points "10.00,77.36 447.50,77.36"}]]]]
[:g
[:g
[:g
[:polyline
{:fill "none",
:stroke "rgb(245,245,245)",
:stroke-opacity 1.0,
:stroke-width 0.6,
:points "10.00,25.55 447.50,25.55"}]]]]
[:g
{:transform "translate(26.89,333.45)", :data-row-idx 0}
[:g
[:g
[:rect
{:y 0,
:rx 3.0,
:stroke "none",
:fill "rgb(228,26,28)",
:width 6.0,
:x 0,
:ry 3.0,
:fill-opacity 0.75,
:height 6.0}]]]]
[:g
{:transform "translate(126.32,126.18)", :data-row-idx 1}
[:g
[:g
[:rect
{:y 0,
:rx 3.0,
:stroke "none",
:fill "rgb(228,26,28)",
:width 6.0,
:x 0,
:ry 3.0,
:fill-opacity 0.75,
:height 6.0}]]]]
[:g
{:transform "translate(225.75,229.82)", :data-row-idx 2}
[:g
[:g
[:rect
{:y 0,
:rx 3.0,
:stroke "none",
:fill "rgb(55,126,184)",
:width 6.0,
:x 0,
:ry 3.0,
:fill-opacity 0.75,
:height 6.0}]]]]
[:g
{:transform "translate(325.18,22.55)", :data-row-idx 3}
[:g
[:g
[:rect
{:y 0,
:rx 3.0,
:stroke "none",
:fill "rgb(55,126,184)",
:width 6.0,
:x 0,
:ry 3.0,
:fill-opacity 0.75,
:height 6.0}]]]]
[:g
{:transform "translate(424.61,126.18)", :data-row-idx 4}
[:g
[:g
[:rect
{:y 0,
:rx 3.0,
:stroke "none",
:fill "rgb(55,126,184)",
:width 6.0,
:x 0,
:ry 3.0,
:fill-opacity 0.75,
:height 6.0}]]]]
[:g
{:transform "translate(29.89,364.00)"}
[:g
[:text
{:fill "rgb(102,102,102)",
:fill-opacity 1.0,
:font-size 11,
:dominant-baseline "hanging",
:text-anchor "middle"}
"1.0"]]]
[:g
{:transform "translate(79.60,364.00)"}
[:g
[:text
{:fill "rgb(102,102,102)",
:fill-opacity 1.0,
:font-size 11,
:dominant-baseline "hanging",
:text-anchor "middle"}
"1.5"]]]
[:g
{:transform "translate(129.32,364.00)"}
[:g
[:text
{:fill "rgb(102,102,102)",
:fill-opacity 1.0,
:font-size 11,
:dominant-baseline "hanging",
:text-anchor "middle"}
"2.0"]]]
[:g
{:transform "translate(179.03,364.00)"}
[:g
[:text
{:fill "rgb(102,102,102)",
:fill-opacity 1.0,
:font-size 11,
:dominant-baseline "hanging",
:text-anchor "middle"}
"2.5"]]]
[:g
{:transform "translate(228.75,364.00)"}
[:g
[:text
{:fill "rgb(102,102,102)",
:fill-opacity 1.0,
:font-size 11,
:dominant-baseline "hanging",
:text-anchor "middle"}
"3.0"]]]
[:g
{:transform "translate(278.47,364.00)"}
[:g
[:text
{:fill "rgb(102,102,102)",
:fill-opacity 1.0,
:font-size 11,
:dominant-baseline "hanging",
:text-anchor "middle"}
"3.5"]]]
[:g
{:transform "translate(328.18,364.00)"}
[:g
[:text
{:fill "rgb(102,102,102)",
:fill-opacity 1.0,
:font-size 11,
:dominant-baseline "hanging",
:text-anchor "middle"}
"4.0"]]]
[:g
{:transform "translate(377.90,364.00)"}
[:g
[:text
{:fill "rgb(102,102,102)",
:fill-opacity 1.0,
:font-size 11,
:dominant-baseline "hanging",
:text-anchor "middle"}
"4.5"]]]
[:g
{:transform "translate(427.61,364.00)"}
[:g
[:text
{:fill "rgb(102,102,102)",
:fill-opacity 1.0,
:font-size 11,
:dominant-baseline "hanging",
:text-anchor "middle"}
"5.0"]]]
[:g
{:transform "translate(7.00,330.95)"}
[:g
[:text
{:fill "rgb(102,102,102)",
:fill-opacity 1.0,
:font-size 11,
:dominant-baseline "hanging",
:text-anchor "end"}
"2.0"]]]
[:g
{:transform "translate(7.00,279.14)"}
[:g
[:text
{:fill "rgb(102,102,102)",
:fill-opacity 1.0,
:font-size 11,
:dominant-baseline "hanging",
:text-anchor "end"}
"2.5"]]]
[:g
{:transform "translate(7.00,227.32)"}
[:g
[:text
{:fill "rgb(102,102,102)",
:fill-opacity 1.0,
:font-size 11,
:dominant-baseline "hanging",
:text-anchor "end"}
"3.0"]]]
[:g
{:transform "translate(7.00,175.50)"}
[:g
[:text
{:fill "rgb(102,102,102)",
:fill-opacity 1.0,
:font-size 11,
:dominant-baseline "hanging",
:text-anchor "end"}
"3.5"]]]
[:g
{:transform "translate(7.00,123.68)"}
[:g
[:text
{:fill "rgb(102,102,102)",
:fill-opacity 1.0,
:font-size 11,
:dominant-baseline "hanging",
:text-anchor "end"}
"4.0"]]]
[:g
{:transform "translate(7.00,71.86)"}
[:g
[:text
{:fill "rgb(102,102,102)",
:fill-opacity 1.0,
:font-size 11,
:dominant-baseline "hanging",
:text-anchor "end"}
"4.5"]]]
[:g
{:transform "translate(7.00,20.05)"}
[:g
[:text
{:fill "rgb(102,102,102)",
:fill-opacity 1.0,
:font-size 11,
:dominant-baseline "hanging",
:text-anchor "end"}
"5.0"]]]]]]]And this is what it looks like when rendered:
(kind/hiccup trace-plot)Shortcut: Pose to Plan
In practice, pj/plan does the pose-to-plan conversion in one step β computing the draft and running draft->plan internally.
(def shortcut-plan (pj/plan trace-pose))(ss/valid? shortcut-plan)truePipeline Summary
| Stage | Type | Coordinates |
|---|---|---|
| Pose | Plain map (leaf or composite) | N/A (declarative) |
| Draft | Vector of maps | N/A (declarative) |
| Plan | Clojure maps + dtype buffers | Data space |
| Membrane | Record tree | Drawing units |
| Plot | Hiccup vectors | Drawing units |
The Plan Boundary
The plan is the boundary between description and rendering. The pose and draft stages assemble the description. The plan resolves it into computed geometry, domains, ticks, and legend β still as plain data, before any layout. The membrane and plot stages then produce the rendered output.
The plan is plain inspectable data β maps, numbers, strings, keywords, and dtype-next buffers for numeric arrays. It validates against a Malli schema.
This separation enables:
Inspecting the plan without rendering
Validating plot structure with Malli
Adding alternate backends that consume plans (SVG and raster are implemented today)
Multi-Layer Example
A pose can hold multiple layers that share one mapping. Here, scatter points and per-species regression lines share the same panel because both lay-point and lay-smooth target the same :petal-length/:petal-width mapping.
(def multi-pose
(-> (rdatasets/datasets-iris)
(pj/pose :petal-length :petal-width {:color :species})
pj/lay-point
(pj/lay-smooth {:stat :linear-model})))The pose has one leaf with two pose-level layers:
(count (:layers multi-pose))2(mapv :layer-type (:layers multi-pose))[:point :smooth]The draft produces two maps β one per layer β both sharing the same columns:
(def multi-draft (pj/draft multi-pose))(count multi-draft)2(mapv :mark multi-draft)[:point :line]Building a plan with a title and checking the layers:
(def multi-plan
(pj/plan multi-pose {:title "Iris Petals with Regression"}))(mapv (fn [layer]
{:mark (:mark layer)
:n-groups (count (:groups layer))})
(:layers (first (:panels multi-plan))))[{:mark :point, :n-groups 3} {:mark :line, :n-groups 3}]Title and legend are top-level plan keys:
multi-plan{: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},
:size-scale nil,
:alpha-scale nil,
: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, :opacity 1.0},
: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 600.0,
:legend-position :right,
:layout-type :single,
:layout
{:subtitle-pad 0,
:legend-w 102,
:caption-pad 0,
:y-label-pad 42.5,
:legend-h 0.0,
:title-pad 33,
:strip-h 0,
:x-label-pad 38,
:strip-w 0.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 329.0,
:title "Iris Petals with Regression",
:y-label "petal width",
:alpha-legend nil,
:x-label "petal length",
:subtitle nil,
:panel-width 455.5,
:size-legend nil,
:total-height 400.0,
:tooltip nil,
:margin 10}And the rendered result:
(-> (rdatasets/datasets-iris)
(pj/pose :petal-length :petal-width {:color :species})
pj/lay-point
(pj/lay-smooth {:stat :linear-model})
(pj/options {:title "Iris Petals with Regression"}))Namespace Structure
impl/pose.clj holds the pose substrate: resolve-tree (merges mappings/data/options down from root to every leaf), leaf->draft (flattens a leaf into draft maps with optional facet expansion), and the multi-pair / grid composite utilities. impl/compositor.clj handles composite rendering β each leaf becomes a sub-plot, tiled via layout. impl/plan.clj holds draft->plan (domains, ticks, legends, layout). impl/resolve.clj holds resolve-draft-layer (single draft layer resolution, column type inference, grouping).
The impl/ directory is pure data β no membrane dependency. The render/ directory uses membrane for layout and SVG conversion.
Dependencies
Plotje builds on several excellent Clojure libraries:
Tablecloth & dtype-next β dataset manipulation and high-performance numeric arrays
Membrane β rendering and layout
Wadogo β scales
Clojure2d β color palettes and gradients
Fastmath β statistics
Malli β schema validation
Whatβs Next
- Exploring Plans β a hands-on tour of the plan stage, building intuition for the data shape that the pipeline produces
- Extensibility β add custom marks, stats, scales, coordinate systems, and output formats by extending the multimethods at each pipeline stage