Rotating text onto a line on a log scale in Matplotlib

回眸只為那壹抹淺笑 提交于 2019-12-06 01:11:53

Note that I provided a general purpose class to achieve this as an answer to the original question. This will update itself on axes limits changes or zoom events etc. And it will work with log scales as well.


In the following I will hence only provide a comparisson between between linear and log scale to help understand that there isn't actually any difference between using a log or a linear scale with the approach from the linked matplotlib "text_rotation_relative_to_line" example.

You first calculate the angle in data coordinates. This can easily be done with numpy.arctan2 and the difference of the first two data (or any other pair of close-by data) as arguments.
Then you use ax.transData.transform_angles to transform the angle given in data coordinates to the angle in screen coordinates.

Below is an example (taking the data from the other answer) for the same case on a linear and a log scale.

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()

It does not really matters for the textbox which kind of axis you use. You only need to adjust its angle to the figure properties. To demonstrate it better, I will change a little your MWE. I will use y(x)=10^(2-x) function, and on logarithmic scale it should provide linear function with -45 degrees slope. And if you examine the grid values, it is the case (the function drops one decade for each unit). But, since the figure aspect ratio is deformed, the visual angle is different (only one square for two units), and you need to adjust to it. So for the given figure, the correct value of the slope is -26.259 degrees. See the value of adjusted_slope in the code.

As for the vertical offset of the textbox you can choose the value that provides best visual results.

# 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)) 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!