Skip to content

Legend marker label placement #3499

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

Merged
Merged
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
6 changes: 6 additions & 0 deletions doc/users/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ revision, see the :ref:`github-stats`.
new in matplotlib-1.5
=====================

Legend
------
Added ability to place the label before the marker in a legend box with
``markerfirst`` keyword


Widgets
-------

Expand Down
5 changes: 5 additions & 0 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,11 @@ def legend(self, *args, **kwargs):
drawn ones. Default is ``None`` which will take the value from
the ``legend.markerscale`` :data:`rcParam <matplotlib.rcParams>`.

*markerfirst*: [ *True* | *False* ]
if *True*, legend marker is placed to the left of the legend label
if *False*, legend marker is placed to the right of the legend
label

frameon : None or bool
Control whether a frame should be drawn around the legend.
Default is ``None`` which will take the value from the
Expand Down
5 changes: 5 additions & 0 deletions lib/matplotlib/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,11 @@ def legend(self, handles, labels, *args, **kwargs):
The relative size of legend markers vs. original. If *None*, use rc
settings.

*markerfirst*: [ *True* | *False* ]
if *True*, legend marker is placed to the left of the legend label
if *False*, legend marker is placed to the right of the legend
label

*fancybox*: [ *None* | *False* | *True* ]
if *True*, draw a frame with a round fancybox. If *None*, use rc

Expand Down
64 changes: 40 additions & 24 deletions lib/matplotlib/legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@
from matplotlib.font_manager import FontProperties
from matplotlib.lines import Line2D
from matplotlib.patches import Patch, Rectangle, Shadow, FancyBboxPatch
from matplotlib.collections import LineCollection, RegularPolyCollection, \
CircleCollection, PathCollection, PolyCollection
from matplotlib.collections import (LineCollection, RegularPolyCollection,
CircleCollection, PathCollection,
PolyCollection)
from matplotlib.transforms import Bbox, BboxBase, TransformedBbox
from matplotlib.transforms import BboxTransformTo, BboxTransformFrom

Expand Down Expand Up @@ -93,7 +94,8 @@ def _update_loc(self, loc_in_canvas):

_bbox_transform = BboxTransformFrom(bbox)
self.legend._loc = tuple(
_bbox_transform.transform_point(loc_in_canvas))
_bbox_transform.transform_point(loc_in_canvas)
)

def _update_bbox_to_anchor(self, loc_in_canvas):

Expand Down Expand Up @@ -150,6 +152,8 @@ def __init__(self, parent, handles, labels,
numpoints=None, # the number of points in the legend line
markerscale=None, # the relative size of legend markers
# vs. original
markerfirst=True, # controls ordering (left-to-right) of
# legend marker and label
scatterpoints=None, # number of scatter points
scatteryoffsets=None,
prop=None, # properties for the legend texts
Expand All @@ -176,7 +180,7 @@ def __init__(self, parent, handles, labels,
shadow=None,
title=None, # set a title for the legend

framealpha=None, # set frame alpha
framealpha=None, # set frame alpha

bbox_to_anchor=None, # bbox that the legend will be anchored.
bbox_transform=None, # transform for the bbox
Expand All @@ -198,6 +202,8 @@ def __init__(self, parent, handles, labels,
prop the font property
fontsize the font size (used only if prop is not specified)
markerscale the relative size of legend markers vs. original
markerfirst If true, place legend marker to left of label
If false, place legend marker to right of label
numpoints the number of points in the legend for line
scatterpoints the number of points in the legend for scatter plot
scatteryoffsets a list of yoffsets for scatter symbols in legend
Expand Down Expand Up @@ -316,13 +322,15 @@ def __init__(self, parent, handles, labels,
if self.isaxes:
warnings.warn('Unrecognized location "%s". Falling back '
'on "best"; valid locations are\n\t%s\n'
% (loc, '\n\t'.join(six.iterkeys(self.codes))))
% (loc, '\n\t'.join(
six.iterkeys(self.codes))))
loc = 0
else:
warnings.warn('Unrecognized location "%s". Falling back '
'on "upper right"; '
'valid locations are\n\t%s\n'
% (loc, '\n\t'.join(six.iterkeys(self.codes))))
% (loc, '\n\t'.join(
six.iterkeys(self.codes))))
loc = 1
else:
loc = self.codes[loc]
Expand Down Expand Up @@ -365,7 +373,7 @@ def __init__(self, parent, handles, labels,
self._drawFrame = rcParams["legend.frameon"]

# init with null renderer
self._init_legend_box(handles, labels)
self._init_legend_box(handles, labels, markerfirst)

if framealpha is None:
self.get_frame().set_alpha(rcParams["legend.framealpha"])
Expand Down Expand Up @@ -396,8 +404,8 @@ def _set_loc(self, loc):
else:
_findoffset = self._findoffset_loc

#def findoffset(width, height, xdescent, ydescent):
# return _findoffset(width, height, xdescent, ydescent, renderer)
# def findoffset(width, height, xdescent, ydescent):
# return _findoffset(width, height, xdescent, ydescent, renderer)

self._legend_box.set_offset(_findoffset)

Expand Down Expand Up @@ -485,7 +493,7 @@ def _approx_text_height(self, renderer=None):
RegularPolyCollection: legend_handler.HandlerRegularPolyCollection(),
CircleCollection: legend_handler.HandlerCircleCollection(),
BarContainer: legend_handler.HandlerPatch(
update_func=legend_handler.update_from_first_child),
update_func=legend_handler.update_from_first_child),
tuple: legend_handler.HandlerTuple(),
PathCollection: legend_handler.HandlerPathCollection(),
PolyCollection: legend_handler.HandlerPolyCollection()
Expand Down Expand Up @@ -558,7 +566,7 @@ def get_legend_handler(legend_handler_map, orig_handle):

return handler

def _init_legend_box(self, handles, labels):
def _init_legend_box(self, handles, labels, markerfirst=True):
"""
Initialize the legend_box. The legend_box is an instance of
the OffsetBox, which is packed with legend handles and
Expand Down Expand Up @@ -606,10 +614,11 @@ def _init_legend_box(self, handles, labels):
handler = self.get_legend_handler(legend_handler_map, orig_handle)
if handler is None:
warnings.warn(
"Legend does not support {!r} instances.\nA proxy artist "
"may be used instead.\nSee: "
"http://matplotlib.org/users/legend_guide.html"
"#using-proxy-artist".format(orig_handle))
"Legend does not support {!r} instances.\nA proxy artist "
"may be used instead.\nSee: "
"http://matplotlib.org/users/legend_guide.html"
"#using-proxy-artist".format(orig_handle)
)
# We don't have a handle for this artist, so we just defer
# to None.
handle_list.append(None)
Expand Down Expand Up @@ -654,11 +663,10 @@ def _init_legend_box(self, handles, labels):
# starting index of each column and number of rows in it.
largecol = safezip(list(xrange(0,
num_largecol * (nrows + 1),
(nrows + 1))),
(nrows + 1))),
[nrows + 1] * num_largecol)
smallcol = safezip(list(xrange(num_largecol * (nrows + 1),
len(handleboxes),
nrows)),
len(handleboxes), nrows)),
[nrows] * num_smallcol)
else:
largecol, smallcol = [], []
Expand All @@ -669,16 +677,24 @@ def _init_legend_box(self, handles, labels):
# pack handleBox and labelBox into itemBox
itemBoxes = [HPacker(pad=0,
sep=self.handletextpad * fontsize,
children=[h, t], align="baseline")
children=[h, t] if markerfirst else [t, h],
align="baseline")
for h, t in handle_label[i0:i0 + di]]
# minimumdescent=False for the text of the last row of the column
itemBoxes[-1].get_children()[1].set_minimumdescent(False)
if markerfirst:
itemBoxes[-1].get_children()[1].set_minimumdescent(False)
else:
itemBoxes[-1].get_children()[0].set_minimumdescent(False)

# pack columnBox
if markerfirst:
alignment = "baseline"
else:
alignment = "right"
columnbox.append(VPacker(pad=0,
sep=self.labelspacing * fontsize,
align="baseline",
children=itemBoxes))
sep=self.labelspacing * fontsize,
align=alignment,
children=itemBoxes))

if self._mode == "expand":
mode = "expand"
Expand Down Expand Up @@ -908,7 +924,7 @@ def _find_best_position(self, width, height, renderer, consider=None):
renderer)
for x in range(1, len(self.codes))]

#tx, ty = self.legendPatch.get_x(), self.legendPatch.get_y()
# tx, ty = self.legendPatch.get_x(), self.legendPatch.get_y()

candidates = []
for l, b in consider:
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion lib/matplotlib/tests/test_coding_standards.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@ def test_pep8_conformance_installed_files():
'font_manager.py',
'fontconfig_pattern.py',
'gridspec.py',
'legend.py',
'legend_handler.py',
'mathtext.py',
'patheffects.py',
Expand Down
12 changes: 12 additions & 0 deletions lib/matplotlib/tests/test_legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ def test_various_labels():
ax.legend(numpoints=1, loc=0)


@image_comparison(baseline_images=['legend_labels_first'], extensions=['png'],
remove_text=True)
def test_labels_first():
# test labels to left of markers
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(np.arange(10), '-o', label=1)
ax.plot(np.ones(10)*5, ':x', label="x")
ax.plot(np.arange(20, 10, -1), 'd', label="diamond")
ax.legend(loc=0, markerfirst=False)


@image_comparison(baseline_images=['fancy'], remove_text=True)
def test_fancy():
# using subplot triggers some offsetbox functionality untested elsewhere
Expand Down