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}))A tall, narrow plot.
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width {:color :species})
(pj/options {:width 300 :height 500}))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)"}))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)"}))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)"}))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)"}))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)"}))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"}))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}))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"}))Log y-scale β reveals the exponential trend.
(-> exponential-data
(pj/lay-point :x :y)
(pj/scale :y :log)
(pj/options {:title "Log Y Scale"}))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]"}))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]}))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"}))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"]}))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}))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))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))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})):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}))Alpha works on bars and polygons too.
(-> (rdatasets/datasets-iris)
(pj/lay-bar :species {:alpha 0.4}))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.15Pass {: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}))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"]}))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}))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}))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}}))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}))Legend on top:
(-> (rdatasets/datasets-iris)
(pj/lay-point :sepal-length :sepal-width {:color :species})
(pj/options {:legend-position :top}))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}))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