Matplotlibで複雑な凡例をスッキリ整理：行列風に魅せるTips

## はじめに

複数の条件（例：Before / After）と複数の系列（例：Group A / Group B）が交差するようなプロットにおいて、`legend()` だけでは情報を整理して見せるのが難しい場合があります。特に、行列のように分類軸を視覚的に整理したい場合には、工夫が必要です。

本記事では、`matplotlib` において `ax.text()` などを活用し、凡例をマトリクス（行列）状に美しく表示する方法を紹介します。論文図やプレゼン資料など、見た目にもこだわりたいケースを想定しています。


本記事のコードはGoogle Colab[こちら](https://colab.research.google.com/drive/1BLu-ScSvEhrtlN_OVA5qo1cgXqHBMUz4?usp=sharing)からも閲覧できます。

## 基本的な凡例の表示方法

まずは、`label` 引数と `legend()` を使った基本的な凡例表示の例です。

```python
import matplotlib.pyplot as plt
import numpy as np

# データ準備
x = np.linspace(0, 10, 100)
y1_wo = np.sin(x)
y1_w = np.sin(x) * 1.2
y2_wo = np.sin(x + 1)
y2_w = np.sin(x + 1) * 1.2

# プロット
fig, ax = plt.subplots()

ax.plot(x, y1_wo, ':', label='Condition A / w/o Corrected', color='teal')
ax.plot(x, y1_w, '-', label='Condition A / w/ Corrected', color='cyan')
ax.plot(x, y2_wo, ':', label='Condition B / w/o Corrected', color='purple')
ax.plot(x, y2_w, '-', label='Condition B / w/ Corrected', color='magenta')

# 凡例
ax.legend(loc='upper right')
ax.set_title("Basic Legend Example")
plt.tight_layout()
plt.show()
```
**出力結果**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/8af1a800-02be-440d-a0b6-6a4f49d677fe.png)


このように凡例に情報をすべて詰め込むと、やや煩雑な印象になりがちです。

## 行列のように整理された凡例の表示

マトリックスで凡例を表示することで視覚的にわかりやすい図を作成することができます。

### 方法1: 空白ラベルを活用したシンプルな方法

```python
import matplotlib.pyplot as plt
import numpy as np

# --- プロットの準備 ---
fig, ax = plt.subplots()

# ダミーデータ
x = np.linspace(0, 10, 100)
y1_wo = np.sin(x)
y1_w = np.sin(x) * 1.2
y2_wo = np.sin(x + 1)
y2_w = np.sin(x + 1) * 1.2

# プロット
# w/o Correction（点線）の2系列（labelに空白を入れることで凡例に残す）
ax.plot(x, y1_wo, ':', color='teal', alpha=0.8, label=' ')
ax.plot(x, y2_wo, ':', color='purple', alpha=0.8, label=' ')

# w/ Correction（実線）の2系列に名前を付ける
ax.plot(x, y1_w, '-', color='cyan', alpha=0.8, label='Condition A')
ax.plot(x, y2_w, '-', color='magenta', alpha=0.8, label='Condition B')

# タイトルのように空白入りで書いてカラム名っぽく見せる
ax.legend(
    ncol=2,
    columnspacing=-0.2,
    loc='upper right',
    title="w/o      w/      Corrected",
    handlelength=2.5,
)

ax.set_title("Custom Grouped Legend Example")
plt.tight_layout()
plt.show()
```
**出力結果**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/e7a2c312-4ef5-4dc1-8b4e-921570fe6e59.png)


**ポイント**
- `ax.plot(label=' ')`のように空白を入れること凡例に残しつつ、ラベルを表示をしない設定にすることができます。
- この方法では、`ax.legend()`に力技で、適当な空白を入れて対応する凡例の位置に揃うようにラベルを入れています。より柔軟な列の凡例を表示する方法を次の節で示します。


### 方法2: 空白ラベルと後入れのtext()を活用した方法
方法1では列ラベルが思うような位置に配置できない場合があります。そこで、`ax.text()` で任意の位置にテキストを表示する自由度の高い方法を紹介します。

```python
import matplotlib.pyplot as plt
import numpy as np

# --- プロットの準備 ---
fig, ax = plt.subplots()

# ダミーデータ
x = np.linspace(0, 10, 100)
y1_wo = np.sin(x)
y1_w = np.sin(x) * 1.2
y2_wo = np.sin(x + 1)
y2_w = np.sin(x + 1) * 1.2

# w/o Correction（点線）の2系列（labelに空白を入れることで凡例に残す）
ax.plot(x, y1_wo, ':', color='teal', alpha=0.8, label=' ')
ax.plot(x, y2_wo, ':', color='purple', alpha=0.8, label=' ')

# w/ Correction（実線）の2系列に名前を付ける
ax.plot(x, y1_w, '-', color='cyan', alpha=0.8, label='Condition A')
ax.plot(x, y2_w, '-', color='magenta', alpha=0.8, label='Condition B')

# 凡例表示（タイトルに余白を入れることで凡例のカラムスペースを確保）
ax.legend(
    ncol=2,
    columnspacing=-0.2,
    loc='upper right',
    title=" ",
    handlelength=2.5,
)

# 凡例の上にカラム名表示（transform=ax.transAxes でグラフ描画内の0〜1で位置を範囲）
ax.text(0.69, 0.965, 'w/o     w/', color='k',
        ha='left', va='top', transform=ax.transAxes, zorder=10, fontsize="medium")

ax.set_title("Custom Grouped Legend Example with ax.text")
plt.tight_layout()
plt.show()
```
**出力結果**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/e341573a-393d-41b5-9fd7-2bb3dbcb3968.png)

**ポイント**
- `legend(title=' ')` に空白を入れて列の文字が入るスペースを確保しています。
- `ax.text(transform=ax.transAxes)` の `transform` オプションを使うことで、`x` および `y` の位置をグラフ描画領域内の 0〜1 の相対座標で指定できるようになります。  
これを指定しない場合、テキスト位置は実際のデータ座標に基づくため、グラフのスケールを変更するたびに再調整が必要になります。そのため、筆者は 汎用性と再利用性を重視して、この`transform`オプションをよく活用しています。
- 基本的に`legend()`の文字サイズのデフォルト値は`fontsize="medium"`です。
`ax.text(fontsize="medium")`と指定することで、凡例とテキストの文字サイズに統一感が生まれ、全体として美しい表示が可能になります。

---

### 方法3: `HandlerTuple` を使って1行に複数線をまとめる

ここではもう少し踏み込んだ方法として、`HandlerTuple` を使って凡例を1行に複数の線でまとめるテクニックを紹介します。以下の記事を参考にしています。

https://qiita.com/yusuke_s_yusuke/items/857f8d753080749c120e

方法1と方法2の弱点は、**プロットの順番に依存して凡例が並んでしまう**点です。一方この方法では、`HandlerTuple` を使って **グループを明示的に指定できる**ため、より柔軟かつ視覚的にわかりやすい凡例配置が可能になります。
とはいえ少し複雑なので、筆者は状況に応じて方法1〜3を使い分けています。

```python
import matplotlib.pyplot as plt
from matplotlib.legend_handler import HandlerTuple
import numpy as np

# --- プロットの準備 ---
fig, ax = plt.subplots()

# ダミーデータ
x = np.linspace(0, 10, 100)
y1_wo = np.sin(x)
y1_w = np.sin(x) * 1.2
y2_wo = np.sin(x + 1)
y2_w = np.sin(x + 1) * 1.2

# プロット
# w/o Correction（点線）の2系列（labelに空白を入れることで凡例に残す）
p1_wo, = ax.plot(x, y1_wo, ':', color='teal', alpha=0.8, label='Condition A (w/o)')
p2_wo, = ax.plot(x, y2_wo, ':', color='purple', alpha=0.8, label='Condition B (w/o)')

# w/ Correction（実線）の2系列に名前を付ける
p1_w, = ax.plot(x, y1_w, '-', color='cyan', alpha=0.8, label='Condition A (w/)')
p2_w, = ax.plot(x, y2_w, '-', color='magenta', alpha=0.8, label='Condition  (w/)')

# 凡例をまとめる
p1 = (p1_wo, p1_w)
p2 = (p2_wo, p2_w)

# HandlerTupleで完結に表示
ax.legend(
    [p1, p2],
    ['Condition A', 'Condition B'],
    handler_map={tuple: HandlerTuple(ndivide=None)},
    loc='upper right',
    title="w/o      w/      Corrected",
    handlelength=5
)

ax.set_title("Custom Grouped Legend Example")
plt.tight_layout()
plt.show()
```
**出力結果**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/d4c45095-db84-409d-8bbd-6d04e496ea26.png)

やや上級ですが、1行に複数の線を表示したい場合にこの方法も活用できます。


## 補足：レイアウト調整Tips
ここでは、各方法で使えるオプションとその効果を整理した比較表をまとめ、実際の凡例を表示して視覚的に整理していきます。

### 凡例オプションの比較表（方法1〜3）

| オプション         | 方法1・2（標準legend） | 方法3（HandlerTuple使用）    | 説明 |
|------------------|----------------------|-----------------------------|------|
| `handlelength`    | ◯ 効く               | ◯ 効く                      | 線の長さ（グループの横幅）を調整 |
| `columnspacing`   | ◯ 効く               | × 効かない（無視される）     | 列間のスペース |
| `HandlerTuple(pad)` | × 使用不可           | ◯ グループ内の表示スペースを調整     | `HandlerTuple(pad=1.0)` など |
| `handletextpad`   | ◯ 効く               | ◯ 効く                      | 線と文字の間隔 |


方法1・2では、`columnspacing` を使って凡例内の線（列）の間隔を調整します。一方、方法3の `HandlerTuple()` を使う場合は `pad` オプションで調整する点が大きな違いです。  
 
そのほかにも、オプションごとに効果の出方が異なる部分があるため、以下の凡例付き出力図に興味のある方はぜひチェックしてみてください。

<details><summary>出力図付き：オプションごとの効果まとめ</summary>


| オプション（設定値） | 方法 | 出力図 | 説明 |
|---------------------|--------|--------|------|
| `handlelength=2` | 方法1〜3共通 | ![h2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/1039c800-4467-49aa-aaa6-14a2f0a8ad79.png) | 短めの線でコンパクトな凡例 |
| `handlelength=10` | 方法1〜3共通 | ![h10](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/a49173ec-03da-468f-9b57-30a3705c589f.png) | 長くすることで破線などを見やすく表示 |
| `columnspacing=-0.2` | 方法1・2専用 | ![cs-0.2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/b6695b0b-b9b2-4955-8954-9ba14366d9f1.png) | 列間を狭くし、詰めて表示 |
| `columnspacing=1.5` | 方法1・2専用 | ![cs1.5](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/48249c5a-9b09-4b79-b59c-1e039ec318dc.png) | 列間を広げて余裕ある表示 |
| `HandlerTuple(pad=0.2)` | 方法3専用 | ![pad0.2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/731b8df5-4b42-40da-9e5d-892120f6ba55.png)| グループ内の2線が近くなる（線の長さも自動で変化） |
| `HandlerTuple(pad=1.5)` | 方法3専用 | ![pad1.5](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/b92c58da-066a-46f7-9530-7c161a712571.png)| グループ内の2線が遠くなる（線の長さも自動で変化） |
| `handletextpad=0.5` | 方法1〜3共通 | ![htp0.5](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/31d4a079-1420-47f2-9040-9f788df62bc5.png) | 線と文字が近く、コンパクトに表示 |
| `handletextpad=3.0` | 方法1〜3共通 | ![htp3.0](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/05ec2787-26eb-4b11-b5c5-9936c72672ab.png) | 線と文字の間に広いスペースを確保 |

</details>


## まとめ

本記事では、matplotlib における複数条件・複数系列の凡例をマトリクス状に整理して表示するテクニックを紹介しました。以下の3つの方法を紹介しました。

- 空白ラベル＋凡例タイトルの工夫
- `ax.text()` による手動カラム名追加
- `HandlerTuple` を使った1行に2系列まとめる方法

図の見た目にこだわりたい論文・発表資料の作成において、ぜひ参考にしてみてください。

## 関連記事

https://qiita.com/yusuke_s_yusuke/items/857f8d753080749c120e