Recipes

There are several ways to extend Gaston to plot data of arbitrary types.

Functions that return a Gaston.Figure

A straightforward way to extend (or customize) Gaston’s functionality is by defining functions that return a value of type Figure.

The example below shows how to plot complex vectors as two subplots, one of the magnitude and the other of phase of the data. This example defines new themes.

# define new themes
Gaston.sthemes[:myplot_mag] = @gpkw {grid, ylabel = Q"Magnitude"}
Gaston.sthemes[:myplot_ph] = @gpkw {grid, ylabel = Q"Angle"}
Gaston.pthemes[:myplot_mag] = @gpkw {w = "lp", marker = :ecircle}
Gaston.pthemes[:myplot_ph] = @gpkw {w = "l", lc = "'black'"}

# define new function
function myplot(f::Figure, data::AbstractVector{<:Complex}; kwargs...)::Figure
     # convert data to a format gnuplot understands
     x = 1:length(data)
     magnitude = abs.(data)
     phase = angle.(data)

     # make sure figure f is empty
     Gaston.reset!(f)

     # fixed layout (two rows, one col)
     f.multiplot = "layout 2,1"
     f.autolayout = false

     # add two plots to f, using the themes defined above
     plot(f[1], x, magnitude, stheme = :myplot_mag, ptheme = :myplot_mag)
     plot(f[2], x, phase, stheme = :myplot_ph, ptheme = :myplot_ph)

     return f
end

# plot on active figure if none specified, or new figure if none exist
myplot(data::AbstractVector{<:Complex}; kwargs...) = myplot(figure(), data; kwargs...)

# plot example: complex damped sinusoid
t = range(0, 1, 20)
y = exp.(-t) .* cis.(2*pi*7.3*t)
myplot(y)  # plot

Note that the function myplot has two methods:

  • The main method takes an existing figure as first argument, and then the data.
  • A second method handles the case where only data is provided by selecting a new figure and then forwarding execution to the main method.

Here, myplot takes a base Julia type. A simple modification handles the case where the data is of a user-defined type:

#define new type
struct ComplexData{T <: Complex}
    samples :: Vector{T}
end

# define new function
function myplot(data::ComplexData; kwargs...)::Figure
                # convert data to a format gnuplot understands
                x = 1:length(data.samples)
                magnitude = abs.(data.samples)
                phase = angle.(data.samples)
                [...]
end

# create new Figure if not provided
myplot(data::ComplexData; kwargs...) = myplot(Figure(), data; kwargs...)

# plot example: complex damped sinusoid
t = range(0, 1, 20)
y = ComplexData(exp.(-t) .* cis.(2*pi*7.3*t))
myplot(y)  # plot

The use of themes allows the user to modify the default properties of the plot, by modifying the themes (such as Gaston.sthemes[:myplot_mag]) instead of having to re-define myplot. Of course, similar functionality can be achieved with the use of keyword arguments.

The main drawback of this method of extending Gaston is that it requires an environement where Gaston has been installed This may be undesirable when sharing code with others, which may prefer to use a different plotting package, or when developing a package, which would burden all users with a relatively large, unneeded dependency. The solution to this problem is to use “recipes”.

Adding new methods to Gaston.convert_args

The package GastonRecipes is a tiny package that allows extending Gaston to plot arbitrary types. It provides three types of recipes:

  • PlotRecipe, which return a single curve that can inserted into an axis.
  • AxisRecipe, which return a single axis that can be inserted into a figure.
  • FigureRecipe, which consists of one or more axes, mostly useful for multiplots or for animations.

Recipes work as follows: Gaston’s plot function calls function Gaston.convert_args (or convert_args3 for 3-D plots), if an appropriate method exists. This function takes the data provided to plot and returns an appropriate object (of type PlotRecipe, AxisRecipe or FigureRecipe) containing data of a type and format that gnuplot understands. The idea then is to add methods to convert_args to handle any arbitrary type we wish to plot.

PlotRecipe

The following example shows how to extend Gaston.convert_args to plot a custom type Data1. This simple example returns a PlotRecipe object (essentially a curve), which contains data and a plotline.

using GastonRecipes: PlotRecipe
import GastonRecipes: convert_args

# define custom type
struct Type1
    samples
end

# add method to convert_args
function convert_args(d::Type1)
    x = 1:length(d.samples)
    y = d.samples
    PlotRecipe((x, y), "") # all coordinates are in a Tuple
end

# create some data
data = Type1(rand(20))

# plot
plot("set title 'Simple data conversion recipe'", "set grid", data, "w lp pt 7 lc 'olive'")

Note that this kind of recipe will also seamlessly work with plot!, which adds the curve to the current axis.

data2 = Type1(rand(20))
plot!(data2, "lc 'red'")

Finally, note that Gaston calls convert_args with all data and keyword arguments given to plot. This may be used to control the recipe’s behavior, as illustrated in the next example.

AxisRecipe

A recipe may also return an entire AxisRecipe object, with its own settings and curves. The following example returns an axis with two curves. Keyword arguments are used to control the linecolor of each curve.

using GastonRecipes: AxisRecipe

struct Type2 end

function convert_args(::Type2, args... ; lc_first = "'blue'", lc_second = "'red'", kwargs...)
    x = range(0, 1, 100)

    # build first curve
    p1 = @gpkw PlotRecipe((x, cos.(4x)), {dt = "'-'", lc = lc_first, t =  "'cosine'"})

    # build second curve
    p2 = PlotRecipe((x, sin.(5x)), "dt '.' lc $(lc_second) t 'sine'")

    # build AxisRecipe, using a vector of PlotRecipes
    AxisRecipe("set grid\nset title 'Full axis recipe'", [p1, p2])
end

plot(Type2())

By default, the line colors will be blue and red for the first and second curve, respectively, but these can be specified by the user with the keyword arguments lc_first and lc_second.

Note that the axis returned by a recipe can be inserted directly into a multiplot:

f = Figure(multiplot = "title 'Recipe example'")
plot(f[1], randn(100), "w p")
plot(f[2], Type2())

Finally, the following example shows how to create a recipe for splot, using convert_args3. Note that now AxisRecipe takes a third argument, which indicates a 3-D plot when set to true (it is false by default).

import Gaston: convert_args3

function convert_args3(::Type2)
    p1 = PlotRecipe((1:20, 1:20, randn(20,20)), "w pm3d")
    @gpkw s = {palette = :matter, title = Q"A Surface"}
    AxisRecipe(s, [p1], true)
end

splot(Type2())

FigureRecipe

Finally, a recipe can also generate a full multiplot, with multiple axes, as illustrated in the example below:

using GastonRecipes: FigureRecipe

struct Type3 end

function convert_args(::Type3)
    # first axis
    p1 = PlotRecipe((1:10, rand(10)), "")
    @gpkw a1 = AxisRecipe({title = Q"First Axis"}, [p1])

    # axis #2
    t1 = range(0, 1, 40)
    p2 = @gpkw PlotRecipe((t1, sin.(5t1)), {lc = Q"black"})
    p3 = @gpkw PlotRecipe((t1, cos.(5t1)), {w = "p", pt = 16})
    a2 = @gpkw AxisRecipe({title = Q"Trig"}, [p2, p3])

    # axis #3
    t2 = range(-5, 5, 50)
    z = Gaston.meshgrid(t2, t2, (x,y) -> cos(x)*cos(y))
    p4 = @gpkw PlotRecipe((t2, t2, z), {w = "pm3d"})
    @gpkw a3 = AxisRecipe({title = Q"Surface", tics = false, palette = (:matter, :reverse)},
                          [p4], true)

    # axis the fourth
    @gpkw a4 = AxisRecipe({tics, title = false, title = Q"Last Axis"},
                          [PlotRecipe((1:10, 1:10, rand(10,10)), "w image")])

    # return named tuple with four axes
    FigureRecipe([a1, a2, a3, a4],
                 "title 'A Four-Axes Recipe' layout 2,2", false)
end

plot(Type3())

Recipes for types owned by other packages

Let’s say we want to create a recipe for Base.Vector. We don’t own either convert_args nor Base.Vector, so creating a recipe would be type piracy. The solution is to define a new type and dispatch on it. Here’s an example: we want to plot the elements of a matrix as lines from the coordinates specified in the first row to to each of the other row.

struct StarPlot end

import GastonRecipes: DataBlock

function convert_args(::Type{StarPlot}, x::Matrix)
    center = x[1,:]
    points = DataBlock([stack((center, x[n,:]), dims=1) for n in 2:size(x,1)]...)
    return PlotRecipe((points,), "w l")
end

x = rand(10, 2)
plot(StarPlot, x)

(Of course, the same functionality could be achieved by wrapping x in StarPlot. The choice of approach is a matter of taste).

This recipe also illustrates the use of DataBlock. In this case, the data that is given to gnuplot has the following form:

x[1,:]
x[2,:]

x[1,:]
x[3,:]

x[1,:]
x[4,:]

....