|
import math |
|
import types |
|
|
|
import numpy as np |
|
|
|
import matplotlib as mpl |
|
from matplotlib import _api, cbook |
|
from matplotlib.axes import Axes |
|
import matplotlib.axis as maxis |
|
import matplotlib.markers as mmarkers |
|
import matplotlib.patches as mpatches |
|
from matplotlib.path import Path |
|
import matplotlib.ticker as mticker |
|
import matplotlib.transforms as mtransforms |
|
from matplotlib.spines import Spine |
|
|
|
|
|
class PolarTransform(mtransforms.Transform): |
|
r""" |
|
The base polar transform. |
|
|
|
This transform maps polar coordinates :math:`\theta, r` into Cartesian |
|
coordinates :math:`x, y = r \cos(\theta), r \sin(\theta)` |
|
(but does not fully transform into Axes coordinates or |
|
handle positioning in screen space). |
|
|
|
This transformation is designed to be applied to data after any scaling |
|
along the radial axis (e.g. log-scaling) has been applied to the input |
|
data. |
|
|
|
Path segments at a fixed radius are automatically transformed to circular |
|
arcs as long as ``path._interpolation_steps > 1``. |
|
""" |
|
|
|
input_dims = output_dims = 2 |
|
|
|
def __init__(self, axis=None, use_rmin=True, |
|
_apply_theta_transforms=True, *, scale_transform=None): |
|
""" |
|
Parameters |
|
---------- |
|
axis : `~matplotlib.axis.Axis`, optional |
|
Axis associated with this transform. This is used to get the |
|
minimum radial limit. |
|
use_rmin : `bool`, optional |
|
If ``True``, subtract the minimum radial axis limit before |
|
transforming to Cartesian coordinates. *axis* must also be |
|
specified for this to take effect. |
|
""" |
|
super().__init__() |
|
self._axis = axis |
|
self._use_rmin = use_rmin |
|
self._apply_theta_transforms = _apply_theta_transforms |
|
self._scale_transform = scale_transform |
|
|
|
__str__ = mtransforms._make_str_method( |
|
"_axis", |
|
use_rmin="_use_rmin", |
|
_apply_theta_transforms="_apply_theta_transforms") |
|
|
|
def _get_rorigin(self): |
|
|
|
return self._scale_transform.transform( |
|
(0, self._axis.get_rorigin()))[1] |
|
|
|
@_api.rename_parameter("3.8", "tr", "values") |
|
def transform_non_affine(self, values): |
|
|
|
theta, r = np.transpose(values) |
|
|
|
|
|
if self._apply_theta_transforms and self._axis is not None: |
|
theta *= self._axis.get_theta_direction() |
|
theta += self._axis.get_theta_offset() |
|
if self._use_rmin and self._axis is not None: |
|
r = (r - self._get_rorigin()) * self._axis.get_rsign() |
|
r = np.where(r >= 0, r, np.nan) |
|
return np.column_stack([r * np.cos(theta), r * np.sin(theta)]) |
|
|
|
def transform_path_non_affine(self, path): |
|
|
|
if not len(path) or path._interpolation_steps == 1: |
|
return Path(self.transform_non_affine(path.vertices), path.codes) |
|
xys = [] |
|
codes = [] |
|
last_t = last_r = None |
|
for trs, c in path.iter_segments(): |
|
trs = trs.reshape((-1, 2)) |
|
if c == Path.LINETO: |
|
(t, r), = trs |
|
if t == last_t: |
|
xys.extend(self.transform_non_affine(trs)) |
|
codes.append(Path.LINETO) |
|
elif r == last_r: |
|
|
|
|
|
|
|
last_td, td = np.rad2deg([last_t, t]) |
|
if self._use_rmin and self._axis is not None: |
|
r = ((r - self._get_rorigin()) |
|
* self._axis.get_rsign()) |
|
if last_td <= td: |
|
while td - last_td > 360: |
|
arc = Path.arc(last_td, last_td + 360) |
|
xys.extend(arc.vertices[1:] * r) |
|
codes.extend(arc.codes[1:]) |
|
last_td += 360 |
|
arc = Path.arc(last_td, td) |
|
xys.extend(arc.vertices[1:] * r) |
|
codes.extend(arc.codes[1:]) |
|
else: |
|
|
|
|
|
while last_td - td > 360: |
|
arc = Path.arc(last_td - 360, last_td) |
|
xys.extend(arc.vertices[::-1][1:] * r) |
|
codes.extend(arc.codes[1:]) |
|
last_td -= 360 |
|
arc = Path.arc(td, last_td) |
|
xys.extend(arc.vertices[::-1][1:] * r) |
|
codes.extend(arc.codes[1:]) |
|
else: |
|
trs = cbook.simple_linear_interpolation( |
|
np.vstack([(last_t, last_r), trs]), |
|
path._interpolation_steps)[1:] |
|
xys.extend(self.transform_non_affine(trs)) |
|
codes.extend([Path.LINETO] * len(trs)) |
|
else: |
|
xys.extend(self.transform_non_affine(trs)) |
|
codes.extend([c] * len(trs)) |
|
last_t, last_r = trs[-1] |
|
return Path(xys, codes) |
|
|
|
def inverted(self): |
|
|
|
return PolarAxes.InvertedPolarTransform(self._axis, self._use_rmin, |
|
self._apply_theta_transforms) |
|
|
|
|
|
class PolarAffine(mtransforms.Affine2DBase): |
|
r""" |
|
The affine part of the polar projection. |
|
|
|
Scales the output so that maximum radius rests on the edge of the axes |
|
circle and the origin is mapped to (0.5, 0.5). The transform applied is |
|
the same to x and y components and given by: |
|
|
|
.. math:: |
|
|
|
x_{1} = 0.5 \left [ \frac{x_{0}}{(r_{\max} - r_{\min})} + 1 \right ] |
|
|
|
:math:`r_{\min}, r_{\max}` are the minimum and maximum radial limits after |
|
any scaling (e.g. log scaling) has been removed. |
|
""" |
|
def __init__(self, scale_transform, limits): |
|
""" |
|
Parameters |
|
---------- |
|
scale_transform : `~matplotlib.transforms.Transform` |
|
Scaling transform for the data. This is used to remove any scaling |
|
from the radial view limits. |
|
limits : `~matplotlib.transforms.BboxBase` |
|
View limits of the data. The only part of its bounds that is used |
|
is the y limits (for the radius limits). |
|
""" |
|
super().__init__() |
|
self._scale_transform = scale_transform |
|
self._limits = limits |
|
self.set_children(scale_transform, limits) |
|
self._mtx = None |
|
|
|
__str__ = mtransforms._make_str_method("_scale_transform", "_limits") |
|
|
|
def get_matrix(self): |
|
|
|
if self._invalid: |
|
limits_scaled = self._limits.transformed(self._scale_transform) |
|
yscale = limits_scaled.ymax - limits_scaled.ymin |
|
affine = mtransforms.Affine2D() \ |
|
.scale(0.5 / yscale) \ |
|
.translate(0.5, 0.5) |
|
self._mtx = affine.get_matrix() |
|
self._inverted = None |
|
self._invalid = 0 |
|
return self._mtx |
|
|
|
|
|
class InvertedPolarTransform(mtransforms.Transform): |
|
""" |
|
The inverse of the polar transform, mapping Cartesian |
|
coordinate space *x* and *y* back to *theta* and *r*. |
|
""" |
|
input_dims = output_dims = 2 |
|
|
|
def __init__(self, axis=None, use_rmin=True, |
|
_apply_theta_transforms=True): |
|
""" |
|
Parameters |
|
---------- |
|
axis : `~matplotlib.axis.Axis`, optional |
|
Axis associated with this transform. This is used to get the |
|
minimum radial limit. |
|
use_rmin : `bool`, optional |
|
If ``True`` add the minimum radial axis limit after |
|
transforming from Cartesian coordinates. *axis* must also be |
|
specified for this to take effect. |
|
""" |
|
super().__init__() |
|
self._axis = axis |
|
self._use_rmin = use_rmin |
|
self._apply_theta_transforms = _apply_theta_transforms |
|
|
|
__str__ = mtransforms._make_str_method( |
|
"_axis", |
|
use_rmin="_use_rmin", |
|
_apply_theta_transforms="_apply_theta_transforms") |
|
|
|
@_api.rename_parameter("3.8", "xy", "values") |
|
def transform_non_affine(self, values): |
|
|
|
x, y = values.T |
|
r = np.hypot(x, y) |
|
theta = (np.arctan2(y, x) + 2 * np.pi) % (2 * np.pi) |
|
|
|
|
|
if self._apply_theta_transforms and self._axis is not None: |
|
theta -= self._axis.get_theta_offset() |
|
theta *= self._axis.get_theta_direction() |
|
theta %= 2 * np.pi |
|
if self._use_rmin and self._axis is not None: |
|
r += self._axis.get_rorigin() |
|
r *= self._axis.get_rsign() |
|
return np.column_stack([theta, r]) |
|
|
|
def inverted(self): |
|
|
|
return PolarAxes.PolarTransform(self._axis, self._use_rmin, |
|
self._apply_theta_transforms) |
|
|
|
|
|
class ThetaFormatter(mticker.Formatter): |
|
""" |
|
Used to format the *theta* tick labels. Converts the native |
|
unit of radians into degrees and adds a degree symbol. |
|
""" |
|
|
|
def __call__(self, x, pos=None): |
|
vmin, vmax = self.axis.get_view_interval() |
|
d = np.rad2deg(abs(vmax - vmin)) |
|
digits = max(-int(np.log10(d) - 1.5), 0) |
|
|
|
|
|
|
|
|
|
return f"{np.rad2deg(x):0.{digits:d}f}\N{DEGREE SIGN}" |
|
|
|
|
|
class _AxisWrapper: |
|
def __init__(self, axis): |
|
self._axis = axis |
|
|
|
def get_view_interval(self): |
|
return np.rad2deg(self._axis.get_view_interval()) |
|
|
|
def set_view_interval(self, vmin, vmax): |
|
self._axis.set_view_interval(*np.deg2rad((vmin, vmax))) |
|
|
|
def get_minpos(self): |
|
return np.rad2deg(self._axis.get_minpos()) |
|
|
|
def get_data_interval(self): |
|
return np.rad2deg(self._axis.get_data_interval()) |
|
|
|
def set_data_interval(self, vmin, vmax): |
|
self._axis.set_data_interval(*np.deg2rad((vmin, vmax))) |
|
|
|
def get_tick_space(self): |
|
return self._axis.get_tick_space() |
|
|
|
|
|
class ThetaLocator(mticker.Locator): |
|
""" |
|
Used to locate theta ticks. |
|
|
|
This will work the same as the base locator except in the case that the |
|
view spans the entire circle. In such cases, the previously used default |
|
locations of every 45 degrees are returned. |
|
""" |
|
|
|
def __init__(self, base): |
|
self.base = base |
|
self.axis = self.base.axis = _AxisWrapper(self.base.axis) |
|
|
|
def set_axis(self, axis): |
|
self.axis = _AxisWrapper(axis) |
|
self.base.set_axis(self.axis) |
|
|
|
def __call__(self): |
|
lim = self.axis.get_view_interval() |
|
if _is_full_circle_deg(lim[0], lim[1]): |
|
return np.deg2rad(min(lim)) + np.arange(8) * 2 * np.pi / 8 |
|
else: |
|
return np.deg2rad(self.base()) |
|
|
|
def view_limits(self, vmin, vmax): |
|
vmin, vmax = np.rad2deg((vmin, vmax)) |
|
return np.deg2rad(self.base.view_limits(vmin, vmax)) |
|
|
|
|
|
class ThetaTick(maxis.XTick): |
|
""" |
|
A theta-axis tick. |
|
|
|
This subclass of `.XTick` provides angular ticks with some small |
|
modification to their re-positioning such that ticks are rotated based on |
|
tick location. This results in ticks that are correctly perpendicular to |
|
the arc spine. |
|
|
|
When 'auto' rotation is enabled, labels are also rotated to be parallel to |
|
the spine. The label padding is also applied here since it's not possible |
|
to use a generic axes transform to produce tick-specific padding. |
|
""" |
|
|
|
def __init__(self, axes, *args, **kwargs): |
|
self._text1_translate = mtransforms.ScaledTranslation( |
|
0, 0, axes.figure.dpi_scale_trans) |
|
self._text2_translate = mtransforms.ScaledTranslation( |
|
0, 0, axes.figure.dpi_scale_trans) |
|
super().__init__(axes, *args, **kwargs) |
|
self.label1.set( |
|
rotation_mode='anchor', |
|
transform=self.label1.get_transform() + self._text1_translate) |
|
self.label2.set( |
|
rotation_mode='anchor', |
|
transform=self.label2.get_transform() + self._text2_translate) |
|
|
|
def _apply_params(self, **kwargs): |
|
super()._apply_params(**kwargs) |
|
|
|
trans = self.label1.get_transform() |
|
if not trans.contains_branch(self._text1_translate): |
|
self.label1.set_transform(trans + self._text1_translate) |
|
trans = self.label2.get_transform() |
|
if not trans.contains_branch(self._text2_translate): |
|
self.label2.set_transform(trans + self._text2_translate) |
|
|
|
def _update_padding(self, pad, angle): |
|
padx = pad * np.cos(angle) / 72 |
|
pady = pad * np.sin(angle) / 72 |
|
self._text1_translate._t = (padx, pady) |
|
self._text1_translate.invalidate() |
|
self._text2_translate._t = (-padx, -pady) |
|
self._text2_translate.invalidate() |
|
|
|
def update_position(self, loc): |
|
super().update_position(loc) |
|
axes = self.axes |
|
angle = loc * axes.get_theta_direction() + axes.get_theta_offset() |
|
text_angle = np.rad2deg(angle) % 360 - 90 |
|
angle -= np.pi / 2 |
|
|
|
marker = self.tick1line.get_marker() |
|
if marker in (mmarkers.TICKUP, '|'): |
|
trans = mtransforms.Affine2D().scale(1, 1).rotate(angle) |
|
elif marker == mmarkers.TICKDOWN: |
|
trans = mtransforms.Affine2D().scale(1, -1).rotate(angle) |
|
else: |
|
|
|
trans = self.tick1line._marker._transform |
|
self.tick1line._marker._transform = trans |
|
|
|
marker = self.tick2line.get_marker() |
|
if marker in (mmarkers.TICKUP, '|'): |
|
trans = mtransforms.Affine2D().scale(1, 1).rotate(angle) |
|
elif marker == mmarkers.TICKDOWN: |
|
trans = mtransforms.Affine2D().scale(1, -1).rotate(angle) |
|
else: |
|
|
|
trans = self.tick2line._marker._transform |
|
self.tick2line._marker._transform = trans |
|
|
|
mode, user_angle = self._labelrotation |
|
if mode == 'default': |
|
text_angle = user_angle |
|
else: |
|
if text_angle > 90: |
|
text_angle -= 180 |
|
elif text_angle < -90: |
|
text_angle += 180 |
|
text_angle += user_angle |
|
self.label1.set_rotation(text_angle) |
|
self.label2.set_rotation(text_angle) |
|
|
|
|
|
|
|
pad = self._pad + 7 |
|
self._update_padding(pad, |
|
self._loc * axes.get_theta_direction() + |
|
axes.get_theta_offset()) |
|
|
|
|
|
class ThetaAxis(maxis.XAxis): |
|
""" |
|
A theta Axis. |
|
|
|
This overrides certain properties of an `.XAxis` to provide special-casing |
|
for an angular axis. |
|
""" |
|
__name__ = 'thetaaxis' |
|
axis_name = 'theta' |
|
_tick_class = ThetaTick |
|
|
|
def _wrap_locator_formatter(self): |
|
self.set_major_locator(ThetaLocator(self.get_major_locator())) |
|
self.set_major_formatter(ThetaFormatter()) |
|
self.isDefault_majloc = True |
|
self.isDefault_majfmt = True |
|
|
|
def clear(self): |
|
|
|
super().clear() |
|
self.set_ticks_position('none') |
|
self._wrap_locator_formatter() |
|
|
|
def _set_scale(self, value, **kwargs): |
|
if value != 'linear': |
|
raise NotImplementedError( |
|
"The xscale cannot be set on a polar plot") |
|
super()._set_scale(value, **kwargs) |
|
|
|
|
|
|
|
self.get_major_locator().set_params(steps=[1, 1.5, 3, 4.5, 9, 10]) |
|
self._wrap_locator_formatter() |
|
|
|
def _copy_tick_props(self, src, dest): |
|
"""Copy the props from src tick to dest tick.""" |
|
if src is None or dest is None: |
|
return |
|
super()._copy_tick_props(src, dest) |
|
|
|
|
|
trans = dest._get_text1_transform()[0] |
|
dest.label1.set_transform(trans + dest._text1_translate) |
|
trans = dest._get_text2_transform()[0] |
|
dest.label2.set_transform(trans + dest._text2_translate) |
|
|
|
|
|
class RadialLocator(mticker.Locator): |
|
""" |
|
Used to locate radius ticks. |
|
|
|
Ensures that all ticks are strictly positive. For all other tasks, it |
|
delegates to the base `.Locator` (which may be different depending on the |
|
scale of the *r*-axis). |
|
""" |
|
|
|
def __init__(self, base, axes=None): |
|
self.base = base |
|
self._axes = axes |
|
|
|
def set_axis(self, axis): |
|
self.base.set_axis(axis) |
|
|
|
def __call__(self): |
|
|
|
if self._axes: |
|
if _is_full_circle_rad(*self._axes.viewLim.intervalx): |
|
rorigin = self._axes.get_rorigin() * self._axes.get_rsign() |
|
if self._axes.get_rmin() <= rorigin: |
|
return [tick for tick in self.base() if tick > rorigin] |
|
return self.base() |
|
|
|
def _zero_in_bounds(self): |
|
""" |
|
Return True if zero is within the valid values for the |
|
scale of the radial axis. |
|
""" |
|
vmin, vmax = self._axes.yaxis._scale.limit_range_for_scale(0, 1, 1e-5) |
|
return vmin == 0 |
|
|
|
def nonsingular(self, vmin, vmax): |
|
|
|
if self._zero_in_bounds() and (vmin, vmax) == (-np.inf, np.inf): |
|
|
|
return (0, 1) |
|
else: |
|
return self.base.nonsingular(vmin, vmax) |
|
|
|
def view_limits(self, vmin, vmax): |
|
vmin, vmax = self.base.view_limits(vmin, vmax) |
|
if self._zero_in_bounds() and vmax > vmin: |
|
|
|
vmin = min(0, vmin) |
|
return mtransforms.nonsingular(vmin, vmax) |
|
|
|
|
|
class _ThetaShift(mtransforms.ScaledTranslation): |
|
""" |
|
Apply a padding shift based on axes theta limits. |
|
|
|
This is used to create padding for radial ticks. |
|
|
|
Parameters |
|
---------- |
|
axes : `~matplotlib.axes.Axes` |
|
The owning axes; used to determine limits. |
|
pad : float |
|
The padding to apply, in points. |
|
mode : {'min', 'max', 'rlabel'} |
|
Whether to shift away from the start (``'min'``) or the end (``'max'``) |
|
of the axes, or using the rlabel position (``'rlabel'``). |
|
""" |
|
def __init__(self, axes, pad, mode): |
|
super().__init__(pad, pad, axes.figure.dpi_scale_trans) |
|
self.set_children(axes._realViewLim) |
|
self.axes = axes |
|
self.mode = mode |
|
self.pad = pad |
|
|
|
__str__ = mtransforms._make_str_method("axes", "pad", "mode") |
|
|
|
def get_matrix(self): |
|
if self._invalid: |
|
if self.mode == 'rlabel': |
|
angle = ( |
|
np.deg2rad(self.axes.get_rlabel_position()) * |
|
self.axes.get_theta_direction() + |
|
self.axes.get_theta_offset() |
|
) |
|
else: |
|
if self.mode == 'min': |
|
angle = self.axes._realViewLim.xmin |
|
elif self.mode == 'max': |
|
angle = self.axes._realViewLim.xmax |
|
|
|
if self.mode in ('rlabel', 'min'): |
|
padx = np.cos(angle - np.pi / 2) |
|
pady = np.sin(angle - np.pi / 2) |
|
else: |
|
padx = np.cos(angle + np.pi / 2) |
|
pady = np.sin(angle + np.pi / 2) |
|
|
|
self._t = (self.pad * padx / 72, self.pad * pady / 72) |
|
return super().get_matrix() |
|
|
|
|
|
class RadialTick(maxis.YTick): |
|
""" |
|
A radial-axis tick. |
|
|
|
This subclass of `.YTick` provides radial ticks with some small |
|
modification to their re-positioning such that ticks are rotated based on |
|
axes limits. This results in ticks that are correctly perpendicular to |
|
the spine. Labels are also rotated to be perpendicular to the spine, when |
|
'auto' rotation is enabled. |
|
""" |
|
|
|
def __init__(self, *args, **kwargs): |
|
super().__init__(*args, **kwargs) |
|
self.label1.set_rotation_mode('anchor') |
|
self.label2.set_rotation_mode('anchor') |
|
|
|
def _determine_anchor(self, mode, angle, start): |
|
|
|
|
|
if mode == 'auto': |
|
if start: |
|
if -90 <= angle <= 90: |
|
return 'left', 'center' |
|
else: |
|
return 'right', 'center' |
|
else: |
|
if -90 <= angle <= 90: |
|
return 'right', 'center' |
|
else: |
|
return 'left', 'center' |
|
else: |
|
if start: |
|
if angle < -68.5: |
|
return 'center', 'top' |
|
elif angle < -23.5: |
|
return 'left', 'top' |
|
elif angle < 22.5: |
|
return 'left', 'center' |
|
elif angle < 67.5: |
|
return 'left', 'bottom' |
|
elif angle < 112.5: |
|
return 'center', 'bottom' |
|
elif angle < 157.5: |
|
return 'right', 'bottom' |
|
elif angle < 202.5: |
|
return 'right', 'center' |
|
elif angle < 247.5: |
|
return 'right', 'top' |
|
else: |
|
return 'center', 'top' |
|
else: |
|
if angle < -68.5: |
|
return 'center', 'bottom' |
|
elif angle < -23.5: |
|
return 'right', 'bottom' |
|
elif angle < 22.5: |
|
return 'right', 'center' |
|
elif angle < 67.5: |
|
return 'right', 'top' |
|
elif angle < 112.5: |
|
return 'center', 'top' |
|
elif angle < 157.5: |
|
return 'left', 'top' |
|
elif angle < 202.5: |
|
return 'left', 'center' |
|
elif angle < 247.5: |
|
return 'left', 'bottom' |
|
else: |
|
return 'center', 'bottom' |
|
|
|
def update_position(self, loc): |
|
super().update_position(loc) |
|
axes = self.axes |
|
thetamin = axes.get_thetamin() |
|
thetamax = axes.get_thetamax() |
|
direction = axes.get_theta_direction() |
|
offset_rad = axes.get_theta_offset() |
|
offset = np.rad2deg(offset_rad) |
|
full = _is_full_circle_deg(thetamin, thetamax) |
|
|
|
if full: |
|
angle = (axes.get_rlabel_position() * direction + |
|
offset) % 360 - 90 |
|
tick_angle = 0 |
|
else: |
|
angle = (thetamin * direction + offset) % 360 - 90 |
|
if direction > 0: |
|
tick_angle = np.deg2rad(angle) |
|
else: |
|
tick_angle = np.deg2rad(angle + 180) |
|
text_angle = (angle + 90) % 180 - 90 |
|
mode, user_angle = self._labelrotation |
|
if mode == 'auto': |
|
text_angle += user_angle |
|
else: |
|
text_angle = user_angle |
|
|
|
if full: |
|
ha = self.label1.get_horizontalalignment() |
|
va = self.label1.get_verticalalignment() |
|
else: |
|
ha, va = self._determine_anchor(mode, angle, direction > 0) |
|
self.label1.set_horizontalalignment(ha) |
|
self.label1.set_verticalalignment(va) |
|
self.label1.set_rotation(text_angle) |
|
|
|
marker = self.tick1line.get_marker() |
|
if marker == mmarkers.TICKLEFT: |
|
trans = mtransforms.Affine2D().rotate(tick_angle) |
|
elif marker == '_': |
|
trans = mtransforms.Affine2D().rotate(tick_angle + np.pi / 2) |
|
elif marker == mmarkers.TICKRIGHT: |
|
trans = mtransforms.Affine2D().scale(-1, 1).rotate(tick_angle) |
|
else: |
|
|
|
trans = self.tick1line._marker._transform |
|
self.tick1line._marker._transform = trans |
|
|
|
if full: |
|
self.label2.set_visible(False) |
|
self.tick2line.set_visible(False) |
|
angle = (thetamax * direction + offset) % 360 - 90 |
|
if direction > 0: |
|
tick_angle = np.deg2rad(angle) |
|
else: |
|
tick_angle = np.deg2rad(angle + 180) |
|
text_angle = (angle + 90) % 180 - 90 |
|
mode, user_angle = self._labelrotation |
|
if mode == 'auto': |
|
text_angle += user_angle |
|
else: |
|
text_angle = user_angle |
|
|
|
ha, va = self._determine_anchor(mode, angle, direction < 0) |
|
self.label2.set_ha(ha) |
|
self.label2.set_va(va) |
|
self.label2.set_rotation(text_angle) |
|
|
|
marker = self.tick2line.get_marker() |
|
if marker == mmarkers.TICKLEFT: |
|
trans = mtransforms.Affine2D().rotate(tick_angle) |
|
elif marker == '_': |
|
trans = mtransforms.Affine2D().rotate(tick_angle + np.pi / 2) |
|
elif marker == mmarkers.TICKRIGHT: |
|
trans = mtransforms.Affine2D().scale(-1, 1).rotate(tick_angle) |
|
else: |
|
|
|
trans = self.tick2line._marker._transform |
|
self.tick2line._marker._transform = trans |
|
|
|
|
|
class RadialAxis(maxis.YAxis): |
|
""" |
|
A radial Axis. |
|
|
|
This overrides certain properties of a `.YAxis` to provide special-casing |
|
for a radial axis. |
|
""" |
|
__name__ = 'radialaxis' |
|
axis_name = 'radius' |
|
_tick_class = RadialTick |
|
|
|
def __init__(self, *args, **kwargs): |
|
super().__init__(*args, **kwargs) |
|
self.sticky_edges.y.append(0) |
|
|
|
def _wrap_locator_formatter(self): |
|
self.set_major_locator(RadialLocator(self.get_major_locator(), |
|
self.axes)) |
|
self.isDefault_majloc = True |
|
|
|
def clear(self): |
|
|
|
super().clear() |
|
self.set_ticks_position('none') |
|
self._wrap_locator_formatter() |
|
|
|
def _set_scale(self, value, **kwargs): |
|
super()._set_scale(value, **kwargs) |
|
self._wrap_locator_formatter() |
|
|
|
|
|
def _is_full_circle_deg(thetamin, thetamax): |
|
""" |
|
Determine if a wedge (in degrees) spans the full circle. |
|
|
|
The condition is derived from :class:`~matplotlib.patches.Wedge`. |
|
""" |
|
return abs(abs(thetamax - thetamin) - 360.0) < 1e-12 |
|
|
|
|
|
def _is_full_circle_rad(thetamin, thetamax): |
|
""" |
|
Determine if a wedge (in radians) spans the full circle. |
|
|
|
The condition is derived from :class:`~matplotlib.patches.Wedge`. |
|
""" |
|
return abs(abs(thetamax - thetamin) - 2 * np.pi) < 1.74e-14 |
|
|
|
|
|
class _WedgeBbox(mtransforms.Bbox): |
|
""" |
|
Transform (theta, r) wedge Bbox into axes bounding box. |
|
|
|
Parameters |
|
---------- |
|
center : (float, float) |
|
Center of the wedge |
|
viewLim : `~matplotlib.transforms.Bbox` |
|
Bbox determining the boundaries of the wedge |
|
originLim : `~matplotlib.transforms.Bbox` |
|
Bbox determining the origin for the wedge, if different from *viewLim* |
|
""" |
|
def __init__(self, center, viewLim, originLim, **kwargs): |
|
super().__init__([[0, 0], [1, 1]], **kwargs) |
|
self._center = center |
|
self._viewLim = viewLim |
|
self._originLim = originLim |
|
self.set_children(viewLim, originLim) |
|
|
|
__str__ = mtransforms._make_str_method("_center", "_viewLim", "_originLim") |
|
|
|
def get_points(self): |
|
|
|
if self._invalid: |
|
points = self._viewLim.get_points().copy() |
|
|
|
points[:, 0] *= 180 / np.pi |
|
if points[0, 0] > points[1, 0]: |
|
points[:, 0] = points[::-1, 0] |
|
|
|
|
|
points[:, 1] -= self._originLim.y0 |
|
|
|
|
|
rscale = 0.5 / points[1, 1] |
|
points[:, 1] *= rscale |
|
width = min(points[1, 1] - points[0, 1], 0.5) |
|
|
|
|
|
wedge = mpatches.Wedge(self._center, points[1, 1], |
|
points[0, 0], points[1, 0], |
|
width=width) |
|
self.update_from_path(wedge.get_path()) |
|
|
|
|
|
w, h = self._points[1] - self._points[0] |
|
deltah = max(w - h, 0) / 2 |
|
deltaw = max(h - w, 0) / 2 |
|
self._points += np.array([[-deltaw, -deltah], [deltaw, deltah]]) |
|
|
|
self._invalid = 0 |
|
|
|
return self._points |
|
|
|
|
|
class PolarAxes(Axes): |
|
""" |
|
A polar graph projection, where the input dimensions are *theta*, *r*. |
|
|
|
Theta starts pointing east and goes anti-clockwise. |
|
""" |
|
name = 'polar' |
|
|
|
def __init__(self, *args, |
|
theta_offset=0, theta_direction=1, rlabel_position=22.5, |
|
**kwargs): |
|
|
|
self._default_theta_offset = theta_offset |
|
self._default_theta_direction = theta_direction |
|
self._default_rlabel_position = np.deg2rad(rlabel_position) |
|
super().__init__(*args, **kwargs) |
|
self.use_sticky_edges = True |
|
self.set_aspect('equal', adjustable='box', anchor='C') |
|
self.clear() |
|
|
|
def clear(self): |
|
|
|
super().clear() |
|
|
|
self.title.set_y(1.05) |
|
|
|
start = self.spines.get('start', None) |
|
if start: |
|
start.set_visible(False) |
|
end = self.spines.get('end', None) |
|
if end: |
|
end.set_visible(False) |
|
self.set_xlim(0.0, 2 * np.pi) |
|
|
|
self.grid(mpl.rcParams['polaraxes.grid']) |
|
inner = self.spines.get('inner', None) |
|
if inner: |
|
inner.set_visible(False) |
|
|
|
self.set_rorigin(None) |
|
self.set_theta_offset(self._default_theta_offset) |
|
self.set_theta_direction(self._default_theta_direction) |
|
|
|
def _init_axis(self): |
|
|
|
self.xaxis = ThetaAxis(self, clear=False) |
|
self.yaxis = RadialAxis(self, clear=False) |
|
self.spines['polar'].register_axis(self.yaxis) |
|
|
|
def _set_lim_and_transforms(self): |
|
|
|
|
|
self._originViewLim = mtransforms.LockableBbox(self.viewLim) |
|
|
|
|
|
self._direction = mtransforms.Affine2D() \ |
|
.scale(self._default_theta_direction, 1.0) |
|
self._theta_offset = mtransforms.Affine2D() \ |
|
.translate(self._default_theta_offset, 0.0) |
|
self.transShift = self._direction + self._theta_offset |
|
|
|
|
|
self._realViewLim = mtransforms.TransformedBbox(self.viewLim, |
|
self.transShift) |
|
|
|
|
|
|
|
self.transScale = mtransforms.TransformWrapper( |
|
mtransforms.IdentityTransform()) |
|
|
|
|
|
|
|
|
|
self.axesLim = _WedgeBbox((0.5, 0.5), |
|
self._realViewLim, self._originViewLim) |
|
|
|
|
|
self.transWedge = mtransforms.BboxTransformFrom(self.axesLim) |
|
|
|
|
|
self.transAxes = mtransforms.BboxTransformTo(self.bbox) |
|
|
|
|
|
|
|
self.transProjection = self.PolarTransform( |
|
self, |
|
_apply_theta_transforms=False, |
|
scale_transform=self.transScale |
|
) |
|
|
|
self.transProjection.set_children(self._originViewLim) |
|
|
|
|
|
|
|
self.transProjectionAffine = self.PolarAffine(self.transScale, |
|
self._originViewLim) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.transData = ( |
|
self.transScale + |
|
self.transShift + |
|
self.transProjection + |
|
( |
|
self.transProjectionAffine + |
|
self.transWedge + |
|
self.transAxes |
|
) |
|
) |
|
|
|
|
|
|
|
|
|
self._xaxis_transform = ( |
|
mtransforms.blended_transform_factory( |
|
mtransforms.IdentityTransform(), |
|
mtransforms.BboxTransformTo(self.viewLim)) + |
|
self.transData) |
|
|
|
|
|
flipr_transform = mtransforms.Affine2D() \ |
|
.translate(0.0, -0.5) \ |
|
.scale(1.0, -1.0) \ |
|
.translate(0.0, 0.5) |
|
self._xaxis_text_transform = flipr_transform + self._xaxis_transform |
|
|
|
|
|
|
|
|
|
self._yaxis_transform = ( |
|
mtransforms.blended_transform_factory( |
|
mtransforms.BboxTransformTo(self.viewLim), |
|
mtransforms.IdentityTransform()) + |
|
self.transData) |
|
|
|
self._r_label_position = mtransforms.Affine2D() \ |
|
.translate(self._default_rlabel_position, 0.0) |
|
self._yaxis_text_transform = mtransforms.TransformWrapper( |
|
self._r_label_position + self.transData) |
|
|
|
def get_xaxis_transform(self, which='grid'): |
|
_api.check_in_list(['tick1', 'tick2', 'grid'], which=which) |
|
return self._xaxis_transform |
|
|
|
def get_xaxis_text1_transform(self, pad): |
|
return self._xaxis_text_transform, 'center', 'center' |
|
|
|
def get_xaxis_text2_transform(self, pad): |
|
return self._xaxis_text_transform, 'center', 'center' |
|
|
|
def get_yaxis_transform(self, which='grid'): |
|
if which in ('tick1', 'tick2'): |
|
return self._yaxis_text_transform |
|
elif which == 'grid': |
|
return self._yaxis_transform |
|
else: |
|
_api.check_in_list(['tick1', 'tick2', 'grid'], which=which) |
|
|
|
def get_yaxis_text1_transform(self, pad): |
|
thetamin, thetamax = self._realViewLim.intervalx |
|
if _is_full_circle_rad(thetamin, thetamax): |
|
return self._yaxis_text_transform, 'bottom', 'left' |
|
elif self.get_theta_direction() > 0: |
|
halign = 'left' |
|
pad_shift = _ThetaShift(self, pad, 'min') |
|
else: |
|
halign = 'right' |
|
pad_shift = _ThetaShift(self, pad, 'max') |
|
return self._yaxis_text_transform + pad_shift, 'center', halign |
|
|
|
def get_yaxis_text2_transform(self, pad): |
|
if self.get_theta_direction() > 0: |
|
halign = 'right' |
|
pad_shift = _ThetaShift(self, pad, 'max') |
|
else: |
|
halign = 'left' |
|
pad_shift = _ThetaShift(self, pad, 'min') |
|
return self._yaxis_text_transform + pad_shift, 'center', halign |
|
|
|
def draw(self, renderer): |
|
self._unstale_viewLim() |
|
thetamin, thetamax = np.rad2deg(self._realViewLim.intervalx) |
|
if thetamin > thetamax: |
|
thetamin, thetamax = thetamax, thetamin |
|
rmin, rmax = ((self._realViewLim.intervaly - self.get_rorigin()) * |
|
self.get_rsign()) |
|
if isinstance(self.patch, mpatches.Wedge): |
|
|
|
|
|
center = self.transWedge.transform((0.5, 0.5)) |
|
self.patch.set_center(center) |
|
self.patch.set_theta1(thetamin) |
|
self.patch.set_theta2(thetamax) |
|
|
|
edge, _ = self.transWedge.transform((1, 0)) |
|
radius = edge - center[0] |
|
width = min(radius * (rmax - rmin) / rmax, radius) |
|
self.patch.set_radius(radius) |
|
self.patch.set_width(width) |
|
|
|
inner_width = radius - width |
|
inner = self.spines.get('inner', None) |
|
if inner: |
|
inner.set_visible(inner_width != 0.0) |
|
|
|
visible = not _is_full_circle_deg(thetamin, thetamax) |
|
|
|
|
|
start = self.spines.get('start', None) |
|
end = self.spines.get('end', None) |
|
if start: |
|
start.set_visible(visible) |
|
if end: |
|
end.set_visible(visible) |
|
if visible: |
|
yaxis_text_transform = self._yaxis_transform |
|
else: |
|
yaxis_text_transform = self._r_label_position + self.transData |
|
if self._yaxis_text_transform != yaxis_text_transform: |
|
self._yaxis_text_transform.set(yaxis_text_transform) |
|
self.yaxis.reset_ticks() |
|
self.yaxis.set_clip_path(self.patch) |
|
|
|
super().draw(renderer) |
|
|
|
def _gen_axes_patch(self): |
|
return mpatches.Wedge((0.5, 0.5), 0.5, 0.0, 360.0) |
|
|
|
def _gen_axes_spines(self): |
|
spines = { |
|
'polar': Spine.arc_spine(self, 'top', (0.5, 0.5), 0.5, 0, 360), |
|
'start': Spine.linear_spine(self, 'left'), |
|
'end': Spine.linear_spine(self, 'right'), |
|
'inner': Spine.arc_spine(self, 'bottom', (0.5, 0.5), 0.0, 0, 360), |
|
} |
|
spines['polar'].set_transform(self.transWedge + self.transAxes) |
|
spines['inner'].set_transform(self.transWedge + self.transAxes) |
|
spines['start'].set_transform(self._yaxis_transform) |
|
spines['end'].set_transform(self._yaxis_transform) |
|
return spines |
|
|
|
def set_thetamax(self, thetamax): |
|
"""Set the maximum theta limit in degrees.""" |
|
self.viewLim.x1 = np.deg2rad(thetamax) |
|
|
|
def get_thetamax(self): |
|
"""Return the maximum theta limit in degrees.""" |
|
return np.rad2deg(self.viewLim.xmax) |
|
|
|
def set_thetamin(self, thetamin): |
|
"""Set the minimum theta limit in degrees.""" |
|
self.viewLim.x0 = np.deg2rad(thetamin) |
|
|
|
def get_thetamin(self): |
|
"""Get the minimum theta limit in degrees.""" |
|
return np.rad2deg(self.viewLim.xmin) |
|
|
|
def set_thetalim(self, *args, **kwargs): |
|
r""" |
|
Set the minimum and maximum theta values. |
|
|
|
Can take the following signatures: |
|
|
|
- ``set_thetalim(minval, maxval)``: Set the limits in radians. |
|
- ``set_thetalim(thetamin=minval, thetamax=maxval)``: Set the limits |
|
in degrees. |
|
|
|
where minval and maxval are the minimum and maximum limits. Values are |
|
wrapped in to the range :math:`[0, 2\pi]` (in radians), so for example |
|
it is possible to do ``set_thetalim(-np.pi / 2, np.pi / 2)`` to have |
|
an axis symmetric around 0. A ValueError is raised if the absolute |
|
angle difference is larger than a full circle. |
|
""" |
|
orig_lim = self.get_xlim() |
|
if 'thetamin' in kwargs: |
|
kwargs['xmin'] = np.deg2rad(kwargs.pop('thetamin')) |
|
if 'thetamax' in kwargs: |
|
kwargs['xmax'] = np.deg2rad(kwargs.pop('thetamax')) |
|
new_min, new_max = self.set_xlim(*args, **kwargs) |
|
|
|
|
|
if abs(new_max - new_min) > 2 * np.pi: |
|
self.set_xlim(orig_lim) |
|
raise ValueError("The angle range must be less than a full circle") |
|
return tuple(np.rad2deg((new_min, new_max))) |
|
|
|
def set_theta_offset(self, offset): |
|
""" |
|
Set the offset for the location of 0 in radians. |
|
""" |
|
mtx = self._theta_offset.get_matrix() |
|
mtx[0, 2] = offset |
|
self._theta_offset.invalidate() |
|
|
|
def get_theta_offset(self): |
|
""" |
|
Get the offset for the location of 0 in radians. |
|
""" |
|
return self._theta_offset.get_matrix()[0, 2] |
|
|
|
def set_theta_zero_location(self, loc, offset=0.0): |
|
""" |
|
Set the location of theta's zero. |
|
|
|
This simply calls `set_theta_offset` with the correct value in radians. |
|
|
|
Parameters |
|
---------- |
|
loc : str |
|
May be one of "N", "NW", "W", "SW", "S", "SE", "E", or "NE". |
|
offset : float, default: 0 |
|
An offset in degrees to apply from the specified *loc*. **Note:** |
|
this offset is *always* applied counter-clockwise regardless of |
|
the direction setting. |
|
""" |
|
mapping = { |
|
'N': np.pi * 0.5, |
|
'NW': np.pi * 0.75, |
|
'W': np.pi, |
|
'SW': np.pi * 1.25, |
|
'S': np.pi * 1.5, |
|
'SE': np.pi * 1.75, |
|
'E': 0, |
|
'NE': np.pi * 0.25} |
|
return self.set_theta_offset(mapping[loc] + np.deg2rad(offset)) |
|
|
|
def set_theta_direction(self, direction): |
|
""" |
|
Set the direction in which theta increases. |
|
|
|
clockwise, -1: |
|
Theta increases in the clockwise direction |
|
|
|
counterclockwise, anticlockwise, 1: |
|
Theta increases in the counterclockwise direction |
|
""" |
|
mtx = self._direction.get_matrix() |
|
if direction in ('clockwise', -1): |
|
mtx[0, 0] = -1 |
|
elif direction in ('counterclockwise', 'anticlockwise', 1): |
|
mtx[0, 0] = 1 |
|
else: |
|
_api.check_in_list( |
|
[-1, 1, 'clockwise', 'counterclockwise', 'anticlockwise'], |
|
direction=direction) |
|
self._direction.invalidate() |
|
|
|
def get_theta_direction(self): |
|
""" |
|
Get the direction in which theta increases. |
|
|
|
-1: |
|
Theta increases in the clockwise direction |
|
|
|
1: |
|
Theta increases in the counterclockwise direction |
|
""" |
|
return self._direction.get_matrix()[0, 0] |
|
|
|
def set_rmax(self, rmax): |
|
""" |
|
Set the outer radial limit. |
|
|
|
Parameters |
|
---------- |
|
rmax : float |
|
""" |
|
self.viewLim.y1 = rmax |
|
|
|
def get_rmax(self): |
|
""" |
|
Returns |
|
------- |
|
float |
|
Outer radial limit. |
|
""" |
|
return self.viewLim.ymax |
|
|
|
def set_rmin(self, rmin): |
|
""" |
|
Set the inner radial limit. |
|
|
|
Parameters |
|
---------- |
|
rmin : float |
|
""" |
|
self.viewLim.y0 = rmin |
|
|
|
def get_rmin(self): |
|
""" |
|
Returns |
|
------- |
|
float |
|
The inner radial limit. |
|
""" |
|
return self.viewLim.ymin |
|
|
|
def set_rorigin(self, rorigin): |
|
""" |
|
Update the radial origin. |
|
|
|
Parameters |
|
---------- |
|
rorigin : float |
|
""" |
|
self._originViewLim.locked_y0 = rorigin |
|
|
|
def get_rorigin(self): |
|
""" |
|
Returns |
|
------- |
|
float |
|
""" |
|
return self._originViewLim.y0 |
|
|
|
def get_rsign(self): |
|
return np.sign(self._originViewLim.y1 - self._originViewLim.y0) |
|
|
|
def set_rlim(self, bottom=None, top=None, *, |
|
emit=True, auto=False, **kwargs): |
|
""" |
|
Set the radial axis view limits. |
|
|
|
This function behaves like `.Axes.set_ylim`, but additionally supports |
|
*rmin* and *rmax* as aliases for *bottom* and *top*. |
|
|
|
See Also |
|
-------- |
|
.Axes.set_ylim |
|
""" |
|
if 'rmin' in kwargs: |
|
if bottom is None: |
|
bottom = kwargs.pop('rmin') |
|
else: |
|
raise ValueError('Cannot supply both positional "bottom"' |
|
'argument and kwarg "rmin"') |
|
if 'rmax' in kwargs: |
|
if top is None: |
|
top = kwargs.pop('rmax') |
|
else: |
|
raise ValueError('Cannot supply both positional "top"' |
|
'argument and kwarg "rmax"') |
|
return self.set_ylim(bottom=bottom, top=top, emit=emit, auto=auto, |
|
**kwargs) |
|
|
|
def get_rlabel_position(self): |
|
""" |
|
Returns |
|
------- |
|
float |
|
The theta position of the radius labels in degrees. |
|
""" |
|
return np.rad2deg(self._r_label_position.get_matrix()[0, 2]) |
|
|
|
def set_rlabel_position(self, value): |
|
""" |
|
Update the theta position of the radius labels. |
|
|
|
Parameters |
|
---------- |
|
value : number |
|
The angular position of the radius labels in degrees. |
|
""" |
|
self._r_label_position.clear().translate(np.deg2rad(value), 0.0) |
|
|
|
def set_yscale(self, *args, **kwargs): |
|
super().set_yscale(*args, **kwargs) |
|
self.yaxis.set_major_locator( |
|
self.RadialLocator(self.yaxis.get_major_locator(), self)) |
|
|
|
def set_rscale(self, *args, **kwargs): |
|
return Axes.set_yscale(self, *args, **kwargs) |
|
|
|
def set_rticks(self, *args, **kwargs): |
|
return Axes.set_yticks(self, *args, **kwargs) |
|
|
|
def set_thetagrids(self, angles, labels=None, fmt=None, **kwargs): |
|
""" |
|
Set the theta gridlines in a polar plot. |
|
|
|
Parameters |
|
---------- |
|
angles : tuple with floats, degrees |
|
The angles of the theta gridlines. |
|
|
|
labels : tuple with strings or None |
|
The labels to use at each theta gridline. The |
|
`.projections.polar.ThetaFormatter` will be used if None. |
|
|
|
fmt : str or None |
|
Format string used in `matplotlib.ticker.FormatStrFormatter`. |
|
For example '%f'. Note that the angle that is used is in |
|
radians. |
|
|
|
Returns |
|
------- |
|
lines : list of `.lines.Line2D` |
|
The theta gridlines. |
|
|
|
labels : list of `.text.Text` |
|
The tick labels. |
|
|
|
Other Parameters |
|
---------------- |
|
**kwargs |
|
*kwargs* are optional `.Text` properties for the labels. |
|
|
|
.. warning:: |
|
|
|
This only sets the properties of the current ticks. |
|
Ticks are not guaranteed to be persistent. Various operations |
|
can create, delete and modify the Tick instances. There is an |
|
imminent risk that these settings can get lost if you work on |
|
the figure further (including also panning/zooming on a |
|
displayed figure). |
|
|
|
Use `.set_tick_params` instead if possible. |
|
|
|
See Also |
|
-------- |
|
.PolarAxes.set_rgrids |
|
.Axis.get_gridlines |
|
.Axis.get_ticklabels |
|
""" |
|
|
|
|
|
angles = self.convert_yunits(angles) |
|
angles = np.deg2rad(angles) |
|
self.set_xticks(angles) |
|
if labels is not None: |
|
self.set_xticklabels(labels) |
|
elif fmt is not None: |
|
self.xaxis.set_major_formatter(mticker.FormatStrFormatter(fmt)) |
|
for t in self.xaxis.get_ticklabels(): |
|
t._internal_update(kwargs) |
|
return self.xaxis.get_ticklines(), self.xaxis.get_ticklabels() |
|
|
|
def set_rgrids(self, radii, labels=None, angle=None, fmt=None, **kwargs): |
|
""" |
|
Set the radial gridlines on a polar plot. |
|
|
|
Parameters |
|
---------- |
|
radii : tuple with floats |
|
The radii for the radial gridlines |
|
|
|
labels : tuple with strings or None |
|
The labels to use at each radial gridline. The |
|
`matplotlib.ticker.ScalarFormatter` will be used if None. |
|
|
|
angle : float |
|
The angular position of the radius labels in degrees. |
|
|
|
fmt : str or None |
|
Format string used in `matplotlib.ticker.FormatStrFormatter`. |
|
For example '%f'. |
|
|
|
Returns |
|
------- |
|
lines : list of `.lines.Line2D` |
|
The radial gridlines. |
|
|
|
labels : list of `.text.Text` |
|
The tick labels. |
|
|
|
Other Parameters |
|
---------------- |
|
**kwargs |
|
*kwargs* are optional `.Text` properties for the labels. |
|
|
|
.. warning:: |
|
|
|
This only sets the properties of the current ticks. |
|
Ticks are not guaranteed to be persistent. Various operations |
|
can create, delete and modify the Tick instances. There is an |
|
imminent risk that these settings can get lost if you work on |
|
the figure further (including also panning/zooming on a |
|
displayed figure). |
|
|
|
Use `.set_tick_params` instead if possible. |
|
|
|
See Also |
|
-------- |
|
.PolarAxes.set_thetagrids |
|
.Axis.get_gridlines |
|
.Axis.get_ticklabels |
|
""" |
|
|
|
radii = self.convert_xunits(radii) |
|
radii = np.asarray(radii) |
|
|
|
self.set_yticks(radii) |
|
if labels is not None: |
|
self.set_yticklabels(labels) |
|
elif fmt is not None: |
|
self.yaxis.set_major_formatter(mticker.FormatStrFormatter(fmt)) |
|
if angle is None: |
|
angle = self.get_rlabel_position() |
|
self.set_rlabel_position(angle) |
|
for t in self.yaxis.get_ticklabels(): |
|
t._internal_update(kwargs) |
|
return self.yaxis.get_gridlines(), self.yaxis.get_ticklabels() |
|
|
|
def format_coord(self, theta, r): |
|
|
|
screen_xy = self.transData.transform((theta, r)) |
|
screen_xys = screen_xy + np.stack( |
|
np.meshgrid([-1, 0, 1], [-1, 0, 1])).reshape((2, -1)).T |
|
ts, rs = self.transData.inverted().transform(screen_xys).T |
|
delta_t = abs((ts - theta + np.pi) % (2 * np.pi) - np.pi).max() |
|
delta_t_halfturns = delta_t / np.pi |
|
delta_t_degrees = delta_t_halfturns * 180 |
|
delta_r = abs(rs - r).max() |
|
if theta < 0: |
|
theta += 2 * np.pi |
|
theta_halfturns = theta / np.pi |
|
theta_degrees = theta_halfturns * 180 |
|
|
|
|
|
|
|
|
|
def format_sig(value, delta, opt, fmt): |
|
|
|
prec = (max(0, -math.floor(math.log10(delta))) if fmt == "f" else |
|
cbook._g_sig_digits(value, delta)) |
|
return f"{value:-{opt}.{prec}{fmt}}" |
|
|
|
return ('\N{GREEK SMALL LETTER THETA}={}\N{GREEK SMALL LETTER PI} ' |
|
'({}\N{DEGREE SIGN}), r={}').format( |
|
format_sig(theta_halfturns, delta_t_halfturns, "", "f"), |
|
format_sig(theta_degrees, delta_t_degrees, "", "f"), |
|
format_sig(r, delta_r, "#", "g"), |
|
) |
|
|
|
def get_data_ratio(self): |
|
""" |
|
Return the aspect ratio of the data itself. For a polar plot, |
|
this should always be 1.0 |
|
""" |
|
return 1.0 |
|
|
|
|
|
|
|
def can_zoom(self): |
|
""" |
|
Return whether this Axes supports the zoom box button functionality. |
|
|
|
A polar Axes does not support zoom boxes. |
|
""" |
|
return False |
|
|
|
def can_pan(self): |
|
""" |
|
Return whether this Axes supports the pan/zoom button functionality. |
|
|
|
For a polar Axes, this is slightly misleading. Both panning and |
|
zooming are performed by the same button. Panning is performed |
|
in azimuth while zooming is done along the radial. |
|
""" |
|
return True |
|
|
|
def start_pan(self, x, y, button): |
|
angle = np.deg2rad(self.get_rlabel_position()) |
|
mode = '' |
|
if button == 1: |
|
epsilon = np.pi / 45.0 |
|
t, r = self.transData.inverted().transform((x, y)) |
|
if angle - epsilon <= t <= angle + epsilon: |
|
mode = 'drag_r_labels' |
|
elif button == 3: |
|
mode = 'zoom' |
|
|
|
self._pan_start = types.SimpleNamespace( |
|
rmax=self.get_rmax(), |
|
trans=self.transData.frozen(), |
|
trans_inverse=self.transData.inverted().frozen(), |
|
r_label_angle=self.get_rlabel_position(), |
|
x=x, |
|
y=y, |
|
mode=mode) |
|
|
|
def end_pan(self): |
|
del self._pan_start |
|
|
|
def drag_pan(self, button, key, x, y): |
|
p = self._pan_start |
|
|
|
if p.mode == 'drag_r_labels': |
|
(startt, startr), (t, r) = p.trans_inverse.transform( |
|
[(p.x, p.y), (x, y)]) |
|
|
|
|
|
dt = np.rad2deg(startt - t) |
|
self.set_rlabel_position(p.r_label_angle - dt) |
|
|
|
trans, vert1, horiz1 = self.get_yaxis_text1_transform(0.0) |
|
trans, vert2, horiz2 = self.get_yaxis_text2_transform(0.0) |
|
for t in self.yaxis.majorTicks + self.yaxis.minorTicks: |
|
t.label1.set_va(vert1) |
|
t.label1.set_ha(horiz1) |
|
t.label2.set_va(vert2) |
|
t.label2.set_ha(horiz2) |
|
|
|
elif p.mode == 'zoom': |
|
(startt, startr), (t, r) = p.trans_inverse.transform( |
|
[(p.x, p.y), (x, y)]) |
|
|
|
|
|
scale = r / startr |
|
self.set_rmax(p.rmax / scale) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PolarAxes.PolarTransform = PolarTransform |
|
PolarAxes.PolarAffine = PolarAffine |
|
PolarAxes.InvertedPolarTransform = InvertedPolarTransform |
|
PolarAxes.ThetaFormatter = ThetaFormatter |
|
PolarAxes.RadialLocator = RadialLocator |
|
PolarAxes.ThetaLocator = ThetaLocator |
|
|