Skip to content

Document idiosyncratic behavior of transform kwarg #12551

Open

Description

@munkm

Bug report

Bug summary

Passing transform=None through imshow results in a blank plot.

I've been playing around a little bit with data transforms and projections in matplotlib. For one of the things I've been testing out, I also want to see how my data looks if the transform isn't set by the user, so I passed in transform=None as a kwarg to imshow. This resulted in a blank plot. I'm not sure if this is a preferred outcome, but I personally would have expected a plot without a transform set, but the data showing.

Code for reproduction

from numpy import random
import matplotlib.pyplot as plt

data = random.random((20,20))

fig, (ax1, ax2)  = plt.subplots(2)

ax1.imshow(data)
ax2.imshow(data, transform=None)
plt.show()

Outcome
image

Matplotlib version

  • Operating system: OSX Sierra 10.12.6
  • Matplotlib version: 2.2.2 and 3.0.0
  • Matplotlib backend (print(matplotlib.get_backend())): MacOSX
  • Python version: 3.6.5 and 3.6.6

I tried testing this script out in two different conda environments that I have on my machine with the same result. Both envs I tested this script out on use matplotlib pulled from conda-forge. One environment has v2.2.2 and the other has v3.0.0

Activity

  1. ImportanceOfBeingErnest commented on Oct 17, 2018

    @ImportanceOfBeingErnest
    Member

    The image is clipped by the axes. You will see it if you make your canvas squared and remove any margins.

    from numpy import random
    import matplotlib.pyplot as plt
    
    data = random.random((20,20))
    
    fig, ax = plt.subplots(figsize=(2,2))
    fig.subplots_adjust(0,0,1,1)
    
    im = ax.imshow(data, transform=None)
    plt.show()
    

    imwithouttrafo

    I currently have no idea why clip_on=False would not show it otherwise as well; there might be something special about images in this respect.

    I will close this as there does not seem to be any bug involved and transform=None does not seem like a use case that needs to be supported. Yet, if you feel that there is a need for this to work, please feel free to reopen.

  2. munkm commented on Oct 17, 2018

    @munkm
    Author

    Why is it that if I pass transform=None to imshow that radically changes the plot? This makes it a lot harder to write code that generically deals with transforms or cases where sometimes there might be a transform and sometimes not. I've been working on a feature for yt (which is linked above) and this seems like pretty reasonable behavior to expect.

  3. anntzer commented on Oct 17, 2018

    @anntzer
    Contributor

    Because that's what transform=None (which is interpreted as =IdentityTransform()) means.
    The transform indicates how to convert from data coordinates to screen (pixel) coordinates; when using IdentityTransform this literally means "the image data maps to the raw pixels".

    You likely want to use transform=ax.transAxes instead, see https://matplotlib.org/tutorials/advanced/transforms_tutorial.html#sphx-glr-tutorials-advanced-transforms-tutorial-py for more details.

  4. munkm commented on Oct 17, 2018

    @munkm
    Author

    Thank you @anntzer. That's helpful and I appreciate your explanation.

  5. ImportanceOfBeingErnest commented on Oct 17, 2018

    @ImportanceOfBeingErnest
    Member

    The confusion is absolutely understandable. This is a semantic pitfall, unfortunately anchored deeply inside matplotlib. On the one hand, argument=None is mostly interpreted as "use the default", e.g.

    • imshow(..., cmap=None) ==> use the default colormap (the one from rcParams["image.cmap"])

    on the other hand it is sometimes interpreted in the literal sense, "no such thing"

    • imshow(..., transform=None) ==> do not use any transform (i.e. the IdentityTransform)
  6. changed the title [-]Using transform=None in imshow kwargs results in a blank plot[/-] [+]Document idiosyncratic behavior of `transform` kwarg[/+] on Oct 18, 2018
  7. added this to the v3.0.0-doc milestone on Oct 18, 2018
  8. tacaswell commented on Oct 18, 2018

    @tacaswell
    Member

    The underlying issue here is how we handle kwargs, what 'default' means, and when we decide what it is.

    In the draw tree each artist is responsible for knowing what transform to use for it's data. This draw tree and and transforms are both hierarchical in the sense that the Figure objects contains Axes objects which contains Axis and all of the actual data Artists as children and the transform object on the Axes typically has a reference to the transform on the Figure and the data artists typically have a reference to the Axes transform (+ the scales, this is how changing from linear to log works for example and how moving where an Axes is placed in a Figure propagates down to tell the line which pixel it should color). However we also allow users to set what ever transform they want on the artists (this is super useful for things like putting text at a fixed location on an Axes so it does not move as you change the data rate or scale). Additionally, when we create the Artists instances, they do not yet know what Axes it will be put into so we start with the _transform attribute being None. The public interface to getting the transform of an artist is get_transform. To keep type stability, the accessor return IdentityTransform (which makes sense, you didn't tell us the transform so we go with the identity as the default). When we add the Artist to a parent (a Figure or an Axes) we set the transform on the new child from the new parent. However, we don't want to do this if the user has explicitly set the transform so we also track if set_transform has been called or not and if it has not been called, we do not set the transfrom. Calling art.set_transform(None) sets the the _transfrom attribute to None which restores the default behavior.

    So for so good? I think all of these make good sense and have decent semantics. The edge case here is the way we handle **kwargs. Almost all of our function take **kwargs and pass them through to the next layer down. In the case of most the Artists we eventually call self.update(kwargs) in the __init__ methods which more-or less does [getattr(self, f'set_{k}')(v) for k, v in kwargs.items()]. Thus, if you pass in transform=None as a keyword argument then the transform on the artist is set to the default value, but the internal state of the 'did the user explicitly set the transform' flag is flipped so that the when the image artist gets added to the Axes, the transform does not get reset.

    We could filter to ignore transform if it is None, but it is not clear where the right place to do that is. Doing it in Artist.update is probably not the right place (as it is part of the public API and we don't want to filter-out valid user input there) and if we do it sub-class level then we could end up in a worse situation where sometimes we have this odd behavior of transform and sometimes not.

    This is one of the more idiosyncratic behaviors of the library (as you can tell from my small novella attempting to explain it), but this is unfortunately the expected behavior and I don't think it can be changed.

    @munkm I'm sorry that the initial comments on this issue were short. This should be documented (where would you have expected to find this information? maybe in the transform tutorial? On every Aritst subclass seems over-kill). I have taken the liberty of re-naming the issue and re-opening it.

  9. anntzer commented on Oct 19, 2018

    @anntzer
    Contributor

    Actually, now that I'm reading this (nice) writeup, I guess a better question is whether we want None to mean IdentityTransform() in the general case. If we decide this behavior is too confusing, we can also just make set_transform() not accept None as input at all. Then the fact that we internally represent an unset transform with None would just be an implementation detail (we can then get rid of the additional, slightly confusing _transformSet flag, which becomes redundant), and we can even make .get_transform() raise an exception if the transform is currently unset.
    I think the proposed change can easily go through a deprecation path, it's just a question of whether the semantics are sufficiently better/clearer to be worth it.

  10. tacaswell commented on Oct 19, 2018

    @tacaswell
    Member

    If we deprecated set_transform taking None it would change the OP in to raising a confusing error instead of silently having confusing behavior which does seem like an improvement.

    What do people think about "conditional deprecation" like "we want to deprecate this so we are warning, but if this is going to be a problem for you let us know so we can re-consider". I think this would help to mitigate the risk of the unknown user base of some of the weird corners of the library.

  11. anntzer commented on Oct 19, 2018

    @anntzer
    Contributor

    The error doesn't even have to be confusing, we can word it appropriately.
    I'd just put in a normal deprecation warning and hopefully someone will complain if they're not happy with it, at which point we can reconsider.

  12. ImportanceOfBeingErnest commented on Oct 19, 2018

    @ImportanceOfBeingErnest
    Member

    The transforms tutorial is pretty clear that None is the same as the Identity.

    image

    It also says

    it is most common for artists placed in an axes or figure to have their transform set to something other than the IdentityTransform(); the default when an artist is placed on an axes using add_artist is for the transform to be ax.transData.

    In contrast, the set_transform must have been written by a purist

    image

    and could sure get some more detailed description.

    In general, a problem is that kwargs are not (well) documented. In imshow it says

    Other Parameters: | **kwargs : Artist properties
    image

    It's not actually clear what "Artist properties" are. It requires some experience to guess that everything that has a set_ in front of it may potentially be used as an argument. Clicking on AxesImage is a dead end, as that inherits from a private undocumented base class.

    So foremost the confusion is really that transform=... isn't documented anywhere (at least not specifically for imshow).

    In addition, some functions take a bbox_transform argument, which could be easily confused with the transform of the Artist. e.g. legend. Here, the behaviour is different,

    bbox_transform : None or matplotlib.transforms.Transform
    The transform for the bounding box (bbox_to_anchor). For a value of None (default) the Axes' transAxes transform will be used.

    None will default to the transAxes. But it is clearly documented.

    For axes_grid1.inset_locator.inset_axes the behaviour is again different,

    bbox_transform : matplotlib.transforms.Transform, optional
    Transformation for the bbox that contains the inset axes. If None, a transforms.IdentityTransform is used.

    None will default to the IdentityTransform. But it is clearly documented.

    All of this to say: Maybe solving the "confusion" problem is best be solved on a documentation level. And changes in the code base will most probably lead to more confusion, not less.

  13. 17 remaining items

  14. added this to the v3.6.2 milestone on Oct 6, 2022
  15. modified the milestones: v3.6.2, v3.6.3 on Oct 27, 2022
  16. modified the milestones: v3.6.3, v3.7.0 on Jan 11, 2023
  17. modified the milestones: v3.7.0, v3.7.1 on Feb 14, 2023
  18. modified the milestones: v3.7.1, v3.7-doc on Mar 4, 2023
  19. modified the milestones: v3.7-doc, v3.8-doc on Sep 9, 2023
  20. github-actions commented on Oct 21, 2024

    @github-actions

    This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help!

  21. added
    keepItems to be ignored by the “Stale” Github Action
    and removed
    status: inactiveMarked by the “Stale” Github Action
    on Oct 22, 2024
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

    DocumentationkeepItems to be ignored by the “Stale” Github Action

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions