matplotlib图例中的多个标题

5gfr0r5j  于 2023-03-09  发布在  其他
关注(0)|答案(3)|浏览(235)

在matplotlib中,是否可以在一个图例中放置多个“标题”?我想要的是:

Title 1
x label 1
o label2

Title 2
^ label 3
v label 4

...

以防我有4条曲线或更多。因为如果我使用超过1个图例,很难让它们正确对齐手动设置位置。

a7qyws3x

a7qyws3x1#

最接近我得到它,是创建一个空的代理艺术家。问题是,在我看来,他们没有左对齐,但空间的(空)标记仍然存在。

from matplotlib.patches import Rectangle
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 1, 100)

# the comma is to get just the first element of the list returned
plot1, = plt.plot(x, x**2) 
plot2, = plt.plot(x, x**3)

title_proxy = Rectangle((0,0), 0, 0, color='w')

plt.legend([title_proxy, plot1, title_proxy, plot2], 
           ["$\textbf{title1}$", "label1","$\textbf{title2}$", "label2"])
plt.show()
j2qf4p5b

j2qf4p5b2#

好吧,我需要这个答案,但目前的答案对我不起作用。在我的情况下,我事先不知道我需要图例中标题的“频率”。这取决于一些输入变量,所以我需要比手动设置标题位置更灵活的方法。在访问了这里的几十个问题后,我找到了这个解决方案,它非常适合我,但也许有一个更好的方法。

## this is what changes sometimes for me depending on how the user decided to input
parameters=[2, 5]

## Titles of each section
title_2 = "\n$\\bf{Title \, parameter \, 2}$"
title_4 = "\n$\\bf{Title \, parameter \, 4}$"
title_5 = "\n$\\bf{Title \, parameter \, 5}$"


def reorderLegend(ax=None, order=None):
    handles, labels = ax.get_legend_handles_labels()
    info = dict(zip(labels, handles))

    new_handles = [info[l] for l in order]
    return new_handles, order

#########
### Plots
fig, ax = plt.subplots(figsize=(10, 10))
ax.set_axis_off()

## Order of labels
all_labels=[]
if 2 in parameters:
    ax.add_line(Line2D([], [], color="none", label=title_2)) 
    all_labels.append(title_2)
    #### Plot your stuff below header 2
    #### Append corresponding label to all_labels


if 4 in parameters:
    ax.add_line(Line2D([], [], color="none", label=title_4))
    all_labels.append(title_4)
    #### Plot your stuff below header 4
    #### Append corresponding label to all_labels

if 5 in parameters:
    ax.add_line(Line2D([], [], color="none", label=title_5))
    all_labels.append(title_5)
    #### Plot your stuff below header 5
    #### Append corresponding label to all_labels

## Make Legend in correct order
handles, labels = reorderLegend(ax=ax, order=all_labels)
leg = ax.legend(handles=handles, labels=labels, fontsize=12, loc='upper left', bbox_to_anchor=(1.05, 1), ncol=1, fancybox=True, framealpha=1, frameon=False)

## Move titles to the left 
for item, label in zip(leg.legendHandles, leg.texts):
    if label._text  in [title_2, title_4, title_5]:
        width=item.get_window_extent(fig.canvas.get_renderer()).width
        label.set_ha('left')
        label.set_position((-2*width,0))

作为一个例子,我得到了下面的图例(裁剪了图像的其余部分)。

k7fdbhmy

k7fdbhmy3#

Matplotlib只支持一个图例标题,但我有时候也需要多个标题,将标签与图例框的左边缘对齐会使它们与其他带句柄的标签相比看起来更像标题。

使用Text.set_position()移动标签

这和@M.O.的答案很相似。棘手的是Text.set_position()使用显示坐标,所以我们需要计算出标签偏移了多少像素,不能像plt.legend()那样只使用字体大小单位。通过阅读matplotlib.legend.Legend._init_legend_box(),我们可以了解其中涉及的类和参数。Matplotlib创建了一个HPacker和VPacker对象树:

VPacker
    Text (title, invisible if unused)
    HPacker (columns)
        VPacker (column)
            HPacker (row of (handle, label) pairs)
                DrawingArea
                    Artist (handle)
                TextArea
                    Text (label)
            ...
        ...

图例kwargs/rcParams像handletextpadhandlelengthcolumnspacing等,被赋予HPacker,VPacker和DrawingArea对象来控制间距。我们感兴趣的间距参数是“字体大小单位”,所以如果handlelength=2,那么句柄将是2 * fontsize点宽。“点”是一个古老的印刷单位,等于1/72英寸。通过查看图中的DPI,我们可以将点转换为像素,但有些matplotlib后端(如SVG)不使用像素,因此我们希望使用Renderer.points_to_pixels(),而不是自己进行计算。
回到_init_legend_box(),看起来标签被移动了handlelength + handletextpad,但是如果我们深入研究HPacker,我们会发现它无条件地在每个子节点周围添加一个像素的填充,所以我们需要再添加2个像素:手柄的每侧一个。
最后,我们需要某种方法将图例条目标记为标题,在句柄上设置visible=False似乎不错,因为句柄必须是一个Artist(或子类)示例,并且每个Artist都有visible属性。
代码:

import matplotlib as mpl

def style_legend_titles_by_setting_position(leg: mpl.legend.Legend, bold: bool = False) -> None:
    """ Style legend "titles"

    A legend entry can be marked as a title by setting visible=False. Titles
    get left-aligned and optionally bolded.
    """
    # matplotlib.offsetbox.HPacker unconditionally adds a pixel of padding
    # around each child.
    hpacker_padding = 2

    for handle, label in zip(leg.legendHandles, leg.texts):
        if not handle.get_visible():
            # See matplotlib.legend.Legend._init_legend_box()
            widths = [leg.handlelength, leg.handletextpad]
            offset_points = sum(leg._fontsize * w for w in widths)
            offset_pixels = leg.figure.canvas.get_renderer().points_to_pixels(offset_points) + hpacker_padding
            label.set_position((-offset_pixels, 0))
            if bold:
                label.set_fontweight('bold')

在使用中:

import matplotlib as mpl
from matplotlib.patches import Patch
import matplotlib.pyplot as plt

def make_legend_with_subtitles() -> mpl.legend.Legend:
    legend_contents = [
        (Patch(visible=False), 'Colors'),
        (Patch(color='red'), 'red'),
        (Patch(color='blue'), 'blue'),

        (Patch(visible=False), ''),  # spacer

        (Patch(visible=False), 'Marks'),
        (plt.Line2D([], [], linestyle='', marker='.'), 'circle'),
        (plt.Line2D([], [], linestyle='', marker='*'), 'star'),
    ]
    fig = plt.figure(figsize=(2, 2))
    leg = fig.legend(*zip(*legend_contents))
    return leg

leg = make_legend_with_subtitles()
style_legend_titles_by_setting_position(leg)
leg.figure.savefig('set_position.png')

修改图例内容并删除不可见控点

另一种方法是将任何具有不可见句柄的图例条目HPacker替换为以下标签:

def style_legend_titles_by_removing_handles(leg: mpl.legend.Legend) -> None:
    for col in leg._legend_handle_box.get_children():
        row = col.get_children()
        new_children: list[plt.Artist] = []
        for hpacker in row:
            if not isinstance(hpacker, mpl.offsetbox.HPacker):
                new_children.append(hpacker)
                continue
            drawing_area, text_area = hpacker.get_children()
            handle_artists = drawing_area.get_children()
            if not all(a.get_visible() for a in handle_artists):
                new_children.append(text_area)
            else:
                new_children.append(hpacker)
        col._children = new_children

leg = make_legend_with_subtitles()
style_legend_titles_by_removing_handles(leg)
leg.figure.savefig('remove_handles.png')

修改图例内容感觉很脆弱,Seaborn有一个函数adjust_legend_subtitles(),它将DrawingArea宽度设置为0,如果你也设置handletextpad=0,标签几乎是左对齐的,除了在DrawingArea周围仍然有HPacker填充,使标签右对齐2个像素。

创建多个图例并将内容连接在一起

Seaborn的最新方法是创建多个图例对象,每个对象使用title参数,将内容组合成一个主图例,然后只将该主图例注册到图形中。我喜欢这种方法,因为它可以让matplotlib控制标题的样式,而且您可以为其指定一个比添加不可见句柄更干净的接口。但我觉得要适应非海运环境比使用其他方法需要更多的工作。

相关问题