Reputation: 114478
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:
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:
#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):
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
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,
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
Upvotes: 3