From 64b97b80910ef19cbbb7fa706d31aa04197ae958 Mon Sep 17 00:00:00 2001 From: Alexander Hartl Date: Sun, 22 Dec 2019 16:56:13 +0100 Subject: [PATCH 1/9] Added axis label legend functionality --- lib/matplotlib/axes/_axes.py | 45 ++++ lib/matplotlib/legend.py | 412 +++++++++++++++++++------------ lib/matplotlib/legend_handler.py | 18 +- lib/matplotlib/text.py | 66 ++++- 4 files changed, 369 insertions(+), 172 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 702b6a0db813..4b5a18222be4 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -214,6 +214,21 @@ def set_xlabel(self, xlabel, fontdict=None, labelpad=None, **kwargs): self.xaxis.labelpad = labelpad return self.xaxis.set_label_text(xlabel, fontdict, **kwargs) + def set_xlabel_legend(self, handle, **kwargs): + """ + Place a legend next to the axis label. + + Parameters + ---------- + handle: `.Artist` + An artist (e.g. lines, patches) to be shown as legend. + + **kwargs: `.BasicLegend` properties + Additional properties to control legend appearance. + """ + label = self.xaxis.get_label() + label.set_legend_handle(handle, **kwargs) + def get_ylabel(self): """ Get the ylabel text string. @@ -248,6 +263,36 @@ def set_ylabel(self, ylabel, fontdict=None, labelpad=None, **kwargs): self.yaxis.labelpad = labelpad return self.yaxis.set_label_text(ylabel, fontdict, **kwargs) + def set_ylabel_legend(self, handle, **kwargs): + """ + Place a legend next to the axis label. + + Parameters + ---------- + handle: `.Artist` + An artist (e.g. lines, patches) to be shown as legend. + + **kwargs: `.BasicLegend` properties + Additional properties to control legend appearance. + + Examples + -------- + Plot two lines on different axes with legends placed next to the + axis labels:: + + artist, = plt.plot([0, 1, 2], [0, 1, 2], "b-") + plt.ylabel('Density') + plt.ylabel_legend(artist) + + plt.twinx() + artist, = plt.plot([0, 1, 2], [0, 3, 2], "r-") + plt.ylabel('Temperature') + plt.ylabel_legend(artist) + plt.show() + """ + label = self.yaxis.get_label() + label.set_legend_handle(handle, **kwargs) + def get_legend_handles_labels(self, legend_handler_map=None): """ Return handles and labels for legend diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 8a6dc90f6d7f..c70eb629766f 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -175,24 +175,6 @@ def _update_bbox_to_anchor(self, loc_in_canvas): absolute font size in points. String values are relative to the current default font size. This argument is only used if *prop* is not specified. -numpoints : int, default: :rc:`legend.numpoints` - The number of marker points in the legend when creating a legend - entry for a `.Line2D` (line). - -scatterpoints : int, default: :rc:`legend.scatterpoints` - The number of marker points in the legend when creating - a legend entry for a `.PathCollection` (scatter plot). - -scatteryoffsets : iterable of floats, default: ``[0.375, 0.5, 0.3125]`` - The vertical offset (relative to the font size) for the markers - created for a scatter plot legend entry. 0.0 is at the base the - legend text, and 1.0 is at the top. To draw all markers at the - same height, set to ``[0.5]``. - -markerscale : float, default: :rc:`legend.markerscale` - The relative size of legend markers compared with the originally - drawn ones. - markerfirst : bool, default: True 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. @@ -236,23 +218,43 @@ def _update_bbox_to_anchor(self, loc_in_canvas): title_fontsize: str or None The fontsize of the legend's title. Default is the default fontsize. -borderpad : float, default: :rc:`legend.borderpad` - The fractional whitespace inside the legend border, in font-size units. - labelspacing : float, default: :rc:`legend.labelspacing` The vertical space between the legend entries, in font-size units. -handlelength : float, default: :rc:`legend.handlelength` - The length of the legend handles, in font-size units. - -handletextpad : float, default: :rc:`legend.handletextpad` - The pad between the legend handle and text, in font-size units. - borderaxespad : float, default: :rc:`legend.borderaxespad` The pad between the axes and legend border, in font-size units. columnspacing : float, default: :rc:`legend.columnspacing` The spacing between columns, in font-size units. +""") + +docstring.interpd.update(_basic_legend_kw_doc=""" +numpoints : int, default: :rc:`legend.numpoints` + The number of marker points in the legend when creating a legend + entry for a `.Line2D` (line). + +scatterpoints : int, default: :rc:`legend.scatterpoints` + The number of marker points in the legend when creating + a legend entry for a `.PathCollection` (scatter plot). + +scatteryoffsets : iterable of floats, default: ``[0.375, 0.5, 0.3125]`` + The vertical offset (relative to the font size) for the markers + created for a scatter plot legend entry. 0.0 is at the base the + legend text, and 1.0 is at the top. To draw all markers at the + same height, set to ``[0.5]``. + +markerscale : float, default: :rc:`legend.markerscale` + The relative size of legend markers compared with the originally + drawn ones. + +borderpad : float, default: :rc:`legend.borderpad` + The fractional whitespace inside the legend border, in font-size units. + +handlelength : float, default: :rc:`legend.handlelength` + The length of the legend handles, in font-size units. + +handletextpad : float, default: :rc:`legend.handletextpad` + The pad between the legend handle and text, in font-size units. handler_map : dict or None The custom dictionary mapping instances or types to a legend @@ -261,7 +263,218 @@ def _update_bbox_to_anchor(self, loc_in_canvas): """) -class Legend(Artist): +class BasicLegend(object): + """ + Shared elements of regular legends and axis-label legends. + """ + @docstring.dedent_interpd + def __init__(self, + numpoints=None, # the number of points in the legend line + markerscale=None, # the relative size of legend markers + # vs. original + scatterpoints=None, # number of scatter points + scatteryoffsets=None, + + # spacing & pad defined as a fraction of the font-size + borderpad=None, # the whitespace inside the legend border + handlelength=None, # the length of the legend handles + handleheight=None, # the height of the legend handles + handletextpad=None, # the pad between the legend handle + # and text + handler_map=None, + ): + """ + Parameters + ---------- + %(_basic_legend_kw_doc)s + """ + + #: A dictionary with the extra handler mappings for this Legend + #: instance. + self._custom_handler_map = handler_map + + locals_view = locals() + for name in ['numpoints', 'markerscale', 'scatterpoints', 'borderpad', + 'handleheight', 'handlelength', 'handletextpad']: + if locals_view[name] is None: + value = rcParams["legend." + name] + else: + value = locals_view[name] + setattr(self, name, value) + del locals_view + + if self.numpoints <= 0: + raise ValueError("numpoints must be > 0; it was %d" % numpoints) + + # introduce y-offset for handles of the scatter plot + if scatteryoffsets is None: + self._scatteryoffsets = np.array([3. / 8., 4. / 8., 2.5 / 8.]) + else: + self._scatteryoffsets = np.asarray(scatteryoffsets) + reps = self.scatterpoints // len(self._scatteryoffsets) + 1 + self._scatteryoffsets = np.tile(self._scatteryoffsets, + reps)[:self.scatterpoints] + + def _approx_box_height(self, fontsize, renderer=None): + """ + Return the approximate height and descent of the DrawingArea + holding the legend handle. + """ + if renderer is None: + size = fontsize + else: + size = renderer.points_to_pixels(fontsize) + + # The approximate height and descent of text. These values are + # only used for plotting the legend handle. + descent = 0.35 * size * (self.handleheight - 0.7) + # 0.35 and 0.7 are just heuristic numbers and may need to be improved. + height = size * self.handleheight - descent + # 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. + return descent, height + + # _default_handler_map defines the default mapping between plot + # elements and the legend handlers. + + _default_handler_map = { + StemContainer: legend_handler.HandlerStem(), + ErrorbarContainer: legend_handler.HandlerErrorbar(), + Line2D: legend_handler.HandlerLine2D(), + Patch: legend_handler.HandlerPatch(), + LineCollection: legend_handler.HandlerLineCollection(), + RegularPolyCollection: legend_handler.HandlerRegularPolyCollection(), + CircleCollection: legend_handler.HandlerCircleCollection(), + BarContainer: legend_handler.HandlerPatch( + update_func=legend_handler.update_from_first_child), + tuple: legend_handler.HandlerTuple(), + PathCollection: legend_handler.HandlerPathCollection(), + PolyCollection: legend_handler.HandlerPolyCollection() + } + + # (get|set|update)_default_handler_maps are public interfaces to + # modify the default handler map. + + @classmethod + def get_default_handler_map(cls): + """ + A class method that returns the default handler map. + """ + return cls._default_handler_map + + @classmethod + def set_default_handler_map(cls, handler_map): + """ + A class method to set the default handler map. + """ + cls._default_handler_map = handler_map + + @classmethod + def update_default_handler_map(cls, handler_map): + """ + A class method to update the default handler map. + """ + cls._default_handler_map.update(handler_map) + + def get_legend_handler_map(self): + """ + Return the handler map. + """ + + default_handler_map = self.get_default_handler_map() + + if self._custom_handler_map: + hm = default_handler_map.copy() + hm.update(self._custom_handler_map) + return hm + else: + return default_handler_map + + @staticmethod + def get_legend_handler(legend_handler_map, orig_handle): + """ + Return a legend handler from *legend_handler_map* that + corresponds to *orig_handler*. + + *legend_handler_map* should be a dictionary object (that is + returned by the get_legend_handler_map method). + + It first checks if the *orig_handle* itself is a key in the + *legend_handler_map* and return the associated value. + Otherwise, it checks for each of the classes in its + method-resolution-order. If no matching key is found, it + returns ``None``. + """ + try: + return legend_handler_map[orig_handle] + except (TypeError, KeyError): # TypeError if unhashable. + pass + for handle_type in type(orig_handle).mro(): + try: + return legend_handler_map[handle_type] + except KeyError: + pass + return None + + def _warn_unsupported_artist(self, handle): + cbook._warn_external( + "Legend does not support {!r} instances.\nA proxy artist " + "may be used instead.\nSee: " + "http://matplotlib.org/users/legend_guide.html" + "#creating-artists-specifically-for-adding-to-the-legend-" + "aka-proxy-artists".format(handle)) + + +class AxisLabelLegend(BasicLegend): + """ + Place a legend next to the axis labels. + """ + def __init__(self, text, **kwargs): + """ + Parameters + ---------- + text: `.Text` + The text object this legend belongs to. + + **kwargs: `.BasicLegend` properties + Additional properties controlling legend appearance. + + """ + BasicLegend.__init__(self, **kwargs) + self.text = text + self.figure = text.figure + + def init_legend(self, handle, rotate): + """Initialize DrawingArea and legend artist""" + fontsize = self.text.get_fontsize() + descent, height = self._approx_box_height(fontsize) + + legend_handler_map = self.get_legend_handler_map() + handler = self.get_legend_handler(legend_handler_map, handle) + if handler is None: + self._warn_unsupported_artist(handle) + return None + if rotate: + box_width, box_height = height, self.handlelength * fontsize + xdescent, ydescent = descent, 0 + else: + box_width, box_height = self.handlelength * fontsize, height + xdescent, ydescent = 0, descent + + self.box = DrawingArea(width=box_width, height=box_height, + xdescent=xdescent, ydescent=ydescent) + # Create the artist for the legend which represents the + # original artist/handle. + handler.legend_artist(self, handle, fontsize, self.box, rotate) + return self + + def _set_artist_props(self, a): + a.set_figure(self.text.figure) + a.axes = self.text.axes + + +class Legend(Artist, BasicLegend): """ Place a legend on the axes at location loc. @@ -287,24 +500,14 @@ def __str__(self): @docstring.dedent_interpd def __init__(self, parent, handles, labels, loc=None, - 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 fontsize=None, # keyword to set font size directly # spacing & pad defined as a fraction of the font-size - borderpad=None, # the whitespace inside the legend border labelspacing=None, # the vertical space between the legend # entries - handlelength=None, # the length of the legend handles - handleheight=None, # the height of the legend handles - handletextpad=None, # the pad between the legend handle - # and text borderaxespad=None, # the pad between the axes and legend # border columnspacing=None, # spacing between columns @@ -325,7 +528,7 @@ def __init__(self, parent, handles, labels, bbox_to_anchor=None, # bbox that the legend will be anchored. bbox_transform=None, # transform for the bbox frameon=None, # draw frame - handler_map=None, + **kwargs ): """ Parameters @@ -345,6 +548,8 @@ def __init__(self, parent, handles, labels, ---------------- %(_legend_kw_doc)s + %(_basic_legend_kw_doc)s + Notes ----- Users can specify any arbitrary location for the legend using the @@ -361,6 +566,7 @@ def __init__(self, parent, handles, labels, from matplotlib.figure import Figure Artist.__init__(self) + BasicLegend.__init__(self, **kwargs) if prop is None: if fontsize is not None: @@ -380,14 +586,8 @@ def __init__(self, parent, handles, labels, self.legendHandles = [] self._legend_title_box = None - #: A dictionary with the extra handler mappings for this Legend - #: instance. - self._custom_handler_map = handler_map - locals_view = locals() - for name in ["numpoints", "markerscale", "shadow", "columnspacing", - "scatterpoints", "handleheight", 'borderpad', - 'labelspacing', 'handlelength', 'handletextpad', + for name in ['shadow', 'columnspacing', 'labelspacing', 'borderaxespad']: if locals_view[name] is None: value = rcParams["legend." + name] @@ -412,18 +612,6 @@ def __init__(self, parent, handles, labels, ncol = 1 self._ncol = ncol - if self.numpoints <= 0: - raise ValueError("numpoints must be > 0; it was %d" % numpoints) - - # introduce y-offset for handles of the scatter plot - if scatteryoffsets is None: - self._scatteryoffsets = np.array([3. / 8., 4. / 8., 2.5 / 8.]) - else: - self._scatteryoffsets = np.asarray(scatteryoffsets) - reps = self.scatterpoints // len(self._scatteryoffsets) + 1 - self._scatteryoffsets = np.tile(self._scatteryoffsets, - reps)[:self.scatterpoints] - # _legend_box is an OffsetBox instance that contains all # legend items and will be initialized from _init_legend_box() # method. @@ -613,98 +801,6 @@ def draw(self, renderer): renderer.close_group('legend') self.stale = False - def _approx_text_height(self, renderer=None): - """ - Return the approximate height of the text. This is used to place - the legend handle. - """ - if renderer is None: - return self._fontsize - else: - return renderer.points_to_pixels(self._fontsize) - - # _default_handler_map defines the default mapping between plot - # elements and the legend handlers. - - _default_handler_map = { - StemContainer: legend_handler.HandlerStem(), - ErrorbarContainer: legend_handler.HandlerErrorbar(), - Line2D: legend_handler.HandlerLine2D(), - Patch: legend_handler.HandlerPatch(), - LineCollection: legend_handler.HandlerLineCollection(), - RegularPolyCollection: legend_handler.HandlerRegularPolyCollection(), - CircleCollection: legend_handler.HandlerCircleCollection(), - BarContainer: legend_handler.HandlerPatch( - update_func=legend_handler.update_from_first_child), - tuple: legend_handler.HandlerTuple(), - PathCollection: legend_handler.HandlerPathCollection(), - PolyCollection: legend_handler.HandlerPolyCollection() - } - - # (get|set|update)_default_handler_maps are public interfaces to - # modify the default handler map. - - @classmethod - def get_default_handler_map(cls): - """ - A class method that returns the default handler map. - """ - return cls._default_handler_map - - @classmethod - def set_default_handler_map(cls, handler_map): - """ - A class method to set the default handler map. - """ - cls._default_handler_map = handler_map - - @classmethod - def update_default_handler_map(cls, handler_map): - """ - A class method to update the default handler map. - """ - cls._default_handler_map.update(handler_map) - - def get_legend_handler_map(self): - """ - Return the handler map. - """ - - default_handler_map = self.get_default_handler_map() - - if self._custom_handler_map: - hm = default_handler_map.copy() - hm.update(self._custom_handler_map) - return hm - else: - return default_handler_map - - @staticmethod - def get_legend_handler(legend_handler_map, orig_handle): - """ - Return a legend handler from *legend_handler_map* that - corresponds to *orig_handler*. - - *legend_handler_map* should be a dictionary object (that is - returned by the get_legend_handler_map method). - - It first checks if the *orig_handle* itself is a key in the - *legend_handler_map* and return the associated value. - Otherwise, it checks for each of the classes in its - method-resolution-order. If no matching key is found, it - returns ``None``. - """ - try: - return legend_handler_map[orig_handle] - except (TypeError, KeyError): # TypeError if unhashable. - pass - for handle_type in type(orig_handle).mro(): - try: - return legend_handler_map[handle_type] - except KeyError: - pass - return None - def _init_legend_box(self, handles, labels, markerfirst=True): """ Initialize the legend_box. The legend_box is an instance of @@ -732,14 +828,7 @@ def _init_legend_box(self, handles, labels, markerfirst=True): fontproperties=self.prop, ) - # The approximate height and descent of text. These values are - # only used for plotting the legend handle. - 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 - # 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. + descent, height = self._approx_box_height(self._fontsize) # The transformation of each handle will be automatically set # to self.get_transform(). If the artist does not use its @@ -750,12 +839,7 @@ def _init_legend_box(self, handles, labels, markerfirst=True): for orig_handle, lab in zip(handles, labels): handler = self.get_legend_handler(legend_handler_map, orig_handle) if handler is None: - cbook._warn_external( - "Legend does not support {!r} instances.\nA proxy artist " - "may be used instead.\nSee: " - "http://matplotlib.org/users/legend_guide.html" - "#creating-artists-specifically-for-adding-to-the-legend-" - "aka-proxy-artists".format(orig_handle)) + self._warn_unsupported_artist(orig_handle) # We don't have a handle for this artist, so we just defer # to None. handle_list.append(None) diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 051c97da0d0b..a29412f3cab2 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -30,6 +30,7 @@ def legend_artist(self, legend, orig_handle, fontsize, handlebox) from matplotlib.lines import Line2D from matplotlib.patches import Rectangle +from matplotlib.transforms import Affine2D import matplotlib.collections as mcoll import matplotlib.colors as mcolors @@ -87,7 +88,7 @@ def adjust_drawing_area(self, legend, orig_handle, return xdescent, ydescent, width, height def legend_artist(self, legend, orig_handle, - fontsize, handlebox): + fontsize, handlebox, rotate=False): """ Return the artist that this HandlerBase generates for the given original artist/handle. @@ -112,9 +113,16 @@ def legend_artist(self, legend, orig_handle, handlebox.xdescent, handlebox.ydescent, handlebox.width, handlebox.height, fontsize) + transform = handlebox.get_transform() + if rotate: + point = (width - xdescent) / 2 + rotate_transform = Affine2D().rotate_deg_around(point, point, 90) + transform = rotate_transform + transform + width, height = height, width + xdescent, ydescent = ydescent, xdescent artists = self.create_artists(legend, orig_handle, xdescent, ydescent, width, height, - fontsize, handlebox.get_transform()) + fontsize, transform) # create_artists will return a list of artists. for a in artists: @@ -407,6 +415,12 @@ def create_artists(self, legend, orig_handle, self.update_prop(p, orig_handle, legend) p._transOffset = trans + if trans.is_affine: + # trans has only been applied to offset so far. + # Manually apply rotation and scaling to p + m = trans.get_matrix().copy() + m[:2, 2] = 0 + p.set_transform(Affine2D(m)) return [p] diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 3fd248bcc586..266cc3599bff 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -75,7 +75,7 @@ def _get_textbox(text, renderer): theta = np.deg2rad(text.get_rotation()) tr = Affine2D().rotate(-theta) - _, parts, d = text._get_layout(renderer) + _, parts, d, _ = text._get_layout(renderer) for t, wh, x, y in parts: w, h = wh @@ -164,6 +164,7 @@ def __init__(self, if linespacing is None: linespacing = 1.2 # Maybe use rcParam later. self._linespacing = linespacing + self.legend = None self.set_rotation_mode(rotation_mode) self.update(kwargs) @@ -281,6 +282,7 @@ def _get_layout(self, renderer): thisx, thisy = 0.0, 0.0 lines = self.get_text().split("\n") # Ensures lines is not empty. + legend_offset = (0, 0) ws = [] hs = [] @@ -307,7 +309,6 @@ def _get_layout(self, renderer): h = max(h, lp_h) d = max(d, lp_d) - ws.append(w) hs.append(h) # Metrics of the last line that are needed later: @@ -316,11 +317,31 @@ def _get_layout(self, renderer): if i == 0: # position at baseline thisy = -(h - d) + # reserve some space for the legend symbol + if self.legend is not None: + legend_extent = self.legend.box.get_extent(renderer) + legend_width, legend_height, _, _ = legend_extent + padding = self.legend.handletextpad * self.get_size() + rotation = self.get_rotation() + if rotation == 0: + legend_spacing = legend_width + padding + w += legend_spacing + thisx += legend_spacing + # position relative to the beginning of first line + legend_offset = (-legend_spacing, 0) + elif rotation == 90: + legend_spacing = legend_height + padding + w += legend_spacing + thisx += legend_spacing + # position relative to the beginning of first line + legend_offset = (-legend_width, -legend_spacing) else: # put baseline a good distance from bottom of previous line thisy -= max(min_dy, (h - d) * self._linespacing) + thisx = 0 - xs.append(thisx) # == 0. + ws.append(w) + xs.append(thisx) ys.append(thisy) thisy -= d @@ -422,7 +443,10 @@ def _get_layout(self, renderer): # now rotate the positions around the first (x, y) position xys = M.transform(offset_layout) - (offsetx, offsety) - ret = bbox, list(zip(lines, zip(ws, hs), *xys.T)), descent + ret = (bbox, + list(zip(lines, zip(ws, hs), *xys.T)), + descent, + xys[0, :] + legend_offset) self._cached[key] = ret return ret @@ -682,7 +706,7 @@ def draw(self, renderer): renderer.open_group('text', self.get_gid()) with _wrap_text(self) as textobj: - bbox, info, descent = textobj._get_layout(renderer) + bbox, info, descent, legend_pos = textobj._get_layout(renderer) trans = textobj.get_transform() # don't use textobj.get_position here, which refers to text @@ -731,11 +755,41 @@ def draw(self, renderer): textrenderer.draw_text(gc, x, y, clean_line, textobj._fontproperties, angle, ismath=ismath, mtext=mtext) + if self.legend is not None and angle in [0, 90]: + x, y = legend_pos + self.legend.box.set_offset((x + posx, y + posy)) + self.legend.box.draw(renderer) gc.restore() renderer.close_group('text') self.stale = False + def set_legend_handle(self, handle=None, **kwargs): + """ + Set a legend to be shown next to the text. + + Parameters + ---------- + handle: `.Artist` + An artist (e.g. lines, patches) to be shown as legend. + + **kwargs: `.BasicLegend` properties + Additional properties to control legend appearance. + """ + # import AxisLabelLegend here to avoid circular import + from matplotlib.legend import AxisLabelLegend + if handle is not None: + rotation = self.get_rotation() + if rotation not in [0, 90]: + cbook._warn_external("Legend symbols are only supported " + "for non-rotated texts and texts rotated by 90°.") + return + legend = AxisLabelLegend(self, **kwargs) + self.legend = legend.init_legend(handle, rotation == 90) + else: + self.legend = None + self.stale = True + def get_color(self): "Return the color of the text" return self._color @@ -902,7 +956,7 @@ def get_window_extent(self, renderer=None, dpi=None): if self._renderer is None: raise RuntimeError('Cannot get window extent w/o renderer') - bbox, info, descent = self._get_layout(self._renderer) + bbox, info, descent, _ = self._get_layout(self._renderer) x, y = self.get_unitless_position() x, y = self.get_transform().transform((x, y)) bbox = bbox.translated(x, y) From a81346aed36bb89c06772bdb1595e70e4c434061 Mon Sep 17 00:00:00 2001 From: Alexander Hartl Date: Sun, 22 Dec 2019 16:56:53 +0100 Subject: [PATCH 2/9] Updated boilerplate.py --- lib/matplotlib/pyplot.py | 12 ++++++++++++ tools/boilerplate.py | 2 ++ 2 files changed, 14 insertions(+) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 508f11cbd286..3a1a4d04b4ad 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -2854,6 +2854,18 @@ def ylabel(ylabel, fontdict=None, labelpad=None, **kwargs): ylabel, fontdict=fontdict, labelpad=labelpad, **kwargs) +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +@docstring.copy(Axes.set_xlabel_legend) +def xlabel_legend(handle, **kwargs): + return gca().set_xlabel_legend(handle, **kwargs) + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +@docstring.copy(Axes.set_ylabel_legend) +def ylabel_legend(handle, **kwargs): + return gca().set_ylabel_legend(handle, **kwargs) + + # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @docstring.copy(Axes.set_xscale) def xscale(value, **kwargs): diff --git a/tools/boilerplate.py b/tools/boilerplate.py index cafb850b44af..fc409f478f9c 100644 --- a/tools/boilerplate.py +++ b/tools/boilerplate.py @@ -260,6 +260,8 @@ def boilerplate_gen(): 'title:set_title', 'xlabel:set_xlabel', 'ylabel:set_ylabel', + 'xlabel_legend:set_xlabel_legend', + 'ylabel_legend:set_ylabel_legend', 'xscale:set_xscale', 'yscale:set_yscale', ) From a507f5d457093a0c9d08818649c638723dec7d3a Mon Sep 17 00:00:00 2001 From: Alexander Hartl Date: Sun, 22 Dec 2019 16:58:31 +0100 Subject: [PATCH 3/9] Added 'What's new' file --- doc/users/next_whats_new/2019-12-22-axis-label-legend.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 doc/users/next_whats_new/2019-12-22-axis-label-legend.rst diff --git a/doc/users/next_whats_new/2019-12-22-axis-label-legend.rst b/doc/users/next_whats_new/2019-12-22-axis-label-legend.rst new file mode 100644 index 000000000000..d16c9f704ac6 --- /dev/null +++ b/doc/users/next_whats_new/2019-12-22-axis-label-legend.rst @@ -0,0 +1,7 @@ +Support for axis label legends +------------------------------ + +Legends can now be placed next to axis labels, enabling more +space-efficient and intuitive ``twinx()`` plots. Such legends are created +using ``plt.xlabel_legend()`` and ``plt.ylabel_legend()``, which each +accept one artist handle as understood by ``plt.legend()``. From 6e4b669766c7a447af16d9c0647569113b26b3e6 Mon Sep 17 00:00:00 2001 From: Alexander Hartl Date: Sun, 22 Dec 2019 17:30:09 +0100 Subject: [PATCH 4/9] Optimized legend alignment --- lib/matplotlib/text.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 266cc3599bff..6aaa3600b9e3 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -328,13 +328,19 @@ def _get_layout(self, renderer): w += legend_spacing thisx += legend_spacing # position relative to the beginning of first line - legend_offset = (-legend_spacing, 0) + legend_offset = ( + -legend_spacing, + (h-d - legend_height) / 2 + ) elif rotation == 90: legend_spacing = legend_height + padding w += legend_spacing thisx += legend_spacing # position relative to the beginning of first line - legend_offset = (-legend_width, -legend_spacing) + legend_offset = ( + -(h-d + legend_width) / 2, + -legend_spacing + ) else: # put baseline a good distance from bottom of previous line thisy -= max(min_dy, (h - d) * self._linespacing) From 9b63123f42bcd8919f812ab90640922e2ba05dfc Mon Sep 17 00:00:00 2001 From: Alexander Hartl Date: Sun, 22 Dec 2019 17:37:46 +0100 Subject: [PATCH 5/9] Fixed failing test --- lib/matplotlib/offsetbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 9f9440abbc05..d7b7574ce30b 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -890,7 +890,7 @@ def get_extent(self, renderer): _, h_, d_ = renderer.get_text_width_height_descent( "lp", self._text._fontproperties, ismath=False) - bbox, info, d = self._text._get_layout(renderer) + bbox, info, d, _ = self._text._get_layout(renderer) w, h = bbox.width, bbox.height self._baseline_transform.clear() From a804d83d9a46f4149f5533031c98612cad668ec2 Mon Sep 17 00:00:00 2001 From: Alexander Hartl Date: Sun, 22 Dec 2019 17:38:37 +0100 Subject: [PATCH 6/9] Added label legend test --- .../test_axes/label_legend.png | Bin 0 -> 45723 bytes lib/matplotlib/tests/test_axes.py | 20 ++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 lib/matplotlib/tests/baseline_images/test_axes/label_legend.png diff --git a/lib/matplotlib/tests/baseline_images/test_axes/label_legend.png b/lib/matplotlib/tests/baseline_images/test_axes/label_legend.png new file mode 100644 index 0000000000000000000000000000000000000000..0bf5921433d536182a472a6f5e843e0cb38b2d73 GIT binary patch literal 45723 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 literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index d65906fdcf92..e13536f8bd76 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -45,6 +45,26 @@ def test_get_labels(): assert ax.get_ylabel() == 'y label' +@image_comparison(['label_legend.png'], style='mpl20') +def test_label_legends(): + fig, ax1 = plt.subplots() + + line1, _, _ = ax1.bar([0, 1, 2], [0, 1, 2], color='gray') + ax1.set_xlabel('X label 1') + ax1.set_ylabel('A very long Y label which requires\nmultiple lines') + ax1.set_ylabel_legend(line1) + + ax2 = ax1.twinx() + line2, = ax2.plot([0, 1, 2], [20, 10, 0], color='red') + ax2.set_ylabel('Y label 2') + ax2.set_ylabel_legend(line2) + + ax3 = ax2.twiny() + line3, = ax3.plot([100, 200, 300], [20, 5, 10], marker='D') + ax3.set_xlabel('X label 2') + ax3.set_xlabel_legend(line3) + + @image_comparison(['acorr.png'], style='mpl20') def test_acorr(): # Remove this line when this test image is regenerated. From 0beaabd404ca5bb5af73db52c03755e57c231ea6 Mon Sep 17 00:00:00 2001 From: Alexander Hartl Date: Sun, 22 Dec 2019 21:50:34 +0100 Subject: [PATCH 7/9] Some docstring fixes --- doc/api/axes_api.rst | 2 ++ lib/matplotlib/legend.py | 2 +- tutorials/intermediate/legend_guide.py | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/api/axes_api.rst b/doc/api/axes_api.rst index 84817c6d6bba..2f903d64f370 100644 --- a/doc/api/axes_api.rst +++ b/doc/api/axes_api.rst @@ -307,6 +307,8 @@ Axis Labels, title, and legend Axes.get_xlabel Axes.set_ylabel Axes.get_ylabel + Axes.set_xlabel_legend + Axes.set_ylabel_legend Axes.set_title Axes.get_title diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index c70eb629766f..f20480ee2cc0 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -259,7 +259,7 @@ def _update_bbox_to_anchor(self, loc_in_canvas): handler_map : dict or None The custom dictionary mapping instances or types to a legend handler. This `handler_map` updates the default handler map - found at :func:`matplotlib.legend.Legend.get_legend_handler_map`. + found at :func:`matplotlib.legend.BasicLegend.get_legend_handler_map`. """) diff --git a/tutorials/intermediate/legend_guide.py b/tutorials/intermediate/legend_guide.py index ec1861cfe6cf..b6e44f7f6a5a 100644 --- a/tutorials/intermediate/legend_guide.py +++ b/tutorials/intermediate/legend_guide.py @@ -165,7 +165,7 @@ # appropriate :class:`~matplotlib.legend_handler.HandlerBase` subclass. # The choice of handler subclass is determined by the following rules: # -# 1. Update :func:`~matplotlib.legend.Legend.get_legend_handler_map` +# 1. Update :func:`~matplotlib.legend.BasicLegend.get_legend_handler_map` # with the value in the ``handler_map`` keyword. # 2. Check if the ``handle`` is in the newly created ``handler_map``. # 3. Check if the type of ``handle`` is in the newly created @@ -174,7 +174,7 @@ # created ``handler_map``. # # For completeness, this logic is mostly implemented in -# :func:`~matplotlib.legend.Legend.get_legend_handler`. +# :func:`~matplotlib.legend.BasicLegend.get_legend_handler`. # # All of this flexibility means that we have the necessary hooks to implement # custom handlers for our own type of legend key. From 70eb85d1eb090f4d61839470ff840ca188316852 Mon Sep 17 00:00:00 2001 From: Alexander Hartl Date: Thu, 23 Jan 2020 15:14:54 +0100 Subject: [PATCH 8/9] Moved functionality to subclass of Text --- lib/matplotlib/axes/_axes.py | 4 +- lib/matplotlib/axis.py | 3 +- lib/matplotlib/legend.py | 122 ++++++++++++++++++++++++----------- lib/matplotlib/offsetbox.py | 2 +- lib/matplotlib/text.py | 80 ++++------------------- 5 files changed, 103 insertions(+), 108 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 4b5a18222be4..78cdbb9c155b 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -223,7 +223,7 @@ def set_xlabel_legend(self, handle, **kwargs): handle: `.Artist` An artist (e.g. lines, patches) to be shown as legend. - **kwargs: `.BasicLegend` properties + **kwargs: `.LegendConfig` properties Additional properties to control legend appearance. """ label = self.xaxis.get_label() @@ -272,7 +272,7 @@ def set_ylabel_legend(self, handle, **kwargs): handle: `.Artist` An artist (e.g. lines, patches) to be shown as legend. - **kwargs: `.BasicLegend` properties + **kwargs: `.LegendConfig` properties Additional properties to control legend appearance. Examples diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index a52bd54ab0d5..352e50aeb10f 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -11,6 +11,7 @@ import matplotlib.artist as martist import matplotlib.cbook as cbook import matplotlib.font_manager as font_manager +import matplotlib.legend as mlegend import matplotlib.lines as mlines import matplotlib.scale as mscale import matplotlib.text as mtext @@ -691,7 +692,7 @@ def __init__(self, axes, pickradius=15): self._autolabelpos = True self._smart_bounds = False # Deprecated in 3.2 - self.label = mtext.Text( + self.label = mlegend.TextWithLegend( np.nan, np.nan, fontproperties=font_manager.FontProperties( size=rcParams['axes.labelsize'], diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index f20480ee2cc0..0285e6d0dc3d 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -36,6 +36,7 @@ from matplotlib.collections import (LineCollection, RegularPolyCollection, CircleCollection, PathCollection, PolyCollection) +from matplotlib.text import Text from matplotlib.transforms import Bbox, BboxBase, TransformedBbox from matplotlib.transforms import BboxTransformTo, BboxTransformFrom @@ -228,7 +229,7 @@ def _update_bbox_to_anchor(self, loc_in_canvas): The spacing between columns, in font-size units. """) -docstring.interpd.update(_basic_legend_kw_doc=""" +docstring.interpd.update(_legend_config_kw_doc=""" numpoints : int, default: :rc:`legend.numpoints` The number of marker points in the legend when creating a legend entry for a `.Line2D` (line). @@ -259,16 +260,17 @@ def _update_bbox_to_anchor(self, loc_in_canvas): handler_map : dict or None The custom dictionary mapping instances or types to a legend handler. This `handler_map` updates the default handler map - found at :func:`matplotlib.legend.BasicLegend.get_legend_handler_map`. + found at :func:`matplotlib.legend.LegendConfig.get_legend_handler_map`. """) -class BasicLegend(object): +class LegendConfig(object): """ Shared elements of regular legends and axis-label legends. """ @docstring.dedent_interpd def __init__(self, + parent, numpoints=None, # the number of points in the legend line markerscale=None, # the relative size of legend markers # vs. original @@ -286,9 +288,10 @@ def __init__(self, """ Parameters ---------- - %(_basic_legend_kw_doc)s + %(_legend_config_kw_doc)s """ + self.parent = parent #: A dictionary with the extra handler mappings for this Legend #: instance. self._custom_handler_map = handler_map @@ -425,56 +428,99 @@ def _warn_unsupported_artist(self, handle): "#creating-artists-specifically-for-adding-to-the-legend-" "aka-proxy-artists".format(handle)) + def _set_artist_props(self, a): + self.parent._set_artist_props(a) + -class AxisLabelLegend(BasicLegend): +class TextWithLegend(Text): """ - Place a legend next to the axis labels. + Place a legend symbol next to a text. """ - def __init__(self, text, **kwargs): + def __init__(self, *args, **kwargs): """ - Parameters - ---------- - text: `.Text` - The text object this legend belongs to. - - **kwargs: `.BasicLegend` properties - Additional properties controlling legend appearance. + Valid keyword arguments are: + %(Text)s """ - BasicLegend.__init__(self, **kwargs) - self.text = text - self.figure = text.figure + Text.__init__(self, *args, **kwargs) + self.legend_config = None + self.legend = None + + def _get_layout_with_legend(self, renderer): + if self.legend is None: + return *Text._get_layout(self, renderer, 0), 0 + legend_extent = self.legend.get_extent(renderer) + legend_width, legend_height, _, _ = legend_extent + padding = self.legend_config.handletextpad * self.get_size() + rotation = self.get_rotation() + if rotation == 0: + line_indent = legend_width + padding + bbox, info, descent = Text._get_layout(self, renderer, line_indent) + line, (w, h, d), x, y = info[0] + legend_offset = ( + x - line_indent, + y + (h-d - legend_height) / 2 + ) + elif rotation == 90: + line_indent = legend_height + padding + bbox, info, descent = Text._get_layout(self, renderer, line_indent) + line, (w, h, d), x, y = info[0] + legend_offset = ( + x - (h-d + legend_width) / 2, + y - line_indent + ) + return bbox, info, descent, legend_offset - def init_legend(self, handle, rotate): - """Initialize DrawingArea and legend artist""" - fontsize = self.text.get_fontsize() - descent, height = self._approx_box_height(fontsize) + def _get_layout(self, renderer, firstline_indent=0): + bbox, info, descent, _ = self._get_layout_with_legend(renderer) + return bbox, info, descent - legend_handler_map = self.get_legend_handler_map() - handler = self.get_legend_handler(legend_handler_map, handle) + def draw(self, renderer): + Text.draw(self, renderer) + if self.legend is not None: + bbox, info, _, offset = self._get_layout_with_legend(renderer) + x, y = offset + trans = self.get_transform() + posx, posy = self.get_unitless_position() + posx, posy = trans.transform((posx, posy)) + self.legend.set_offset((x + posx, y + posy)) + self.legend.draw(renderer) + + def set_legend_handle(self, handle, **kwargs): + """Initialize DrawingArea and legend artist""" + rotation = self.get_rotation() + if rotation not in [0, 90]: + cbook._warn_external("Legend symbols are only supported " + "for non-rotated texts and texts rotated by 90°.") + return + rotate = rotation == 90 + config = LegendConfig(self.figure, **kwargs) + self.legend_config = config + fontsize = self.get_fontsize() + descent, height = config._approx_box_height(fontsize) + + legend_handler_map = config.get_legend_handler_map() + handler = config.get_legend_handler(legend_handler_map, handle) if handler is None: - self._warn_unsupported_artist(handle) - return None + config._warn_unsupported_artist(handle) + self.legend_config = None + self.legend = None + return if rotate: - box_width, box_height = height, self.handlelength * fontsize + box_width, box_height = height, config.handlelength * fontsize xdescent, ydescent = descent, 0 else: - box_width, box_height = self.handlelength * fontsize, height + box_width, box_height = config.handlelength * fontsize, height xdescent, ydescent = 0, descent - self.box = DrawingArea(width=box_width, height=box_height, - xdescent=xdescent, ydescent=ydescent) + self.legend = DrawingArea(width=box_width, height=box_height, + xdescent=xdescent, ydescent=ydescent) # Create the artist for the legend which represents the # original artist/handle. - handler.legend_artist(self, handle, fontsize, self.box, rotate) - return self - - def _set_artist_props(self, a): - a.set_figure(self.text.figure) - a.axes = self.text.axes + handler.legend_artist(config, handle, fontsize, self.legend, rotate) -class Legend(Artist, BasicLegend): +class Legend(Artist, LegendConfig): """ Place a legend on the axes at location loc. @@ -548,7 +594,7 @@ def __init__(self, parent, handles, labels, ---------------- %(_legend_kw_doc)s - %(_basic_legend_kw_doc)s + %(_legend_config_kw_doc)s Notes ----- @@ -566,7 +612,7 @@ def __init__(self, parent, handles, labels, from matplotlib.figure import Figure Artist.__init__(self) - BasicLegend.__init__(self, **kwargs) + LegendConfig.__init__(self, parent, **kwargs) if prop is None: if fontsize is not None: diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index d7b7574ce30b..9f9440abbc05 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -890,7 +890,7 @@ def get_extent(self, renderer): _, h_, d_ = renderer.get_text_width_height_descent( "lp", self._text._fontproperties, ismath=False) - bbox, info, d, _ = self._text._get_layout(renderer) + bbox, info, d = self._text._get_layout(renderer) w, h = bbox.width, bbox.height self._baseline_transform.clear() diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 6aaa3600b9e3..8795bf01e996 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -75,10 +75,10 @@ def _get_textbox(text, renderer): theta = np.deg2rad(text.get_rotation()) tr = Affine2D().rotate(-theta) - _, parts, d, _ = text._get_layout(renderer) + _, parts, d = text._get_layout(renderer) - for t, wh, x, y in parts: - w, h = wh + for t, whd, x, y in parts: + w, h, _ = whd xt1, yt1 = tr.transform((x, y)) yt1 -= d @@ -164,7 +164,6 @@ def __init__(self, if linespacing is None: linespacing = 1.2 # Maybe use rcParam later. self._linespacing = linespacing - self.legend = None self.set_rotation_mode(rotation_mode) self.update(kwargs) @@ -270,7 +269,7 @@ def update_from(self, other): self._linespacing = other._linespacing self.stale = True - def _get_layout(self, renderer): + def _get_layout(self, renderer, firstline_indent=0): """ return the extent (bbox) of the text together with multiple-alignment information. Note that it returns an extent @@ -282,10 +281,10 @@ def _get_layout(self, renderer): thisx, thisy = 0.0, 0.0 lines = self.get_text().split("\n") # Ensures lines is not empty. - legend_offset = (0, 0) ws = [] hs = [] + ds = [] xs = [] ys = [] @@ -310,6 +309,7 @@ def _get_layout(self, renderer): d = max(d, lp_d) hs.append(h) + ds.append(d) # Metrics of the last line that are needed later: baseline = (h - d) - thisy @@ -318,33 +318,12 @@ def _get_layout(self, renderer): # position at baseline thisy = -(h - d) # reserve some space for the legend symbol - if self.legend is not None: - legend_extent = self.legend.box.get_extent(renderer) - legend_width, legend_height, _, _ = legend_extent - padding = self.legend.handletextpad * self.get_size() - rotation = self.get_rotation() - if rotation == 0: - legend_spacing = legend_width + padding - w += legend_spacing - thisx += legend_spacing - # position relative to the beginning of first line - legend_offset = ( - -legend_spacing, - (h-d - legend_height) / 2 - ) - elif rotation == 90: - legend_spacing = legend_height + padding - w += legend_spacing - thisx += legend_spacing - # position relative to the beginning of first line - legend_offset = ( - -(h-d + legend_width) / 2, - -legend_spacing - ) + w += firstline_indent + thisx = firstline_indent else: # put baseline a good distance from bottom of previous line thisy -= max(min_dy, (h - d) * self._linespacing) - thisx = 0 + thisx = 0.0 ws.append(w) xs.append(thisx) @@ -450,9 +429,8 @@ def _get_layout(self, renderer): xys = M.transform(offset_layout) - (offsetx, offsety) ret = (bbox, - list(zip(lines, zip(ws, hs), *xys.T)), - descent, - xys[0, :] + legend_offset) + list(zip(lines, zip(ws, hs, ds), *xys.T)), + descent) self._cached[key] = ret return ret @@ -712,7 +690,7 @@ def draw(self, renderer): renderer.open_group('text', self.get_gid()) with _wrap_text(self) as textobj: - bbox, info, descent, legend_pos = textobj._get_layout(renderer) + bbox, info, descent = textobj._get_layout(renderer) trans = textobj.get_transform() # don't use textobj.get_position here, which refers to text @@ -737,7 +715,7 @@ def draw(self, renderer): angle = textobj.get_rotation() - for line, wh, x, y in info: + for line, whd, x, y in info: mtext = textobj if len(info) == 1 else None x = x + posx @@ -761,41 +739,11 @@ def draw(self, renderer): textrenderer.draw_text(gc, x, y, clean_line, textobj._fontproperties, angle, ismath=ismath, mtext=mtext) - if self.legend is not None and angle in [0, 90]: - x, y = legend_pos - self.legend.box.set_offset((x + posx, y + posy)) - self.legend.box.draw(renderer) gc.restore() renderer.close_group('text') self.stale = False - def set_legend_handle(self, handle=None, **kwargs): - """ - Set a legend to be shown next to the text. - - Parameters - ---------- - handle: `.Artist` - An artist (e.g. lines, patches) to be shown as legend. - - **kwargs: `.BasicLegend` properties - Additional properties to control legend appearance. - """ - # import AxisLabelLegend here to avoid circular import - from matplotlib.legend import AxisLabelLegend - if handle is not None: - rotation = self.get_rotation() - if rotation not in [0, 90]: - cbook._warn_external("Legend symbols are only supported " - "for non-rotated texts and texts rotated by 90°.") - return - legend = AxisLabelLegend(self, **kwargs) - self.legend = legend.init_legend(handle, rotation == 90) - else: - self.legend = None - self.stale = True - def get_color(self): "Return the color of the text" return self._color @@ -962,7 +910,7 @@ def get_window_extent(self, renderer=None, dpi=None): if self._renderer is None: raise RuntimeError('Cannot get window extent w/o renderer') - bbox, info, descent, _ = self._get_layout(self._renderer) + bbox, info, descent = self._get_layout(self._renderer) x, y = self.get_unitless_position() x, y = self.get_transform().transform((x, y)) bbox = bbox.translated(x, y) From dded358b89e04b519f741cd9f59378f97df71944 Mon Sep 17 00:00:00 2001 From: Alexander Hartl Date: Thu, 23 Jan 2020 15:23:29 +0100 Subject: [PATCH 9/9] Fixed failing test --- lib/matplotlib/legend.py | 3 ++- tutorials/intermediate/legend_guide.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 0285e6d0dc3d..e662ed052887 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -448,7 +448,8 @@ def __init__(self, *args, **kwargs): def _get_layout_with_legend(self, renderer): if self.legend is None: - return *Text._get_layout(self, renderer, 0), 0 + bbox, info, descent = Text._get_layout(self, renderer, 0) + return bbox, info, descent, 0 legend_extent = self.legend.get_extent(renderer) legend_width, legend_height, _, _ = legend_extent padding = self.legend_config.handletextpad * self.get_size() diff --git a/tutorials/intermediate/legend_guide.py b/tutorials/intermediate/legend_guide.py index b6e44f7f6a5a..1f46ef7a5886 100644 --- a/tutorials/intermediate/legend_guide.py +++ b/tutorials/intermediate/legend_guide.py @@ -165,7 +165,7 @@ # appropriate :class:`~matplotlib.legend_handler.HandlerBase` subclass. # The choice of handler subclass is determined by the following rules: # -# 1. Update :func:`~matplotlib.legend.BasicLegend.get_legend_handler_map` +# 1. Update :func:`~matplotlib.legend.LegendConfig.get_legend_handler_map` # with the value in the ``handler_map`` keyword. # 2. Check if the ``handle`` is in the newly created ``handler_map``. # 3. Check if the type of ``handle`` is in the newly created @@ -174,7 +174,7 @@ # created ``handler_map``. # # For completeness, this logic is mostly implemented in -# :func:`~matplotlib.legend.BasicLegend.get_legend_handler`. +# :func:`~matplotlib.legend.LegendConfig.get_legend_handler`. # # All of this flexibility means that we have the necessary hooks to implement # custom handlers for our own type of legend key. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy