Legend outside plot when legend not directly created

Hello!

I have a plot I’m fairly happy with. But I’d like the legend to be located outside the plot area.

The documentation says that " To position a legend outside the central area, use the add_layout method of a plot. This requires creating the Legend object directly".

The reason I have not created the legend directly, e.g.,

legend = Legend(items=('var1',[var1]),

is that the exact variables (number of them and the content) change.
I’m creating the plot by:

for i in range(0, N_sites):
    
    site = sites[i]
    
    fullname = site_fullname(site)
    
    fig.circle(x=df['t'], y=df['value_'+site],
               fill_color = colours[i],
               size=10, alpha=0.7,
               visible      = False,
               legend_label = fullname)

So my thought is that I need to (1) create the figure, (2) extract the information from the figure needed to make the legend, and then (3) re-create the legend explicitly so it can be re-located outside the plot.

This seems awkward and inefficient. More importantly, I’m not sure how to do this.

Maybe I have to write a less flexible version of the code that does only a set of variables and values, but that’s a big compromise for the sake of moving the legend.

Advice on the best approach would be greatly appreciated!

There are a lot of ways to do this and the best approach will depend on your needs:

  • Do you have multiple glyph types or is it always “circle”/Scatter?
  • Do you have the same labels/color map for each one?
  • Do you need interactivity in the legend?

One way that would cover you minus the interactivity:

Let bokeh do the legend building, but then just retrieve the legend model and then “move it” with add_layout:

from bokeh.models import Scatter, ColumnDataSource
from bokeh.plotting import figure,show

import numpy as np

f = figure()

#make a bunch of random stuff, adding to the legend as we go

for i in range(5):
    src= ColumnDataSource(data={'x':np.random.random(3),'y':np.random.random(3),'lbl':['A','B','C'],'c':['red','blue','green']})
    r = f.scatter(x='x',y='y',legend_group='lbl',fill_color='c',source=src,size=10)

leg = f.legend[0]
f.add_layout(leg,'below')
show(f)

Now you’ll see that this gets you 90% of the way there:

But you’ll probably be wanting to remove “duplicate” things from the legend… In my example it’s pretty glaring because all 5 renderers are A,B, and C… but you might have one renderer with a “D” in it/different colors or even glyphs etc. that need their own unique legend item.

To remove the redundancy, you can inspect the legend for all the items it has:

image

and you look at one of these items’ properties:

image

you can kinda get the idea of collecting unique labels:

item_list = []
lbl_list = []
for item in leg.items:
    if item.label['value'] not in lbl_list:
        lbl_list.append(item.label['value'])
        item_list.append(item)

then reassign the legend.items with that cleaned up list:

leg.items = item_list

Now the caveat to this, is that it will break if you have duplicate labels with different glyphs (e.g. if ‘A’ has a scatter and a line)… now to work around that I’d probably break up my legend building glyph by glyph:

#scatter glyphs
for i in range(5):
    src= ColumnDataSource(data={'x':np.random.random(3),'y':np.random.random(3),'lbl':['A','B','C'],'c':['red','blue','green']})
    r = f.scatter(x='x',y='y',legend_group='lbl',fill_color='c',source=src,size=10)

#line glyphs
for i in range(5):
    src= ColumnDataSource(data={'x':np.random.random(3),'y':np.random.random(3),'lbl':['A','B','C'],'c':['red','blue','green']})
    r = f.line(.....)

Then it should be pretty straightforward to do the label trimming operation on each of these and then put them together at the end.

Overall everything is there for you to access and customize to suit your needs, you just gotta go digging a bit to get it and rearrange/organize.

1 Like

THANKS!

I also had line glyphs, which I didn’t mention.

All I needed to add was:

leg = fig.legend[0]
fig.add_layout(leg,'right')

Fantastic.