Skip to content

fix tightbbox to account for markeredgewidth #16607

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
90dce86
fix tightbbox to account for markeredgewidth
brunobeltran Feb 29, 2020
5ed49fd
cleanup definition of "point" marker
brunobeltran Mar 1, 2020
9088cf4
document unit_regular_polygon
brunobeltran Mar 1, 2020
fd6df9b
untested version of new code to get marker bbox
brunobeltran Mar 3, 2020
ef2fefd
fix for marker bbox now works except for on miter
brunobeltran Mar 3, 2020
db033e4
fixed mis-ordered PathEndAngles for ticks
brunobeltran Mar 3, 2020
7e41bf5
flake8 for new markers code
brunobeltran Mar 3, 2020
f1014b5
factor marker bbox code to be within MarkerStyles
brunobeltran Mar 3, 2020
42cc5db
bugfix, forgot self in MarkerStyle.get_centered_bbox
brunobeltran Mar 3, 2020
8fcb223
misc bugfixes after factoring get_centered_bbox
brunobeltran Mar 3, 2020
d95e4d8
markers bbox code visually tested, now works
brunobeltran Mar 3, 2020
4592cda
flake8 for new markers bbox code
brunobeltran Mar 3, 2020
c08826e
fixed formula for miter marker bbox, bevel broke
brunobeltran Mar 4, 2020
7f9db16
bugfix caret bbox calculation, incorrect angles
brunobeltran Mar 4, 2020
1805dc7
fixed star tip angle in marker bbox calculation
brunobeltran Mar 4, 2020
a13598d
test marker bbox. failing here, pass in jupyter
brunobeltran Mar 4, 2020
0f7300a
bugfix so markers bbox api stays in pts units
brunobeltran Mar 4, 2020
d6f1571
forgot to push new test references images up
brunobeltran Mar 4, 2020
2bee048
cleanup variable name consistency
brunobeltran Mar 6, 2020
66694a0
use conversion not magic nums for line get_extents
brunobeltran Mar 9, 2020
c4a45de
iter_curves: iterate over path more conveniently
brunobeltran Mar 9, 2020
c5bdd8d
helper functions for bezier curve zeros/tangents
brunobeltran Mar 10, 2020
41f268e
CornerInfo in bezier.py, should be path.py
brunobeltran Mar 10, 2020
637a7f2
update marker bbox to work for arbitrary paths
brunobeltran Mar 10, 2020
5ac9114
bugfix, new marker bbox code now runs, untested
brunobeltran Mar 10, 2020
ef36ec2
pyflake fixes for marker bbox code
brunobeltran Mar 10, 2020
82e3c12
cleanup path/bezier to prevent import triangle
brunobeltran Mar 12, 2020
8585eca
fix prev commit, make split_in_out method of Path
brunobeltran Mar 12, 2020
54e3a3e
generalized path bbox code tested on some markers
brunobeltran Mar 12, 2020
b390653
path bbox now works for all markers but "pixel"
brunobeltran Mar 12, 2020
85a3050
reorg'd path/bezier code now builds docs no errors
brunobeltran Mar 12, 2020
79aa3b7
cleanup docstrings of stroked path bbox code
brunobeltran Mar 12, 2020
1ea37be
fixed sphinx warnings in path.py's docstrings
brunobeltran Mar 12, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 63 additions & 91 deletions lib/matplotlib/bezier.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import numpy as np

import matplotlib.cbook as cbook
from matplotlib.path import Path


class NonIntersectingPathException(ValueError):
Expand Down Expand Up @@ -177,18 +176,74 @@ class BezierSegment:
"""

def __init__(self, control_points):
n = len(control_points)
self._orders = np.arange(n)
coeff = [math.factorial(n - 1)
// (math.factorial(i) * math.factorial(n - 1 - i))
for i in range(n)]
self._px = np.asarray(control_points).T * coeff
self.cpoints = np.asarray(control_points)
self.n, self.d = self.cpoints.shape
self._orders = np.arange(self.n)
coeff = [math.factorial(self.n - 1)
// (math.factorial(i) * math.factorial(self.n - 1 - i))
for i in range(self.n)]
self._px = self.cpoints.T * coeff

def point_at_t(self, t):
"""Return the point on the Bezier curve for parameter *t*."""
return tuple(
self._px @ (((1 - t) ** self._orders)[::-1] * t ** self._orders))

@property
def tan_in(self):
if self.n < 2:
raise ValueError("Need at least two control points to get tangent "
"vector!")
return self.cpoints[1] - self.cpoints[0]

@property
def tan_out(self):
if self.n < 2:
raise ValueError("Need at least two control points to get tangent "
"vector!")
return self.cpoints[-1] - self.cpoints[-2]

@property
def interior_extrema(self):
if self.n <= 2: # a line's extrema are always its tips
return np.array([]), np.array([])
elif self.n == 3: # quadratic curve
# the bezier curve in standard form is
# cp[0] * (1 - t)^2 + cp[1] * 2t(1-t) + cp[2] * t^2
# can be re-written as
# cp[0] + 2 (cp[1] - cp[0]) t + (cp[2] - 2 cp[1] + cp[0]) t^2
# which has simple derivative
# 2*(cp[2] - 2*cp[1] + cp[0]) t + 2*(cp[1] - cp[0])
num = 2*(self.cpoints[2] - 2*self.cpoints[1] + self.cpoints[0])
denom = self.cpoints[1] - self.cpoints[0]
mask = ~np.isclose(denom, 0)
zeros = num[mask]/denom[mask]
dims = np.arange(self.d)[mask]
in_range = (0 <= zeros) & (zeros <= 1)
return dims[in_range], zeros[in_range]
elif self.n == 4: # cubic curve
P = self.cpoints
# derivative of cubic bezier curve has coefficients
a = 3*(P[3] - 3*P[2] + 3*P[1] - P[0])
b = 6*(P[2] - 2*P[1] + P[0])
c = 3*(P[1] - P[0])
discriminant = b**2 - 4*a*c
dims = []
zeros = []
for i in range(self.d):
if discriminant[i] < 0:
continue
roots = [(-b[i] + np.sqrt(discriminant[i]))/2/a[i],
(-b[i] - np.sqrt(discriminant[i]))/2/a[i]]
for root in roots:
if 0 <= root <= 1:
dims.append(i)
zeros.append(root)
return np.asarray(dims), np.asarray(zeros)
else: # self.n > 4:
raise NotImplementedError("Zero finding only implemented up to "
"cubic curves.")


def split_bezier_intersecting_with_closedpath(
bezier, inside_closedpath, tolerance=0.01):
Expand Down Expand Up @@ -225,68 +280,6 @@ def split_bezier_intersecting_with_closedpath(
# matplotlib specific


def split_path_inout(path, inside, tolerance=0.01, reorder_inout=False):
"""
Divide a path into two segments at the point where ``inside(x, y)`` becomes
False.
"""
path_iter = path.iter_segments()

ctl_points, command = next(path_iter)
begin_inside = inside(ctl_points[-2:]) # true if begin point is inside

ctl_points_old = ctl_points

concat = np.concatenate

iold = 0
i = 1

for ctl_points, command in path_iter:
iold = i
i += len(ctl_points) // 2
if inside(ctl_points[-2:]) != begin_inside:
bezier_path = concat([ctl_points_old[-2:], ctl_points])
break
ctl_points_old = ctl_points
else:
raise ValueError("The path does not intersect with the patch")

bp = bezier_path.reshape((-1, 2))
left, right = split_bezier_intersecting_with_closedpath(
bp, inside, tolerance)
if len(left) == 2:
codes_left = [Path.LINETO]
codes_right = [Path.MOVETO, Path.LINETO]
elif len(left) == 3:
codes_left = [Path.CURVE3, Path.CURVE3]
codes_right = [Path.MOVETO, Path.CURVE3, Path.CURVE3]
elif len(left) == 4:
codes_left = [Path.CURVE4, Path.CURVE4, Path.CURVE4]
codes_right = [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4]
else:
raise AssertionError("This should never be reached")

verts_left = left[1:]
verts_right = right[:]

if path.codes is None:
path_in = Path(concat([path.vertices[:i], verts_left]))
path_out = Path(concat([verts_right, path.vertices[i:]]))

else:
path_in = Path(concat([path.vertices[:iold], verts_left]),
concat([path.codes[:iold], codes_left]))

path_out = Path(concat([verts_right, path.vertices[i:]]),
concat([codes_right, path.codes[i:]]))

if reorder_inout and not begin_inside:
path_in, path_out = path_out, path_in

return path_in, path_out


def inside_circle(cx, cy, r):
"""
Return a function that checks whether a point is in a circle with center
Expand All @@ -306,6 +299,7 @@ def _f(xy):

# quadratic Bezier lines


def get_cos_sin(x0, y0, x1, y1):
dx, dy = x1 - x0, y1 - y0
d = (dx * dx + dy * dy) ** .5
Expand Down Expand Up @@ -478,25 +472,3 @@ def make_wedged_bezier2(bezier2, width, w1=1., wm=0.5, w2=0.):
c3x_right, c3y_right)

return path_left, path_right


def make_path_regular(p):
"""
If the ``codes`` attribute of `.Path` *p* is None, return a copy of *p*
with ``codes`` set to (MOVETO, LINETO, LINETO, ..., LINETO); otherwise
return *p* itself.
"""
c = p.codes
if c is None:
c = np.full(len(p.vertices), Path.LINETO, dtype=Path.code_type)
c[0] = Path.MOVETO
return Path(p.vertices, c)
else:
return p


def concatenate_paths(paths):
"""Concatenate a list of paths into a single path."""
vertices = np.concatenate([p.vertices for p in paths])
codes = np.concatenate([make_path_regular(p).codes for p in paths])
return Path(vertices, codes)
9 changes: 7 additions & 2 deletions lib/matplotlib/lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,8 +617,13 @@ def get_window_extent(self, renderer):
ignore=True)
# correct for marker size, if any
if self._marker:
ms = (self._markersize / 72.0 * self.figure.dpi) * 0.5
bbox = bbox.padded(ms)
m_bbox = self._marker.get_bbox(
self._markersize, self._markeredgewidth)
# markers use units of pts, not pixels
box_points_px = renderer.points_to_pixels(m_bbox.get_points())
# add correct padding to each side of bbox (note: get_points means
# the four points of the bbox, not units of "pt".
bbox = Bbox(bbox.get_points() + box_points_px)
return bbox

@Artist.axes.setter
Expand Down
48 changes: 43 additions & 5 deletions lib/matplotlib/markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@

from . import cbook, rcParams
from .path import Path
from .transforms import IdentityTransform, Affine2D
from .transforms import IdentityTransform, Affine2D, Bbox

# special-purpose marker identifiers:
(TICKLEFT, TICKRIGHT, TICKUP, TICKDOWN,
Expand Down Expand Up @@ -198,9 +198,6 @@ class MarkerStyle:
fillstyles = ('full', 'left', 'right', 'bottom', 'top', 'none')
_half_fillstyles = ('left', 'right', 'bottom', 'top')

# TODO: Is this ever used as a non-constant?
_point_size_reduction = 0.5

def __init__(self, marker=None, fillstyle=None):
"""
Attributes
Expand Down Expand Up @@ -408,7 +405,8 @@ def _set_pixel(self):
self._snap_threshold = None

def _set_point(self):
self._set_circle(reduction=self._point_size_reduction)
# a "point" is defined to a circle with half the requested markersize
self._set_circle(reduction=0.5)

_triangle_path = Path(
[[0.0, 1.0], [-1.0, -1.0], [1.0, -1.0], [0.0, 1.0]],
Expand Down Expand Up @@ -898,3 +896,43 @@ def _set_x_filled(self):
self._alt_transform = Affine2D().translate(-0.5, -0.5)
self._transform.rotate_deg(rotate)
self._alt_transform.rotate_deg(rotate_alt)

def get_bbox(self, markersize, markeredgewidth=0, **kwargs):
"""Get size of bbox of marker directly from its path.

Parameters
----------
markersize : float
"Size" of the marker, in points.

markeredgewidth : float, optional, default: 0
Width, in points, of the stroke used to create the marker's edge.

kwargs : Dict[str, object]
forwarded to path's iter_curves and iter_corners

Returns
-------
bbox : matplotlib.transforms.Bbox
The extents of the marker including its edge (in points) if it were
centered at (0,0).

Note
----
The approach used is simply to notice that the bbox with no marker edge
must be defined by a corner (control point of the linear parts of path)
or a an extremal point on one of the curved parts of the path.

For a nonzero marker edge width, because the interior extrema will by
definition be parallel to the bounding box, we need only check if the
path location + width/2 extends the bbox at each interior extrema.
Then, for each join and cap, we check if that join extends the bbox.
"""
if np.isclose(markersize, 0):
return Bbox([[0, 0], [0, 0]])
unit_path = self._transform.transform_path(self._path)
scale = Affine2D().scale(markersize)
path = scale.transform_path(unit_path)
return Bbox.from_extents(path.get_stroked_extents(markeredgewidth,
self._joinstyle,
self._capstyle))
19 changes: 9 additions & 10 deletions lib/matplotlib/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@
import matplotlib as mpl
from . import artist, cbook, colors, docstring, lines as mlines, transforms
from .bezier import (
NonIntersectingPathException, concatenate_paths, get_cos_sin,
get_intersection, get_parallels, inside_circle, make_path_regular,
make_wedged_bezier2, split_bezier_intersecting_with_closedpath,
split_path_inout)
NonIntersectingPathException, get_cos_sin, get_intersection, get_parallels,
inside_circle, make_wedged_bezier2,
split_bezier_intersecting_with_closedpath)
from .path import Path


Expand Down Expand Up @@ -2724,7 +2723,7 @@ def insideA(xy_display):
return patchA.contains(xy_event)[0]

try:
left, right = split_path_inout(path, insideA)
left, right = path.split_path_inout(insideA)
except ValueError:
right = path

Expand All @@ -2736,7 +2735,7 @@ def insideB(xy_display):
return patchB.contains(xy_event)[0]

try:
left, right = split_path_inout(path, insideB)
left, right = path.split_path_inout(insideB)
except ValueError:
left = path

Expand All @@ -2751,13 +2750,13 @@ def _shrink(self, path, shrinkA, shrinkB):
if shrinkA:
insideA = inside_circle(*path.vertices[0], shrinkA)
try:
left, path = split_path_inout(path, insideA)
left, path = path.split_path_inout(insideA)
except ValueError:
pass
if shrinkB:
insideB = inside_circle(*path.vertices[-1], shrinkB)
try:
path, right = split_path_inout(path, insideB)
path, right = path.split_path_inout(insideB)
except ValueError:
pass
return path
Expand Down Expand Up @@ -3187,7 +3186,7 @@ def __call__(self, path, mutation_size, linewidth,
and takes care of the aspect ratio.
"""

path = make_path_regular(path)
path = path.make_path_regular()

if aspect_ratio is not None:
# Squeeze the given height by the aspect_ratio
Expand Down Expand Up @@ -4174,7 +4173,7 @@ def get_path(self):
"""
_path, fillable = self.get_path_in_displaycoord()
if np.iterable(fillable):
_path = concatenate_paths(_path)
_path = Path.make_compound_path(*_path)
return self.get_transform().inverted().transform_path(_path)

def get_path_in_displaycoord(self):
Expand Down
Loading