Ok, so after countless hours of research, getting stuck in all sorts of rabbit holes and circling through the same ideas (some of them documented here), I have finally given up for now. My last approach which I post here, was actually to manually hardcode functions that could retrieve the relevant information, depending on the type of artist. But even this approach ultimately failed, because:
using the convenience functions that are supposed to copy the properties from one artist to the next, such as artist.update_from()
unfortunately:
- either copy incompatible properties as well, i.e. if you run the method after you added the new artist to the other axes, it will raise errors
- or seemingly not copy any properties at all, as is the case with
AxesImages
e.g.
this means you would have to come up with your own way of individually copying this information as well. This would again be extremely cumbersome, but more importantly:
- For some artist types, it is simply impossible to retrieve all the necessary
args
and kwargs
at all from an instance. This is for example the case with FancyArrows
. Literally none of the kwargs and only two of the args that are used for the initialization of this class can be accessed again in any way from the artist instance itself. This is so incredibly frustrating.
In any case, here is my attempt at hardcoding the duplication methods, maybe they will be of use to somebody else.
duplicate.py
from matplotlib.patches import Ellipse, Rectangle, FancyArrow
from matplotlib.text import Annotation, Text
from matplotlib.lines import Line2D
from matplotlib.image import AxesImage
from matplotlib.artist import Artist
from matplotlib.axes import Axes
from matplotlib_scalebar.scalebar import ScaleBar
from typing import Callable
def _get_ellipse_args(ellipse: Ellipse) -> list:
return [ellipse.center, ellipse.width, ellipse.height]
def _get_rectangle_args(rectangle: Rectangle) -> list:
return [rectangle.get_xy(), rectangle.get_width(), rectangle.get_height()]
def _get_fancy_arroy_args(arrow: FancyArrow) -> list:
return [*arrow.xy, arrow.dx, arrow.dy]
def _get_scalebar_args(scalebar: ScaleBar) -> list:
return [scalebar.dx]
def _get_line2d_args(line: Line2D) -> list:
return line.get_data()
def _get_text_args(text: Text) -> list:
return []
def _get_annotation_args(text: Text) -> list:
return [text.get_text(), text.xy]
class Duplicator:
_arg_fetchers = {
Line2D: _get_line2d_args,
# Ellipse: _get_ellipse_args,
Rectangle: _get_rectangle_args,
Text: _get_text_args,
Annotation: _get_annotation_args,
ScaleBar: _get_scalebar_args,
AxesImage: lambda: None,
}
def args(self, artist):
return self._arg_fetchers[type(artist)](artist)
@classmethod
def duplicate(
cls, artist: Artist, other_ax: Axes,
duplication_method: Callable = None
) -> Artist:
if duplication_method is not None:
cls._arg_fetchers[type(artist)] = duplication_method
if type(artist) in cls._arg_fetchers:
if isinstance(artist, AxesImage):
duplicate = other_ax.imshow(artist.get_array())
# duplicate.update_from(artist) has no effect on AxesImage
# instances for some reason.
# duplicate.update(artist.properties()) causes an
# AttributeError for some other reason.
# Thus it seems kwargs need to be set individually for
# AxesImages.
duplicate.set_cmap(artist.get_cmap())
duplicate.set_clim(artist.get_clim())
else:
duplicate = type(artist)(*cls.args(cls, artist))
# this unfortunately copies properties that should not be
# copied, resulting in the artist being absent in the new axes
# duplicate.update_from(artist)
other_ax.add_artist(duplicate)
return duplicate
else:
raise TypeError(
'There is no duplication method for this type of artist',
type(artist)
)
@classmethod
def can_duplicate(cls, artist: Artist) -> bool:
return type(artist) in cls._arg_fetchers
duplicate_test.py
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
from matplotlib.text import Annotation
from matplotlib.lines import Line2D
from duplicate import Duplicator, _get_ellipse_args
fig, (ax1, ax2, ax3) = plt.subplots(1, 3)
# determine artists that were there before we manually added some
default_artists1 = set(ax1.get_children())
default_artists2 = set(ax2.get_children())
# add several artists to ax1
ax1.add_line(Line2D([0, 1], [2, 3], lw=4, color='red'))
ax1.add_patch(Ellipse((1, 1), 1, 1))
ax1.imshow(np.random.uniform(0, 1, (10, 10)))
ax2.add_patch(Ellipse((3, 5), 2, 4, fc='purple'))
ax2.add_artist(Annotation('text', (1, 1), fontsize=20))
# set axes limits, optional, but usually necessary
for ax in [ax1, ax2]:
ax.relim()
ax.autoscale_view()
ax2.axis('square')
for ax in [ax2, ax3]:
ax.set_xlim(ax1.get_xlim())
ax.set_ylim(ax1.get_ylim())
# determine artists that were added manually
new_artists1 = set(ax1.get_children()) - default_artists1
new_artists2 = set(ax2.get_children()) - default_artists2
new_artists = new_artists1 | new_artists2
# declare our own arg fetchers for artists types that may not be
# covered by the Duplicator class
arg_fetchers = {Ellipse: _get_ellipse_args}
# duplicate artists to ax3
for artist in new_artists:
if Duplicator.can_duplicate(artist):
Duplicator.duplicate(artist, ax3)
else:
Duplicator.duplicate(artist, ax3, arg_fetchers[type(artist)])
fig.show()

I really don't understand why, while matplotlib is adament about not re-using the same artist in different figures / axes (for which there is probably a good technical reason), they at the same time make it impossible, if not at least very awkward / hacky to move or copy artists.
Matplotlib gods, please, for all that is good, introduce a standard way of copying artists