matplotlib: как автоматически масштабировать размер шрифта, чтобы текст соответствовал некоторой ограничивающей рамке

#matplotlib

#matplotlib

Вопрос:

Проблема

Мое приложение matplotlib генерирует определяемые пользователем динамические изображения, и поэтому такие вещи, как текст заголовка страницы, могут быть разной длины. Я хочу иметь возможность указать ограничивающую рамку для matplotlib, а затем автоматически масштабировать размер шрифта, чтобы текст помещался в эту ограничивающую рамку. Мое приложение использует только серверную часть AGG.

Мое решение для взлома

Я наименее острый инструмент в наборе инструментов, но вот что я придумал для решения этой проблемы. Я перебираю начальный размер шрифта 50 , а затем перебираю вниз, пока не подумаю, что смогу поместить текст в поле.

 def fitbox(fig, text, x0, x1, y0, y1, **kwargs):
    """Fit text into a NDC box."""
    figbox = fig.get_window_extent().transformed(
        fig.dpi_scale_trans.inverted())
    # need some slop for decimal comparison below
    px0 = x0 * fig.dpi * figbox.width - 0.15
    px1 = x1 * fig.dpi * figbox.width   0.15
    py0 = y0 * fig.dpi * figbox.height - 0.15
    py1 = y1 * fig.dpi * figbox.height   0.15
    # print("px0: %s px1: %s py0: %s py1: %s" % (px0, px1, py0, py1))
    xanchor = x0
    if kwargs.get('ha', '') == 'center':
        xanchor = x0   (x1 - x0) / 2.
    yanchor = y0
    if kwargs.get('va', '') == 'center':
        yanchor = y0   (y1 - y0) / 2.
    txt = fig.text(
        xanchor, yanchor, text,
        fontsize=50, ha=kwargs.get('ha', 'left'),
        va=kwargs.get('va', 'bottom'),
        color=kwargs.get('color', 'k')
    )
    for fs in range(50, 1, -2):
        txt.set_fontsize(fs)
        tbox = txt.get_window_extent(fig.canvas.get_renderer())
        # print("fs: %s tbox: %s" % (fs, str(tbox)))
        if (tbox.x0 >= px0 and tbox.x1 < px1 and tbox.y0 >= py0 and
                tbox.y1 <= py1):
            break
    return txt
 

Итак, я могу вызвать эту функцию следующим образом

 fitbox(fig, "Hello there, this is my title!", 0.1, 0.99, 0.95, 0.99)
 

Вопрос / Запрос обратной связи

  1. Предлагает ли matplotlib лучшее встроенное решение для этой проблемы?
  2. Какие-либо существенные недостатки этого подхода? Производительность не ощущается как прерыватель игры. Вероятно, я должен сделать так, чтобы эта функция позволяла указывать координаты в пределах одной axes , а не общей цифры. Возможно, это уже работает 🙂

Кроме того, мне нравится, как некоторые другие приложения для построения графиков позволяют указывать размер шрифта в безразмерных координатах отображения. Например, PyNGL. Таким образом, вы можете установить его fontsize=0.04 , например.

Спасибо.

Комментарии:

1. Какой объект находится fig в вашем коде? В любом случае решение не кажется слишком плохим. Подход такой же , как и для ячеек таблицы. Что, в свою очередь, означает, что вы можете использовать таблицу для достижения того же эффекта, но только если fig используемый вами объект определен в тех же координатах, что и таблица (?).

2. рис plt.gcf() . Спасибо за ваш отзыв. Я рад видеть тот же код в таблице, интересно.

3. Привет, вы нашли какой-нибудь более эффективный способ автоматической подгонки текста в поле? У меня есть аналогичная реализация, но она неэффективна, особенно когда используется для добавления метки в древовидную карту. Моя реализация находится в ответе.

4. Спасибо, что поделились @Z-Y.L, я не нашел ничего более эффективного: (

Ответ №1:

Моя реализация автоматической подгонки текста в поле:

 def text_with_autofit(self, txt, xy, width, height, *, 
                      transform=None, 
                      ha='center', va='center',
                      min_size=1, adjust=0,
                      **kwargs):
    if transform is None:
        if isinstance(self, Axes):
            transform = self.transData
        if isinstance(self, Figure):
            transform = self.transFigure
        
        
    x_data = {'center': (xy[0] - width/2, xy[0]   width/2), 
            'left': (xy[0], xy[0]   width),
            'right': (xy[0] - width, xy[0])}
    y_data = {'center': (xy[1] - height/2, xy[1]   height/2),
            'bottom': (xy[1], xy[1]   height),
            'top': (xy[1] - height, xy[1])}
    
    (x0, y0) = transform.transform((x_data[ha][0], y_data[va][0]))
    (x1, y1) = transform.transform((x_data[ha][1], y_data[va][1]))
    # rectange region size to constrain the text
    rect_width = x1 - x0
    rect_height = y1- y0
    
    fig = self.get_figure() if isinstance(self, Axes) else self
    dpi = fig.dpi
    rect_height_inch = rect_height / dpi
    fontsize = rect_height_inch * 72
    
    if isinstance(self, Figure):
        text = self.text(*xy, txt, ha=ha, va=va, transform=transform, 
                         **kwargs)
    if isinstance(self, Axes):
        text = self.annotate(txt, xy, ha=ha, va=va, xycoords=transform,
                             **kwargs)
    
    while fontsize > min_size:
        text.set_fontsize(fontsize)
        bbox = text.get_window_extent(fig.canvas.get_renderer())
        if bbox.width < rect_width:
            break;
        fontsize -= 1
    if fig.get_constrained_layout():
        text.set_fontsize(fontsize   adjust   0.5)
    if fig.get_tight_layout():
        text.set_fontsize(fontsize   adjust)
        
    return text