1import math
2import types
3
4import numpy as np
5
6import matplotlib as mpl
7from matplotlib import _api, cbook
8from matplotlib.axes import Axes
9import matplotlib.axis as maxis
10import matplotlib.markers as mmarkers
11import matplotlib.patches as mpatches
12from matplotlib.path import Path
13import matplotlib.ticker as mticker
14import matplotlib.transforms as mtransforms
15from matplotlib.spines import Spine
16
17
18def _apply_theta_transforms_warn():
19 _api.warn_deprecated(
20 "3.9",
21 message=(
22 "Passing `apply_theta_transforms=True` (the default) "
23 "is deprecated since Matplotlib %(since)s. "
24 "Support for this will be removed in Matplotlib %(removal)s. "
25 "To prevent this warning, set `apply_theta_transforms=False`, "
26 "and make sure to shift theta values before being passed to "
27 "this transform."
28 )
29 )
30
31
32class PolarTransform(mtransforms.Transform):
33 r"""
34 The base polar transform.
35
36 This transform maps polar coordinates :math:`\theta, r` into Cartesian
37 coordinates :math:`x, y = r \cos(\theta), r \sin(\theta)`
38 (but does not fully transform into Axes coordinates or
39 handle positioning in screen space).
40
41 This transformation is designed to be applied to data after any scaling
42 along the radial axis (e.g. log-scaling) has been applied to the input
43 data.
44
45 Path segments at a fixed radius are automatically transformed to circular
46 arcs as long as ``path._interpolation_steps > 1``.
47 """
48
49 input_dims = output_dims = 2
50
51 def __init__(self, axis=None, use_rmin=True, *,
52 apply_theta_transforms=True, scale_transform=None):
53 """
54 Parameters
55 ----------
56 axis : `~matplotlib.axis.Axis`, optional
57 Axis associated with this transform. This is used to get the
58 minimum radial limit.
59 use_rmin : `bool`, optional
60 If ``True``, subtract the minimum radial axis limit before
61 transforming to Cartesian coordinates. *axis* must also be
62 specified for this to take effect.
63 """
64 super().__init__()
65 self._axis = axis
66 self._use_rmin = use_rmin
67 self._apply_theta_transforms = apply_theta_transforms
68 self._scale_transform = scale_transform
69 if apply_theta_transforms:
70 _apply_theta_transforms_warn()
71
72 __str__ = mtransforms._make_str_method(
73 "_axis",
74 use_rmin="_use_rmin",
75 apply_theta_transforms="_apply_theta_transforms")
76
77 def _get_rorigin(self):
78 # Get lower r limit after being scaled by the radial scale transform
79 return self._scale_transform.transform(
80 (0, self._axis.get_rorigin()))[1]
81
82 @_api.rename_parameter("3.8", "tr", "values")
83 def transform_non_affine(self, values):
84 # docstring inherited
85 theta, r = np.transpose(values)
86 # PolarAxes does not use the theta transforms here, but apply them for
87 # backwards-compatibility if not being used by it.
88 if self._apply_theta_transforms and self._axis is not None:
89 theta *= self._axis.get_theta_direction()
90 theta += self._axis.get_theta_offset()
91 if self._use_rmin and self._axis is not None:
92 r = (r - self._get_rorigin()) * self._axis.get_rsign()
93 r = np.where(r >= 0, r, np.nan)
94 return np.column_stack([r * np.cos(theta), r * np.sin(theta)])
95
96 def transform_path_non_affine(self, path):
97 # docstring inherited
98 if not len(path) or path._interpolation_steps == 1:
99 return Path(self.transform_non_affine(path.vertices), path.codes)
100 xys = []
101 codes = []
102 last_t = last_r = None
103 for trs, c in path.iter_segments():
104 trs = trs.reshape((-1, 2))
105 if c == Path.LINETO:
106 (t, r), = trs
107 if t == last_t: # Same angle: draw a straight line.
108 xys.extend(self.transform_non_affine(trs))
109 codes.append(Path.LINETO)
110 elif r == last_r: # Same radius: draw an arc.
111 # The following is complicated by Path.arc() being
112 # "helpful" and unwrapping the angles, but we don't want
113 # that behavior here.
114 last_td, td = np.rad2deg([last_t, t])
115 if self._use_rmin and self._axis is not None:
116 r = ((r - self._get_rorigin())
117 * self._axis.get_rsign())
118 if last_td <= td:
119 while td - last_td > 360:
120 arc = Path.arc(last_td, last_td + 360)
121 xys.extend(arc.vertices[1:] * r)
122 codes.extend(arc.codes[1:])
123 last_td += 360
124 arc = Path.arc(last_td, td)
125 xys.extend(arc.vertices[1:] * r)
126 codes.extend(arc.codes[1:])
127 else:
128 # The reverse version also relies on the fact that all
129 # codes but the first one are the same.
130 while last_td - td > 360:
131 arc = Path.arc(last_td - 360, last_td)
132 xys.extend(arc.vertices[::-1][1:] * r)
133 codes.extend(arc.codes[1:])
134 last_td -= 360
135 arc = Path.arc(td, last_td)
136 xys.extend(arc.vertices[::-1][1:] * r)
137 codes.extend(arc.codes[1:])
138 else: # Interpolate.
139 trs = cbook.simple_linear_interpolation(
140 np.vstack([(last_t, last_r), trs]),
141 path._interpolation_steps)[1:]
142 xys.extend(self.transform_non_affine(trs))
143 codes.extend([Path.LINETO] * len(trs))
144 else: # Not a straight line.
145 xys.extend(self.transform_non_affine(trs))
146 codes.extend([c] * len(trs))
147 last_t, last_r = trs[-1]
148 return Path(xys, codes)
149
150 def inverted(self):
151 # docstring inherited
152 return PolarAxes.InvertedPolarTransform(
153 self._axis, self._use_rmin,
154 apply_theta_transforms=self._apply_theta_transforms
155 )
156
157
158class PolarAffine(mtransforms.Affine2DBase):
159 r"""
160 The affine part of the polar projection.
161
162 Scales the output so that maximum radius rests on the edge of the Axes
163 circle and the origin is mapped to (0.5, 0.5). The transform applied is
164 the same to x and y components and given by:
165
166 .. math::
167
168 x_{1} = 0.5 \left [ \frac{x_{0}}{(r_{\max} - r_{\min})} + 1 \right ]
169
170 :math:`r_{\min}, r_{\max}` are the minimum and maximum radial limits after
171 any scaling (e.g. log scaling) has been removed.
172 """
173 def __init__(self, scale_transform, limits):
174 """
175 Parameters
176 ----------
177 scale_transform : `~matplotlib.transforms.Transform`
178 Scaling transform for the data. This is used to remove any scaling
179 from the radial view limits.
180 limits : `~matplotlib.transforms.BboxBase`
181 View limits of the data. The only part of its bounds that is used
182 is the y limits (for the radius limits).
183 """
184 super().__init__()
185 self._scale_transform = scale_transform
186 self._limits = limits
187 self.set_children(scale_transform, limits)
188 self._mtx = None
189
190 __str__ = mtransforms._make_str_method("_scale_transform", "_limits")
191
192 def get_matrix(self):
193 # docstring inherited
194 if self._invalid:
195 limits_scaled = self._limits.transformed(self._scale_transform)
196 yscale = limits_scaled.ymax - limits_scaled.ymin
197 affine = mtransforms.Affine2D() \
198 .scale(0.5 / yscale) \
199 .translate(0.5, 0.5)
200 self._mtx = affine.get_matrix()
201 self._inverted = None
202 self._invalid = 0
203 return self._mtx
204
205
206class InvertedPolarTransform(mtransforms.Transform):
207 """
208 The inverse of the polar transform, mapping Cartesian
209 coordinate space *x* and *y* back to *theta* and *r*.
210 """
211 input_dims = output_dims = 2
212
213 def __init__(self, axis=None, use_rmin=True,
214 *, apply_theta_transforms=True):
215 """
216 Parameters
217 ----------
218 axis : `~matplotlib.axis.Axis`, optional
219 Axis associated with this transform. This is used to get the
220 minimum radial limit.
221 use_rmin : `bool`, optional
222 If ``True``, add the minimum radial axis limit after
223 transforming from Cartesian coordinates. *axis* must also be
224 specified for this to take effect.
225 """
226 super().__init__()
227 self._axis = axis
228 self._use_rmin = use_rmin
229 self._apply_theta_transforms = apply_theta_transforms
230 if apply_theta_transforms:
231 _apply_theta_transforms_warn()
232
233 __str__ = mtransforms._make_str_method(
234 "_axis",
235 use_rmin="_use_rmin",
236 apply_theta_transforms="_apply_theta_transforms")
237
238 @_api.rename_parameter("3.8", "xy", "values")
239 def transform_non_affine(self, values):
240 # docstring inherited
241 x, y = values.T
242 r = np.hypot(x, y)
243 theta = (np.arctan2(y, x) + 2 * np.pi) % (2 * np.pi)
244 # PolarAxes does not use the theta transforms here, but apply them for
245 # backwards-compatibility if not being used by it.
246 if self._apply_theta_transforms and self._axis is not None:
247 theta -= self._axis.get_theta_offset()
248 theta *= self._axis.get_theta_direction()
249 theta %= 2 * np.pi
250 if self._use_rmin and self._axis is not None:
251 r += self._axis.get_rorigin()
252 r *= self._axis.get_rsign()
253 return np.column_stack([theta, r])
254
255 def inverted(self):
256 # docstring inherited
257 return PolarAxes.PolarTransform(
258 self._axis, self._use_rmin,
259 apply_theta_transforms=self._apply_theta_transforms
260 )
261
262
263class ThetaFormatter(mticker.Formatter):
264 """
265 Used to format the *theta* tick labels. Converts the native
266 unit of radians into degrees and adds a degree symbol.
267 """
268
269 def __call__(self, x, pos=None):
270 vmin, vmax = self.axis.get_view_interval()
271 d = np.rad2deg(abs(vmax - vmin))
272 digits = max(-int(np.log10(d) - 1.5), 0)
273 return f"{np.rad2deg(x):0.{digits}f}\N{DEGREE SIGN}"
274
275
276class _AxisWrapper:
277 def __init__(self, axis):
278 self._axis = axis
279
280 def get_view_interval(self):
281 return np.rad2deg(self._axis.get_view_interval())
282
283 def set_view_interval(self, vmin, vmax):
284 self._axis.set_view_interval(*np.deg2rad((vmin, vmax)))
285
286 def get_minpos(self):
287 return np.rad2deg(self._axis.get_minpos())
288
289 def get_data_interval(self):
290 return np.rad2deg(self._axis.get_data_interval())
291
292 def set_data_interval(self, vmin, vmax):
293 self._axis.set_data_interval(*np.deg2rad((vmin, vmax)))
294
295 def get_tick_space(self):
296 return self._axis.get_tick_space()
297
298
299class ThetaLocator(mticker.Locator):
300 """
301 Used to locate theta ticks.
302
303 This will work the same as the base locator except in the case that the
304 view spans the entire circle. In such cases, the previously used default
305 locations of every 45 degrees are returned.
306 """
307
308 def __init__(self, base):
309 self.base = base
310 self.axis = self.base.axis = _AxisWrapper(self.base.axis)
311
312 def set_axis(self, axis):
313 self.axis = _AxisWrapper(axis)
314 self.base.set_axis(self.axis)
315
316 def __call__(self):
317 lim = self.axis.get_view_interval()
318 if _is_full_circle_deg(lim[0], lim[1]):
319 return np.deg2rad(min(lim)) + np.arange(8) * 2 * np.pi / 8
320 else:
321 return np.deg2rad(self.base())
322
323 def view_limits(self, vmin, vmax):
324 vmin, vmax = np.rad2deg((vmin, vmax))
325 return np.deg2rad(self.base.view_limits(vmin, vmax))
326
327
328class ThetaTick(maxis.XTick):
329 """
330 A theta-axis tick.
331
332 This subclass of `.XTick` provides angular ticks with some small
333 modification to their re-positioning such that ticks are rotated based on
334 tick location. This results in ticks that are correctly perpendicular to
335 the arc spine.
336
337 When 'auto' rotation is enabled, labels are also rotated to be parallel to
338 the spine. The label padding is also applied here since it's not possible
339 to use a generic axes transform to produce tick-specific padding.
340 """
341
342 def __init__(self, axes, *args, **kwargs):
343 self._text1_translate = mtransforms.ScaledTranslation(
344 0, 0, axes.figure.dpi_scale_trans)
345 self._text2_translate = mtransforms.ScaledTranslation(
346 0, 0, axes.figure.dpi_scale_trans)
347 super().__init__(axes, *args, **kwargs)
348 self.label1.set(
349 rotation_mode='anchor',
350 transform=self.label1.get_transform() + self._text1_translate)
351 self.label2.set(
352 rotation_mode='anchor',
353 transform=self.label2.get_transform() + self._text2_translate)
354
355 def _apply_params(self, **kwargs):
356 super()._apply_params(**kwargs)
357 # Ensure transform is correct; sometimes this gets reset.
358 trans = self.label1.get_transform()
359 if not trans.contains_branch(self._text1_translate):
360 self.label1.set_transform(trans + self._text1_translate)
361 trans = self.label2.get_transform()
362 if not trans.contains_branch(self._text2_translate):
363 self.label2.set_transform(trans + self._text2_translate)
364
365 def _update_padding(self, pad, angle):
366 padx = pad * np.cos(angle) / 72
367 pady = pad * np.sin(angle) / 72
368 self._text1_translate._t = (padx, pady)
369 self._text1_translate.invalidate()
370 self._text2_translate._t = (-padx, -pady)
371 self._text2_translate.invalidate()
372
373 def update_position(self, loc):
374 super().update_position(loc)
375 axes = self.axes
376 angle = loc * axes.get_theta_direction() + axes.get_theta_offset()
377 text_angle = np.rad2deg(angle) % 360 - 90
378 angle -= np.pi / 2
379
380 marker = self.tick1line.get_marker()
381 if marker in (mmarkers.TICKUP, '|'):
382 trans = mtransforms.Affine2D().scale(1, 1).rotate(angle)
383 elif marker == mmarkers.TICKDOWN:
384 trans = mtransforms.Affine2D().scale(1, -1).rotate(angle)
385 else:
386 # Don't modify custom tick line markers.
387 trans = self.tick1line._marker._transform
388 self.tick1line._marker._transform = trans
389
390 marker = self.tick2line.get_marker()
391 if marker in (mmarkers.TICKUP, '|'):
392 trans = mtransforms.Affine2D().scale(1, 1).rotate(angle)
393 elif marker == mmarkers.TICKDOWN:
394 trans = mtransforms.Affine2D().scale(1, -1).rotate(angle)
395 else:
396 # Don't modify custom tick line markers.
397 trans = self.tick2line._marker._transform
398 self.tick2line._marker._transform = trans
399
400 mode, user_angle = self._labelrotation
401 if mode == 'default':
402 text_angle = user_angle
403 else:
404 if text_angle > 90:
405 text_angle -= 180
406 elif text_angle < -90:
407 text_angle += 180
408 text_angle += user_angle
409 self.label1.set_rotation(text_angle)
410 self.label2.set_rotation(text_angle)
411
412 # This extra padding helps preserve the look from previous releases but
413 # is also needed because labels are anchored to their center.
414 pad = self._pad + 7
415 self._update_padding(pad,
416 self._loc * axes.get_theta_direction() +
417 axes.get_theta_offset())
418
419
420class ThetaAxis(maxis.XAxis):
421 """
422 A theta Axis.
423
424 This overrides certain properties of an `.XAxis` to provide special-casing
425 for an angular axis.
426 """
427 __name__ = 'thetaaxis'
428 axis_name = 'theta' #: Read-only name identifying the axis.
429 _tick_class = ThetaTick
430
431 def _wrap_locator_formatter(self):
432 self.set_major_locator(ThetaLocator(self.get_major_locator()))
433 self.set_major_formatter(ThetaFormatter())
434 self.isDefault_majloc = True
435 self.isDefault_majfmt = True
436
437 def clear(self):
438 # docstring inherited
439 super().clear()
440 self.set_ticks_position('none')
441 self._wrap_locator_formatter()
442
443 def _set_scale(self, value, **kwargs):
444 if value != 'linear':
445 raise NotImplementedError(
446 "The xscale cannot be set on a polar plot")
447 super()._set_scale(value, **kwargs)
448 # LinearScale.set_default_locators_and_formatters just set the major
449 # locator to be an AutoLocator, so we customize it here to have ticks
450 # at sensible degree multiples.
451 self.get_major_locator().set_params(steps=[1, 1.5, 3, 4.5, 9, 10])
452 self._wrap_locator_formatter()
453
454 def _copy_tick_props(self, src, dest):
455 """Copy the props from src tick to dest tick."""
456 if src is None or dest is None:
457 return
458 super()._copy_tick_props(src, dest)
459
460 # Ensure that tick transforms are independent so that padding works.
461 trans = dest._get_text1_transform()[0]
462 dest.label1.set_transform(trans + dest._text1_translate)
463 trans = dest._get_text2_transform()[0]
464 dest.label2.set_transform(trans + dest._text2_translate)
465
466
467class RadialLocator(mticker.Locator):
468 """
469 Used to locate radius ticks.
470
471 Ensures that all ticks are strictly positive. For all other tasks, it
472 delegates to the base `.Locator` (which may be different depending on the
473 scale of the *r*-axis).
474 """
475
476 def __init__(self, base, axes=None):
477 self.base = base
478 self._axes = axes
479
480 def set_axis(self, axis):
481 self.base.set_axis(axis)
482
483 def __call__(self):
484 # Ensure previous behaviour with full circle non-annular views.
485 if self._axes:
486 if _is_full_circle_rad(*self._axes.viewLim.intervalx):
487 rorigin = self._axes.get_rorigin() * self._axes.get_rsign()
488 if self._axes.get_rmin() <= rorigin:
489 return [tick for tick in self.base() if tick > rorigin]
490 return self.base()
491
492 def _zero_in_bounds(self):
493 """
494 Return True if zero is within the valid values for the
495 scale of the radial axis.
496 """
497 vmin, vmax = self._axes.yaxis._scale.limit_range_for_scale(0, 1, 1e-5)
498 return vmin == 0
499
500 def nonsingular(self, vmin, vmax):
501 # docstring inherited
502 if self._zero_in_bounds() and (vmin, vmax) == (-np.inf, np.inf):
503 # Initial view limits
504 return (0, 1)
505 else:
506 return self.base.nonsingular(vmin, vmax)
507
508 def view_limits(self, vmin, vmax):
509 vmin, vmax = self.base.view_limits(vmin, vmax)
510 if self._zero_in_bounds() and vmax > vmin:
511 # this allows inverted r/y-lims
512 vmin = min(0, vmin)
513 return mtransforms.nonsingular(vmin, vmax)
514
515
516class _ThetaShift(mtransforms.ScaledTranslation):
517 """
518 Apply a padding shift based on axes theta limits.
519
520 This is used to create padding for radial ticks.
521
522 Parameters
523 ----------
524 axes : `~matplotlib.axes.Axes`
525 The owning Axes; used to determine limits.
526 pad : float
527 The padding to apply, in points.
528 mode : {'min', 'max', 'rlabel'}
529 Whether to shift away from the start (``'min'``) or the end (``'max'``)
530 of the axes, or using the rlabel position (``'rlabel'``).
531 """
532 def __init__(self, axes, pad, mode):
533 super().__init__(pad, pad, axes.figure.dpi_scale_trans)
534 self.set_children(axes._realViewLim)
535 self.axes = axes
536 self.mode = mode
537 self.pad = pad
538
539 __str__ = mtransforms._make_str_method("axes", "pad", "mode")
540
541 def get_matrix(self):
542 if self._invalid:
543 if self.mode == 'rlabel':
544 angle = (
545 np.deg2rad(self.axes.get_rlabel_position()
546 * self.axes.get_theta_direction())
547 + self.axes.get_theta_offset()
548 - np.pi / 2
549 )
550 elif self.mode == 'min':
551 angle = self.axes._realViewLim.xmin - np.pi / 2
552 elif self.mode == 'max':
553 angle = self.axes._realViewLim.xmax + np.pi / 2
554 self._t = (self.pad * np.cos(angle) / 72, self.pad * np.sin(angle) / 72)
555 return super().get_matrix()
556
557
558class RadialTick(maxis.YTick):
559 """
560 A radial-axis tick.
561
562 This subclass of `.YTick` provides radial ticks with some small
563 modification to their re-positioning such that ticks are rotated based on
564 axes limits. This results in ticks that are correctly perpendicular to
565 the spine. Labels are also rotated to be perpendicular to the spine, when
566 'auto' rotation is enabled.
567 """
568
569 def __init__(self, *args, **kwargs):
570 super().__init__(*args, **kwargs)
571 self.label1.set_rotation_mode('anchor')
572 self.label2.set_rotation_mode('anchor')
573
574 def _determine_anchor(self, mode, angle, start):
575 # Note: angle is the (spine angle - 90) because it's used for the tick
576 # & text setup, so all numbers below are -90 from (normed) spine angle.
577 if mode == 'auto':
578 if start:
579 if -90 <= angle <= 90:
580 return 'left', 'center'
581 else:
582 return 'right', 'center'
583 else:
584 if -90 <= angle <= 90:
585 return 'right', 'center'
586 else:
587 return 'left', 'center'
588 else:
589 if start:
590 if angle < -68.5:
591 return 'center', 'top'
592 elif angle < -23.5:
593 return 'left', 'top'
594 elif angle < 22.5:
595 return 'left', 'center'
596 elif angle < 67.5:
597 return 'left', 'bottom'
598 elif angle < 112.5:
599 return 'center', 'bottom'
600 elif angle < 157.5:
601 return 'right', 'bottom'
602 elif angle < 202.5:
603 return 'right', 'center'
604 elif angle < 247.5:
605 return 'right', 'top'
606 else:
607 return 'center', 'top'
608 else:
609 if angle < -68.5:
610 return 'center', 'bottom'
611 elif angle < -23.5:
612 return 'right', 'bottom'
613 elif angle < 22.5:
614 return 'right', 'center'
615 elif angle < 67.5:
616 return 'right', 'top'
617 elif angle < 112.5:
618 return 'center', 'top'
619 elif angle < 157.5:
620 return 'left', 'top'
621 elif angle < 202.5:
622 return 'left', 'center'
623 elif angle < 247.5:
624 return 'left', 'bottom'
625 else:
626 return 'center', 'bottom'
627
628 def update_position(self, loc):
629 super().update_position(loc)
630 axes = self.axes
631 thetamin = axes.get_thetamin()
632 thetamax = axes.get_thetamax()
633 direction = axes.get_theta_direction()
634 offset_rad = axes.get_theta_offset()
635 offset = np.rad2deg(offset_rad)
636 full = _is_full_circle_deg(thetamin, thetamax)
637
638 if full:
639 angle = (axes.get_rlabel_position() * direction +
640 offset) % 360 - 90
641 tick_angle = 0
642 else:
643 angle = (thetamin * direction + offset) % 360 - 90
644 if direction > 0:
645 tick_angle = np.deg2rad(angle)
646 else:
647 tick_angle = np.deg2rad(angle + 180)
648 text_angle = (angle + 90) % 180 - 90 # between -90 and +90.
649 mode, user_angle = self._labelrotation
650 if mode == 'auto':
651 text_angle += user_angle
652 else:
653 text_angle = user_angle
654
655 if full:
656 ha = self.label1.get_horizontalalignment()
657 va = self.label1.get_verticalalignment()
658 else:
659 ha, va = self._determine_anchor(mode, angle, direction > 0)
660 self.label1.set_horizontalalignment(ha)
661 self.label1.set_verticalalignment(va)
662 self.label1.set_rotation(text_angle)
663
664 marker = self.tick1line.get_marker()
665 if marker == mmarkers.TICKLEFT:
666 trans = mtransforms.Affine2D().rotate(tick_angle)
667 elif marker == '_':
668 trans = mtransforms.Affine2D().rotate(tick_angle + np.pi / 2)
669 elif marker == mmarkers.TICKRIGHT:
670 trans = mtransforms.Affine2D().scale(-1, 1).rotate(tick_angle)
671 else:
672 # Don't modify custom tick line markers.
673 trans = self.tick1line._marker._transform
674 self.tick1line._marker._transform = trans
675
676 if full:
677 self.label2.set_visible(False)
678 self.tick2line.set_visible(False)
679 angle = (thetamax * direction + offset) % 360 - 90
680 if direction > 0:
681 tick_angle = np.deg2rad(angle)
682 else:
683 tick_angle = np.deg2rad(angle + 180)
684 text_angle = (angle + 90) % 180 - 90 # between -90 and +90.
685 mode, user_angle = self._labelrotation
686 if mode == 'auto':
687 text_angle += user_angle
688 else:
689 text_angle = user_angle
690
691 ha, va = self._determine_anchor(mode, angle, direction < 0)
692 self.label2.set_ha(ha)
693 self.label2.set_va(va)
694 self.label2.set_rotation(text_angle)
695
696 marker = self.tick2line.get_marker()
697 if marker == mmarkers.TICKLEFT:
698 trans = mtransforms.Affine2D().rotate(tick_angle)
699 elif marker == '_':
700 trans = mtransforms.Affine2D().rotate(tick_angle + np.pi / 2)
701 elif marker == mmarkers.TICKRIGHT:
702 trans = mtransforms.Affine2D().scale(-1, 1).rotate(tick_angle)
703 else:
704 # Don't modify custom tick line markers.
705 trans = self.tick2line._marker._transform
706 self.tick2line._marker._transform = trans
707
708
709class RadialAxis(maxis.YAxis):
710 """
711 A radial Axis.
712
713 This overrides certain properties of a `.YAxis` to provide special-casing
714 for a radial axis.
715 """
716 __name__ = 'radialaxis'
717 axis_name = 'radius' #: Read-only name identifying the axis.
718 _tick_class = RadialTick
719
720 def __init__(self, *args, **kwargs):
721 super().__init__(*args, **kwargs)
722 self.sticky_edges.y.append(0)
723
724 def _wrap_locator_formatter(self):
725 self.set_major_locator(RadialLocator(self.get_major_locator(),
726 self.axes))
727 self.isDefault_majloc = True
728
729 def clear(self):
730 # docstring inherited
731 super().clear()
732 self.set_ticks_position('none')
733 self._wrap_locator_formatter()
734
735 def _set_scale(self, value, **kwargs):
736 super()._set_scale(value, **kwargs)
737 self._wrap_locator_formatter()
738
739
740def _is_full_circle_deg(thetamin, thetamax):
741 """
742 Determine if a wedge (in degrees) spans the full circle.
743
744 The condition is derived from :class:`~matplotlib.patches.Wedge`.
745 """
746 return abs(abs(thetamax - thetamin) - 360.0) < 1e-12
747
748
749def _is_full_circle_rad(thetamin, thetamax):
750 """
751 Determine if a wedge (in radians) spans the full circle.
752
753 The condition is derived from :class:`~matplotlib.patches.Wedge`.
754 """
755 return abs(abs(thetamax - thetamin) - 2 * np.pi) < 1.74e-14
756
757
758class _WedgeBbox(mtransforms.Bbox):
759 """
760 Transform (theta, r) wedge Bbox into Axes bounding box.
761
762 Parameters
763 ----------
764 center : (float, float)
765 Center of the wedge
766 viewLim : `~matplotlib.transforms.Bbox`
767 Bbox determining the boundaries of the wedge
768 originLim : `~matplotlib.transforms.Bbox`
769 Bbox determining the origin for the wedge, if different from *viewLim*
770 """
771 def __init__(self, center, viewLim, originLim, **kwargs):
772 super().__init__([[0, 0], [1, 1]], **kwargs)
773 self._center = center
774 self._viewLim = viewLim
775 self._originLim = originLim
776 self.set_children(viewLim, originLim)
777
778 __str__ = mtransforms._make_str_method("_center", "_viewLim", "_originLim")
779
780 def get_points(self):
781 # docstring inherited
782 if self._invalid:
783 points = self._viewLim.get_points().copy()
784 # Scale angular limits to work with Wedge.
785 points[:, 0] *= 180 / np.pi
786 if points[0, 0] > points[1, 0]:
787 points[:, 0] = points[::-1, 0]
788
789 # Scale radial limits based on origin radius.
790 points[:, 1] -= self._originLim.y0
791
792 # Scale radial limits to match axes limits.
793 rscale = 0.5 / points[1, 1]
794 points[:, 1] *= rscale
795 width = min(points[1, 1] - points[0, 1], 0.5)
796
797 # Generate bounding box for wedge.
798 wedge = mpatches.Wedge(self._center, points[1, 1],
799 points[0, 0], points[1, 0],
800 width=width)
801 self.update_from_path(wedge.get_path())
802
803 # Ensure equal aspect ratio.
804 w, h = self._points[1] - self._points[0]
805 deltah = max(w - h, 0) / 2
806 deltaw = max(h - w, 0) / 2
807 self._points += np.array([[-deltaw, -deltah], [deltaw, deltah]])
808
809 self._invalid = 0
810
811 return self._points
812
813
814class PolarAxes(Axes):
815 """
816 A polar graph projection, where the input dimensions are *theta*, *r*.
817
818 Theta starts pointing east and goes anti-clockwise.
819 """
820 name = 'polar'
821
822 def __init__(self, *args,
823 theta_offset=0, theta_direction=1, rlabel_position=22.5,
824 **kwargs):
825 # docstring inherited
826 self._default_theta_offset = theta_offset
827 self._default_theta_direction = theta_direction
828 self._default_rlabel_position = np.deg2rad(rlabel_position)
829 super().__init__(*args, **kwargs)
830 self.use_sticky_edges = True
831 self.set_aspect('equal', adjustable='box', anchor='C')
832 self.clear()
833
834 def clear(self):
835 # docstring inherited
836 super().clear()
837
838 self.title.set_y(1.05)
839
840 start = self.spines.get('start', None)
841 if start:
842 start.set_visible(False)
843 end = self.spines.get('end', None)
844 if end:
845 end.set_visible(False)
846 self.set_xlim(0.0, 2 * np.pi)
847
848 self.grid(mpl.rcParams['polaraxes.grid'])
849 inner = self.spines.get('inner', None)
850 if inner:
851 inner.set_visible(False)
852
853 self.set_rorigin(None)
854 self.set_theta_offset(self._default_theta_offset)
855 self.set_theta_direction(self._default_theta_direction)
856
857 def _init_axis(self):
858 # This is moved out of __init__ because non-separable axes don't use it
859 self.xaxis = ThetaAxis(self, clear=False)
860 self.yaxis = RadialAxis(self, clear=False)
861 self.spines['polar'].register_axis(self.yaxis)
862
863 def _set_lim_and_transforms(self):
864 # A view limit where the minimum radius can be locked if the user
865 # specifies an alternate origin.
866 self._originViewLim = mtransforms.LockableBbox(self.viewLim)
867
868 # Handle angular offset and direction.
869 self._direction = mtransforms.Affine2D() \
870 .scale(self._default_theta_direction, 1.0)
871 self._theta_offset = mtransforms.Affine2D() \
872 .translate(self._default_theta_offset, 0.0)
873 self.transShift = self._direction + self._theta_offset
874 # A view limit shifted to the correct location after accounting for
875 # orientation and offset.
876 self._realViewLim = mtransforms.TransformedBbox(self.viewLim,
877 self.transShift)
878
879 # Transforms the x and y axis separately by a scale factor
880 # It is assumed that this part will have non-linear components
881 self.transScale = mtransforms.TransformWrapper(
882 mtransforms.IdentityTransform())
883
884 # Scale view limit into a bbox around the selected wedge. This may be
885 # smaller than the usual unit axes rectangle if not plotting the full
886 # circle.
887 self.axesLim = _WedgeBbox((0.5, 0.5),
888 self._realViewLim, self._originViewLim)
889
890 # Scale the wedge to fill the axes.
891 self.transWedge = mtransforms.BboxTransformFrom(self.axesLim)
892
893 # Scale the axes to fill the figure.
894 self.transAxes = mtransforms.BboxTransformTo(self.bbox)
895
896 # A (possibly non-linear) projection on the (already scaled)
897 # data. This one is aware of rmin
898 self.transProjection = self.PolarTransform(
899 self,
900 apply_theta_transforms=False,
901 scale_transform=self.transScale
902 )
903 # Add dependency on rorigin.
904 self.transProjection.set_children(self._originViewLim)
905
906 # An affine transformation on the data, generally to limit the
907 # range of the axes
908 self.transProjectionAffine = self.PolarAffine(self.transScale,
909 self._originViewLim)
910
911 # The complete data transformation stack -- from data all the
912 # way to display coordinates
913 #
914 # 1. Remove any radial axis scaling (e.g. log scaling)
915 # 2. Shift data in the theta direction
916 # 3. Project the data from polar to cartesian values
917 # (with the origin in the same place)
918 # 4. Scale and translate the cartesian values to Axes coordinates
919 # (here the origin is moved to the lower left of the Axes)
920 # 5. Move and scale to fill the Axes
921 # 6. Convert from Axes coordinates to Figure coordinates
922 self.transData = (
923 self.transScale +
924 self.transShift +
925 self.transProjection +
926 (
927 self.transProjectionAffine +
928 self.transWedge +
929 self.transAxes
930 )
931 )
932
933 # This is the transform for theta-axis ticks. It is
934 # equivalent to transData, except it always puts r == 0.0 and r == 1.0
935 # at the edge of the axis circles.
936 self._xaxis_transform = (
937 mtransforms.blended_transform_factory(
938 mtransforms.IdentityTransform(),
939 mtransforms.BboxTransformTo(self.viewLim)) +
940 self.transData)
941 # The theta labels are flipped along the radius, so that text 1 is on
942 # the outside by default. This should work the same as before.
943 flipr_transform = mtransforms.Affine2D() \
944 .translate(0.0, -0.5) \
945 .scale(1.0, -1.0) \
946 .translate(0.0, 0.5)
947 self._xaxis_text_transform = flipr_transform + self._xaxis_transform
948
949 # This is the transform for r-axis ticks. It scales the theta
950 # axis so the gridlines from 0.0 to 1.0, now go from thetamin to
951 # thetamax.
952 self._yaxis_transform = (
953 mtransforms.blended_transform_factory(
954 mtransforms.BboxTransformTo(self.viewLim),
955 mtransforms.IdentityTransform()) +
956 self.transData)
957 # The r-axis labels are put at an angle and padded in the r-direction
958 self._r_label_position = mtransforms.Affine2D() \
959 .translate(self._default_rlabel_position, 0.0)
960 self._yaxis_text_transform = mtransforms.TransformWrapper(
961 self._r_label_position + self.transData)
962
963 def get_xaxis_transform(self, which='grid'):
964 _api.check_in_list(['tick1', 'tick2', 'grid'], which=which)
965 return self._xaxis_transform
966
967 def get_xaxis_text1_transform(self, pad):
968 return self._xaxis_text_transform, 'center', 'center'
969
970 def get_xaxis_text2_transform(self, pad):
971 return self._xaxis_text_transform, 'center', 'center'
972
973 def get_yaxis_transform(self, which='grid'):
974 if which in ('tick1', 'tick2'):
975 return self._yaxis_text_transform
976 elif which == 'grid':
977 return self._yaxis_transform
978 else:
979 _api.check_in_list(['tick1', 'tick2', 'grid'], which=which)
980
981 def get_yaxis_text1_transform(self, pad):
982 thetamin, thetamax = self._realViewLim.intervalx
983 if _is_full_circle_rad(thetamin, thetamax):
984 return self._yaxis_text_transform, 'bottom', 'left'
985 elif self.get_theta_direction() > 0:
986 halign = 'left'
987 pad_shift = _ThetaShift(self, pad, 'min')
988 else:
989 halign = 'right'
990 pad_shift = _ThetaShift(self, pad, 'max')
991 return self._yaxis_text_transform + pad_shift, 'center', halign
992
993 def get_yaxis_text2_transform(self, pad):
994 if self.get_theta_direction() > 0:
995 halign = 'right'
996 pad_shift = _ThetaShift(self, pad, 'max')
997 else:
998 halign = 'left'
999 pad_shift = _ThetaShift(self, pad, 'min')
1000 return self._yaxis_text_transform + pad_shift, 'center', halign
1001
1002 def draw(self, renderer):
1003 self._unstale_viewLim()
1004 thetamin, thetamax = np.rad2deg(self._realViewLim.intervalx)
1005 if thetamin > thetamax:
1006 thetamin, thetamax = thetamax, thetamin
1007 rmin, rmax = ((self._realViewLim.intervaly - self.get_rorigin()) *
1008 self.get_rsign())
1009 if isinstance(self.patch, mpatches.Wedge):
1010 # Backwards-compatibility: Any subclassed Axes might override the
1011 # patch to not be the Wedge that PolarAxes uses.
1012 center = self.transWedge.transform((0.5, 0.5))
1013 self.patch.set_center(center)
1014 self.patch.set_theta1(thetamin)
1015 self.patch.set_theta2(thetamax)
1016
1017 edge, _ = self.transWedge.transform((1, 0))
1018 radius = edge - center[0]
1019 width = min(radius * (rmax - rmin) / rmax, radius)
1020 self.patch.set_radius(radius)
1021 self.patch.set_width(width)
1022
1023 inner_width = radius - width
1024 inner = self.spines.get('inner', None)
1025 if inner:
1026 inner.set_visible(inner_width != 0.0)
1027
1028 visible = not _is_full_circle_deg(thetamin, thetamax)
1029 # For backwards compatibility, any subclassed Axes might override the
1030 # spines to not include start/end that PolarAxes uses.
1031 start = self.spines.get('start', None)
1032 end = self.spines.get('end', None)
1033 if start:
1034 start.set_visible(visible)
1035 if end:
1036 end.set_visible(visible)
1037 if visible:
1038 yaxis_text_transform = self._yaxis_transform
1039 else:
1040 yaxis_text_transform = self._r_label_position + self.transData
1041 if self._yaxis_text_transform != yaxis_text_transform:
1042 self._yaxis_text_transform.set(yaxis_text_transform)
1043 self.yaxis.reset_ticks()
1044 self.yaxis.set_clip_path(self.patch)
1045
1046 super().draw(renderer)
1047
1048 def _gen_axes_patch(self):
1049 return mpatches.Wedge((0.5, 0.5), 0.5, 0.0, 360.0)
1050
1051 def _gen_axes_spines(self):
1052 spines = {
1053 'polar': Spine.arc_spine(self, 'top', (0.5, 0.5), 0.5, 0, 360),
1054 'start': Spine.linear_spine(self, 'left'),
1055 'end': Spine.linear_spine(self, 'right'),
1056 'inner': Spine.arc_spine(self, 'bottom', (0.5, 0.5), 0.0, 0, 360),
1057 }
1058 spines['polar'].set_transform(self.transWedge + self.transAxes)
1059 spines['inner'].set_transform(self.transWedge + self.transAxes)
1060 spines['start'].set_transform(self._yaxis_transform)
1061 spines['end'].set_transform(self._yaxis_transform)
1062 return spines
1063
1064 def set_thetamax(self, thetamax):
1065 """Set the maximum theta limit in degrees."""
1066 self.viewLim.x1 = np.deg2rad(thetamax)
1067
1068 def get_thetamax(self):
1069 """Return the maximum theta limit in degrees."""
1070 return np.rad2deg(self.viewLim.xmax)
1071
1072 def set_thetamin(self, thetamin):
1073 """Set the minimum theta limit in degrees."""
1074 self.viewLim.x0 = np.deg2rad(thetamin)
1075
1076 def get_thetamin(self):
1077 """Get the minimum theta limit in degrees."""
1078 return np.rad2deg(self.viewLim.xmin)
1079
1080 def set_thetalim(self, *args, **kwargs):
1081 r"""
1082 Set the minimum and maximum theta values.
1083
1084 Can take the following signatures:
1085
1086 - ``set_thetalim(minval, maxval)``: Set the limits in radians.
1087 - ``set_thetalim(thetamin=minval, thetamax=maxval)``: Set the limits
1088 in degrees.
1089
1090 where minval and maxval are the minimum and maximum limits. Values are
1091 wrapped in to the range :math:`[0, 2\pi]` (in radians), so for example
1092 it is possible to do ``set_thetalim(-np.pi / 2, np.pi / 2)`` to have
1093 an axis symmetric around 0. A ValueError is raised if the absolute
1094 angle difference is larger than a full circle.
1095 """
1096 orig_lim = self.get_xlim() # in radians
1097 if 'thetamin' in kwargs:
1098 kwargs['xmin'] = np.deg2rad(kwargs.pop('thetamin'))
1099 if 'thetamax' in kwargs:
1100 kwargs['xmax'] = np.deg2rad(kwargs.pop('thetamax'))
1101 new_min, new_max = self.set_xlim(*args, **kwargs)
1102 # Parsing all permutations of *args, **kwargs is tricky; it is simpler
1103 # to let set_xlim() do it and then validate the limits.
1104 if abs(new_max - new_min) > 2 * np.pi:
1105 self.set_xlim(orig_lim) # un-accept the change
1106 raise ValueError("The angle range must be less than a full circle")
1107 return tuple(np.rad2deg((new_min, new_max)))
1108
1109 def set_theta_offset(self, offset):
1110 """
1111 Set the offset for the location of 0 in radians.
1112 """
1113 mtx = self._theta_offset.get_matrix()
1114 mtx[0, 2] = offset
1115 self._theta_offset.invalidate()
1116
1117 def get_theta_offset(self):
1118 """
1119 Get the offset for the location of 0 in radians.
1120 """
1121 return self._theta_offset.get_matrix()[0, 2]
1122
1123 def set_theta_zero_location(self, loc, offset=0.0):
1124 """
1125 Set the location of theta's zero.
1126
1127 This simply calls `set_theta_offset` with the correct value in radians.
1128
1129 Parameters
1130 ----------
1131 loc : str
1132 May be one of "N", "NW", "W", "SW", "S", "SE", "E", or "NE".
1133 offset : float, default: 0
1134 An offset in degrees to apply from the specified *loc*. **Note:**
1135 this offset is *always* applied counter-clockwise regardless of
1136 the direction setting.
1137 """
1138 mapping = {
1139 'N': np.pi * 0.5,
1140 'NW': np.pi * 0.75,
1141 'W': np.pi,
1142 'SW': np.pi * 1.25,
1143 'S': np.pi * 1.5,
1144 'SE': np.pi * 1.75,
1145 'E': 0,
1146 'NE': np.pi * 0.25}
1147 return self.set_theta_offset(mapping[loc] + np.deg2rad(offset))
1148
1149 def set_theta_direction(self, direction):
1150 """
1151 Set the direction in which theta increases.
1152
1153 clockwise, -1:
1154 Theta increases in the clockwise direction
1155
1156 counterclockwise, anticlockwise, 1:
1157 Theta increases in the counterclockwise direction
1158 """
1159 mtx = self._direction.get_matrix()
1160 if direction in ('clockwise', -1):
1161 mtx[0, 0] = -1
1162 elif direction in ('counterclockwise', 'anticlockwise', 1):
1163 mtx[0, 0] = 1
1164 else:
1165 _api.check_in_list(
1166 [-1, 1, 'clockwise', 'counterclockwise', 'anticlockwise'],
1167 direction=direction)
1168 self._direction.invalidate()
1169
1170 def get_theta_direction(self):
1171 """
1172 Get the direction in which theta increases.
1173
1174 -1:
1175 Theta increases in the clockwise direction
1176
1177 1:
1178 Theta increases in the counterclockwise direction
1179 """
1180 return self._direction.get_matrix()[0, 0]
1181
1182 def set_rmax(self, rmax):
1183 """
1184 Set the outer radial limit.
1185
1186 Parameters
1187 ----------
1188 rmax : float
1189 """
1190 self.viewLim.y1 = rmax
1191
1192 def get_rmax(self):
1193 """
1194 Returns
1195 -------
1196 float
1197 Outer radial limit.
1198 """
1199 return self.viewLim.ymax
1200
1201 def set_rmin(self, rmin):
1202 """
1203 Set the inner radial limit.
1204
1205 Parameters
1206 ----------
1207 rmin : float
1208 """
1209 self.viewLim.y0 = rmin
1210
1211 def get_rmin(self):
1212 """
1213 Returns
1214 -------
1215 float
1216 The inner radial limit.
1217 """
1218 return self.viewLim.ymin
1219
1220 def set_rorigin(self, rorigin):
1221 """
1222 Update the radial origin.
1223
1224 Parameters
1225 ----------
1226 rorigin : float
1227 """
1228 self._originViewLim.locked_y0 = rorigin
1229
1230 def get_rorigin(self):
1231 """
1232 Returns
1233 -------
1234 float
1235 """
1236 return self._originViewLim.y0
1237
1238 def get_rsign(self):
1239 return np.sign(self._originViewLim.y1 - self._originViewLim.y0)
1240
1241 def set_rlim(self, bottom=None, top=None, *,
1242 emit=True, auto=False, **kwargs):
1243 """
1244 Set the radial axis view limits.
1245
1246 This function behaves like `.Axes.set_ylim`, but additionally supports
1247 *rmin* and *rmax* as aliases for *bottom* and *top*.
1248
1249 See Also
1250 --------
1251 .Axes.set_ylim
1252 """
1253 if 'rmin' in kwargs:
1254 if bottom is None:
1255 bottom = kwargs.pop('rmin')
1256 else:
1257 raise ValueError('Cannot supply both positional "bottom"'
1258 'argument and kwarg "rmin"')
1259 if 'rmax' in kwargs:
1260 if top is None:
1261 top = kwargs.pop('rmax')
1262 else:
1263 raise ValueError('Cannot supply both positional "top"'
1264 'argument and kwarg "rmax"')
1265 return self.set_ylim(bottom=bottom, top=top, emit=emit, auto=auto,
1266 **kwargs)
1267
1268 def get_rlabel_position(self):
1269 """
1270 Returns
1271 -------
1272 float
1273 The theta position of the radius labels in degrees.
1274 """
1275 return np.rad2deg(self._r_label_position.get_matrix()[0, 2])
1276
1277 def set_rlabel_position(self, value):
1278 """
1279 Update the theta position of the radius labels.
1280
1281 Parameters
1282 ----------
1283 value : number
1284 The angular position of the radius labels in degrees.
1285 """
1286 self._r_label_position.clear().translate(np.deg2rad(value), 0.0)
1287
1288 def set_yscale(self, *args, **kwargs):
1289 super().set_yscale(*args, **kwargs)
1290 self.yaxis.set_major_locator(
1291 self.RadialLocator(self.yaxis.get_major_locator(), self))
1292
1293 def set_rscale(self, *args, **kwargs):
1294 return Axes.set_yscale(self, *args, **kwargs)
1295
1296 def set_rticks(self, *args, **kwargs):
1297 return Axes.set_yticks(self, *args, **kwargs)
1298
1299 def set_thetagrids(self, angles, labels=None, fmt=None, **kwargs):
1300 """
1301 Set the theta gridlines in a polar plot.
1302
1303 Parameters
1304 ----------
1305 angles : tuple with floats, degrees
1306 The angles of the theta gridlines.
1307
1308 labels : tuple with strings or None
1309 The labels to use at each theta gridline. The
1310 `.projections.polar.ThetaFormatter` will be used if None.
1311
1312 fmt : str or None
1313 Format string used in `matplotlib.ticker.FormatStrFormatter`.
1314 For example '%f'. Note that the angle that is used is in
1315 radians.
1316
1317 Returns
1318 -------
1319 lines : list of `.lines.Line2D`
1320 The theta gridlines.
1321
1322 labels : list of `.text.Text`
1323 The tick labels.
1324
1325 Other Parameters
1326 ----------------
1327 **kwargs
1328 *kwargs* are optional `.Text` properties for the labels.
1329
1330 .. warning::
1331
1332 This only sets the properties of the current ticks.
1333 Ticks are not guaranteed to be persistent. Various operations
1334 can create, delete and modify the Tick instances. There is an
1335 imminent risk that these settings can get lost if you work on
1336 the figure further (including also panning/zooming on a
1337 displayed figure).
1338
1339 Use `.set_tick_params` instead if possible.
1340
1341 See Also
1342 --------
1343 .PolarAxes.set_rgrids
1344 .Axis.get_gridlines
1345 .Axis.get_ticklabels
1346 """
1347
1348 # Make sure we take into account unitized data
1349 angles = self.convert_yunits(angles)
1350 angles = np.deg2rad(angles)
1351 self.set_xticks(angles)
1352 if labels is not None:
1353 self.set_xticklabels(labels)
1354 elif fmt is not None:
1355 self.xaxis.set_major_formatter(mticker.FormatStrFormatter(fmt))
1356 for t in self.xaxis.get_ticklabels():
1357 t._internal_update(kwargs)
1358 return self.xaxis.get_ticklines(), self.xaxis.get_ticklabels()
1359
1360 def set_rgrids(self, radii, labels=None, angle=None, fmt=None, **kwargs):
1361 """
1362 Set the radial gridlines on a polar plot.
1363
1364 Parameters
1365 ----------
1366 radii : tuple with floats
1367 The radii for the radial gridlines
1368
1369 labels : tuple with strings or None
1370 The labels to use at each radial gridline. The
1371 `matplotlib.ticker.ScalarFormatter` will be used if None.
1372
1373 angle : float
1374 The angular position of the radius labels in degrees.
1375
1376 fmt : str or None
1377 Format string used in `matplotlib.ticker.FormatStrFormatter`.
1378 For example '%f'.
1379
1380 Returns
1381 -------
1382 lines : list of `.lines.Line2D`
1383 The radial gridlines.
1384
1385 labels : list of `.text.Text`
1386 The tick labels.
1387
1388 Other Parameters
1389 ----------------
1390 **kwargs
1391 *kwargs* are optional `.Text` properties for the labels.
1392
1393 .. warning::
1394
1395 This only sets the properties of the current ticks.
1396 Ticks are not guaranteed to be persistent. Various operations
1397 can create, delete and modify the Tick instances. There is an
1398 imminent risk that these settings can get lost if you work on
1399 the figure further (including also panning/zooming on a
1400 displayed figure).
1401
1402 Use `.set_tick_params` instead if possible.
1403
1404 See Also
1405 --------
1406 .PolarAxes.set_thetagrids
1407 .Axis.get_gridlines
1408 .Axis.get_ticklabels
1409 """
1410 # Make sure we take into account unitized data
1411 radii = self.convert_xunits(radii)
1412 radii = np.asarray(radii)
1413
1414 self.set_yticks(radii)
1415 if labels is not None:
1416 self.set_yticklabels(labels)
1417 elif fmt is not None:
1418 self.yaxis.set_major_formatter(mticker.FormatStrFormatter(fmt))
1419 if angle is None:
1420 angle = self.get_rlabel_position()
1421 self.set_rlabel_position(angle)
1422 for t in self.yaxis.get_ticklabels():
1423 t._internal_update(kwargs)
1424 return self.yaxis.get_gridlines(), self.yaxis.get_ticklabels()
1425
1426 def format_coord(self, theta, r):
1427 # docstring inherited
1428 screen_xy = self.transData.transform((theta, r))
1429 screen_xys = screen_xy + np.stack(
1430 np.meshgrid([-1, 0, 1], [-1, 0, 1])).reshape((2, -1)).T
1431 ts, rs = self.transData.inverted().transform(screen_xys).T
1432 delta_t = abs((ts - theta + np.pi) % (2 * np.pi) - np.pi).max()
1433 delta_t_halfturns = delta_t / np.pi
1434 delta_t_degrees = delta_t_halfturns * 180
1435 delta_r = abs(rs - r).max()
1436 if theta < 0:
1437 theta += 2 * np.pi
1438 theta_halfturns = theta / np.pi
1439 theta_degrees = theta_halfturns * 180
1440
1441 # See ScalarFormatter.format_data_short. For r, use #g-formatting
1442 # (as for linear axes), but for theta, use f-formatting as scientific
1443 # notation doesn't make sense and the trailing dot is ugly.
1444 def format_sig(value, delta, opt, fmt):
1445 # For "f", only count digits after decimal point.
1446 prec = (max(0, -math.floor(math.log10(delta))) if fmt == "f" else
1447 cbook._g_sig_digits(value, delta))
1448 return f"{value:-{opt}.{prec}{fmt}}"
1449
1450 return ('\N{GREEK SMALL LETTER THETA}={}\N{GREEK SMALL LETTER PI} '
1451 '({}\N{DEGREE SIGN}), r={}').format(
1452 format_sig(theta_halfturns, delta_t_halfturns, "", "f"),
1453 format_sig(theta_degrees, delta_t_degrees, "", "f"),
1454 format_sig(r, delta_r, "#", "g"),
1455 )
1456
1457 def get_data_ratio(self):
1458 """
1459 Return the aspect ratio of the data itself. For a polar plot,
1460 this should always be 1.0
1461 """
1462 return 1.0
1463
1464 # # # Interactive panning
1465
1466 def can_zoom(self):
1467 """
1468 Return whether this Axes supports the zoom box button functionality.
1469
1470 A polar Axes does not support zoom boxes.
1471 """
1472 return False
1473
1474 def can_pan(self):
1475 """
1476 Return whether this Axes supports the pan/zoom button functionality.
1477
1478 For a polar Axes, this is slightly misleading. Both panning and
1479 zooming are performed by the same button. Panning is performed
1480 in azimuth while zooming is done along the radial.
1481 """
1482 return True
1483
1484 def start_pan(self, x, y, button):
1485 angle = np.deg2rad(self.get_rlabel_position())
1486 mode = ''
1487 if button == 1:
1488 epsilon = np.pi / 45.0
1489 t, r = self.transData.inverted().transform((x, y))
1490 if angle - epsilon <= t <= angle + epsilon:
1491 mode = 'drag_r_labels'
1492 elif button == 3:
1493 mode = 'zoom'
1494
1495 self._pan_start = types.SimpleNamespace(
1496 rmax=self.get_rmax(),
1497 trans=self.transData.frozen(),
1498 trans_inverse=self.transData.inverted().frozen(),
1499 r_label_angle=self.get_rlabel_position(),
1500 x=x,
1501 y=y,
1502 mode=mode)
1503
1504 def end_pan(self):
1505 del self._pan_start
1506
1507 def drag_pan(self, button, key, x, y):
1508 p = self._pan_start
1509
1510 if p.mode == 'drag_r_labels':
1511 (startt, startr), (t, r) = p.trans_inverse.transform(
1512 [(p.x, p.y), (x, y)])
1513
1514 # Deal with theta
1515 dt = np.rad2deg(startt - t)
1516 self.set_rlabel_position(p.r_label_angle - dt)
1517
1518 trans, vert1, horiz1 = self.get_yaxis_text1_transform(0.0)
1519 trans, vert2, horiz2 = self.get_yaxis_text2_transform(0.0)
1520 for t in self.yaxis.majorTicks + self.yaxis.minorTicks:
1521 t.label1.set_va(vert1)
1522 t.label1.set_ha(horiz1)
1523 t.label2.set_va(vert2)
1524 t.label2.set_ha(horiz2)
1525
1526 elif p.mode == 'zoom':
1527 (startt, startr), (t, r) = p.trans_inverse.transform(
1528 [(p.x, p.y), (x, y)])
1529
1530 # Deal with r
1531 scale = r / startr
1532 self.set_rmax(p.rmax / scale)
1533
1534
1535# To keep things all self-contained, we can put aliases to the Polar classes
1536# defined above. This isn't strictly necessary, but it makes some of the
1537# code more readable, and provides a backwards compatible Polar API. In
1538# particular, this is used by the :doc:`/gallery/specialty_plots/radar_chart`
1539# example to override PolarTransform on a PolarAxes subclass, so make sure that
1540# that example is unaffected before changing this.
1541PolarAxes.PolarTransform = PolarTransform
1542PolarAxes.PolarAffine = PolarAffine
1543PolarAxes.InvertedPolarTransform = InvertedPolarTransform
1544PolarAxes.ThetaFormatter = ThetaFormatter
1545PolarAxes.RadialLocator = RadialLocator
1546PolarAxes.ThetaLocator = ThetaLocator