# define new themes
:myplot_mag] = @gpkw {grid, ylabel = Q"Magnitude"}
Gaston.sthemes[:myplot_ph] = @gpkw {grid, ylabel = Q"Angle"}
Gaston.sthemes[:myplot_mag] = @gpkw {w = "lp", marker = :ecircle}
Gaston.pthemes[:myplot_ph] = @gpkw {w = "l", lc = "'black'"}
Gaston.pthemes[
# define new function
function myplot(f::Figure, data::AbstractVector{<:Complex}; kwargs...)::Figure
# convert data to a format gnuplot understands
= 1:length(data)
x = abs.(data)
magnitude = angle.(data)
phase
# make sure figure f is empty
reset!(f)
Gaston.
# fixed layout (two rows, one col)
= "layout 2,1"
f.multiplot = false
f.autolayout
# 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
= range(0, 1, 20)
t = exp.(-t) .* cis.(2*pi*7.3*t)
y myplot(y) # plot
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.
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}
:: Vector{T}
samples end
# define new function
function myplot(data::ComplexData; kwargs...)::Figure
# convert data to a format gnuplot understands
= 1:length(data.samples)
x = abs.(data.samples)
magnitude = angle.(data.samples)
phase ...]
[end
# create new Figure if not provided
myplot(data::ComplexData; kwargs...) = myplot(Figure(), data; kwargs...)
# plot example: complex damped sinusoid
= range(0, 1, 20)
t = ComplexData(exp.(-t) .* cis.(2*pi*7.3*t))
y 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
samplesend
# add method to convert_args
function convert_args(d::Type1)
= 1:length(d.samples)
x = d.samples
y PlotRecipe((x, y), "") # all coordinates are in a Tuple
end
# create some data
= Type1(rand(20))
data
# 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.
= Type1(rand(20))
data2 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...)
= range(0, 1, 100)
x
# build first curve
= @gpkw PlotRecipe((x, cos.(4x)), {dt = "'-'", lc = lc_first, t = "'cosine'"})
p1
# build second curve
= PlotRecipe((x, sin.(5x)), "dt '.' lc $(lc_second) t 'sine'")
p2
# 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:
= Figure(multiplot = "title 'Recipe example'")
f 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)
= PlotRecipe((1:20, 1:20, randn(20,20)), "w pm3d")
p1 @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
= PlotRecipe((1:10, rand(10)), "")
p1 @gpkw a1 = AxisRecipe({title = Q"First Axis"}, [p1])
# axis #2
= range(0, 1, 40)
t1 = @gpkw PlotRecipe((t1, sin.(5t1)), {lc = Q"black"})
p2 = @gpkw PlotRecipe((t1, cos.(5t1)), {w = "p", pt = 16})
p3 = @gpkw AxisRecipe({title = Q"Trig"}, [p2, p3])
a2
# axis #3
= range(-5, 5, 50)
t2 = Gaston.meshgrid(t2, t2, (x,y) -> cos(x)*cos(y))
z = @gpkw PlotRecipe((t2, t2, z), {w = "pm3d"})
p4 @gpkw a3 = AxisRecipe({title = Q"Surface", tics = false, palette = (:matter, :reverse)},
true)
[p4],
# 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)
= x[1,:]
center = DataBlock([stack((center, x[n,:]), dims=1) for n in 2:size(x,1)]...)
points return PlotRecipe((points,), "w l")
end
= rand(10, 2)
x 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,:]
....