Create equal aspect (square) plot with multiple axes when data limits are different?
matplotlib, python
Solution
One way to deal with the problem is to keep the data limits of the x and y axis equal. This can be done by normalising the values to be between, say, 0 and 1. This way the command `ax.set_aspect('equal')` works as expected. Of course, if one only does this, the tick labels will only range from 0 to 1, so one has to apply a little matplotlib magic to adjust the tick labels to the original data range. The answer here shows how this can be accomplished using a `FuncFormatter`. However, as the original ticks are chosen with respect to the interval [0,1], using a `FuncFormatter` alone will result in odd ticks e.g. if the factor is 635 an original tick of 0.2 would become 127. To get 'nice' ticks, one can additionally use a `AutoLocator`, which can compute ticks for the original data range with the `tick_values()` function. These ticks can then again be scaled to the interval [0,1] and then `FuncFormatter` can compute the tick labels. It is a bit involved, but in the end it only requires about 10 lines of extra code:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
from mpl_toolkits.axes_grid1 import make_axes_locatable
x = np.random.normal(512, 112, 240)
y = np.random.normal(0.5, 0.1, 240)
fig,ax=plt.subplots()
divider = make_axes_locatable(ax)
##increased pad from 0.1 to 0.2 so that tick labels don't overlap
xhax = divider.append_axes("top", size=1, pad=0.2, sharex=ax)
yhax = divider.append_axes("right", size=1, pad=0.2, sharey=ax)
##'normalizing' x and y values to be between 0 and 1:
xn = (x-min(x))/(max(x)-min(x))
yn = (y-min(y))/(max(y)-min(y))
##producinc the plots
ax.scatter(xn, yn)
xhax.hist(xn)
yhax.hist(yn, orientation="horizontal")
##turning off duplicate ticks (if needed):
plt.setp(xhax.get_xticklabels(), visible=False)
plt.setp(yhax.get_yticklabels(), visible=False)
ax.set_aspect('equal')
##setting up ticks and labels to simulate real data:
locator = mticker.AutoLocator()
xticks = (locator.tick_values(min(x),max(x))-min(x))/(max(x)-min(x))
ax.set_xticks(xticks)
ax.xaxis.set_major_formatter(mticker.FuncFormatter(
lambda t, pos: '{0:g}'.format(t*(max(x)-min(x))+min(x))
))
yticks = (locator.tick_values(min(y),max(y))-min(y))/(max(y)-min(y))
ax.set_yticks(yticks)
ax.yaxis.set_major_formatter(mticker.FuncFormatter(
lambda t, pos: '{0:g}'.format(t*(max(y)-min(y))+min(y))
))
fig.tight_layout()
plt.show()
The resulting picture looks as expected and stays square-shaped also upon resizing of the image.
Old Answer:
This is more a workaround than a solution:
Instead of using `ax.set_aspect()`, you can set up your figure such that it is a square by providing `figsize=(n,n)` to `plt.subplots`, where `n` would be the width and height in inches. As the height of `xhax` and the width of `yhax` are both 1 inch, this means that `ax` becomes square as well.
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
x = np.random.normal(512, 112, 240)
y = np.random.normal(0.5, 0.1, 240)
fig, ax = plt.subplots(figsize=(5,5))
divider = make_axes_locatable(ax)
xhax = divider.append_axes("top", size=1, pad=0.1, sharex=ax)
yhax = divider.append_axes("right", size=1, pad=0.1, sharey=ax)
ax.scatter(x, y)
xhax.hist(x)
yhax.hist(y, orientation="horizontal")
##turning off duplicate ticks:
plt.setp(xhax.get_xticklabels(), visible=False)
plt.setp(yhax.get_yticklabels(), visible=False)
plt.show()
The result looks like this:
Of course, as soon as you resize your figure, the square aspect will be gone. But if you already know the final size of your figure and just want to save it for further use, this should be a good enough quick fix.
Problem
I would like to create a square plot using multiple axes using `make_axes_locateable` as demonstrated in the matplotlib documentation. However, while this works on plots where the x and y data have the same range, it does not work when the ranges are orders of magnitude different. ``` import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.axes_grid1 import make_axes_locatable x = np.random.normal(512, 112, 240) y = np.random.normal(0.5, 0.1, 240) _, ax = plt.subplots() divider = make_axes_locatable(ax) xhax = divider.append_axes("top", size=1, pad=0.1, sharex=ax) yhax = divider.append_axes("right", size=1, pad=0.1, sharey=ax) ax.scatter(x, y) xhax.hist(x) yhax.hist(y, orientation="horizontal") x0,x1 = ax.get_xlim() y0,y1 = ax.get_ylim() ax.set_aspect(abs(x1-x0)/abs(y1-y0)) plt.show() ``` Although this code uses the `set_aspect` answer as in How do I make a matplotlib scatter plot square? the axes are not modified correctly as shown here: I attempted to repair this with: ``` ax.set_aspect(abs(x1-x0)/abs(y1-y0), share=True) ``` But this resulted in the following: Setting the aspect after calling scatter and before creating the two histogram axes seemed to have no effect, even though it appears that was done in the documentation example. This code does work when the data range is the same: Update: One of the key constraints of this question is to use `make_axes_locateable` and not `GridSpec` as discussed in the comments below. The problem I'm working on involves creating plotting functions that accept an Axes object to work on and modify it without having knowledge of the figure or any other Axes in the plot as in the following code: ``` import numpy as np import matplotlib.pyplot as plt import matplotlib.gridspec as grid from mpl_toolkits.axes_grid1 import make_axes_locatable, axes_size def joint_plot(x, y, ax=None): """ Create a square joint plot of x and y. """ if ax is None: ax = plt.gca() divider = make_axes_locatable(ax) xhax = divider.append_axes("top", size=1, pad=0.1, sharex=ax) yhax = divider.append_axes("right", size=1, pad=0.1, sharey=ax) ax.scatter(x, y) xhax.hist(x) yhax.hist(y, orientation="horizontal") x0,x1 = ax.get_xlim() y0,y1 = ax.get_ylim() ax.set_aspect(abs(x1-x0)/abs(y1-y0)) plt.sca(ax) return ax, xhax, yhax def color_plot(x, y, colors, ax=None): if ax is None: ax = plt.gca() divider = make_axes_locatable(ax) cbax = divider.append_axes("right", size="5%", pad=0.1) sc = ax.scatter(x, y, marker='o', c=colors, cmap='RdBu') plt.colorbar(sc, cax=cbax) ax.set_aspect("equal") plt.sca(ax) return ax, cbax if __name__ == "__main__": _, axes = plt.subplots(nrows=2, ncols=2, figsize=(9,6)) # Plot 1 x = np.random.normal(100, 17, 120) y = np.random.normal(0.5, 0.1, 120) joint_plot(x, y, axes[0,0]) # Plot 2 x = np.random.normal(100, 17, 120) y = np.random.normal(100, 17, 120) c = np.random.normal(100, 17, 120) color_plot(x, y, c, axes[0,1]) # Plot 3 x = np.random.normal(100, 17, 120) y = np.random.normal(0.5, 0.1, 120) c = np.random.uniform(0.0, 1.0, 120) color_plot(x, y, c, axes[1,0]) # Plot 4 x = np.random.normal(0.5, 0.1, 120) y = np.random.normal(0.5, 0.1, 120) joint_plot(x, y, axes[1,1]) plt.tight_layout() plt.show() ``` This question extends questions such as Set equal aspect in plot with colorbar and python interplay between axis('square') and set_xlim because of the Axes-only constraint.