Skip to content

[Bug]: Colorbar cannot be added to another figure #25871

Closed

Description

@Wrzlprmft

Bug summary

Calling the colorbar method of one Figure object does not attach the colour bar to that figure, but to the one where the object informing the colour scale resides.

Code for reproduction

from matplotlib.pyplot import subplots
import numpy as np

fig1, axes1 = subplots()
fig2, axes2 = subplots()
fig1.suptitle("Figure 1")
fig2.suptitle("Figure 2")
image1 = axes1.imshow(np.random.random((9,9)))
image2 = axes2.imshow(np.random.random((9,9)))
fig2.colorbar(image1)

Actual outcome

The colour bar gets attached to Image 1:

colorbar_bug_1 colorbar_bug_2

Expected outcome

The colour bar should get attached to Image 2. At the very least there should be an error.

Matplotlib Version

3.7.1

Installation

pip

Activity

  1. added this to the v3.8.0 milestone on May 12, 2023
  2. added
    🌱 Good first issueOpen a pull request against these issues if there are no active ones!
    Difficulty: Mediumhttps://matplotlib.org/devdocs/devel/contribute.html#good-first-issues
    on May 12, 2023
  3. tacaswell commented on May 12, 2023

    @tacaswell
    Member

    oh, that should definitly raise! On consideration this should warn.

    I suspect that that is going on here is because you did not specify an Axes to put the color bar in it is asking the AxesImage object about it's containing Axes and then stealing space from that Axes to put in the color bar. It then (surprisingly!) does the right thing and steals the space from the Axes containing the image in the other figure and then continues to do the right thing and puts the colorbar in that Axes!

    I think the correct path here is to, if we have been asked to steal space, verify that the axes we are going to steal from is in the (Sub)Figure we are calling it from.

    If we want to flatly error of the image is not an (eventual) child of the figure I think we will have to go through a warning cycle as I have a strong suspicion that users have found they can use this to put colorbars in a different figure for "reasons". On one hand, I do not think this is something we meant to support and conceptually is a bit fishy to me, but on the other hand we should be cautious about breaking users for "that works and we do not think it should" reasons.

    Labeling as good first issue because I think than when stealing space that this "works" is a bug (which is clear) so the work is

    • add a check before we steal space
    • add a test that it fails

    however medium difficulty as I think that a more through analysis of my comments above for "should we always raise?!" needs to be done and a plan for deprecation of that needs to be made.

  4. tacaswell commented on May 12, 2023

    @tacaswell
    Member

    Also, if you change the images to have different color maps:

    from matplotlib.pyplot import subplots
    import numpy as np
    
    fig1, axes1 = subplots()
    fig2, axes2 = subplots()
    fig1.suptitle("Figure 1")
    fig2.suptitle("Figure 2")
    image1 = axes1.imshow(np.random.random((9,9)), cmap='magma')
    image2 = axes2.imshow(np.random.random((9,9)))
    fig2.colorbar(image1)

    it is clear that it is "doing the right thing", just a very surprising thing!

  5. Wrzlprmft commented on May 12, 2023

    @Wrzlprmft
    Author

    I have a strong suspicion that users have found they can use this to put colorbars in a different figure for "reasons".

    How so? Right now they cannot. This actually what caused me to write this report.

    As for “doing the right thing”: If I wanted Matplotlib to guess what I want, I would use that legacy interface. Instead I want Matplotlib to do what it’s told (or at least tell me that it refuses).

  6. tacaswell commented on May 12, 2023

    @tacaswell
    Member

    It is doing exactly what you told it to. If you do not pass cax or ax then Figure.colorbar will steal space from the Axes that the scalar mappable is in.

    If you do

    import matplotlib.pyplot as plt
    import numpy as np
    
    
    fig1, axes1 = plt.subplots()
    fig2, (axes2, cax) = plt.subplots(2)
    fig1.suptitle("Figure 1")
    fig2.suptitle("Figure 2")
    image1 = axes1.imshow(np.random.random((9,9)), cmap='magma')
    image2 = axes2.imshow(np.random.random((9,9)))
    fig2.colorbar(image1, cax=cax)

    you get
    so2
    so1

    and if you do

    import matplotlib.pyplot as plt
    import numpy as np
    
    
    fig1, axes1 = plt.subplots()
    fig2, axes2 = plt.subplots()
    fig1.suptitle("Figure 1")
    fig2.suptitle("Figure 2")
    image1 = axes1.imshow(np.random.random((9,9)), cmap='magma')
    image2 = axes2.imshow(np.random.random((9,9)))
    fig2.colorbar(image1, ax=axes2)

    you get

    so2
    so1

    The docstrings on colorbar is not the most clear: https://matplotlib.org/stable/api/figure_api.html#matplotlib.figure.Figure.colorbar but for ax it says these are the axes that will have space stolen from them if cax is not set (and it is missing that if ax is not passed the axes of the mappable will be used).

  7. anntzer commented on May 12, 2023

    @anntzer
    Contributor

    Sorry to slightly hijack the thread here, but I think this may be a good opportunity to discuss whether colorbar() could rather be a method on the ScalarMappable rather than on the Figure (we can always keep figure.colorbar(...) available "forever", of course, for backcompat).
    Indeed, in an call like fig.colorbar(mappable) or fig.colorbar(mappable, ax=...) or fig.colorbar(mappable, cax=...), fig plays no role at all (*): the axes where the colorbar ends up is entirely determined by the mappable's axes (or by the ax kwarg, or the cax kwarg.
    (*) Actually I lied a bit: we currently check whether there's a layout engine on fig (but that actually seems more like a bug; we should really check that on mappable.axes.figure).

  8. Wrzlprmft commented on May 13, 2023

    @Wrzlprmft
    Author

    It is doing exactly what you told it to. If you do not pass cax or ax then Figure.colorbar will steal space from the Axes that the scalar mappable is in.

    The problem here is that colorbar being a method of Figure strongly suggests that you are adding your colour bar to the respective figure. As that would be a reasonable behaviour, I had seen to reason to question it and look into the documentation. In general, that’s fine because a good interface is as self-explanatory as possible. So, the documentation was not the problem (though the improvements were fine), the object structure was. Instead of colorbar being a method of Figure, there should be something like:

    • axes.make_colorbar(image) instead of fig.colorbar(image,cax=axes)
    • axes.add_colorbar(image) instead of fig.colorbar(image,ax=axes)

    … or like @anntzer suggested:

    • image.colorbar() instead of fig.colorbar(image)
    • image.colorbar(use_axes=axes) instead of fig.colorbar(image,cax=axes)
    • image.colorbar(add_to=axes) instead of fig.colorbar(image,ax=axes)
  9. jklymak commented on May 13, 2023

    @jklymak
    Member

    Matplotlib explicitly allows stealing space from multiple axes:

    fig, axs = plt.subplots(2, 2)
    im = axs[0].imshow(np.random.randn(10, 10), vmin=-1, vmax=-1)
    for i in range(1, 4):
        axs[i].imshow(np.random.randn(10, 10), vmin=-1, vmax=-1)
    fig.colorbar(im, ax=axs)

    which if you didn't have this, you get the colorbar attached to one axes that is then forced to be smaller than the rest.

    I wouldn't object to mappable.colorbar or axes.colorbar as shortcuts to figure.colorbar, but the basic API should be via (sub)figure.

  10. anntzer commented on May 13, 2023

    @anntzer
    Contributor

    Let's perhaps try to keep the discussion on API design at #25880, but to answer here quickly: I agree that due to the "steal from multiple axes case", having an axes method doesn't really work, but a method on mappables works just fine (image.colorbar(ax=axs)).

  11. klorine28 commented on Jun 29, 2023

    @klorine28

    is this issue still open and it would seem to me that now the issue may be a bit different can I get some clarification into what needs to be done? this would be my first issue here so id like to properly understand the issue.

  12. Sc1xya commented on Jun 30, 2023

    @Sc1xya

    It's easy to solve this problem, you can get the colorbar attached to both of the two figures.

    from matplotlib.pyplot import subplots, show
    import numpy as np
    
    fig1, axes1 = subplots()
    fig2, axes2 = subplots()
    fig1.suptitle("Figure 1")
    fig2.suptitle("Figure 2")
    image1 = axes1.imshow(np.random.random((9,9)))
    image2 = axes2.imshow(np.random.random((9,9)))
    fig2.colorbar(image1)
    fig1.colorbar(image2)
    show()
    
  13. Wrzlprmft commented on Jun 30, 2023

    @Wrzlprmft
    Author

    It's easy to solve this problem, you can get the colorbar attached to both of the two figures.

    That’s just two images with their own colour bars. None of the images has the colour bar belonging to the other. Also, this issue is not about solving a particular problem, but the interface not acting as expected.

  14. jklymak commented on Jun 30, 2023

    @jklymak
    Member

    Is there a practical issue here?

  15. tacaswell commented on Jun 30, 2023

    @tacaswell
    Member

    I think we should warn if what ends up being the cax is in a different figure than the figure it is called from (with maybe some forgiveness for SubFigure sharing).

    I do not think there is a bug, just a surprising behavior due to way the API evolved.

  16. removed this from the v3.8.0 milestone on Sep 15, 2023
  17. tacaswell commented on Sep 18, 2023

    @tacaswell
    Member

    I think that this is still a live issue and the work is:

    • implement a way to detect of the mappable is in a different Figure than the one fig.colorbar is being called on (we should forgive mappables with no host Figure).
    • warn if there is a miss-match
    • add a test that we warn
  18. Mihirshrivas commented on Oct 3, 2023

    @Mihirshrivas

    The colorbar attached to both figure , by the following code 👍
    from matplotlib.pyplot import subplots, show
    import numpy as np

    fig1, axes1 = subplots()
    fig2, axes2 = subplots()
    fig1.suptitle("Figure 1")
    fig2.suptitle("Figure 2")
    image1 = axes1.imshow(np.random.random((9,9)))
    image2 = axes2.imshow(np.random.random((9,9)))
    fig2.colorbar(image1)
    fig1.colorbar(image2)
    show()

  19. timhoffm commented on Mar 5, 2024

    @timhoffm
    Member

    We do not formally assign issues. You can leave a note that you are working on this to make people aware this is looked into (which you’ve done with your comment 😃 👍 ). Please open a pull request if you have a solution.

  20. MohitHire commented on Mar 5, 2024

    @MohitHire

    ok thanks for guidance, one more help I want that
    I am not getting file address of this bug, In which file/folder need to make commit please help me with that

    Screenshot 2024-03-05 at 10 55 41 PM
  21. rcomer commented on Mar 5, 2024

    @rcomer
    Member

    Actually it looks like the task outlined at #25871 (comment) was completed at #27458. Is there anything more to do here?

  22. MohitHire commented on Mar 5, 2024

    @MohitHire

    I am not able getting in which file should I make Pull request for this issue

  23. added this to the v3.9.0 milestone on Mar 5, 2024
  24. tacaswell commented on Mar 5, 2024

    @tacaswell
    Member

    I agree With @rcomer , this was addressed by #27458 but was not properly xrefed, closing as done.

    @MohitHire Sorry for the confusion, please have a look at another issue. In general, at least half of the work of fixing a bug is sorting out where to fix the bug so it is not always trivial to predict which file to start editing.

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

    Difficulty: Mediumhttps://matplotlib.org/devdocs/devel/contribute.html#good-first-issues🌱 Good first issueOpen a pull request against these issues if there are no active ones!

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions