Как заставить ширину поля заголовка охватывать весь участок?

рассмотрим следующие серии панд 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')

enter image description here

Как получить поле, содержащее заголовок, для охвата всего сюжета?

Ответ 1

Конечно, можно получить ограничивающий прямоугольник заголовка, который является элементом Text. Это можно сделать с помощью

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

В принципе, затем можно манипулировать ограничивающим прямоугольником, например, через bb.set_width(...). Однако все настройки теряются, когда matplotlib рисует заголовок на холсте. По крайней мере, так я интерпретирую метод Text draw().

Я не знаю других способов установки ограничительной рамки. Например, ограничивающую рамку 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.

Следующее решение основано на 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()

enter image description here


На случай, если кто-то заинтересован в более легком решении, есть также возможность поэкспериментировать с mutation_aspect ограничивающей рамки заголовка, которая, очевидно, остается неизменной при рисовании заголовка. Хотя сам mutation_aspect основном только изменяет высоту блока, можно использовать очень большой отступ для блока и установить для mutation_aspect очень маленькое число, чтобы в конце блок казался расширенным по ширине. Очевидным недостатком этого решения является то, что значения для отступа и аспекта должны быть найдены методом проб и ошибок и будут меняться для разных размеров шрифта и рисунка. В моем случае значения mutation_aspect = 0.04 и pad=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()

Ответ 2

Вместо масштабирования ограничивающего прямоугольника самого заголовка вы можете создать вторичную ось над основным и использовать ее как "поле" для своего названия. Поскольку оси обычно не выглядят как коробки, мы будем отключать метки осей и тики и установить цвет фона в черный, чтобы он соответствовал OP.

Я использую тот же подход, чтобы сделать вторичную ось совпадения, как здесь.

Кроме того, я использовал AnchoredText чтобы привязать текст заголовка к оси, чтобы он мог быть легко расположен в центре его.

import matplotlib.pyplot as plt 
from matplotlib.offsetbox import AnchoredText
from mpl_toolkits.axes_grid1 import make_axes_locatable
import pandas as pd
import numpy as np

s = pd.Series(np.random.lognormal(.001, .01, 100))
ax = s.cumprod().plot()

divider = make_axes_locatable(ax)
cax = divider.append_axes("top", size="11%", pad=0)
cax.get_xaxis().set_visible(False)
cax.get_yaxis().set_visible(False)
cax.set_facecolor('black')

at = AnchoredText("My Log Normal Example", loc=10,
                  prop=dict(backgroundcolor='black',
                            size=12, color='white'))
cax.add_artist(at)

plt.show()

enter image description here

Изменить: для старых версий matplotlib вам может потребоваться переключиться на cax.set_axis_bgcolor('black') при настройке цвета фона.