I am trying to create a circle that displays a circle regardless of axis scaling, but placed in data coordinates and whose radius is dependent on the scaling of the y-axis. Based on the transforms tutorial, and more specifically the bit about plotting in physical coordinates, I need a pipeline that looks like this:
from matplotlib import pyplot as plt, patches as mpatch, transforms as mtrans
fig, ax = plt.subplots()
x, y = 5, 10
r = 3
transform = fig.dpi_scale_trans + fig_to_data_scaler + mtrans.ScaledTranslation(x, y, ax.transData)
ax.add_patch(mpatch.Circle((0, 0), r, edgecolor='k', linewidth=2, facecolor='w', transform=t))
The goal is to create a circle that's scaled correctly at the figure level, scale it to the correct height, and then move it in data coordinates. fig.dpi_scale_trans
and mtrans.ScaledTranslation(x, y, ax.transData)
work as expected. However, I am unable to come up with an adequate definition for fig_to_data_scaler
.
It is pretty clear that I need a blended transformation that takes the y-scale from ax.transData
combined with fig.dpi_scale_trans
(inverted?) and then uses the same values for x
, regardless of data transforms. How do I do that?
Another reference that I looked at: https://stackoverflow.com/a/56079290/2988730.
Here's a transform graph I've attempted to construct unsuccessfully:
vertical_scale_transform = mtrans.blended_transform_factory(mtrans.IdentityTransform(), fig.dpi_scale_trans.inverted() + mtrans.AffineDeltaTransform(ax.transData))
reflection = mtrans.Affine2D.from_values(0, 1, 1, 0, 0, 0)
fig_to_data_scaler = vertical_scale_transform + reflection + vertical_scale_transform # + reflection, though it's optional
It looks like the previous attempt was a bit over-complicated. It does not matter what the figure aspect ratio is. The axes data transform literally handles all of that out-of-the box. The following attempt almost works. The only thing it does not handle is pixel aspect ratio:
vertical_scale_transform = mtrans.AffineDeltaTransform(ax.transData)
reflection = mtrans.Affine2D.from_values(0, 1, 1, 0, 0, 0)
uniform_scale_transform = mtrans.blended_transform_factory(reflection + vertical_scale_transform + reflection, vertical_scale_transform)
t = uniform_scale_transform + mtrans.ScaledTranslation(x, y, ax.transData)
ax.add_patch(mpatch.Circle((0, 0), r, edgecolor='k', linewidth=2, facecolor='w', transform=t))
This places perfect circles at the correct locations. Panning works as expected. The only issue is that the size of the circles does not update when I zoom. Given mtrans.AffineDeltaTransform(ax.transData)
on the y-axis, I find that to be surprising.
I guess the updated question is then, why is the scaling part of the transform graph not updating fully when I zoom the axes?
It appears that the approach I proposed in the question is supposed to work. To create a transform that has data scaling in the y-direction and the same scaling regardless of data in the x-direction, we can do the following:
ax.transData
Affine2D
ScalesTranslation
to place the object at the correct data locationHere is the full solution:
from matplotlib import pyplot as plt, patches as mpatch, transforms as mtrans
fig, ax = plt.subplots()
x, y = 5, 10
r = 3
# AffineDeltaTransform returns just the scaling portion
vertical_scale_transform = mtrans.AffineDeltaTransform(ax.transData)
reflection = mtrans.Affine2D.from_values(0, 1, 1, 0, 0, 0)
# The first argument relies on the fact that `reflection` is its own inverse
uniform_scale_transform = mtrans.blended_transform_factory(reflection + vertical_scale_transform + reflection, vertical_scale_transform)
t = uniform_scale_transform + mtrans.ScaledTranslation(x, y, ax.transData)
# Create a circle at origin, and move it with the transform
ax.add_patch(mpatch.Circle((0, 0), r, edgecolor='k', linewidth=2, facecolor='w', transform=t))
This answer is encapsulated in a proposed gallery example: https://github.com/matplotlib/matplotlib/pull/28364
The issue with this solution at time of writing is that AffineDeltaTransform
is not updating correctly when axes are zoomed or resized. The issue has been filed in matplotlib#28372, and resolved in matplotlib#28375. Future versions of matplotlib will be able to run the code above interactively.