Setting a fixed size for points in legend
Asked Answered
T

6

88

I'm making some scatter plots and I want to set the size of the points in the legend to a fixed, equal value.

Right now I have this:

import matplotlib.pyplot as plt
import numpy as np

def rand_data():
    return np.random.uniform(low=0., high=1., size=(100,))

# Generate data.
x1, y1 = [rand_data() for i in range(2)]
x2, y2 = [rand_data() for i in range(2)]


plt.figure()
plt.scatter(x1, y1, marker='o', label='first', s=20., c='b')
plt.scatter(x2, y2, marker='o', label='second', s=35., c='r')
# Plot legend.
plt.legend(loc="lower left", markerscale=2., scatterpoints=1, fontsize=10)
plt.show()

which produces this:

enter image description here

The sizes of the points in the legend are scaled but not the same. How can I fix the sizes of the points in the legend to an equal value without affecting the sizes in the scatter plot?

Trophy answered 11/7, 2014 at 20:35 Comment(0)
A
112

I had a look into the source code of matplotlib. Bad news is that there does not seem to be any simple way of setting equal sizes of points in the legend. It is especially difficult with scatter plots (wrong: see the update below). There are essentially two alternatives:

  1. Change the maplotlib code
  2. Add a transform into the PathCollection objects representing the dots in the image. The transform (scaling) has to take the original size into account.

Neither of these is very much fun, though #1 seems to be easier. The scatter plots are especially challenging in this respect.

However, I have a hack which does probably what you want:

import matplotlib.pyplot as plt
import numpy as np

def rand_data():
    return np.random.uniform(low=0., high=1., size=(100,))

# Generate data.
x1, y1 = [rand_data() for i in range(2)]
x2, y2 = [rand_data() for i in range(2)]

plt.figure()
plt.plot(x1, y1, 'o', label='first', markersize=np.sqrt(20.), c='b')
plt.plot(x2, y2, 'o', label='second', markersize=np.sqrt(35.), c='r')
# Plot legend.
lgnd = plt.legend(loc="lower left", numpoints=1, fontsize=10)

#change the marker size manually for both lines
lgnd.legendHandles[0]._legmarker.set_markersize(6)
lgnd.legendHandles[1]._legmarker.set_markersize(6)
plt.show()

This gives:

enter image description here

Which seems to be what you wanted.

The changes:

  • scatter changed into a plot, which changes the marker scaling (hence the sqrt) and makes it impossible to use changing marker size (if that was intended)
  • the marker size changed manually to be 6 points for both markers in the legend

As you can see, this utilizes hidden underscore properties (_legmarker) and is bug-ugly. It may break down at any update in matplotlib.

Update

Haa, I found it. A better hack:

import matplotlib.pyplot as plt
import numpy as np

def rand_data():
    return np.random.uniform(low=0., high=1., size=(100,))

# Generate data.
x1, y1 = [rand_data() for i in range(2)]
x2, y2 = [rand_data() for i in range(2)]

plt.figure()
plt.scatter(x1, y1, marker='o', label='first', s=20., c='b')
plt.scatter(x2, y2, marker='o', label='second', s=35., c='r')
# Plot legend.
lgnd = plt.legend(loc="lower left", scatterpoints=1, fontsize=10)
lgnd.legendHandles[0]._sizes = [30]
lgnd.legendHandles[1]._sizes = [30]
plt.show()

Now the _sizes (another underscore property) does the trick. No need to touch the source, even though this is quite a hack. But now you can use everything scatter offers.

enter image description here

Anthropophagi answered 11/7, 2014 at 22:29 Comment(9)
blue is supposed to be smaller, but it should be straight forward to fix, +1Seep
Amazing answer DrV! Sorry I didn't comment sooner, I thought I had but apparently I never sent the cmmt.Trophy
I just wanted to point out that the second hack doesn't work anymore, at least for me (python 3.5, matplotlib 1.5.1). Perhaps they changed something in the code of matplotlib. The first one does work though, thanks a lot for that.Carinthia
Thanks. I also needed to move my marker because it looked to be in a weird spot after it was resized: lgnd.legendHandles[0]._offsets += np.array([[5,0]])Extremadura
Great tip. legendHandles method doesn't work for me on python 3.6. But first method still does.Credo
Where did you find out how to do this? legendHandles doesn't even seem to exist! matplotlib.org/stable/search.html?q=legendHandlesSalaried
matplotlib 3.4 doesn't seem to expose _sizes anymore. Ref: matplotlib.org/stable/api/legend_handler_api.htmlRodmur
@Daniel Chin: in newer matplotlib, they have lgnd.legendHandles[0]._legmarker.set_markersize(MY_SIZE)Sabir
In matplotlib 3.8.0 this worked for me: for count, legend_handle in enumerate(ax.get_legend().legend_handles): / legend_handle.set(markersize = 5, alpha = 0.8)Lottie
S
77

Similarly to the answer, assuming you want all the markers with the same size:

lgnd = plt.legend(loc="lower left", scatterpoints=1, fontsize=10)
for handle in lgnd.legend_handles:
    handle.set_sizes([6.0])

With MatPlotlib 2.0.0

Snowdrop answered 24/4, 2017 at 2:40 Comment(2)
I prefer using the built-in functions like you do here (using set_sizes()) instead of manipulating the class variables directly. I mean, these functions are there for a reason, I guess.Tabloid
If you're drawing lines with markers, you'll use handle.set_markersize(8) instead - I guess it depends on the type of plot being built (matplotlib v3.8).Shelves
T
24

You can make a Line2D object that resembles your chosen markers, except with a different marker size of your choosing, and use that to construct the legend. This is nice because it doesn't require placing an object in your axes (potentially triggering a resize event), and it doesn't require use of any hidden attributes. The only real downside is that you have to construct the legend explicitly from lists of objects and labels, but this is a well-documented matplotlib feature so it feels pretty safe to use.

from matplotlib.lines import Line2D
import matplotlib.pyplot as plt
import numpy as np

def rand_data():
    return np.random.uniform(low=0., high=1., size=(100,))

# Generate data.
x1, y1 = [rand_data() for i in range(2)]
x2, y2 = [rand_data() for i in range(2)]

plt.figure()
plt.scatter(x1, y1, marker='o', label='first', s=20., c='b')
plt.scatter(x2, y2, marker='o', label='second', s=35., c='r')

# Create dummy Line2D objects for legend
h1 = Line2D([0], [0], marker='o', markersize=np.sqrt(20), color='b', linestyle='None')
h2 = Line2D([0], [0], marker='o', markersize=np.sqrt(20), color='r', linestyle='None')

# Set axes limits
plt.gca().set_xlim(-0.2, 1.2)
plt.gca().set_ylim(-0.2, 1.2)

# Plot legend.
plt.legend([h1, h2], ['first', 'second'], loc="lower left", markerscale=2,
           scatterpoints=1, fontsize=10)
plt.show()

resulting figure

Transubstantiate answered 21/4, 2018 at 2:54 Comment(0)
T
13

I did not have much success using @DrV's solution though perhaps my use case is unique. Because of the density of points, I am using the smallest marker size, i.e. plt.plot(x, y, '.', ms=1, ...), and want the legend symbols larger.

I followed the recommendation I found on the matplotlib forums:

  1. plot the data (no labels)
  2. record axes limit (xlimits = plt.xlim())
  3. plot fake data far away from real data with legend-appropriate symbol colors and sizes
  4. restore axes limits (plt.xlim(xlimits))
  5. create legend

Here is how it turned out (for this the dots are actually less important that the lines): enter image description here

Hope this helps someone else.

Turkey answered 18/9, 2015 at 6:46 Comment(2)
That's a beautiful plotCarnap
I had the same problem, none of the other solutions seemed to work for my bubble plot where I plot each bubble individually using matplotlib scatter. The only one that really worked was your solution. It feels a little hacky but hey... whatever works right? Thanks a lotOmega
D
10

Just another alternative here. This has the advantage that it would not use any "private" methods and works even with other objects than scatters present in the legend. The key is to map the scatter PathCollection to a HandlerPathCollection with an updating function being set to it.

def update(handle, orig):
    handle.update_from(orig)
    handle.set_sizes([64])

plt.legend(handler_map={PathCollection : HandlerPathCollection(update_func=update)})

Complete code example:

import matplotlib.pyplot as plt
import numpy as np; np.random.seed(42)
from matplotlib.collections import PathCollection
from matplotlib.legend_handler import HandlerPathCollection, HandlerLine2D

colors = ["limegreen", "crimson", "indigo"]
markers = ["o", "s", r"$\clubsuit$"]
labels = ["ABC", "DEF", "XYZ"]
plt.plot(np.linspace(0,1,8), np.random.rand(8), marker="o", markersize=22, label="A line")
for i,(c,m,l) in enumerate(zip(colors,markers,labels)):
    plt.scatter(np.random.rand(8),np.random.rand(8), 
                c=c, marker=m, s=10+np.exp(i*2.9), label=l)

def updatescatter(handle, orig):
    handle.update_from(orig)
    handle.set_sizes([64])

def updateline(handle, orig):
    handle.update_from(orig)
    handle.set_markersize(8)

plt.legend(handler_map={PathCollection : HandlerPathCollection(update_func=updatescatter),
                        plt.Line2D : HandlerLine2D(update_func = updateline)})

plt.show()

enter image description here

Dikmen answered 6/10, 2018 at 1:32 Comment(0)
T
0

To piggyback on 'ImportanceOfBeingErnest' excellent solution above - to make legend markers hollow update function 'updateline(handle, orig)' to:

    def updateline(handle, orig):
        handle.update_from(orig)
        handle.set_markersize(3)
        handle.set_markeredgewidth(.5)
        """ 
        Order is critical in order to match legend to plot colors
        1. Set markeredgecolor to markerfacecolor then
        2. Set markerfacecolor to 'w' or 'none'

        """
        handle.set_markeredgecolor(handle.get_markerfacecolor())
        handle.set_markerfacecolor('none')
Teri answered 23/12, 2023 at 7:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.