FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Adding colorbar without affecting the plotting area size. · Issue #15010 · matplotlib/matplotlib · GitHub

Repository navigation

Adding colorbar without affecting the plotting area size. #15010

Closed

Description

I would like to have a .mplstyle that by default produces square plots, per discussion started in #15001 and I still think "square" plots would be a reasonable thing to be able to set as default.

tl;dr Want square plots of the same size with or without colorbar. What should change is the xdim of the figure.

Anyway, for a single plot this is easy setting figsize either live or in the style. This doesn't let me specify the box size other than manually, but I can live with that

x = np.linspace(0,1,51)
y = np.random.uniform(0,5,51)
z = np.random.normal(0,1,51)

fig = plt.figure(figsize=(5,5))
ax = fig.add_subplot()
ax.scatter(x, y, c = z);

It gets trickier if I want to add a colorbar as well, but I would like the main box size to not change. The simplest method as far as I can tell is with fig.colorbar()

fig = plt.figure(figsize=(5,5))
ax = fig.add_subplot()
sc = ax.scatter(x, y, c = z);
fig.colorbar(sc, ax=ax);

However, this modifies the main box to fit within figure and changes the aspect.

The other option is to use make_axes_locatable Which if I understand correctly better splits the current axes and some reason this results in a less screwed up aspect than the above, but still changes it.

from mpl_toolkits.axes_grid1 import make_axes_locatable
fig = plt.figure(figsize=(5,5))
ax = fig.add_subplot()
divider = make_axes_locatable(ax)
cax = divider.append_axes('right', size="7%", pad=0.2,)
sc = ax.scatter(x, y, c = z);
cbar = fig.colorbar(sc, cax=cax);

Current best solution I found is resizing figure afterwards based on a set axes size with @ImportanceOfBeingErnest 's answer https://stackoverflow.com/a/44971177. However, this is sub optimal because based on ticks/labels etc it doesn't always produce the same size output figure, which can be a problem when formatting a tex document later for example.

One solutions would be to have custom function(s) presetting the sizes manually like make_rgb_axes in https://matplotlib.org/3.1.1/gallery/axes_grid1/demo_axes_rgb.html which would get rid of the trial and error of setting the fig/ax size until it's just right. The obvious drawback of that is someone then wants a different size cbar or different padding it will screw up the layout and the fact that it needs to be packages somewhere else, which makes it more complicated for user to look up syntax/problems.

Activity

  1. jklymak commented on Aug 8, 2019

    Member

    If you want an automatic solution either the figure size stays constant and you resize the elements inside to fit, or the elements stay the same and you resize the figure. In general, Matplotlib assumes you want the figure to be a certain size and changes the size of elements to fit.

    It’s not possible for a given axes to know that other axes will have a colorbar. Constrained layout makes the axes have the same size regardless of whether they have a colorbar or not for a single figure. But of course it knows nothing of what you may have done in another figure.

    Making the plots square is something proposed in the previously noted PR. That’s pretty orthogonal to the rest of this discussion.

    Overall it seems you need a custom solution and as you noted before it’d probably be best as a fig, axs = mysubplotarrange(params) type method.

  2. andrzejnovak commented on Aug 8, 2019

    ContributorAuthor

    Making the plots square is something proposed in the previously noted PR. That’s pretty orthogonal to the rest of this discussion.

    Yes and no. If I could have a default figsize(7,5) and a default option of square axes aspect ratio, resulting in a (5,5) axes. Appending colorbar would not have to affect the main plot, it would just eat the extra x whitespace. Then if I want them side by side in a pdf. I can size them by height to get a consistent result.

  3. jklymak commented on Aug 8, 2019

    Member

    OK, so what more than #14917 is needed?

  4. andrzejnovak commented on Aug 8, 2019

    ContributorAuthor

    That's it for functionality, but I really think having an rcParam to be able to get square plots by default is reasonable, though I understand the concern of not having an infinitely sized config.

  5. jklymak commented on Aug 8, 2019

    Member

    OK, if you want to advocate for the functionality in #14917 to have an rcParam, we can have that discussion there or after that PR is merged. I'm -0.5 on this as something that belongs in rcParams, but others have more expansive ideas of how many things should be automated vs explicitly stated in user code.

    I'll close this as covered in #14917

  6. ImportanceOfBeingErnest commented on Aug 8, 2019

    Member

    It all depends on the constrains, so solutions may become arbitrarily complicated. IIUC, the constrains here are:

    • One or more square subplots
    • Arbitrary data ranges
    • A colorbar next to each subplot
    • The colorbar should have the same height as the axes it belongs to

    There is currently no inbuilt way to achieve this. Even the proposed #14917 would not automatically change the colorbar height. Also, inbuilt options in axes_grid1 do not allow for arbitrary data ranges. So the only solution for all those constrains I can think of is the following. It looks a bit complicated, but is copy-pastable without need to understand each detail.

    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])
    
    
    def make_square_axes_with_colorbar(ax, size=0.1, pad=0.1):
        """ Make an axes square, add a colorbar axes next to it, 
            Parameters: size: Size of colorbar axes in inches
                        pad : Padding between axes and cbar in inches
            Returns: colorbar axes
        """
        divider = make_axes_locatable(ax)
        margin_size = axes_size.Fixed(size)
        pad_size = axes_size.Fixed(pad)
        xsizes = [pad_size, margin_size]
        yhax = divider.append_axes("right", size=margin_size, pad=pad_size)
        divider.set_horizontal([RemainderFixed(xsizes, [], divider)] + xsizes)
        divider.set_vertical([RemainderFixed(xsizes, [], divider)])
        return yhax
    
    
    
    x = np.random.normal(512, 112, 240)
    y = np.random.normal(0.5, 0.1, 240)
    
    fig, ax = plt.subplots()
    
    sc = ax.scatter(x,y, c=y)
    
    cax = make_square_axes_with_colorbar(ax, size=0.15, pad=0.1)
    fig.colorbar(sc, cax=cax)
    
    plt.show()
    

    The power of this solution becomes clear when interactively resizing the figure - all constraints are kept.

  7. andrzejnovak commented on Aug 9, 2019

    ContributorAuthor

    @ImportanceOfBeingErnest This is pretty great, though I get some strange behaviour. Setting plt.subplots(figsize=(10,5))
    I get this with the function

    And this if I don't call the make_square_axes_with_colorbar()

    For some reason the y-size is visibly different, even though fig.get_size_inches() gives the initial numbers back in both cases.

    If it can be factored out into make_axes_square() and append_axes_not_changing_current_layout() or just specifically append_cbar_not_changing_current_layout() it would fit all the constraints. Either way would be fine if it required extra white space in the figure from the beginning or if it extend the figsize in the append direction.

  8. ImportanceOfBeingErnest commented on Aug 9, 2019

    Member

    The approach above does not change the figure size. I'm not sure I understand "axes_not_changing_current_layout" or "cbar_not_changing_current_layout", because adding a colorbar necessarily needs to change the layout (either by taking space from the axes, or adding the colorbar outside).
    In any case, the result you get seems to be a consequence of using bbox_inches="tight" when saving the figure (or using the %matplotlib inline backend, which does so automatically). So this would need to be turned off in case the same image size is desired.

  9. andrzejnovak commented on Aug 9, 2019

    ContributorAuthor

    Indeed seems, that was due to using jupyter. If I run the same code in a script, setting matplotlib.use('Agg') to be sure, the result is that the plot is outside of the plottable area.

    Indeed what changed was the main axes y dimensions, which is not what I want. Maybe I didn't describe it so well.

    Based on the discussed constraint on not being able to have a square fig by default I start with regular output of fig, ax = plt.subplots()

    After calling make_axes_square() this would be

    and after appending the cbar this would be

    Or maybe it would just be simpler to start with a square figsize like this:

    and for the cbar to extend the figure so that it fits in whithout modifying the main axes

  10. ImportanceOfBeingErnest commented on Aug 9, 2019

    Member

    I cannot reproduce the cut figure, it works fine, also when using agg backend.

    In case you don't want to show the colorbar you could just set it invisible (cax.set_visible(False)).

    I'm a bit confused about the rest here, because this all started off with the desire not to have the figure size changed, now the last proposal turns this around again and changes the figure size?

  11. andrzejnovak commented on Aug 9, 2019

    ContributorAuthor

    That's why I put up the illustrations. I realize now that "plotting area" in the title might have been interpreted as the figure size as opposed to the main axes size.

    It should be possible to get this two ways 1) think ahead and have a figsize that will accommodate the cbar if added (i.e square main axes with extra white space on the sides and then what i thought your function above would do or 2) extending the figsize in the direction where the cax is added.
    Does that make any sense?

    From the example, I took your function above and just tested different figsize, pad, and size

    x = np.random.normal(512, 112, 240)
    y = np.random.normal(0.5, 0.1, 240)
    
    fig, ax = plt.subplots(figsize=(10,5))
    
    sc = ax.scatter(x,y, c=y)
    cax = make_square_axes_with_colorbar(ax, size=1, pad=1)
    fig.colorbar(sc, cax=cax);
    fig.savefig('figtest.png')
    fig.savefig('figtest.pdf')
    

    I see the same effect regardless if png/pdf.

  12. ImportanceOfBeingErnest commented on Aug 9, 2019

    Member

    Are you by chance running this in PyCharm or some other environment? Which matplotlib version do you use?

  13. andrzejnovak commented on Aug 9, 2019

    ContributorAuthor

    I had it build from master, but just ran pip install --upgrade --force-reinstall matplotlib to x-check and I still see the same result. Version: 3.1.1. I am using conda and python3.7

  14. andrzejnovak commented on Aug 23, 2019

    ContributorAuthor

    Just for the record, if someone stumbles upon this, here's what I meant, implemented in the second approach. @ImportanceOfBeingErnest

    def append_axes(ax, size=0.1, pad=0.1, position="right", extend=False):
        fig = ax.figure
        bbox = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
        width, height = bbox.width, bbox.height
    
        divider = make_axes_locatable(ax)
        margin_size = axes_size.Fixed(size)
        pad_size = axes_size.Fixed(pad)
        xsizes = [pad_size, margin_size]
        if position in ['top', 'bottom']: xsizes = xsizes[::-1]
        yhax = divider.append_axes(position, size=margin_size, pad=pad_size)
    
        def extend_ratio(ax):
            ax.figure.canvas.draw()
            orig_size = ax.get_position().size
            new_size = 0
            for itax in ax.figure.axes:
                new_size += itax.get_position().size
    
            return new_size/orig_size
    
        if position in ["right"]:
            divider.set_horizontal([axes_size.Fixed(width)] + xsizes)
            fig.set_size_inches(fig.get_size_inches()[0] * extend_ratio(ax)[0],
                                fig.get_size_inches()[1])
        elif position in ["left"]:
            divider.set_horizontal(xsizes[::-1] + [axes_size.Fixed(width)])
            fig.set_size_inches(fig.get_size_inches()[0] * extend_ratio(ax)[0],
                                fig.get_size_inches()[1])
        elif position in ['top']:
            divider.set_vertical([axes_size.Fixed(height)] + xsizes[::-1])
            fig.set_size_inches(fig.get_size_inches()[0],
                                fig.get_size_inches()[1] * extend_ratio(ax)[1])
        elif position in ['bottom']:
            divider.set_vertical(xsizes + [axes_size.Fixed(height)])
            fig.set_size_inches(fig.get_size_inches()[0],
                                fig.get_size_inches()[1] * extend_ratio(ax)[1])
    
        return yhax
    

    Which will take you from

    to

  15. ImportanceOfBeingErnest commented on Aug 23, 2019

    Member

    That doesn't take the padding between axes and colorbar into account, right? I am a bit lost on what the ultimate purpose is here, I always thought it to be about square axes. But if you found something that suits your needs, I'm glad I could help.

  16. andrzejnovak commented on Aug 25, 2019

    ContributorAuthor

    Good point, but I think I can add that. Square axes, was my use case, but really the idea is to be generally able to append a cbar or ratio on the bottom and not changing the sizing of the main plot. If I take the two plots above and size them vertically in a document, the main plot will be the same size for both.

  17. 392781 commented on Nov 10, 2021

    2 years later, this is still a problem. Why is it that this still requires hacky solutions to work?

  18. souvikita commented on May 28, 2022

    I agree.. It is a problem even now!!! :(

  19. nmiles2718 commented on Oct 18, 2022

    If you really want to be clever and use the code base in an unintended way, you can just add the same axis to all your subplots and then hide all but the one you want to actually show the color bar. Obviously this is not desirable, but it works nonetheless.

    MWE

    import matplotlib.pyplot as plt
    from mpl_toolkits.axes_grid1 import make_axes_locatable
    import numpy as np
    
    fig, ax = plt.subplots(nrows=4, ncols=1, sharex=True, gridspec_kw={'hspace':0.05})
    imgs = []
    for i, a in enumerate(ax):
        im = a.imshow(np.random.randn(100,100), cmap='jet', origin='lower')
        imgs.append(im)
        divider = make_axes_locatable(a)
        cax = divider.append_axes('right', size='5%', pad='5%')
        if i <= 3:
            cax.set_axis_off()
    cbar = fig.colorbar(imgs[-1], cax=cax)
    plt.show()
  20. Laser-Cho commented on Oct 20, 2022

    yeah. it is still a problem.

  21. tacaswell commented on Oct 20, 2022

    Member

    Using the LayoutEngine API that we added in 3.6 (https://matplotlib.org/stable/api/layout_engine_api.html) it should now be possible to write a layout engine that can implement what @ImportanceOfBeingErnest did in a more generic way, however I expect there to be some surprises while implementing it (I suspect you will have to replace the transforms on the Axes which could get interesting).

    The core of the problem is that currently the absolute figure size (fig.get_size_inches()) is fixed and everything in the figure is positioned in a fractional coordinate system of the Figure (and things like fixed aspect are implemented as a second step of the layout that will adjust very late in the draw process). Additionally the elements in the figure do not actually know about each other in a meaningful way (the LayoutEngines know about all of the elements and can make global choices to re-position each of the elements).

    In contrast, what is being requested here is that the Axes have absolute sizes and positions and then the figure resizes to accommodate. This is reasonable, but goes against a lot of the assumptions in the mpl code base (and you can sort of fake this behavior with fig.savefig(..., bbox_inches='tight') (which is used by the matplotlib-inline backend)). As I said above, I think this can be done using LayoutEngine, but am not sure what other changes would be needed to make it "nice".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions


      Back | FazBrowse Home | New Git URL