Quick Plot

Building plots with composable, ggplot-style helper functions

Overview

The geom and gg packages provide a set of composable helper functions for building Vega-Lite plot specifications. Inspired by R’s ggplot2, specifications are constructed by combining independent layers — geometry, labels, scales, coordinates and themes — rather than writing monolithic JSON-like plists by hand.

Each helper returns a plist. The function merge-plists recursively merges them into a single Vega-Lite spec, which vega:defplot compiles and plot:plot renders.

Design philosophy

In ggplot2, a plot is built from independent concerns:

ggplot2 Lisp-Stat Package Responsibility
geom_point() point geom Mark type and encoding
geom_bar() bar geom Mark type and encoding
geom_boxplot() box-plot geom Mark type and encoding
geom_histogram() histogram geom Binning, aggregation
geom_line() line geom Series connection, interpolation
labs() label gg Axis titles
scale_*() axes gg Axis transforms, domains, color schemes
coord_cartesian() coord gg Viewport clipping
theme() theme gg Dimensions, fonts, appearance
(no direct equiv.) tooltip gg Hover field definitions

Each function knows about one concern and nothing else. A mark function never sets axis titles; label never touches mark types; theme never alters encodings. This separation means any helper can be used with any plot type.

The merge pattern

Every helper returns a plist fragment. merge-plists performs a recursive deep merge: when two plists both supply a nested plist for the same key (e.g. :encoding), the inner plists are merged rather than one replacing the other. This is what allows label to add :axis entries to the :encoding that point already created.

Note that merge-plists is a utility function, not a layer helper — it is the mechanism that makes composition work, not a layer you pass to qplot yourself. See the API reference for full details.

Two ways to plot

Lisp-Stat provides two entry points for creating plots. Choose the one that fits your workflow.

defplot + plot:plot — the explicit pattern

The traditional approach separates definition from rendering. Use this when you want full control, or when writing scripts and notebooks. Replace :x-field, :y-field, and my-data with your actual field names and data source:

(plot:plot (vega:defplot my-plot (merge-plists `(:title "My Plot" :data (:values ,my-data)) (point :x-field :y-field) (label :x "X Label" :y "Y Label") (theme :width 600))))

defplot is a macro that:

  1. Calls %defplot to create a vega-plot object
  2. Binds it to a global variable (my-plot)
  3. Registers it in *all-plots* so show-plots can list it

plot:plot then renders the object to the browser.

qplot — quick plot for the REPL

For interactive exploration, the three-line scaffold of plot:plot / vega:defplot / merge-plists is repetitive. qplot collapses it into a single function call:

(qplot 'my-plot my-data `(:title "My Plot") (point :x-field :y-field) (label :x "X Label" :y "Y Label") (theme :width 600))

qplot takes a name (a symbol), a data object (a data frame, plist data, or URI), and any number of layer plists. It:

  1. Prepends (:data (:values ,data)) and merges all layers
  2. Creates the plot object via %defplot
  3. Binds it to the named global variable
  4. Registers it in *all-plots*
  5. Renders immediately via plot:plot
  6. Returns the plot object

Because qplot binds a named variable, the standard REPL workflow is to re-evaluate the same form as you iterate on a plot. Each call overwrites the previous definition — there is no accumulation in *all-plots* or in the global namespace:

;; First attempt — rough sketch (qplot 'cars vgcars (point :horsepower :miles-per-gallon)) ;; Second attempt — add color and a title (qplot 'cars vgcars `(:title "HP vs MPG") (point :horsepower :miles-per-gallon :color :origin :filled t)) ;; Third attempt — polish for presentation (qplot 'cars vgcars `(:title "HP vs MPG") (point :horsepower :miles-per-gallon :color :origin :filled t) (label :x "Horsepower" :y "Fuel Efficiency") (theme :width 600 :height 400)) ;; Later — the variable is still bound cars ; => #<VEGA-PLOT ...> (plot:plot cars) ; re-render (describe cars) ; inspect the spec (show-plots) ; lists one 'cars' entry

Helpers reference

The layering helpers work with all mark types.

label — Set axis titles.

(label :x "Horsepower" :y "Miles per Gallon")

axes — Set axis types, domains, ranges, and color schemes.

(axes :x-type :log :color-scheme :dark2) (axes :x-domain #(0 300) :y-domain #(0 50))

tooltip — Add hover tooltips. Each argument is a field spec.

(tooltip '(:field :name :type :nominal) '(:field :horsepower :type :quantitative))

coord — Restrict the visible viewport and clip marks, like coord_cartesian() in ggplot2. Only data within the domain is visible; points outside are clipped rather than dropped.

(coord :x-domain #(50 150) :y-domain #(20 40))

theme — Set dimensions, font, background, or named presets.

(theme :width 600 :height 400 :font "Georgia")

Loading example data

The examples below use datasets from the Vega datasets collection. Load them into your session before running the examples:

(vega:load-vega-examples)

This makes the following variables available in your environment:

  • vgcars — Automobile specifications (horsepower, MPG, origin, etc.) for 406 cars.
  • stocks — Daily closing prices for several major tech stocks over multiple years.

For datasets loaded using vega:read-vega, field names are automatically converted to Lisp-style keywords (e.g. Miles_per_Gallon becomes :miles-per-gallon). Note that the :year field in vgcars is stored as a date string (e.g. "1970-01-01") rather than an integer, which is why the line chart examples pass :x-type :temporal for that field.

Scatter plot

A scatter plot maps two quantitative variables to x and y positions. Use point for exploring relationships, correlations and clusters in data. The name follows the ggplot2 convention where geom_point() produces a scatter plot.

Basic scatter plot

The simplest scatter plot needs just a name, a data frame, and two field names:

(qplot 'cars-basic vgcars `(:title "Horsepower vs. MPG") (point :horsepower :miles-per-gallon))
020406080100120140160180200220240horsepower05101520253035404550milesPerGallonHorsepower vs. MPG

Color by category

Pass a keyword to :color to map a nominal variable to hue. Use :filled t to fill the point marks:

(qplot 'cars-colored vgcars `(:title "Cars by Origin") (point :horsepower :miles-per-gallon :color :origin :filled t))
020406080100120140160180200220240horsepower05101520253035404550milesPerGallonEuropeJapanUSAoriginCars by Origin

Bubble plot

When :size is a keyword, it encodes a third quantitative field as point area — a bubble plot:

(qplot 'cars-bubble vgcars `(:title "Bubble: Size = Acceleration") (point :horsepower :miles-per-gallon :color :origin :size :acceleration :filled t) (label :x "Horsepower" :y "Miles per Gallon"))
020406080100120140160180200220240Horsepower05101520253035404550Miles per GallonEuropeJapanUSAorigin05101520accelerationBubble: Size = Acceleration

Adding axis labels

Use label to give axes meaningful titles:

(qplot 'cars-with-labels vgcars `(:title "Vega Cars") (point :horsepower :miles-per-gallon :filled t) (label :x "Engine Horsepower" :y "Fuel Efficiency (MPG)"))
020406080100120140160180200220240Engine Horsepower05101520253035404550Fuel Efficiency (MPG)Vega Cars

Log scale with custom color scheme

Use axes to transform an axis and change the color palette:

(qplot 'cars-log-scale vgcars `(:title "Horsepower (log) vs. MPG") (point :horsepower :miles-per-gallon :color :origin :filled t) (axes :x-type :log :color-scheme :dark2) (label :x "Horsepower (log scale)" :y "Miles per Gallon"))
10203040501002003004005001,000Horsepower (log scale)05101520253035404550Miles per GallonEuropeJapanUSAoriginHorsepower (log) vs. MPG

Tooltips on hover

Add tooltip to show details when the user hovers over a point:

(qplot 'cars-tooltip vgcars `(:title "Car Details on Hover") (point :horsepower :miles-per-gallon :color :origin :filled t) (tooltip '(:field :name :type :nominal) '(:field :horsepower :type :quantitative) '(:field :miles-per-gallon :type :quantitative) '(:field :origin :type :nominal)))
020406080100120140160180200220240horsepower05101520253035404550milesPerGallonEuropeJapanUSAoriginCar Details on Hover

Zoom into a region

Use coord to restrict the visible area. Unlike axes, this clips marks that fall outside the domain — only points within the viewport are drawn:

(qplot 'cars-zoomed vgcars `(:title "Cars: 50-150 HP, 20-40 MPG") (point :horsepower :miles-per-gallon :color :origin :filled t) (coord :x-domain #(50 150) :y-domain #(20 40)) (label :x "Horsepower" :y "Miles per Gallon"))
5060708090100110120130140150Horsepower2022242628303234363840Miles per GallonEuropeJapanUSAoriginCars: 50-150 HP, 20-40 MPG

Custom theme

Use theme to set plot dimensions, font, and visual style:

(qplot 'cars-themed vgcars `(:title "Themed Scatter Plot") (point :horsepower :miles-per-gallon :color :origin :filled t) (label :x "Horsepower" :y "MPG") (theme :width 600 :height 400 :font "Georgia"))
020406080100120140160180200220240Horsepower05101520253035404550MPGEuropeJapanUSAoriginThemed Scatter Plot

Full example — all layers

Combine every layer for a production-quality plot:

(qplot 'cars-full vgcars `(:title "Complete Example: All Layers" :description "Demonstrating label, axes, tooltip, coord, and theme") (point :horsepower :miles-per-gallon :color :origin :size :acceleration :filled t) (label :x "Engine Horsepower" :y "Fuel Efficiency (MPG)") (axes :color-scheme :category10) (tooltip '(:field :name :type :nominal) '(:field :horsepower :type :quantitative) '(:field :miles-per-gallon :type :quantitative) '(:field :origin :type :nominal)) (coord :x-domain #(40 240) :y-domain #(5 50)) (theme :width 700 :height 450))
405060708090100110120130140150160170180190200210220230240Engine Horsepower5101520253035404550Fuel Efficiency (MPG)EuropeJapanUSAorigin05101520accelerationComplete Example: All Layers

LOESS smoother

A LOESS (locally estimated scatterplot smoothing) curve fits a non-parametric smooth line through data, revealing trends without assuming a fixed functional form. Use loess to overlay a trend line on a scatter plot or to compare smoothed trajectories across groups.

Scatter plot with LOESS smoother

Use gg:layer to compose the scatter points and the smoother into a single layered view:

(qplot 'cars-loess vgcars `(:title "HP vs. MPG with LOESS Smoother") (gg:layer (point :horsepower :miles-per-gallon :color :origin :filled t :opacity 0.5) (loess :horsepower :miles-per-gallon :group :origin :stroke-width 2)))
020406080100120140160180200220240horsepower05101520253035404550milesPerGallonEuropeJapanUSAoriginHP vs. MPG with LOESS Smoother

The :group :origin argument fits a separate curve for each origin and encodes it with the matching hue automatically. Increase :bandwidth toward 1.0 for a flatter, more global fit; decrease it toward 0.05 for a curve that tracks local variation closely.

Histogram

A histogram bins a quantitative variable and counts observations per bin. Use histogram to visualize the distribution of a single variable.

Basic histogram

Pass a single field name. The default uses Vega-Lite’s automatic binning and counts occurrences:

(qplot 'mpg-hist vgcars `(:title "Distribution of Miles per Gallon") (histogram :miles-per-gallon) (label :x "Miles per Gallon" :y "Count"))
5101520253035404550Miles per Gallon0102030405060708090100CountDistribution of Miles per Gallon

Custom bin count

Control granularity with the :bin keyword. Pass a plist with :maxbins to limit the number of bins:

(qplot 'mpg-hist-bins vgcars `(:title "MPG Distribution (10 bins)") (histogram :miles-per-gallon :bin '(:maxbins 10)) (label :x "Miles per Gallon" :y "Count"))
5101520253035404550Miles per Gallon0102030405060708090100CountMPG Distribution (10 bins)

Horizontal histogram

Set :orient :horizontal to place bins on the y-axis:

(qplot 'mpg-hist-horiz vgcars `(:title "MPG Distribution (Horizontal)") (histogram :miles-per-gallon :orient :horizontal) (label :x "Count" :y "Miles per Gallon"))
0102030405060708090100Count5101520253035404550Miles per GallonMPG Distribution (Horizontal)

Stacked histogram by group

Pass :group to split bins by a nominal field. Vega-Lite automatically stacks the bars:

(qplot 'mpg-hist-stacked vgcars `(:title "MPG Distribution by Origin") (histogram :miles-per-gallon :group :origin) (label :x "Miles per Gallon" :y "Count"))
5101520253035404550Miles per Gallon0102030405060708090100CountEuropeJapanUSAoriginMPG Distribution by Origin

Layered histogram

Use :stack :null with :opacity to overlay distributions transparently instead of stacking them:

(qplot 'mpg-hist-layered vgcars `(:title "MPG: Overlaid by Origin") (histogram :miles-per-gallon :group :origin :stack :null :opacity 0.5) (label :x "Miles per Gallon" :y "Count"))
5101520253035404550Miles per Gallon0102030405060708090CountEuropeJapanUSAoriginMPG: Overlaid by Origin

Normalized (100%) stacked histogram

Use :stack :normalize to show proportions instead of counts:

(qplot 'mpg-hist-normalized vgcars `(:title "MPG: Proportion by Origin") (histogram :miles-per-gallon :group :origin :stack :normalize) (label :x "Miles per Gallon" :y "Proportion"))
5101520253035404550Miles per Gallon0%10%20%30%40%50%60%70%80%90%100%ProportionEuropeJapanUSAoriginMPG: Proportion by Origin

Styled histogram

Use :color, :corner-radius-end, and :bin-spacing for visual polish. Combine with theme for custom dimensions:

(qplot 'mpg-hist-styled vgcars `(:title "Styled Histogram") (histogram :miles-per-gallon :color "darkslategray" :corner-radius-end 3 :bin-spacing 0) (label :x "Miles per Gallon" :y "Count") (theme :width 500 :height 300))
5101520253035404550Miles per Gallon0102030405060708090100CountStyled Histogram

Bar chart

A bar chart maps a categorical variable to position and a quantitative variable to bar length. Use bar when your x-axis is nominal or ordinal rather than a continuous distribution. The name follows the ggplot2 convention where geom_bar() produces a bar chart.

Basic bar chart

Supply the categorical field and the quantitative field:

(qplot 'origin-bar vgcars `(:title "Average MPG by Origin") (bar :origin :miles-per-gallon :aggregate :mean) (label :x "Origin" :y "Mean Miles per Gallon"))
EuropeJapanUSAOrigin05101520253035Mean Miles per GallonAverage MPG by Origin

Horizontal bar chart

Set :orient :horizontal to swap axes — useful for long category labels:

(qplot 'origin-bar-horiz vgcars `(:title "Average MPG by Origin (Horizontal)") (bar :origin :miles-per-gallon :aggregate :mean :orient :horizontal) (label :x "Mean Miles per Gallon" :y "Origin"))
05101520253035Mean Miles per GallonEuropeJapanUSAOriginAverage MPG by Origin (Horizontal)

Grouped (stacked) bar chart

Pass :group to split bars by a second nominal field. Bars are stacked by default:

(qplot 'cylinders-by-origin vgcars `(:title "Car Count: Cylinders by Origin") (bar :cylinders :miles-per-gallon :aggregate :count :group :origin) (label :x "Cylinders" :y "Count"))
34568Cylinders020406080100120140160180200220CountEuropeJapanUSAoriginCar Count: Cylinders by Origin

Styled bar chart

Combine visual options with layering helpers:

(qplot 'origin-bar-styled vgcars `(:title "Mean MPG by Origin") (bar :origin :miles-per-gallon :aggregate :mean :color "teal" :corner-radius-end 4) (label :x "Origin" :y "Mean MPG") (theme :width 400 :height 300 :font "Helvetica"))
EuropeJapanUSAOrigin05101520253035Mean MPGMean MPG by Origin

Box plot

A box plot summarizes the distribution of a quantitative variable, showing the median, interquartile range and outliers. Use box-plot to compare distributions across groups.

1D box plot

A single quantitative field produces a box plot summarizing the entire variable:

(qplot 'mpg-box-1d vgcars `(:title "MPG Distribution") (box-plot :miles-per-gallon) (label :x "Miles per Gallon"))
5101520253035404550Miles per GallonMPG Distribution

2D box plot — compare groups

Pass :category to split the box plot by a nominal field:

(qplot 'mpg-box-by-origin vgcars `(:title "MPG by Origin") (box-plot :miles-per-gallon :category :origin) (label :x "Miles per Gallon" :y "Origin"))
5101520253035404550Miles per GallonEuropeJapanUSAOriginMPG by Origin

Vertical orientation

Set :orient :vertical to place categories on the x-axis and values on the y-axis:

(qplot 'mpg-box-vertical vgcars `(:title "MPG by Cylinders (Vertical)") (box-plot :miles-per-gallon :category :cylinders :orient :vertical) (label :x "Cylinders" :y "Miles per Gallon"))
34568Cylinders5101520253035404550Miles per GallonMPG by Cylinders (Vertical)

Min-max whiskers

Set :extent "min-max" to extend whiskers to the minimum and maximum values instead of the default 1.5× IQR (Tukey) whiskers:

(qplot 'mpg-box-minmax vgcars `(:title "MPG by Origin (Min-Max Whiskers)") (box-plot :miles-per-gallon :category :origin :extent "min-max") ; note string value (label :x "Miles per Gallon" :y "Origin"))
5101520253035404550Miles per GallonEuropeJapanUSAOriginMPG by Origin (Min-Max Whiskers)

Styled box plot

Combine visual options with layering helpers for presentation:

(qplot 'mpg-box-styled vgcars `(:title "MPG by Origin") (box-plot :miles-per-gallon :category :origin :orient :vertical :size 40) (label :x "Origin" :y "Miles per Gallon") (theme :width 500 :height 350))
EuropeJapanUSAOrigin5101520253035404550Miles per GallonMPG by Origin

Line chart

A line chart connects data points in order, typically along a temporal or sequential x-axis. Use line for time series, trends, and any data where the relationship between consecutive points is meaningful. The name follows the ggplot2 convention where geom_line() produces a line chart.

Basic line chart

The simplest line chart needs two field names. Points are connected in x-axis order:

(qplot 'stock-basic stocks `(:title "Google Stock Price" :transform #((:filter "datum.symbol === 'GOOG'"))) (line :date :price :x-type :temporal) (label :x "Date" :y "Price (USD)"))
200520062007200820092010Date0100200300400500600700800Price (USD)Google Stock Price

Multiple series by color

Pass a keyword to :color to draw a separate line for each category:

(qplot 'stock-colored stocks `(:title "Stock Prices by Company") (line :date :price :color :symbol :x-type :temporal) (label :x "Date" :y "Price (USD)"))
20002001200220032004200520062007200820092010Date0100200300400500600700800Price (USD)AAPLAMZNGOOGIBMMSFTsymbolStock Prices by Company

Smoothed interpolation

Set :interpolate to control how points are connected. Common values are :linear (default), :monotone (smooth, monotonic curves), :step (step function), :basis (B-spline), and :cardinal:

(qplot 'stock-smooth stocks `(:title "Stock Prices (Smoothed)") (line :date :price :color :symbol :interpolate :monotone :x-type :temporal) (label :x "Date" :y "Price (USD)"))
20002001200220032004200520062007200820092010Date0100200300400500600700800Price (USD)AAPLAMZNGOOGIBMMSFTsymbolStock Prices (Smoothed)

Line with point markers

Set :point t to overlay point marks on each data position — useful for sparse data or when exact values matter. Note that :x-type :temporal is required here because the :year field in vgcars is stored as a date string rather than an integer:

(qplot 'mpg-trend vgcars `(:title "Mean MPG by Model Year") (line :year :miles-per-gallon :point t :x-type :temporal) (label :x "Model Year" :y "Miles per Gallon"))
1970197219741976197819801982Model Year05101520253035404550Miles per GallonMean MPG by Model Year

Custom stroke width

Use :stroke-width to set a fixed line thickness:

(qplot 'stock-thick stocks `(:title "AAPL Stock Price" :transform #((:filter "datum.symbol === 'AAPL'"))) (line :date :price :stroke-width 3 :x-type :temporal) (label :x "Date" :y "Price (USD)") (theme :width 600 :height 300))
20002001200220032004200520062007200820092010Date020406080100120140160180200220240Price (USD)AAPL Stock Price

Dashed lines

Use :stroke-dash with a vector to create dashed or dotted lines. The vector specifies alternating dash and gap lengths:

(qplot 'stock-dashed stocks `(:title "Stock Prices (Dashed)") (line :date :price :color :symbol :stroke-dash #(6 3) :opacity 0.8 :x-type :temporal) (label :x "Date" :y "Price (USD)"))
20002001200220032004200520062007200820092010Date0100200300400500600700800Price (USD)AAPLAMZNGOOGIBMMSFTsymbolStock Prices (Dashed)

Step chart

Use :interpolate :step for piecewise-constant lines — useful for data that changes at discrete intervals (e.g. interest rates, pricing tiers):

(qplot 'mpg-step vgcars `(:title "MPG by Year (Step)") (line :year :miles-per-gallon :interpolate :step :x-type :temporal) (label :x "Model Year" :y "Miles per Gallon"))
1970197219741976197819801982Model Year05101520253035404550Miles per GallonMPG by Year (Step)

Styled multi-series line chart

Combine all options with layering helpers for a polished presentation:

(qplot 'stock-full stocks `(:title "Stock Comparison" :description "Daily closing prices for major tech stocks") (line :date :price :color :symbol :interpolate :monotone :stroke-width 2 :x-type :temporal) (label :x "Date" :y "Closing Price (USD)") (axes :color-scheme :dark2) (tooltip '(:field :symbol :type :nominal) '(:field :date :type :temporal) '(:field :price :type :quantitative)) (theme :width 700 :height 400))
20002001200220032004200520062007200820092010Date0100200300400500600700800Closing Price (USD)AAPLAMZNGOOGIBMMSFTsymbolStock Comparison

Function curves

geom:func plots a Lisp function as a smooth line by evaluating it at evenly-spaced sample points and embedding the resulting (x, y) pairs directly in the Vega-Lite specification. It mirrors the behaviour of R’s geom_function() from ggplot2.

Unlike the data-driven helpers (point, bar, histogram, etc.), func is self-contained: it carries its own :data block and requires no external data frame. Use it anywhere you want to visualise a mathematical relationship — probability densities, regression curves, physical models, or any other computable function.

Import func alongside the other helpers you use:

(import '(geom:func))

How it works

func calls aops:linspace to generate n evenly-spaced x values over the closed interval [xmin, xmax] specified by :xlim. It then calls fn at each x, collects the (x, y) pairs into a vector of plists, and embeds them as an inline Vega-Lite :data block. A :line mark connects the points using the chosen interpolation method (:monotone by default, giving smooth curves without overshoot).

Points where fn signals a condition (e.g. (log 0), (/ 1 0)) or returns a non-finite value (± infinity, NaN) are silently dropped. Vega-Lite renders a visible gap at each discontinuity — the correct visual for functions like tan or 1/x.

Design note: self-contained data

All other geom helpers return only :mark and :encoding keys and rely on the caller to supply :data. func also returns a :data key, because the data is the function. This means it composes slightly differently from the other geom helpers:

Helper Data source Typical entry point
point, bar, histogram, … external data frame qplot
func self-generated inline defplot + vega:merge-plists

For multi-layer plots (function overlaid on data) use Vega-Lite’s :layer array directly inside defplot; see Overlay on scatter data below.

The following table describes the ggplot2 equivalent and responsibility:

ggplot2 Lisp-Stat Package Responsibility
geom_function() func geom Sample a function, encode as a line

Reference

(geom:func fn &key xlim n color stroke-width stroke-dash opacity interpolate)
Parameter Type Default Description
fn function A Lisp function (real → real). Receives a double-float; must return a real.
:xlim vector #(0d0 1d0) Domain #(xmin xmax). Both endpoints are always sampled.
:n integer ≥ 2 100 Number of sample points. Increase for oscillatory functions.
:color string nil CSS color for the line, e.g. "steelblue" or "#e63946". nil lets Vega-Lite choose.
:stroke-width number nil Line thickness in pixels.
:stroke-dash vector nil Dash/gap pattern, e.g. #(6 3) for dashes or #(2 2) for dots.
:opacity number 0–1 nil Line opacity.
:interpolate keyword :monotone Vega-Lite interpolation method. :linear, :basis, :cardinal, :step are also accepted.

Basic function plot

Supply a function and a domain. vega:merge-plists combines the self-contained func layer with a title and axis labels:

(vega:defplot sine-wave (vega:merge-plists `(:title "Sine Wave") (func #'sin :xlim #(-6.283 6.283) :n 200) (label :x "x" :y "sin(x)")))
−6−4−20246x−1.0−0.8−0.6−0.4−0.20.00.20.40.60.81.0sin(x)Sine Wave

Custom domain and resolution

Increase :n for functions that oscillate rapidly. Use :xlim to set the evaluation domain precisely:

(vega:defplot damped-oscillation (vega:merge-plists `(:title "Damped Oscillation") (func (lambda (x) (* (exp (* -0.3 x)) (sin (* 4 x)))) :xlim #(0 20) :n 400) (label :x "t" :y "Amplitude")))
02468101214161820t−0.8−0.6−0.4−0.20.00.20.40.60.81.0AmplitudeDamped Oscillation

Functions with singularities

Points where fn raises a condition or returns ±infinity are silently dropped. Vega-Lite draws a gap at each discontinuity — the correct rendering for functions like tan(x):

(vega:defplot tangent-curve (vega:merge-plists `(:title "tan(x) — gaps at singularities") (func #'tan :xlim #(-4.5 4.5) :n 500) (label :x "x" :y "tan(x)") (axes :y-domain #(-10 10))))
−5−4−3−2−1012345x−10−8−6−4−20246810tan(x)tan(x) — gaps at singularities

Probability density function

Plot a probability density function using the distributions system. Load it before running these examples:

(asdf:load-system :distributions)

Create a distribution object with distributions:r-normal, then pass its pdf method to func. r-normal takes mean and variance (not standard deviation), so the standard normal is (r-normal 0d0 1d0):

(let ((d (distributions:r-normal 0d0 1d0))) ; mean=0, variance=1 (vega:defplot normal-pdf (vega:merge-plists `(:title "Standard Normal PDF") (func (lambda (x) (distributions:pdf d x)) :xlim #(-4 4) :n 200) (label :x "x" :y "Density") (theme :width 500 :height 300))))
−4.0−3.5−3.0−2.5−2.0−1.5−1.0−0.50.00.51.01.52.02.53.03.54.0x0.000.050.100.150.200.250.300.350.40DensityStandard Normal PDF

Styled function curve

Pass :color, :stroke-width, and :stroke-dash for visual polish. To show two styled curves together, use :layer — each func call carries its own inline data:

(vega:defplot sin-and-cos-styled `(:title "sin and cos — styled lines" :data (:values #()) :layer #(,(func #'sin :xlim #(-6.283 6.283) :n 200 :color "steelblue" :stroke-width 2) ,(func #'cos :xlim #(-6.283 6.283) :n 200 :color "firebrick" :stroke-width 2 :stroke-dash #(8 4)))))
−6−4−20246x−1.0−0.8−0.6−0.4−0.20.00.20.40.60.81.0ysin and cos — styled lines

Step interpolation

Use :interpolate :step for piecewise-constant functions — useful for visualising floor, ceiling, or any discrete-valued function:

(vega:defplot step-function (vega:merge-plists `(:title "Floor function") (func #'ffloor :xlim #(0 6) :n 300 :interpolate :step :color "darkslategray" :stroke-width 2) (label :x "x" :y "floor(x)")))

Two functions on the same axes

Use Vega-Lite’s :layer array directly inside defplot. Each func call produces an independent layer with its own inline data:

(vega:defplot sin-vs-cos `(:title "sin(x) and cos(x)" :data (:values #()) :layer #(;; Layer 1 — sine ,(func #'sin :xlim #(-6.283 6.283) :n 200 :color "steelblue" :stroke-width 2) ;; Layer 2 — cosine ,(func #'cos :xlim #(-6.283 6.283) :n 200 :color "firebrick" :stroke-width 2 :stroke-dash #(6 3)))))
−6−4−20246x−1.0−0.8−0.6−0.4−0.20.00.20.40.60.81.0ysin(x) and cos(x)

Family of curves

Plot a parameterised family by building the :layer vector in a loop:

;; Gaussian PDFs with increasing standard deviations (let* ((sigmas #(0.5 1.0 1.5 2.0)) (colors #("steelblue" "seagreen" "darkorange" "firebrick")) (layers (map 'vector (lambda (sigma color) (func (lambda (x) (* (/ 1 (* sigma (sqrt (* 2 pi)))) (exp (* -0.5 (expt (/ x sigma) 2))))) :xlim #(-5 5) :n 200 :color color)) sigmas colors))) (vega:defplot gaussian-family `(:title "Gaussian PDFs for sigma = 0.5, 1, 1.5, 2" :data (:values #()) :layer ,layers)))
−5−4−3−2−1012345x0.00.10.20.30.40.50.60.70.8yGaussian PDFs for sigma = 0.5, 1, 1.5, 2

Overlay on scatter data

When overlaying a function on top of data, each layer supplies its own :data. The data layer uses the original data frame; the function layer uses the inline data generated by func:

(vega:defplot cars-with-trend `(:title "HP vs MPG with Quadratic Trend" :data (:values #()) :layer #(;; Layer 1: raw data as a scatter plot (:data (:values ,vgcars) :mark (:type :point :filled t :opacity 0.5) :encoding (:x (:field :horsepower :type :quantitative :title "Horsepower") :y (:field :miles-per-gallon :type :quantitative :title "Miles per Gallon") :color (:field :origin :type :nominal))) ;; Layer 2: fitted quadratic y = 52 - 0.23x + 3e-4*x^2 ,(func (lambda (x) (+ 52.0d0 (* -0.23d0 x) (* 3.0d-4 (expt x 2)))) :xlim #(40 230) :n 300 :color "firebrick" :stroke-width 2.5))))
020406080100120140160180200220240Horsepower05101520253035404550Miles per GallonEuropeJapanUSAoriginHP vs MPG with Quadratic Trend

Normal distribution fit over a histogram

Overlay the theoretical PDF on an empirical histogram. Use select:select to extract the column as a vector, statistics:sd for the standard deviation, and distributions:r-normal to construct the fitted distribution — recall that r-normal takes the variance, so pass (expt sigma 2):

(let* ((mpg (select:select vgcars t :miles-per-gallon)) (mu (statistics:mean mpg)) (sigma (statistics:sd mpg)) (d (distributions:r-normal mu (expt sigma 2)))) (vega:defplot mpg-fit `(:title "MPG: Empirical Histogram with Normal Fit" :data (:values #()) :layer #(;; Histogram layer (:data (:values ,vgcars) :mark :bar :encoding (:x (:field :miles-per-gallon :bin (:maxbins 15) :type :quantitative :title "Miles per Gallon") :y (:aggregate :count :stack :null :type :quantitative :title "Count"))) ;; Density curve — scaled by (n × bin-width) to match count axis ,(func (lambda (x) (* 406 3 (distributions:pdf d x))) :xlim #(5 50) :n 300 :color "firebrick" :stroke-width 2)))))
5101520253035404550Miles per Gallon0102030405060708090100CountMPG: Empirical Histogram with Normal Fit

Chebyshev approximation

numerical-utilities provides chebyshev-regression and evaluate-chebyshev for polynomial approximation. Plot the exact function and its approximation together to inspect accuracy:

(let* ((coeffs (nu:chebyshev-regression (lambda (x) (exp (- (nu:square x)))) 12)) ; 12-term approximation (approx (lambda (x) (nu:evaluate-chebyshev coeffs x)))) (vega:defplot chebyshev-approx `(:title "exp(-x^2): Exact vs 12-Term Chebyshev Approximation" :data (:values #()) :layer #(,(func (lambda (x) (exp (- (nu:square x)))) :xlim #(-1 1) :n 200 :color "steelblue" :stroke-width 2) ,(func approx :xlim #(-1 1) :n 200 :color "firebrick" :stroke-width 1.5 :stroke-dash #(6 3))))))
−1.0−0.8−0.6−0.4−0.20.00.20.40.60.81.0x0.00.10.20.30.40.50.60.70.80.91.0yexp(-x^2): Exact vs 12-Term Chebyshev Approximation

Combining layers across plot types

Because every helper returns an independent plist, you can mix and match freely. Here are patterns that work with all mark types.

Labels on any plot

label works identically on every plot type — it only touches :encoding :x/:y :axis :title. The examples below produce the same visual results as the per-section examples above; they are shown here to illustrate that label requires no changes across mark types:

;; On a scatter plot (qplot 'point-labeled vgcars (point :horsepower :miles-per-gallon) (label :x "Engine Horsepower" :y "Fuel Efficiency"))
;; On a histogram (qplot 'hist-labeled vgcars (histogram :horsepower) (label :x "Engine Horsepower" :y "Number of Cars"))
;; On a bar chart (qplot 'bar-labeled vgcars (bar :origin :miles-per-gallon :aggregate :mean) (label :x "Country of Origin" :y "Average MPG"))
;; On a box plot (qplot 'box-labeled vgcars (box-plot :miles-per-gallon :category :origin) (label :x "Fuel Efficiency" :y "Manufacturing Origin"))

Theme on any plot

theme sets top-level properties that apply to every plot type:

(qplot 'hist-themed vgcars `(:title "Horsepower Distribution") (histogram :horsepower :color "darkslategray") (label :x "Horsepower" :y "Count") (theme :width 500 :height 300 :font "Georgia"))
406080100120140160180200220240Horsepower020406080100120CountHorsepower Distribution

coord with continuous axes

coord works best with quantitative (continuous) axes. For bar charts and box plots whose axes are categorical, use coord with :clip nil to avoid cutting marks, or use axes with :x-domain or :y-domain instead:

;; Zoom a scatter plot — clip marks outside the viewport (coord :x-domain #(50 150) :y-domain #(20 40)) ;; Restrict a bar chart's quantitative axis — no clipping (coord :y-domain #(0 35) :clip nil) ;; Alternatively, use axes for categorical axes (axes :y-domain #(0 35))

Recipe: exploring a new dataset

When you first load a dataset, a quick exploratory sequence might look like this. Note how the same plot name can be reused — each qplot call overwrites the previous definition.

Step 1 — Distribution of a key variable

Start with a histogram to understand the shape of the data:

(qplot 'explore-1 vgcars `(:title "Horsepower Distribution") (histogram :horsepower))
406080100120140160180200220240horsepower (binned)020406080100120Count of RecordsHorsepower Distribution

Step 2 — Scatter plot of two main variables

Look for relationships between variables:

(qplot 'explore-2 vgcars `(:title "HP vs. MPG") (point :horsepower :miles-per-gallon :filled t))
020406080100120140160180200220240horsepower05101520253035404550milesPerGallonHP vs. MPG

Step 3 — Compare groups

Break the data down by category to see if patterns differ:

(qplot 'explore-3 vgcars `(:title "MPG by Origin") (box-plot :miles-per-gallon :category :origin :orient :vertical) (label :x "Origin" :y "MPG"))
EuropeJapanUSAOrigin5101520253035404550MPGMPG by Origin

Step 4 — Add detail

Color by group and add labels to confirm the story:

(qplot 'explore-4 vgcars `(:title "HP vs. MPG by Origin") (point :horsepower :miles-per-gallon :color :origin :filled t) (label :x "Horsepower" :y "Miles per Gallon") (tooltip '(:field :name :type :nominal) '(:field :origin :type :nominal) '(:field :horsepower :type :quantitative) '(:field :miles-per-gallon :type :quantitative)))
020406080100120140160180200220240Horsepower05101520253035404550Miles per GallonEuropeJapanUSAoriginHP vs. MPG by Origin

At the REPL, every call overwrites the same explore variable. The variable always holds the latest version:

explore ; => #<VEGA-PLOT ...> (show-plots) ; one entry for EXPLORE

Recipe: presentation-ready plot

For reports or publications, combine all the layering helpers:

(qplot 'presentation vgcars `(:title "Automobile Performance by Country of Origin" :description "Horsepower vs fuel efficiency for 406 cars") (point :horsepower :miles-per-gallon :color :origin :filled t) (label :x "Engine Horsepower" :y "Fuel Efficiency (MPG)") (axes :color-scheme :tableau10) (tooltip '(:field :name :type :nominal) '(:field :horsepower :type :quantitative) '(:field :miles-per-gallon :type :quantitative)) (theme :width 700 :height 450 :font "Helvetica"))
0102030405060708090100110120130140150160170180190200210220230240Engine Horsepower05101520253035404550Fuel Efficiency (MPG)EuropeJapanUSAoriginAutomobile Performance by Country of Origin

Recipe: saving a plot for later

Once you are happy with a plot created via qplot, it is a first-class vega-plot object. You can re-render, inspect, or write it to a file. This example assumes presentation was created in the recipe above:

;; Re-render in the browser (plot:plot presentation) ;; Inspect the spec (describe presentation) ;; List all named plots (show-plots)

Recipe: exploring a function

A typical REPL workflow when investigating a mathematical function. Note how the same plot name is reused — each defplot call overwrites the previous definition.

Step 1 — Quick sketch over the natural domain

(vega:defplot explore-fn (vega:merge-plists `(:title "First look") (func (lambda (x) (/ (sin x) x)) ; sinc :xlim #(-20 20) :n 400)))

Step 2 — Zoom in on a region of interest

(vega:defplot explore-fn (vega:merge-plists `(:title "sinc(x) — detail near origin") (func (lambda (x) (/ (sin x) x)) :xlim #(-6.283 6.283) :n 400) (label :x "x" :y "sin(x)/x") (axes :y-domain #(-0.3 1.1))))

Step 3 — Presentation polish

(vega:defplot sinc-final (vega:merge-plists `(:title "sinc(x) = sin(x)/x" :description "Classic sinc function over [-2pi, 2pi]") (func (lambda (x) (/ (sin x) x)) :xlim #(-6.283 6.283) :n 500 :color "steelblue" :stroke-width 2) (label :x "x" :y "sin(x) / x") (theme :width 600 :height 350 :font "Georgia")))

Recipe: comparing model fits

Overlay multiple fitted curves on a scatter plot to compare competing models visually:

(let* ((linear (lambda (x) (+ 39.9 (* -0.158 x)))) (quadratic (lambda (x) (+ 52.0 (* -0.23 x) (* 3.0e-4 (expt x 2)))))) (vega:defplot model-comparison `(:title "Linear vs Quadratic Fit" :data (:values #()) :layer #(;; Raw data (:data (:values ,vgcars) :mark (:type :point :filled t :color "lightgray") :encoding (:x (:field :horsepower :type :quantitative :title "Horsepower") :y (:field :miles-per-gallon :type :quantitative :title "Miles per Gallon"))) ;; Linear fit ,(func linear :xlim #(40 230) :n 200 :color "steelblue" :stroke-width 2) ;; Quadratic fit ,(func quadratic :xlim #(40 230) :n 200 :color "firebrick" :stroke-width 2 :stroke-dash #(6 3))))))

Recipe: dropping down to raw Vega-Lite

The helpers cover common patterns. When you need something they don’t support — transforms, selections, parameters, calculated fields, multi-view compositions — you can write raw Vega-Lite directly.

Example: brush selection with linked data table

This example reproduces the Vega-Lite brush table: drag a rectangle over the scatter plot to see the selected cars in a table. It uses hconcat (side-by-side views), params (interactive brush), and transform (filter + rank) — none of which the helpers generate. For specs this complex, write the full Vega-Lite plist and use defplot directly:

(plot:plot (vega:defplot brush-table `(:description "Drag a rectangular brush to show selected points in a table." :data (:values ,vgcars) :transform #((:window #((:op :row-number :as "row_number")))) :hconcat #(;; Left panel: scatter plot with brush (:params #((:name "brush" :select "interval")) :mark :point :encoding (:x (:field :horsepower :type :quantitative) :y (:field :miles-per-gallon :type :quantitative) :color (:condition (:param "brush" :field :cylinders :type :ordinal) :value "grey"))) ;; Right panel: table of selected points (:transform #((:filter (:param "brush")) (:window #((:op :rank :as "rank"))) (:filter (:field "rank" :lt 20))) :hconcat #((:width 50 :title "Horsepower" :mark :text :encoding (:text (:field :horsepower :type :nominal) :y (:field "row_number" :type :ordinal :axis :null))) (:width 50 :title "MPG" :mark :text :encoding (:text (:field :miles-per-gallon :type :nominal) :y (:field "row_number" :type :ordinal :axis :null))) (:width 50 :title "Origin" :mark :text :encoding (:text (:field :origin :type :nominal) :y (:field "row_number" :type :ordinal :axis :null)))))) :resolve (:legend (:color :independent)))))
020406080100120140160180200220240horsepower05101520253035404550milesPerGallon34568cylinders8279845286908496112928511067676770758870Horsepower31283244272736322226382538323834363636MPGUSAUSAUSAEuropeUSAUSAUSAJapanUSAUSAUSAUSAJapanJapanJapanJapanJapanJapanUSAOrigin

This plot cannot be built with qplot and the layering helpers; the hconcat layout requires two independent views with different marks, encodings, and transforms. The rule of thumb:

  • Use qplot + helpers for single-view plots (scatter, line, bar, histogram, box plot) with standard encodings
  • Use defplot + raw plists for multi-view layouts (hconcat, vconcat, layer, facet), complex interactions, or any Vega-Lite feature the helpers don’t cover

Both approaches produce the same vega-plot objects and coexist in the same session.