Skip to content

Commit 5822546

Browse files
committed
Allow tuning the shape of {L,R,D}Arrow tips.
1 parent 443c728 commit 5822546

2 files changed

Lines changed: 97 additions & 56 deletions

File tree

lib/matplotlib/patches.py

Lines changed: 94 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2587,83 +2587,124 @@ def __call__(self, x0, y0, width, height, mutation_size):
25872587
return trans.transform_path(Path.unit_circle())
25882588

25892589
@_register_style(_style_list)
2590-
class LArrow:
2591-
"""A box in the shape of a left-pointing arrow."""
2590+
class RArrow:
2591+
"""A box in the shape of a right-pointing arrow."""
25922592

2593-
def __init__(self, pad=0.3):
2593+
def __init__(self, pad=0.3, head_width=1.5, head_angle=90):
25942594
"""
25952595
Parameters
25962596
----------
25972597
pad : float, default: 0.3
25982598
The amount of padding around the original box.
2599+
head_width : float, default: 1.5
2600+
The head width, relative to the arrow shaft width; must be
2601+
nonnegative.
2602+
head_angle : float, default: 90
2603+
The angle at the tip of the arrow, in degrees; must be nonzero
2604+
(modulo 360). Negative angles result in arrow heads pointing
2605+
backwards.
25992606
"""
26002607
self.pad = pad
2608+
if head_width < 0:
2609+
raise ValueError("'head_width' must be nonnegative")
2610+
self.head_width = head_width
2611+
if head_angle % 360 == 0:
2612+
raise ValueError("'head_angle' must be nonzero")
2613+
self.head_angle = head_angle
26012614

26022615
def __call__(self, x0, y0, width, height, mutation_size):
2603-
# padding
2616+
# padding & padded dimensions
26042617
pad = mutation_size * self.pad
2605-
# width and height with padding added.
2606-
width, height = width + 2 * pad, height + 2 * pad
2607-
# boundary of the padded box
2618+
dx, dy = width + 2 * pad, height + 2 * pad
26082619
x0, y0 = x0 - pad, y0 - pad,
2609-
x1, y1 = x0 + width, y0 + height
2610-
2611-
dx = (y1 - y0) / 2
2612-
dxx = dx / 2
2613-
x0 = x0 + pad / 1.4 # adjust by ~sqrt(2)
2614-
2615-
return Path._create_closed(
2616-
[(x0 + dxx, y0), (x1, y0), (x1, y1), (x0 + dxx, y1),
2617-
(x0 + dxx, y1 + dxx), (x0 - dx, y0 + dx),
2618-
(x0 + dxx, y0 - dxx), # arrow
2619-
(x0 + dxx, y0)])
2620+
x1, y1 = x0 + dx, y0 + dy
2621+
2622+
head_dy = self.head_width * dy
2623+
mid_y = (y0 + y1) / 2
2624+
shaft_y0 = mid_y - head_dy / 2
2625+
shaft_y1 = mid_y + head_dy / 2
2626+
2627+
cot = 1 / math.tan(math.radians(self.head_angle / 2))
2628+
2629+
if cot > 0:
2630+
# tip_x is chosen s.t. the angled line moving back from the tip hits
2631+
# i) if head_width > 1: the box corner, or ii) if head_width <
2632+
# 1 the box edge at the point giving the correct shaft width.
2633+
tip_x = x1 + cot * min(dy, head_dy) / 2
2634+
shaft_x = tip_x - cot * head_dy / 2
2635+
return Path._create_closed([
2636+
(x0, y0), (shaft_x, y0), (shaft_x, shaft_y0),
2637+
(tip_x, mid_y),
2638+
(shaft_x, shaft_y1), (shaft_x, y1), (x0, y1),
2639+
])
2640+
else: # Reverse arrowhead.
2641+
# Make the long (outer) side of the arrowhead flush with the
2642+
# original box, and move back accordingly (but clipped to no
2643+
# more than the box length). If this clipping is necessary,
2644+
# the y positions at the short (inner) side of the arrowhead
2645+
# will be thicker than the original box, hence the need to
2646+
# recompute mid_y0 & mid_y1.
2647+
# If head_width < 1 no arrowhead is drawn.
2648+
dx = min(-cot * max(head_dy - dy, 0) / 2, dx) # cot < 0!
2649+
mid_y0 = min(shaft_y0, y0) - dx / cot
2650+
mid_y1 = max(shaft_y1, y1) + dx / cot
2651+
return Path._create_closed([
2652+
(x0, y0), (x1 - dx, mid_y0), (x1, shaft_y0),
2653+
(x1, shaft_y1), (x1 - dx, mid_y1), (x0, y1),
2654+
])
26202655

26212656
@_register_style(_style_list)
2622-
class RArrow(LArrow):
2623-
"""A box in the shape of a right-pointing arrow."""
2657+
class LArrow(RArrow):
2658+
"""A box in the shape of a left-pointing arrow."""
26242659

26252660
def __call__(self, x0, y0, width, height, mutation_size):
2626-
p = BoxStyle.LArrow.__call__(
2627-
self, x0, y0, width, height, mutation_size)
2661+
p = super().__call__(x0, y0, width, height, mutation_size)
26282662
p.vertices[:, 0] = 2 * x0 + width - p.vertices[:, 0]
26292663
return p
26302664

26312665
@_register_style(_style_list)
2632-
class DArrow:
2666+
class DArrow(RArrow):
26332667
"""A box in the shape of a two-way arrow."""
2634-
# Modified from LArrow to add a right arrow to the bbox.
2635-
2636-
def __init__(self, pad=0.3):
2637-
"""
2638-
Parameters
2639-
----------
2640-
pad : float, default: 0.3
2641-
The amount of padding around the original box.
2642-
"""
2643-
self.pad = pad
2668+
# Modified from RArrow to have arrows on both sides; see comments above.
26442669

26452670
def __call__(self, x0, y0, width, height, mutation_size):
2646-
# padding
2671+
# padding & padded dimensions
26472672
pad = mutation_size * self.pad
2648-
# width and height with padding added.
2649-
# The width is padded by the arrows, so we don't need to pad it.
2650-
height = height + 2 * pad
2651-
# boundary of the padded box
2652-
x0, y0 = x0 - pad, y0 - pad
2653-
x1, y1 = x0 + width, y0 + height
2654-
2655-
dx = (y1 - y0) / 2
2656-
dxx = dx / 2
2657-
x0 = x0 + pad / 1.4 # adjust by ~sqrt(2)
2658-
2659-
return Path._create_closed([
2660-
(x0 + dxx, y0), (x1, y0), # bot-segment
2661-
(x1, y0 - dxx), (x1 + dx + dxx, y0 + dx),
2662-
(x1, y1 + dxx), # right-arrow
2663-
(x1, y1), (x0 + dxx, y1), # top-segment
2664-
(x0 + dxx, y1 + dxx), (x0 - dx, y0 + dx),
2665-
(x0 + dxx, y0 - dxx), # left-arrow
2666-
(x0 + dxx, y0)])
2673+
dx, dy = width + 2 * pad, height + 2 * pad
2674+
x0, y0 = x0 - pad, y0 - pad,
2675+
x1, y1 = x0 + dx, y0 + dy
2676+
2677+
head_dy = self.head_width * dy
2678+
mid_y = (y0 + y1) / 2
2679+
shaft_y0 = mid_y - head_dy / 2
2680+
shaft_y1 = mid_y + head_dy / 2
2681+
2682+
cot = 1 / math.tan(math.radians(self.head_angle / 2))
2683+
2684+
if cot > 0:
2685+
tip_x0 = x0 - cot * min(dy, head_dy) / 2
2686+
shaft_x0 = tip_x0 + cot * head_dy / 2
2687+
tip_x1 = x1 + cot * min(dy, head_dy) / 2
2688+
shaft_x1 = tip_x1 - cot * head_dy / 2
2689+
return Path._create_closed([
2690+
(shaft_x0, y1), (shaft_x0, shaft_y1),
2691+
(tip_x0, mid_y),
2692+
(shaft_x0, shaft_y0), (shaft_x0, y0),
2693+
(shaft_x1, y0), (shaft_x1, shaft_y0),
2694+
(tip_x1, mid_y),
2695+
(shaft_x1, shaft_y1), (shaft_x1, y1),
2696+
])
2697+
else:
2698+
# Don't move back by more than half the box length.
2699+
dx = min(-cot * max(head_dy - dy, 0) / 2, dx / 2) # cot < 0!
2700+
mid_y0 = min(shaft_y0, y0) - dx / cot
2701+
mid_y1 = max(shaft_y1, y1) + dx / cot
2702+
return Path._create_closed([
2703+
(x0, shaft_y0), (x0 + dx, mid_y0),
2704+
(x1 - dx, mid_y0), (x1, shaft_y0),
2705+
(x1, shaft_y1), (x1 - dx, mid_y1),
2706+
(x0 + dx, mid_y1), (x0, shaft_y1),
2707+
])
26672708

26682709
@_register_style(_style_list)
26692710
class Round:

lib/matplotlib/patches.pyi

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ class BoxStyle(_Style):
379379
mutation_size: float,
380380
) -> Path: ...
381381

382-
class LArrow(BoxStyle):
382+
class RArrow(BoxStyle):
383383
pad: float
384384
def __init__(self, pad: float = ...) -> None: ...
385385
def __call__(
@@ -391,7 +391,7 @@ class BoxStyle(_Style):
391391
mutation_size: float,
392392
) -> Path: ...
393393

394-
class RArrow(LArrow):
394+
class LArrow(RArrow):
395395
def __call__(
396396
self,
397397
x0: float,
@@ -401,7 +401,7 @@ class BoxStyle(_Style):
401401
mutation_size: float,
402402
) -> Path: ...
403403

404-
class DArrow(BoxStyle):
404+
class DArrow(RArrow):
405405
pad: float
406406
def __init__(self, pad: float = ...) -> None: ...
407407
def __call__(

0 commit comments

Comments
 (0)