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
[Bug]: Figure.add_artist() fails to re-render an image previously added via Figure.figimage() after .remove() #31730
After calling .remove() on the FigureImage object returned by Figure.figimage(...) and then re-attaching it via Figure.add_artist(...), the image is no longer rendered in the saved figure. The same remove/re-add cycle works correctly for a Text artist created via Figure.text(...), suggesting figimage objects are not properly handled by Figure.add_artist.
What is the use case for removing and re-adding an artist? If you want to make it disappear and re-appear, you can toggle visibility, e.g. figimg.set_visible(False).
👀 I’m not convinced we should do or encourage this. While it can work in simple cases, we loose control over the content. With simple lists of artists, users would be in the responsibility of setting everything up right (e.g. transform), also they don’t allow for validation, caching/lazy evaluation etc. Just as an example, the meaningful introduction of layered drawing requires that we know whether something has changed.
My read is that this should be handled through Figure.add_artist() rather than asking users to mutate fig.images directly. I’d start with a small regression test for the figimage -> remove -> add_artist -> savefig case, then check whether FigureImage needs to be routed back through the figure image container instead of fig.artists.
If the preferred direction is different, e.g. documenting set_visible() as the supported approach instead of changing add_artist() behavior here, I can adjust before opening a PR.
The counter position is that I don't think this is something we should currently say is going to work reliably (see #24626 for an old attempt to prevent this that has not made it high enough up anyone's priority queue to deal with because it was mostly theoretical).
As Ruth said, if you just want to prevent an artist from drawing use art.set_visible(False).
👀 I’m not convinced we should do or encourage this. While it can work in simple cases, we loose control over the content. With simple lists of artists, users would be in the responsibility of setting everything up right (e.g. transform), also they don’t allow for validation, caching/lazy evaluation etc.
We deprecated directly mutating the artist lists on Axes a while ago, but apparently forgot to do that on Figure as well!
👀 I’m not convinced we should do or encourage this. While it can work in simple cases, we loose control over the content. With simple lists of artists, users would be in the responsibility of setting everything up right (e.g. transform), also they don’t allow for validation, caching/lazy evaluation etc.
We deprecated directly mutating the artist lists on Axes a while ago, but apparently forgot to do that on Figure as well!
So should we add add_line, add_image, etc for consistency with the Axes methods? Or should we tell users to use fig.add_artist for all of them for simplicity?
Edit: I think I would vote for just using add_artist. Most of the differences between the various Axes.add_* methods seem to be about autoscaling, which we of course don't need to worry about on a figure.
TL;DR: Don't introduce specific add_line/image/... for figures. add_artist is enough.
IMHO we should get rid of the specific add_line/image/... methods on Axes mid-term. It's confusing that there is a disctinction between add_line(line) and add.artist(line)most logic is the same and should be extracted, specialities should be dispatched internally based on type. There's a slight complication that currently,add_collectionhas the extraautolim` parameter, but 3.11 made collection behavior more consistent compared to other artists, so the need for that parameter will go away.
Ideally, at some point the specific methods are only thin wrappers like
i see everyone is saying remove+add_artist should not really work for FigureImage, better to use set_visible() and maybe cleanup figure artist lists later. so should i just update docs to recommend set_visible() or is there something else you guys want me to fix?
I have no preference whether this should work or not, but I thought the conclusion of #24626 was that we should make it work if and when it is reasonably easy to do so.
For this specific case, the diagnosis is at #31730 (comment), and the second part of that is fixed by #31746.
You are right, I advocated for a more nuanced approach there. My more defensive answer above was coming from limited time to dive into the topic. I'll rephrase my statement from there „re-adding is permitted but not well-tested behavior.“ to "re-adding behavoir is currently undefined. It is not prohibited, but not guaranteed to work for all artists".
I actually looked at the code now. Here's what I see:
Figure.text() appends to self.texts, Figure.figimage() appends to self.images, but Figure.add_artist() always appends to self.artists. The get_children() draw order is: artists -> _localaxes -> texts -> images.
So when you re-add a FigureImage via add_artist, it goes into fig.artists and gets drawn before the axes. Since FigureImage has zorder=0 (same as axes), the axes background covers it. That's why it looks "missing".
Text doesn't have this problem because it's positioned in figure coordinates and doesn't get obscured by axes backgrounds.
I think the right fix is to make add_artist route FigureImage back to fig.images, just like Figure.text() routes Text to fig.texts. It's a small isinstance check in add_artist:
This keeps the draw order correct and is consistent with how other figure-level artists are handled. The transform issue @rcomer mentioned is a non-issue since FigureImage.make_image() doesn't use self.get_transform() — it computes the bbox directly from ox, oy, and the array shape.
Happy to put up a PR with this fix + regression test if the group agrees!
Now that #31746 is merged, the example works as OP expects if we explicitly set figimg.set_transform(matplotlib.transforms.IdentityTransform()). We could make the figimage method set the IdentityTransform by default. I'm not sure whether there is a reason not to. No tests broke when I tried it. I note that figimage appears to be older than IdentityTransform.
Opened #32380 implementing @rcomer's suggestion: FigureImage now sets IdentityTransform on construction, so add_artist no longer replaces it with transSubfigure. Combined with #31746, the remove → add_artist round trip now draws correctly.
Activity
timhoffm commentedon May 22, 2026
Likely cause: figimage adds the image to
fig.imageswhereas add_artist adds it tofig.artistsmatplotlib/lib/matplotlib/figure.py
Line 3110 in dd8d78b
matplotlib/lib/matplotlib/figure.py
Line 526 in dd8d78b
rcomer commentedon May 22, 2026
The
Artisttutorial encourages directly interacting withfig.lines. I guess you can similarly interact withfig.images?https://matplotlib.org/stable/tutorials/artists.html#figure-container
What is the use case for removing and re-adding an artist? If you want to make it disappear and re-appear, you can toggle visibility, e.g.
figimg.set_visible(False).timhoffm commentedon May 22, 2026
👀 I’m not convinced we should do or encourage this. While it can work in simple cases, we loose control over the content. With simple lists of artists, users would be in the responsibility of setting everything up right (e.g. transform), also they don’t allow for validation, caching/lazy evaluation etc. Just as an example, the meaningful introduction of layered drawing requires that we know whether something has changed.
macayu17 commentedon May 22, 2026
I can take a look at this.
My read is that this should be handled through
Figure.add_artist()rather than asking users to mutatefig.imagesdirectly. I’d start with a small regression test for thefigimage -> remove -> add_artist -> savefigcase, then check whetherFigureImageneeds to be routed back through the figure image container instead offig.artists.If the preferred direction is different, e.g. documenting
set_visible()as the supported approach instead of changingadd_artist()behavior here, I can adjust before opening a PR.tacaswell commentedon May 22, 2026
The counter position is that I don't think this is something we should currently say is going to work reliably (see #24626 for an old attempt to prevent this that has not made it high enough up anyone's priority queue to deal with because it was mostly theoretical).
As Ruth said, if you just want to prevent an artist from drawing use
art.set_visible(False).We deprecated directly mutating the artist lists on
Axesa while ago, but apparently forgot to do that onFigureas well!rcomer commentedon May 23, 2026
Two things stop the image from showing after it is re-added.
add_artistsets one herematplotlib/lib/matplotlib/figure.py
Lines 529 to 530 in dd8d78b
This can be prevented by
figimg.set_transform(matplotlib.transforms.IdentityTransform())before the call toadd_artist.matplotlib/lib/matplotlib/figure.py
Lines 220 to 230 in dd8d78b
So if a user wants an image in the
artistslist to show above an axes, they need to modify the zorder of either the image or the axes.rcomer commentedon May 23, 2026
So should we add
add_line,add_image, etc for consistency with theAxesmethods? Or should we tell users to usefig.add_artistfor all of them for simplicity?Edit: I think I would vote for just using
add_artist. Most of the differences between the variousAxes.add_*methods seem to be about autoscaling, which we of course don't need to worry about on a figure.timhoffm commentedon May 27, 2026
TL;DR: Don't introduce specific
add_line/image/...for figures.add_artistis enough.IMHO we should get rid of the specific
add_line/image/...methods on Axes mid-term. It's confusing that there is a disctinction betweenadd_line(line)and add.artist(line)most logic is the same and should be extracted, specialities should be dispatched internally based on type. There's a slight complication that currently,add_collectionhas the extraautolim` parameter, but 3.11 made collection behavior more consistent compared to other artists, so the need for that parameter will go away.Ideally, at some point the specific methods are only thin wrappers like
We can then discurage these methods in favor of a simple "add any artist through
add_artist().sanrishi commentedon Jul 22, 2026
i see everyone is saying remove+add_artist should not really work for FigureImage, better to use set_visible() and maybe cleanup figure artist lists later. so should i just update docs to recommend set_visible() or is there something else you guys want me to fix?
timhoffm commentedon Jul 22, 2026
An artist that has been removed should be considered unusable.
We don't make any guarantees that such Artists can be reused again, because we do not clean up the Artist's state on removal.
Therefore remvoal is a terminal action. This should be documented in the tutorials and
remove().set_visible(False)has a similar aspect as a temporary removal and may be an alternative.rcomer commentedon Jul 23, 2026
I have no preference whether this should work or not, but I thought the conclusion of #24626 was that we should make it work if and when it is reasonably easy to do so.
For this specific case, the diagnosis is at #31730 (comment), and the second part of that is fixed by #31746.
timhoffm commentedon Jul 23, 2026
You are right, I advocated for a more nuanced approach there. My more defensive answer above was coming from limited time to dive into the topic. I'll rephrase my statement from there „re-adding is permitted but not well-tested behavior.“ to "re-adding behavoir is currently undefined. It is not prohibited, but not guaranteed to work for all artists".
sanrishi commentedon Jul 23, 2026
Hey chief @timhoffm
I actually looked at the code now. Here's what I see:
Figure.text()appends toself.texts,Figure.figimage()appends toself.images, butFigure.add_artist()always appends toself.artists. Theget_children()draw order is:artists->_localaxes->texts->images.So when you re-add a
FigureImageviaadd_artist, it goes intofig.artistsand gets drawn before the axes. SinceFigureImagehaszorder=0(same as axes), the axes background covers it. That's why it looks "missing".Textdoesn't have this problem because it's positioned in figure coordinates and doesn't get obscured by axes backgrounds.I think the right fix is to make
add_artistrouteFigureImageback tofig.images, just likeFigure.text()routesTexttofig.texts. It's a small isinstance check inadd_artist:This keeps the draw order correct and is consistent with how other figure-level artists are handled. The transform issue @rcomer mentioned is a non-issue since
FigureImage.make_image()doesn't useself.get_transform()— it computes the bbox directly fromox,oy, and the array shape.Happy to put up a PR with this fix + regression test if the group agrees!
timhoffm commentedon Jul 24, 2026
@sanrishi as said in #31730 (comment)
Your proposal only addresses the second part, which will already be covered through #31746.
rcomer commentedon Jul 28, 2026
Now that #31746 is merged, the example works as OP expects if we explicitly set
figimg.set_transform(matplotlib.transforms.IdentityTransform()). We could make thefigimagemethod set theIdentityTransformby default. I'm not sure whether there is a reason not to. No tests broke when I tried it. I note thatfigimageappears to be older thanIdentityTransform.timhoffm commentedon Jul 28, 2026
Yes. I'd expect that
NoneandIdentityTransformshould yield the same result based onmatplotlib/lib/matplotlib/artist.py
Lines 478 to 481 in d4975f9
ahmed5145 commentedon Sep 22, 2026
Opened #32380 implementing @rcomer's suggestion: FigureImage now sets IdentityTransform on construction, so add_artist no longer replaces it with transSubfigure. Combined with #31746, the remove → add_artist round trip now draws correctly.
Fix FigureImage losing its transform when re-added via add_artist