6 Composition
Plotje’s pose substrate lets you combine whole plots into a single rendered image. A composite pose holds other poses and a layout; each sub-pose renders independently and the composite tiles them together.
This chapter walks through composition patterns from simple side-by-side arrangements to shared-scale marginal plots, using pj/arrange and explicit composite-pose maps – compactly for simple cases, with a bit of literal map construction for nested layouts.
(ns plotje-book.composition
(:require
;; Tablecloth -- dataset manipulation
[tablecloth.api :as tc]
;; Kindly -- notebook rendering protocol
[scicloj.kindly.v4.kind :as kind]
;; Plotje -- composable plotting
[scicloj.plotje.api :as pj]
;; Rdatasets -- standard datasets
[scicloj.metamorph.ml.rdatasets :as rdatasets]))Side-by-Side via pj/arrange
The simplest composite: two independent poses, placed next to each other. pj/arrange takes a vector of poses and returns a composite. Each sub-pose has its own data, mapping, layers, and options. Coming from R, this is the same shape as patchwork’s p1 | p2 operator or cowplot’s plot_grid(p1, p2).
(pj/arrange
[(-> (rdatasets/datasets-iris) (pj/lay-point :sepal-length :sepal-width {:color :species}))
(-> (rdatasets/datasets-iris) (pj/lay-point :petal-length :petal-width {:color :species}))])Pass {:cols 1} for a stacked arrangement (one column means each pose goes on its own row):
(pj/arrange
[(-> (rdatasets/datasets-iris) (pj/lay-point :sepal-length :sepal-width {:color :species}))
(-> (rdatasets/datasets-iris) (pj/lay-point :petal-length :petal-width {:color :species}))]
{:cols 1})pj/arrange divides space equally among its sub-poses. For unequal splits (e.g., give the first panel twice the space of the second), construct the composite as an explicit map; the next section shows how.
Explicit Composite Poses
Under pj/arrange there is a plain-map composite pose. You can construct one directly when you need finer control – unequal weights, shared scales, or (in future work) non-plot leaves like text panels and KPIs.
An explicit :layout accepts :direction (:horizontal or :vertical) and :weights (one weight per sub-pose). Here the first panel gets twice the space of the second:
(def weighted
(pj/pose
{:layout {:direction :horizontal :weights [2 1]}
:poses [{:mapping {:x :sepal-length :y :sepal-width}
:layers [{:layer-type :point}]}
{:mapping {:x :petal-length :y :petal-width}
:layers [{:layer-type :point}]}]
:data (rdatasets/datasets-iris)}))pj/pose accepts a literal composite map and tags it for notebook auto-render – the same pose value the threaded API produces:
weightedAnd printed, showing the composite’s structure – :layout with direction and weights at the top, then each sub-pose with its own :mapping and :layers, and the outer :data dataset:
(kind/pprint weighted){:layout {:direction :horizontal, :weights [2 1]},
:poses
[{:mapping {:x :sepal-length, :y :sepal-width},
:layers [{:layer-type :point}]}
{:mapping {:x :petal-length, :y :petal-width},
:layers [{: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 |
}The outer :data is inherited by both sub-poses. Each sub-pose has its own :mapping and :layers, and need not repeat the dataset. Subsequent examples in this chapter follow the same shape and show only the rendered plot.
A Marginal-Plot Pattern
The classic “scatter with top density” – a distribution strip above the main plot – is a vertical composite with shared x:
(def marginal
(pj/pose
{:share-scales #{:x}
:layout {:direction :vertical :weights [1 3]}
:poses [{:mapping {:x :sepal-length}
:layers [{:layer-type :density}]}
{:mapping {:x :sepal-length :y :sepal-width :color :species}
:layers [{:layer-type :point}]}]
:data (rdatasets/datasets-iris)}))marginalThe top panel’s density curve aligns with the scatter’s x-axis. Each panel retains its own y-axis because :share-scales here contains only :x.
A Small Dashboard
Composite poses can combine heterogeneous chart types. Here is a dashboard-style 2x2 layout: a histogram of sepal length, a boxplot of sepal width by species, a scatter of petal dimensions, and a density of petal length.
Each cell is built as its own leaf pose, then pj/arrange takes rows of cells and produces the 2x2 grid:
(def dashboard
(pj/arrange
[[(-> (rdatasets/datasets-iris) (pj/lay-histogram :sepal-length))
(-> (rdatasets/datasets-iris) (pj/lay-boxplot :species :sepal-width {:color :species}))]
[(-> (rdatasets/datasets-iris) (pj/lay-point :petal-length :petal-width {:color :species}))
(-> (rdatasets/datasets-iris) (pj/lay-density :petal-length {:color :species}))]]))dashboardFour panels, each its own layer type. pj/arrange stacks the rows vertically; each row is laid out horizontally. Every cell is a leaf pose – pj/arrange does not accept composite cells.
Notes on the Current Implementation
A few details about how composition renders today, in case they matter for a layout you’re sketching:
Each leaf draws its own axes, labels, and ticks. Shared scales align the data ranges across panels, but these decorations are per-leaf in
:horizontaland:verticallayouts, so the marginal example renders the x-axis label on both the strip and the scatter. (Matrix layouts – the SPLOM grid in particular – replace per-leaf x/y labels with shared strip labels, and SPLOM additionally suppresses ticks on interior cells.)Plot-area edges may not line up across composite siblings, since each leaf reserves its own padding for axes and labels – two sub-poses with different label lengths can produce visibly different panel widths.
Legends merge when sibling sub-poses agree on an aesthetic. If every leaf maps the same aesthetic identically (e.g.,
:color :speciesin all cells), the compositor renders a single shared legend at composite level. When the mappings disagree – or when only some leaves carry the aesthetic, as in the dashboard above – each leaf with that aesthetic renders its own legend.Multi-row layouts go through
pj/arrange. Bothpj/arrangeand the explicit-map form accept only leaf cells; a row of rows or column of rows is built by passing nested vectors of leaves topj/arrange(the dashboard example above shows the shape). Nested composites (a sub-pose that is itself composite) are out of scope today.
What’s Next
Options and Scopes – the taxonomy of layer options, plot options, and configuration (next chapter in Foundations)
Faceting – panel splits by a categorical column (a data-driven composite, covered in How-to)
Gallery – more composition examples alongside single-plot chart types