Skip to content

[Bug]: Figure.add_artist() fails to re-render an image previously added via Figure.figimage() after .remove() #31730

Closed

Description

@williamlus

Bug summary

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.

Code for reproduction

import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from io import BytesIO

def text_to_rgba(s, *, dpi, **kwargs):
    fig = Figure(facecolor="none")
    fig.text(0, 0, s, **kwargs)
    with BytesIO() as buf:
        fig.savefig(buf, dpi=dpi, format="png", bbox_inches="tight",
                    pad_inches=0)
        buf.seek(0)
        rgba = plt.imread(buf)
    return rgba

# --- Case 1: figimage ---
fig, ax = plt.subplots()
rgba1 = text_to_rgba(r"IQ: $\sigma_i=15$", color="blue", fontsize=10, dpi=200)
figimg = fig.figimage(rgba1, 100, 50)
plt.savefig("figimage_before.png")   # image visible (correct)
figimg.remove()
fig.add_artist(figimg)
plt.savefig("figimage_after.png")    # image MISSING (bug)

# --- Case 2: text (control) ---
fig, ax = plt.subplots()
mytext = fig.text(0.2, 0.15, r"IQ: $\sigma_i=15$", color="blue", fontsize=20)
plt.savefig("text_before.png")       # text visible (correct)
mytext.remove()
fig.add_artist(mytext)
plt.savefig("text_after.png")        # text visible (correct)

Actual outcome

figimage_before.png

Image

figimage_after.png

Image

Expected outcome

figimage_before.png

Image

figimage_after.png

Image

Additional information

No response

Operating system

No response

Matplotlib Version

0.2.0.dev54524+unknown.ga94275af4

Matplotlib Backend

No response

Python version

No response

Jupyter version

No response

Installation

None

Activity

  1. timhoffm commented on May 22, 2026

    @timhoffm
    Member

    Likely cause: figimage adds the image to fig.images whereas add_artist adds it to fig.artists

    self.images.append(im)

    self.artists.append(artist)

  2. rcomer commented on May 22, 2026

    @rcomer
    Member

    The Artist tutorial encourages directly interacting with fig.lines. I guess you can similarly interact with fig.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).

  3. timhoffm commented on May 22, 2026

    @timhoffm
    Member

    The Artist tutorial encourages directly interacting with fig.lines. I guess you can similarly interact with fig.images? https://matplotlib.org/stable/tutorials/artists.html#figure-container

    👀 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.

  4. macayu17 commented on May 22, 2026

    @macayu17

    I can take a look at this.

    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.

  5. tacaswell commented on May 22, 2026

    @tacaswell
    Member

    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!

  6. rcomer commented on May 23, 2026

    @rcomer
    Member

    Two things stop the image from showing after it is re-added.

    • The image does not have its own transform, so add_artist sets one here
      if not artist.is_transform_set():
      artist.set_transform(self.transSubfigure)

      This can be prevented by figimg.set_transform(matplotlib.transforms.IdentityTransform()) before the call to add_artist.
    • The image and the axes have the same zorder (zero). So the order they are drawn in is determined by which list they are in
      def get_children(self):
      """Get a list of artists contained in the figure."""
      return [self.patch,
      *self.artists,
      *self._localaxes,
      *self.lines,
      *self.patches,
      *self.texts,
      *self.images,
      *self.legends,
      *self.subfigs]

      So if a user wants an image in the artists list to show above an axes, they need to modify the zorder of either the image or the axes.
  7. rcomer commented on May 23, 2026

    @rcomer
    Member

    👀 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.

  8. timhoffm commented on May 27, 2026

    @timhoffm
    Member

    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

    def add_line(self, line):
        _api.check_isinstance(mlines.Line2D, line=line)
        return self.add_artist(line)
    

    We can then discurage these methods in favor of a simple "add any artist through add_artist().

  9. sanrishi commented on Jul 22, 2026

    @sanrishi
    Contributor

    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?

  10. timhoffm commented on Jul 22, 2026

    @timhoffm
    Member

    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.

  11. rcomer commented on Jul 23, 2026

    @rcomer
    Member

    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.

  12. timhoffm commented on Jul 23, 2026

    @timhoffm
    Member

    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".

  13. sanrishi commented on Jul 23, 2026

    @sanrishi
    Contributor

    Hey chief @timhoffm

    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:

    if isinstance(artist, mimage.FigureImage):
        self.images.append(artist)
        artist._remove_method = self.images.remove
    else:
        self.artists.append(artist)
        artist._remove_method = self.artists.remove

    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!

  14. timhoffm commented on Jul 24, 2026

    @timhoffm
    Member

    @sanrishi as said in #31730 (comment)

    For this specific case, the diagnosis is at #31730 (comment), and the second part of that is fixed by #31746.

    Your proposal only addresses the second part, which will already be covered through #31746.

  15. rcomer commented on Jul 28, 2026

    @rcomer
    Member

    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.

  16. timhoffm commented on Jul 28, 2026

    @timhoffm
    Member

    Yes. I'd expect that None and IdentityTransform should yield the same result based on

    def get_transform(self):
    """Return the `.Transform` instance used by this artist."""
    if self._transform is None:
    self._transform = IdentityTransform()

  17. ahmed5145 commented on Sep 22, 2026

    @ahmed5145
    Contributor

    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.

  18. added a commit that references this issue on Oct 20, 2026
    06c917f
  19. added this to the v3.12.0 milestone on Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions