Create equal aspect (square) plot with multiple axes when data limits are different?

前端 未结 2 2021
滥情空心
滥情空心 2020-12-17 01:27

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 o

相关标签:
2条回答
  • 2020-12-17 02:05

    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.

    0 讨论(0)
  • 2020-12-17 02:26

    The axes_grid1's Divider works a bit differently than usual subplots. It cannot directly cope with aspects, because the size of the axes is determined at draw time either in relative or absolute coordinates.

    If you want, you can manually specify the axes size in absolute coordinates to obtain a square subplot.

    import numpy as np
    import matplotlib.pyplot as plt
    from mpl_toolkits.axes_grid1 import make_axes_locatable, axes_size
    
    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)
    
    horiz = [axes_size.Fixed(2.8), axes_size.Fixed(.1), axes_size.Fixed(1)]
    vert = [axes_size.Fixed(2.8), axes_size.Fixed(.1), axes_size.Fixed(1)]
    divider.set_horizontal(horiz)
    divider.set_vertical(vert)
    
    ax.scatter(x, y)
    xhax.hist(x)
    yhax.hist(y, orientation="horizontal")
    
    plt.setp(xhax.get_xticklabels(), visible=False)
    plt.setp(yhax.get_yticklabels(), visible=False)
    
    plt.show()
    

    This solution is robust against figure size changes in the sense that the grid is always 2.8 + 0.1 + 1 = 3.9 inches wide and heigh. So one just needs to make sure the figure size is always large enough to host the grid. Else it might crop the marginal plots and look like this:

    To have an adaptive solution that would still scale with the figure size, one could define a custom Size, which takes the remainder of the absolutely sizes padding and marginal axes and returns the minimum of those in absolute coordinates (inches), for both directions such that the axes is always square.

    import numpy as np
    import matplotlib.pyplot as plt
    from matplotlib.transforms import Bbox
    from mpl_toolkits.axes_grid1 import make_axes_locatable, axes_size
    
    class RemainderFixed(axes_size.Scaled):
        def __init__(self, xsizes, ysizes, divider):
            self.xsizes =xsizes
            self.ysizes =ysizes
            self.div = divider
    
        def get_size(self, renderer):
            xrel, xabs = axes_size.AddList(self.xsizes).get_size(renderer)
            yrel, yabs = axes_size.AddList(self.ysizes).get_size(renderer)
            bb = Bbox.from_bounds(*self.div.get_position()).transformed(self.div._fig.transFigure)
            w = bb.width/self.div._fig.dpi - xabs
            h = bb.height/self.div._fig.dpi - yabs
            return 0, min([w,h])
    
    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)
    
    margin_size = axes_size.Fixed(1)
    pad_size = axes_size.Fixed(.1)
    xsizes = [pad_size, margin_size]
    ysizes = xsizes
    
    xhax = divider.append_axes("top", size=margin_size, pad=pad_size, sharex=ax)
    yhax = divider.append_axes("right", size=margin_size, pad=pad_size, sharey=ax)
    
    divider.set_horizontal([RemainderFixed(xsizes, ysizes, divider)] + xsizes)
    divider.set_vertical([RemainderFixed(xsizes, ysizes, divider)] + ysizes)
    
    ax.scatter(x, y)
    xhax.hist(x)
    yhax.hist(y, orientation="horizontal")
    
    plt.setp(xhax.get_xticklabels(), visible=False)
    plt.setp(yhax.get_yticklabels(), visible=False)
    
    plt.show()
    

    Note how the sizes of the marginals is always 1 inch, independent of the figure size how the scatter axes adjusts to the remaining space and stays square.

    0 讨论(0)
提交回复
热议问题