From 963f52819d076ca8434f900547706fa1f8138605 Mon Sep 17 00:00:00 2001 From: Ian Hunt-Isaak Date: Sat, 24 Oct 2020 18:55:29 -0400 Subject: [PATCH 1/4] make FancyArrow animatable --- .../next_whats_new/animatable_FancyArrow.rst | 5 + lib/matplotlib/patches.py | 96 +++++++++++++++---- 2 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 doc/users/next_whats_new/animatable_FancyArrow.rst diff --git a/doc/users/next_whats_new/animatable_FancyArrow.rst b/doc/users/next_whats_new/animatable_FancyArrow.rst new file mode 100644 index 000000000000..55513fba6db0 --- /dev/null +++ b/doc/users/next_whats_new/animatable_FancyArrow.rst @@ -0,0 +1,5 @@ +``set_data`` method for ``FancyArrow`` patch +-------------------------------------------- + +`.FancyArrow`, the patch returned by ``ax.arrow``, now has a ``set_data`` +method that allows for animating the arrow. diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 29b76f7b74ab..c1e0d749b045 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -1333,22 +1333,81 @@ def __init__(self, x, y, dx, dy, width=0.001, length_includes_head=False, %(Patch_kwdoc)s """ - if head_width is None: - head_width = 3 * width - if head_length is None: + self.x = x + self.y = y + self.dx = dx + self.dy = dy + self.width = width + self.length_includes_head = length_includes_head + self.head_width = head_width + self.head_length = head_length + self.shape = shape + self.overhang = overhang + self.head_starts_at_zero = head_starts_at_zero + self._make_verts() + super().__init__(self.verts, closed=True, **kwargs) + + def set_data(self, *, x=None, y=None, dx=None, dy=None, width=None, + head_width=None, head_length=None): + """ + Set `.FancyArrow` x, y, dx, dy, width, head_with, and head_length. + Values left as None will not be updated. + + Parameters + ---------- + x, y : float or None, default: None + The x and y coordinates of the arrow base. + + dx, dy : float or None, default: None + The length of the arrow along x and y direction. + + width: float or None, default: None + Width of full arrow tail. + + head_width: float or None, default: None + Total width of the full arrow head. + + head_length: float or None, default: None + Length of arrow head. + """ + if x is not None: + self.x = x + if y is not None: + self.y = y + if dx is not None: + self.dx = dx + if dy is not None: + self.dy = dy + if width is not None: + self.width = width + if head_width is not None: + self.head_width = head_width + if head_length is not None: + self.head_length = head_length + self._make_verts() + self.set_xy(self.verts) + + def _make_verts(self): + if self.head_width is None: + head_width = 3 * self.width + else: + head_width = self.head_width + if self.head_length is None: head_length = 1.5 * head_width + else: + head_length = self.head_length - distance = np.hypot(dx, dy) + distance = np.hypot(self.dx, self.dy) - if length_includes_head: + if self.length_includes_head: length = distance else: length = distance + head_length if not length: - verts = np.empty([0, 2]) # display nothing if empty + self.verts = np.empty([0, 2]) # display nothing if empty else: # start by drawing horizontal arrow, point at (0, 0) - hw, hl, hs, lw = head_width, head_length, overhang, width + hw, hl, hs, lw = head_width, head_length, self.overhang, self.width left_half_arrow = np.array([ [0.0, 0.0], # tip [-hl, -hw / 2], # leftmost @@ -1357,36 +1416,37 @@ def __init__(self, x, y, dx, dy, width=0.001, length_includes_head=False, [-length, 0], ]) # if we're not including the head, shift up by head length - if not length_includes_head: + if not self.length_includes_head: left_half_arrow += [head_length, 0] # if the head starts at 0, shift up by another head length - if head_starts_at_zero: + if self.head_starts_at_zero: left_half_arrow += [head_length / 2, 0] # figure out the shape, and complete accordingly - if shape == 'left': + if self.shape == 'left': coords = left_half_arrow else: right_half_arrow = left_half_arrow * [1, -1] - if shape == 'right': + if self.shape == 'right': coords = right_half_arrow - elif shape == 'full': + elif self.shape == 'full': # The half-arrows contain the midpoint of the stem, # which we can omit from the full arrow. Including it # twice caused a problem with xpdf. coords = np.concatenate([left_half_arrow[:-1], right_half_arrow[-2::-1]]) else: - raise ValueError("Got unknown shape: %s" % shape) + raise ValueError("Got unknown shape: %s" % self.shape) if distance != 0: - cx = dx / distance - sx = dy / distance + cx = self.dx / distance + sx = self.dy / distance else: # Account for division by zero cx, sx = 0, 1 M = [[cx, sx], [-sx, cx]] - verts = np.dot(coords, M) + (x + dx, y + dy) - - super().__init__(verts, closed=True, **kwargs) + self.verts = np.dot(coords, M) + [ + self.x + self.dx, + self.y + self.dy, + ] docstring.interpd.update( From 5215d846aea030645538138c3d79b5c18dd074bb Mon Sep 17 00:00:00 2001 From: Ian Hunt-Isaak Date: Tue, 3 Nov 2020 18:48:49 -0500 Subject: [PATCH 2/4] simplify docstrings previously the FancyArrow docstring was missing x,y,dx, and dy --- lib/matplotlib/axes/_axes.py | 6 ------ lib/matplotlib/patches.py | 6 ++++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 1b0c881245af..2d4ee16cb668 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -5035,12 +5035,6 @@ def arrow(self, x, y, dx, dy, **kwargs): Parameters ---------- - x, y : float - The x and y coordinates of the arrow base. - - dx, dy : float - The length of the arrow along x and y direction. - %(FancyArrow)s Returns diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index c1e0d749b045..6248df4b9ba6 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -1305,6 +1305,12 @@ def __init__(self, x, y, dx, dy, width=0.001, length_includes_head=False, """ Parameters ---------- + x, y : float + The x and y coordinates of the arrow base. + + dx, dy : float + The length of the arrow along x and y direction. + width : float, default: 0.001 Width of full arrow tail. From bbe2c90b3c035f8e66d93822c87a77fc330b84b6 Mon Sep 17 00:00:00 2001 From: ianhi Date: Mon, 15 Feb 2021 19:32:32 -0500 Subject: [PATCH 3/4] privatize fancyarrowpatch attributes --- lib/matplotlib/patches.py | 71 ++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 6248df4b9ba6..70ba885ff343 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -1339,17 +1339,17 @@ def __init__(self, x, y, dx, dy, width=0.001, length_includes_head=False, %(Patch_kwdoc)s """ - self.x = x - self.y = y - self.dx = dx - self.dy = dy - self.width = width - self.length_includes_head = length_includes_head - self.head_width = head_width - self.head_length = head_length - self.shape = shape - self.overhang = overhang - self.head_starts_at_zero = head_starts_at_zero + self._x = x + self._y = y + self._dx = dx + self._dy = dy + self._width = width + self._length_includes_head = length_includes_head + self._head_width = head_width + self._head_length = head_length + self._shape = shape + self._overhang = overhang + self._head_starts_at_zero = head_starts_at_zero self._make_verts() super().__init__(self.verts, closed=True, **kwargs) @@ -1377,35 +1377,35 @@ def set_data(self, *, x=None, y=None, dx=None, dy=None, width=None, Length of arrow head. """ if x is not None: - self.x = x + self._x = x if y is not None: - self.y = y + self._y = y if dx is not None: - self.dx = dx + self._dx = dx if dy is not None: - self.dy = dy + self._dy = dy if width is not None: - self.width = width + self._width = width if head_width is not None: - self.head_width = head_width + self._head_width = head_width if head_length is not None: - self.head_length = head_length + self._head_length = head_length self._make_verts() self.set_xy(self.verts) def _make_verts(self): - if self.head_width is None: - head_width = 3 * self.width + if self._head_width is None: + head_width = 3 * self._width else: - head_width = self.head_width - if self.head_length is None: + head_width = self._head_width + if self._head_length is None: head_length = 1.5 * head_width else: - head_length = self.head_length + head_length = self._head_length - distance = np.hypot(self.dx, self.dy) + distance = np.hypot(self._dx, self._dy) - if self.length_includes_head: + if self._length_includes_head: length = distance else: length = distance + head_length @@ -1413,7 +1413,8 @@ def _make_verts(self): self.verts = np.empty([0, 2]) # display nothing if empty else: # start by drawing horizontal arrow, point at (0, 0) - hw, hl, hs, lw = head_width, head_length, self.overhang, self.width + hw, hl = head_width, head_length + hs, lw = self._overhang, self._width left_half_arrow = np.array([ [0.0, 0.0], # tip [-hl, -hw / 2], # leftmost @@ -1422,19 +1423,19 @@ def _make_verts(self): [-length, 0], ]) # if we're not including the head, shift up by head length - if not self.length_includes_head: + if not self._length_includes_head: left_half_arrow += [head_length, 0] # if the head starts at 0, shift up by another head length - if self.head_starts_at_zero: + if self._head_starts_at_zero: left_half_arrow += [head_length / 2, 0] # figure out the shape, and complete accordingly - if self.shape == 'left': + if self._shape == 'left': coords = left_half_arrow else: right_half_arrow = left_half_arrow * [1, -1] - if self.shape == 'right': + if self._shape == 'right': coords = right_half_arrow - elif self.shape == 'full': + elif self._shape == 'full': # The half-arrows contain the midpoint of the stem, # which we can omit from the full arrow. Including it # twice caused a problem with xpdf. @@ -1443,15 +1444,15 @@ def _make_verts(self): else: raise ValueError("Got unknown shape: %s" % self.shape) if distance != 0: - cx = self.dx / distance - sx = self.dy / distance + cx = self._dx / distance + sx = self._dy / distance else: # Account for division by zero cx, sx = 0, 1 M = [[cx, sx], [-sx, cx]] self.verts = np.dot(coords, M) + [ - self.x + self.dx, - self.y + self.dy, + self._x + self._dx, + self._y + self._dy, ] From 630494c37153dbe9560fa9b9bde337bd7546646b Mon Sep 17 00:00:00 2001 From: ianhi Date: Mon, 15 Feb 2021 19:56:45 -0500 Subject: [PATCH 4/4] add test for fancyarrowpatch set_data --- lib/matplotlib/tests/test_patches.py | 32 +++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_patches.py b/lib/matplotlib/tests/test_patches.py index 2a908098364e..33b86dfcdad5 100644 --- a/lib/matplotlib/tests/test_patches.py +++ b/lib/matplotlib/tests/test_patches.py @@ -522,7 +522,37 @@ def test_fancyarrow_units(): dtime = datetime(2000, 1, 1) fig, ax = plt.subplots() arrow = FancyArrowPatch((0, dtime), (0.01, dtime)) - ax.add_patch(arrow) + + +def test_fancyarrow_setdata(): + fig, ax = plt.subplots() + arrow = ax.arrow(0, 0, 10, 10, head_length=5, head_width=1, width=.5) + expected1 = np.array( + [[13.54, 13.54], + [10.35, 9.65], + [10.18, 9.82], + [0.18, -0.18], + [-0.18, 0.18], + [9.82, 10.18], + [9.65, 10.35], + [13.54, 13.54]] + ) + assert np.allclose(expected1, np.round(arrow.verts, 2)) + + expected2 = np.array( + [[16.71, 16.71], + [16.71, 15.29], + [16.71, 15.29], + [1.71, 0.29], + [0.29, 1.71], + [15.29, 16.71], + [15.29, 16.71], + [16.71, 16.71]] + ) + arrow.set_data( + x=1, y=1, dx=15, dy=15, width=2, head_width=2, head_length=1 + ) + assert np.allclose(expected2, np.round(arrow.verts, 2)) @image_comparison(["large_arc.svg"], style="mpl20")