Matplotlibで縦横比は合ってるのに目盛りがズレて見える？美しく整えるための簡単Tips

## はじめに

Matplotlibで画像やヒートマップを描いたとき、
縦横比は `aspect='equal'` で揃えているのに、なぜかtick（目盛り）の間隔がズレて見えること、ありませんか？

たとえば、下の図のようなケースです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/e66b2ca8-fed7-4530-94a4-6b807236917b.png)

左側が「よくある問題のグラフ」、右側が**今回めざす理想のグラフ**（＝この変換を自動で実現したい！）です。

今回は、こうした「tickのズレ問題」を**縦横で美しく揃えるコツ**を紹介します。

※本記事では、ヒートマップなど「縦横比が同じ」プロットを扱うことを前提としています。


> 本記事のコードはGoogle Colabでも動かせます
> [すぐ試したい方はこちら](https://colab.research.google.com/drive/14_DO1TvswY0L3ccujkK0pacRgJ3bLW7U?usp=sharing)


## なぜtickがズレるのか？

Matplotlibは、tickラベルが重ならないように自動で間引いて配置してくれます。
この処理自体はとても便利ですが、ラベルの方向や長さの違いによって、以下のような非対称性が生まれます。

> 横軸のラベルは横向きに並ぶため、縦軸よりも重なりやすく、tickが間引かれやすいといった現象が起きます。

その結果、同じスケール感であっても、tickの“数”や“表示密度”がズレてしまうのです。

グラフ全体の印象に微妙な違和感を与える原因になります。


## 解決策：tickを“片方に合わせて”整える

tickが自動調整されるなら、一方のtickにもう一方を合わせてあげればいい。
特に、**ラベルが間引かれやすい横軸に合わせる**と、グラフの見た目は一気に整います。

最終的に行き着いたコードを紹介します。
なぜこのコードに行き着いたかは少し長くなるのですが、興味がある方は補足をご覧ください。


## サンプルコード

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

data = np.random.rand(305, 305)
fig, ax = plt.subplots(figsize=(3, 3))

# ヒートマップ表示（正方形アスペクト）
im = ax.imshow(data, origin='lower', cmap='viridis')
ax.set_aspect('equal')
ax.tick_params(labelsize=20)

# ticksを確定し、x軸とy軸で揃える（ここが重要）
fig.canvas.draw()
ticks = ax.get_xticks()
ax.set_xticks(ticks)
ax.set_yticks(ticks)
ax.set_xlim(-0.5, data.shape[1] - 0.5)
ax.set_ylim(-0.5, data.shape[0] - 0.5)

plt.show()
```

**出力結果（自動で設定された目盛りが縦横で揃う）**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/ed2f404d-e43d-45c4-8088-4e3f56431bb1.png)


```python:これを加えるだけ
fig.canvas.draw()
ticks = ax.get_xticks()
ax.set_xticks(ticks)
ax.set_yticks(ticks)
ax.set_xlim(-0.5, data.shape[1] - 0.5)
ax.set_ylim(-0.5, data.shape[0] - 0.5)
```

これを加えるだで、**tickの間隔が縦横で一致し、グリッドや枠線も美しく見えるようになります。**


## 補足

ここからは先のコードの深掘りになります。
サンプルコードの重要な箇所は以下になります。

```python:重要なコード
fig.canvas.draw()
ticks = ax.get_xticks()
ax.set_xticks(ticks)
ax.set_yticks(ticks)
ax.set_xlim(-0.5, data.shape[1] - 0.5)
ax.set_ylim(-0.5, data.shape[0] - 0.5)
```

このコードが必要な理由について説明します。

### ここがややこしい！tickの世界

Matplotlibでは、tick（目盛り）の位置や数は「描画の直前」に自動調整されます。
そのため、`get_xticks()` を使っても、その時点で取得できるのは「仮のtickリスト」であり、実際に画面に描かれるtickかどうかは分かりません。

そこで、

1. `fig.canvas.draw()` で一度“仮描画”させてtick配置を確定させる
2. そのタイミングで `get_xticks()` を呼び出して、「実際に使われるtickリスト」を取得する
3. そのtickリストを `set_xticks`, `set_yticks` で両軸の目盛りとして指定する
4. さらに、見せたくない範囲まで出てしまうtickをシャットアウトするため、`set_xlim`, `set_ylim` で表示範囲を指定する

4番は不要では？と思う方も多いと思います。
実はここがややこしいポイントなので、以下に補足します。

### なぜ4番が必要なの？

`get_xticks()`で得られるtickリストは、Matplotlibが「データ全体をカバーする間隔」で自動配置してしまうため、実際のデータ範囲（たとえば0〜304）より外側（たとえば−50や350など）にまでtickが付く場合があります。

そのため、そのまま `set_xticks`, `set_yticks` を使うと、「本当は見せたくない範囲」までtickが表示されてしまいます。

そこで重要なのがこの2行です。

```python
ax.set_xlim(-0.5, data.shape[1] - 0.5)
ax.set_ylim(-0.5, data.shape[0] - 0.5)
```

これを加えるだけで、余分なtickや外側の目盛りをきれいにシャットアウトできます。


## おわりに

グラフって、一見脇役に見える存在に、どれだけ真摯に向き合えるかにかかっていると思います。 
その中でもtickは、グラフのスケール感を支える大切なパーツです。  
ただ、tick配置の内部で起こるせめぎ合いが、ときにグラフ全体の雰囲気を微妙に揺らしてしまうんですよね。

こうした、個々のパーツの葛藤に目を向けることが、最終的に「伝えたいこと」の輪郭を、より確実に届けてくれるのだと思います。

Matplotlibは、デフォルトだけでは届かない壁がある。  
でも手を加えすぎれば、それは“独りよがり”になってしまう。  
そのあいだにある、絶妙なバランスこそが、世界観を“量産可能なかたち”に昇華させてくれるのだと思います。

……と、つい語ってしまいましたが、Matplotlibに込められた思想は、きっともっともっと深くて。
この文章もまた、“グラフのためのグラフ”を描いているに過ぎません。

でも、結局「きれいなグラフ」って、何なんでしょうか。
Matplotlib自身も、きっとまだその答えを探し続けていて。
世に出ることのなかった“グラフのためのグラフ”たちの中に、まだ誰も知らない黄金比が、静かに眠っているのかもしれません。
いつかそれに気づける日が来ると思うと、今から楽しみで仕方ありません。