Skip to content

Commit 2b2016a

Browse files
committed
Merge pull request #3499 from cimarronm/legend_marker_label_placement
ENH : Legend marker label placement control
2 parents 68176eb + d7210ff commit 2b2016a

File tree

7 files changed

+68
-25
lines changed

7 files changed

+68
-25
lines changed

doc/users/whats_new.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ revision, see the :ref:`github-stats`.
2929
new in matplotlib-1.5
3030
=====================
3131

32+
Legend
33+
------
34+
Added ability to place the label before the marker in a legend box with
35+
``markerfirst`` keyword
36+
37+
3238
Widgets
3339
-------
3440

lib/matplotlib/axes/_axes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,11 @@ def legend(self, *args, **kwargs):
360360
drawn ones. Default is ``None`` which will take the value from
361361
the ``legend.markerscale`` :data:`rcParam <matplotlib.rcParams>`.
362362
363+
*markerfirst*: [ *True* | *False* ]
364+
if *True*, legend marker is placed to the left of the legend label
365+
if *False*, legend marker is placed to the right of the legend
366+
label
367+
363368
frameon : None or bool
364369
Control whether a frame should be drawn around the legend.
365370
Default is ``None`` which will take the value from the

lib/matplotlib/figure.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,11 @@ def legend(self, handles, labels, *args, **kwargs):
11481148
The relative size of legend markers vs. original. If *None*, use rc
11491149
settings.
11501150
1151+
*markerfirst*: [ *True* | *False* ]
1152+
if *True*, legend marker is placed to the left of the legend label
1153+
if *False*, legend marker is placed to the right of the legend
1154+
label
1155+
11511156
*fancybox*: [ *None* | *False* | *True* ]
11521157
if *True*, draw a frame with a round fancybox. If *None*, use rc
11531158

lib/matplotlib/legend.py

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@
3838
from matplotlib.font_manager import FontProperties
3939
from matplotlib.lines import Line2D
4040
from matplotlib.patches import Patch, Rectangle, Shadow, FancyBboxPatch
41-
from matplotlib.collections import LineCollection, RegularPolyCollection, \
42-
CircleCollection, PathCollection, PolyCollection
41+
from matplotlib.collections import (LineCollection, RegularPolyCollection,
42+
CircleCollection, PathCollection,
43+
PolyCollection)
4344
from matplotlib.transforms import Bbox, BboxBase, TransformedBbox
4445
from matplotlib.transforms import BboxTransformTo, BboxTransformFrom
4546

@@ -93,7 +94,8 @@ def _update_loc(self, loc_in_canvas):
9394

9495
_bbox_transform = BboxTransformFrom(bbox)
9596
self.legend._loc = tuple(
96-
_bbox_transform.transform_point(loc_in_canvas))
97+
_bbox_transform.transform_point(loc_in_canvas)
98+
)
9799

98100
def _update_bbox_to_anchor(self, loc_in_canvas):
99101

@@ -150,6 +152,8 @@ def __init__(self, parent, handles, labels,
150152
numpoints=None, # the number of points in the legend line
151153
markerscale=None, # the relative size of legend markers
152154
# vs. original
155+
markerfirst=True, # controls ordering (left-to-right) of
156+
# legend marker and label
153157
scatterpoints=None, # number of scatter points
154158
scatteryoffsets=None,
155159
prop=None, # properties for the legend texts
@@ -176,7 +180,7 @@ def __init__(self, parent, handles, labels,
176180
shadow=None,
177181
title=None, # set a title for the legend
178182

179-
framealpha=None, # set frame alpha
183+
framealpha=None, # set frame alpha
180184

181185
bbox_to_anchor=None, # bbox that the legend will be anchored.
182186
bbox_transform=None, # transform for the bbox
@@ -198,6 +202,8 @@ def __init__(self, parent, handles, labels,
198202
prop the font property
199203
fontsize the font size (used only if prop is not specified)
200204
markerscale the relative size of legend markers vs. original
205+
markerfirst If true, place legend marker to left of label
206+
If false, place legend marker to right of label
201207
numpoints the number of points in the legend for line
202208
scatterpoints the number of points in the legend for scatter plot
203209
scatteryoffsets a list of yoffsets for scatter symbols in legend
@@ -316,13 +322,15 @@ def __init__(self, parent, handles, labels,
316322
if self.isaxes:
317323
warnings.warn('Unrecognized location "%s". Falling back '
318324
'on "best"; valid locations are\n\t%s\n'
319-
% (loc, '\n\t'.join(six.iterkeys(self.codes))))
325+
% (loc, '\n\t'.join(
326+
six.iterkeys(self.codes))))
320327
loc = 0
321328
else:
322329
warnings.warn('Unrecognized location "%s". Falling back '
323330
'on "upper right"; '
324331
'valid locations are\n\t%s\n'
325-
% (loc, '\n\t'.join(six.iterkeys(self.codes))))
332+
% (loc, '\n\t'.join(
333+
six.iterkeys(self.codes))))
326334
loc = 1
327335
else:
328336
loc = self.codes[loc]
@@ -365,7 +373,7 @@ def __init__(self, parent, handles, labels,
365373
self._drawFrame = rcParams["legend.frameon"]
366374

367375
# init with null renderer
368-
self._init_legend_box(handles, labels)
376+
self._init_legend_box(handles, labels, markerfirst)
369377

370378
if framealpha is None:
371379
self.get_frame().set_alpha(rcParams["legend.framealpha"])
@@ -396,8 +404,8 @@ def _set_loc(self, loc):
396404
else:
397405
_findoffset = self._findoffset_loc
398406

399-
#def findoffset(width, height, xdescent, ydescent):
400-
# return _findoffset(width, height, xdescent, ydescent, renderer)
407+
# def findoffset(width, height, xdescent, ydescent):
408+
# return _findoffset(width, height, xdescent, ydescent, renderer)
401409

402410
self._legend_box.set_offset(_findoffset)
403411

@@ -485,7 +493,7 @@ def _approx_text_height(self, renderer=None):
485493
RegularPolyCollection: legend_handler.HandlerRegularPolyCollection(),
486494
CircleCollection: legend_handler.HandlerCircleCollection(),
487495
BarContainer: legend_handler.HandlerPatch(
488-
update_func=legend_handler.update_from_first_child),
496+
update_func=legend_handler.update_from_first_child),
489497
tuple: legend_handler.HandlerTuple(),
490498
PathCollection: legend_handler.HandlerPathCollection(),
491499
PolyCollection: legend_handler.HandlerPolyCollection()
@@ -558,7 +566,7 @@ def get_legend_handler(legend_handler_map, orig_handle):
558566

559567
return handler
560568

561-
def _init_legend_box(self, handles, labels):
569+
def _init_legend_box(self, handles, labels, markerfirst=True):
562570
"""
563571
Initialize the legend_box. The legend_box is an instance of
564572
the OffsetBox, which is packed with legend handles and
@@ -606,10 +614,11 @@ def _init_legend_box(self, handles, labels):
606614
handler = self.get_legend_handler(legend_handler_map, orig_handle)
607615
if handler is None:
608616
warnings.warn(
609-
"Legend does not support {!r} instances.\nA proxy artist "
610-
"may be used instead.\nSee: "
611-
"http://matplotlib.org/users/legend_guide.html"
612-
"#using-proxy-artist".format(orig_handle))
617+
"Legend does not support {!r} instances.\nA proxy artist "
618+
"may be used instead.\nSee: "
619+
"http://matplotlib.org/users/legend_guide.html"
620+
"#using-proxy-artist".format(orig_handle)
621+
)
613622
# We don't have a handle for this artist, so we just defer
614623
# to None.
615624
handle_list.append(None)
@@ -654,11 +663,10 @@ def _init_legend_box(self, handles, labels):
654663
# starting index of each column and number of rows in it.
655664
largecol = safezip(list(xrange(0,
656665
num_largecol * (nrows + 1),
657-
(nrows + 1))),
666+
(nrows + 1))),
658667
[nrows + 1] * num_largecol)
659668
smallcol = safezip(list(xrange(num_largecol * (nrows + 1),
660-
len(handleboxes),
661-
nrows)),
669+
len(handleboxes), nrows)),
662670
[nrows] * num_smallcol)
663671
else:
664672
largecol, smallcol = [], []
@@ -669,16 +677,24 @@ def _init_legend_box(self, handles, labels):
669677
# pack handleBox and labelBox into itemBox
670678
itemBoxes = [HPacker(pad=0,
671679
sep=self.handletextpad * fontsize,
672-
children=[h, t], align="baseline")
680+
children=[h, t] if markerfirst else [t, h],
681+
align="baseline")
673682
for h, t in handle_label[i0:i0 + di]]
674683
# minimumdescent=False for the text of the last row of the column
675-
itemBoxes[-1].get_children()[1].set_minimumdescent(False)
684+
if markerfirst:
685+
itemBoxes[-1].get_children()[1].set_minimumdescent(False)
686+
else:
687+
itemBoxes[-1].get_children()[0].set_minimumdescent(False)
676688

677689
# pack columnBox
690+
if markerfirst:
691+
alignment = "baseline"
692+
else:
693+
alignment = "right"
678694
columnbox.append(VPacker(pad=0,
679-
sep=self.labelspacing * fontsize,
680-
align="baseline",
681-
children=itemBoxes))
695+
sep=self.labelspacing * fontsize,
696+
align=alignment,
697+
children=itemBoxes))
682698

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

911-
#tx, ty = self.legendPatch.get_x(), self.legendPatch.get_y()
927+
# tx, ty = self.legendPatch.get_x(), self.legendPatch.get_y()
912928

913929
candidates = []
914930
for l, b in consider:

lib/matplotlib/tests/test_coding_standards.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,6 @@ def test_pep8_conformance_installed_files():
178178
'font_manager.py',
179179
'fontconfig_pattern.py',
180180
'gridspec.py',
181-
'legend.py',
182181
'legend_handler.py',
183182
'mathtext.py',
184183
'patheffects.py',

lib/matplotlib/tests/test_legend.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,18 @@ def test_various_labels():
6464
ax.legend(numpoints=1, loc=0)
6565

6666

67+
@image_comparison(baseline_images=['legend_labels_first'], extensions=['png'],
68+
remove_text=True)
69+
def test_labels_first():
70+
# test labels to left of markers
71+
fig = plt.figure()
72+
ax = fig.add_subplot(111)
73+
ax.plot(np.arange(10), '-o', label=1)
74+
ax.plot(np.ones(10)*5, ':x', label="x")
75+
ax.plot(np.arange(20, 10, -1), 'd', label="diamond")
76+
ax.legend(loc=0, markerfirst=False)
77+
78+
6779
@image_comparison(baseline_images=['fancy'], remove_text=True)
6880
def test_fancy():
6981
# using subplot triggers some offsetbox functionality untested elsewhere

0 commit comments

Comments
 (0)