consider the following pandas series
s and plot
import pandas as pd import numpy as np s = pd.Series(np.random.lognormal(.001, .01, 100)) ax = s.cumprod().plot() ax.set_title('My Log Normal Example', position=(.5, 1.02), backgroundcolor='black', color='white')
How do I get the box that contains the title to span the entire plot?
It is of course possible to get the bounding box of the title, which is a
Text element. This can be done with
title = ax.set_title(...) bb = title.get_bbox_patch()
In principle, one can then manipulate the bounding box, e.g. via
bb.set_width(...). However all settings are lost, once matplotlib draws the title to the canvas. At least this is how I interprete the
I'm not aware of other methods of setting the bounding box. For example a
legend's bounding box can be set via
plt.legend(bbox_to_anchor=(0., 1.02, 1., .102), loc=3, mode="expand") such that it expands over the full axes range (see here). It would be very useful to have the same option for
Text as well. But as for now, we don't.
Text object allows to set a
bbox argument which is normally meant for setting the style of the bounding box. There is no way to set the bounding box extents, but it accepts some dictionary of properties of the surrounding box. And one of the accepted properties is a
boxstyle. Per default this is a
square, but can be set to a circle or arrow or other strange shapes.
boxstyles are actually the key to a possible solution. They all inherit from
BoxStyle._Base and - as can be seen at the bottom of the annotations guide - one can define a custom shape, subclassing
The following solution is based on subclassing
BoxStyle._Base in a way that it accepts the width of the axes as argument and draws the title's rectangle path such that it has exactly this width.
As a bonus we can register an event handler such that this width, once it changes due to resizing of the window, is adapted.
Here is the code:
import matplotlib.pyplot as plt import pandas as pd import numpy as np from matplotlib.path import Path from matplotlib.patches import BoxStyle class ExtendedTextBox(BoxStyle._Base): """ An Extended Text Box that expands to the axes limits if set in the middle of the axes """ def __init__(self, pad=0.3, width=500.): """ width: width of the textbox. Use `ax.get_window_extent().width` to get the width of the axes. pad: amount of padding (in vertical direction only) """ self.width=width self.pad = pad super(ExtendedTextBox, self).__init__() def transmute(self, x0, y0, width, height, mutation_size): """ x0 and y0 are the lower left corner of original text box They are set automatically by matplotlib """ # padding pad = mutation_size * self.pad # we add the padding only to the box height height = height + 2.*pad # boundary of the padded box y0 = y0 - pad y1 = y0 + height _x0 = x0 x0 = _x0 +width /2. - self.width/2. x1 = _x0 +width /2. + self.width/2. cp = [(x0, y0), (x1, y0), (x1, y1), (x0, y1), (x0, y0)] com = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY] path = Path(cp, com) return path dpi = 80 # register the custom style BoxStyle._style_list["ext"] = ExtendedTextBox plt.figure(dpi=dpi) s = pd.Series(np.random.lognormal(.001, .01, 100)) ax = s.cumprod().plot() # set the title position to the horizontal center (0.5) of the axes title = ax.set_title('My Log Normal Example', position=(.5, 1.02), backgroundcolor='black', color='white') # set the box style of the title text box toour custom box bb = title.get_bbox_patch() # use the axes' width as width of the text box bb.set_boxstyle("ext", pad=0.4, width=ax.get_window_extent().width ) # Optionally: use eventhandler to resize the title box, in case the window is resized def on_resize(event): print "resize" bb.set_boxstyle("ext", pad=0.4, width=ax.get_window_extent().width ) cid = plt.gcf().canvas.mpl_connect('resize_event', on_resize) # use the same dpi for saving to file as for plotting on screen plt.savefig(__file__+".png", dpi=dpi) plt.show()
Just in case someone is interested in a lighter solution, there is also the option to play around with the
mutation_aspect of the title's bounding box, which is apparently left unchanged when drawing the title. While the
mutation_aspect itself basically only changes the height of the box, one can use an extremely large padding for the box and set
mutation_aspect to a very small number such that at the end the box appears extended in width. The clear drawback of this solution is, that the values for the padding and aspect have to be found by trial and error and will change for different font and figure sizes. In my case the values of
mutation_aspect = 0.04 and
pad=11.9 produce the desired result, but on other systems they may of course be different.
import matplotlib.pyplot as plt import pandas as pd import numpy as np s = pd.Series(np.random.lognormal(.001, .01, 100)) ax = s.cumprod().plot() title = ax.set_title('My Log Normal Example', position=(.5, 1.02), backgroundcolor='black', color='white', verticalalignment="bottom", horizontalalignment="center") title._bbox_patch._mutation_aspect = 0.04 title.get_bbox_patch().set_boxstyle("square", pad=11.9) plt.tight_layout() plt.savefig(__file__+".png") plt.show()