diff --git a/doc/users/whats_new.rst b/doc/users/whats_new.rst index 3590d725ff51..bd09d89be67d 100644 --- a/doc/users/whats_new.rst +++ b/doc/users/whats_new.rst @@ -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 ------- diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index cbcff3fa2e2d..482ba62b5226 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -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 `. + *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 diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index af9c34d527d6..ee34e9d8b938 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -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 diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 00ffe4485ce0..e5c0fc47a05a 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -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 @@ -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): @@ -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 @@ -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 @@ -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 @@ -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] @@ -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"]) @@ -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) @@ -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() @@ -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 @@ -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) @@ -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 = [], [] @@ -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" @@ -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: diff --git a/lib/matplotlib/tests/baseline_images/test_legend/legend_labels_first.png b/lib/matplotlib/tests/baseline_images/test_legend/legend_labels_first.png new file mode 100644 index 000000000000..accaee423015 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_legend/legend_labels_first.png differ diff --git a/lib/matplotlib/tests/test_coding_standards.py b/lib/matplotlib/tests/test_coding_standards.py index 5a0c35331a25..ea372a8f0652 100644 --- a/lib/matplotlib/tests/test_coding_standards.py +++ b/lib/matplotlib/tests/test_coding_standards.py @@ -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', diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 6242e1782900..048357410b35 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -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