16

Is there an easy and reliable way to determine the current aspect ratio of an axes when its aspect is set to 'auto'?

The obvious thing to check is ax.get_aspect(), but that just returns 'auto'. I can set it to an arbitrary constant value by ax.set_aspect(aspect), after which that same constant is returned by ax.get_aspect(). By default (and very usefully) we have aspect = 'auto', in which case the aspect ratio is automatically calculated and adjusted to match the data limits and axes size.
How can I get the numeric aspect ratio that was automatically selected?

To clarify, this is neither the aspect ratio of the data limits returned by ax.get_data_ratio(), nor the aspect ratio of the display size of the figure or subplot returned by fig.get_figheight() / fig.get_figwidth() (for the figure). It's a bit subtle, as it depends on both the display size and the data limits. (Which can lead to confusing the different ratios and the reason I find it important to have it easily accessible.)

askewchan
  • 45,161
  • 17
  • 118
  • 134

4 Answers4

17

From the docs, the aspect ratio is the ratio of data-to-display scaling units in the x- and y-axes. i.e., if there are 10 data units per display in the y-direction and 1 data unit per display unit in the x-direction, the ratio would be 1/10. The circle would be 10 times wider than it is tall. This corresponds to the statement that an aspect of num does the following:

a circle will be stretched such that the height is num times the width. aspect=1 is the same as aspect=’equal’.

Based on the same original piece of code that you looked at (matplotlib.axes._base.adjust_aspect, starting around line 1405), I think we can come up with a simplified formula as long as you only need this ratio for linear Cartesian axes. Things get complicated with polar and logarithmic axes, so I will ignore them.

To reiterate the formula:

(x_data_unit / x_display_unit) / (y_data_unit / y_display_unit)

This happens to be the same as

(y_display_unit / x_display_unit) / (y_data_unit / x_data_unit)

This last formulation is just the ratio of the display sizes in the two directions divided by the ratio of the x and y limits. Note that ax.get_data_ratio does NOT apply here because that returns the results for the actual data bounds, not the axis limits at all:

from operator import sub
def get_aspect(ax):
    # Total figure size
    figW, figH = ax.get_figure().get_size_inches()
    # Axis size on figure
    _, _, w, h = ax.get_position().bounds
    # Ratio of display units
    disp_ratio = (figH * h) / (figW * w)
    # Ratio of data units
    # Negative over negative because of the order of subtraction
    data_ratio = sub(*ax.get_ylim()) / sub(*ax.get_xlim())

    return disp_ratio / data_ratio

Now let's test it:

from matplotlib import pyplot as plt
fig, ax = plt.subplots()
ax.set_aspect('equal')
print('{} == {}'.format(ax.get_aspect(), get_aspect(ax)))
ax.set_aspect(10)
print('{} == {}'.format(ax.get_aspect(), get_aspect(ax)))
Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • This works. I am surprised there is no easier way to do that. I wonder why `matplotlib` does not provide `get_aspect` function. – Pygmalion Feb 29 '20 at 18:12
  • @Pygmalion. It's a very niche application. – Mad Physicist Feb 29 '20 at 18:55
  • @MadPhysicist You need that value every time you want to draw a circle. I don't know if it's that niche. – Guimoute Jun 17 '20 at 21:55
  • @Guimoute. This is getting the aspect ratio. You just need `set_aspect('equal')` to draw a circle. So I'd say my comment aged reasonably well. – Mad Physicist Jun 17 '20 at 22:50
  • Yes but `set_aspect("equal")` is not a universal solution. In many cases you might want to work on circles in an already existing canvas with a given and constant aspect ratio. For example, I stumbled upon here because I'm writing a function to get the closest data point to my cursor, and of course I did not anticipate that `sqrt(dx² + dy²)` is not an acceptable definition of distance because of the changing data limits and figure ratios. I'm looking at a circle in display units which is an ellipsis in data units. **Edit:** so your solution is perfect for that and `dx /= get_aspect(ax)` works. – Guimoute Jun 17 '20 at 23:10
4

The best thing I can find is this:

def get_aspect(ax=None):
    if ax is None:
        ax = plt.gca()
    fig = ax.figure

    ll, ur = ax.get_position() * fig.get_size_inches()
    width, height = ur - ll
    axes_ratio = height / width
    aspect = axes_ratio / ax.get_data_ratio()

    return aspect

But it's surprisingly complicated, and I'm not sure if it is reliable under transforms, etc, as I know nothing about the bbox and transform objects.

askewchan
  • 45,161
  • 17
  • 118
  • 134
2

How about

import numpy as np
aspect = sum(np.abs(ax.get_xlim())) / sum(np.abs(ax.get_ylim()))
Frank Breitling
  • 942
  • 1
  • 10
  • 27
0

I am not sure if it does exactly what you want, but try it

    bbox = axis.get_window_extent().transformed(figure.dpi_scale_trans.inverted())
    aspect_ratio = bbox.width / bbox.height
Vladyslav Savchenko
  • 1,282
  • 13
  • 10