在 Matplotlib 中将文本旋转到对数刻度上的一条线上

Rotating text onto a line on a log scale in Matplotlib

问题

我正在处理对数刻度的数据,想旋转它以适合一条线。我知道模型,但不确定我应该插入什么角度 transform_angles 以恢复正确的旋转。经过一些试验和错误后,我知道答案是我需要的轴限制大约 10 度。

MWE

import matplotlib.pylab as plt
import numpy as np

plt.clf()
plt.yscale('log')
plt.ylim((1e-11, 1e-1))  # Other data is usually plotted and these are the ranges I need. 
plt.xlim((-0.2, 7.2))
x_fit = np.linspace(0.8, 3.2, 1000)
y_ols = (lambda x: np.exp(np.log(2)*(-20.8 + -1.23 * x)))(x_fit)  # I get these numbers from OLS fitting. 
plt.plot(x_fit, y_ols, 'b-', dashes='', label='__nolegend__')
plt.gca().text(np.min(x_fit), 1.2*y_ols[0], r'$O(2^{{ {:.3}x }})$'.format(-1.23), rotation=-10).set_bbox(dict(facecolor='w', alpha=0.7, edgecolor='k', linewidth=0))  # There are several others lines which have been omitted. 

类似问题(keeps text rotated in data coordinate system after resizing?) only use linear axes, as do the matplotlib demos.

Remarks on the plot to answer comments


  • In my full plot I use a dual axis (both on log scales) with the twinx() feature. All the data are plotted on ax1 which uses a log-10 scale (as shown). (I could be more explicit and write yscale('log', basey=10)...). Ultimately I want a base-10 axis.
  • The model used in making y_ols comes from a regression fit to some original data and requires base-2. On a log scale it is easy enough to recover the gradient in any required base.

使用渐变

使用 np.gradient 和使用 np.arctan 的角度(以弧度为单位)的混合,很容易恢复对数刻度的渐变,但我似乎无法恢复接近 10 度(0.17 弧度)的数字。

transData.transform_angles(np.array((np.mean(np.gradient(np.log10(y_ols), np.mean(np.diff(x_fit)))),)), np.array([np.min(x_fit), 1.2*y_ols[0]]).reshape((1, 2)), radians=True)[0]

给出 -1.6 弧度(大约 -90 度),而我需要一个更接近 0.17 弧度的数字。也许我应该使用不同的基础,或者我做错了(因此 post)。

附加功能 - 垂直偏移

从代码中可以看出,我在使用1.2*y_ols[0]时为锚点添加了垂直偏移。如果解决方案需要考虑到这一点,那就更好了。

对于 textbox 您使用哪种轴并不重要。您只需要将其角度调整为 figure 属性即可。为了更好地展示它,我将稍微更改您的 MWE。我将使用 y(x)=10^(2-x) 函数,并且在对数刻度上它应该提供具有 -45 度斜率的线性函数。如果您检查网格值,情况就是如此(函数为每个单位下降一个十进制)。但是,由于图形长宽比变形,视角不同(两个单位只有一个方格),你需要调整它。因此对于给定的图形,斜率的正确值为 -26.259 度。查看代码中adjusted_slope的值。

至于 textbox 的垂直偏移,您可以选择提供最佳视觉效果的值。

# you can set whatever size you need
plt.figure(figsize=(6, 4))

# these are the original settings
plt.yscale('log')
plt.ylim((1e-11, 1e-1))  
plt.xlim((-0.2, 7.2))

x_fit = np.linspace(0.8, 6.2, 100)
slope = -1.0
# a slight change in the function
y_ols = (lambda x: 10**(( -2 + slope * x)))(x_fit)  

plt.plot(x_fit, y_ols, 'b-', dashes='', label='__nolegend__')

# estimate the "right" slope 
calc_slope = np.mean(np.gradient(np.log10(y_ols), np.mean(np.diff(x_fit))))

# get current figure properties
x_min, x_max = plt.xlim()
y_min, y_max = plt.ylim()
x_sz, y_sz = plt.gcf().get_size_inches()
x_factor = x_sz / (x_max - x_min)
y_factor = y_sz / (np.log10(y_max) - np.log10(y_min))  # adjust to logarithmic values 

# calculate adjustment
adjusted_slope = (calc_slope * y_factor / x_factor)  # in radians

plt.gca().text(np.min(x_fit), 1.2*y_ols[0], r'$O(10^{{ {:.3}x }})$'.format(slope),
            rotation_mode='anchor', rotation=np.arctan(adjusted_slope)*180/np.pi).set_bbox(
                dict(facecolor='w', alpha=0.7, edgecolor='k', linewidth=0)) 

请注意,我提供了一个通用目的 class 来实现此目的作为对 original question 的回答。这将在轴限制更改或缩放事件等上自行更新。它也适用于对数刻度。


因此,在下文中,我将仅提供线性和对数刻度之间的比较,以帮助理解使用链接 matplotlib "text_rotation_relative_to_line" example 中的方法使用对数或线性刻度之间实际上没有任何区别。

您首先计算数据坐标中的角度。这可以很容易地用 numpy.arctan2 和前两个数据(或任何其他一对附近数据)的差异作为参数来完成。
然后使用 ax.transData.transform_angles 将数据坐标中给定的角度转换为屏幕坐标中的角度。

下面是同一案例在线性和对数尺度上的示例(从其他答案中获取数据)。

import matplotlib.pyplot as plt
import numpy as np

fig, (ax1, ax2) = plt.subplots(nrows=2, figsize=(6, 4), sharex=True)

ax2.set_yscale('log')
ax2.set(ylim=(1e-11, 1e-1), xlim=(-0.2, 7.2))

x = np.linspace(0.8, 6.2, 100)
y = (lambda x: 10**(( -2 - x)))(x)  

# angle in data coordinates
angle_data = np.rad2deg(np.arctan2(y[1]-y[0], x[1]-x[0]))

# Apply the exact same code to linear and log axes
for ax in (ax1, ax2):

    ax.plot(x, y, 'b-')

    # angle in screen coordinates
    angle_screen = ax.transData.transform_angles(np.array((angle_data,)), 
                                              np.array([x[0], y[0]]).reshape((1, 2)))[0]

    # using `annotate` allows to specify an offset in units of points
    ax.annotate("Text", xy=(x[0],y[0]), xytext=(2,2), textcoords="offset points", 
                rotation_mode='anchor', rotation=angle_screen)

plt.show()