A causal model diagram

用于绘制图表的 Matplotlib#

这是我为 Matplotlib 博客撰写的第一篇文章,因此我想以我最喜欢的 Matplotlib 特性之一作为开头:Matplotlib 提供了极大的控制力。我喜欢将其用作一种可编程的绘图工具,它碰巧擅长绘制数据。

Matplotlib 的默认布局对于很多事情都非常适用,但有时您希望能够进行更多控制。有时您希望将图表窗口视为一个空白画布,并创建图表来传达您的想法。在这里,我们将逐步介绍设置此功能的过程。大多数技巧都详细记录在这个绘制图表的速查表中。

import matplotlib.pyplot as plt
import numpy as np

第一步是选择画布的大小。

(先说一下,我非常喜欢画布这个比喻,所以这里我将使用这个术语。在 Matplotlib 代码库中,Canvas 对象是一个非常具体的东西。这里我指的是另一个概念。)

我计划绘制一个宽度为 16 厘米,高度为 9 厘米的图表。这样可以舒适地放在一张 A4 或美国标准信纸上,并且宽度几乎是高度的两倍。它还可以很好地缩放以适应宽幅幻灯片演示文稿。

plt.figure() 函数接受一个 figsize 参数,这是一个表示 英寸(宽度, 高度) 元组。要从厘米转换为英寸,我们将除以 2.54。

fig_width = 16  # cm
fig_height = 9  # cm
fig = plt.figure(figsize=(fig_width / 2.54, fig_height / 2.54))

下一步是添加一个可以进行绘制的 Axes 对象。默认情况下,Matplotlib 会调整 Axes 的大小和位置,以便留出一些边框以及 x 轴和 y 轴标签的空间。但是,这次我们不需要这样。我们希望我们的 Axes 扩展到图形的边缘。

add_axes() 函数允许我们精确指定放置新 Axes 的位置以及其大小。它接受一个格式为 (左, 下, 宽, 高) 的元组。图形的坐标系始终是左下角为 (0, 0),右上角为 (1, 1),无论您使用的是什么尺寸的图形。位置、宽度和高度都成为图形总宽度和高度的一部分。

要完全用我们的 Axes 填充图形,我们指定左位置为 0,下位置为 0,宽度为 1,高度为 1。

ax = fig.add_axes((0, 0, 1, 1))

为了使我们的图表创建更容易,我们可以设置轴限,以便图形中的一个单位等于一厘米。这为我们提供了一种直观的方式来控制图表中对象的大小。半径为 2 的圆将在最终图像中绘制为圆形(而不是椭圆形),并且半径为 2 厘米。

ax.set_xlim(0, fig_width)
ax.set_ylim(0, fig_height)

我们也可以使用这两行代码去除自动生成的刻度和刻度标签。

ax.tick_params(bottom=False, top=False, left=False, right=False)
ax.tick_params(labelbottom=False, labeltop=False, labelleft=False, labelright=False)

此时,我们拥有了一个完全正确的大小和形状的空白区域。现在我们可以开始构建我们的图表了。图像的基础将是背景颜色。白色是可以的,但有时混合使用其他颜色会很有趣。这里有一些想法可以帮助您入门。

ax.set_facecolor("antiquewhite")

我们还可以向图表添加边框,以在视觉上将其与其他内容区分开来。

ax.spines["top"].set_color("midnightblue")
ax.spines["bottom"].set_color("midnightblue")
ax.spines["left"].set_color("midnightblue")
ax.spines["right"].set_color("midnightblue")
ax.spines["top"].set_linewidth(4)
ax.spines["bottom"].set_linewidth(4)
ax.spines["left"].set_linewidth(4)
ax.spines["right"].set_linewidth(4)

现在,我们已经建立了基础和背景,并且终于可以开始绘制了。您可以自由地绘制曲线和形状放置点,以及添加各种文本,这些都在我们 16 x 9 的“花园围墙”内。

然后,当您完成时,最后一步是将图形保存为 .png 文件。以这种格式,它可以导入到您正在处理的任何文档或演示文稿中,并添加到其中。

fig.savefig("blank_diagram.png", dpi=300)

Blank diagram example.

如果您正在创建一系列图表,您可以为您的空白画布创建一个方便的模板。

def blank_diagram(
    fig_width=16, fig_height=9, bg_color="antiquewhite", color="midnightblue"
):
    fig = plt.figure(figsize=(fig_width / 2.54, fig_height / 2.54))
    ax = fig.add_axes((0, 0, 1, 1))
    ax.set_xlim(0, fig_width)
    ax.set_ylim(0, fig_height)
    ax.set_facecolor(bg_color)

    ax.tick_params(bottom=False, top=False, left=False, right=False)
    ax.tick_params(labelbottom=False, labeltop=False, labelleft=False, labelright=False)

    ax.spines["top"].set_color(color)
    ax.spines["bottom"].set_color(color)
    ax.spines["left"].set_color(color)
    ax.spines["right"].set_color(color)
    ax.spines["top"].set_linewidth(4)
    ax.spines["bottom"].set_linewidth(4)
    ax.spines["left"].set_linewidth(4)
    ax.spines["right"].set_linewidth(4)

    return fig, ax

然后,您可以使用该画布添加任意文本、形状和线条。

fig, ax = blank_diagram()

for x0 in np.arange(-3, 16, 0.5):
    ax.plot([x0, x0 + 3], [0, 9], color="black")

fig.savefig("stripes.png", dpi=300)

White rectangle with black stripes.

或者更复杂一些

fig, ax = blank_diagram()

centers = [(3.5, 6.5), (8, 6.5), (12.5, 6.5), (8, 2.5)]
radii = 1.5
texts = [
    "\n".join(["My roommate", "is a Philistine", "and a boor"]),
    "\n".join(["My roommate", "ate the last", "of the", "cold cereal"]),
    "\n".join(["I am really", "really hungy"]),
    "\n".join(["I'm annoyed", "at my roommate"]),
]

# Draw circles with text in the center
for i, center in enumerate(centers):
    x, y = center
    theta = np.linspace(0, 2 * np.pi, 100)
    ax.plot(
        x + radii * np.cos(theta),
        y + radii * np.sin(theta),
        color="midnightblue",
    )
    ax.text(
        x,
        y,
        texts[i],
        horizontalalignment="center",
        verticalalignment="center",
        color="midnightblue",
    )

# Draw arrows connecting them
# https://e2eml.school/matplotlib_text.html#annotate
ax.annotate(
    "",
    (centers[1][0] - radii, centers[1][1]),
    (centers[0][0] + radii, centers[0][1]),
    arrowprops=dict(arrowstyle="-|>"),
)
ax.annotate(
    "",
    (centers[2][0] - radii, centers[2][1]),
    (centers[1][0] + radii, centers[1][1]),
    arrowprops=dict(arrowstyle="-|>"),
)
ax.annotate(
    "",
    (centers[3][0] - 0.7 * radii, centers[3][1] + 0.7 * radii),
    (centers[0][0] + 0.7 * radii, centers[0][1] - 0.7 * radii),
    arrowprops=dict(arrowstyle="-|>"),
)
ax.annotate(
    "",
    (centers[3][0] + 0.7 * radii, centers[3][1] + 0.7 * radii),
    (centers[2][0] - 0.7 * radii, centers[2][1] - 0.7 * radii),
    arrowprops=dict(arrowstyle="-|>"),
)

fig.savefig("causal.png", dpi=300)

A joke flow chart. The top level goes from left to right, and then both left and right edges lead to the bottom level. The top level is: 1. My roommate is a Philistine and a boor. 2. My roommate ate the last of the cold cereal. 3. I am really really hungry. The bottom level is: I’m annoyed at my roommate.

一旦您开始走上这条道路,就可以开始制作带有大量注释的图表。它可以将您的数据演示提升到真正的讲故事的高度。

祝您图表绘制愉快!