22  Faceting

Faceting splits data into subsets and draws each in its own panel, making it easy to compare patterns across groups.

(ns plotje-book.faceting
  (:require
   ;; 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))
sepal widthsepal lengthspeciessetosaversicolorvirginica682.02.53.03.54.04.56868setosaversicolorvirginica

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

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))
sepal widthsepal lengthspeciessetosaversicolorvirginica2342344.55.05.56.06.57.07.58.0234setosaversicolorvirginica

Facet Grid

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))
tiptotal billsexFemaleMale51020405102040NoYesFemaleMale

Row labels appear on the right, column labels on top.

Faceted Histogram

(-> (rdatasets/datasets-iris)
    (pj/lay-histogram :sepal-length {:color :species})
    (pj/facet :species))
sepal lengthspeciessetosaversicolorvirginica6802468101214166868setosaversicolorvirginica

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))
tiptotal billsexFemaleMale51020405102040NoYesFemaleMale

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}))
sepal widthsepal lengthspeciessetosaversicolorvirginica682.02.53.03.54.04.56868setosaversicolorvirginica

Inspect the coordinated domains directly:

(->> (-> (rdatasets/datasets-iris)
         (pj/lay-point :sepal-length :sepal-width)
         (pj/facet :species)
         pj/plan
         :panels)
     (mapv :x-domain))
[[4.225 8.05] [4.225 8.05] [4.225 8.05]]

Free y – each panel has its own y-range:

(->> (-> (rdatasets/datasets-iris)
         (pj/lay-point :sepal-length :sepal-width)
         (pj/facet :species)
         (pj/options {:scales :free-y})
         pj/plan
         :panels)
     (mapv :y-domain))
[[2.195 4.505000000000001] [1.93 3.4699999999999998] [2.12 3.88]]

Other values: :free-x (x per-panel, y shared), :free (both axes per-panel).

Facet Plan Structure

Under the hood, faceting produces multiple panels in the plan:

(def faceted-plan
  (-> (rdatasets/datasets-iris)
      (pj/lay-point :sepal-length :sepal-width {:color :species})
      (pj/facet :species)
      pj/plan))
(:grid faceted-plan)
{:rows 1, :cols 3}
(count (:panels faceted-plan))
3

Each panel has a grid position and a strip label:

(:panels faceted-plan)
[{:coord :cartesian,
  :y-domain [1.93 4.505000000000001],
  :col-label "setosa",
  :x-scale {:type :linear},
  :x-domain [4.225 8.05],
  :x-ticks {:values [6.0 8.0], :labels ["6" "8"], :categorical? false},
  :col 0,
  :layers
  [{:mark :point,
    :style {:opacity 0.75, :radius 3.0},
    :size-scale nil,
    :alpha-scale nil,
    :groups
    [{:color
      [0.8941176470588236 0.10196078431372549 0.10980392156862745 1.0],
      :xs #tech.v3.dataset.column<float64>[50]
:sepal-length
[5.100, 4.900, 4.700, 4.600, 5.000, 5.400, 4.600, 5.000, 4.400, 4.900, 5.400, 4.800, 4.800, 4.300, 5.800, 5.700, 5.400, 5.100, 5.700, 5.100...],
      :ys #tech.v3.dataset.column<float64>[50]
:sepal-width
[3.500, 3.000, 3.200, 3.100, 3.600, 3.900, 3.400, 3.400, 2.900, 3.100, 3.700, 3.400, 3.000, 3.000, 4.000, 4.400, 3.900, 3.500, 3.800, 3.800...],
      :label "setosa",
      :row-indices #tech.v3.dataset.column<int64>[50]
:__row-idx
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19...]}],
    :y-domain [2.3 4.4],
    :x-domain [4.3 5.8]}],
  :y-scale {:type :linear},
  :y-ticks
  {:values [2.0 2.5 3.0 3.5 4.0 4.5],
   :labels ["2.0" "2.5" "3.0" "3.5" "4.0" "4.5"],
   :categorical? false},
  :row 0}
 {:coord :cartesian,
  :y-domain [1.93 4.505000000000001],
  :col-label "versicolor",
  :x-scale {:type :linear},
  :x-domain [4.225 8.05],
  :x-ticks {:values [6.0 8.0], :labels ["6" "8"], :categorical? false},
  :col 1,
  :layers
  [{:mark :point,
    :style {:opacity 0.75, :radius 3.0},
    :size-scale nil,
    :alpha-scale nil,
    :groups
    [{:color
      [0.21568627450980393 0.49411764705882355 0.7215686274509804 1.0],
      :xs #tech.v3.dataset.column<float64>[50]
:sepal-length
[7.000, 6.400, 6.900, 5.500, 6.500, 5.700, 6.300, 4.900, 6.600, 5.200, 5.000, 5.900, 6.000, 6.100, 5.600, 6.700, 5.600, 5.800, 6.200, 5.600...],
      :ys #tech.v3.dataset.column<float64>[50]
:sepal-width
[3.200, 3.200, 3.100, 2.300, 2.800, 2.800, 3.300, 2.400, 2.900, 2.700, 2.000, 3.000, 2.200, 2.900, 2.900, 3.100, 3.000, 2.700, 2.200, 2.500...],
      :label "versicolor",
      :row-indices #tech.v3.dataset.column<int64>[50]
:__row-idx
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19...]}],
    :y-domain [2.0 3.4],
    :x-domain [4.9 7.0]}],
  :y-scale {:type :linear},
  :y-ticks
  {:values [2.0 2.5 3.0 3.5 4.0 4.5],
   :labels ["2.0" "2.5" "3.0" "3.5" "4.0" "4.5"],
   :categorical? false},
  :row 0}
 {:coord :cartesian,
  :y-domain [1.93 4.505000000000001],
  :col-label "virginica",
  :x-scale {:type :linear},
  :x-domain [4.225 8.05],
  :x-ticks {:values [6.0 8.0], :labels ["6" "8"], :categorical? false},
  :col 2,
  :layers
  [{:mark :point,
    :style {:opacity 0.75, :radius 3.0},
    :size-scale nil,
    :alpha-scale nil,
    :groups
    [{:color
      [0.30196078431372547 0.6862745098039216 0.2901960784313726 1.0],
      :xs #tech.v3.dataset.column<float64>[50]
:sepal-length
[6.300, 5.800, 7.100, 6.300, 6.500, 7.600, 4.900, 7.300, 6.700, 7.200, 6.500, 6.400, 6.800, 5.700, 5.800, 6.400, 6.500, 7.700, 7.700, 6.000...],
      :ys #tech.v3.dataset.column<float64>[50]
:sepal-width
[3.300, 2.700, 3.000, 2.900, 3.000, 3.000, 2.500, 2.900, 2.500, 3.600, 3.200, 2.700, 3.000, 2.500, 2.800, 3.200, 3.000, 3.800, 2.600, 2.200...],
      :label "virginica",
      :row-indices #tech.v3.dataset.column<int64>[50]
:__row-idx
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19...]}],
    :y-domain [2.2 3.8],
    :x-domain [4.9 7.9]}],
  :y-scale {:type :linear},
  :y-ticks
  {:values [2.0 2.5 3.0 3.5 4.0 4.5],
   :labels ["2.0" "2.5" "3.0" "3.5" "4.0" "4.5"],
   :categorical? false},
  :row 0}]

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 Scatter chapter for the canonical SPLOM example.

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})
68024681012141623402468101214161850510152025sepal lengthsepal widthpetal lengthspeciessetosaversicolorvirginica

Faceted Bar Chart

(-> (rdatasets/palmerpenguins-penguins)
    (pj/lay-bar :species {:color :species})
    (pj/facet :island))
speciesspeciesAdelieGentooChinstrapAdelieGentooChinstrap020406080100120AdelieGentooChinstrapAdelieGentooChinstrapTorgersenBiscoeDream

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)"}))
Iris by SpeciesSepal Width (cm)Sepal Length (cm)speciessetosaversicolorvirginica682.02.53.03.54.04.56868setosaversicolorvirginica

What’s Next

source: notebooks/plotje_book/faceting.clj