Plots.jl: Combine two plots into one with two series

Hi,
I’m using the Plots.jl package and I’d like to do some manipulation to graph layout inside my functions, that return an aray of plot objects, that I finaly plot with grid layout. When I run the function with two sets of parameters, I’d like to combine the two arrays of plots into one grid layout, but each plot presenting two data series. Unfortunatelly, my naive approach didn’t work.
This works:

plot(1:50, rand(50))
plot!(1:50, 0.5*rand(50))

A nice plot with two series is shown. But, this doesn’t work:

p1 = plot(1:50, rand(50))
p2 = plot(1:50, 0.5*rand(50))
plot!(p1, p2)

Is there a better approach to combine two plots into one with two series?

I’m pretty sure you can’t do that - except by some hacking where you extract the data manually from the Plot object, e.g. x1 = p1.series_list[1].d[:x].

1 Like

The problem with your code is that the plot/plot! functions are used to build plot objects - not display them.

Here is what you need to do instead:

#So that display() used below generates new figure windows:
Plots.default(overwrite_figure=false)

#Generate a few plots
p1 = plot(1:50, rand(50))
p2 = plot(1:50, 0.5*rand(50))
p3 = plot(1:50, 0.25*rand(50))

#...

#Display plots later on:
display(p2)
display(p3)
display(p1)

Also note that, by default, plot/plot! work on a “global”-ish object using a module-level variable inside the Plots module.

When you start working with multiple plot objects like that, it becomes very useful to use to use the plot!(plotref, ...) syntax:

#Add data to the p1-plot object:
plot!(p1, 1:50, 1000*rand(50))
2 Likes

I know the Plot function builds plots and i know how to display them. My usecase is indeed building a Plot by combining two plots.

Imagine I create a complex layout of polar/line/scatter plots, where there are many formating arguments not easy to write each time by hand. So I encapsulate them into a function foo returning the p1::Plot object.

But, I’d like to quickly compare two sets of data using the same layout view - so I run the foo function with different data and imediatelly I get second p2::Plot object. Now, I’d like to build a third plot with exactly the same layout and formating, but presenting the p1 and p2 as two series in each subplot in the complex Plot object.

So far, I’ve used the hint above (thanks, @mkborregaard) :

p1 = plot(1:50, rand(50))
p2 = plot(1:50, 0.5*rand(50))
p3 = plot([1:50 1:50], [0.2*rand(50) 0.8*rand(50)])

using RecipesBase
function insideout(a::AbstractArray)
    n = length(a[1])
    b = [[a[i][t] for i=1:size(a,1)] for t=1:n]
    tuple(b...)
end
@recipe f(p2::Plots.Plot) = insideout([(p2.series_list[i].d[:x], p2.series_list[i].d[:y]) for i=1:length(p2.series_list)])
plot!(p1, p2)  #works
plot!(p1, p3)  #works also

But this is working only for the simplest single subplot usecase. The :label is not propagated and this approach will definitely not work for the more copmplex layouts with many subplots.

I’ve used the UserRecipe from RecipesBase, but unfortunatelly this type of recipe is not well documented. How can I forward to the pipeline the series_list[i].d dictionary? How do the user recipes work in case of complex layouts?

I think the usecase of combining two almost exact plots (difference only in data, not formating and layout) is strong, as this is the natural way one can easily compare 2 different complex outputs of a calculation. Plots should have a built-in mechanism to allow this intuitive approach.I’m willing to prepare a PR, but will definitely need some guidance on the user recipe machinery.

I think the best way of doing it is to define a userplot recipe that takes as arg an array of Plot objects. You can extract the information you need from the individual Plot objects and build a new plot. This could be a good PR for the PlotRecipes repo.
An easy way to get an overview of Plot objects is to use the new hdf5 backend to save the plots to an HDF5 object, then inspect that with an hdf5viewer (in fact contributed by @MA_Laforge). Follow the instructions in the first post here: https://github.com/JuliaPlots/Plots.jl/pull/747
and open with e.g. The HDF Group - Information, Support, and Software

You should be able to get help with user recipes on the Plots gitter.

Another possibility:
I don’t know about your specific use case, but an easier solution may be to return a custom object. You can define a plot recipe to dispatch on that custom type (e.g. @recipe function f(myT::MyType) so it’s just as easy to plot as if you’d returned the plot object. You can then define another user recipe that takes a Vector{MyType} and dispatches on that. In this case you’ll need no post-manipulating Plots objects.

1 Like

I have a slightly different philosophy on building plot objects that involves wrapping plot commands into slightly higher-level functons.

That way, instead of merging two plot objects, you call functions that append data to an existing plot object.

Maybe this solution would be acceptable with your work flow as well…

#------------Define how to append 3 different types of datasets------------
#Typically, data is passed in as argument to "Append" functions, but for the sake of simplicity...
function PlotAppendThing1(p::Plots.Plot)
	plot!(p, 1:50, rand(50), label="thing1")
	#Potentially more formatting here...
end
function PlotAppendThing2(p::Plots.Plot)
	plot!(p, 1:50, 0.5*rand(50), label="thing2")
end
function PlotAppendThing3(p::Plots.Plot, subplot::Int)
	plot!(p, [1:50 1:50], [0.2*rand(50) 0.8*rand(50)], label=["thing3a" "thing3b"], subplot=[subplot subplot])
end

#------------Generate new plot and add datasets------------
plt = plot(title="MyPlot", layout=2) #Construct base object
PlotAppendThing1(plt)
PlotAppendThing3(plt, 2) #Want to append to 2nd subplot
PlotAppendThing2(plt)

display(plt) #Display newly constructed plot

So, instead of passing around a vector of plots, you would instead pass around a vector of datasets you want added to the plot.

@phlavenk Did you manage to figure out how to do this?

Here’s a starting point:

using Plots

function merge_series!(sp1::Plots.Subplot, sp2::Plots.Subplot)
    append!(sp1.series_list, sp2.series_list)
    Plots.expand_extrema!(sp1[:xaxis], xlims(sp2))
    Plots.expand_extrema!(sp1[:yaxis], ylims(sp2))
    Plots.expand_extrema!(sp1[:zaxis], zlims(sp2))
    return sp1
end

function merge_series!(plt, plts...)
    for (i, sp) in enumerate(plt.subplots)
        for other_plt in plts
            if i in eachindex(other_plt.subplots)
                merge_series!(sp, other_plt[i])
            end
        end
    end
    return plt
end

plt1 = plot(rand(10, 4), layout = 4, color = 1, label = "plt1")
plt2 = plot(0.1 * rand(20, 2), layout = 2, color = 2, label = "plt2")
plt3 = plot(5 * rand(5, 3), layout = 3, color = 3, label = "plt3")

plt = merge_series!(plt1, plt2, plt3)

merge_series
Note that this approach requires to specify the series colors explicitly beforehand. Otherwise the first default color is chosen for all series. Also, only the number of subplots in the first argument of merge_series! are considered. I hope you can expand this approach to fit your needs.

7 Likes

I was using this code today, and was really confused until I realized that it wasn’t doing the same thing when using the plotly() backend. Not sure why exactly—any ideas?