`matplotlib.patches.PathPatch` с шириной линии, указанной в единицах данных (не в точках) - PullRequest
1 голос
/ 16 апреля 2020

Я хотел бы создать matplotlib.patches.PathPatch подобный патч с шириной линии, указанной в единицах данных (не точках).

Я знаю, что для патчей с правильной формой, такой объект можно имитировать c, рисуя два фрагмента разных размеров друг на друге. Тем не менее, этот подход становится намного более вычислительно сложным для произвольных фигур (необходимо вычислить параллельную линию для произвольной формы). Кроме того, я хотел бы сохранить методы объекта PathPath, поэтому в конечном итоге я ищу класс, производный от PathPatch или 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()

enter image description here

Существует другой пост SO , производный от Line2D для создания строки с ширинами в единицах данных.

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)

Я не совсем понимаю, как эта реализация работает (например, никакие методы Line2D не кажутся перезаписанными) Тем не менее, я попытался скопировать этот подход (class LineDataUnits(Line2D) -> class PathPatchDataUnits(PathPatch)), но - что неудивительно - не могу заставить его работать (AttributeError: 'NoneType' object has no attribute 'set_figure').

1 Ответ

1 голос
/ 21 апреля 2020

РЕДАКТИРОВАТЬ:

Я сделал это работает и получил некоторые идеи на пути. Прежде всего, вот код:

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()

Результат: The results I got on my machine

Теперь для моих мыслей:

axes.transData transform действительно представляет собой композицию из двух преобразований, одно для x-координат, другое для y-координат. Однако ширина линии - это единственный скаляр, поэтому нам нужно выбрать, какое из двух преобразований мы будем использовать. В моем решении я использую y-преобразование; однако, изменив строку под комментарием на index [0], вы можете выбрать использование x-transform. Если аспект оси равен (т. Е. Горизонтальная линия длиной 1 в единицах данных имеет ту же длину в единицах отображения, что и вертикальная линия длиной 1 в единицах данных), это не является проблемой. Однако, если аспект не равен, это может привести к неожиданному поведению, как показано на рисунке ниже.

enter image description here

Аспект оси может быть Вынужден быть равным с помощью команды ax.set_aspect('equal').

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...