0

enter image description here

In search for a solution to vertically stack lines in a matplotlib legend, I came across this stack post Two line styles in legend, but I can't make the code work, I always get an error in line "legline = handler.createartists(...):

"Exception has occurred: AttributeError'NoneType' object has no attribute 'create artists'"

Here I reproduce the code(@gyger) from that stackoverflow question:

import matplotlib.pyplot as plt
from matplotlib.legend_handler import HandlerTuple

class HandlerTupleVertical(HandlerTuple):
    def __init__(self, **kwargs):
        HandlerTuple.__init__(self, **kwargs)

    def create_artists(self, legend, orig_handle,
                       xdescent, ydescent, width, height, fontsize, trans):
        # How many lines are there.
        numlines = len(orig_handle)
        handler_map = legend.get_legend_handler_map()

        # divide the vertical space where the lines will go
        # into equal parts based on the number of lines
        height_y = (height / numlines)

        leglines = []
        for i, handle in enumerate(orig_handle):
            handler = legend.get_legend_handler(handler_map, handle)

            legline = handler.create_artists(legend, handle,
                                             xdescent,
                                             (2*i + 1)*height_y,
                                             width,
                                             2*height,
                                             fontsize, trans)
            leglines.extend(legline)

        return leglines

To use this code:

line1 = plt.plot(xy,xy, c='k', label='solid')
line2 = plt.plot(xy,xy+1, c='k', ls='dashed', label='dashed')
line3 = plt.plot(xy,xy-1, c='k', ls='dashed', label='dashed')

plt.legend([(line1, line2), line3], ['text', 'more text', 'even more'],
           handler_map = {tuple : HandlerTupleVertical()})
MrT77
  • 811
  • 6
  • 25

2 Answers2

1

It appears that handle inside the for loop is a legnth-1 list with a handle inside it, which is confusing the get_legend_handler function, as that is expecting a handle, not a list.

If instead you send just the handler from inside that list to the legend.get_legend_handler() and handler.create_artists(), it seems to work.

perhaps the simplest way to do that is to add a line of code handle = handle[0] just before calling the get_legend_handler function.

for i, handle in enumerate(orig_handle):

    handle = handle[0]
    handler = legend.get_legend_handler(handler_map, handle)

    legline = handler.create_artists(legend, handle,
                                     xdescent,
                                     (2*i + 1)*height_y,
                                     width,
                                     2*height,
                                     fontsize, trans)
    leglines.extend(legline)

enter image description here

tmdavison
  • 64,360
  • 12
  • 187
  • 165
  • This solves a part of the problem thanks. But it doesn't produce the legend as intended: one entry 2-lined for "text", another 2-lined for "more text", and a triple entry for "extra line". – MrT77 Aug 06 '20 at 16:15
  • Already solved thanks to @tmdavison. I left the answer below. – MrT77 Aug 06 '20 at 19:07
0

As @tmdavison pointed out, the problem was that get_legend_handler function was expecting a handle, not a list. Probably the original code I found was from matplotlib version where this behaviour was different. I solved this by making lines be a handle (by adding a comma):

line1, = plt.plot(xy,xy, c='k', label='solid')
line2, = plt.plot(xy,xy+1, c='k', ls='dashed', label='dashed')
line3, = plt.plot(xy,xy-1, c='k', ls='dashed', label='dashed')

plt.legend([(line1, line2), line3], ['text', 'more text', 'even more'],
           handler_map = {tuple : HandlerTupleVertical()})
MrT77
  • 811
  • 6
  • 25