8000 Annotation: always use FancyBboxPatch instead of bbox_artist by efiring · Pull Request #4178 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content

Annotation: always use FancyBboxPatch instead of bbox_artist #4178

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 22, 2015
Prev Previous commit
Next Next commit
Removed YAArrow usage from Annotation.
YAArrow is now partly simulated with the FancyArrowPatch, which
remains as the only arrow class used by Annotation.  I ignored
the 'frac' key and added the 'headlength' key; the 'frac' was
never a good API because it scaled head length with the arrow
length, but left all other dimensions in units of points.
  • Loading branch information
efiring committed Jul 19, 2015
commit 197fc9fa29472c7e7f88591e91c5d118dc552d5a
14 changes: 7 additions & 7 deletions lib/matplotlib/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -3119,14 +3119,14 @@ class ArrowStyle(_Style):
%(AvailableArrowstyles)s


An instance of any arrow style class is an callable object,
An instance of any arrow style class is a callable object,
whose call signature is::

__call__(self, path, mutation_size, linewidth, aspect_ratio=1.)

and it returns a tuple of a :class:`Path` instance and a boolean
value. *path* is a :class:`Path` instance along witch the arrow
will be drawn. *mutation_size* and *aspect_ratio* has a same
value. *path* is a :class:`Path` instance along which the arrow
will be drawn. *mutation_size* and *aspect_ratio* have the same
meaning as in :class:`BoxStyle`. *linewidth* is a line width to be
stroked. This is meant to be used to correct the location of the
head so that it does not overshoot the destination point, but not all
Expand Down Expand Up @@ -3175,11 +3175,11 @@ def ensure_quadratic_bezier(path):

def transmute(self, path, mutation_size, linewidth):
"""
The transmute method is a very core of the ArrowStyle
The transmute method is the very core of the ArrowStyle
class and must be overriden in the subclasses. It receives
the path object along which the arrow will be drawn, and
the mutation_size, with which the amount arrow head and
etc. will be scaled. The linewidth may be used to adjust
the mutation_size, with which the arrow head etc.
will be scaled. The linewidth may be used to adjust
the path so that it does not pass beyond the given
points. It returns a tuple of a Path instance and a
boolean. The boolean value indicate whether the path can
Expand Down Expand Up @@ -4077,7 +4077,7 @@ def set_connectionstyle(self, connectionstyle, **kw):

*connectionstyle* can be a string with connectionstyle name with
optional comma-separated attributes. Alternatively, the attrs can be
probided as keywords.
provided as keywords.

set_connectionstyle("arc,angleA=0,armA=30,rad=10")
set_connectionstyle("arc", angleA=0,armA=30,rad=10)
Expand Down
202 changes: 97 additions & 105 deletions lib/matplotlib/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from matplotlib.transforms import Affine2D, Bbox, Transform
from matplotlib.transforms import BboxBase, BboxTransformTo
from matplotlib.lines import Line2D

from matplotlib.path import Path
from matplotlib.artist import allow_rasterization

from matplotlib.backend_bases import RendererBase
Expand Down Expand Up @@ -539,7 +539,7 @@ def update_bbox_position_size(self, renderer):
self._bbox_patch.set_transform(tr)
fontsize_in_pixel = renderer.points_to_pixels(self.get_size())
self._bbox_patch.set_mutation_scale(fontsize_in_pixel)
# self._bbox_patch.draw(renderer)


def _draw_bbox(self, renderer, posx, posy):

Expand All @@ -558,6 +558,7 @@ def _draw_bbox(self, renderer, posx, posy):
self._bbox_patch.set_mutation_scale(fontsize_in_pixel)
self._bbox_patch.draw(renderer)


def _update_clip_properties(self):
clipprops = dict(clip_box=self.clipbox,
clip_path=self._clippath,
Expand Down Expand Up @@ -2046,9 +2047,18 @@ def __init__(self, s, xy,

self.arrow = None

if arrowprops and "arrowstyle" in arrowprops:
arrowprops = self.arrowprops.copy()
self._arrow_relpos = arrowprops.pop("relpos", (0.5, 0.5))
if arrowprops:
if "arrowstyle" in arrowprops:
arrowprops = self.arrowprops.copy()
self._arrow_relpos = arrowprops.pop("relpos", (0.5, 0.5))
else:
# modified YAArrow API to be used with FancyArrowPatch
shapekeys = ('width', 'headwidth', 'headlength',
'shrink', 'frac')
arrowprops = dict()
for key, val in self.arrowprops.items():
if key not in shapekeys:
arrowprops[key] = val # basic Patch properties
self.arrow_patch = FancyArrowPatch((0, 0), (1, 1),
**arrowprops)
else:
Expand All @@ -2059,7 +2069,9 @@ def contains(self, event):
if self.arrow is not None:
in_arrow, _ = self.arrow.contains(event)
contains = contains or in_arrow
# self.arrow_patch is currently not checked as this can be a line - J
if self.arrow_patch is not None:
in_patch, _ = self.arrow_patch.contains(event)
contains = contains or in_patch

return contains, tinfo

Expand Down Expand Up @@ -2102,6 +2114,7 @@ def _update_position_xytext(self, renderer, xy_pixel):
self.set_transform(self._get_xy_transform(
renderer, self.xy, self.anncoords))


ox0, oy0 = self._get_xy_display()
ox1, oy1 = xy_pixel

Expand All @@ -2114,111 +2127,94 @@ def _update_position_xytext(self, renderer, xy_pixel):
yc = 0.5 * (b + t)

d = self.arrowprops.copy()
ms = d.pop("mutation_scale", self.get_size())
ms = renderer.points_to_pixels(ms)
self.arrow_patch.set_mutation_scale(ms)

# Use FancyArrowPatch if self.arrowprops has "arrowstyle" key.
# Otherwise, fallback to YAArrow.

#if d.has_key("arrowstyle"):
if self.arrow_patch:

# adjust the starting point of the arrow relative to
# the textbox.
# TODO : Rotation needs to be accounted.
relpos = self._arrow_relpos
bbox = Text.get_window_extent(self, renderer)
ox0 = bbox.x0 + bbox.width * relpos[0]
oy0 = bbox.y0 + bbox.height * relpos[1]

# The arrow will be drawn from (ox0, oy0) to (ox1,
# oy1). It will be first clipped by patchA and patchB.
# Then it will be shrinked by shirnkA and shrinkB
# (in points). If patch A is not set, self.bbox_patch
# is used.

self.arrow_patch.set_positions((ox0, oy0), (ox1, oy1))
mutation_scale = d.pop("mutation_scale", self.get_size())
mutation_scale = renderer.points_to_pixels(mutation_scale)
self.arrow_patch.set_mutation_scale(mutation_scale)

if "patchA" in d:
self.arrow_patch.set_patchA(d.pop("patchA"))
else:
if self._bbox_patch:
self.arrow_patch.set_patchA(self._bbox_patch)
else:
pad = renderer.points_to_pixels(4)
if self.get_text().strip() == "":
self.arrow_patch.set_patchA(None)
return

bbox = Text.get_window_extent(self, renderer)
l, b, w, h = bbox.bounds
l -= pad / 2.
b -= pad / 2.
w += pad
h += pad
r = Rectangle(xy=(l, b),
width=w,
height=h,
)
r.set_transform(mtransforms.IdentityTransform())
r.set_clip_on(False)

self.arrow_patch.set_patchA(r)
if "arrowstyle" not in d:
# Approximately simulate the YAArrow.
# Pop its kwargs:
shrink = d.pop('shrink', 0.0)
width = d.pop('width', 4)
headwidth = d.pop('headwidth', 12)
# Ignore frac--it is useless.
frac = d.pop('frac', None)
if frac is not None:
warnings.warn(
"'frac' option in 'arrowstyle' is no longer supported;"
" use 'headlength' to set the head length in points.")
headlength = d.pop('headlength', 12)

else:
# using YAArrow
stylekw = dict(head_length=headlength / ms,
head_width=headwidth / ms,
tail_width=width / ms)

self.arrow_patch.set_arrowstyle('simple', **stylekw)

# using YAArrow style:
# pick the x,y corner of the text bbox closest to point
# annotated
dsu = [(abs(val - x0), val) for val in (l, r, xc)]
xpos = ((l, 0), (xc, 0.5), (r, 1))
ypos = ((b, 0), (yc, 0.5), (t, 1))

dsu = [(abs(val[0] - x0), val) for val in xpos]
dsu.sort()
_, x = dsu[0]
_, (x, relposx) = dsu[0]

dsu = [(abs(val - y0), val) for val in (b, t, yc)]
dsu = [(abs(val[0] - y0), val) for val in ypos]
dsu.sort()
_, y = dsu[0]
_, (y, relposy) = dsu[0]

shrink = d.pop('shrink', 0.0)
self._arrow_relpos = (relposx, relposy)

theta = math.atan2(y - y0, x - x0)
r = np.hypot((y - y0), (x - x0))
dx = shrink * r * math.cos(theta)
dy = shrink * r * math.sin(theta)
shrink_pts = shrink * r / renderer.points_to_pixels(1)
self.arrow_patch.shrinkA = shrink_pts
self.arrow_patch.shrinkB = shrink_pts

width = d.pop('width', 4)
headwidth = d.pop('headwidth', 12)
frac = d.pop(&# 6D40 39;frac', 0.1)
self.arrow = YAArrow(self.figure,
(x0 + dx, y0 + dy), (x - dx, y - dy),
width=width, headwidth=headwidth,
frac=frac,
**d)

self.arrow.set_clip_box(self.get_clip_box())
# adjust the starting point of the arrow relative to
# the textbox.
# TODO : Rotation needs to be accounted.
relpos = self._arrow_relpos
bbox = Text.get_window_extent(self, renderer)
ox0 = bbox.x0 + bbox.width * relpos[0]
oy0 = bbox.y0 + bbox.height * relpos[1]

def update_bbox_position_size(self, renderer):
"""
Update the location and the size of the bbox. This method
should be used when the position and size of the bbox needs to
be updated before actually drawing the bbox.
"""
# The arrow will be drawn from (ox0, oy0) to (ox1,
# oy1). It will be first clipped by patchA and patchB.
# Then it will be shrunk by shirnkA and shrinkB
# (in points). If patch A is not set, self.bbox_patch
# is used.

# For arrow_patch, use textbox as patchA by default.
self.arrow_patch.set_positions((ox0, oy0), (ox1, oy1))

if not isinstance(self.arrow_patch, FancyArrowPatch):
return
if "patchA" in d:
self.arrow_patch.set_patchA(d.pop("patchA"))
else:
if self._bbox_patch:
self.arrow_patch.set_patchA(self._bbox_patch)
else:
pad = renderer.points_to_pixels(4)
if self.get_text().strip() == "":
self.arrow_patch.set_patchA(None)
return

bbox = Text.get_window_extent(self, renderer)
l, b, w, h = bbox.bounds
l -= pad / 2.
b -= pad / 2.
9E88 w += pad
h += pad
r = Rectangle(xy=(l, b),
width=w,
height=h,
)
r.set_transform(mtransforms.IdentityTransform())
r.set_clip_on(False)

self.arrow_patch.set_patchA(r)

if self._bbox_patch:
posx, posy = self._x, self._y

x_box, y_box, w_box, h_box = _get_textbox(self, renderer)
self._bbox_patch.set_bounds(0., 0., w_box, h_box)
theta = np.deg2rad(self.get_rotation())
tr = mtransforms.Affine2D().rotate(theta)
tr = tr.translate(posx + x_box, posy + y_box)
self._bbox_patch.set_transform(tr)
fontsize_in_pixel = renderer.points_to_pixels(self.get_size())
self._bbox_patch.set_mutation_scale(fontsize_in_pixel)

@allow_rasterization
def draw(self, renderer):
Expand All @@ -2238,20 +2234,16 @@ def draw(self, renderer):
self._update_position_xytext(renderer, xy_pixel)
self.update_bbox_position_size(renderer)

# Draw text, including FancyBboxPatch, before FancyArrowPatch.
# Otherwise, the transform of the former Patch will be incomplete.
Text.draw(self, renderer)

if self.arrow is not None:
if self.arrow.figure is None and self.figure is not None:
self.arrow.figure = self.figure
self.arrow.draw(renderer)

if self.arrow_patch is not None:
if self.arrow_patch is not None: # FancyArrowPatch
if self.arrow_patch.figure is None and self.figure is not None:
self.arrow_patch.figure = self.figure
self.arrow_patch.draw(renderer)

# Draw text, including FancyBboxPatch, after FancyArrowPatch.
# Otherwise, a wedge arrowstyle can land partly on top of the Bbox.
Text.draw(self, renderer)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use super here (you use it in update)?

9EBF
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, I did. That is new code, and right next to another method in which super is used. But the "Text.draw()" is not new, and it is one of several such lines in this module. I'm inclined to think that "super-ifying" the module is out of scope for this PR. I also note that not all parent class method calls in this module can be handled with super; Annotation.__init__ needs to call the __init__ from each of its immediate parents. The only way to avoid that would be to rewrite it so as to use composition rather than multiple inheritance. In the long run that could bring a big gain in readability and maintainability, but given the present complexity of this entire family of code, it would be quite a job.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair.



def get_window_extent(self, renderer=None):
'''
Return a :class:`~matplotlib.transforms.Bbox` object bounding
Expand Down
0