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