如何使标题框的宽度跨整个图? [英] How do I make the width of the title box span the entire plot?

查看:74
本文介绍了如何使标题框的宽度跨整个图?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

考虑以下熊猫系列s并作图

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')

我如何获得包含标题的框,以跨越整个情节?

解决方案

当然,有可能获得标题的边框,即Text元素.可以使用

title = ax.set_title(...) 
bb = title.get_bbox_patch() 

原则上,然后可以操纵边界框,例如通过 bb.set_width(...).但是,一旦matplotlib将标题绘制到画布上,所有设置都会丢失.至少这就是我解释Textdraw()方法的方式.

我不知道其他设置边界框的方法.例如,可以通过
设置legend的边界框 plt.legend(bbox_to_anchor=(0., 1.02, 1., .102), loc=3, mode="expand"),使其在整个轴范围内扩展(请参见此处) .对Text也具有相同的选项将非常有用.但是就目前而言,我们还没有.

Text对象允许设置 bbox 参数,通常用于设置边界框的样式.没有设置边界框范围的方法,但是它接受了周围框的一些属性字典.可接受的属性之一是 boxstyle .默认情况下,这是一个square,但可以设置为圆形,箭头或其他奇怪的形状.

实际上,这些boxstyle是解决方案的关键.它们都继承自BoxStyle._Base并且-如注释指南-一个可以定义自定义形状的子类,它是BoxStyle._Base的子类.

以下解决方案基于子类BoxStyle._Base,其方式是接受轴的宽度作为参数,并绘制标题的矩形路径,以使其恰好具有此宽度.

作为奖励,我们可以注册一个事件处理程序,以便该宽度(由于窗口大小调整而改变)会得到调整.

这是代码:

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()


即使有人对更简单的解决方案感兴趣,也可以选择使用标题边框的mutation_aspect,在绘制标题时,该边框显然保持不变.虽然mutation_aspect本身基本上只改变盒子的高度,但是可以为盒子使用极大的填充,并将mutation_aspect设置为非常小的数字,以使盒子的宽度最终扩大.此解决方案的明显缺点是,必须通过反复试验来找到填充值和长宽比的值,并且对于不同的字体和图形大小,它们的值会有所变化. 在我的情况下,mutation_aspect = 0.04pad=11.9的值会产生所需的结果,但是在其他系统上,它们的值当然可能会有所不同.

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()

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 interpret the Text's draw() method.

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.

The Text object allows setting 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.

Those 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 BoxStyle._Base.

The following solution is based on subclassing BoxStyle._Base in a way that it accepts the width of the axes as an 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 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()

这篇关于如何使标题框的宽度跨整个图?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆