24 Faceting
Faceting splits data into subsets and draws each in its own panel, making it easy to compare patterns across groups.
A facet is a mapping: :col and :row are aesthetics, and pj/facet is the call that writes one. Everything else follows from that β a facet reaches into a composite, a cell can override the facet it inherits, and several columns under :col unite into one distinction. The sections below teach those ideas, and the Examples section at the end shows faceted plots of several kinds.
(ns plotje-book.faceting
(:require
;; Tablecloth -- dataset manipulation
[tablecloth.api :as tc]
;; Rdatasets -- standard datasets
[scicloj.metamorph.ml.rdatasets :as rdatasets]
;; Kindly -- notebook rendering protocol
[scicloj.kindly.v4.kind :as kind]
;; Plotje -- composable plotting
[scicloj.plotje.api :as pj]))Facet Wrap
pj/facet splits a pose into panels by one categorical column. The default direction is :col β a horizontal row of panels:
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width {:color :species})
(pj/facet :species))Each species gets its own panel with a strip label on top. Scales are shared by default β all panels use the same x and y range, making direct comparison easy.
Vertical Facet and Facet Grid
Pass :row as the direction for a vertical column of panels:
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width {:color :species})
(pj/facet :species :row))pj/facet-grid splits by two columns β one for rows, one for columns:
(-> (rdatasets/reshape2-tips)
(pj/lay-point :total-bill :tip {:color :sex})
(pj/facet-grid :smoker :sex))Row labels appear on the right, column labels on top.
Faceting is a mapping
The panel aesthetics :col and :row give each value of a column a panel of its own, told apart by a strip label β where :color tells the marks apart by colour on one panel. They are documented here, where they are written, rather than beside the layer options, because no pj/lay-* call accepts one.
(kind/table
{:column-names [:aesthetic :what-it-reads]
:row-vectors (mapv (fn [[k doc]] [k doc])
(sort-by (comp str key) pj/panel-aesthetic-docs))})| aesthetic | what-it-reads |
|---|---|
| col | A column of the pose's data, or a vector of them, giving each value a panel of its own, laid out across. A vector unites the columns into a compound key, so a panel is drawn for each combination the data holds. Written by pj/facet, and by pj/facet-grid for the across direction. |
| row | A column of the pose's data, or a vector of them, giving each value a panel of its own, laid out down. The same readings as :col. Written by (pj/facet my-pose col :row), and by pj/facet-grid for the down direction. |
pj/facet writes :col into the poseβs mapping, beside :x and :y. Here is the plot, and then the pose that draws it.
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width)
(pj/facet :species))(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width)
(pj/facet :species)
kind/pprint){: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 |
,
:mapping {:x :sepal-length, :y :sepal-width, :col :species}}(pj/facet my-pose col :row) writes :row instead, and pj/facet-grid writes both.
(-> (rdatasets/ggplot2-mpg)
(pj/lay-point :displ :hwy)
(pj/facet-grid :drv :cyl)
kind/pprint){:layers [{:layer-type :point}],
:data
https://vincentarelbundock.github.io/Rdatasets/csv/ggplot2/mpg.csv [234 12]:
| :rownames | :manufacturer | :model | :displ | :year | :cyl | :trans | :drv | :cty | :hwy | :fl | :class |
|----------:|---------------|------------|-------:|------:|-----:|------------|------|-----:|-----:|-----|------------|
| 1 | audi | a4 | 1.8 | 1999 | 4 | auto(l5) | f | 18 | 29 | p | compact |
| 2 | audi | a4 | 1.8 | 1999 | 4 | manual(m5) | f | 21 | 29 | p | compact |
| 3 | audi | a4 | 2.0 | 2008 | 4 | manual(m6) | f | 20 | 31 | p | compact |
| 4 | audi | a4 | 2.0 | 2008 | 4 | auto(av) | f | 21 | 30 | p | compact |
| 5 | audi | a4 | 2.8 | 1999 | 6 | auto(l5) | f | 16 | 26 | p | compact |
| 6 | audi | a4 | 2.8 | 1999 | 6 | manual(m5) | f | 18 | 26 | p | compact |
| 7 | audi | a4 | 3.1 | 2008 | 6 | auto(av) | f | 18 | 27 | p | compact |
| 8 | audi | a4 quattro | 1.8 | 1999 | 4 | manual(m5) | 4 | 18 | 26 | p | compact |
| 9 | audi | a4 quattro | 1.8 | 1999 | 4 | auto(l5) | 4 | 16 | 25 | p | compact |
| 10 | audi | a4 quattro | 2.0 | 2008 | 4 | manual(m6) | 4 | 20 | 28 | p | compact |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 224 | volkswagen | new beetle | 2.0 | 1999 | 4 | manual(m5) | f | 21 | 29 | r | subcompact |
| 225 | volkswagen | new beetle | 2.0 | 1999 | 4 | auto(l4) | f | 19 | 26 | r | subcompact |
| 226 | volkswagen | new beetle | 2.5 | 2008 | 5 | manual(m5) | f | 20 | 28 | r | subcompact |
| 227 | volkswagen | new beetle | 2.5 | 2008 | 5 | auto(s6) | f | 20 | 29 | r | subcompact |
| 228 | volkswagen | passat | 1.8 | 1999 | 4 | manual(m5) | f | 21 | 29 | p | midsize |
| 229 | volkswagen | passat | 1.8 | 1999 | 4 | auto(l5) | f | 18 | 29 | p | midsize |
| 230 | volkswagen | passat | 2.0 | 2008 | 4 | auto(s6) | f | 19 | 28 | p | midsize |
| 231 | volkswagen | passat | 2.0 | 2008 | 4 | manual(m6) | f | 21 | 29 | p | midsize |
| 232 | volkswagen | passat | 2.8 | 1999 | 6 | auto(l5) | f | 16 | 26 | p | midsize |
| 233 | volkswagen | passat | 2.8 | 1999 | 6 | manual(m5) | f | 18 | 26 | p | midsize |
| 234 | volkswagen | passat | 3.6 | 2008 | 6 | auto(s6) | f | 17 | 26 | p | midsize |
,
:mapping {:x :displ, :y :hwy, :col :drv, :row :cyl}}Any mapping value may be written as a map naming its source, and a panel aesthetic is no exception. These two draw the same plot.
(-> (rdatasets/datasets-iris)
(pj/pose {:x :sepal-length :y :sepal-width :col {:column :species}})
(pj/lay-point))What the scope rules give
Mappings flow downward: a mapping written on a pose reaches every layer and every sub-pose below it, and a sub-pose that writes its own key overrides what it inherits. A panel aesthetic is a mapping, so all of that applies to faceting.
A composite can be faceted
A facet written on an arranged pose divides every cell of it.
(-> (pj/arrange [[(pj/pose (rdatasets/datasets-iris) :sepal-length :sepal-width)]
[(pj/pose (rdatasets/datasets-iris) :petal-length :petal-width)]])
(pj/lay-point)
(pj/facet :species)
(pj/options {:width 700 :height 560}))Two cells, three species, six panels. pj/arrange takes one vector per row, so [[a] [b]] stacks the two cells and each has the full width for its three panels.
One cell can be faceted differently from another
A cell that writes its own :col overrides the one it inherits, which is the rule for every mapping key. Here the top cell is divided by species and the bottom cell is not divided at all.
(-> (pj/arrange [[(-> (pj/pose (rdatasets/datasets-iris) :sepal-length :sepal-width)
(pj/facet :species))]
[(pj/pose (rdatasets/datasets-iris) :petal-length :petal-width)]])
(pj/lay-point)
(pj/options {:width 700 :height 560}))Three panels in the top row and one in the bottom row.
A facet multiplies the panels a pose already draws
A pose whose layers name different columns draws a panel for each place. Faceting such a pose draws every one of those panels once per value the column holds: the facet is the outer division.
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width)
(pj/lay-point :petal-length :petal-width)
(pj/facet :species)
(pj/options {:width 900 :height 480}))A marginal follows the division
pj/marginal builds a fresh leaf rather than a descendant, so the scope rules do not reach it on their own. It carries the panel aesthetics across itself, giving a faceted main panel a marginal above each of its panels. The strip label is drawn once per column, at the top.
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width)
(pj/facet :species)
(pj/marginal :top)
(pj/options {:width 900 :height 480}))Several columns as one key
Under some aesthetics, several columns in a vector are one distinction made of those columns united β a compound key. That is what {:group [:a :b]} means, and the panel aesthetics read a vector the same way. pj/compound-key-aesthetics names the set, and it is the set the check reads, so an aesthetic outside it reports several columns where one goes rather than uniting them:
(pj/compound-key-aesthetics)#{:group :col :row}The table below holds three of the four combinations of :part and :dimension: there is no petal width row.
(def measures
(tc/dataset {:part ["sepal" "sepal" "sepal" "sepal" "petal" "petal"]
:dimension ["length" "length" "width" "width" "length" "length"]
:t [1 2 1 2 1 2]
:v [1.0 2.0 1.5 2.5 2.0 3.0]}))measures_unnamed [6 4]:
| :part | :dimension | :t | :v |
|---|---|---|---|
| sepal | length | 1 | 1.0 |
| sepal | length | 2 | 2.0 |
| sepal | width | 1 | 1.5 |
| sepal | width | 2 | 2.5 |
| petal | length | 1 | 2.0 |
| petal | length | 2 | 3.0 |
:color is outside the set, because a mark has one colour. A vector written there is reported rather than united:
(try
(-> measures
(pj/lay-point :t :v {:color [:part :dimension]}))
(catch clojure.lang.ExceptionInfo e
(ex-message e)))"lay-point :color was given several columns, [:part :dimension]. Several columns where one goes are read as several series, which belongs on :x or :y -- the aesthetics that name what a mark is drawn from. Written there, the key column the pivot invents is mapped to :color on its own."A compound facet draws one panel per combination the data holds, labelled by each columnβs value in turn.
(-> measures
(pj/lay-point :t :v)
(pj/facet [:part :dimension]))pj/facet-grid crosses two distinctions and fills the rectangle, so the combination the data does not hold is drawn as an empty panel.
(-> measures
(pj/lay-point :t :v)
(pj/facet-grid :part :dimension))Free Scales (Independent Axis Ranges)
By default all panels share the same axis ranges. Use the :scales option to let axes vary per panel.
Shared (default) β all panels carry the same x and y ranges:
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width {:color :species})
(pj/facet :species)
(pj/options {:scales :shared}))Free y β each panel has its own y-range:
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width {:color :species})
(pj/facet :species)
(pj/options {:scales :free-y}))Other values: :free-x (x per-panel, y shared), :free (both axes per-panel).
Appearance Aesthetics Across Panels
:scales frees the axes, and the axes alone. A :size or :alpha column is read against every value the plot holds, whichever panel a mark sits in, so one value is one size everywhere and the legend explains every panel at once.
Below, the left panelβs values run from 1 to 3 and the right panelβs from 4 to 10, so every mark on the left is smaller than every mark on the right:
(def per-panel
{:g ["L" "L" "L" "R" "R" "R"]
:x [1 2 3 1 2 3]
:y [1 1 1 1 1 1]
:n [1 2 3 4 7 10]})(-> per-panel
(pj/lay-point :x :y {:size :n})
(pj/facet :g))A :domain sets that one extent. Given more room than the data needs, no mark reaches either end of the range:
(-> per-panel
(pj/lay-point :x :y {:size :n})
(pj/facet :g)
(pj/scale :size {:domain [0 20]}))A related multi-panel layout, the scatter plot matrix (SPLOM), uses pj/cross rather than pj/facet β the panels show all pairs of variables instead of one variable split across panels. See the Relationships chapter for the canonical SPLOM example.
Where it is refused
A facet divides every layer of the pose alike, and a panel aesthetic is read from a poseβs mapping and not from a layerβs, so a panel aesthetic belongs on the pose. Written in a pj/lay-* options map it is reported, and the report names the places to write it instead.
(try
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width {:col :species})
pj/plan)
(catch Exception e (ex-message e)))"A panel aesthetic col was written in a layer's options map. A facet divides every layer of the pose, and a panel aesthetic is read from a pose's mapping and not from a layer's, so write it on the pose: (pj/facet pose col), (pj/facet-grid pose col-col row-col), or in the pose's mapping. On a composite, write it on the cell whose panels it divides."A second facet in the same direction on one pose is reported rather than replacing the first. A facet written on a cell of a composite overrides the facet the cell inherits, by the scope rules.
(try
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width)
(pj/facet :species)
(pj/facet :sepal-length))
(catch Exception e (ex-message e)))"pj/facet was given :sepal-length for :col, which this pose already facets by :species. A pose divides its panels once per direction: use the other direction, or pj/facet-grid for both at once, or facet a compound key -- (pj/facet my-pose [:species :sepal-length])."Examples
Faceted plots of other kinds, each built from the ideas above.
Faceted Histogram
(-> (rdatasets/datasets-iris)
(pj/lay-histogram :sepal-length {:color :species})
(pj/facet :species))Faceted Regression
Layers compose with faceting β scatter plus regression per panel:
(-> (rdatasets/reshape2-tips)
(pj/pose :total-bill :tip {:color :sex})
pj/lay-point
(pj/lay-smooth {:stat :linear-model})
(pj/facet-grid :smoker :sex))Faceted Bar Chart
Five bars total across the three island panels: Adelie appears on every island, while Gentoo lives only on Biscoe and Chinstrap only on Dream. Empty species-island combinations produce no bar.
(-> (rdatasets/palmerpenguins-penguins)
(pj/lay-bar :species {:color :species})
(pj/facet :island))Comparing Multiple Columns
Pass a vector of column names to create one panel per column:
(pj/lay-histogram (rdatasets/datasets-iris) [:sepal-length :sepal-width :petal-length] {:color :species})Labels and Faceting
pj/options works with faceted plots:
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width {:color :species})
(pj/facet :species)
(pj/options {:title "Iris by Species"
:x-label "Sepal Length (cm)" :y-label "Sepal Width (cm)"}))See Also
- Specifying Aesthetics β the role of every aesthetic,
:coland:rowamong them - Options and Scopes β the scope rules a panel aesthetic obeys
- Composition β composite poses, shared scales, and arbitrary layouts
- Core Concepts β mapping scope as it flows through faceted panels
Whatβs Next
- Series β reading several columns by name rather than by value
- Troubleshooting β common issues and how to fix them
- API Reference β complete function listing with docstrings