目录5

文章绘图第 23 章

DOUGHNUT

绘图 · 第 23 / 36 章

基础甜甜圈图

chart_091

基础甜甜圈图
Python38 行
import matplotlib.pyplot as plt
import numpy as np

COLORS = ["#A8552E", "#35666E", "#D0A64F", "#3F2E18", "#7E8F52"]

labels = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon"]
values = [35, 25, 20, 12, 8]

fig, ax = plt.subplots(figsize=(8, 8))

wedges, texts, autotexts = ax.pie(
    values,
    labels=labels,
    colors=COLORS[:len(values)],
    autopct="%1.1f%%",
    startangle=90,
    counterclock=False,
    pctdistance=0.80,          # ← 关键:把百分比标签推到色环中间
    wedgeprops=dict(width=0.4, edgecolor="white", linewidth=2.0),
    textprops=dict(color=COLORS[3], fontsize=11),
)

# 百分比文字:白字(因为在深色环上)
for at in autotexts:
    at.set_color("white")
    at.set_fontsize(10)
    at.set_fontweight("bold")

# 中心文字
total = sum(values)
ax.text(0, 0, f"{total}\nTotal", ha="center", va="center",
        fontsize=16, color=COLORS[3], fontweight="bold")

ax.set_title("Doughnut Chart", fontsize=15, color=COLORS[3], pad=12)
ax.set_aspect("equal")

plt.tight_layout()
plt.show()

中心标注关键指标

chart_092

中心标注关键指标
Python45 行
import matplotlib.pyplot as plt
import numpy as np

COLORS = ["#A8552E", "#35666E", "#D0A64F", "#3F2E18", "#7E8F52"]
BG = "#F7F3EC"

labels = ["North", "South", "East", "West"]
values = [42, 28, 18, 12]
total = sum(values)

fig, ax = plt.subplots(figsize=(8.5, 8))

wedges, texts, autotexts = ax.pie(
    values,
    labels=None,                    # 标签移到图例
    colors=COLORS[:len(values)],
    autopct=lambda p: f"{p:.0f}%",
    startangle=90,
    counterclock=False,
    pctdistance=0.79,               # 百分比在环中间
    wedgeprops=dict(width=0.38, edgecolor="white", linewidth=2.5),
    textprops=dict(color="white", fontsize=11, fontweight="bold"),
)

# 中心:总量 + 单位
ax.text(0, 0.08, f"{total}", ha="center", va="center",
        fontsize=32, color=COLORS[3], fontweight="bold")
ax.text(0, -0.18, "Sales (M$)", ha="center", va="center",
        fontsize=12, color=COLORS[3])

ax.set_title("Sales Distribution by Region",
             fontsize=15, color=COLORS[3], pad=12)

# 图例(含数值)
from matplotlib.patches import Patch
handles = [Patch(facecolor=c, edgecolor="white",
                 label=f"{name}  —  {v}")
           for name, v, c in zip(labels, values, COLORS)]
ax.legend(handles=handles, frameon=False, fontsize=11,
          labelcolor=COLORS[3], loc="center left",
          bbox_to_anchor=(1.02, 0.5))

ax.set_aspect("equal")
plt.tight_layout()
plt.show()

半环甜甜圈

chart_093

半环甜甜圈
Python57 行
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Patch

COLORS = ["#A8552E", "#35666E", "#D0A64F", "#3F2E18", "#7E8F52"]
BG = "#F7F3EC"

values = [65, 25, 10]
labels = ["Completed", "In Progress", "Pending"]
colors_used = [COLORS[0], COLORS[2], COLORS[4]]

fig, ax = plt.subplots(figsize=(9, 5.5))

# 把占比换算成角度(总共 180°)
total = sum(values)
angles = [v / total * 180 for v in values]

# 手动画三个半环扇形
start = 180  # 从左侧开始
for ang, color in zip(angles, colors_used):
    ax.pie(
        [ang, 360 - ang],           # 该段 + 剩余补满整圆
        colors=[color, "none"],     # 剩余部分透明
        startangle=start,
        counterclock=False,
        wedgeprops=dict(width=0.38, edgecolor="white", linewidth=2.5),
    )
    start -= ang

# 百分比标签位置
start = 180
for ang, v in zip(angles, values):
    mid = np.deg2rad(start - ang / 2)
    x, y = 0.81 * np.cos(mid), 0.81 * np.sin(mid)
    ax.text(x, y, f"{v}%", ha="center", va="center",
            color="white", fontsize=11, fontweight="bold")
    start -= ang

ax.set_ylim(-0.05, 1.05)
ax.set_xlim(-1.1, 1.1)

ax.text(0, 0.35, "65%", ha="center", va="center",
        fontsize=34, color=COLORS[0], fontweight="bold")
ax.text(0, 0.12, "Completed", ha="center", va="center",
        fontsize=13, color=COLORS[3])

ax.set_title("Progress Gauge", fontsize=15, color=COLORS[3], pad=12)

handles = [Patch(facecolor=c, edgecolor="white", label=name)
           for name, c in zip(labels, colors_used)]
ax.legend(handles=handles, frameon=False, fontsize=10,
          labelcolor=COLORS[3], loc="lower center",
          ncol=3, bbox_to_anchor=(0.5, -0.05))

ax.set_aspect("equal")
plt.tight_layout()
plt.show()

多环嵌套甜甜圈(旭日图变体)

chart_094

多环嵌套甜甜圈(旭日图变体)
Python63 行
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.patheffects as pe

COLORS = ["#A8552E", "#35666E", "#D0A64F", "#3F2E18", "#7E8F52"]
BG = "#F7F3EC"

inner_labels = ["Product", "Service"]
inner_values = [65, 35]
outer_labels = ["P-A", "P-B", "P-C", "S-A", "S-B"]
outer_values = [30, 22, 13, 20, 15]

fig, ax = plt.subplots(figsize=(9, 9))

# 外环
outer_colors = [COLORS[0], COLORS[2], COLORS[4], COLORS[1], COLORS[3]]
ax.pie(
    outer_values,
    radius=1.0,
    colors=outer_colors,
    labels=outer_labels,
    labeldistance=1.08,
    startangle=90,
    pctdistance=0.9,
    counterclock=False,
    wedgeprops=dict(width=0.28, edgecolor="white", linewidth=2.0),
    textprops=dict(color=COLORS[3], fontsize=10),
)

# 内环:不画 labels,手动放
ax.pie(
    inner_values,
    radius=0.72,
    colors=[COLORS[0], COLORS[1]],
    startangle=90,
    counterclock=False,
    wedgeprops=dict(width=0.30, edgecolor="white", linewidth=2.5),
)

# 手动把内环标签放到色环正中(半径 0.57)
inner_mid_r = 0.72 - 0.30 / 2   # 0.57
start = 90
total_inner = sum(inner_values)
for val, lab in zip(inner_values, inner_labels):
    span = 360 * val / total_inner
    mid_ang = np.deg2rad(start - span / 2)
    ax.text(
        inner_mid_r * np.cos(mid_ang),
        inner_mid_r * np.sin(mid_ang),
        lab,
        ha="center", va="center",
        fontsize=11, fontweight="bold",
        color="white",
        path_effects=[pe.withStroke(linewidth=2.5, foreground=COLORS[3])],
    )
    start -= span

ax.set_title("Nested Doughnut — Two Levels",
             fontsize=15, color=COLORS[3], pad=12)
ax.set_aspect("equal")

plt.tight_layout()
plt.show()

带引导线的甜甜圈

chart_095

带引导线的甜甜圈
Python58 行
import matplotlib.pyplot as plt
import numpy as np

COLORS = ["#A8552E", "#35666E", "#D0A64F", "#3F2E18", "#7E8F52"]
BG = "#F7F3EC"

labels = ["Alpha Division", "Beta Division",
          "Gamma Division", "Delta Division"]
values = [38, 28, 20, 14]

fig, ax = plt.subplots(figsize=(10, 8))

wedges, texts = ax.pie(
    values,
    labels=None,
    colors=COLORS[:len(values)],
    startangle=90,
    counterclock=False,
    wedgeprops=dict(width=0.4, edgecolor="white", linewidth=2.5),
    labeldistance=1.15,
)

# 手动加外部标签 + 引导线
total = sum(values)
for i, (wedge, label, val) in enumerate(zip(wedges, labels, values)):
    ang = (wedge.theta2 + wedge.theta1) / 2
    x = np.cos(np.deg2rad(ang))
    y = np.sin(np.deg2rad(ang))

    # 引导线终点
    x_text = 1.25 * x
    y_text = 1.25 * y

    # 引导线
    ax.annotate(
        f"{label}\n{val}  ({val / total * 100:.0f}%)",
        xy=(0.95 * x, 0.95 * y),         # 环边缘
        xytext=(x_text, y_text),          # 文字位置
        ha="left" if x >= 0 else "right",
        va="center",
        fontsize=11,
        color=COLORS[3],
        arrowprops=dict(arrowstyle="-", color=COLORS[3],
                        linewidth=1.0, alpha=0.6),
    )

# 中心总量
ax.text(0, 0.06, f"{total}", ha="center", va="center",
        fontsize=28, color=COLORS[3], fontweight="bold")
ax.text(0, -0.14, "Total", ha="center", va="center",
        fontsize=11, color=COLORS[3])

ax.set_title("Doughnut with Leader Lines",
             fontsize=15, color=COLORS[3], pad=12)
ax.set_aspect("equal")

plt.tight_layout()
plt.show()