3

Problem

I am working with data on a logarithmic scale and would like to rotate it to fit a line. I know the model but am unsure quite what angle I should be plugging into transform_angles to recover the correct rotation. After a bit of trial and error I know the answer is around 10 degrees for the axes limits I require.

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. 

enter image description here

Similar questions (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.

Using gradients

It is easy enough to recover the gradient on a logarithmic scale, using a mix of np.gradient and an angle (in radians) using np.arctan, but I can't seem to recover a number close to the 10 degrees (0.17 radians).

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]

gives -1.6radians (approximately -90 degrees), whereas I require a number closer to 0.17radians. Perhaps I should be using a different base, or I am doing this all wrong (hence the post).

Extras - vertical offset

As can be seen in the code, I have added a vertical offset for the anchor point when using 1.2*y_ols[0]. If a solution needs to take this into consideration then all the better.

Community
  • 1
  • 1
oliversm
  • 1,771
  • 4
  • 22
  • 44
  • There are couple of issues you need to clarify: 1) You are setting y-scale to be logarithmic but the base is 10 (`plt.yscale('log')`). You are then using `np.log(2)` which is of base `e`. And then you are using `np.log10(y_ols..` which is base 10. Moreover -1.6 is in radians, you need to convert it to degrees for comparison – Sheldore Oct 22 '18 at 12:00
  • @Bazingaa, I will add a bit more clarification to the OP shortly. 1) Log-10 scale is desired (in my full code I use a dual axis, one base 10, the other base-2, using twin axes). 2) The reason for the `exp` and `log` is because I desire the fitted parameters (slope and intercept) for the model in base-2 for comparison to theory. 3) I thought trying to work out the gradient in base-10 might be suitable, but as the OP shows I am unclear about how to achieve this yet. 3) Yes, -1.6 radians, (in degrees for rotation I need about -0.17 radians ~ -10 degrees). – oliversm Oct 22 '18 at 12:46

2 Answers2

6

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

enter image description here

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • I now also gave an answer to the [original question](https://stackoverflow.com/a/53111799/4124317), which is directly applicable here. – ImportanceOfBeingErnest Nov 02 '18 at 02:12
  • [I have tried to repeat this for very large and small angles](https://stackoverflow.com/q/53434558/5134817) when computed on a log scale, but find my results are getting incorrectly rounded. Any suggestions? – oliversm Nov 22 '18 at 15:58
0

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

enter image description here

igrinis
  • 12,398
  • 20
  • 45