# 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) # plotRecipes
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.
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) # plotThe 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,:]
....