Search code examples
plotjulia

How to create Julia plots that share plotting region limits (using layout)


I have to Julia datasets that share the same Y-axes. I want to plot these side by side using layout, but have the two plots abutting, with no white space between them, while preserving outer margins for the tick labels and axis label on the left-hand plot.

Ideally I'd like to have the plot regions align, and have tick marks on the right-hand plot pointing into the plot space of the left-hand plot (with no tick labels).

In R (which I'm more familiar with), I could achieve this by setting the internal margins to zero (e.g., par(mrow=c(1,2), mar=c(0,0,0,0), oma=c(4,4,1,1)).

Minimum example.

using Plots

x = 1:10
y1 = rand(10)
y2 = rand(10)

plot1 = plot(x, y1, label="Plot 1", ylabel="Shared Y-Axis", framestyle=:box, right_margin=0mm)
plot2 = plot(x, y2, label="Plot 2", yticks=[] , framestyle=:box, left_margin=0mm)

l = @layout [a b]
plot(plot1, plot2, layout=l, left_margin=0mm, right_margin=0mm)
display(Plots.plot!())

R example of desired outcome: enter image description here

Thanks!

I tried setting margins to 0, both in the plot function and in the layout function. I also tried setting custom width and height in the layout macro.


Solution

  • I'm pretty sure you can't accomplish what you ideally want to do with the default GR back-end (maybe any of the possible Plots.jl backends?), but here are two possible answers for you (other may have better solutions!) that may work:

    1. This is "easy" to do in matplotlib (Python visualization software), and there is a nice Julia wrapper for this called PyPlot.jl that allows you to access pretty much everything you can in Python with similar syntax. There are some good Python -> Julia translation examples of its use here. Lightly adapting one of these, we can get what you want pretty easily:
    using PyPlot
    fig = figure("pyplot_dual_axis",figsize=(10,5)) #figsize controls x,y size of figure
    subplots_adjust(wspace=0.0) #remove horizontal space between subplots
    subplot(121) #1 row, 2 columns 1st plot (left)
    ax1= gca() #get the current axis
    ax1.plot([1,2,3],[1,2,3]) #plot to it
    subplot(122,sharey=ax1) #1 row, 2 columns 2nd plot (right)
    ax2 = gca() #get the newly created axis for right plot
    ax2.plot([1,2,3],[1,2,3]) #plot to it
    fig.canvas.draw() #update the figure
    suptitle("Dual y-axis example") #add a title 
    gcf() #get current figure, needed for some inline plotting in Julia(?)
    

    This produces the following figure:PyPlot dual y axis example

    For further customization you can pretty much just google "matplotlib how do I do [x]" and even though you are coding in Julia the result will mostly still apply as you can call all the ax and fig methods you need to from Julia using PyPlot.jl.

    1. The "harder" way is you can just draw everything yourself...for example here is a quick and dirty function I just wrote that will work with (probably) any Plots backend and draws a custom axis:
    function draw_custom_axis!(P,origin,orientation,ticks,tickLabels=nothing,tickLength=0.05)
        P = orientation == "y" ? plot!(P,[origin,origin],[minimum(ticks),maximum(ticks)],c="black",label="") : plot!(P,[minimum(ticks),maximum(ticks)],[origin,origin],c="black",label="")
        for (i,tick) in enumerate(ticks)
            P = orientation == "y" ? plot!([origin,origin-tickLength],[tick,tick],c="black",label="") : plot!([tick,tick],[origin,origin-tickLength],c="black",label="")
            if tickLabels != nothing
                t = orientation == "y" ? Plots.text("$(tickLabels[i])",rotation=90,halign=:hcenter,valign=:bottom) : Plots.text("$(tickLabels[i])",halign=:hcenter,valign=:top)
                P = orientation == "y" ? annotate!((origin-tickLength,tick,t)) : annotate!((tick,origin-tickLength,t))
            end
        end
        return P
    end
    

    Creating an empty canvas as a plot and then calling this will produce a similar result to what you want as well:

    P = plot(frame=false,axis=false,xlims=(-0.5,5.5),ylims=(-0.5,5.5)) #initialize empty white plot
    P = draw_custom_axis!(P,0,"y",[0,1,2,3,4,5],["$i" for i=0:5],0.1) #add a y axis that goes up from x=0 with tick marks at 0,1,2,3,4,5 and associated labels, with tick width = 0.1
    P = draw_custom_axis!(P,3,"y",[0,1,2,3,4,5],["","1","2","3","4","5"],0.1) #add a second y axis that goes up from x = 3, and this time omitting the first tick label for aesthetics
    P = draw_custom_axis!(P,0,"x",[0,1,2,3,4,5],["","1","2","0","1","2"],0.1) #add the x axis, with 5 tick marks but set the labels to start over at x=3 since this is where we added the second y axis and where the second "plot" begins
    P = plot!([rand()*2 for i=1:5],[rand()*5 for i=1:5],label="data 1") #plot some data in the left plot (which goes up to x=3)
    P = plot!(3 .+[rand()*2 for i=1:5],[rand()*5 for i=1:5],label="data 2") #plot some data in the right half, which has a span of 2 and starts at x=3 but has the same height in y
    

    This results in the following figure: julia custom dual y axis plot

    The advantages here are that you don't need to install a new package and you can use all the well Julia Plots documentation (which is pretty good!), and you get infinite customization options because you're making everything yourself, but the downside is that you have to make it yourself and do it in a little bit of a "hacky" way. For what it's worth I usually do something like this when I have some "niche" (not that this is actually that niche -- it really is something the library should just do by default) that I want to do. It usually ends up looking the best to you when you have full control over everything!