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

Text and Label Placement

pj/lay-text and pj/lay-label place a label at a data point. By default the label starts at the point and reads to the right, centered on the point vertically. Two options move the label relative to the point by choosing which part of the text the point pins:

  • :align-x – :left, :center, or :right (default :left)
  • :align-y – :top, :center, or :bottom (default :center)

The value names the part of the text placed at the point. :align-x :right pins the right edge, so the label extends leftward; :align-x :center straddles the point. :align-y reads in data orientation: :top pins the top edge, so the text reads downward from the point, and :bottom pins the bottom edge, so the text floats above it.

Three labels on a column of points at the same x, one per horizontal anchor – the text fans right of, across, and left of the point:

(-> {:x [2 2 2] :y [3 2 1]}
    (pj/lay-point :x :y {:size 6 :color "#888888"})
    (pj/lay-text :x :y {:text :tag :align-x :left
                        :data {:x [2] :y [3] :tag ["align-x :left"]}})
    (pj/lay-text :x :y {:text :tag :align-x :center
                        :data {:x [2] :y [2] :tag ["align-x :center"]}})
    (pj/lay-text :x :y {:text :tag :align-x :right
                        :data {:x [2] :y [1] :tag ["align-x :right"]}}))
yxalign-x :leftalign-x :centeralign-x :right1.01.21.41.61.82.02.22.42.62.83.01.01.21.41.61.82.02.22.42.62.83.0

A practical use of :align-y: float a value label just above each bar by pinning the label’s bottom edge to the bar top, centered over the bar.

(-> {:species ["setosa" "versicolor" "virginica"]
     :pct     [33.3 33.3 33.3]}
    (pj/lay-value-bar :species :pct {:color "#a6cee3"})
    (pj/lay-text :species :pct {:text :pct :align-x :center :align-y :bottom}))
pctspecies33.333.333.3setosaversicolorvirginica051015202530

Every anchor value flows through for both pj/lay-text and pj/lay-label; the defaults reproduce the original placement, and an unrecognized value is rejected at plan time.

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