はじめに
Matplotlibで画像やヒートマップを描いたとき、
縦横比は aspect='equal'
で揃えているのに、なぜかtick(目盛り)の間隔がズレて見えること、ありませんか?
たとえば、下の図のようなケースです。
左側が「よくある問題のグラフ」、右側が今回めざす理想のグラフ(=この変換を自動で実現したい!)です。
今回は、こうした「tickのズレ問題」を縦横で美しく揃えるコツを紹介します。
※本記事では、ヒートマップなど「縦横比が同じ」プロットを扱うことを前提としています。
本記事のコードはGoogle Colabでも動かせます
すぐ試したい方はこちら
なぜtickがズレるのか?
Matplotlibは、tickラベルが重ならないように自動で間引いて配置してくれます。
この処理自体はとても便利ですが、ラベルの方向や長さの違いによって、以下のような非対称性が生まれます。
横軸のラベルは横向きに並ぶため、縦軸よりも重なりやすく、tickが間引かれやすいといった現象が起きます。
その結果、同じスケール感であっても、tickの“数”や“表示密度”がズレてしまうのです。
グラフ全体の印象に微妙な違和感を与える原因になります。
解決策:tickを“片方に合わせて”整える
tickが自動調整されるなら、一方のtickにもう一方を合わせてあげればいい。
特に、ラベルが間引かれやすい横軸に合わせると、グラフの見た目は一気に整います。
最終的に行き着いたコードを紹介します。
なぜこのコードに行き着いたかは少し長くなるのですが、興味がある方は補足をご覧ください。
サンプルコード
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()
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の間隔が縦横で一致し、グリッドや枠線も美しく見えるようになります。
補足
ここからは先のコードの深掘りになります。
サンプルコードの重要な箇所は以下になります。
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かどうかは分かりません。
そこで、
-
fig.canvas.draw()
で一度“仮描画”させてtick配置を確定させる - そのタイミングで
get_xticks()
を呼び出して、「実際に使われるtickリスト」を取得する - そのtickリストを
set_xticks
,set_yticks
で両軸の目盛りとして指定する - さらに、見せたくない範囲まで出てしまうtickをシャットアウトするため、
set_xlim
,set_ylim
で表示範囲を指定する
4番は不要では?と思う方も多いと思います。
実はここがややこしいポイントなので、以下に補足します。
なぜ4番が必要なの?
get_xticks()
で得られるtickリストは、Matplotlibが「データ全体をカバーする間隔」で自動配置してしまうため、実際のデータ範囲(たとえば0〜304)より外側(たとえば−50や350など)にまでtickが付く場合があります。
そのため、そのまま set_xticks
, set_yticks
を使うと、「本当は見せたくない範囲」までtickが表示されてしまいます。
そこで重要なのがこの2行です。
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自身も、きっとまだその答えを探し続けていて。
世に出ることのなかった“グラフのためのグラフ”たちの中に、まだ誰も知らない黄金比が、静かに眠っているのかもしれません。
いつかそれに気づける日が来ると思うと、今から楽しみで仕方ありません。