diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index d6a25dfc487c..edd669f9852b 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -2372,14 +2372,35 @@ def __call__(self, x0, y0, width, height, mutation_size): class LArrow: """A box in the shape of a left-pointing arrow.""" - def __init__(self, pad=0.3): + def __init__(self, pad=0.3, head_width=1.5, head_angle=90.0): """ Parameters ---------- pad : float, default: 0.3 The amount of padding around the original box. + head_width : float, default: 1.5 + The width of the arrow head versus the body. The minimum value + is 0.0 and the maximum value is 10.0. Any value smaller or + greater than this is contrained to the edge values. + head_angle : float, default: 90.0 + The inside angle of the tip of the arrow. The minimum value is + 10.0 and the maximum value is 179.0. Any value smaller or + greater than this is contrained to the edge values. """ self.pad = pad + if head_width > 10: + self.head_width = 10 + elif head_width < 0: + self.head_width = 0 + else: + self.head_width = head_width + + if head_angle >= 180: + self.head_angle = 179 + elif head_angle < 10: + self.head_angle = 10 + else: + self.head_angle = head_angle def __call__(self, x0, y0, width, height, mutation_size): # padding @@ -2394,10 +2415,26 @@ def __call__(self, x0, y0, width, height, mutation_size): dxx = dx / 2 x0 = x0 + pad / 1.4 # adjust by ~sqrt(2) + # The width adjustment is the value that must be added or + # subtracted from y_0 and y_1 for the ends of the head. + # The body width is 2dx. + # Subtracting 1 from the head_width gives what percentage of the + # body width is in the head, and there is .5x of that on each side. + # The .5 cancels out the 2dx for the body width. + width_adjustment = (self.head_width - 1) * dx + + # The angle adjustment is the value that must be subtracted/added + # from x_0 or x_1 for the position of the tip. + # each half of the arrow head is a right angle triangle. Therefore, + # each half of the arrow head has the equation tan(head_angle/2)= + # (dx+width_adjustment)/(dxx+angle_adjustment). + angle_adjustment = ((dx + width_adjustment) / math.tan((self. + head_angle/2) * (math.pi/180))) - dxx + return Path._create_closed( [(x0 + dxx, y0), (x1, y0), (x1, y1), (x0 + dxx, y1), - (x0 + dxx, y1 + dxx), (x0 - dx, y0 + dx), - (x0 + dxx, y0 - dxx), # arrow + (x0 + dxx, y1 + width_adjustment), (x0 - angle_adjustment, y0 + + dx), (x0 + dxx, y0 - width_adjustment), # arrow (x0 + dxx, y0)]) @_register_style(_style_list) @@ -2415,14 +2452,35 @@ class DArrow: """A box in the shape of a two-way arrow.""" # Modified from LArrow to add a right arrow to the bbox. - def __init__(self, pad=0.3): + def __init__(self, pad=0.3, head_width=1.5, head_angle=90.0): """ Parameters ---------- pad : float, default: 0.3 The amount of padding around the original box. + head_width : float, default: 1.5 + The width of the arrow head versus the body. The minimum value + is 0.0 and the maximum value is 10.0. Any value smaller or + greater than this is contrained to the edge values. + head_angle : float, default: 90.0 + The inside angle of the tip of the arrow. The minimum value is + 10.0 and the maximum value is 179.0. Any value smaller or + greater than this is contrained to the edge values. """ self.pad = pad + if head_width > 10: + self.head_width = 10 + elif head_width < 0: + self.head_width = 0 + else: + self.head_width = head_width + + if head_angle >= 180: + self.head_angle = 179 + elif head_angle < 10: + self.head_angle = 10 + else: + self.head_angle = head_angle def __call__(self, x0, y0, width, height, mutation_size): # padding @@ -2438,13 +2496,31 @@ def __call__(self, x0, y0, width, height, mutation_size): dxx = dx / 2 x0 = x0 + pad / 1.4 # adjust by ~sqrt(2) + # The width adjustment is the value that must be added or + # subtracted from y_0 and y_1 for the ends of the head. + # The body width is 2dx. + # Subtracting 1 from the head_width gives what percentage of the + # body width is in the head, and there is .5x of that on each side. + # The .5 cancels out the 2dx for the body width. + width_adjustment = (self.head_width - 1) * dx + + # The angle adjustment is the value that must be subtracted/added + # from x_0 or x_1 for the position of the tip. + # each half of the arrow head is a right angle triangle. Therefore, + # each half of the arrow head has the equation tan(head_angle/2)= + # (dx+width_adjustment)/(dxx+angle_adjustment). + angle_adjustment = ((dx + width_adjustment) / math.tan((self. + head_angle/2) * (math.pi/180))) - dxx + return Path._create_closed([ (x0 + dxx, y0), (x1, y0), # bot-segment - (x1, y0 - dxx), (x1 + dx + dxx, y0 + dx), - (x1, y1 + dxx), # right-arrow + (x1, y0 - width_adjustment), + (x1 + angle_adjustment + dxx, y0 + dx), + (x1, y1 + width_adjustment), # right-arrow (x1, y1), (x0 + dxx, y1), # top-segment - (x0 + dxx, y1 + dxx), (x0 - dx, y0 + dx), - (x0 + dxx, y0 - dxx), # left-arrow + (x0 + dxx, y1 + width_adjustment), + (x0 - angle_adjustment, y0 + dx), + (x0 + dxx, y0 - width_adjustment), # left-arrow (x0 + dxx, y0)]) @_register_style(_style_list) diff --git a/lib/matplotlib/tests/baseline_images/test_arrow_patches/roadsign_test_image.png b/lib/matplotlib/tests/baseline_images/test_arrow_patches/roadsign_test_image.png new file mode 100644 index 000000000000..d9cf3646b585 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_arrow_patches/roadsign_test_image.png differ diff --git a/lib/matplotlib/tests/test_arrow_patches.py b/lib/matplotlib/tests/test_arrow_patches.py index 8d573b4adb1b..bf71bc4a674c 100644 --- a/lib/matplotlib/tests/test_arrow_patches.py +++ b/lib/matplotlib/tests/test_arrow_patches.py @@ -48,6 +48,29 @@ def test_boxarrow(): bbox=dict(boxstyle=stylename, fc="w", ec="k")) +@image_comparison(['roadsign_test_image.png']) +def temp_test_boxarrow(): + + styles = mpatches.BoxStyle.get_styles() + + n = len(styles) + spacing = 1.2 + + figheight = (n * spacing + .5) + fig = plt.figure(figsize=(4 / 1.5, figheight / 1.5)) + + fontsize = 0.3 * 72 + + for i, stylename in enumerate(sorted(styles)): + if stylename in ("larrow", "rarrow", "darrow"): + fig.text(0.5, ((n - i) * spacing - 0.5)/figheight, stylename, + ha="center", + size=fontsize, + transform=fig.transFigure, + bbox=dict(boxstyle=stylename+",head_width=1", fc="w", + ec="k")) + + def __prepare_fancyarrow_dpi_cor_test(): """ Convenience function that prepares and returns a FancyArrowPatch. It aims diff --git a/tutorials/text/annotations.py b/tutorials/text/annotations.py index bee2e244238d..8cb1239e56c8 100644 --- a/tutorials/text/annotations.py +++ b/tutorials/text/annotations.py @@ -192,10 +192,10 @@ # Class Name Attrs # ========== ============== ========================== # Circle ``circle`` pad=0.3 -# DArrow ``darrow`` pad=0.3 +# DArrow ``darrow`` pad=0.3,head_width=1.5,head_angle=90 # Ellipse ``ellipse`` pad=0.3 -# LArrow ``larrow`` pad=0.3 -# RArrow ``rarrow`` pad=0.3 +# LArrow ``larrow`` pad=0.3,head_width=1.5,head_angle=90 +# RArrow ``rarrow`` pad=0.3,head_width=1.5,head_angle=90 # Round ``round`` pad=0.3,rounding_size=None # Round4 ``round4`` pad=0.3,rounding_size=None # Roundtooth ``roundtooth`` pad=0.3,tooth_size=None @@ -254,8 +254,8 @@ def custom_box_style(x0, y0, width, height, mutation_size): x0, y0 = x0 - pad, y0 - pad x1, y1 = x0 + width, y0 + height # return the new path - return Path([(x0, y0), (x1, y0), (x1, y1), (x0, y1), - (x0-pad, (y0+y1)/2), (x0, y0), (x0, y0)], + return Path([(x0, y0), (x1, y0), (x1, y1), (x0, y1), (x0, y1-pad), + (x0-pad, (y0+y1)/2), (x0, y0+pad), (x0, y0), (x0, y0)], closed=True) fig, ax = plt.subplots(figsize=(3, 3))