28 API Reference
Complete reference for every public function in scicloj.plotje.api.
Each entry shows the docstring, a live example, and a test. For galleries of mark variations, see the Visualization Goals chapters (Distributions, Ranking, Change Over Time, Timelines, Relationships).
Sample Data
(def tiny {:x [1 2 3 4 5]
:y [2 4 1 5 3]
:group [:a :a :b :b :b]})(def sales {:product [:widget :gadget :gizmo :doohickey]
:revenue [120 340 210 95]})(def measurements {:treatment ["A" "B" "C" "D"]
:mean [10.0 15.0 12.0 18.0]
:ci-lo [8.0 12.0 9.5 15.5]
:ci-hi [12.0 18.0 14.5 20.5]})Construction
pj/pose is not a literal composition of the single-step transitions. Its 1-arity is (-> x pj/->pose pj/infer-mapping) β the same lift-and-default-map the other shortcuts (pj/draft, pj/plan, pj/membrane, pj/plot, pj/save) apply on the way in β but its typed arities add positional column-arg parsing, multi-pair composite construction, and pose extend-or-promote on top of pj/->pose. The examples below walk each shape.
pose
[]
[x]
[x y]
[x y z]
[x y z opts]
Construct or extend a pose.
On raw data (first argument is not itself a pose):
(pj/pose)β empty leaf.(pj/pose data)β leaf with data; on 1-3 column datasets the mapping is auto-inferred (:x, then:y, then:color) so the pose renders without an explicit mapping call.(pj/pose data {:color :species})β leaf with aesthetic mapping.(pj/pose data :x-col)β leaf with{:x :x-col}.(pj/pose data :x-col {:color :c})β univariate x with opts.(pj/pose data :x-col :y-col)β leaf with:xand:y.(pj/pose data :x-col :y-col {:color :c})β positional x/y with opts.(pj/pose data [[:a :b] [:c :d]])β multi-pair: N bivariate panels.(pj/pose data [:a :b :c])β multi-pair: N univariate panels.(pj/pose data (pj/cross cols cols) {:color :c})β multi-pair plus aesthetic mapping at the composite root.
Threaded over an existing pose (first argument is a pose):
(pj/pose fr)β pass-through; lifts a literal map for notebook auto-render if it is not already tagged.(pj/pose fr :x-col :y-col)β extend a leaf-without-position, or promote a leaf-with-position into a 2-panel composite, or append a panel to a composite.(pj/pose fr :x-col :y-col {:color :c})β same, with aesthetic routed to the composite root on promote.(pj/pose fr {:color :c})β aesthetic-only: extend mapping or (on leaf-with-position) promote.(pj/pose fr [[:a :b] [:c :d]])β multi-pair: append N panels.(pj/pose fr (pj/cross cols cols))β SPLOM N^2 panels in one call.(pj/pose fr (pj/cross cols cols) {:color :c})β SPLOM plus aesthetic mapping at the composite root.(pj/pose fr {:data X :color :c})β extend mapping AND replace the top-level data with X.
On a hand-built pose-shaped map (1-arity, input has :layers or :poses): the map is validated and tagged with Kindly auto-render metadata, but its keys are not reordered and its :data is not coerced β the typed shape is preserved verbatim. A flat composite (:poses of leaf maps) is supported; literal nested composites (any sub-pose itself has :poses) are rejected, matching pj/arrangeβs rule that its elements must be leaves.
Writing a mapping out in full. Any mapping value may be written as a map naming its source, and optionally which side of the scale to read it through:
{:column :species}β the column, even where a value of that name could be drawn.{:value "blue"}β the color, even where the data carries a column called blue.{:scale false}β draw the value as it stands rather than pass it through the aestheticβs scale. On a column this is the only route to an identity scale:{:color {:column :hex :scale false}}draws the colors the column holds.{:scale true}β read it as data.{:color {:value "Model A" :scale true}}draws one palette color and earns a legend entry.
The conventions decide when :scale is absent: a column passes through the scale, a written value is drawn on the appearance aesthetics and is a data value on :x and :y. See pj/layer-option-docs for what each aesthetic accepts, and pj/scale for choosing a scaleβs type.
Create a leaf pose with data and columns:
(-> (rdatasets/datasets-iris)
(pj/pose :sepal-length :sepal-width)
pj/lay-point)Map form β include aesthetics on the pose so every layer inherits them:
(-> (rdatasets/datasets-iris)
(pj/pose :sepal-length :sepal-width {:color :species})
pj/lay-point
(pj/lay-smooth {:stat :linear-model}))A bare collection of scalars (numbers, strings, or keywords) is data too β it becomes a single column named :value. With no chart type, a single numeric column infers a histogram:
(pj/pose [1 4 1 5 6 2 3 3 3 2 4 5 1 2 3 4])with-data
[pose data]
Supply or replace the top-level dataset on a pose. Useful for building a template once and applying it to different datasets:
(def template (-> (pj/pose)
(pj/pose :x :y {:color :group})
pj/lay-point
(pj/lay-smooth {:stat :linear-model})))
(-> template (pj/with-data my-data))
(-> template (pj/with-data other-data))
At attach time, every keyword column reference in the templateβs mapping, layers, sub-poses, and facet options must exist in the dataset β otherwise an error is thrown naming the missing columns and listing what is available. Per-layer / per-sub-pose :data still overrides the top-level data.
Attach or replace the top-level dataset on a pose. Useful for building a dataless template and applying it to many datasets:
(def scatter-template
(-> (pj/pose nil {:x :x :y :y :color :group})
pj/lay-point))(-> scatter-template
(pj/with-data tiny))Multi-pair pose β a vector of [x y] pairs creates a composite with one pose per pair:
(-> (rdatasets/datasets-iris)
(pj/pose [[:sepal-length :sepal-width]
[:petal-length :petal-width]])
(pj/lay-point {:color :species}))Map form β explicit keys on a pose:
(-> (rdatasets/datasets-iris)
(pj/pose {:x :sepal-length :y :sepal-width})
pj/lay-point)cross
[xs ys]
Build a vector of [x y] pairs from two column-name sequences. Pair with pj/pose for SPLOM grids: when an MxN rectangle of pairs is threaded through pj/pose, the result is an MxN composite with shared scales.
(pj/cross [:a :b] [:c :d])returns[[:a :c] [:a :d] [:b :c] [:b :d]].
(pj/cross [:a :b] [1 2 3])([:a 1] [:a 2] [:a 3] [:b 1] [:b 2] [:b 3])Combine pj/cross with pj/pose to build a SPLOM:
(-> (rdatasets/datasets-iris)
(pj/pose (pj/cross [:sepal-length :petal-length]
[:sepal-width :petal-width])
{:color :species}))Layer Functions
lay
[pose-or-data layer-type-key]
[pose-or-data layer-type-key opts]
Add a root-scope layer. The layer attaches to :layers and flows to every descendant leaf at plan time (composite) or renders on the single panel (leaf).
The layer type is named either by its keyword or by the entry pj/layer-type-lookup answers with; both behave the same.
The generic layer adder. pj/lay-point, pj/lay-bar, etc. are convenience wrappers around pj/lay with a registered layer-type key. Use pj/lay directly when you have a custom layer type (from pj/layer-type-lookup on a registered key, or a raw layer-type map from an extension):
(-> (rdatasets/datasets-iris)
(pj/pose :sepal-length :sepal-width)
(pj/lay :point))The above delegates to :point β equivalent to pj/lay-point. The intended use of pj/lay is with a layer type that isnβt a built-in convenience: a registered custom layer type from an extension, or a raw layer-type map. See the Extension Example chapter for the full pattern β registering a :waterfall layer type and calling (pj/lay pose (layer-type/lookup :waterfall)) or wrapping it in a pj/lay-waterfall convenience function.
lay-point
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add a :point (scatter) layer to a pose. Without columns -> bare layer at the poseβs root (flows to every leaf). With columns -> position-bearing layer (attaches to the matching leaf via DFS-last identity, or appends a new sub-pose on miss).
(lay-point fr)β bare layer at root.(lay-point fr {:color :species})β bare layer with layer options.(lay-point data :x :y)β coerce data to a leaf, then attach.(lay-point data :x :y {:color :c})β same with layer options.
Accepted options: :alpha :color :color-type :data :group :in :jitter :mark :nudge-x :nudge-y :offset-x :offset-y :position :shape :size :stat :x :x-type :y :y-type.
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width {:color :species}))lay-line
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :line layer type β connected line through data points. Requires x (numerical) and y (numerical). Accepts :color, :alpha, :size (stroke width), :stroke-dash (:dashed/:dotted/:solid or a raw [dash gap] vector), :nudge-x, :nudge-y.
Accepted options: :alpha :color :color-type :data :group :in :mark :nudge-x :nudge-y :offset-x :offset-y :position :size :stat :stroke-dash :x :x-type :y :y-type.
(def wave {:x (range 30)
:y (map #(Math/sin (* % 0.3)) (range 30))})(-> wave
(pj/lay-line :x :y))lay-histogram
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :histogram layer type β bin numerical values into bars. X-only: pass one column. Accepts :bins (count), :binwidth, :color, :normalize (:density for density-normalized heights).
Accepted options: :alpha :bins :binwidth :color :color-type :data :group :in :mark :normalize :offset-x :offset-y :position :stat :x :x-type :y :y-type.
(-> (rdatasets/datasets-iris)
(pj/lay-histogram :sepal-length))A vector of columns creates one panel per column:
(pj/lay-histogram (rdatasets/datasets-iris) [:sepal-length :sepal-width])lay-bar
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add a :bar layer type.
One column (x only): counts occurrences of each category. Requires a categorical x.
Two columns (x and y): uses the y value directly as the bar height.
The stat is inferred from whether a y column is present, and overridable: pass {:stat :count} to count even with a y column, or {:stat :identity} to require an explicit height. :color (with :position :dodge/:stack) gives grouped or stacked bars.
The categorical axis can be x (vertical bars) or y (horizontal bars): (pj/lay-bar :value :category) with a categorical y draws horizontal value bars, no pj/coord needed. (Stacked/filled horizontal bars are not yet supported directly β put the category on x and add (pj/coord :flip).) To treat a numeric column as categorical, pass {:x-type :categorical} (or {:y-type :categorical}).
When both axes are numeric or temporal ((pj/lay-bar :x :y) with no categorical axis), each bar sits at its x position with a width taken from 0.9 of the smallest gap between adjacent x values β a time-series or numeric-position bar chart. Pass {:bar-width n} (data units) to set the width. Grouped numeric bars currently overlap rather than dodge.
Accepted options: :alpha :bar-width :color :color-type :data :group :in :mark :offset-x :offset-y :position :stat :x :x-type :y :y-type.
(-> (rdatasets/datasets-iris)
(pj/lay-bar :species))Stacked bars: pass {:position :stack} to pj/lay-bar.
(-> (rdatasets/palmerpenguins-penguins)
(pj/lay-bar :island {:position :stack :color :species}))100% stacked bars: pass {:position :fill} to pj/lay-bar.
(-> (rdatasets/palmerpenguins-penguins)
(pj/lay-bar :island {:position :fill :color :species}))Pass a y column and pj/lay-bar uses it as the bar height instead of counting (value bars):
(-> sales
(pj/lay-bar :product :revenue))The stat is inferred from whether a y column is present. Override it to count even when a y column is supplied:
(-> sales
(pj/lay-bar :product :revenue {:stat :count}))A bar chart needs a categorical axis. To put the categories on a numeric column, declare it categorical with {:x-type :categorical} (or {:y-type :categorical}):
(-> {:hour [9 10 11] :sales [3 5 4]}
(pj/lay-bar :hour :sales {:x-type :categorical}))The categorical axis can be either x or y. Put the category on y and the value on x for horizontal bars β no pj/coord needed (the orientation follows the categorical axis, the same way pj/lay-boxplot does):
(-> sales
(pj/lay-bar :revenue :product))Grouped horizontal bars dodge as well. Stacked or filled horizontal bars are not supported directly yet β put the category on x and add (pj/coord :flip) for those.
When neither axis is categorical, each bar sits at its numeric x position. The width is 0.9 of the smallest gap between adjacent x values, or set it with {:bar-width n} (in data units):
(-> {:x [1 2 3 4 5] :y [10 20 15 30 25]}
(pj/lay-bar :x :y))A temporal x gives a time-series bar chart, with calendar tick labels:
(-> {:month [#inst "2024-01-01" #inst "2024-02-01" #inst "2024-03-01"]
:revenue [120 180 150]}
(pj/lay-bar :month :revenue))lay-smooth
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :smooth layer type β a smoothed trend line. Defaults to LOESS (local regression). Pass {:stat :linear-model} for ordinary least squares instead. Requires x and y (both numerical). Accepts {:confidence-band true} for a confidence ribbon, and :stroke-dash.
Accepted options: :alpha :bandwidth :bootstrap-resamples :color :color-type :confidence-band :data :group :in :level :mark :nudge-x :nudge-y :offset-x :offset-y :position :size :stat :stroke-dash :x :x-type :y :y-type.
Linear regression: pass {:stat :linear-model} to pj/lay-smooth.
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width)
(pj/lay-smooth {:stat :linear-model}))(-> (let [r (rng/rng :jdk 42)
xs (vec (range 50))]
{:x xs
:y (mapv #(+ (Math/sin (* % 0.2))
(* 0.3 (- (rng/drandom r) 0.5)))
xs)})
(pj/lay-point :x :y)
(pj/lay-smooth {:bandwidth 0.2}))lay-density
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :density layer type β kernel density estimate curve. X-only: pass one numerical column. Accepts :color (fill), :bandwidth, and an opt-in outline on the curve: :stroke (outline color), :stroke-width, :stroke-dash (:dashed/:dotted/:solid or a raw [dash gap] vector).
Accepted options: :alpha :bandwidth :color :color-type :data :group :in :mark :offset-x :offset-y :position :stat :stroke :stroke-dash :stroke-width :trim :x :x-type :y :y-type.
(-> (rdatasets/datasets-iris)
(pj/lay-density :sepal-length))lay-area
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :area layer type β filled region between y and the baseline. Requires x and y (both numerical). Accepts :color (fill), :alpha, and an opt-in outline on the top curve: :stroke (outline color), :stroke-width, :stroke-dash.
Accepted options: :alpha :color :color-type :data :group :in :mark :offset-x :offset-y :position :stat :stroke :stroke-dash :stroke-width :x :x-type :y :y-type.
(-> wave
(pj/lay-area :x :y))Stacked areas: pass {:position :stack} to pj/lay-area.
(-> {:x (concat (range 10) (range 10) (range 10))
:y (concat [1 2 3 4 5 4 3 2 1 0]
[2 2 2 3 3 3 2 2 2 2]
[1 1 1 1 2 2 2 1 1 1])
:group (concat (repeat 10 "A") (repeat 10 "B") (repeat 10 "C"))}
(pj/lay-area :x :y {:position :stack :color :group}))lay-text
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :text layer type β text labels at data coordinates. Requires x, y, and {:text :column} for label content. :align-x (:left/:center/:right, default :left) and :align-y (:top/:center/:bottom, default :center) set which part of the text lands on the data point β e.g. :align-x :right tucks the label inside a barβs end, extending leftward. :box puts the text on a background box: true for the default box, or a map of box properties ({:corner-radius 8}). pj/lay-label is this layer with the box on.
Accepted options: :align-x :align-y :alpha :box :color :color-type :data :font-size :font-style :font-weight :group :in :mark :nudge-x :nudge-y :offset-x :offset-y :position :stat :text :x :x-type :y :y-type.
(-> {:x [1 2 3 4] :y [4 7 5 8] :name ["A" "B" "C" "D"]}
(pj/lay-text :x :y {:text :name}))lay-label
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :label layer type β text labels on a background box at data coordinates, for readability over dense data.
:label is the :text layer type with {:box true} preset, so it draws through the same mark and takes the same options, including :align-x/:align-y (defaults :left/:center); the box follows the anchored text. Pass :box to shape it β {:box {:corner-radius 0}} for square corners, {:box false} for bare text.
Accepted options: :align-x :align-y :alpha :box :color :color-type :data :font-size :font-style :font-weight :group :in :mark :nudge-x :nudge-y :offset-x :offset-y :position :stat :text :x :x-type :y :y-type.
(-> {:x [1 2 3 4] :y [4 7 5 8] :name ["A" "B" "C" "D"]}
(pj/lay-point :x :y {:size 5})
(pj/lay-label {:text :name}))lay-boxplot
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :boxplot layer type β box-and-whisker plot. Requires categorical x and numerical y. Shows median, quartiles, whiskers, and outliers. Accepts :color for grouped boxplots.
Accepted options: :alpha :box-width :color :color-type :data :group :in :mark :offset-x :offset-y :position :size :stat :x :x-type :y :y-type.
(-> (rdatasets/datasets-iris)
(pj/lay-boxplot :species :sepal-width))lay-violin
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :violin layer type β mirrored density estimate by category. Requires categorical x and numerical y. Accepts :color, :bandwidth.
Accepted options: :alpha :bandwidth :color :color-type :data :group :in :mark :offset-x :offset-y :position :size :stat :trim :x :x-type :y :y-type.
(-> (rdatasets/reshape2-tips)
(pj/lay-violin :day :total-bill))lay-errorbar
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :errorbar layer type β vertical error bars from pre-computed bounds. Requires x, y, and {:y-min :col :y-max :col} for lower/upper bounds.
Accepted options: :alpha :cap-width :color :color-type :data :group :in :mark :nudge-x :nudge-y :offset-x :offset-y :position :size :stat :x :x-type :y :y-max :y-min :y-type.
(-> measurements
(pj/lay-point :treatment :mean)
(pj/lay-errorbar {:y-min :ci-lo :y-max :ci-hi}))lay-lollipop
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :lollipop layer type β dot on a stem from the baseline. Requires categorical x and numerical y. Like a value bar but with a circle+line instead of a filled rectangle.
Accepted options: :alpha :color :color-type :data :group :in :mark :offset-x :offset-y :position :size :stat :x :x-type :y :y-type.
(-> sales
(pj/lay-lollipop :product :revenue))lay-tile
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :tile layer type β colored grid cells (heatmap). With :fill option: pre-computed tile colors from a column. Without :fill: auto-binned 2D histogram (stat :bin2d).
Accepted options: :alpha :color :color-type :data :density-2d-grid :fill :group :in :mark :offset-x :offset-y :position :stat :x :x-type :y :y-type.
(-> (rdatasets/datasets-iris)
(pj/lay-tile :sepal-length :sepal-width))lay-density-2d
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :density-2d layer type β 2D kernel density heatmap. Requires x and y (both numerical). Produces a smoothed density surface as colored tiles with a continuous gradient legend.
Accepted options: :alpha :color :color-type :data :density-2d-grid :group :in :mark :offset-x :offset-y :position :stat :x :x-type :y :y-type.
(-> (rdatasets/datasets-iris)
(pj/lay-density-2d :sepal-length :sepal-width))lay-contour
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :contour layer type β iso-density contour lines from 2D KDE. Requires x and y (both numerical). Accepts {:levels 10} for the number of contour levels.
Accepted options: :alpha :color :color-type :data :group :in :levels :mark :offset-x :offset-y :position :size :stat :x :x-type :y :y-type.
(-> (rdatasets/datasets-iris)
(pj/lay-contour :sepal-length :sepal-width))lay-ridgeline
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :ridgeline layer type β stacked density curves by category. Requires categorical x and numerical y. Categories stack vertically with density curves rendered horizontally.
Accepted options: :alpha :bandwidth :color :color-type :data :group :in :mark :offset-x :offset-y :position :stat :trim :x :x-type :y :y-type.
(-> (rdatasets/datasets-iris)
(pj/lay-ridgeline :species :sepal-length))lay-rug
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :rug layer type β short tick marks along the axis showing individual values. X-only: pass one column. Often layered with density or scatter.
Accepted options: :alpha :color :color-type :data :group :in :length :mark :offset-x :offset-y :position :side :stat :x :x-type :y :y-type.
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width)
(pj/lay-rug {:side :both}))lay-step
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :step layer type β staircase line (horizontal then vertical). Requires x and y (both numerical). Accepts :stroke-dash.
Accepted options: :alpha :color :color-type :data :group :in :mark :offset-x :offset-y :position :size :stat :stroke-dash :x :x-type :y :y-type.
(-> tiny
(pj/lay-step :x :y)
pj/lay-point)lay-summary
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :summary layer type β mean +/- standard error per category. Requires categorical x and numerical y. Shows a point at the mean with error bars for +/- 1 SE. Accepts :color for grouped summaries.
Accepted options: :alpha :color :color-type :data :group :in :mark :offset-x :offset-y :position :size :stat :x :x-type :y :y-type.
(-> (rdatasets/datasets-iris)
(pj/lay-summary :species :sepal-length))lay-interval-h
[pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :interval-h layer type β horizontal bar from x to x-end at categorical y. Each row becomes one rectangle; the y column is treated categorically so each distinct value occupies its own lane. Required: x (numeric or temporal start), y (categorical lane), :x-end column ref in opts (numeric or temporal end). Accepts :color, :alpha, :interval-thickness (band fill fraction, 0.0-1.0, default 0.7). (lay-interval-h data :start :task {:x-end :end :color :status})
Accepted options: :alpha :color :color-type :data :group :in :interval-thickness :mark :offset-x :offset-y :stat :x :x-end :x-type :y :y-type.
(-> {:start [#inst "2024-01-01" #inst "2024-03-01" #inst "2024-05-01"]
:end [#inst "2024-04-01" #inst "2024-06-01" #inst "2024-08-01"]
:task ["Design" "Build" "Test"]}
(pj/lay-interval-h :start :task {:x-end :end}))Reference Lines and Bands
Reference lines and shaded bands are regular layers. Position comes from the options map (:y-intercept for lay-rule-h, :x-intercept for lay-rule-v; :y-min/:y-max for lay-band-h, :x-min/:x-max for lay-band-v); :color overrides the default annotation color, and bands additionally honor :alpha to override the :band-opacity configuration default. Without x/y columns they attach at the root (every panel); with x/y columns they attach to one matching leaf.
Rule intercepts also accept temporal values (LocalDate, LocalDateTime, Instant, java.util.Date) so date-axis annotations need no manual conversion β see the second lay-rule-v example below.
Note on :y-min/:y-max. The same option keys carry two meanings depending on the layer kind. On lay-band-h/-v they are written numeric bounds (the band sits at fixed coordinates, independent of the data). On lay-errorbar (above) they are column references β one row per error bar, with :y-min and :y-max naming columns that supply the lower and upper bounds. ggplot2 keeps these separate via aes() (column) versus a value written outside it; Plotje overloads the keyword and dispatches by mark. Writing the mapping in full β {:column :lo} on the errorbar, {:value 12} on the band β says which reading you mean without changing which one the mark accepts.
lay-rule-v
[_pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :rule-v layer β vertical reference line at x = x-intercept. Position comes from opts (not data columns); :x-intercept is required. Accepts :x-intercept (numeric or temporal β LocalDate, LocalDateTime, Instant, java.util.Date), :color (literal string), and :stroke-dash (:dashed/:dotted/:solid or a raw [dash gap] vector). Temporal values are converted internally to match the x-axis scale so date-axis annotations work without manual conversion. The 4-arity finds or creates a sub-pose with these x/y columns and attaches the rule there (only panels matching that leaf show it).
(lay-rule-v pose {:x-intercept 5})β root-level, flows to every panel.(lay-rule-v pose :x :y {:x-intercept 5})β panel-scope (columns pick or create a sub-pose).(lay-rule-v pose {:x-intercept 5 :color "red"})β with override color.(lay-rule-v pose {:x-intercept #inst "2008-09-15"})β temporal intercept on a date axis.
Accepted options: :alpha :color :data :mark :offset-x :offset-y :stat :stroke-dash :x :x-intercept :y.
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width)
(pj/lay-rule-v {:x-intercept 6.0}))A temporal rule on a date axis β the same pose pattern, with :x-intercept taking a LocalDate.
(-> {:date [#inst "2024-01-01" #inst "2024-04-01" #inst "2024-08-01"]
:value [3 5 9]}
(pj/lay-line :date :value)
(pj/lay-rule-v {:x-intercept (java.time.LocalDate/parse "2024-06-01")
:color "#c0392b"}))lay-rule-h
[_pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :rule-h layer β horizontal reference line at y = y-intercept. Position comes from opts (not data columns); :y-intercept is required. Accepts :y-intercept (numeric or temporal β LocalDate, LocalDateTime, Instant, java.util.Date), :color (literal string), and :stroke-dash (:dashed/:dotted/:solid or a raw [dash gap] vector). Temporal values are converted internally to match the y-axis scale so date-axis annotations work without manual conversion. The 4-arity finds or creates a sub-pose with these x/y columns and attaches the rule there (only panels matching that leaf show it).
(lay-rule-h pose {:y-intercept 3})β root-level, flows to every panel.(lay-rule-h pose :x :y {:y-intercept 3})β panel-scope (columns pick or create a sub-pose).(lay-rule-h pose {:y-intercept 3 :color "red"})β with override color.(lay-rule-h pose {:y-intercept (java.time.LocalDate/parse "2024-01-01")})β temporal intercept on a date axis.
Accepted options: :alpha :color :data :mark :offset-x :offset-y :stat :stroke-dash :x :y :y-intercept.
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width)
(pj/lay-rule-h {:y-intercept 3.0}))lay-band-v
[_pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :band-v layer β vertical shaded band between x = x-min and x = x-max. Position comes from opts (not data columns); :x-min and :x-max are required and :x-min must be <= :x-max. Accepts :x-min (required), :x-max (required), :color (literal string), :alpha. Bounds may be numeric or temporal (LocalDate, LocalDateTime, Instant, java.util.Date); temporal values are converted internally to match the x-axis scale. The 4-arity finds or creates a sub-pose with these x/y columns and attaches the band there (only panels matching that leaf show it).
(lay-band-v pose {:x-min 4 :x-max 6})β root-level, flows to every panel.(lay-band-v pose :x :y {:x-min 4 :x-max 6})β panel-scope (columns pick or create a sub-pose).(lay-band-v pose {:x-min 4 :x-max 6 :color "blue" :alpha 0.3})β with color and opacity overrides.
Accepted options: :alpha :color :data :mark :offset-x :offset-y :stat :x :x-max :x-min :y.
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width)
(pj/lay-band-v {:x-min 5.5 :x-max 6.5}))lay-band-h
[_pose-or-data]
[pose-or-data x-or-opts]
[pose-or-data x y-or-opts]
[pose-or-data x y opts]
Add :band-h layer β horizontal shaded band between y = y-min and y = y-max. Position comes from opts (not data columns); :y-min and :y-max are required and :y-min must be <= :y-max. Accepts :y-min (required), :y-max (required), :color (literal string), :alpha. Bounds may be numeric or temporal (LocalDate, LocalDateTime, Instant, java.util.Date); temporal values are converted internally to match the y-axis scale. The 4-arity finds or creates a sub-pose with these x/y columns and attaches the band there (only panels matching that leaf show it).
(lay-band-h pose {:y-min 2 :y-max 4})β root-level, flows to every panel.(lay-band-h pose :x :y {:y-min 2 :y-max 4})β panel-scope (columns pick or create a sub-pose).(lay-band-h pose {:y-min 2 :y-max 4 :color "blue" :alpha 0.3})β with color and opacity overrides.
Accepted options: :alpha :color :data :mark :offset-x :offset-y :stat :x :y :y-max :y-min.
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width)
(pj/lay-band-h {:y-min 2.5 :y-max 3.5}))Transforms
coord
[pose coord-type]
Set coordinate transform on a pose. The coord applies to the pose it is called on and to everything below it, as a scale does: called on the pose you are building it covers every panel, and called on one cell before the cells are arranged, that cell alone. On a composite pose it attaches to the root, so every descendant leaf inherits it at plan time.
Supported coord-types:
:cartesianβ standard x-right, y-up mapping (the default).:flipβ swap x and y axes (horizontal bars / boxplots).:fixedβ equal aspect ratio (1 data unit = 1 data unit).:polarβ radial mapping: x to angle, y to radius.
Flip axes:
(-> (rdatasets/datasets-iris)
(pj/lay-bar :species) (pj/coord :flip))Polar coordinates:
(-> (rdatasets/datasets-iris)
(pj/lay-bar :species) (pj/coord :polar))scale
[pose aesthetic scale-type]
Set scale on a pose. The scale applies to the pose it is called on and to everything below it: called on the pose you are building, it covers the whole plot; called on one cell before the cells are arranged, that cell alone, so two cells can carry different scales. Under faceting every panel comes from one pose, so the panels share a type; :scales :free varies their domains.
A mapping written out in full can name a scale too, for that one mapping: {:size {:column :weight :scale :log}}. Where a scale is set at more than one scope the settings accumulate and the inner scope wins, key by key. Written on the same pose, this call is the inner one; written on a layer, the layerβs mapping is.
Accepts a type keyword or a scale spec map. :type and :domain belong to every scale. The rest are per aesthetic, and a key an aesthetic does not read is refused where it is written rather than dropped in silence:
Every aesthetic takes
:label, the title of whatever explains its scale to a reader: the axis for:xand:y, the legend for the rest. It wins over the<aesthetic>-labelplot option β:x-label,:color-labeland their siblings β which names the same thing one scope further out.:xand:ytake:include, a value the axis has to reach, or a collection of them.{:include 0}puts zero on the axis so that lengths drawn along it are proportional to the values, and the value lands exactly at the edge of the panel.:includeextends the extent the data gives where:domainreplaces it, so the two are not written together, and it is a set of values where a:domainis an ordered pair.:xand:ytake:breaks(explicit tick locations),:tick-labels(custom tick text paired with:breaks),:n-ticks(about this many ticks) and:tick-spacing(about this much room in drawing units per tick). A numeric axis reads whichever of the last two is named; a categorical one is ticked at its categories, which:n-ticksthins.:sizeand:alphatake:rangeβ what the aesthetic spans, in the quantity the mark draws it as, so[2 8]on:sizeis a radius in drawing units.:sizefurther takes:by, how a value spreads across that range (:sqrtby default, or:linearor:area), and:from-zero, which anchors both the domain and the range at zero so that twice the value is twice the ink.:shapetakes:values, the marker symbols to draw with.:colorand:filltake:range, the gradient a numeric column is read through;:coloralso takes:values, the colours a categorical column is drawn in. Both take:midpoint, the value the middle of the gradient is drawn at.
A value outside :domain is drawn at the nearer end rather than dropped, so a narrower domain says what the reader should compare without leaving rows off the panel. On an axis a :domain is two finite numbers, or two dates where the column is temporal; against a categorical column it is the list of categories, in the order they are to be drawn.
Aesthetics and accepted scale types:
The axis aesthetics (
:x,:y) accept:linear,:log,:categorical.The continuous appearance aesthetics (
:size,:alpha,:fill,:color) accept:linearand:logonly β:categoricaldoes not apply.The discrete appearance aesthetic
:shapeaccepts:categoricalonly β:linearand:logdo not apply to a discrete encoding.
:group is refused: it draws nothing of its own, so there is no scale to set. It splits a layer into one drawn group per value, and the order of those groups is the order of the data.
To take an aesthetic off its scale for one mapping rather than choose a type for the whole plot, write that mapping out in full: {:color {:column :hex :scale false}} draws the columnβs values as they stand, and {:size {:value 7 :scale true}} sends a written value through the scale. See the layer option docs for :color and :size.
A :domain has two readings, and the domain itself decides which: two numbers are a range, and anything else is a list of categories.
On a categorical :color, :fill or :shape column it gives the order the categories are placed in, which the legend and the palette both follow. A category the list leaves out is still drawn, ordered after the ones listed, with a warning. On :shape, :values supplies the symbols to draw those categories with in that same order; pj/shape-symbols lists the ones available.
On a numeric :color or :fill column it gives the two ends of the gradient, as it gives the ends of the drawn range on :size and :alpha. That is how every panel of a facet can be given one scale to share.
:tick-labels requires :breaks and must match it in count. Use it to draw numeric breaks with text of your own β for example, days of the week on a tile heatmap.
:n-ticks asks for about that many ticks. On a categorical axis it thins a crowded one, which otherwise labels every category; on a numeric axis it replaces the count that :tick-spacing would give. :tick-spacing asks for about that much room, in drawing units, per tick, and the count follows from how many fit. It is a target in the way :n-ticks is: the ticks are still rounded to values a reader can read off, so the room each one ends up with can come out under the number asked for. It steers the choice of numeric ticks and does nothing on a categorical axis, which says so. The :x-tick-spacing / :y-tick-spacing plot options name the same setting one scope further out.
(scale pose :x :log)β log scale on x-axis.(scale pose :x {:type :categorical :domain [...]})β explicit category order.(scale pose :x {:n-ticks 8})β about eight ticks, which on a crowded categorical axis thins it to eight of its categories.(scale pose :y {:type :linear :breaks [0 5 10]})β pin tick locations.(scale pose :x {:type :linear :breaks [1 2 3 4 5 6 7] :tick-labels ["Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun"]})β numeric positions with custom tick text.(scale pose :y {:type :log :domain [1 1000]})β log scale with explicit range.(scale pose :size :log)β log-spaced point sizes.(scale pose :size {:range [3 14]})β wider points than the default 2 to 8.(scale pose :size {:by :area :from-zero true})β a point of twice the value covers twice the ink.(scale pose :fill :log)β log-spaced tile fill.(scale pose :shape {:type :categorical :domain [...]})β shape legend order.(scale pose :shape {:values [:cross :plus]})β pick the symbols.
Log scale:
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width) (pj/scale :x :log))Fixed domain:
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width) (pj/scale :x {:domain [3 9]}))Log scale on an appearance aesthetic (:size, :alpha, :fill, or :color):
(-> {:user [:a :b :c] :n [10 100 1000]}
(pj/lay-point :user :n {:size :n :x-type :categorical})
(pj/scale :size :log))What a size spans, and how the values spread across it β :range in the quantity the mark draws (a radius here), :by the method, and :from-zero to make the ink proportional to the value:
(-> {:user [:a :b :c] :n [10 100 1000]}
(pj/lay-point :user :n {:size :n :x-type :categorical})
(pj/scale :size {:range [3 16] :by :area :from-zero true}))Shape symbols on a discrete aesthetic β :domain orders the categories, :values picks the markers:
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width {:shape :species})
(pj/scale :shape {:domain ["virginica" "versicolor" "setosa"]
:values [:cross :plus :diamond]}))shape-symbols
The marker symbols a categorical :shape mapping draws with, in the order they are assigned to categories. A plot with more categories than this repeats a symbol, so two categories cannot be told apart; that warns at plan time. Pass a selection of these as :values to (pj/scale pose :shape {:values [...]}) to choose them yourself.
pj/shape-symbols[:circle :square :triangle :diamond :triangle-down :plus :cross]Custom tick labels on a numeric axis β pair :breaks with :tick-labels:
(-> (for [d (range 1 8)] {:day d :v (mod d 3)})
(pj/lay-point :day :v)
(pj/scale :x {:type :linear
:breaks [1 2 3 4 5 6 7]
:tick-labels ["Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun"]}))Faceting
facet
[pose col]
[pose col direction]
Facet a pose by a column. direction is :col (default, horizontal row) or :row (vertical column). Faceting is plot-level β every panel is faceted the same way. Composite poses are not supported yet.
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width {:color :species})
(pj/facet :species))facet-grid
[pose col-col row-col]
Facet a pose by two columns (2D grid). Faceting is plot-level β every panel is faceted the same way. Composite poses are not supported yet.
(-> (rdatasets/reshape2-tips)
(pj/lay-point :total-bill :tip {:color :sex})
(pj/facet-grid :smoker :sex))Composition
arrange
[plots]
[plots opts]
Arrange multiple leaf poses in a grid. Returns a composite pose that renders through the compositor via membrane β so :svg, :bufimg, and any other membrane target work uniformly.
Inputs must be leaf poses. Pre-rendered hiccup is not accepted; build your own [:div ...] if you need to combine already-rendered values outside the library.
Opts:
:colsβ explicit column count (default: min(4, n-plots)).:titleβ centered title band above the grid.:widthβ total composite width.:heightβ total composite height.:share-scalesβ subset of#{:x :y}shared across cells (default:#{}).(arrange [fr-a fr-b])β 1x2 row.(arrange [fr-a fr-b fr-c] {:cols 2 :width 900})β 2x2 grid (wraps).(arrange [[fr-a fr-b] [fr-c fr-d]])β explicit 2x2 grid.
(pj/arrange [(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width {:color :species})
(pj/options {:width 250 :height 200}))
(-> (rdatasets/datasets-iris)
(pj/lay-point :petal-length :petal-width {:color :species})
(pj/options {:width 250 :height 200}))]
{:cols 2})Rendering
plot
[pose]
[pose opts]
Render a pose to a figure. The format keyword in the poseβs :opts ({:format :svg} β default; {:format :bufimg} for raster PNG via Java2D; or any other registered backend) selects which membrane->plot defmethod runs.
On a composite pose, leaves are rendered individually and tiled via the layout in the resolved chrome, in the same chosen format. The pose flows through the canonical pose -> draft -> plan -> membrane -> plot pipeline for both leaf and composite shapes. pj/plot is a literal composition of the public atomic steps:
(let [pose (->pose x) opts (:opts pose {}) fmt (or (:format opts) :svg)] (-> pose pose->draft draft->plan (plan->membrane opts) (membrane->plot fmt opts)))
Plan-derived dimensions ride as record fields on the membrane (accessed via membrane.ui/width/membrane.ui/height); the title rides as :plotje/title. membrane->plot reads them from there.
Raw data (a dataset or a bare collection of values) is given a default mapping first, exactly as pj/pose would, so (plot data) renders the inferred default instead of a blank figure.
(plot pose)(plot pose {:width 800 :title "My Plot"})(plot pose {:format :bufimg})β returns a BufferedImage.
See the Customization notebook for options (title, theme, tooltip, brush, legend position, palette).
(-> tiny
(pj/lay-point :x :y))pj/plot (and the other terminal steps pj/save, pj/draft, pj/plan, pj/membrane) also accept raw data directly, giving it a default mapping first β no explicit pj/pose needed:
(pj/plot {:height [150 160 170 175]
:weight [50 60 72 78]})options
[pose opts]
Set plot-level options (title, labels, width, height, etc.). Nested maps (e.g. :theme) are deep-merged. :width and :height are coerced to long (rounded) so the plan carries integer dimensions through to render. On a composite pose the options attach to the root so every descendant leaf inherits them at plan time.
Set render options on a pose:
(-> tiny
(pj/lay-point :x :y)
(pj/options {:width 400 :height 200 :title "Small Plot"}))Predicates
pose?
[x]
Return true if x is a pose-shaped plain map (a map carrying at least one of :layers or :poses).
Check whether a value is a pose (leaf or composite):
(pj/pose? (-> tiny (pj/pose :x :y) pj/lay-point))trueplan?
[x]
Return true if x is a plan (leaf or composite) β the resolved geometry returned by pj/plan.
Check whether a value is a plan (from pj/plan):
(pj/plan? (pj/plan (pj/lay-point tiny :x :y)))trueleaf-plan?
[x]
Return true if x is a leaf plan (single-pose resolved geometry).
Check whether a plan is a leaf (single-panel resolved geometry). A non-composite plan from a leaf pose is a leaf plan:
(pj/leaf-plan? (pj/plan (pj/lay-point tiny :x :y)))truecomposite-plan?
[x]
Return true if x is a composite plan (a tree of sub-plots with shared chrome).
Check whether a plan is a composite (a tree of sub-plots, returned by pj/plan on a composite pose like one from pj/arrange):
(pj/composite-plan?
(pj/plan (pj/arrange [(pj/lay-point tiny :x :y)
(pj/lay-point tiny :x :y)])))truedraft?
[x]
Return true if x is a draft β the intermediate representation produced by pj/pose->draft (and so by pj/draft). A draft is either a LeafDraft record (leaf pose) or a CompositeDraft record (composite pose). Used by cross-stage misuse guards on pj/plan and pj/plot.
Check whether a value is a draft (from pj/draft). True for both LeafDraft records and CompositeDraft records:
(pj/draft? (pj/draft (pj/lay-point tiny :x :y)))trueleaf-draft?
[x]
Return true if x is a leaf draft (a LeafDraft record carrying :layers β a vector of layer maps β and :opts β the pose-level options that flow into the plan stage).
Check whether a draft is a leaf (a LeafDraft record carrying :layers and :opts, returned by pj/draft on a leaf pose):
(pj/leaf-draft? (pj/draft (pj/lay-point tiny :x :y)))truecomposite-draft?
[x]
Return true if x is a composite draft (a tree of sub-drafts with shared chrome-spec, returned by pj/draft on a composite pose).
Check whether a draft is a composite (a tree of sub-drafts, returned by pj/draft on a composite pose):
(pj/composite-draft?
(pj/draft (pj/arrange [(pj/lay-point tiny :x :y)
(pj/lay-point tiny :x :y)])))trueplan-layer?
[x]
Return true if x is a plan-layer (resolved geometry for one mark).
Check whether a value is a resolved plan layer:
(pj/plan-layer? (first (:layers (first (:panels (pj/plan (pj/lay-point tiny :x :y)))))))truelayer-type?
[x]
Return true if x is a layer type (mark + stat + position bundle from the registry).
Check whether a value is a registered layer-type map:
(pj/layer-type? (pj/layer-type-lookup :point))truemembrane?
[x]
Return true if x is a PlotjeMembrane β the value returned by pj/plan->membrane and pj/membrane. A PlotjeMembrane is a Membrane UI component (implements IOrigin, IBounds, IChildren) carrying the rendered drawables and plan-derived width/height; the plot title rides as :plotje/title.
Check whether a value is a PlotjeMembrane β the value returned by pj/plan->membrane and pj/membrane:
(pj/membrane? (pj/membrane (pj/lay-point tiny :x :y)))trueInspection
draft
[pose]
[pose opts]
Resolve raw input into a draft. Literal composition of the atomic steps: (-> x ->pose pose->draft). The 2-arity folds opts into the pose with pj/options first, mirroring pj/plan and pj/plot: (-> x ->pose (options opts) draft).
Raw data (a dataset or a bare collection of values) is given a default mapping first, exactly as pj/pose would, so (draft data) works without an explicit pj/pose call.
For a leaf pose, returns a LeafDraft record (:layers is a vector of flat maps, one per applicable layer with merged scope; :opts carries the pose-level options that flow into the plan stage). For a composite pose, returns a CompositeDraft carrying per-leaf drafts (each contextualized β shared-scale domains injected, suppress-* flags applied), the resolved chrome geometry, and the layout (path -> rect).
(draft pose)(draft pose {:width 800 :title "Plot"})
Flatten a pose into a draft β a LeafDraft record holding :layers (one map per applicable layer, with all scope merged) and :opts (pose-level options). Useful for inspecting exactly what the plan stage will consume:
(-> (rdatasets/datasets-iris)
(pj/pose :sepal-length :sepal-width)
pj/lay-point
pj/draft
kind/pprint){:layers
[{:x :sepal-length,
:y :sepal-width,
:mark :point,
:stat :identity,
:layer-type :point,
:data
https://vincentarelbundock.github.io/Rdatasets/csv/datasets/iris.csv [150 6]:
| :rownames | :sepal-length | :sepal-width | :petal-length | :petal-width | :species |
|----------:|--------------:|-------------:|--------------:|-------------:|-----------|
| 1 | 5.1 | 3.5 | 1.4 | 0.2 | setosa |
| 2 | 4.9 | 3.0 | 1.4 | 0.2 | setosa |
| 3 | 4.7 | 3.2 | 1.3 | 0.2 | setosa |
| 4 | 4.6 | 3.1 | 1.5 | 0.2 | setosa |
| 5 | 5.0 | 3.6 | 1.4 | 0.2 | setosa |
| 6 | 5.4 | 3.9 | 1.7 | 0.4 | setosa |
| 7 | 4.6 | 3.4 | 1.4 | 0.3 | setosa |
| 8 | 5.0 | 3.4 | 1.5 | 0.2 | setosa |
| 9 | 4.4 | 2.9 | 1.4 | 0.2 | setosa |
| 10 | 4.9 | 3.1 | 1.5 | 0.1 | setosa |
| ... | ... | ... | ... | ... | ... |
| 140 | 6.9 | 3.1 | 5.4 | 2.1 | virginica |
| 141 | 6.7 | 3.1 | 5.6 | 2.4 | virginica |
| 142 | 6.9 | 3.1 | 5.1 | 2.3 | virginica |
| 143 | 5.8 | 2.7 | 5.1 | 1.9 | virginica |
| 144 | 6.8 | 3.2 | 5.9 | 2.3 | virginica |
| 145 | 6.7 | 3.3 | 5.7 | 2.5 | virginica |
| 146 | 6.7 | 3.0 | 5.2 | 2.3 | virginica |
| 147 | 6.3 | 2.5 | 5.0 | 1.9 | virginica |
| 148 | 6.5 | 3.0 | 5.2 | 2.0 | virginica |
| 149 | 6.2 | 3.4 | 5.4 | 2.3 | virginica |
| 150 | 5.9 | 3.0 | 5.1 | 1.8 | virginica |
,
:__panel-idx 0}],
:opts {}}plan
[pose]
[pose opts]
Convert a pose into a plan. Literal composition of the atomic steps: (-> x ->pose pose->draft draft->plan). The 2-arity folds opts into the pose with pj/options first: (-> x ->pose (options opts) plan).
Raw data (a dataset or a bare collection of values) is given a default mapping first, exactly as pj/pose would, so (plan data) works without an explicit pj/pose call.
For a leaf pose, returns a Plan record with one panel per facet variant. For a composite pose, returns a CompositePlan record with :sub-plots tying each leaf path to its rect and sub-plan, plus :chrome carrying the resolved layout geometry (title-band, grid-rect, strip labels, shared-legend spec).
(plan pose)(plan pose {:title "My Plot"})
Returns the intermediate plan data structure:
(def plan1 (-> tiny
(pj/lay-point :x :y)
pj/plan))plan1{:panels
[{:coord :cartesian,
:y-domain [0.8 5.2],
:x-scale {:type :linear},
:x-domain [0.8 5.2],
:x-ticks
{:values [1.0 2.0 3.0 4.0 5.0],
:labels ["1" "2" "3" "4" "5"],
:categorical? false},
:col 0,
:layers
[{:mark :point,
:style {:opacity 0.75, :radius 3.0},
:size-scale nil,
:alpha-scale nil,
:size-drawn? nil,
:alpha-drawn? nil,
: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 2.0 3.0 4.0 5.0],
:labels ["1" "2" "3" "4" "5"],
:categorical? false},
:row 0}],
:width 600,
:height 400,
:shape-legend nil,
:caption nil,
:total-width 600.0,
:legend-position :none,
:layout-type :single,
:layout
{:subtitle-pad 0,
:legend-w 0,
: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 nil,
:panel-height 362.0,
:title nil,
:y-label "y",
:alpha-legend nil,
:x-label "x",
:subtitle nil,
:panel-width 557.5,
:size-legend nil,
:total-height 400.0,
:tooltip nil,
:margin 10}frames
[plan-or-pose]
Where a plotβs panels sit on the canvas, and how to get between data space and drawing space.
Takes a plan or a pose; a pose is planned first. Returns a map:
:canvasβ[x y width height]of the whole image, in drawing units:panelsβ one entry per panel, each carrying:row,:col,:coord,:x-domain,:y-domain,:x-scale,:y-scale,:invertible?and:frames
A panelβs :frames names two rectangles, both [x y width height] in canvas coordinates: :panel-box (the panel with its axis margin) and :drawing-area (the background inside that margin, where data marks are clipped). The canvas is reported once, at the top: it belongs to the plot rather than to any panel.
The result contains no functions, so it can be printed, compared and read back from pr-str. To map between the spaces, pass a panel entry to pj/to-drawing or pj/to-data.
For a composite, every cellβs panels report canvas coordinates, so their rectangles can be compared without further arithmetic.
This is the same computation the renderer draws with. Use it to place your own annotations beside a plot, to compose a Plotje membrane with hand-built Membrane views, or to read a pointer position back as data.
(frames my-pose)(-> my-plan frames :panels first :frames :drawing-area)
The whole result for this single-panel plot: the canvas, and one entry per panel with its position in the grid, its domains and scale specs, whether it can be mapped back to data, and its two frames.
(-> plan1 pj/frames kind/pprint){:canvas [0.0 0.0 600.0 400.0],
:panels
[{:row 0,
:col 0,
:coord :cartesian,
:x-domain [0.8 5.2],
:y-domain [0.8 5.2],
:x-scale {:type :linear},
:y-scale {:type :linear},
:invertible? true,
:frames
{:panel-box [42.5 0.0 557.5 362.0],
:drawing-area [52.5 10.0 537.5 342.0]}}]}The canvas is reported once rather than on every panel: it belongs to the plot. Each panelβs rectangles are given in canvas coordinates, so they nest inside it and can be compared with each other directly. That holds for a composite too, where the panels come from separate sub-plans β here a 700-wide image of two cells, whose panel boxes start at different x and both end inside the canvas:
(let [f (pj/frames (pj/arrange [(pj/lay-point tiny :x :y)
(pj/lay-line tiny :x :y)]
{:width 700 :height 300}))
[_ _ cw ch] (:canvas f)
boxes (mapv #(-> % :frames :panel-box) (:panels f))
inside? (fn [[x y w h]] (and (>= x 0) (>= y 0)
(<= (+ x w) cw) (<= (+ y h) ch)))]
{:canvas (:canvas f)
:panel-boxes boxes
:every-box-inside-the-canvas (every? inside? boxes)
:panel-rectangle-keys (mapv #(vec (keys (:frames %))) (:panels f))}){:canvas [0.0 0.0 700.0 300.0],
:panel-boxes [[42.5 0.0 307.5 262.0] [392.5 0.0 307.5 262.0]],
:every-box-inside-the-canvas true,
:panel-rectangle-keys
[[:panel-box :drawing-area] [:panel-box :drawing-area]]}The result contains no functions, so it can be printed, compared and read back from pr-str. The two mappings below take a panel entry as their first argument.
to-drawing
[panel x y]
[panel data]
Where data values land on the canvas, for one panel of pj/frames.
Takes a panel entry β an element of (:panels (frames plot)) β and either one x and y, or a dataset of them with :x and :y columns. The dataset arity maps whole columns and builds the panelβs scales once, so it is the one to reach for when placing many of them.
(to-drawing panel 3.2 21.0)returns[x y]in canvas coordinates(to-drawing panel {:x [3.2 4.0] :y [21.0 18.5]})returns a dataset with the same two column names, now in canvas coordinates
A dataset rather than a collection of pairs because the two coordinates of a point share one index space, which a dataset states and two loose sequences only promise. Anything tc/dataset coerces works.
The result is in canvas coordinates, measured from the top left of the whole image. A {:in :drawing-area} layer measures from the drawing areaβs own corner instead, so drawing these coordinates back means subtracting that corner first.
A pose, a plan or the whole frames map in the panelβs place is refused, with a message naming which of them it got and the call that reaches a panel entry from there.
A categorical axis is a band scale: it has a place for each of its categories and none between them. Asking it for anything else β a category the axis does not carry, or a fractional place such as 2.5 β throws, naming the value and the categories it could have been. Under :coord :flip the arguments stay in data order, even though a panel entryβs :x-domain and :y-domain describe the drawn axes and the flip has already swapped those.
(pj/to-drawing (-> plan1 pj/frames :panels first) 2 5)[199.0909090909091 25.54545454545456]to-data
[panel cx cy]
[panel data]
What data values the canvas coordinates name, for one panel of pj/frames. The inverse of pj/to-drawing. An interaction reads this direction: which value is under the pointer, which range a selection covers.
Throws under a coordinate system with no inverse. :polar maps x and y together to an angle and a radius, so a canvas position there does not name one pair of data values. A panel entry reports which case it is in :invertible?.
(to-data panel 412.0 88.5)returns[x y]in data values(to-data panel {:x [412.0] :y [88.5]})returns a dataset with the same two column names, now in data values
A continuous axis answers with numbers, so its column is :float64. A categorical axis answers with the category whose band holds the coordinate, so its column holds those.
A pose, a plan or the whole frames map in the panelβs place is refused, with a message naming which of them it got and the call that reaches a panel entry from there.
Many positions at once go in and come back as a dataset:
(pj/to-drawing (-> plan1 pj/frames :panels first)
{:x [2 3] :y [5 6]})_unnamed [2 2]:
| :x | :y |
|---|---|
| 199.09090909 | 25.54545455 |
| 321.25000000 | -52.18181818 |
A position survives the round trip:
(let [panel (-> plan1 pj/frames :panels first)]
(->> (pj/to-drawing panel 2 5)
(apply pj/to-data panel)
(mapv #(Math/round (double %)))))[2 5]svg-summary
[svg-or-pose]
[svg-or-pose theme]
Extract structural summary from SVG hiccup for testing. Returns a map with :width, :height, :panels, :points, :lines, :dashed-lines, :dash-patterns, :polygons, :tiles, :visible-tiles, and :texts β useful for asserting plot structure. Accepts SVG hiccup or a pose (auto-renders to SVG first).
(svg-summary (plot fr))β summary of rendered SVG.(svg-summary my-pose)β auto-renders pose (leaf or composite).
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width {:color :species}) pj/svg-summary){:sizes #{3.0},
:bold-texts 0,
:italic-texts 0,
:dash-patterns #{},
:texts
["sepal width"
"sepal length"
"species"
"setosa"
"versicolor"
"virginica"
"4.5"
"5.0"
"5.5"
"6.0"
"6.5"
"7.0"
"7.5"
"8.0"
"2.0"
"2.5"
"3.0"
"3.5"
"4.0"
"4.5"],
:width 600,
:lines 0,
:colors #{"none" "rgb(228,26,28)" "rgb(55,126,184)" "rgb(77,175,74)"},
:points 150,
:label-boxes 0,
:alphas #{0.75},
:dashed-lines 0,
:clips 1,
:tiles 0,
:polygons 0,
:visible-tiles 0,
:panels 1,
:height 400,
:shapes #{:rect}}valid-pose?
[pose]
Check if a pose conforms to the Malli schema.
(valid-pose? (lay-point data :x :y))β true if valid.
The schema is structural and deliberately permissive: it says what shape a pose has, not whether the columns it names are in the data. A pose built by pj/pose, pj/lay-*, pj/options, pj/facet, pj/arrange, pj/coord or pj/scale conforms by construction; the check is for a pose that has been reached into and changed, which is the one place the constructors cannot speak for.
(pj/valid-pose? (pj/lay-point tiny :x :y))trueexplain-pose
[pose]
Explain why a pose does not conform to the Malli schema. Returns nil if valid, or a Malli explanation map if invalid.
(explain-pose (assoc (lay-point data :x :y) :layers {}))
(pj/explain-pose (pj/lay-point tiny :x :y))nilvalid-plan?
[plan]
Check if a plan conforms to the Malli schema.
(valid-plan? (plan pose))β true if valid.
(pj/valid-plan? plan1)trueexplain-plan
[plan]
Explain why a plan does not conform to the Malli schema. Returns nil if valid, or a Malli explanation map if invalid.
(explain-plan (plan pose))
(pj/explain-plan plan1)nilPipeline
The pipeline is a composition of single-step transitions. The user-facing functions (pj/draft, pj/plan, pj/membrane, pj/plot, pj/save) run the chain up through their stage: each lifts raw input with pj/->pose, applies pj/infer-mapping (a default mapping when the input is a bare dataset, a no-op on a built pose), then runs the single-step transitions. So each of them accepts raw data as well as a pose. The Architecture chapter shows the full composition.
Each step is independently callable, so you can stop the pipeline at any point to inspect the intermediate value.
membrane
[pose]
[pose opts]
Resolve a pose into a PlotjeMembrane. Literal composition of the atomic steps: (let [pose (->pose x), opts (:opts pose {})] (-> pose pose->draft draft->plan (plan->membrane opts))). The let lifts the pose once so the chain can pluck pose-level opts and pass them to plan->membrane. The 2-arity folds opts into the pose with pj/options first.
Returns a PlotjeMembrane β a Membrane UI component (implements IOrigin, IBounds, IChildren) carrying the rendered drawables plus plan-derived width and height; the title, when set, rides as :plotje/title. Render-time options (:tooltip, :theme, :color-values, :color-range, :color-midpoint) ride along on the poseβs :opts and reach plan->membrane through this call.
Useful for exploring rendering targets beyond the SVG and Java2D backends Plotje wires in today: any Membrane backend can consume the result of pj/membrane via the standard Membrane protocols.
Raw data (a dataset or a bare collection of values) is given a default mapping first, exactly as pj/pose would, so (membrane data) works without an explicit pj/pose call.
(membrane pose)(membrane pose {:tooltip true})
Resolve a pose into a PlotjeMembrane β a format-agnostic Membrane UI component (a record implementing IOrigin, IBounds, IChildren). Useful for exploring rendering targets beyond the SVG and Java2D backends Plotje wires in today, and for composing Plotje plots into larger Membrane interfaces. The Membranes chapter walks the recordβs anatomy and the protocols.
(let [m (pj/membrane (pj/lay-point tiny :x :y))]
{:membrane? (pj/membrane? m)
:width (membrane.ui/width m)
:height (membrane.ui/height m)
:record-keys (sort (filter keyword? (keys m)))}){:membrane? true,
:width 600,
:height 400,
:record-keys (:drawables :height :width)}->pose
[x]
[x caller]
Lift the input to a pose. The first atomic step of the pipeline. Polymorphic on input:
a pose-shaped map flows through
pose-kind(validated,*config*captured, Kindly auto-render metadata attached); idempotent on input that already carries the metadata, so repeated lifts are cheap;raw data (a dataset, vector of row maps, or column map) becomes a leaf pose with
:dataset and no mapping, run throughprepare-poseso the Kindly metadata is attached.
Throws on nil or non-collection scalars. Use (pj/pose) for an explicit empty leaf instead of passing nil.
The optional caller argument names the public-facing function shown in error messages, so users see βpj/lay-point requires dataβ¦β rather than an internal helper name. Defaults to βpj/->poseβ.
(->pose data)β raw dataset becomes a leaf pose(->pose pose)β already a pose; idempotent lift
Lift raw input to a pose. Raw data (datasets, vectors of row maps, column maps) becomes an empty leaf pose with :data set; an existing pose flows through unchanged (idempotent). The first step of the pipeline.
(pj/pose? (pj/->pose tiny))trueinfer-mapping
[fr]
Infer and attach a default mapping to a pose that carries data but no mapping yet β the fresh leaf pj/->pose produces from raw input. Position and color are taken from the first 1-3 columns (1 column to :x, 2 columns to :x and :y, 3 columns add :color).
A pose that already has a mapping, has layers, is composite, or has 4+ columns is returned unchanged, so the step is idempotent and safe to include anywhere in a pipeline. This is the step that lets raw data render a sensible default: the user-facing entry points (pj/pose 1-arity and the shortcuts pj/draft, pj/plan, pj/membrane, pj/plot, pj/save) apply it right after pj/->pose, while pj/->pose itself stays a bare structural lift.
(-> data pj/->pose pj/infer-mapping)β lift, then default map(pj/infer-mapping built-pose)β no-op on an already-mapped pose
The default-mapping step the shortcuts apply right after pj/->pose. On a fresh leaf (data, no mapping) it maps the first 1-3 columns to :x, :y, and :color:
(-> {:height [150 160 170] :weight [50 60 72]}
pj/->pose
pj/infer-mapping
:mapping){:x :height, :y :weight}It is a no-op on a pose that already carries a mapping, so it is safe anywhere in a pipeline:
(let [built (pj/lay-point tiny :x :y)]
(= (:mapping built)
(:mapping (pj/infer-mapping built))))truepose->draft
[pose]
Single-step transition: convert a pose into a draft. Dispatches on pose shape β a leaf pose becomes a LeafDraft (a record carrying :layers β a vector of one map per applicable layer with merged scope β and :opts β the pose-level options that flow into the plan stage); a composite pose becomes a CompositeDraft carrying per-leaf drafts (each contextualized with shared-scale domains and chrome-driven opt adjustments), the resolved chrome geometry, and the layout (path -> rect).
(pose->draft (pj/lay-point data :x :y))
Single-step transition: pose to draft. Dispatches on shape β leaf poses produce LeafDraft records, composite poses produce CompositeDraft records.
(pj/leaf-draft?
(pj/pose->draft (pj/lay-point tiny :x :y)))trueplan->membrane
[plan-data]
[plan-data opts]
Convert a plan into a PlotjeMembrane β a Membrane UI component carrying the rendered drawables, plan-derived width and height, and the plot title.
The 1-arity uses no rendering options. The 2-arity takes an opts map with optional :tooltip, :theme, :color-values, etc.
The result implements membrane.ui IOrigin, IBounds, and IChildren, so width and height are accessible via (membrane.ui/width m) and (membrane.ui/height m). The title, when set, rides as :plotje/title. Future per-membrane attributes use the same :plotje/* namespaced-keyword convention. The shape is captured by the PlotjeMembraneSchema in scicloj.plotje.impl.membrane.
(plan->membrane (plan fr))(plan->membrane (plan fr) {:tooltip true})
(def m1 (pj/plan->membrane plan1))(pj/membrane? m1)truevalid-membrane?
[membrane]
Check if a membrane conforms to the Malli schema.
(valid-membrane? (membrane pose))β true if valid.
(pj/valid-membrane? m1)trueexplain-membrane
[membrane]
Explain why a membrane does not conform to the Malli schema. Returns nil if valid, or a Malli explanation map if invalid.
(explain-membrane (membrane pose))
(pj/explain-membrane m1)nilmembrane->plot
[membrane-tree format opts]
Convert a PlotjeMembrane into a figure for the given format. Dispatches on format keyword; :svg is always available.
Reads width and height from the membrane via (membrane.ui/width m) / (membrane.ui/height m) (so any Membrane backend can introspect the canvas size), and the title from (:plotje/title m).
(membrane->plot (plan->membrane (plan pose)) :svg {})
(first (pj/membrane->plot m1 :svg {})):svgplan->plot
[plan format opts]
Convert a plan into a figure for the given format. Dispatches on format keyword. Each renderer is a separate namespace that registers a defmethod; :svg is always available.
(plan->plot (plan fr) :svg {})(plan->plot (plan fr) :plotly {})
(first (pj/plan->plot plan1 :svg {})):svgThe draft->* family lets the same pipeline start from a draft instead of a fully-resolved plan. Useful when you have a draft in hand (e.g. from inspection) and want to skip re-running the layer-flattening step.
draft->plan
[draft]
Single-step transition: convert a draft into a plan. Dispatches on draft shape β a LeafDraft carries :layers and pose-level :opts that flow into plan/draft->plan; a CompositeDraft goes through compositor/composite-draft->plan (which uses the chrome-spec already baked in at draft emission).
Plan-stage opts (:width, :height, :title, β¦) ride on the draft itself β on the LeafDraftβs :opts for leaves, on the CompositeDraftβs chrome-spec for composites. Set them on the pose via pj/options before drafting.
(draft->plan (draft pose))
(def draft1 (pj/draft (pj/lay-point tiny :x :y)))(pj/plan? (pj/draft->plan draft1))truedraft->membrane
[draft]
[draft opts]
Compose draft -> plan -> membrane. The 2-arity takes an opts map for plan->membrane (e.g. {:tooltip true}).
Render-stage options set on the original pose via pj/options (:theme, :color-values, β¦) ride on the draftβs :opts and form the base; any opts passed here override them per key. This keeps the explicit pipeline consistent with pj/plot, which feeds the poseβs opts into the membrane stage.
(draft->membrane (draft pose))(draft->membrane (draft pose) {:tooltip true})
(pj/membrane? (pj/draft->membrane draft1))truedraft->plot
[draft format opts]
Compose draft -> plan -> plot for the given format.
Render-stage options set on the original pose via pj/options (:theme, :color-values, β¦) ride on the draftβs :opts and form the base; the passed opts override them per key.
(draft->plot (draft pose) :svg {})(draft->plot (draft pose) :bufimg {})
(first (pj/draft->plot draft1 :svg {})):svgConfiguration
config
[]
Return the effective resolved configuration as a map. Merges: library defaults < plotje.edn < set-config! < *config*. Useful for inspecting which values are in effect.
(config)β show current resolved config.
(pj/config){:strict false,
:margin-multi 10,
:validate true,
:point-stroke "none",
:title-offset 18,
:panel-size 200,
:min-panel-size 20,
:label-offset 38,
:label-font-size 13,
:x-tick-spacing 60,
:thousands-separator nil,
:default-color "#333",
:width 600,
:legend-header-pad 20,
:y-tick-spacing 40,
:point-stroke-width 0,
:annotation-dash [4 3],
:decimal-separator nil,
:legend-width 100,
:legend-entry-height 18,
:theme {:bg "#E8E8E8", :grid "#F5F5F5", :font-size 11},
:bin-method :sturges,
:domain-padding 0.05,
:strip-height 16,
:point-opacity 0.75,
:line-width 2.5,
:grid-stroke-width 0.6,
:fit-text-domain true,
:strip-font-size 11,
:title-font-size 15,
:band-opacity 0.15,
:bar-opacity 0.85,
:annotation-stroke "#333",
:height 400,
:margin 10,
:point-radius 3.0}set-config!
[m]
Set global config overrides. Persists across calls until reset.
(set-config! {:color-values :dark2 :theme {:bg "#FFFFFF"}})β override the categorical colours and the background.(set-config! nil)β reset to defaults.
with-config
[config-map & body]
Execute body with thread-local config overrides. Overrides take precedence over set-config! and defaults, but plot options still win.
(with-config {:theme {:bg "#FFF"}} (plot ...))
(pj/with-config {:color-values :pastel1}
(:color-values (pj/config))):pastel1Documentation Metadata
Three maps document the option keys at each scope level.
config-key-docs
Documentation metadata for configuration keys. Maps each config key to [category description]. Use with (pj/config) to build reference tables.
(count pj/config-key-docs)44plot-option-docs
Documentation for plot-level option keys. These are accepted by pj/options, pj/plan, and pj/plot but are inherently per-plot (text content or nested config override). Maps each key to [category description].
(count pj/plot-option-docs)15layer-option-docs
Documentation for layer option keys accepted by lay- functions. Maps each key to a description string.
(count pj/layer-option-docs)54Layer Type Registry
layer-type-lookup
[k]
Look up a registered layer type by keyword. Returns the layer-type map (with :mark, :stat, :position, :doc), or nil if not found.
(layer-type-lookup :histogram)returns{:mark :bar, :stat :bin, ...}.
(pj/layer-type-lookup :smooth){:mark :line,
:stat :loess,
:accepts
[:confidence-band
:level
:bootstrap-resamples
:bandwidth
:size
:stroke-dash
:nudge-x
:nudge-y],
:doc
"Smoothed trend line β defaults to LOESS; pass {:stat :linear-model} for OLS."}registered-layer-types
[]
Return all registered layer types as a map of keyword -> layer-type map. Useful for generating documentation tables.
(count (pj/registered-layer-types))25(first (pj/registered-layer-types))[:smooth
{:mark :line,
:stat :loess,
:accepts
[:confidence-band
:level
:bootstrap-resamples
:bandwidth
:size
:stroke-dash
:nudge-x
:nudge-y],
:doc
"Smoothed trend line β defaults to LOESS; pass {:stat :linear-model} for OLS."}]Documentation Helpers
Query the self-documenting dispatch tables for any extensible concept.
stat-doc
[k]
Return the prose description for a stat keyword. Returns "(no description)" if no [:key :doc] defmethod is registered.
(stat-doc :bin)returns"Bin numerical values into ranges".
(pj/stat-doc :linear-model)"Linear model β OLS regression line + optional confidence band"mark-doc
[k]
Return the prose description for a mark keyword. Returns "(no description)" if no [:key :doc] defmethod is registered.
(mark-doc :point)returns"Filled circle".
(pj/mark-doc :point)"Filled circle"position-doc
[k]
Return the prose description for a position keyword. Returns "(no description)" if no [:key :doc] defmethod is registered.
(position-doc :dodge)returns"Shift groups side-by-side within a band".
(pj/position-doc :dodge)"Shift groups side-by-side within a band"scale-doc
[k]
Return the prose description for a scale keyword. Returns "(no description)" if no [:key :doc] defmethod is registered.
(scale-doc :linear)returns"Continuous linear mapping".
(pj/scale-doc :linear)"Continuous linear mapping"coord-doc
[k]
Return the prose description for a coordinate type keyword. Returns "(no description)" if no [:key :doc] defmethod is registered.
(coord-doc :polar)returns"Radial mapping: x->angle, y->radius".
(pj/coord-doc :cartesian)"Standard x-right, y-up mapping"membrane-mark-doc
[k]
Return the prose description for how a mark renders to membrane drawables. Returns "(no description)" if no [:key :doc] defmethod is registered.
(membrane-mark-doc :point)returns"Translated colored rounded-rectangles".
(pj/membrane-mark-doc :point)"Translated colored rounded-rectangles"Export
save
[pose path]
[pose path opts]
Save a plot to a file. Format resolution, in precedence order: 1. :format in the 3-arity opts map wins (must be :svg or :png). 2. :format on the poseβs :opts (:svg or :png; legacy :bufimg is translated to :png). 3. Otherwise inferred from the path extension (.svg -> :svg, .png -> :png). 4. Default :svg.
When the resolved format and the path extension disagree, prints a warning β the file still gets the bytes the resolved format produces, but the extension is misleading.
The save vocabulary names the file format. The plot vocabulary (pj/plotβs :format) names the JVM return type β :svg for hiccup, :bufimg for a Java2D BufferedImage. A pose-level :format flows into both contexts; save reinterprets :bufimg as :png because the file on disk is a PNG.
Arguments:
poseβ a pose, or raw data (a dataset or a bare collection of values), which is given a default mapping first, exactly aspj/posewould.pathβ file path (string orjava.io.File).optsβ same options as plot, but:formataccepts only:svgor:png.
Tooltip and brush interactivity are not included in saved files.
Returns the written file as a java.io.File carrying :kind/image metadata, so evaluating a pj/save call in a notebook also shows the saved chart. The file prints as its path and compares equal to a plain java.io.File on the same path, so (str (pj/save ...)) still gives the path string.
(save my-pose "plot.svg")β SVG.(save my-pose "plot.png")β inferred PNG.(save my-pose "plot.svg" {:format :png})β opts override (warns).
Save a plot to an SVG file:
(let [path (str (java.io.File/createTempFile "plotje-example" ".svg"))]
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width {:color :species})
(pj/save path {:title "Iris Export"}))
(.contains (slurp path) "<svg"))trueSave a plot to a PNG file. Extension inference picks the raster backend; the returned bytes start with the PNG magic header:
(let [path (str (java.io.File/createTempFile "plotje-example" ".png"))]
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width {:color :species})
(pj/save path))
(with-open [in (java.io.FileInputStream. path)]
(let [bs (byte-array 8)]
(.read in bs)
(mapv #(bit-and ^int % 0xFF) (vec bs)))))[137 80 78 71 13 10 26 10]Pass {:format :png} explicitly when the pathβs extension does not match the desired format, or when it is built dynamically:
(let [path (str (java.io.File/createTempFile "plotje-example" ".out"))]
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width {:color :species})
(pj/save path {:format :png}))
(with-open [in (java.io.FileInputStream. path)]
(let [bs (byte-array 4)]
(.read in bs)
(mapv #(bit-and ^int % 0xFF) (vec bs)))))[137 80 78 71]