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}))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"]}}))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}))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.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