目录4

文章绘图第 33 章

CHORD DIAGRAM

绘图 · 第 33 / 36 章

基础弦图

chart_122

基础弦图
Python33 行
import matplotlib.pyplot as plt
import pandas as pd
from pycirclize import Circos

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

matrix = [
    [0,  10,  5,  8,  3],
    [10,  0,  6,  4,  7],
    [5,   6,  0,  9,  2],
    [8,   4,  9,  0,  5],
    [3,   7,  2,  5,  0],
]

labels = list("ABCDE")
df = pd.DataFrame(matrix, index=labels, columns=labels)

# 关键:cmap 需要 {名称: 颜色} 的字典
name2color = {name: color for name, color in zip(labels, COLORS)}

circos = Circos.chord_diagram(
    df,
    space=5,
    cmap=name2color,                # ← 字典,不是列表
    label_kws=dict(size=13, color=COLORS[3], fontweight="bold"),
    link_kws=dict(ec="white", lw=0.8, alpha=0.4, direction=1),
)

fig = circos.plotfig()
fig.suptitle("Chord Diagram — Basic", fontsize=15, color=COLORS[3], y=0.98)

plt.show()

贸易流向弦图

chart_123

贸易流向弦图
Python66 行
import matplotlib.pyplot as plt
from pycirclize import Circos
import numpy as np

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

countries = ["USA", "China", "Germany", "Japan", "Brazil"]
matrix = np.array([
    [0,  50, 30, 25, 15],
    [45,  0, 35, 20, 18],
    [28, 32,  0, 15, 12],
    [22, 18, 14,  0, 10],
    [12, 15, 10,  8,  0],
])

# 每个扇区的大小 = 该国家所有连线的总和
# 注意:为了对称,扇区大小要包含出口 + 进口
sector_sizes = {}
for i, c in enumerate(countries):
    out_sum = matrix[i, :].sum()
    in_sum  = matrix[:, i].sum()
    sector_sizes[c] = out_sum + in_sum

circos = Circos(sectors=sector_sizes, space=4)

color_map = {name: COLORS[i] for i, name in enumerate(countries)}

# 画弧 + 标签
for sector in circos.sectors:
    track = sector.add_track((70, 100))
    track.axis(fc=color_map[sector.name], alpha=0.85, ec="white", lw=1.5)
    sector.text(sector.name, r=115, size=12,
                color=COLORS[3], fontweight="bold")

# 为每个扇区内的每个连线分配位置
# 用一个字典记录每个扇区当前已经用掉的长度
used = {c: 0 for c in countries}

for i, src in enumerate(countries):
    for j, tgt in enumerate(countries):
        if i == j or matrix[i, j] == 0:
            continue
        # 源扇区中,这条线段的起始与结束位置(相对扇区大小)
        src_start = used[src]
        src_end   = src_start + matrix[i, j]
        used[src] = src_end

        # 目标扇区中,对应线段的起始与结束位置
        # 注意:目标扇区中是 matrix[i, j] 这条线在 tgt 扇区里占的份额
        tgt_start = used[tgt]
        tgt_end   = tgt_start + matrix[i, j]
        used[tgt] = tgt_end

        circos.link(
            (src, src_start, src_end),
            (tgt, tgt_start, tgt_end),
            color=color_map[src],
            alpha=0.4,
        )

fig = circos.plotfig()
fig.suptitle("Trade Flow Chord Diagram",
             fontsize=15, color=COLORS[3], y=0.98)

plt.show()

分组弦图(多类别)

chart_124

分组弦图(多类别)
Python81 行
import matplotlib.pyplot as plt
from pycirclize import Circos
import numpy as np

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

# 分组:产品类 / 服务类
groups = {
    "Product": ["P-A", "P-B", "P-C"],
    "Service": ["S-A", "S-B", "S-C"],
}
group_colors = {
    "Product": COLORS[0],
    "Service": COLORS[1],
}

matrix = np.array([
    [0,   15,  10,  8,   5,   3],
    [15,  0,   12,  6,   4,   2],
    [10,  12,  0,   5,   7,   4],
    [8,   6,   5,   0,   10,  8],
    [5,   4,   7,   10,  0,   9],
    [3,   2,   4,   8,   9,   0],
])

all_labels = [l for g in groups.values() for l in g]
label_to_group = {l: g for g, ls in groups.items() for l in ls}
label_to_idx = {l: i for i, l in enumerate(all_labels)}

# ① 每个扇区大小 = 该实体的所有连线总和(出 + 入)
sector_sizes = {}
for i, name in enumerate(all_labels):
    out_sum = matrix[i, :].sum()
    in_sum = matrix[:, i].sum()
    sector_sizes[name] = out_sum + in_sum

circos = Circos(sectors=sector_sizes, space=3)

# ② 画弧 + 标签
for sector in circos.sectors:
    g = label_to_group[sector.name]
    c = group_colors[g]
    track = sector.add_track((70, 100))
    track.axis(fc=c, alpha=0.85, ec="white", lw=1.5)
    sector.text(sector.name, r=112, size=10,
                color=COLORS[3], fontweight="bold")

# ③ 累积分配每条弦在扇区中的位置
used = {l: 0 for l in all_labels}

for i, src in enumerate(all_labels):
    for j, tgt in enumerate(all_labels):
        if i >= j or matrix[i, j] == 0:
            continue

        # 源扇区段落
        src_start = used[src]
        src_end   = src_start + matrix[i, j]
        used[src] = src_end

        # 目标扇区段落
        tgt_start = used[tgt]
        tgt_end   = tgt_start + matrix[i, j]
        used[tgt] = tgt_end

        # 按源所在组着色
        c = group_colors[label_to_group[src]]

        circos.link(
            (src, src_start, src_end),
            (tgt, tgt_start, tgt_end),
            color=c,
            alpha=0.35,
        )

fig = circos.plotfig()
fig.suptitle("Grouped Chord Diagram",
             fontsize=15, color=COLORS[3], y=0.98)

plt.show()

带弦颜色渐变的弦图

chart_125

带弦颜色渐变的弦图
Python73 行
import matplotlib.pyplot as plt
from matplotlib.colors import to_rgb, to_hex
from pycirclize import Circos
import numpy as np

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

labels = ["A", "B", "C", "D", "E"]
matrix = np.array([
    [0,  20, 12, 8,  5],
    [18, 0,  15, 10, 6],
    [10, 14, 0,  12, 8],
    [7,  9,  11, 0,  10],
    [4,  5,  7,  9,  0],
])

# ① 每个扇区大小 = 该实体的出入总和
sector_sizes = {}
for i, name in enumerate(labels):
    sector_sizes[name] = matrix[i, :].sum() + matrix[:, i].sum()

circos = Circos(sectors=sector_sizes, space=5)

# ② 画弧 + 标签
for sector in circos.sectors:
    idx = labels.index(sector.name)
    track = sector.add_track((72, 100))
    track.axis(fc=COLORS[idx], alpha=0.9, ec="white", lw=1.5)
    sector.text(sector.name, r=112, size=14,
                color=COLORS[3], fontweight="bold")

# ③ 工具:混合两个 hex 颜色
def blend_hex(c1, c2):
    r1, g1, b1 = to_rgb(c1)
    r2, g2, b2 = to_rgb(c2)
    return to_hex(((r1 + r2) / 2, (g1 + g2) / 2, (b1 + b2) / 2))

# ④ 累积分配每条弦在扇区中的位置
used = {l: 0 for l in labels}

for i, src in enumerate(labels):
    for j, tgt in enumerate(labels):
        if i >= j or matrix[i, j] == 0:
            continue

        # 非对称矩阵:用两个方向的平均值作为弦宽
        w = (matrix[i, j] + matrix[j, i]) / 2

        # 源扇区段落
        src_start = used[src]
        src_end   = src_start + w
        used[src] = src_end

        # 目标扇区段落
        tgt_start = used[tgt]
        tgt_end   = tgt_start + w
        used[tgt] = tgt_end

        c_hex = blend_hex(COLORS[i], COLORS[j])

        circos.link(
            (src, src_start, src_end),
            (tgt, tgt_start, tgt_end),
            color=c_hex,
            alpha=0.45,
        )

fig = circos.plotfig()
fig.suptitle("Chord Diagram — Blended Colors",
             fontsize=15, color=COLORS[3], y=0.98)

plt.show()