Search code examples
pythonmatplotlibannotate

Draw ellipse to annotate on top of logarithmic scale


Using matplotlib, I would like to draw an ellipse on top of a semilogy plot.

Consider the following figure and the associated code.

import numpy as np
import scipy.io
from matplotlib import pyplot as plt
from matplotlib.patches import Ellipse

plt.figure(figsize=[3.3, 3.3])
plt.rcParams.update({'font.size': 8, 'text.usetex': True})

plt.semilogy([1, 2, 3, 4], [4, 8, 12, 16], color='r')
plt.semilogy([1, 2, 3, 4], [2, 4, 6, 8], color='r')
plt.semilogy([1, 2, 3, 4], [12, 15, 20, 27], color='b')

ax = plt.gca()
plt.annotate('blue curve', xy=(1.5, 22.5), xytext=(1.5, 22.5), ha='center', va='center')
plt.annotate('', xy=(2, 15), xytext=(1.5, 22), arrowprops=dict(width=0.1, headwidth=2, headlength=2, color='grey'))
plt.annotate('red curves', xy=(2.5, 22.5), xytext=(2.5, 22.5), ha='center', va='center')
plt.annotate('', xy=(3, 15), xytext=(2.5, 22), arrowprops=dict(width=0.1, headwidth=2, headlength=2, color='grey'))
ax.add_patch(Ellipse(xy=(3, 10), width=0.2, height=10, color="grey", fill=False, lw=1, zorder=5))

plt.grid()
plt.xlabel('x')
plt.ylabel('y')

plt.savefig('filename.pdf', format='pdf')

plt.show()

enter image description here

As you can see, the ellipse is deformed due to the logarithmic scale in the y axis. I have tried to overlap a new axis in natural scale and I have also tried to get inspiration from existing questions (such as this one): both approaches didn't work since, unfortunately, my knowledge of python is close to zero.

Is there an easy way to draw a (non deformed) ellipse without modifying much the existing code?


Solution

  • The answer in the Q&A you linked to does provide a solution to your problem. You want to use a composite transform as documented there. I have modified the relevant part of your script to do that.

    Note: the one change that was necessary using this method was that the height of the ellipse is given in axes coordinates (I used a height of 0.5) rather than in data coordinates as you had (height=10). There may be a way to give that in data coordinates with another transform, but I haven't included that here. I also slightly moved the ellipse centre so it was centred on the two red lines.

    from matplotlib.transforms import ScaledTranslation
    
    # Ellipse centre coordinates
    x, y = 3, 8
    
    # use the axis scale tform to figure out how far to translate 
    ell_offset = ScaledTranslation(x, y, ax.transScale)
    
    # construct the composite tform
    ell_tform = ell_offset + ax.transLimits + ax.transAxes
    
    # Create the ellipse centred on the origin, apply the composite tform
    ax.add_patch(Ellipse(xy=(0, 0), width=0.2, height=0.5, color="grey", fill=False, lw=1, zorder=5, transform=ell_tform))
    

    enter image description here