复制 matplotlib 艺术家

2022-01-20 00:00:00 python python-3.x 复制 matplotlib

问题描述

I created an array of Line2D-objects with matplotlib that I'd like to use in various plots. However, using the same artist in multiple plots doesn't work since I get:

RuntimeError: Can not put single artist in more than one figure

As I found out, the artists once attached to an axis, cannot be attached to another anymore. Well, my idea was to simply copy the array containing the lines with copy() but it won't work. The copied array still refers to the same objects. I suppose, that is because you simply cannot copy artists (?).

Is there any way to avoid the recalculation of the Line2Ds and to only have to calculate them once?

解决方案

I so dearly wish there was a copy method in the Artist Class of matplotlibs!

Perhaps, someone wants to adapt the code below into the Artist Class in order to have a kind of transfer method to copy one Artist object into another.

Wrong way to go

Using the standard copy or deepcopy functions for objects in the package copy does not work.

For instance, it is useless to script:

import matplotlib.pyplot as plt
import numpy as np
import copy

x = np.linspace(0, 2*np.pi, 100)

fig  = plt.figure()
ax1 = plt.subplot(211)
ax2 = plt.subplot(212)

# create a Line2D object
line1, = ax1.plot(x, np.sin(x))

# copy the Line2D object 
line2 = copy.deepcopy(line1) #ERROR!

Solutions:

So, the only way I found to copy an Artist object, is to create an empty object of the desired type, and then transfer via loop all necessary attributes from the original object into the new created one.

The tool to transfer attributes values from one object to the other is this function I defined:

# -- Function to attributes copy
#It copies the attributes given in attr_list (a sequence of the attributes names as
#  strings) from the object 'obj1' into the object 'obj2'
#It should work for any objects as long as the attributes are accessible by
# 'get_attribute' and 'set_attribute' methods.

def copy_attributes(obj2, obj1, attr_list):
        for i_attribute  in attr_list:
            getattr(obj2, 'set_' + i_attribute)( getattr(obj1, 'get_' + i_attribute)() )

In this function the most important parameter is attr_list. This is a list of the names of the attributes that we want to copy from obj1 into obj2, for example, for an object Artist.Line2D, it could be attr_list = ('xdata', 'ydata', 'animated', 'antialiased', 'color', 'dash_capstyle', 'dash_joinstyle', 'drawstyle')

As any Artist object has different attributes, the key in this transfer process is to generate the list of attributes names with the right attributes to be transferred.

There are two ways to generate the list of the attributes names:

  • First option: we specify the attributes to be selected. That is, we hard code a list with all attributes we want to transfer. It is more arduous than the second option. We have to fully specify the attributes for every type of object: these are usually long lists. It is only recommendable, when we deal with only one type of Artist object.

  • Second option: we specify the attributes that are not selected. That is, we write an "exceptions list" with the attributes that we do not want to transfer, we automatically choose all transferable attributes of the object, but those in our "exception list". This is the quickest option, and we can use it with different types of Artist objects simultaneously.

First option: specifying the list of transferred attributes

We just write an assignment to define the list of attributes we want to transfer, as we have shown above.

The drawback of this option, is that it cannot be immediately extended to different Artist objects, for instance, Line2D and Circle. Because we have to hard code different lists of attributes names, one for each type of Artist object.

Full Example

I show an example for Line2D Artist class, as in the question specified.

import matplotlib.pyplot as plt
import numpy as np

# -- Function to attributes copy
#It copies the attributes given in attr_list (a sequence of the attributes names as
#  strings) from the object 'obj1' into the object 'obj2'
#It should work for any objects as long as the attributes are accessible by
# 'get_attribute' and 'set_attribute' methods.
def copy_attributes(obj2, obj1, attr_list):
    for i_attribute  in attr_list:
        getattr(obj2, 'set_' + i_attribute)( getattr(obj1, 'get_' + i_attribute)() )

#Creating a figure with to axes
fig  = plt.figure()
ax1 = plt.subplot(211)
ax2 = plt.subplot(212)
plt.tight_layout() #Tweak to improve subplot layout

#create a Line2D object 'line1' via plot
x = np.linspace(0, 2*np.pi, 100)
line1, = ax1.plot(x, np.sin(x))

ax1.set_xlabel('line1 in ax1') #Labelling axis

#Attributes of the old Line2D object that must be copied to the new object
#It's just a strings list, you can add or take away attributes to your wishes
copied_line_attributes = ('xdata', 'ydata', 'animated', 'antialiased', 'color',  
                    'dash_capstyle', 'dash_joinstyle', 
                    'drawstyle', 'fillstyle', 'linestyle', 'linewidth',
                    'marker', 'markeredgecolor', 'markeredgewidth', 'markerfacecolor',
                    'markerfacecoloralt', 'markersize', 'markevery', 'pickradius',
                    'solid_capstyle', 'solid_joinstyle', 'visible', 'zorder')

#Creating an empty Line2D object
line2 = plt.Line2D([],[])

#Copying the list of attributes 'copied_line_attributes' of line1 into line2
copy_attributes(line2, line1, copied_line_attributes)

#Setting the new axes ax2 with the same limits as ax1
ax2.set_xlim(ax1.get_xlim())
ax2.set_ylim(ax1.get_ylim())

#Adding the copied object line2 to the new axes
ax2.add_artist(line2)

ax2.set_xlabel('line2 in ax2') #Labelling axis

plt.show()

Output

Second Option: specifying the list of NOT transferred attributes

In this case, we specify the names of the attributes, that we do not want to transfer: we make a exception list. We gather automatically all transferable attributes of the Artist object and exclude the names of our exception list.

The advantage is that usually for different Artist objects the excluded attributes are the same short list and, consequently, this option can be more quickly scripted. In the example below, the list is as short as except_attributes = ('transform', 'figure')

The key function in this case is list_transferable_attributes as shown below:

#Returns a list of the transferable attributes, that is the attributes having
# both a 'get' and 'set' method. But the methods in 'except_attributes' are not
# included
def list_transferable_attributes(obj, except_attributes):
    obj_methods_list = dir(obj)

    obj_get_attr = []
    obj_set_attr = []
    obj_transf_attr =[]

    for name in obj_methods_list:
        if len(name) > 4:
            prefix = name[0:4]
            if prefix == 'get_':
                obj_get_attr.append(name[4:])
            elif prefix == 'set_':
                obj_set_attr.append(name[4:])

    for attribute in obj_set_attr:
        if attribute in obj_get_attr and attribute not in except_attributes:
            obj_transf_attr.append(attribute)

    return obj_transf_attr

Full Example

import matplotlib.pyplot as plt
import numpy as np

# -- Function to copy, or rather, transfer, attributes
#It copies the attributes given in attr_list (a sequence of the attributes names as
#  strings) from the object 'obj1' into the object 'obj2'
#It should work for any objects as long as the attributes are accessible by
# 'get_attribute' and 'set_attribute' methods.
def copy_attributes(obj2, obj1, attr_list):
    for i_attribute  in attr_list:
        getattr(obj2, 
                'set_' + i_attribute)( getattr(obj1, 'get_' + i_attribute)() )

# #Returns a list of pairs (attribute string, attribute value) of the given 
# # attributes list 'attr_list' of the given object 'obj'                
# def get_attributes(obj, attr_list):
#     attr_val_list = []
#     for i_attribute  in attr_list:
#         i_val = getattr(obj, 'get_' + i_attribute)()
#         attr_val_list.append((i_attribute, i_val))
#     
#     return attr_val_list

#Returns a list of the transferable attributes, that is the attributes having
# both a 'get' and 'set' method. But the methods in 'except_attributes' are not
# included
def list_transferable_attributes(obj, except_attributes):
    obj_methods_list = dir(obj)

    obj_get_attr = []
    obj_set_attr = []
    obj_transf_attr =[]

    for name in obj_methods_list:
        if len(name) > 4:
            prefix = name[0:4]
            if prefix == 'get_':
                obj_get_attr.append(name[4:])
            elif prefix == 'set_':
                obj_set_attr.append(name[4:])

    for attribute in obj_set_attr:
        if attribute in obj_get_attr and attribute not in except_attributes:
            obj_transf_attr.append(attribute)

    return obj_transf_attr


#Creating a figure with to axes
fig  = plt.figure()
ax1 = plt.subplot(211) #First axes
ax2 = plt.subplot(212) #Second axes
plt.tight_layout() #Optional: Tweak to improve subplot layout

#create an artist Line2D object 'line1' via plot
x = np.linspace(0, 2*np.pi, 100)
line1, = ax1.plot(x, np.sin(x))

#create an artist Circle object
circle1 = plt.Circle([1,0], 0.5, facecolor='yellow', edgecolor='k')
#Adding the object to the first axes
ax1.add_patch(circle1)

#Labelling first axis
ax1.set_xlabel('line1 and circle1 in ax1') 

#Methods that we should not copy from artist to artist
except_attributes = ('transform', 'figure')

#Obtaining the names of line2D attributes that can be transfered 
transferred_line_attributes = list_transferable_attributes(line1, except_attributes)

#Obtaining the names of Circle attributes that can be transfered
transferred_circle_attributes = list_transferable_attributes(circle1, except_attributes)

#Creating an empty Line2D object
line2 = plt.Line2D([],[])
circle2 = plt.Circle([],[])

#Copying the list of attributes 'transferred_line_attributes' of line1 into line2
copy_attributes(line2, line1, transferred_line_attributes)
copy_attributes(circle2, circle1, transferred_circle_attributes)

#attr_val_list_line2 = get_attributes(line2, line1_attr_list)

#Setting the new axes ax2 with the same limits as ax1
ax2.set_xlim(ax1.get_xlim())
ax2.set_ylim(ax1.get_ylim())

#Adding the copied object line2 to the new axes
ax2.add_line(line2) #.add_artist(line2) also possible
ax2.add_patch(circle2) #.add_artist(circle2) also possible

ax2.set_xlabel('line2 and circle2 in ax2') #Labelling axis

plt.show()

Output

相关文章