Reputation: 13031
I would like to create a matplotlib.patches.PathPatch
like patch, with the linewidth given in data units (not points).
I am aware that for patches with a regular shape, one can mimic such an object by drawing two patches of different sizes on top of each other.
However, this approach becomes a lot more computationally involved for arbitrary shapes (need to compute a parallel line for an arbitrary shape).
Furthermore, I would like to retain the PathPath
object methods, so ultimately I am looking for a class derived from PathPatch
or Patch
.
import matplotlib.pyplot as plt
from matplotlib.patches import PathPatch, Rectangle
fig, (ax1, ax2) = plt.subplots(1, 2, sharex=True, sharey=True)
origin = (0, 0)
width = 1
height = 2
lw = 0.25
outer = Rectangle((origin[0], origin[1]), width, height, facecolor='darkblue', zorder=1)
inner = Rectangle((origin[0]+lw, origin[1]+lw), width-2*lw, height-2*lw, facecolor='lightblue', zorder=2)
ax1.add_patch(outer)
ax1.add_patch(inner)
ax1.axis([-0.5, 1.5, -0.5, 2.5])
ax1.set_title('Desired')
path = outer.get_path().transformed(outer.get_patch_transform())
pathpatch = PathPatch(path, facecolor='lightblue', edgecolor='darkblue', linewidth=lw)
ax2.add_patch(pathpatch)
ax2.set_title('Actual')
plt.show()
There is a different SO post that derives from Line2D
to create a line with widths in data units.
class LineDataUnits(Line2D):
# https://stackoverflow.com/a/42972469/2912349
def __init__(self, *args, **kwargs):
_lw_data = kwargs.pop("linewidth", 1)
super().__init__(*args, **kwargs)
self._lw_data = _lw_data
def _get_lw(self):
if self.axes is not None:
ppd = 72./self.axes.figure.dpi
trans = self.axes.transData.transform
return ((trans((1, self._lw_data))-trans((0, 0)))*ppd)[1]
else:
return 1
def _set_lw(self, lw):
self._lw_data = lw
_linewidth = property(_get_lw, _set_lw)
I don't quite understand how this implementation works (for example, no methods of Line2D
seem to be overwritten)
Nevertheless, I have tried copying this approach (class LineDataUnits(Line2D)
-> class PathPatchDataUnits(PathPatch)
), but -- unsurprisingly -- cannot get it to work (AttributeError: 'NoneType' object has no attribute 'set_figure'
).
Upvotes: 1
Views: 1172
Reputation: 355
EDIT:
I made it work and got some insights on the way. First of all, here is the code:
import matplotlib.pyplot as plt
from matplotlib.patches import PathPatch, Rectangle
# The class from the solution you found, I just changed inheritance
class PatchDataUnits(PathPatch):
# https://stackoverflow.com/a/42972469/2912349
def __init__(self, *args, **kwargs):
_lw_data = kwargs.pop("linewidth", 1)
super().__init__(*args, **kwargs)
self._lw_data = _lw_data
def _get_lw(self):
if self.axes is not None:
ppd = 72./self.axes.figure.dpi
trans = self.axes.transData.transform
# the line mentioned below
return ((trans((self._lw_data, self._lw_data))-trans((0, 0)))*ppd)[1]
else:
return 1
def _set_lw(self, lw):
self._lw_data = lw
_linewidth = property(_get_lw, _set_lw)
# Your code example
fig, (ax1, ax2) = plt.subplots(1, 2, sharex=True, sharey=True)
origin = (0, 0)
width = 1
height = 2
lw = 0.25
outer = Rectangle((origin[0], origin[1]), width, height, facecolor='darkblue', zorder=1)
inner = Rectangle((origin[0]+lw, origin[1]+lw), width-2*lw, height-2*lw, facecolor='lightblue', zorder=2)
ax1.add_patch(outer)
ax1.add_patch(inner)
ax1.axis([-0.5, 1.5, -0.5, 2.5])
ax1.set_title('Desired')
# change lw because it overlaps some of the fig
mlw = lw /2
# create new patch with the adusted size
mid = Rectangle((origin[0]+mlw, origin[1]+mlw), width-2*mlw, height-2*mlw, facecolor='lightblue', zorder=1)
path = mid.get_path().transformed(mid.get_patch_transform())
# Use the data unit class to create the path patch with the original lw
pathpatch = PatchDataUnits(path, facecolor='lightblue', edgecolor='darkblue', linewidth=lw)
ax2.add_patch(pathpatch)
ax2.set_title('Actual')
plt.show()
Now for my thoughts:
The axes.transData
transform really is a composition of two transforms, one for x-coordinates, the other for y-coordinates.
However, a linewidth is a single scalar, therefor we need to make a choice which of the two transforms we will use.
In my solution, I use the y-transform; however, by changing the line below the comment to index [0], you can select to use the x-transform.
If the aspect of the axis is equal (i.e. a horizontal line of length 1 in data units has the same length in display units as a vertical line of length 1 in data units),
this is not an issue. However, if the aspect is not equal, this may yield to unexpected behaviour as shown in the figure below.
The aspect of an axis can be forced to be equal using the command ax.set_aspect('equal')
.
Upvotes: 1