Making Plots place its legend in a better spot

I think it tries to pick an optimal place. The optimal position is subjective and potentially expensive. But guessing something “reasonable” most of the times is not (the function above takes 0.5s to run, and is not optimized in any sense (the time would increase by increasing the number of data series, but not the size of the data series themselves, because I choose a random sample from the data of constant size to check the overlaps - by decreasing the nsamples parameter the time decreases roughly proportionally).

edit: This version now takes 13 μs to find the same good position for the legend (with the same number of samples):


using Test
using Plots
using LinearAlgebra: norm

function dmin_series(lim,x,y,nsamples)
    dmin = +Inf
    for _ in 1:nsamples
        isample = rand(1:length(x))
        d = norm(lim .- (x[isample],y[isample]))
        if d < dmin
            dmin = d
        end
    end
    return dmin
end

function find_best_legend_position(plt;nsamples=50)
    ylims = Plots.ylims(plt)
    xlims = Plots.xlims(plt)
    dmin_max = 0.
    ibest = 0
    i = 0
    for lim in Iterators.product(xlims,ylims)
        i += 1
        for series in plt.series_list
            x = series[:x]
            y = series[:y]
            dmin = dmin_series(lim,x,y,nsamples)
            if dmin > dmin_max
                dmin_max = dmin
                ibest = i
            end
        end
    end
    ibest == 1 && return :bottomleft
    ibest == 2 && return :bottomright
    ibest == 3 && return :topleft
    return :topright
end

function test()

    x = 0:0.01:2;
    plt = plot(x,x,label="linear")
    plt = plot!(x,x.^2,label="quadratic")
    plt = plot!(x,x.^3,label="cubic")
    @test find_best_legend_position(plt) == :topleft

    x = 0:0.01:2;
    plt = plot(x,-x,label="linear")
    plt = plot!(x,-x.^2,label="quadratic")
    plt = plot!(x,-x.^3,label="cubic")
    @test find_best_legend_position(plt) == :bottomleft

    x = [0,1,0,1]
    y = [0,0,1,1]
    plt = scatter(x,y,xlims=[0.0,1.3],ylims=[0.0,1.3],label="test")
    @test find_best_legend_position(plt) == :topright
    
    plt = scatter(x,y,xlims=[-0.3,1.0],ylims=[-0.3,1.0],label="test")
    @test find_best_legend_position(plt) == :bottomleft

    plt = scatter(x,y,xlims=[0.0,1.3],ylims=[-0.3,1.0],label="test")
    @test find_best_legend_position(plt) == :bottomright

    plt = scatter(x,y,xlims=[-0.3,1.0],ylims=[0.0,1.3],label="test")
    @test find_best_legend_position(plt) == :topleft

    true
end

1 Like