Skip to content

Update legend to automatically grow with marker_size #10765

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions lib/matplotlib/legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,12 +244,23 @@ def _update_bbox_to_anchor(self, loc_in_canvas):
Default is ``None``, which will take the value from
:rc:`legend.labelspacing`.

scalehandlebox : None or bool
Control whether to scale each individual handlebox up to fit it's handle.
Default is ``None``, which will take the value from
:rc:`legend.scalehandlebox`.

handlelength : float or None
The length of the legend handles.
Measured in font-size units.
Default is ``None``, which will take the value from
:rc:`legend.handlelength`.

handleheight : float or None
The height of the legend handles.
Measured in font-size units.
Default is ``None``, which will take the value from
:rc:`legend.handleheight`.

handletextpad : float or None
The pad between the legend handle and text.
Measured in font-size units.
Expand Down Expand Up @@ -317,6 +328,7 @@ def __init__(self, parent, handles, labels,
borderpad=None, # the whitespace inside the legend border
labelspacing=None, # the vertical space between the legend
# entries
scalehandlebox=None, # scale handlebox to fit handle
handlelength=None, # the length of the legend handles
handleheight=None, # the height of the legend handles
handletextpad=None, # the pad between the legend handle
Expand Down Expand Up @@ -404,9 +416,9 @@ def __init__(self, parent, handles, labels,

locals_view = locals()
for name in ["numpoints", "markerscale", "shadow", "columnspacing",
"scatterpoints", "handleheight", 'borderpad',
'labelspacing', 'handlelength', 'handletextpad',
'borderaxespad']:
"scatterpoints", "scalehandlebox", "handleheight",
'handlelength', 'borderpad', 'labelspacing',
'handletextpad', 'borderaxespad']:
if locals_view[name] is None:
value = rcParams["legend." + name]
else:
Expand Down Expand Up @@ -746,6 +758,7 @@ def _init_legend_box(self, handles, labels, markerfirst=True):
descent = 0.35 * self._approx_text_height() * (self.handleheight - 0.7)
# 0.35 and 0.7 are just heuristic numbers and may need to be improved.
height = self._approx_text_height() * self.handleheight - descent
width = self.handlelength * fontsize
# each handle needs to be drawn inside a box of (x, y, w, h) =
# (0, -descent, width, height). And their coordinates should
# be given in the display coordinates.
Expand Down Expand Up @@ -773,7 +786,7 @@ def _init_legend_box(self, handles, labels, markerfirst=True):
textbox = TextArea(lab, textprops=label_prop,
multilinebaseline=True,
minimumdescent=True)
handlebox = DrawingArea(width=self.handlelength * fontsize,
handlebox = DrawingArea(width=width,
height=height,
xdescent=0., ydescent=descent)

Expand Down
165 changes: 160 additions & 5 deletions lib/matplotlib/legend_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,58 @@ def adjust_drawing_area(self, legend, orig_handle,
height = height - self._ypad * fontsize
return xdescent, ydescent, width, height

def _scale_dimensions(self, legend, handlebox, orig_handle):
'''
Scales up handlebox dimensions to fit orig_handle

Parameters
----------
legend : :class:`matplotlib.legend.Legend` instance
The legend that will contain the orig_handle
handlebox: :class:`matplotlib.offsetbox.DrawingArea`
The drawing area that is to be scaled
orig_handle : :class:`matplotlib.artist.Artist` or similar
The object that the output width and height must be large
enough to fit.

'''
width = max(handlebox.width, self.handle_width(legend, orig_handle))
height = max(handlebox.height, self.handle_height(legend, orig_handle))
handlebox.set_width(width)
handlebox.set_height(height)

def handle_width(self, legend, orig_handle):
'''
Overriden in children classes if orig_handle could be larger
than the default DrawingArea height, returns the height of orig_handle.

Parameters
----------
legend : :class:`matplotlib.legend.Legend` instance
The legend that will contain the orig_handle.
orig_handle : :class:`matplotlib.artist.Artist` or similar
The object that the output width and height must be large
enough to fit.

'''
return -1

def handle_height(self, legend, orig_handle):
'''
Overriden in children classes if orig_handle could be larger
than the default DrawingArea height, returns the height of orig_handle.

Parameters
----------
legend : :class:`matplotlib.legend.Legend` instance
The legend that will contain the orig_handle.
orig_handle : :class:`matplotlib.artist.Artist` or similar
The object that the output width and height must be large
enough to fit.

'''
return -1

def legend_artist(self, legend, orig_handle,
fontsize, handlebox):
"""
Expand All @@ -105,6 +157,8 @@ def legend_artist(self, legend, orig_handle,
be added to this handlebox inside this method.

"""
if legend.scalehandlebox:
self._scale_dimensions(legend, handlebox, orig_handle)
xdescent, ydescent, width, height = self.adjust_drawing_area(
legend, orig_handle,
handlebox.xdescent, handlebox.ydescent,
Expand All @@ -113,7 +167,6 @@ def legend_artist(self, legend, orig_handle,
artists = self.create_artists(legend, orig_handle,
xdescent, ydescent, width, height,
fontsize, handlebox.get_transform())

# create_artists will return a list of artists.
for a in artists:
handlebox.add_artist(a)
Expand Down Expand Up @@ -224,6 +277,54 @@ def __init__(self, marker_pad=0.3, numpoints=None, **kw):
HandlerNpoints.__init__(self, marker_pad=marker_pad,
numpoints=numpoints, **kw)

def handle_width(self, legend, orig_handle):
'''
If *orig_handle* contains a marker, returns markersize multiplied by
legend.marker_scale, if the marker is not significantly
larger in width, do nothing.

Parameters
----------
legend : :class:`matplotlib.legend.Legend` instance
The legend that will contain the orig_handle.
orig_handle : :class:`matplotlib.artist.Artist` or similar
The object that the output width and height must be large
enough to fit.

'''

marker = orig_handle.get_marker()
marker_size = orig_handle.get_markersize()
if marker and marker_size > 0:
if legend.markerscale != 1:
marker_size = marker_size * legend.markerscale
return marker_size
return -1

def handle_height(self, legend, orig_handle):
'''
If *orig_handle* contains a marker, returns markersize multiplied by
legend.marker_scale, if the marker is not significantly
larger in height, do nothing.

Parameters
----------
legend : :class:`matplotlib.legend.Legend` instance
The legend that will contain the orig_handle.
orig_handle : :class:`matplotlib.artist.Artist` or similar
The object that the output width and height must be large
enough to fit.

'''

marker = orig_handle.get_marker()
marker_size = orig_handle.get_markersize()
if marker and marker_size > 0:
if legend.markerscale != 1:
marker_size = marker_size * legend.markerscale
return marker_size
return -1

def create_artists(self, legend, orig_handle,
xdescent, ydescent, width, height, fontsize,
trans):
Expand Down Expand Up @@ -305,6 +406,12 @@ class HandlerLineCollection(HandlerLine2D):
"""
Handler for `.LineCollection` instances.
"""
def handle_width(self, legend, orig_handle):
return -1

def handle_height(self, legend, orig_handle):
return -1

def get_numpoints(self, legend):
if self._numpoints is None:
return legend.scatterpoints
Expand Down Expand Up @@ -437,6 +544,12 @@ class HandlerErrorbar(HandlerLine2D):
"""
Handler for Errorbars.
"""
def handle_width(self, legend, orig_handle):
return -1

def handle_height(self, legend, orig_handle):
return -1

def __init__(self, xerr_size=0.5, yerr_size=None,
marker_pad=0.3, numpoints=None, **kw):

Expand Down Expand Up @@ -499,8 +612,8 @@ def create_artists(self, legend, orig_handle,
handle_caplines = []

if orig_handle.has_xerr:
verts = [ ((x - xerr_size, y), (x + xerr_size, y))
for x, y in zip(xdata_marker, ydata_marker)]
verts = [((x - xerr_size, y), (x + xerr_size, y))
for x, y in zip(xdata_marker, ydata_marker)]
coll = mcoll.LineCollection(verts)
self.update_prop(coll, barlinecols[0], legend)
handle_barlinecols.append(coll)
Expand All @@ -517,8 +630,8 @@ def create_artists(self, legend, orig_handle,
handle_caplines.append(capline_right)

if orig_handle.has_yerr:
verts = [ ((x, y - yerr_size), (x, y + yerr_size))
for x, y in zip(xdata_marker, ydata_marker)]
verts = [((x, y - yerr_size), (x, y + yerr_size))
for x, y in zip(xdata_marker, ydata_marker)]
coll = mcoll.LineCollection(verts)
self.update_prop(coll, barlinecols[0], legend)
handle_barlinecols.append(coll)
Expand Down Expand Up @@ -652,6 +765,48 @@ def __init__(self, ndivide=1, pad=None, **kwargs):
self._pad = pad
HandlerBase.__init__(self, **kwargs)

def handle_width(self, legend, orig_handle):
'''
Returns width of largest handle in *orig_handle* tuple.

Parameters
----------
legend : :class:`matplotlib.legend.Legend` instance
The legend that will contain the orig_handle.
orig_handle : :class:`matplotlib.artist.Artist` or similar
The object that the output width and height must be large
enough to fit.

'''
handler_map = legend.get_legend_handler_map()
largest_width = -1
for handle1 in orig_handle:
handler = legend.get_legend_handler(handler_map, handle1)
largest_width = max(largest_width,
handler.handle_width(legend, handle1))
return largest_width

def handle_height(self, legend, orig_handle):
'''
Returns width of largest handle in *orig_handle* tuple.

Parameters
----------
legend : :class:`matplotlib.legend.Legend` instance
The legend that will contain the orig_handle.
orig_handle : :class:`matplotlib.artist.Artist` or similar
The object that the output width and height must be large
enough to fit.

'''
handler_map = legend.get_legend_handler_map()
largest_height = -1
for handle1 in orig_handle:
handler = legend.get_legend_handler(handler_map, handle1)
largest_height = max(largest_height,
handler.handle_height(legend, handle1))
return largest_height

def create_artists(self, legend, orig_handle,
xdescent, ydescent, width, height, fontsize,
trans):
Expand Down
2 changes: 2 additions & 0 deletions lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,8 @@ def _validate_linestyle(ls):
'legend.borderpad': [0.4, validate_float], # units are fontsize
# the vertical space between the legend entries
'legend.labelspacing': [0.5, validate_float],
# whether or not to scale handlebox to fit handle
'legend.scalehandlebox': [True, validate_bool],
# the length of the legend lines
'legend.handlelength': [2., validate_float],
# the length of the legend lines
Expand Down
Binary file modified lib/matplotlib/tests/baseline_images/test_cycles/marker_cycle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions lib/matplotlib/tests/test_legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,3 +488,38 @@ def test_legend_title_empty():
leg = ax.legend()
assert leg.get_title().get_text() == ""
assert leg.get_title().get_visible() is False


@image_comparison(baseline_images=['legend_large_markers'],
extensions=['png'], style='mpl20')
def test_large_markers():
# test that legend scales to fit markers with large markersize
plt.figure()
a, = plt.plot([1], [1], 's', markersize=40.)
b, = plt.plot([2], [1], '*', markersize=40.)
plt.xticks([], [])
plt.yticks([], [])
plt.legend([a, b], ["big square", "big star"], loc=2, numpoints=1)


@image_comparison(baseline_images=['legend_large_markerscale'],
extensions=['png'], style='mpl20')
def test_large_markerscale():
# test that legend scales to fit markers with large markerscale
plt.figure()
a, = plt.plot([1], [1], 's')
plt.xticks([], [])
plt.yticks([], [])
plt.legend([a], ["big markerscale"], markerscale=20., loc=2, numpoints=1)


@image_comparison(baseline_images=['legend_large_marker_in_tuple'],
extensions=['png'], style='mpl20')
def test_large_marker_in_tuple():
# test that legend scales to fit large markers in tuple
plt.figure()
a, = plt.plot([1], [1], "ro", c="green", markersize=15)
b, = plt.plot([1], [1], "w+", c="red", markeredgewidth=3, markersize=40)
plt.xticks([], [])
plt.yticks([], [])
plt.legend([(a, b)], ["a and b"], loc=2, numpoints=1)
1 change: 1 addition & 0 deletions matplotlibrc.template
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ backend : $TEMPLATE_BACKEND
## Dimensions as fraction of fontsize:
#legend.borderpad : 0.4 ## border whitespace
#legend.labelspacing : 0.5 ## the vertical space between the legend entries
#legend.scalehandlebox : True ## whether or not to scale handlebox to fit handle
#legend.handlelength : 2.0 ## the length of the legend lines
#legend.handleheight : 0.7 ## the height of the legend handle
#legend.handletextpad : 0.8 ## the space between the legend line and legend text
Expand Down