# Making Plots place its legend in a better spot

**URL:** https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986
**Category:** Visualization
**Tags:** plots, gr
**Created:** [January 20, 2022, 9:00pm UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986 "2022-01-20T21:00:45Z")
**Posts on this page:** 19
**Page:** 1

<div class="post-metadata">

### Author: ![lmiq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lmiq/32/18314_2.png) [@lmiq](https://discourse.julialang.org/u/lmiq)
#### Post date: [January 20, 2022, 9:00pm UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986/1 "2022-01-20T21:00:45Z")

</div>

> [@Why Plots.jl produce figures looks not as good as other packages even with pyplot](https://discourse.julialang.org/t/why-plots-jl-produce-figures-looks-not-as-good-as-other-packages-even-with-pyplot/74946/15):
>
> eg the legend autoplacement in Plots.jl can indeed be improved to avoid

Apparently this is a feature, or not of the backend, and Plots sends `legend=:best` to them. PyPlot should place the legend in a nice position by default, avoiding the data, but `GR` apparently does not have this feature. I think that is a sensible issue to open as a feature request (or just implement that, if anyone has any clue on how to do it).

---

<div class="post-metadata">

### Author: ![lmiq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lmiq/32/18314_2.png) [@lmiq](https://discourse.julialang.org/u/lmiq)
#### Post date: [January 21, 2022, 12:22pm UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986/2 "2022-01-21T12:22:09Z")

</div>

I’ve opened one issue in `GR.jl` about the legend position, which I think is the objective point here: [Set non-overlapping legend position by default · Issue #444 · jheinen/GR.jl · GitHub](https://github.com/jheinen/GR.jl/issues/444)

If anyone has any idea on how to contribute to that, it would be nice.

This is a small function that can find the best legend position. I don’t know how to integrate it into the plots package, though (and probably it can be more general and simpler):

> **Code**
>
> ```julia
> using Plots
> 
> function find_best_legend_position(plt;nsamples=50)
> ylims = Plots.ylims(plt)
> xlims = Plots.xlims(plt)
> dmin_max = 0.
> ibest = 0
> for series in plt.series_list
> x = series[:x]
> y = series[:y]
> i = 0
> for lim in Iterators.product(xlims,ylims)
> i += 1
> dmin = +Inf
> for _ in 1:nsamples
> isample = rand(1:length(x))
> d = sum((lim .- (x[isample],y[isample])).^2)
> if d < dmin
> dmin = d
> end
> end
> if dmin > dmin_max
> dmin_max = dmin
> ibest = i
> end
> end
> end
> ibest == 1 && return :bottomleft
> ibest == 2 && return :bottomright
> ibest == 3 && return :topleft
> ibest == 4 && return :topright
> end
> 
> ```

It could be used like this (not that I think that anynone will use it, but it may be a startup for doing that automatically):

```julia
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")
plt = plot!(legend=find_best_legend_position(plt)) # find best legend position

```

will produce:

![image](https://global.discourse-cdn.com/julialang/original/3X/d/a/dae4339eea1bd412ebfe06bd3fb767879f4db88e.png)

or, for example,

```julia
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")
plt = plot!(legend=find_best_legend_position(plt)) # find best legend position

```

will produce:

![image](https://global.discourse-cdn.com/julialang/original/3X/d/2/d22a4a9339492f71214b2cbb9559e672cb75750a.png)

---

<div class="post-metadata">

### Author: ![rafael.guerra](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/rafael.guerra/32/216610_2.png) [@rafael.guerra](https://discourse.julialang.org/u/rafael.guerra)
#### Post date: [January 21, 2022, 1:49pm UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986/3 "2022-01-21T13:49:00Z")

</div>

And if it is not possible to display the legend without hiding the data, it should be automatically placed outside the plot area.

---

<div class="post-metadata">

### Author: ![lmiq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lmiq/32/18314_2.png) [@lmiq](https://discourse.julialang.org/u/lmiq)
#### Post date: [January 21, 2022, 1:52pm UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986/4 "2022-01-21T13:52:01Z")

</div>

> [@rafael.guerra](#):
>
> And if not possible to display the legend without hiding data then it should be placed automatically outside the plot area.

That is relatively more complicated. I only check which point, between the extrema of the graph, has the maximum minimum distance to some randomly sampled points of the data. To check actual overlaps, one needs to take into account the legend size, and _all_ the data, and that can be come cumbersome and expensive. Anyway the user can always (and probably will) change the legend position when fine tuning things.

---

<div class="post-metadata">

### Author: ![gustaphe](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gustaphe/32/18174_2.png) [@gustaphe](https://discourse.julialang.org/u/gustaphe)
#### Post date: [January 21, 2022, 2:01pm UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986/5 "2022-01-21T14:01:44Z")

</div>

I would disagree with that. Changing the axes size is a lot more intrusive than just covering some data.

---

<div class="post-metadata">

### Author: ![lmiq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lmiq/32/18314_2.png) [@lmiq](https://discourse.julialang.org/u/lmiq)
#### Post date: [January 21, 2022, 2:12pm UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986/6 "2022-01-21T14:12:30Z")

</div>

Despite the title of the thread, it may be interesting to let `Plot` do the job, and pass the best position to all backends (mentioned the possibility [here](https://github.com/JuliaPlots/Plots.jl/issues/3399#issuecomment-1018520542)).

---

<div class="post-metadata">

### Author: ![mbauman](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mbauman/32/31082_2.png) [@mbauman](https://discourse.julialang.org/u/mbauman)
#### Post date: [January 21, 2022, 2:19pm UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986/7 "2022-01-21T14:19:44Z")

</div>

Please feel free to change the title to better suit your objectives!

---

<div class="post-metadata">

### Author: ![rafael.guerra](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/rafael.guerra/32/216610_2.png) [@rafael.guerra](https://discourse.julialang.org/u/rafael.guerra)
#### Post date: [January 21, 2022, 2:30pm UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986/8 "2022-01-21T14:30:59Z")

</div>

> [@gustaphe](#):
>
> Changing the axes size is a lot more intrusive than just covering some data.

Nothing is worse than hiding data. But there is another way, by plotting the legend inside the plot area, but extending the bounds to give it more space.

---

<div class="post-metadata">

### Author: ![lmiq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lmiq/32/18314_2.png) [@lmiq](https://discourse.julialang.org/u/lmiq)
#### Post date: [January 21, 2022, 2:38pm UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986/9 "2022-01-21T14:38:50Z")

</div>

> [@rafael.guerra](#):
>
> Nothing is worse than hiding data. But there is another way, by plotting the legend inside the plot area, but extending the bounds to give it more space.

We are back on the subjective side of things, I prefer not to mess up with the plot size or figure size by using an option like this (otherwise much more plot attributes would become correlated). Anyway, as I mentioned, not overlapping with data is quite more complicated than the “reasonable position” option.

---

<div class="post-metadata">

### Author: ![joa-quim](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/joa-quim/32/227_2.png) [@joa-quim](https://discourse.julialang.org/u/joa-quim)
#### Post date: [January 21, 2022, 2:47pm UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986/10 "2022-01-21T14:47:40Z")

</div>

Does mapplotlib picks an empty place in the plot like we saw in the example on the original thread, or it just happened that it uses an UpperLeft default position and by coincidence it didn’t overlap the example data? Finding the legend _optimal_ position seems a tough problem and a potentially expensive one.

---

<div class="post-metadata">

### Author: ![lmiq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lmiq/32/18314_2.png) [@lmiq](https://discourse.julialang.org/u/lmiq)
#### Post date: [January 21, 2022, 2:52pm UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986/11 "2022-01-21T14:52:51Z")

</div>

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):

```julia

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

```

---

<div class="post-metadata">

### Author: ![ptoche](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ptoche/32/23554_2.png) [@ptoche](https://discourse.julialang.org/u/ptoche)
#### Post date: [January 21, 2022, 4:25pm UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986/12 "2022-01-21T16:25:43Z")

</div>

In an ideal world there would be an option to place the plot inside/outside the axes. Placing the legend outside ought to be upon the user’s request, both for simplicity of implementation and to match the typical user’s expectation.

Python’s `matplotlib` will happily place the legend on top of the data if there’s too much data. You can place the legend relative to the axis or relative to the figure (`ax` vs `plt` in common parlance), but [it isn’t a walk in the park for the beginner](https://stackoverflow.com/questions/4700614/how-to-put-the-legend-out-of-the-plot/43439132#43439132).

---

<div class="post-metadata">

### Author: ![lmiq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lmiq/32/18314_2.png) [@lmiq](https://discourse.julialang.org/u/lmiq)
#### Post date: [January 21, 2022, 4:29pm UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986/13 "2022-01-21T16:29:03Z")

</div>

```julia
plot(rand(10),label="test",legend=:outertopright)

```

![image](https://global.discourse-cdn.com/julialang/original/3X/a/e/ae3d5f699b4ca1fae3aa899098d9f62d6e98cafc.png)

---

<div class="post-metadata">

### Author: ![goerz](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/goerz/32/3269_2.png) [@goerz](https://discourse.julialang.org/u/goerz)
#### Post date: [January 21, 2022, 5:56pm UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986/14 "2022-01-21T17:56:39Z")

</div>

> [@rafael.guerra](#):
>
> Nothing is worse than hiding data

I often find that if the legend has no frame, but a white (depending on the colorscheme) background at 50% opacity, the result is quite nice. You can still see the data behind the legend, and they don’t really clash.

This is only if there’s no place for the legend that doesn’t cover any data, of course.

---

<div class="post-metadata">

### Author: ![guoyongzhi](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/guoyongzhi/32/28306_2.png) [@guoyongzhi](https://discourse.julialang.org/u/guoyongzhi)
#### Post date: [January 23, 2022, 4:16am UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986/15 "2022-01-23T04:16:14Z")

</div>

Based on `@leandromartinez98`’s code, and I replaced Euclidean distance with Manhattan distance. Now, this version has the same outputs and about 2x acceleration.

```julia
using Plots

function datalims!(L, sx, sy, nsamples)
    @inbounds for _ in 1:nsamples
        i = rand(1:length(sx))
        x, y = sx[i], sy[i]
        L[1] = min(L[1], x+y)
        L[2] = min(L[2], -x+y)
        L[3] = min(L[3], x-y)
        L[4] = min(L[4], -x-y)
    end
end
function find_best_legend_position(plt; nsamples=50)
    L = [Inf, Inf, Inf, Inf]
    yb, yt = Plots.ylims(plt)
    xl, xr = Plots.xlims(plt)
    for series in plt.series_list
        datalims!(L, series[:x], series[:y], nsamples)
    end
    @inbounds begin
    L[1] += -xl - yb
    L[2] += xr - yb
    L[3] += -xl + yt
    L[4] += xr + yt
    end
    i = argmax(L)
    return (:bottomleft, :bottomright, :topleft, :topright)[i]
end

```

I assumed the positive direction of the coordinate axis. I’m not sure if it’s correct.

---

<div class="post-metadata">

### Author: ![rafael.guerra](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/rafael.guerra/32/216610_2.png) [@rafael.guerra](https://discourse.julialang.org/u/rafael.guerra)
#### Post date: [January 23, 2022, 2:07pm UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986/16 "2022-01-23T14:07:06Z")

</div>

A different approach using `findfirst()` which checks if some rectangular areas around the 4 corners are empty:

```julia
function placelegend()
    p = Plots.current()
    xl, yl = collect.(extrema.((xlims(p), ylims(p))))
    dx, dy = (xl[2] - xl[1])/3, (yl[2] - yl[1])/4
    x1, x2 = xl + [dx, -dx]
    y1, y2 = yl + [dy, -dy]
    tr = tl = br = bl = true
    for series in p.series_list
        x, y = series[:x], series[:y]
        tr && (tr = isnothing(findfirst(@. (x > x2) & (y > y2))))
        tl && (tl = isnothing(findfirst(@. (x < x1) & (y > y2))))
        br && (br = isnothing(findfirst(@. (x > x2) & (y < y1))))
        bl && (bl = isnothing(findfirst(@. (x < x1) & (y < y1))))
    end
    tr && return :topright
    tl && return :topleft
    br && return :bottomright
    bl && return :bottomleft
    return :outertopright # did not find empty corner, place legend outside
end

```

> **To test the code:**
>
> ```julia
> using Plots
> 
> # Ex.1:
> x = 0:0.01:2;
> plot(x,x,label="linear")
> plot!(x,x.^2,label="quadratic")
> plot!(x,x.^3,label="cubic")
> plot!(legend=placelegend(), fg_legend=:lightgrey) # fg_legend=false
> 
> # Ex.2: repeat code block several times to test
> x0 = rand(-1:0.1:1, 9); y0 = rand(-1:0.1:1, 9)
> x1 = rand((-1,1)) * rand(1000)
> y1 = rand((-1,1)) * rand(1000) .* x1
> y2 = rand((-1,1)) * x1 .* y1.^2
> y3 = rand((-1,1)) * x1 .* y1.^3
> scatter(x0, y0, label="series0")
> label=["series1" "series2" "series3"]
> scatter!(x1, [y1, y2, y3], label=label);
> scatter!(legend=placelegend(), fg_legend=:lightgrey)
> 
> ```
> 
> ![Plots_gr_placelegend](https://global.discourse-cdn.com/julialang/original/3X/5/7/57cea775b2f6a4308628c3ed8a8aca505514e6be.png)

---

<div class="post-metadata">

### Author: ![guoyongzhi](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/guoyongzhi/32/28306_2.png) [@guoyongzhi](https://discourse.julialang.org/u/guoyongzhi)
#### Post date: [May 20, 2022, 2:20pm UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986/18 "2022-05-20T14:20:40Z")

</div>

I tried my code again with [lmiq](https://discourse.julialang.org/u/lmiq)’s `test()` function under julia 1.7.2, and it still didn’t output any errors. Do you have some new test samples that failed? Can you share them?

---

<div class="post-metadata">

### Author: ![fdamore](https://avatars.discourse-cdn.com/v4/letter/f/b2d939/32.png) [@fdamore](https://discourse.julialang.org/u/fdamore)
#### Post date: [May 24, 2022, 9:33pm UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986/19 "2022-05-24T21:33:52Z")

</div>

> [@guoyongzhi](#):
>
> ```julia
> function datalims!(L, sx, sy, nsamples)
> @inbounds for _ in 1:nsamples
> i = rand(1:length(sx))
> x, y = sx[i], sy[i]
> L[1] = min(L[1], x+y)
> L[2] = min(L[2], -x+y)
> L[3] = min(L[3], x-y)
> L[4] = min(L[4], -x-y)
> end
> end
> function find_best_legend_position(plt; nsamples=50)
> L = [Inf, Inf, Inf, Inf]
> yb, yt = Plots.ylims(plt)
> xl, xr = Plots.xlims(plt)
> for series in plt.series_list
> datalims!(L, series[:x], series[:y], nsamples)
> end
> @inbounds begin
> L[1] += -xl - yb
> L[2] += xr - yb
> L[3] += -xl + yt
> L[4] += xr + yt
> end
> i = argmax(L)
> return (:bottomleft, :bottomright, :topleft, :topright)[i]
> end
> 
> ```

I cannot reply the error anymore ☹ I will delete my comments

---

<div class="post-metadata">

### Author: ![guoyongzhi](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/guoyongzhi/32/28306_2.png) [@guoyongzhi](https://discourse.julialang.org/u/guoyongzhi)
#### Post date: [May 26, 2022, 10:27am UTC](https://discourse.julialang.org/t/making-plots-place-its-legend-in-a-better-spot/74986/20 "2022-05-26T10:27:21Z")

</div>

Ha ha, that’s life.
