5

I'd like to generate a custom matplotlib legend which, for each entry has two lines for each label as shown in this example:

enter image description here

From some research, it seems possible to simply provide two handles' to thefig.legend(handles, labels)` method, (see this post as an example). However, as shown by the following example code, this simply overlays the lines on top of each other.

import matplotlib.lines as mlines
import matplotlib.pyplot as plt

blue_line = mlines.Line2D([], [], color='r')
green_line = mlines.Line2D([], [], linestyle='--', color='k')
fig, ax = plt.subplots(figsize=(5, 5))
handles = [(blue_line,green_line)]
labels = ['test'] 
fig.legend(handles=handles, labels=labels, fontsize=20)  

So, I think I either need to transform one of the Line2D objects, or generate a new Patch object which contains two lines. However, I can't work out how to do this - is there a simple way to combine two patches, or have I missed a trick in combining the handles?

Context

In case this helps others, the context is that I'm using the technique discussed here, that is a twin axis which is coloured to show two different plots simultaneously. However, the two lines have the same label, hence why I wanted to combined them together.

Community
  • 1
  • 1
Greg
  • 11,654
  • 3
  • 44
  • 50
  • Note that there's an example on the `matplotlib` gallery page that does just this http://matplotlib.org/examples/pylab_examples/legend_demo5.html – tmdavison Jan 25 '17 at 14:22

2 Answers2

14

In the matplotlib legend guide there is a chapter about custom legend handlers. You could adapt it to your needs, e.g. like this:

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.legend_handler import HandlerBase


class AnyObjectHandler(HandlerBase):
    def create_artists(self, legend, orig_handle,
                       x0, y0, width, height, fontsize, trans):
        l1 = plt.Line2D([x0,y0+width], [0.7*height,0.7*height], 
                                                linestyle='--', color='k')
        l2 = plt.Line2D([x0,y0+width], [0.3*height,0.3*height], color='r')
        return [l1, l2]


x = np.linspace(0, 3)
fig, axL = plt.subplots(figsize=(4,3))
axR = axL.twinx()

axL.plot(x, np.sin(x), color='k', linestyle='--')
axR.plot(x, 100*np.cos(x), color='r')

axL.set_ylabel('sin(x)', color='k')
axR.set_ylabel('100 cos(x)', color='r')
axR.tick_params('y', colors='r')

plt.legend([object], ['label'],
           handler_map={object: AnyObjectHandler()})

plt.show()

enter image description here

In order to have multiple such entries, one can supply some tuple of parameters that the Handler then uses to draw the legend.

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.legend_handler import HandlerBase


class AnyObjectHandler(HandlerBase):
    def create_artists(self, legend, orig_handle,
                       x0, y0, width, height, fontsize, trans):
        l1 = plt.Line2D([x0,y0+width], [0.7*height,0.7*height],
                           linestyle=orig_handle[1], color='k')
        l2 = plt.Line2D([x0,y0+width], [0.3*height,0.3*height], 
                           color=orig_handle[0])
        return [l1, l2]


x = np.linspace(0, 3)
fig, axL = plt.subplots(figsize=(4,3))
axR = axL.twinx()

axL.plot(x, np.sin(x), color='k', linestyle='--')
axR.plot(x, 100*np.cos(x), color='r')

axL.plot(x, .3*np.sin(x), color='k', linestyle=':')
axR.plot(x, 20*np.cos(x), color='limegreen')

axL.set_ylabel('sin(x)', color='k')
axR.set_ylabel('100 cos(x)', color='r')
axR.tick_params('y', colors='r')

plt.legend([("r","--"), ("limegreen",":")], ['label', "label2"],
           handler_map={tuple: AnyObjectHandler()})

plt.show()

enter image description here

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • Nice, thanks for this. It should be noted than in order to have several different 'sets' of lines once must create different `AnyObject` classes which can be used as the keys. – Greg Jan 20 '17 at 16:52
  • I updated the solution for the case where you want to have several such entries. – ImportanceOfBeingErnest Oct 31 '17 at 22:34
1

Here is an inadequate solution I have come to after playing around. I post it in case it helps others, but would prefer to do it properly. It uses two columns and the label of the first as a '/' or one could equally use "and".

import matplotlib.pyplot as plt

x = np.linspace(0, 3)
fig, axL = plt.subplots()
axR = axL.twinx()

axL.plot(x, np.sin(x), color='k', label='/')
axR.plot(x, 100*np.cos(x), color='r', label='label')

axL.set_ylabel('sin(x)', color='k')
axR.set_ylabel('100 cos(x)', color='r')
axR.tick_params('y', colors='r')

handlesL, labelsL = axL.get_legend_handles_labels()
handlesR, labelsR = axR.get_legend_handles_labels()
handles = handlesL + handlesR
labels = labelsL + labelsR
axR.legend(handles, labels, loc='lower center', ncol=2, fontsize=16,
           handletextpad=0.4, columnspacing=0.4)

enter image description here

Greg
  • 11,654
  • 3
  • 44
  • 50