You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Document idiosyncratic behavior of transform kwarg #12551
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.
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
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()
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.
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.
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".
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)
changed the title [-]Using transform=None in imshow kwargs results in a blank plot[/-][+]Document idiosyncratic behavior of `transform` kwarg[/+]on Oct 18, 2018
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.
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.
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.
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.
The transforms tutorial is pretty clear that None is the same as the Identity.
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
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
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.
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.
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!
Activity
ImportanceOfBeingErnest commentedon Oct 17, 2018
The image is clipped by the axes. You will see it if you make your canvas squared and remove any margins.
I currently have no idea why
clip_on=Falsewould 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.
munkm commentedon Oct 17, 2018
Why is it that if I pass
transform=Noneto 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.anntzer commentedon Oct 17, 2018
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.transAxesinstead, see https://matplotlib.org/tutorials/advanced/transforms_tutorial.html#sphx-glr-tutorials-advanced-transforms-tutorial-py for more details.munkm commentedon Oct 17, 2018
Thank you @anntzer. That's helpful and I appreciate your explanation.
ImportanceOfBeingErnest commentedon Oct 17, 2018
The confusion is absolutely understandable. This is a semantic pitfall, unfortunately anchored deeply inside matplotlib. On the one hand,
argument=Noneis mostly interpreted as "use the default", e.g.imshow(..., cmap=None)==> use the default colormap (the one fromrcParams["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. theIdentityTransform)[-]Using transform=None in imshow kwargs results in a blank plot[/-][+]Document idiosyncratic behavior of `transform` kwarg[/+]tacaswell commentedon Oct 18, 2018
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
Figureobjects containsAxesobjects 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 theArtistsinstances, they do not yet know what Axes it will be put into so we start with the_transformattribute beingNone. The public interface to getting the transform of an artist isget_transform. To keep type stability, the accessor returnIdentityTransform(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 ifset_transformhas been called or not and if it has not been called, we do not set the transfrom. Callingart.set_transform(None)sets the the_transfromattribute toNonewhich 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**kwargsand pass them through to the next layer down. In the case of most the Artists we eventually callself.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 intransform=Noneas 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
transformif it is None, but it is not clear where the right place to do that is. Doing it inArtist.updateis 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 oftransformand 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.
anntzer commentedon Oct 19, 2018
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.
tacaswell commentedon Oct 19, 2018
If we deprecated
set_transformtakingNoneit 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.
anntzer commentedon Oct 19, 2018
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.
ImportanceOfBeingErnest commentedon Oct 19, 2018
The transforms tutorial is pretty clear that
Noneis the same as the Identity.It also says
In contrast, the set_transform must have been written by a purist
and could sure get some more detailed description.
In general, a problem is that kwargs are not (well) documented. In
imshowit saysIt'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 onAxesImageis 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 forimshow).In addition, some functions take a
bbox_transformargument, which could be easily confused with thetransformof theArtist. e.g.legend. Here, the behaviour is different,Nonewill default to thetransAxes. But it is clearly documented.For
axes_grid1.inset_locator.inset_axesthe behaviour is again different,Nonewill default to theIdentityTransform. 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.
17 remaining items
github-actions commentedon Oct 21, 2024
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!