Skip to content

Plotting

Makie.jl (1.7)

The general flow is:

  • make a Figure
  • make an Axis
  • plot into the Axis

Once an Axis is "assigned" a Figure, it cannot be moved to another Figure. Therefore, plotting functions should take a Figure input argument.

Axes

Moving xlabel. The idea to simply place a Label into a separate layout position:

Label(f[2, 1], "Test ", halign = :right, tellwidth = false)

Subplots

It helps to write plotting functions that take a Figure as an input:

# Make a stand-alone figure. Could be omitted.
function myplot(x, y; kwargs...)
   fig = Figure();
   myplot!(f, x, y; kwargs...)
   return fig
end

# Plot into existing Figure.
function myplot!(fig :: Figure, x, y; pos = (1,1), kwargs...)
   ax = fig[pos...] = Axis(fig; xlabel = "x");
   myplot!(ax, x, y; kwargs...);
   return ax
end

# Plot into existing Axis
function myplot!(ax :: Axis, x, y; kwargs...)
   lines!(ax, x, y; kwargs...);
end

# Now we can make a stand-alone figure with
fig = Figure();
myplot!(fig, x, y);
# Or we can make a subplot with
fig = Figure();
myplot!(fig, x, y; pos = (2,1));

Limitations: The kwargs cannot be used to create the axis.

Looping over axes

One way of creating a figure with subplots is:

fig = Figure();
nr = 2; nc = 3;
for ir = 1:nr, ic = 1:nc
   ax = fig[ir, ic] = Axis(fig);
end

This fixes the layout. Now we can iterate over the subplots with

ind2sub(j, sz) = Tuple(CartesianIndices(sz)[j]);
for j = 1 : (nr * nc)
    fig[ind2sub(j, (nr,nc))...]
end

The roundabout way is necessary because Figure does not support linear or cartesian indexing.

Grouped bar graphs

Building up a grouped bar graph by plotting each series one at a time does not work. The first series bar is too wide (it covers the width of the entire group of bars). One could set it by hand, but it is not clear how.

axislegend does not work for grouped bar graphs (lacking labels).

Keyword arguments

As for 0.8, CairoMakie no longer ignores invalid keyword arguments. One approach is to provide all kwargs to the main function. It then splits the args into valid args for Figure, Axis, lines, etc.

nothing works with keyword arguments such as title.

ylims appears to be ignored. Need to use ylims!(ax, lims). Unspecified bounds are set to nothing (not Inf).

Legends

A legend outside of the plot area is created by providing a new figure position (but not an Axis) to Legend, as in Legend(fig[1,2], ...). The position often spans one dimension, as in fig[:, 2].

Missing values

NaN values are simply not plotted.

Text boxes

One way of adding a text box to a figure:

gl = GridLayout(fig[1,1],
                    tellwidth = false, tellheight = false,
                    halign = :center, valign = :top)
box = Box(gl[1, 1], color = (:orange,0.5), strokecolor = :black)
lbl = Label(gl[1, 1], "Box text", padding = (10, 10, 20, 20))

Themes

update_theme!(thm, Lines = (linewidth = 4,)) creates a new "section" thm.Lines that applies to lines.

update_theme!(thm, Lines = (linestyle = :dash,)) merges the new information into the existing Lines section. It does not replace previous content.

Text sizes are determined by separate attributes for all elements; e.g., xticklabelsize.

Setting line colors from the theme's colormap:

thm = theme_dark();
update_theme!(thm; colormap = ColorSchemes.leonardo);
function make_figure()
   fig = Figure();
   ax = fig[1,1] = Axis(fig);
   for j = 1 : 4; 
      lines!(ax, 1:10, (1:10) .^ (1/j); color = fill(j, 10), colorrange = (1,4)); 
   end
   return fig
end
fig = with_theme(make_figure, thm);

Errors in theme settings cause merge! errors when plotting that bear no obvious relationship to themes.

AlgebraOfGraphics

Example of a weighted density plot that stacks multiple densities and groups data:

using AlgebraOfGraphics, CairoMakie, Random;
# y = weights, z = lines, grp = groups (subplots)
df = (x=randn(5000), y=1.0 .+ rand(5000), z=rand(["a", "b", "c"], 5000), grp=rand(["g1", "g2"], 5000));
specs = data(df) * mapping(:x, layout=:z, color = :grp) * AlgebraOfGraphics.density(datalimits = ((-2.5, 2.5),));
fig = draw(specs);

Results in 3 subplots (one for each z value). Each subplot contains 2 density line graphs (one for each grp value). Legends are automatic. Dense, but very powerful.

The key is to have the data in something like a DataFrame with all the grouping variables.

Annotations

Use the Text visual:

ptext = data((x = [1.5], y = [2.5], label = ["My text"])) * visual(Makie.Text) * mapping(:x, :y, text = :label => verbatim);
draw(plt + ptext)

BarPlots

By default, a plot with a single series results in grey bars, regardless of the theme. To match the theme color:

thm = AlgebraOfGraphics.aog_theme();
barColors = first(thm.palette);
# and in the plot
visual(BarPlot; color = first(barColors))

Ordering the bars is done in the color mapping (the bar mapping ignores it):

mapping(color = :x => sorter(["x1", "x2"]))

Crossbars

Crossbars are bars that are centered on a y value.

plt = data(df) * (mapping(:x, :y) * visual(AlgebraOfGraphics.BarPlot) + mapping(:x, :y, :yMin, :yMax) * visual(AlgebraOfGraphics.CrossBar, color = (:gray50, 0.5)))

creates a bar graph with crossbars around the y values.

Bar colors are set in the visual.

A standard error band can be added to a bar graph with

mapping(:x, :y, (:y, :yError) => ((x,y) -> x - y), (:y, :yError) => ((x,y) -> x + y)) * visual(AlgebraOfGraphics.CrossBar, color = :gray50))

Errorbars

Errorbars are simply vertical lines, centered at y values.

plt = data(df) * mapping("x", "y", "yError") * visual(Errorbars)

draws errorbars at values of y +/- yError. To combine with bar graph, add mapping("x", "y") * visual(BarPlot). This requires a separate mapping (otherwise BarPlot plots bars of height yError, not y).

Legends

For continuous variables, the default graph produces a colorbar instead of a legend. Avoid by declaring the variable governing color nonnumeric:

mapping(:x, :y, color = :z => nonnumeric).

Hiding the legend title currently does not work (titlevisible is ignored).

See Legend tweaking.

Layouts

Use a renamer to assign better labels to subplots:

mapping(layout = :z => renamer(["Label $j" for j = 1 : n]...))

Note the splatting. The renamer function takes pairs.

See also notes on multiple y variables.

Multiple y variables

Just create two plots and draw(plt1 + plt2).

Creating a subplot with different variables in each panel:

plt = data(df) * mapping(:x, [:y, :z], layout = dims(1) => renamer(fill(" ", 2))) * visual(BarPlot)

The renamer for the dims argument zeros out the subplot titles that would otherwise shows as CartesianIndex.

By default, axes are linked when the y variables are the same. To link, use

fig = draw(plt; facet = (; linkyaxes = :all))

This suppresses the y labels for the interior axes. To make the visible again:

for g in fig.grid
    g.axis.ylabelvisible = true;
end

Theming

There is no documented way of setting a theme (other than the AOG theme). However, one may plot with a theme:

with_theme(theme_ggplot2(), resolution = (600,400)) do
    plt |> draw
end

Colormaps are ignored. But one can pass color palettes to draw:

draw(p, palettes=(; color=[:red, :green, :blue]))

or

draw(plt; palettes=(; color=ColorSchemes.Set1_3.colors))

Titles

One main title for multiple subplots: Not directly implemented (see this issue).

Label(fig.figure[0,:], "Title"; font = :bold) works.

Tutorials

Pumas

Plots.jl (1.5)

Visually, PlotlyJS produces the most appealing plots (for me). But it does not install on my system (1.5).

When a plotting related library is not found (as in “error compiling display”), try ]build Plots.

Defaults are set with the defaults command.

Axis labels

bar tends to have too small default margins for axis labels to show (at least in subplots). Try

using Plots.PlotMeasures

Colors

Saturation is given by a number between 0 and 1 as in color = (:blue, 0.2).

Legends

The label is set when each series is plotted. If labels are set when the plot is created (before the series are plotted), the entries are ignored.

Bar graphs

Switch off lines around bars with linealpha = 0.

Default arguments

Make a Dict with default arguments for each plot type, such as:

bar_defaults() = Dict([:leg => :bottom, :linecolor => :black]);

Make a function for each plot type, such as:

function bar_graph(groupLabelV, dataV; kwargs...)
    args = merge(bar_defaults(), kwargs);
    p = bar(groupLabelV, dataV;  args...);
    return p
end

Now any default arguments can be overridden and bar_graph is called exactly as bar would be.

Showing plots in the REPL

ElectronDisplay is easy. Generate a Figure (fig) with Makie. Then issue electrondisplay(fig).

Saving data with plots

VegaLite does this natively.

with Plots.jl one can use hdf5plot_write to write an entire plot, including the data, to an hdf5 file.

This means that each plot has to be generated twice; once with whatever backend is used to generate PDF files; and then again with hdf5. In particular, one cannot first plot with another backend and then save the resulting plot object to hdf5.

The approach is then to first save the plot to hdf5, then load it and save it with another backend.

Note: In my current (v.1.3) installation, hdf5plot_write generates a bunch or warnings followed by a crash due to world age problems.