21  Customization

How to customize plots: dimensions, labels, scales, mark styling, annotations, palettes, themes, legend placement, and interactivity.

(ns plotje-book.customization
  (:require
   ;; 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]
   ;; Clojure2d -- palette and gradient discovery
   [clojure2d.color :as c2d]))

Dimensions

A wide, short plot.

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/options {:width 800 :height 250}))
sepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

A tall, narrow plot.

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/options {:width 300 :height 500}))
sepal widthsepal lengthspeciessetosaversicolorvirginica682.02.22.42.62.83.03.23.43.63.84.04.24.4

Titles and Labels

Override axis labels and add a title.

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/options {:title "Iris Sepal Measurements"
                 :x-label "Length (cm)"
                 :y-label "Width (cm)"}))
Iris Sepal MeasurementsWidth (cm)Length (cm)speciessetosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

Add a subtitle and caption for context.

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/options {:title "Iris Measurements"
                 :subtitle "Sepal dimensions across three species"
                 :caption "Source: Fisher's Iris dataset (1936)"}))
Iris MeasurementsSepal dimensions across three speciessepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5Source: Fisher's Iris dataset (1936)

Legend titles default to the column name. Override with :color-label, :size-label, or :alpha-label:

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/options {:color-label "Species (override)"}))
sepal widthsepal lengthSpecies (override)setosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

The size legend title comes from :size-label:

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:size :petal-length})
    (pj/options {:size-label "Petal length (override)"}))
sepal widthsepal lengthPetal length (override)1.02.03.04.05.06.04.55.05.56.06.57.07.58.02.02.53.03.54.04.5

And :alpha-label overrides the alpha legend title:

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:alpha :petal-length})
    (pj/options {:alpha-label "Petal length (override)"}))
sepal widthsepal lengthPetal length (override)1.02.03.04.05.06.04.55.05.56.06.57.07.58.02.02.53.03.54.04.5

Color vs Fill

:color and :fill are kept separate, matching the ggplot2 model: :color paints the stroke or outline (point edge, line), and :fill paints the interior (tile, density2d cell, bar). For point layers and line layers :color is the natural channel; for area or interior-painted marks (lay-tile, lay-density-2d, lay-bin2d) :fill is the natural channel. They each have their own legend title override – :color-label for :color and :fill-label for :fill:

(-> {:x [1 2 3 1 2 3] :y [1 1 1 2 2 2] :z [10 20 30 40 50 60]}
    (pj/lay-tile :x :y {:fill :z})
    (pj/options {:fill-label "Score"}))
yxScore10.0060.00no data1.01.21.41.61.82.02.22.42.62.83.01.01.11.21.31.41.51.61.71.81.92.0

Scales

Use a log scale for data spanning orders of magnitude.

(def exponential-data
  {:x (range 1 50)
   :y (map #(* 2 (Math/pow 1.1 %)) (range 1 50))})

Linear scale – hard to see the structure.

(-> exponential-data
    (pj/lay-point :x :y)
    (pj/options {:title "Linear Scale"}))
Linear Scaleyx05101520253035404550050100150200

Log y-scale – reveals the exponential trend.

(-> exponential-data
    (pj/lay-point :x :y)
    (pj/scale :y :log)
    (pj/options {:title "Log Y Scale"}))
Log Y Scaleyx05101520253035404550110100

Lock the y-axis to a specific range.

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/scale :y {:type :linear :domain [0 6]})
    (pj/options {:title "Fixed Y Domain [0, 6]"}))
Fixed Y Domain [0, 6]sepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.00123456

Pin exact tick locations with :breaks (ggplot2’s scale_*_continuous(breaks=...)).

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/scale :y {:type :linear :breaks [2.0 3.0 4.0]}))
sepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.0234

Order a categorical axis explicitly with :type :categorical and a :domain vector. Without this, categories appear in their order of first occurrence in the data.

(-> {:size ["medium" "small" "large"]
     :count [12 30 7]}
    (pj/lay-value-bar :size :count)
    (pj/scale :x {:type :categorical :domain ["large" "medium" "small"]}))
countsizelargemediumsmall051015202530

Log scale on visual channels

pj/scale works on continuous visual channels too – :size, :alpha, :fill, and :color. When the encoded column spans many orders of magnitude, a log scale spaces the legend ticks logarithmically and maps the visual property (radius, alpha, gradient color) in log-space, so each tick step represents the same multiplicative ratio. :categorical does not apply to a continuous encoding – visual channels accept :linear (the default) and :log only.

Point sizes from a column whose values jump by factors of ten:

(-> {:user [:a :b :c] :n [10 100 1000]}
    (pj/lay-point :user :n {:size :n :x-type :categorical})
    (pj/scale :size :log))
nusern10.0100.01000.0abc01002003004005006007008009001000

The size legend’s tick values are the original numbers (10, 100, 1000), but the dot radii grow in log-space – each step reflects the same factor, matching what you see at the same data values in the plot.

Tile heatmap with log-scaled fill:

(-> (for [r (range 5) c (range 5)]
      {:r r :c c :v (Math/pow 10.0 (/ (+ r c) 2.0))})
    (pj/lay-tile :r :c {:fill :v})
    (pj/scale :fill :log))
crfill110100100010000no data0.00.51.01.52.02.53.03.54.00.00.51.01.52.02.53.03.54.0

The continuous fill legend draws log-spaced tick labels along the gradient bar so a tile’s color reads as its log-space position between the data minimum and maximum.

Column type overrides

A column’s inferred type (numerical / categorical / temporal) drives scale type, axis formatting, and which marks accept it. To override the inference, pass :x-type, :y-type, or :color-type in the layer or pose options. For example, a numeric column representing subject IDs:

(-> {:hour [9 10 11 12] :count [5 8 12 7]}
    (pj/lay-value-bar :hour :count {:x-type :categorical}))
counthour9101112024681012

The override propagates into column-type inference, so every downstream step (scale type, tick placement, domain) treats the column as the overridden type. See Inference Rules for the full mechanism, and the Troubleshooting chapter for the symptoms each override addresses.

Mark Styling

Pass :alpha and :size directly to layer functions.

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species :alpha 0.5 :size 5}))
sepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

:size controls line thickness on line-based marks:

(-> {:x [1 2 3 4 5] :y [2 4 3 5 4]}
    (pj/lay-line :x :y {:size 3}))
yx1.01.52.02.53.03.54.04.55.02.02.53.03.54.04.55.0

Alpha works on bars and polygons too.

(-> (rdatasets/datasets-iris)
    (pj/lay-bar :species {:alpha 0.4}))
speciessetosaversicolorvirginica05101520253035404550

Annotations

Reference lines and shaded bands are layers added with pj/lay-rule-h, pj/lay-rule-v, pj/lay-band-h, pj/lay-band-v. Position comes from the options map (:y-intercept or :x-intercept for rules; :y-min/:y-max or :x-min/:x-max for bands); appearance aesthetics (:color, :alpha) work the same way they do on any other layer.

Horizontal and vertical reference lines.

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/lay-rule-h {:y-intercept 3.0})
    (pj/lay-rule-v {:x-intercept 6.0}))
sepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

On a temporal axis, the intercept can also be a date or instant (LocalDate, LocalDateTime, Instant, java.util.Date). Plotje converts it to the same numeric scale the data uses, so the rule lands at the right calendar position.

(-> {: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"}))
valuedateJan-02Jan-28Feb-23Mar-20Apr-15May-11Jun-06Jul-02Jul-283456789

Shaded bands use a default opacity of 0.15. Pass {:alpha ...} to override.

(:band-opacity (pj/config))
0.15
(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/lay-band-v {:x-min 5.5 :x-max 6.5})
    (pj/lay-band-h {:y-min 3.0 :y-max 3.5 :alpha 0.3}))
sepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

Note: position values must be literal numbers in this release. A faceted plot with a different reference value per panel (column-mapped intercept, ggplot2’s geom_hline(aes(yintercept=...))) is on the post-alpha roadmap. Today, an annotation added once with the same intercept appears on every panel of the faceted pose.

Palettes

Pass :palette to override the default color cycle. It accepts a vector of hex strings, a map from category to hex, or a keyword naming one of the built-in palettes (:set1, :set2, :dark2, :tableau-10, :category10, :pastel1, :accent, :paired, and many more).

For the full list of forms, the project-level / thread-local / plot-level precedence chain, and the key table, see the Configuration chapter.

Custom vector:

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/options {:palette ["#E74C3C" "#3498DB" "#2ECC71"]}))
sepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

Named preset – here :dark2 for a high-contrast qualitative palette:

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/options {:palette :dark2}))
sepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

Discovering Palettes and Gradients

Plotje delegates color to the clojure2d library, which bundles thousands of named palettes and gradients. Use clojure2d.color/find-palette and clojure2d.color/find-gradient to search by regex pattern.

Find palettes whose name contains β€œbudapest”.

(c2d/find-palette #"budapest")
(:grand-budapest-1 :grand-budapest-2)

Find palettes whose name contains β€œset”.

(c2d/find-palette #"^:set")
(:set1 :set2 :set3)

Find gradients related to β€œviridis”.

(c2d/find-gradient #"viridis")
(:mpl/viridis
 :viridis/cividis
 :viridis/inferno
 :viridis/magma
 :viridis/mako
 :viridis/plasma
 :viridis/rocket
 :viridis/turbo
 :viridis/viridis)

c2d/palette returns the colors for a given name. Each color is a clojure2d Vec4 (RGBA, 0-255 range).

(c2d/palette :grand-budapest-1)
[[241.0 187.0 123.0 255.0]
 [253.0 100.0 103.0 255.0]
 [91.0 26.0 24.0 255.0]
 [214.0 114.0 54.0 255.0]]

Colorblind-friendly palettes

For presentations and publications, consider palettes designed for colorblind readers. Several good options are built in:

  • :set2 – muted qualitative, 8 colors
  • :dark2 – dark qualitative, 8 colors
  • :khroma/okabeito – designed specifically for color vision deficiency
  • :tableau-10 – Tableau default, high contrast
(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/options {:palette :khroma/okabeito}))
sepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

Theme

Customize background color, grid color, and font size.

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/options {:title "White Theme"
                 :theme {:bg "#FFFFFF" :grid "#EEEEEE" :font-size 10}}))
White Themesepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

Legend Position

Control where the legend appears: :right (default), :bottom, :top, or :none.

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/options {:legend-position :bottom}))
sepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

Legend on top:

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/options {:legend-position :top}))
sepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

No legend at all – useful when the color encoding is documented in the title or caption rather than a separate legend. The plan’s :legend-w becomes 0, so the panel takes the full width:

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/options {:legend-position :none})
    pj/plan
    (get-in [:layout :legend-w]))
0

Tooltip

Enable mouseover data values with {:tooltip true}.

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/options {:tooltip true}))
sepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

Brush Selection

Enable drag-to-select with {:brush true}. Click to reset.

(-> (rdatasets/datasets-iris)
    (pj/lay-point :sepal-length :sepal-width {:color :species})
    (pj/options {:brush true}))
sepal widthsepal lengthspeciessetosaversicolorvirginica4.55.05.56.06.57.07.58.02.02.53.03.54.04.5

Brushing becomes especially useful in a SPLOM (scatter plot matrix). Drag to select points in any panel – the selection highlights across all panels, revealing multivariate structure.

(def splom-cols [:sepal-length :sepal-width :petal-length :petal-width])
(-> (rdatasets/datasets-iris)
    (pj/pose (pj/cross splom-cols splom-cols) {:color :species})
    (pj/options {:brush true}))
01066126121212sepal-lengthsepal-widthpetal-lengthpetal-widthsepal-lengthsepal-widthpetal-lengthpetal-widthspeciessetosaversicolorvirginica

What’s Next

  • Faceting – split any chart into panels by one or two variables
  • API Reference – complete function listing with docstrings
source: notebooks/plotje_book/customization.clj