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.
(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
{:data (rdatasets/datasets-iris)
: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}]}]}))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){: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 |
,
: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}]}]}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
{:data (rdatasets/datasets-iris)
: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}]}]}))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
(let [iris (rdatasets/datasets-iris)]
(pj/arrange
[[(-> iris (pj/lay-histogram :sepal-length))
(-> iris (pj/lay-boxplot :species :sepal-width {:color :species}))]
[(-> iris (pj/lay-point :petal-length :petal-width {:color :species}))
(-> 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 carries its own chrome โ axes, labels, ticks. Shared scales align the data ranges across panels, but the visible chrome is per-leaf, so a marginal plot shows the x-axis label on both the main and the marginal panel.
Plot-area edges may not line up across composite siblings, since each leaf computes its own chrome padding โ two sub-poses with different label lengths can produce visibly different panel widths.
Each sub-pose produces its own legend โ if you map
:colorin two sibling sub-poses, the rendered plot shows two legends rather than a merged one.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