Skip to content

Polar limits enhancements #4699

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 14 commits into from
Aug 21, 2017
Merged
Next Next commit
ENH: Add an "arc" spine type.
This can be used in the polar axes when plotting less than the full
circle.
  • Loading branch information
QuLogic committed Aug 16, 2017
commit 24cd817c9a98cbf1c6c7673f67e1c65f33e5a15e
95 changes: 75 additions & 20 deletions lib/matplotlib/spines.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ class Spine(mpatches.Patch):
Spines are subclasses of class:`~matplotlib.patches.Patch`, and
inherit much of their behavior.

Spines draw a line or a circle, depending if
function:`~matplotlib.spines.Spine.set_patch_line` or
function:`~matplotlib.spines.Spine.set_patch_circle` has been
called. Line-like is the default.
Spines draw a line, a circle, or an arc depending if
function:`~matplotlib.spines.Spine.set_patch_line`,
function:`~matplotlib.spines.Spine.set_patch_circle`, or
function:`~matplotlib.spines.Spine.set_patch_arc` has been called.
Line-like is the default.

"""
def __str__(self):
Expand Down Expand Up @@ -77,10 +78,11 @@ def __init__(self, axes, spine_type, path, **kwargs):
self._path = path

# To support drawing both linear and circular spines, this
# class implements Patch behavior two ways. If
# class implements Patch behavior three ways. If
# self._patch_type == 'line', behave like a mpatches.PathPatch
# instance. If self._patch_type == 'circle', behave like a
# mpatches.Ellipse instance.
# mpatches.Ellipse instance. If self._patch_type == 'arc', behave like
# a mpatches.Arc instance.
self._patch_type = 'line'

# Behavior copied from mpatches.Ellipse:
Expand All @@ -102,13 +104,25 @@ def get_smart_bounds(self):
"""get whether the spine has smart bounds"""
return self._smart_bounds

def set_patch_arc(self, center, radius, theta1, theta2):
"""set the spine to be arc-like"""
self._patch_type = 'arc'
self._center = center
self._width = radius * 2
self._height = radius * 2
self._theta1 = theta1
self._theta2 = theta2
self._path = mpath.Path.arc(theta1, theta2)
# arc drawn on axes transform
self.set_transform(self.axes.transAxes)
self.stale = True

def set_patch_circle(self, center, radius):
"""set the spine to be circular"""
self._patch_type = 'circle'
self._center = center
self._width = radius * 2
self._height = radius * 2
self._angle = 0
# circle drawn on axes transform
self.set_transform(self.axes.transAxes)
self.stale = True
Expand All @@ -125,18 +139,17 @@ def _recompute_transform(self):
maxes it very important to call the accessor method and
not directly access the transformation member variable.
"""
assert self._patch_type == 'circle'
assert self._patch_type in ('arc', 'circle')
center = (self.convert_xunits(self._center[0]),
self.convert_yunits(self._center[1]))
width = self.convert_xunits(self._width)
height = self.convert_yunits(self._height)
self._patch_transform = mtransforms.Affine2D() \
.scale(width * 0.5, height * 0.5) \
.rotate_deg(self._angle) \
.translate(*center)

def get_patch_transform(self):
if self._patch_type == 'circle':
if self._patch_type in ('arc', 'circle'):
self._recompute_transform()
return self._patch_transform
else:
Expand Down Expand Up @@ -255,17 +268,48 @@ def _adjust_location(self):
else:
low, high = self._bounds

v1 = self._path.vertices
assert v1.shape == (2, 2), 'unexpected vertices shape'
if self.spine_type in ['left', 'right']:
v1[0, 1] = low
v1[1, 1] = high
elif self.spine_type in ['bottom', 'top']:
v1[0, 0] = low
v1[1, 0] = high
if self._patch_type == 'arc':
if self.spine_type in ('bottom', 'top'):
try:
direction = self.axes.get_theta_direction()
except AttributeError:
direction = 1
try:
offset = self.axes.get_theta_offset()
except AttributeError:
offset = 0
low = low * direction + offset
high = high * direction + offset
if low > high:
low, high = high, low

self._path = mpath.Path.arc(np.rad2deg(low), np.rad2deg(high))

if self.spine_type == 'bottom':
rmin, rmax = self.axes.viewLim.intervaly
try:
rorigin = self.axes.get_rorigin()
except AttributeError:
rorigin = rmin
scaled_diameter = (rmin - rorigin) / (rmax - rorigin)
self._height = scaled_diameter
self._width = scaled_diameter

else:
raise ValueError('unable to set bounds for spine "%s"' %
self.spine_type)
else:
raise ValueError('unable to set bounds for spine "%s"' %
self.spine_type)
v1 = self._path.vertices
assert v1.shape == (2, 2), 'unexpected vertices shape'
if self.spine_type in ['left', 'right']:
v1[0, 1] = low
v1[1, 1] = high
elif self.spine_type in ['bottom', 'top']:
v1[0, 0] = low
v1[1, 0] = high
else:
raise ValueError('unable to set bounds for spine "%s"' %
self.spine_type)

@allow_rasterization
def draw(self, renderer):
Expand Down Expand Up @@ -463,6 +507,17 @@ def linear_spine(cls, axes, spine_type, **kwargs):

return result

@classmethod
def arc_spine(cls, axes, spine_type, center, radius, theta1, theta2,
**kwargs):
"""
(classmethod) Returns an arc :class:`Spine`.
"""
path = mpath.Path.arc(theta1, theta2)
result = cls(axes, spine_type, path, **kwargs)
result.set_patch_arc(center, radius, theta1, theta2)
return result

@classmethod
def circular_spine(cls, axes, center, radius, **kwargs):
"""
Expand Down