Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to apply a persistent coordinate transformation to Matplotlib Patches?

I am a bit confused by matplotlib coordinate system (using it in a Jupyter notebook for interactive animation). If you create a patch, say a Circle, and translate it, using set_transform() I'm finding that the transformation is not persistent, meaning that if you apply the transformation again with the same (x,y) shifts the circle will not move because it appears that the Circle position does not get updated after a transformation, so a subsequent transform is applied to the same original patch position. My question is How do I apply a transformation that actually not only moves the patch after it's applied, but also updates the patch position?. Say I want to apply a series of translations, then the circle should move around NOT wiggle back and forth from it's original position. Here's an example code:

%import matplotlib
%matplotlib notebook

fig = plt.figure()
ax = fig.add_subplot(111, xlim=(-10, 10), ylim=(-10, 10))
c = ax.add_patch(plt.Circle((x, y), radius=0.5)
c.set_transform(ax.transData + mpl.transforms.Affine2D().translate(-5,-5))
c.set_transform(ax.transData + mpl.transforms.Affine2D().translate(10,10))

if you run this, you'll see that commenting out the first translation will not affect the final position of the circle. I would have expected that the final position of the circle center to be (5,5) NOT (10,10). This means that the transformation does not actually update the patch (circle) position; it just translates it in the figure/axes.

Question 2: Another thing I find confusing is that the circle produced by the code above does seem to have a radius of 0.5 as requested, however after applying a translation by (10,10) it is translated much less in the figure!! as if the translation shifts are scaled down by some factor before they are applied!! I have no explanation for this and it just shows that I do not understand matplotlib coordinate systems and transformations.

On the other hand, objects produced by plot(), which are Line2D objects, can be translated via the set_data() method, which updates the object's position as follows (assuming fig, and ax objects from code segment above):

L, = ax.plot(0, 0, 'ro', ms=8)
sx = 10 # shift in x
sy = 10 # shift in y
L.set_data(L.get_data()[0] + sx, L.get_data()[1] + sy)

I'm not sure how to do the same for matplotlib patches?

like image 450
Kai Avatar asked Feb 27 '17 18:02

Kai


1 Answers

The ax.transData transform converts data coordinates to display coordinates.

When you add transforms together, they are applied from left to right. So

ax.transData + mpl.transforms.Affine2D().translate(-5,-5)

first converts from data to display coordinates, then translates by an offset of (-5, -5) in display coordinates (pixels).

In contrast,

mpl.transforms.Affine2D().translate(-5,-5) + ax.transData

would first shift by (-5, -5) in data coordinates, and then convert data coordinates to display coordinates.


Since transforms can be composed by addition, to update a transformation use

transform = transform + mpl.transforms.Affine2D().translate(...)

For example,

import matplotlib.pyplot as plt
import matplotlib as mpl

fig = plt.figure()
ax = fig.add_subplot(111, xlim=(-10, 10), ylim=(-10, 10))
x, y = 0, 0
c = ax.add_patch(plt.Circle((x, y), radius=5))
transform = mpl.transforms.Affine2D().translate(-5,-5)
transform += mpl.transforms.Affine2D().translate(10,10)
c.set_transform(transform+ax.transData)
ax.set_aspect('equal')
plt.show()

enter image description here

The center of the circle is at (5,5), as expected, since (x,y)+(-5,-5)+(10,10) = (5,5).

Alternatively, instead of composing transforms, you could compute (or keep track of) a sequence of offsets, and generate the transforms as you need them. See this post for an example of this approach.


Also note that transforms are not applied until the patch is rendered (such as by a call to plt.show()). This explains why multiple calls to c.set_transform have the same effect as a single call to c.set_transform. Only the last transform is applied to the patch.

The patch, c, stores a single transform in a private attribute which you can access using c.get_transform:

In [10]: c.get_transform()
Out[15]: 
CompositeGenericTransform(Affine2D(array([[ 5.,  0.,  0.],
       [ 0.,  5.,  0.],
       [ 0.,  0.,  1.]])), CompositeGenericTransform(TransformWrapper(BlendedAffine2D(IdentityTransform(),IdentityTransform())), CompositeGenericTransform(BboxTransformFrom(TransformedBbox(Bbox([[-10.0, -10.0], [10.0, 10.0]]), TransformWrapper(BlendedAffine2D(IdentityTransform(),IdentityTransform())))), BboxTransformTo(TransformedBbox(Bbox([[0.125, 0.09999999999999998], [0.9, 0.9]]), BboxTransformTo(TransformedBbox(Bbox([[0.0, 0.0], [6.4, 4.8]]), Affine2D(array([[ 100.,    0.,    0.],
       [   0.,  100.,    0.],
       [   0.,    0.,    1.]])))))))))

c.set_transform reassigns that private attribute to a new transform. But no transform gets applied until plt.show() or some other rendering call like plt.savefig is made.

like image 92
unutbu Avatar answered Sep 22 '22 14:09

unutbu