using Plots
using StatsPlots # for distribution plots; re-exports Plots
using CairoMakie # the vector-graphics Makie backend
using LaTeXStrings
using Measures
using Random
Random.seed!(1);Making Plots with Julia
Introduction
There are several ways to make plots in Julia. The two most widely-used graphing ecosystems are:
Plots.jl— a high-level interface that can target many different rendering backends. You write the samePlots.jlcode regardless of which backend you choose.Makie.jl— a modern, GPU-accelerated plotting system that gives you very fine control over every visual element. Its most popular backend for publication-quality vector output isCairoMakie.jl.
Other notable libraries include VegaLite.jl (a declarative grammar-of-graphics inspired by ggplot2) and Gadfly.jl.
Plots.jl and Its Backends
Plots.jl itself does not draw anything — it hands off the rendering to a backend. Several backends are available: 1
1 This tutorial focuses on GR and PythonPlot; see the Plots.jl backend gallery for the full list.
- GR — the default backend. Fast, simple, and works out of the box.
- PythonPlot — uses Python’s
matplotlibviaPythonCall.jl. If you already knowmatplotlib, this backend may feel the most familiar. - PlotlyJS — interactive, web-based plots using plotly.js.
- PGFPlotsX — high-quality LaTeX/TikZ output for publications.
- UnicodePlots — plots directly in the terminal using Unicode characters.
This tutorial will give examples of making plots using Plots.jl (with the GR backend), and will show how the PythonPlot backend and Makie.jl produce equivalent results. Relevant documentation resources are listed below.
Some Resources
Plots.jl
Makie.jl
PythonPlot
Color and styling
Setup
Since we’ll be generating random numbers, load the packages and set a seed for reproducibility.
Plots.jl Backends and Makie
Plots.jl is more of a common interface for several different plotting ecosystems (called backends) than a self-contained plotting package. By default, Plots.jl uses the GR backend, which is pretty basic and fast; I will use this in class for simplicity. You switch backends at any time by calling their name, e.g. gr() or pythonplot().
You can and should feel free to use other backends if you find them more intuitive or useful for the plot(s) you are trying to make. In particular, you might find PythonPlot simple to use if you feel comfortable with matplotlib in Python. The downside to PythonPlot is that it relies on Python to render the plots; if you don’t have Python installed, CondaPkg.jl will install a private copy automatically (which can take a few minutes the first time).
Below, each plot example is shown in a tabset for three approaches: the GR backend, the PythonPlot backend, and Makie.jl.
Because this document has both Plots.jl and Makie loaded at once, a few function names that exist in both packages (such as scatter and heatmap) are written as Makie.scatter and Makie.heatmap in the Makie tabs. In your own scripts, where you would normally load only one of the two, you can drop the Makie. prefix.
Making a Basic Plot
Let’s walk through making a basic line plot.
First, to generate a basic line plot, use plot():
x = 1:5
y = rand(length(x))
plot(x, y, label="Original Data", legend=:topleft)- 1
-
This creates
xas a range of integers between 1 and 5 (inclusive of both endpoints). You can use arbitrary steps with syntax likex = 1:0.1:5. To turnxinto a vector instead of a range, you can usecollect(x), but this is not needed for plotting (Julia does this under the hood). - 2
-
yis a vector of random values (uniformly distributed between 0 and 1) with the same length asx. This type of syntax is better than hard-coding the length intoy, since you might want to use different lengths. - 3
-
We use two different arguments related to constructing the legend:
labelsets the text associated with the line we just created (to not include an element in the legend, uselabel=false), andlegendeither sets the position of the legend (in this case, at the top left) or can be used to turn off the legend (withlabel=false). But there are many others you could use to customize the plot.
Here, we explicitly passed in x to provide the \(x\) coordinates for the plotted values. If only one array is passed2, Plots.jl will interpret the values as \(y\) coordinates and use their indices for the \(x\) positions.
2 This syntax might look like plot(y, ...) instead of plot(x, y, ...).
Now add more series and axis labels.
The exclamation mark says that plot!() is a mutating function, which changes an existing plot instead of creating a new one. Try removing the ! — you will get a new plot that no longer contains the old elements.
y2 = rand(5)
y3 = rand(5)5-element Vector{Float64}:
0.5710874493423871
0.4528085872833483
0.30232547191787174
0.0013502779247226426
0.5670236732404312
gr()
plot(x, y, label="Original Data")
plot!(x, y2, color=:red, linewidth=2, linestyle=:dot, label="New Data")
scatter!(x, y3, markercolor=:black, markershape=:square, markersize=5, label="Point Data")
xlabel!("Regular String (days)")
ylabel!(L"LaTeX String $x_2$ (m$^3$)")pythonplot()
plot(x, y, label="Original Data")
plot!(x, y2, color=:red, linewidth=2, linestyle=:dot, label="New Data")
scatter!(x, y3, markercolor=:black, markershape=:square, markersize=5, label="Point Data")
xlabel!("Regular String (days)")
ylabel!(L"LaTeX String $x_2$ (m$^3$)")fig = Figure()
ax = Axis(fig[1, 1], xlabel="Regular String (days)", ylabel=L"LaTeX String $x_2$ (m$^3$)")
lines!(ax, x, y, label="Original Data")
lines!(ax, x, y2, color=:red, linewidth=2, linestyle=:dot, label="New Data")
Makie.scatter!(ax, x, y3, color=:black, marker=:rect, markersize=10, label="Point Data")
axislegend(ax, position=:lt)
figRemoving Plot Elements
Sometimes we want to remove legends, axes, grid lines, and/or ticks.
plot!(legend=false, axis=false, grid=false, ticks=false)Notice that this unintentionally modified the image dimensions to move the axis labels off the page. If we wanted to keep them, we could modify the dimensions with plot!(size=...).
plot!(size=(400, 400))The lesson is that sometimes the Plots.jl defaults don’t look ideal, and we need to adjust sizes and margins. Don’t shy away from these tweaks if they make your figures easier to read or interpret!
Aspect Ratio
For a square aspect ratio, Plots.jl uses ratio = 1; Makie.jl uses aspect = AxisAspect(1).
v = rand(5)5-element Vector{Float64}:
0.6159379234562881
0.19573857852575793
0.012461945950411835
0.3119923865097316
0.11479916823306191
gr()
plot(v, ratio=1, legend=false)
scatter!(v)pythonplot()
plot(v, ratio=1, legend=false)
scatter!(v)fig = Figure()
ax = Axis(fig[1, 1], aspect=AxisAspect(1))
lines!(ax, v)
Makie.scatter!(ax, v)
figPlot Demos
This section includes some examples of how to make other types of plots.
Heatmaps
A heatmap is a plotted matrix whose cells are colored by value. Use clim (Plots) or colorrange (Makie) to fix the color scale.
A = rand(10, 10)- 1
- Create a random 10×10 matrix, but this could come from actual data.
10×10 Matrix{Float64}:
0.742414 0.108674 0.0909513 0.818597 … 0.239896 0.96551 0.229329
0.65913 0.265374 0.914553 0.859535 0.327377 0.81217 0.575938
0.360275 0.699443 0.590472 0.375186 0.109344 0.430081 0.243464
0.892222 0.036808 0.967915 0.947293 0.51529 0.599123 0.342309
0.337329 0.633275 0.673935 0.373811 0.0913972 0.82805 0.835509
0.671766 0.295391 0.228969 0.0223076 … 0.585548 0.592562 0.0590473
0.534097 0.348069 0.46191 0.110385 0.120938 0.982151 0.551654
0.655354 0.427535 0.577283 0.226571 0.628025 0.439115 0.139791
0.404693 0.60398 0.139681 0.857178 0.59816 0.938309 0.540769
0.703556 0.641963 0.129069 0.0568809 0.717561 0.593831 0.775324
gr()
heatmap(A, clim=(0, 1))pythonplot()
heatmap(A, clim=(0, 1))fig = Figure()
ax = Axis(fig[1, 1])
hm = Makie.heatmap!(ax, A, colorrange=(0, 1))
Colorbar(fig[1, 2], hm)
figM = [ 0 1 0; 0 0 0; 1 0 0]
whiteblack = [RGBA(1,1,1,0), RGB(0,0,0)]
heatmap(M, c=whiteblack, aspect_ratio = 1, ticks=.5:3.5, lims=(.5,3.5), gridalpha=1, legend=false, axis=false, ylabel="i", xlabel="j")- 1
-
This creates a vector of colors, so
0(the lower value) will map towhiteblack[1](which is white) and1will map towhiteblack[2](black). The specific0and1values don’t matter for this syntax, just that there are two distinct values; the lower one will always be mapped to white. Try changing the values ofMto1and2!
Custom Colormaps
Use a custom set of colors instead of the default gradients.
using Colors
mycolors = [colorant"lightslateblue",colorant"limegreen",colorant"red"]
A2 = [i for i=50:300, j=1:100]- 1
-
Colors.jlprovides many named colors. Thecolorantfunction converts a name to an RGB value. - 2
- This comprehension creates a 251×100 array where each row’s value increases from 50 to 300.
251×100 Matrix{Int64}:
50 50 50 50 50 50 50 50 … 50 50 50 50 50 50 50
51 51 51 51 51 51 51 51 51 51 51 51 51 51 51
52 52 52 52 52 52 52 52 52 52 52 52 52 52 52
53 53 53 53 53 53 53 53 53 53 53 53 53 53 53
54 54 54 54 54 54 54 54 54 54 54 54 54 54 54
55 55 55 55 55 55 55 55 … 55 55 55 55 55 55 55
56 56 56 56 56 56 56 56 56 56 56 56 56 56 56
57 57 57 57 57 57 57 57 57 57 57 57 57 57 57
58 58 58 58 58 58 58 58 58 58 58 58 58 58 58
59 59 59 59 59 59 59 59 59 59 59 59 59 59 59
⋮ ⋮ ⋱ ⋮
292 292 292 292 292 292 292 292 292 292 292 292 292 292 292
293 293 293 293 293 293 293 293 293 293 293 293 293 293 293
294 294 294 294 294 294 294 294 294 294 294 294 294 294 294
295 295 295 295 295 295 295 295 … 295 295 295 295 295 295 295
296 296 296 296 296 296 296 296 296 296 296 296 296 296 296
297 297 297 297 297 297 297 297 297 297 297 297 297 297 297
298 298 298 298 298 298 298 298 298 298 298 298 298 298 298
299 299 299 299 299 299 299 299 299 299 299 299 299 299 299
300 300 300 300 300 300 300 300 … 300 300 300 300 300 300 300
gr()
heatmap(A2, c=mycolors, clim=(50, 300))pythonplot()
heatmap(A2, c=mycolors, clim=(50, 300))fig = Figure()
ax = Axis(fig[1, 1])
hm = Makie.heatmap!(ax, A2, colormap=mycolors, colorrange=(50, 300))
Colorbar(fig[1, 2], hm)
figArea Under a Curve
areaplot() fills the region between a curve and the x-axis. The Makie equivalent is band! with a zero lower bound.
xa = -3:0.01:3
fa = exp.(-xa.^2/2) / √(2π)601-element Vector{Float64}:
0.0044318484119380075
0.004566589954670145
0.004704957526933979
0.004847032905978945
0.004992899213612376
0.005142640923053939
0.00529634386531102
0.005454095235056545
0.005615983595990969
0.005782098885669473
⋮
0.005615983595990969
0.005454095235056545
0.00529634386531102
0.005142640923053939
0.004992899213612376
0.004847032905978945
0.004704957526933979
0.004566589954670145
0.0044318484119380075
gr()
areaplot(xa, fa, alpha=0.25, legend=false)pythonplot()
areaplot(xa, fa, alpha=0.25, legend=false)fig = Figure()
ax = Axis(fig[1, 1])
band!(ax, xa, fill(0.0, length(xa)), fa, color=(:blue, 0.25))
figStacked Area Plots
We can stack multiple area series on top of each other by passing a matrix to areaplot(), where each column (or row) represents one layer. The layers are stacked cumulatively. A practical example: visualize the monthly energy use of four sources over three years.
years = ["2021", "2022", "2023"]
# Each row is a source; each column is a year.
# Sources: Solar, Wind, Hydro, Coal (values in GWh)
energy = [
120 180 260; # Solar (growing)
200 220 240; # Wind
150 140 130; # Hydro (declining)
80 60 40 # Coal (declining)
]
sources = ["Solar", "Wind", "Hydro", "Coal"]
p = areaplot(1:3, energy', # transpose so each column = layer
seriescolor = [:orange :skyblue :steelblue :gray],
fillalpha = [0.6, 0.5, 0.4, 0.5],
labels = reshape(sources, 1, :), # one label per layer
legend = :topleft,
xticks = (1:3, years),
xlabel = "Year",
ylabel = "Energy Use (GWh)"
)When you pass a matrix, each column becomes its own stacked layer. The seriescolor and fillalpha vectors control the color and transparency per layer. The labels must be a row matrix (reshape(sources, 1, :)) to match the matrix orientation. Try experimenting — change the values or colors to see how the plot adjusts.
The fillrange option lets us color the area between two arbitrary lines/curves if we only want to treat one of those curves as a boundary. fillcolor and fillalpha let you change the color and transparency of the filled area.
y = rand(10)
plot(y, fillrange= y.*0 .+ .5, label= "above/below 1/2", fillcolor=:red, legend =:top)Confidence Band
Use fillrange to color the region between two curves, e.g. to show a confidence band.
xb = LinRange(0, 2, 100)
y1 = exp.(xb)
y2 = exp.(1.3 .* xb)- 1
-
LinRangecreates a range from 0 to 2 with 100 evenly spaced points.
100-element Vector{Float64}:
1.0
1.0266105279586968
1.0539291761156344
1.0819747879231458
1.1107667082677797
1.1403247968137291
1.1706694417013364
1.2018215736101217
1.233802680196039
1.2666348209129108
⋮
10.912391625589951
11.20277612803896
11.500887915409168
11.806932614831997
12.121121325285433
12.443670763202702
12.774803411955727
13.11474767531643
13.463738035001692
gr()
plot(xb, y1, fillrange=y2, fillalpha=0.35, c=1, label="Confidence band", legend=:topleft)pythonplot()
plot(xb, y1, fillrange=y2, fillalpha=0.35, c=1, label="Confidence band", legend=:topleft)fig = Figure()
ax = Axis(fig[1, 1])
band!(ax, xb, y1, y2, color=(:blue, 0.35), label="Confidence band")
lines!(ax, xb, y1, label="Estimate")
axislegend(ax, position=:lt)
figWe can also get more creative and color different parts of a curve differently. Here, we divide a normal distribution into 100 quantiles and alternate red and blue stripes. We’ll do this using the erfinv() function from SpecialFunctions.jl to calculate the quantiles using the inverse cumulative distribution function, but there are other approaches using Distributions.jl.
using SpecialFunctions
# write a function for the normal distribution density
f(x) = exp(-x^2/2)/√(2π)
# get the edges of the quantiles
δ = .01
x = √2 .* erfinv.(2 .* (δ/2 : δ : 1) .- 1)
# make the plot and draw the density line in black
areaplot(x, f.(x), seriescolor=[ :red,:blue], legend=false)
plot!(x, f.(x),c=:black)Plotting Shapes
We can also draw shapes more directly, such as rectangles and circles.
rectangle(w, h, x, y) = Shape(x .+ [0,w,w,0], y .+ [0,0,h,h])
circle(r,x,y) = (θ = LinRange(0,2π,500); (x.+r.*cos.(θ), y.+r.*sin.(θ)))
plot(circle(5,0,0), ratio=1, c=:red, fill=true)
plot!(rectangle(5*√2,5*√2,-2.5*√2,-2.5*√2),c=:white,fill=true,legend=false)Plotting Distributions
StatsPlots.jl adds distribution-plotting recipes. Makie.jl requires computing the PDF explicitly with Distributions.jl.
using Distributions
d = Normal(2, 5)
xs_dist = range(-15, 20, length=300)-15.0:0.11705685618729098:20.0
gr()
plot(d)pythonplot()
plot(d)fig = Figure()
ax = Axis(fig[1, 1], xlabel="x", ylabel="pdf(x)")
lines!(ax, xs_dist, pdf.(d, xs_dist))
figKernel Density Estimate
samples = rand(d, 2000)2000-element Vector{Float64}:
1.5245899213514686
4.24310012818435
3.4922212945438504
4.857580339253022
-1.0847650373991686
0.34742627716202557
-3.8740430084774573
0.6981636365869883
-1.7086625454229645
0.5531704345757209
⋮
2.519087067670294
0.2482312995844831
9.895844005945166
6.946334488918343
9.527402479478546
3.6776118640373947
9.825093587382582
3.1282935080762977
-1.4111479173941746
gr()
density(samples, label="KDE", legend=:topleft)pythonplot()
density(samples, label="KDE", legend=:topleft)fig = Figure()
ax = Axis(fig[1, 1], xlabel="Value", ylabel="Density")
Makie.density!(ax, samples)
figDataFrame Density
StatsPlots.jl also integrates with DataFrames.jl:
using DataFrames
dat = DataFrame(a = 1:10, b = 10 .+ rand(10), c = 10 .* rand(10))
@df dat density([:b :c], color=[:black :red])- 1
-
@dfis an example of a macro, which modifies the subsequent function. There are many other examples of macros in Julia.
Log-Scaled Axes
xx = 0.1:0.1:100.1:0.1:10.0
gr()
plot(xx, xx.^2, xaxis=:log, yaxis=:log, legend=false)pythonplot()
plot(xx, xx.^2, xaxis=:log, yaxis=:log, legend=false)fig = Figure()
ax = Axis(fig[1, 1], xscale=log10, yscale=log10)
lines!(ax, xx, xx .^ 2)
figplot(exp.(x), yaxis=:log)Editing Plots Manually
Now let’s look at how to modify plot attributes directly.
pl = plot(1:4,[1, 4, 9, 16])To get a list of properties we can modify, use p.attr:
pl.attrRecipesPipeline.DefaultsDict with 30 entries:
:dpi => 96
:background_color_outside => :match
:plot_titlefontvalign => :vcenter
:warn_on_unsupported => true
:background_color => RGBA{Float64}(1.0, 1.0, 1.0, 1.0)
:inset_subplots => nothing
:size => (672, 480)
:display_type => :auto
:overwrite_figure => true
:html_output_format => :auto
:plot_titlefontfamily => :match
:plot_titleindex => 0
:foreground_color => RGB{N0f8}(0.0, 0.0, 0.0)
:window_title => "Plots.jl"
:plot_titlefontrotation => 0.0
:extra_plot_kwargs => Dict{Any, Any}()
:pos => (0, 0)
:plot_titlefonthalign => :hcenter
:tex_output_standalone => false
⋮ => ⋮
We can also directly access properties of the plotted series.
pl.series_list[1]Plots.Series(RecipesPipeline.DefaultsDict(:plot_object => Plot{Plots.PythonPlotBackend() n=1}, :subplot => Subplot{1}, :label => "y1", :serieshandle => Any[<py [<matplotlib.lines.Line2D object at 0x33c625290>]>], :fillalpha => nothing, :linealpha => nothing, :linecolor => RGBA{Float64}(0.0, 0.6056031704619725, 0.9786801190138923, 1.0), :x_extrema => (NaN, NaN), :series_index => 1, :markerstrokealpha => nothing…))
pl[:size]=(300,200)- 1
-
This is a little contrived: we could just use
plot!(pl, size=(300, 200))to do the same thing.
(300, 200)
plSaving Plots
To save plots to a file for inclusion in a report writeup:
save("figure.png", p)