如何在matplotlib中制作带有热力图的正方形子图?

13

我想要在一个subplot中创建一个包含树状图的子图和一个包含热力图的子图,同时保持正方形轴。我尝试使用以下代码:

from scipy.cluster.hierarchy import linkage
from scipy.cluster.hierarchy import dendrogram
from scipy.spatial.distance import pdist

fig = plt.figure(figsize=(7,7))
plt.subplot(2, 1, 1)
cm = matplotlib.cm.Blues
X = np.random.random([5,5])
pmat = pdist(X, "euclidean")
linkmat = linkage(pmat)
dendrogram(linkmat)
plt.subplot(2, 1, 2)
labels = ["a", "b", "c", "d", "e", "f"]
Y = np.random.random([6,6])
plt.xticks(arange(0.5, 7.5, 1))
plt.gca().set_xticklabels(labels)
plt.pcolor(Y)
plt.colorbar()

这产生了以下结果:

enter image description here

但问题在于轴不是正方形的,而且色条被认为是第二个子图的一部分。我希望它能够悬停在图外,并且使树状图框和热力图框都是正方形且彼此对齐(即大小相同)。

我尝试使用aspect='equal'调用subplot时来获得正方形轴,因为文档建议这样做,但这毁了图表,导致出现这种情况...

enter image description here

如果我尝试在每个子图后面使用plt.axis('equal')而不是aspect='equal',它会奇怪地将热力图变成正方形,但不是它的边界框(如下所示),同时破坏了树状图并且还混乱了xtick标签的对齐.... - 造成这样的混乱:

enter image description here

如何解决这个问题?总结一下,我想绘制一个非常简单的东西:顶部子图中的正方形树状图,底部子图中的正方形热力图,并带有右侧的色条。不需要花哨的东西。

最后,更一般的问题是:是否有一个通用的规则/原理来强制matplotlib始终使轴成为正方形?我想不出任何一个情况不需要正方形轴,但它通常不是默认行为。如果可能的话,我想强制所有图都是正方形的。

3个回答

15

aspect="equal" 意味着数据空间中相同长度在屏幕空间中也是相同的长度,但在你的顶部轴线上,x轴和y轴的数据范围不同,因此它不会成为一个正方形。要解决这个问题,可以将 aspect 设置为 x 轴范围与 y 轴范围的比率:

from scipy.cluster.hierarchy import linkage
from scipy.cluster.hierarchy import dendrogram
from scipy.spatial.distance import pdist
import matplotlib
from matplotlib import pyplot as plt
import numpy as np
from numpy import arange

fig = plt.figure(figsize=(5,7))
ax1 = plt.subplot(2, 1, 1)
cm = matplotlib.cm.Blues
X = np.random.random([5,5])
pmat = pdist(X, "euclidean")
linkmat = linkage(pmat)
dendrogram(linkmat)
x0,x1 = ax1.get_xlim()
y0,y1 = ax1.get_ylim()
ax1.set_aspect((x1-x0)/(y1-y0))
plt.subplot(2, 1, 2, aspect=1)
labels = ["a", "b", "c", "d", "e", "f"]
Y = np.random.random([6,6])
plt.xticks(arange(0.5, 7.5, 1))
plt.gca().set_xticklabels(labels)
plt.pcolor(Y)
plt.colorbar()

以下是输出结果:

在此输入图像描述

要定位颜色条,我们需要编写一个ColorBarLocator类,pad和width参数以像素为单位。

  • pad:设置轴与其颜色条之间的间距
  • width:颜色条的宽度

用以下代码替换plt.colorbar()

class ColorBarLocator(object):
    def __init__(self, pax, pad=5, width=10):
        self.pax = pax
        self.pad = pad
        self.width = width

    def __call__(self, ax, renderer):
        x, y, w, h = self.pax.get_position().bounds
        fig = self.pax.get_figure()
        inv_trans = fig.transFigure.inverted()
        pad, _ = inv_trans.transform([self.pad, 0])
        width, _ = inv_trans.transform([self.width, 0])
        return [x+w+pad, y, width, h]

cax = fig.add_axes([0,0,0,0], axes_locator=ColorBarLocator(ax2))
plt.colorbar(cax = cax)

在这里输入图片描述


谢谢你的回答,但是你的图表仍然没有像常规子图一样对齐。顶部和底部的边界框未对齐。我希望它们可以与颜色背景垂直对齐,向右倾斜,就像你现在有的那样,但不要对齐错误。你有什么想法吗? - user248237
谢谢你的回答 - 输出结果正是我想要的,但代码看起来非常复杂!有更简单的方法吗?好像只有 matplotlib 的开发人员才知道如何编写这样的东西,只是为了让一个颜色条对齐... - user248237
这里有一些例子,但我认为它们也很难理解:http://matplotlib.sourceforge.net/examples/axes_grid/demo_colorbar_with_inset_locator.html http://matplotlib.sourceforge.net/examples/axes_grid/demo_axes_divider.html - HYRY
我很熟悉那些例子,但我认为它们不适用于另一个子图中有树状图的情况,因为这会出现某些问题。 - user248237
你知道为什么Matplotlib会在底部子图中搞乱x轴标签的垂直对齐吗?为什么a, b, c, d, e, f不能对齐呢? - user248237

10

@HYRY的回答非常好,值得所有的赞誉。但是对于如何将正方形图表放置好,你可以通过欺骗matplotlib,使其认为两个图表都有colorbar,只让第一个隐形即可:

from scipy.cluster.hierarchy import linkage
from scipy.cluster.hierarchy import dendrogram
from scipy.spatial.distance import pdist
import matplotlib
from matplotlib import pyplot as plt
import numpy as np
from numpy import arange

fig = plt.figure(figsize=(5,7))
ax1 = plt.subplot(2, 1, 1)
cm = matplotlib.cm.Blues
X = np.random.random([5,5])
pmat = pdist(X, "euclidean")
linkmat = linkage(pmat)
dendrogram(linkmat)
x0,x1 = ax1.get_xlim()
y0,y1 = ax1.get_ylim()
ax1.set_aspect((x1-x0)/(y1-y0))

plt.subplot(2, 1, 2, aspect=1)
labels = ["a", "b", "c", "d", "e", "f"]
Y = np.random.random([6,6])
plt.xticks(arange(0.5, 7.5, 1))
plt.gca().set_xticklabels(labels)
plt.pcolor(Y)
plt.colorbar()

# add a colorbar to the first plot and immediately make it invisible
cb = plt.colorbar(ax=ax1)
cb.ax.set_visible(False)

plt.show()

代码输出


1

除了其他答案之外,您需要对.set_aspect的参数取绝对值:

x0,x1 = ax1.get_xlim()
y0,y1 = ax1.get_ylim()
ax1.set_aspect(abs(x1-x0)/abs(y1-y0))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接