19  Customization

How to tweak the look of a plot: dimensions, labels, scales, mark styling, palettes, themes, and legend placement.

Other appearance topics live in their natural homes: column-to-aesthetic mapping in Core Concepts, reference lines and bands in Core Concepts (constant positions) and Timelines (temporal intercepts), and tooltips/brushing in 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 and Fill

Most marks expose :color as the encoding channel – scatter dots, lines, bar interiors, area fills, violins, lollipops – all styled with :color and named via :color-label in the legend. The separate :fill channel is currently reserved for the heatmap family: lay-tile (and the :bin2d output beneath lay-density-2d) reads the encoded value as a continuous fill, with its own legend title override :fill-label:

(-> {: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.001.01.21.41.61.82.02.22.42.62.83.01.01.11.21.31.41.51.61.71.81.92.0

Coming from ggplot2. ggplot’s colour= (stroke) and fill= (interior) split is partial in Plotje today. On filled marks like lay-bar, lay-area, and lay-violin, the :color aesthetic paints the interior; there is no separate stroke channel, and :fill is not accepted. A lay-bar styled with {:color :species} produces one filled polygon per category:

(-> (rdatasets/datasets-iris)
    (pj/lay-bar :species {:color :species}))
speciesspeciessetosaversicolorvirginicasetosaversicolorvirginica05101520253035404550

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

Pair :breaks with :labels to render numeric positions with custom tick text. The two vectors must match in count – each label is shown at its corresponding break. This is the path for cases like a tile heatmap where the axis is numerically indexed (1-7) but the natural labels are categorical (days of the week).

(-> (for [day (range 1 8) hour (range 0 24)]
      {:day day :hour hour :load (+ (* 0.3 (Math/sin (* 0.5 hour)))
                                    (* 0.2 (mod day 3)))})
    (pj/lay-tile :day :hour {:fill :load})
    (pj/scale :x {:type :linear
                  :breaks [1 2 3 4 5 6 7]
                  :labels ["Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun"]})
    (pj/options {:title "Weekly Load by Hour"}))
Weekly Load by Hourhourdayfill-0.30000.6992MonTueWedThuFriSatSun05101520

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. Without :scale :size :log, the default linear mapping puts the n=10 and n=100 points at nearly the same radius – only n=1000 stands out. Linear scaling reflects absolute distance, which is dominated by the largest value:

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

With pj/scale :size :log, each factor-of-10 step reflects the same proportional jump in radius, so the n=10 and n=100 points are now visibly distinct:

(-> {: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))
crfill1101001000100000.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.

To override the inferred type of a column (e.g. force a numeric :hour column to render as categorical bands), see Inference Rules.

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

Annotation Appearance

Reference lines and bands are introduced in Core Concepts; on temporal axes, intercepts can be LocalDate / Instant values – see Timelines. This section covers the appearance defaults you can override.

Shaded bands draw at a default opacity of 0.15:

(:band-opacity (pj/config))
0.15

Pass {:alpha ...} on a band layer to override:

(-> (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: intercept and band-edge positions must be literal values (numbers, or temporal values on a time axis) 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 panel takes the full width since no legend strip is reserved:

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

See Also

  • Core Concepts – the mapping and aesthetic vocabulary used throughout this chapter
  • Options and Scopes – where layer options, plot options, and configuration live
  • Interactivity – tooltips and brush selection

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