Mad Physicist
Mad Physicist

Reputation: 114478

Rotating legend or adding patch to axis label in matplotlib

I have an unusual use-case in matplotlib that I am trying to solve. I have a pair of axes created using twinx(). Each holds exactly one line object, with different styles. Rather than having a conventional legend, I must add a patch that represents each line to the y-label of each axis. Here is what I am trying to achieve:

expected

Ideally, the patch would be above the text, but that is a very minor detail compared to achieving the desired result.

I have attempted two approaches to this problem, both of which ended in failure. Perhaps a matplotlib guru can help:

#1: Using displaced legends

from matplotlib import pyplot as plt
fig = plt.figure()
ax = fig.subplots(111)
ax = fig.add_subplot(111)
ax2 = ax.twinx()
l1, = ax.plot((0, 1), (1, 0), linestyle='-', color='blue')
l2, = ax.plot((0, 1), (0, 1), linestyle='--', color='red')
leg1 = ax.legend([l1], ['Solid Blue Line'], bbox_to_anchor=(-0.102, 0., 0.102, 1.), frameon=False, mode='expand', loc=4)
leg2 = ax2.legend([l2], ['Dashed Red Line'], bbox_to_anchor=(1.102, 0., 0.102, 1.), frameon=False, mode='expand', loc=3)

As expected, this places the legends correctly, but does not rotate them. I am unable to carry out the rotation because apparently legends can not be rotated, despite being Artist extensions. E.g., leg1.set_transform(leg1.get_transform() + Affine2D().rotate_deg(-90)) does nothing to change the angle of the legend:

actual1

#2 Using a custom AnchoredOffsetBox

In this approach, I attempted to construct a custom box containing a text and a patch. Unfortunately, the patch does not show up properly.

from matplotlib import pyplot as plt
from matplotlib.lines import Line2D
from matplotlib.offsetbox import AnchoredOffsetbox, TextArea, DrawingArea, HPacker
from matplotlib.transform import Affine2D

fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot([1, 2, 3], [0, 3, -1], color='g', linestyle='solid')
textArea = TextArea('This is an x-label')
drawArea = DrawingArea(1., 1.)
patch = Line2D((0, 1), (0, 1), color='g', linestyle='solid')
drawArea.add_artist(patch)
content = HPacker(children=[textArea, drawArea], align="center", pad=0, sep=5)
label = AnchoredOffsetbox(loc=10, child=content, pad=0, bbox_to_anchor=(0.5, -0.1), bbox_transform=ax.transAxes, frameon=False)
ax.add_artist(label)
label.set_transform(label.get_transform() + Affine2D().rotate_deg(-90))

Again, the rotation does not work at all. Also, the patch is not scaled reasonably as it would be in a legend entry (notice the tiny green dot at the end of the label):

actual2

Is it possible to achieve something like the result I desire in MatPlotLib? I am willing to delve into the source code and possibly submit a PR if it helps any, but I have no clear idea of where to begin.

UPDATE

Looking at the source code, the set_transform methods of matplotlib.offsetbox.TextArea and matplotlib.offsetbox.DrawingArea are completely ignored. This means that my second attempt would be pointless even if I were to get the patch to draw correctly.

Upvotes: 3

Views: 4462

Answers (1)

Ed Smith
Ed Smith

Reputation: 13216

One solution which avoids the nightmare of trying to line up boxes outside of axes is to use latex labels with textcolor. The following code

import matplotlib
matplotlib.use('ps')
from matplotlib import rc
rc('text',usetex=True)
rc('text.latex', preamble=r'\usepackage{color}')
import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(111)
ax2 = ax.twinx()
l1, = ax.plot((0, 1), (1, 0), linestyle='-', color='blue')
l2, = ax.plot((0, 1), (0, 1), linestyle='--', color='red')

ax.set_ylabel(r'\textcolor{blue}{--}' + r'$\;$ Solid Blue Line')
ax2.set_ylabel(r'\textcolor{red}{- -}' + r'$\;$ Dashed Red Line')

#may need to convert test.ps -trim test.png for picture
plt.savefig('test.ps')

results in the test file which looks like,

enter image description here

Latex lets you use \hline to design lines (or add \n \usepackage{dashrule} to the preamble), \circle etc for markers, colours of your choosing and you can control spaces with \;

UPDATE: few more examples of marker, colour and line styles,

import matplotlib
matplotlib.use('ps')
from matplotlib import rc
rc('text',usetex=True)
latex_pream = matplotlib.rcParams['text.latex.preamble']
latex_pream.append(r'\usepackage{color}')
latex_pream.append(r"\definecolor{lllgrey}{rgb}{0.9,0.9,0.9}")
latex_pream.append(r"\definecolor{lightblue}{rgb}{0.56485968018118948, 0.7663975529283894, 0.86758939636894861}")
latex_pream.append(r"\newcommand*{\xlinethick}[1][1.0em]{\rule[0.4ex]{#1}{1.5pt}}")
latex_pream.append(r"\newcommand*{\xdashthick}[1][1.0em]{\rule[0.5ex]{2.5pt}{1.5pt} $\!$ \rule[0.5ex]{2.5pt}{1.5pt}}")

import matplotlib.pyplot as plt

plt.text(0.1,0.5,"Latex marker and line examples: \n $\;\;\;\;\;$ \{" 
          + r'\textcolor{blue}{$ - - \!\!\!\!\!\!\!\! \bullet \;$} ' + ',$\;\;$'
          + r'\textcolor{red}{$\bullet$} $\!\!\!\!\! \:\! \circ$' + ',$\;\;$'
          + r'\textcolor{lllgrey}{\xlinethick}' + ',$\;\;$'
          + r'\textcolor{lightblue}{\xdashthick}' + ' \}', fontsize=24)

#may need to convert test.ps -trim test.png for picture
plt.savefig('test.ps')

which results in

enter image description here

Upvotes: 3

Related Questions