Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/matplotlib/projections/polar.py : 21%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from collections import OrderedDict
2import types
4import numpy as np
6from matplotlib import cbook, rcParams
7from matplotlib.axes import Axes
8import matplotlib.axis as maxis
9import matplotlib.markers as mmarkers
10import matplotlib.patches as mpatches
11import matplotlib.path as mpath
12import matplotlib.ticker as mticker
13import matplotlib.transforms as mtransforms
14import matplotlib.spines as mspines
17class PolarTransform(mtransforms.Transform):
18 """
19 The base polar transform. This handles projection *theta* and
20 *r* into Cartesian coordinate space *x* and *y*, but does not
21 perform the ultimate affine transformation into the correct
22 position.
23 """
24 input_dims = 2
25 output_dims = 2
26 is_separable = False
28 def __init__(self, axis=None, use_rmin=True,
29 _apply_theta_transforms=True):
30 mtransforms.Transform.__init__(self)
31 self._axis = axis
32 self._use_rmin = use_rmin
33 self._apply_theta_transforms = _apply_theta_transforms
35 def __str__(self):
36 return ("{}(\n"
37 "{},\n"
38 " use_rmin={},\n"
39 " _apply_theta_transforms={})"
40 .format(type(self).__name__,
41 mtransforms._indent_str(self._axis),
42 self._use_rmin,
43 self._apply_theta_transforms))
45 def transform_non_affine(self, tr):
46 # docstring inherited
47 t, r = np.transpose(tr)
48 # PolarAxes does not use the theta transforms here, but apply them for
49 # backwards-compatibility if not being used by it.
50 if self._apply_theta_transforms and self._axis is not None:
51 t *= self._axis.get_theta_direction()
52 t += self._axis.get_theta_offset()
53 if self._use_rmin and self._axis is not None:
54 r = (r - self._axis.get_rorigin()) * self._axis.get_rsign()
55 r = np.where(r >= 0, r, np.nan)
56 return np.column_stack([r * np.cos(t), r * np.sin(t)])
58 def transform_path_non_affine(self, path):
59 # docstring inherited
60 vertices = path.vertices
61 if len(vertices) == 2 and vertices[0, 0] == vertices[1, 0]:
62 return mpath.Path(self.transform(vertices), path.codes)
63 ipath = path.interpolated(path._interpolation_steps)
64 return mpath.Path(self.transform(ipath.vertices), ipath.codes)
66 def inverted(self):
67 # docstring inherited
68 return PolarAxes.InvertedPolarTransform(self._axis, self._use_rmin,
69 self._apply_theta_transforms)
72class PolarAffine(mtransforms.Affine2DBase):
73 """
74 The affine part of the polar projection. Scales the output so
75 that maximum radius rests on the edge of the axes circle.
76 """
77 def __init__(self, scale_transform, limits):
78 """
79 *limits* is the view limit of the data. The only part of
80 its bounds that is used is the y limits (for the radius limits).
81 The theta range is handled by the non-affine transform.
82 """
83 mtransforms.Affine2DBase.__init__(self)
84 self._scale_transform = scale_transform
85 self._limits = limits
86 self.set_children(scale_transform, limits)
87 self._mtx = None
89 def __str__(self):
90 return ("{}(\n"
91 "{},\n"
92 "{})"
93 .format(type(self).__name__,
94 mtransforms._indent_str(self._scale_transform),
95 mtransforms._indent_str(self._limits)))
97 def get_matrix(self):
98 # docstring inherited
99 if self._invalid:
100 limits_scaled = self._limits.transformed(self._scale_transform)
101 yscale = limits_scaled.ymax - limits_scaled.ymin
102 affine = mtransforms.Affine2D() \
103 .scale(0.5 / yscale) \
104 .translate(0.5, 0.5)
105 self._mtx = affine.get_matrix()
106 self._inverted = None
107 self._invalid = 0
108 return self._mtx
111class InvertedPolarTransform(mtransforms.Transform):
112 """
113 The inverse of the polar transform, mapping Cartesian
114 coordinate space *x* and *y* back to *theta* and *r*.
115 """
116 input_dims = 2
117 output_dims = 2
118 is_separable = False
120 def __init__(self, axis=None, use_rmin=True,
121 _apply_theta_transforms=True):
122 mtransforms.Transform.__init__(self)
123 self._axis = axis
124 self._use_rmin = use_rmin
125 self._apply_theta_transforms = _apply_theta_transforms
127 def __str__(self):
128 return ("{}(\n"
129 "{},\n"
130 " use_rmin={},\n"
131 " _apply_theta_transforms={})"
132 .format(type(self).__name__,
133 mtransforms._indent_str(self._axis),
134 self._use_rmin,
135 self._apply_theta_transforms))
137 def transform_non_affine(self, xy):
138 # docstring inherited
139 x, y = xy.T
140 r = np.hypot(x, y)
141 theta = (np.arctan2(y, x) + 2 * np.pi) % (2 * np.pi)
142 # PolarAxes does not use the theta transforms here, but apply them for
143 # backwards-compatibility if not being used by it.
144 if self._apply_theta_transforms and self._axis is not None:
145 theta -= self._axis.get_theta_offset()
146 theta *= self._axis.get_theta_direction()
147 theta %= 2 * np.pi
148 if self._use_rmin and self._axis is not None:
149 r += self._axis.get_rorigin()
150 r *= self._axis.get_rsign()
151 return np.column_stack([theta, r])
153 def inverted(self):
154 # docstring inherited
155 return PolarAxes.PolarTransform(self._axis, self._use_rmin,
156 self._apply_theta_transforms)
159class ThetaFormatter(mticker.Formatter):
160 """
161 Used to format the *theta* tick labels. Converts the native
162 unit of radians into degrees and adds a degree symbol.
163 """
164 def __call__(self, x, pos=None):
165 vmin, vmax = self.axis.get_view_interval()
166 d = np.rad2deg(abs(vmax - vmin))
167 digits = max(-int(np.log10(d) - 1.5), 0)
169 if rcParams['text.usetex'] and not rcParams['text.latex.unicode']:
170 format_str = r"${value:0.{digits:d}f}^\circ$"
171 return format_str.format(value=np.rad2deg(x), digits=digits)
172 else:
173 # we use unicode, rather than mathtext with \circ, so
174 # that it will work correctly with any arbitrary font
175 # (assuming it has a degree sign), whereas $5\circ$
176 # will only work correctly with one of the supported
177 # math fonts (Computer Modern and STIX)
178 format_str = "{value:0.{digits:d}f}\N{DEGREE SIGN}"
179 return format_str.format(value=np.rad2deg(x), digits=digits)
182class _AxisWrapper:
183 def __init__(self, axis):
184 self._axis = axis
186 def get_view_interval(self):
187 return np.rad2deg(self._axis.get_view_interval())
189 def set_view_interval(self, vmin, vmax):
190 self._axis.set_view_interval(*np.deg2rad((vmin, vmax)))
192 def get_minpos(self):
193 return np.rad2deg(self._axis.get_minpos())
195 def get_data_interval(self):
196 return np.rad2deg(self._axis.get_data_interval())
198 def set_data_interval(self, vmin, vmax):
199 self._axis.set_data_interval(*np.deg2rad((vmin, vmax)))
201 def get_tick_space(self):
202 return self._axis.get_tick_space()
205class ThetaLocator(mticker.Locator):
206 """
207 Used to locate theta ticks.
209 This will work the same as the base locator except in the case that the
210 view spans the entire circle. In such cases, the previously used default
211 locations of every 45 degrees are returned.
212 """
213 def __init__(self, base):
214 self.base = base
215 self.axis = self.base.axis = _AxisWrapper(self.base.axis)
217 def set_axis(self, axis):
218 self.axis = _AxisWrapper(axis)
219 self.base.set_axis(self.axis)
221 def __call__(self):
222 lim = self.axis.get_view_interval()
223 if _is_full_circle_deg(lim[0], lim[1]):
224 return np.arange(8) * 2 * np.pi / 8
225 else:
226 return np.deg2rad(self.base())
228 @cbook.deprecated("3.2")
229 def autoscale(self):
230 return self.base.autoscale()
232 def pan(self, numsteps):
233 return self.base.pan(numsteps)
235 def refresh(self):
236 # docstring inherited
237 return self.base.refresh()
239 def view_limits(self, vmin, vmax):
240 vmin, vmax = np.rad2deg((vmin, vmax))
241 return np.deg2rad(self.base.view_limits(vmin, vmax))
243 def zoom(self, direction):
244 return self.base.zoom(direction)
247class ThetaTick(maxis.XTick):
248 """
249 A theta-axis tick.
251 This subclass of `XTick` provides angular ticks with some small
252 modification to their re-positioning such that ticks are rotated based on
253 tick location. This results in ticks that are correctly perpendicular to
254 the arc spine.
256 When 'auto' rotation is enabled, labels are also rotated to be parallel to
257 the spine. The label padding is also applied here since it's not possible
258 to use a generic axes transform to produce tick-specific padding.
259 """
260 def __init__(self, axes, *args, **kwargs):
261 self._text1_translate = mtransforms.ScaledTranslation(
262 0, 0,
263 axes.figure.dpi_scale_trans)
264 self._text2_translate = mtransforms.ScaledTranslation(
265 0, 0,
266 axes.figure.dpi_scale_trans)
267 super().__init__(axes, *args, **kwargs)
269 def _get_text1(self):
270 t = super()._get_text1()
271 t.set_rotation_mode('anchor')
272 t.set_transform(t.get_transform() + self._text1_translate)
273 return t
275 def _get_text2(self):
276 t = super()._get_text2()
277 t.set_rotation_mode('anchor')
278 t.set_transform(t.get_transform() + self._text2_translate)
279 return t
281 def _apply_params(self, **kw):
282 super()._apply_params(**kw)
284 # Ensure transform is correct; sometimes this gets reset.
285 trans = self.label1.get_transform()
286 if not trans.contains_branch(self._text1_translate):
287 self.label1.set_transform(trans + self._text1_translate)
288 trans = self.label2.get_transform()
289 if not trans.contains_branch(self._text2_translate):
290 self.label2.set_transform(trans + self._text2_translate)
292 def _update_padding(self, pad, angle):
293 padx = pad * np.cos(angle) / 72
294 pady = pad * np.sin(angle) / 72
295 self._text1_translate._t = (padx, pady)
296 self._text1_translate.invalidate()
297 self._text2_translate._t = (-padx, -pady)
298 self._text2_translate.invalidate()
300 def update_position(self, loc):
301 super().update_position(loc)
302 axes = self.axes
303 angle = loc * axes.get_theta_direction() + axes.get_theta_offset()
304 text_angle = np.rad2deg(angle) % 360 - 90
305 angle -= np.pi / 2
307 marker = self.tick1line.get_marker()
308 if marker in (mmarkers.TICKUP, '|'):
309 trans = mtransforms.Affine2D().scale(1, 1).rotate(angle)
310 elif marker == mmarkers.TICKDOWN:
311 trans = mtransforms.Affine2D().scale(1, -1).rotate(angle)
312 else:
313 # Don't modify custom tick line markers.
314 trans = self.tick1line._marker._transform
315 self.tick1line._marker._transform = trans
317 marker = self.tick2line.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.tick2line._marker._transform
325 self.tick2line._marker._transform = trans
327 mode, user_angle = self._labelrotation
328 if mode == 'default':
329 text_angle = user_angle
330 else:
331 if text_angle > 90:
332 text_angle -= 180
333 elif text_angle < -90:
334 text_angle += 180
335 text_angle += user_angle
336 self.label1.set_rotation(text_angle)
337 self.label2.set_rotation(text_angle)
339 # This extra padding helps preserve the look from previous releases but
340 # is also needed because labels are anchored to their center.
341 pad = self._pad + 7
342 self._update_padding(pad,
343 self._loc * axes.get_theta_direction() +
344 axes.get_theta_offset())
347class ThetaAxis(maxis.XAxis):
348 """
349 A theta Axis.
351 This overrides certain properties of an `XAxis` to provide special-casing
352 for an angular axis.
353 """
354 __name__ = 'thetaaxis'
355 axis_name = 'theta' #: Read-only name identifying the axis.
357 def _get_tick(self, major):
358 if major:
359 tick_kw = self._major_tick_kw
360 else:
361 tick_kw = self._minor_tick_kw
362 return ThetaTick(self.axes, 0, '', major=major, **tick_kw)
364 def _wrap_locator_formatter(self):
365 self.set_major_locator(ThetaLocator(self.get_major_locator()))
366 self.set_major_formatter(ThetaFormatter())
367 self.isDefault_majloc = True
368 self.isDefault_majfmt = True
370 def cla(self):
371 super().cla()
372 self.set_ticks_position('none')
373 self._wrap_locator_formatter()
375 def _set_scale(self, value, **kwargs):
376 super()._set_scale(value, **kwargs)
377 self._wrap_locator_formatter()
379 def _copy_tick_props(self, src, dest):
380 'Copy the props from src tick to dest tick'
381 if src is None or dest is None:
382 return
383 super()._copy_tick_props(src, dest)
385 # Ensure that tick transforms are independent so that padding works.
386 trans = dest._get_text1_transform()[0]
387 dest.label1.set_transform(trans + dest._text1_translate)
388 trans = dest._get_text2_transform()[0]
389 dest.label2.set_transform(trans + dest._text2_translate)
392class RadialLocator(mticker.Locator):
393 """
394 Used to locate radius ticks.
396 Ensures that all ticks are strictly positive. For all other
397 tasks, it delegates to the base
398 :class:`~matplotlib.ticker.Locator` (which may be different
399 depending on the scale of the *r*-axis.
400 """
402 def __init__(self, base, axes=None):
403 self.base = base
404 self._axes = axes
406 def __call__(self):
407 show_all = True
408 # Ensure previous behaviour with full circle non-annular views.
409 if self._axes:
410 if _is_full_circle_rad(*self._axes.viewLim.intervalx):
411 rorigin = self._axes.get_rorigin() * self._axes.get_rsign()
412 if self._axes.get_rmin() <= rorigin:
413 show_all = False
414 if show_all:
415 return self.base()
416 else:
417 return [tick for tick in self.base() if tick > rorigin]
419 @cbook.deprecated("3.2")
420 def autoscale(self):
421 return self.base.autoscale()
423 def pan(self, numsteps):
424 return self.base.pan(numsteps)
426 def zoom(self, direction):
427 return self.base.zoom(direction)
429 def refresh(self):
430 # docstring inherited
431 return self.base.refresh()
433 def nonsingular(self, vmin, vmax):
434 # docstring inherited
435 return ((0, 1) if (vmin, vmax) == (-np.inf, np.inf) # Init. limits.
436 else self.base.nonsingular(vmin, vmax))
438 def view_limits(self, vmin, vmax):
439 vmin, vmax = self.base.view_limits(vmin, vmax)
440 if vmax > vmin:
441 # this allows inverted r/y-lims
442 vmin = min(0, vmin)
443 return mtransforms.nonsingular(vmin, vmax)
446class _ThetaShift(mtransforms.ScaledTranslation):
447 """
448 Apply a padding shift based on axes theta limits.
450 This is used to create padding for radial ticks.
452 Parameters
453 ----------
454 axes : `~matplotlib.axes.Axes`
455 The owning axes; used to determine limits.
456 pad : float
457 The padding to apply, in points.
458 mode : {'min', 'max', 'rlabel'}
459 Whether to shift away from the start (``'min'``) or the end (``'max'``)
460 of the axes, or using the rlabel position (``'rlabel'``).
461 """
462 def __init__(self, axes, pad, mode):
463 mtransforms.ScaledTranslation.__init__(self, pad, pad,
464 axes.figure.dpi_scale_trans)
465 self.set_children(axes._realViewLim)
466 self.axes = axes
467 self.mode = mode
468 self.pad = pad
470 def __str__(self):
471 return ("{}(\n"
472 "{},\n"
473 "{},\n"
474 "{})"
475 .format(type(self).__name__,
476 mtransforms._indent_str(self.axes),
477 mtransforms._indent_str(self.pad),
478 mtransforms._indent_str(repr(self.mode))))
480 def get_matrix(self):
481 if self._invalid:
482 if self.mode == 'rlabel':
483 angle = (
484 np.deg2rad(self.axes.get_rlabel_position()) *
485 self.axes.get_theta_direction() +
486 self.axes.get_theta_offset()
487 )
488 else:
489 if self.mode == 'min':
490 angle = self.axes._realViewLim.xmin
491 elif self.mode == 'max':
492 angle = self.axes._realViewLim.xmax
494 if self.mode in ('rlabel', 'min'):
495 padx = np.cos(angle - np.pi / 2)
496 pady = np.sin(angle - np.pi / 2)
497 else:
498 padx = np.cos(angle + np.pi / 2)
499 pady = np.sin(angle + np.pi / 2)
501 self._t = (self.pad * padx / 72, self.pad * pady / 72)
502 return mtransforms.ScaledTranslation.get_matrix(self)
505class RadialTick(maxis.YTick):
506 """
507 A radial-axis tick.
509 This subclass of `YTick` provides radial ticks with some small modification
510 to their re-positioning such that ticks are rotated based on axes limits.
511 This results in ticks that are correctly perpendicular to the spine. Labels
512 are also rotated to be perpendicular to the spine, when 'auto' rotation is
513 enabled.
514 """
515 def _get_text1(self):
516 t = super()._get_text1()
517 t.set_rotation_mode('anchor')
518 return t
520 def _get_text2(self):
521 t = super()._get_text2()
522 t.set_rotation_mode('anchor')
523 return t
525 def _determine_anchor(self, mode, angle, start):
526 # Note: angle is the (spine angle - 90) because it's used for the tick
527 # & text setup, so all numbers below are -90 from (normed) spine angle.
528 if mode == 'auto':
529 if start:
530 if -90 <= angle <= 90:
531 return 'left', 'center'
532 else:
533 return 'right', 'center'
534 else:
535 if -90 <= angle <= 90:
536 return 'right', 'center'
537 else:
538 return 'left', 'center'
539 else:
540 if start:
541 if angle < -68.5:
542 return 'center', 'top'
543 elif angle < -23.5:
544 return 'left', 'top'
545 elif angle < 22.5:
546 return 'left', 'center'
547 elif angle < 67.5:
548 return 'left', 'bottom'
549 elif angle < 112.5:
550 return 'center', 'bottom'
551 elif angle < 157.5:
552 return 'right', 'bottom'
553 elif angle < 202.5:
554 return 'right', 'center'
555 elif angle < 247.5:
556 return 'right', 'top'
557 else:
558 return 'center', 'top'
559 else:
560 if angle < -68.5:
561 return 'center', 'bottom'
562 elif angle < -23.5:
563 return 'right', 'bottom'
564 elif angle < 22.5:
565 return 'right', 'center'
566 elif angle < 67.5:
567 return 'right', 'top'
568 elif angle < 112.5:
569 return 'center', 'top'
570 elif angle < 157.5:
571 return 'left', 'top'
572 elif angle < 202.5:
573 return 'left', 'center'
574 elif angle < 247.5:
575 return 'left', 'bottom'
576 else:
577 return 'center', 'bottom'
579 def update_position(self, loc):
580 super().update_position(loc)
581 axes = self.axes
582 thetamin = axes.get_thetamin()
583 thetamax = axes.get_thetamax()
584 direction = axes.get_theta_direction()
585 offset_rad = axes.get_theta_offset()
586 offset = np.rad2deg(offset_rad)
587 full = _is_full_circle_deg(thetamin, thetamax)
589 if full:
590 angle = (axes.get_rlabel_position() * direction +
591 offset) % 360 - 90
592 tick_angle = 0
593 else:
594 angle = (thetamin * direction + offset) % 360 - 90
595 if direction > 0:
596 tick_angle = np.deg2rad(angle)
597 else:
598 tick_angle = np.deg2rad(angle + 180)
599 text_angle = (angle + 90) % 180 - 90 # between -90 and +90.
600 mode, user_angle = self._labelrotation
601 if mode == 'auto':
602 text_angle += user_angle
603 else:
604 text_angle = user_angle
606 if full:
607 ha = self.label1.get_horizontalalignment()
608 va = self.label1.get_verticalalignment()
609 else:
610 ha, va = self._determine_anchor(mode, angle, direction > 0)
611 self.label1.set_horizontalalignment(ha)
612 self.label1.set_verticalalignment(va)
613 self.label1.set_rotation(text_angle)
615 marker = self.tick1line.get_marker()
616 if marker == mmarkers.TICKLEFT:
617 trans = mtransforms.Affine2D().rotate(tick_angle)
618 elif marker == '_':
619 trans = mtransforms.Affine2D().rotate(tick_angle + np.pi / 2)
620 elif marker == mmarkers.TICKRIGHT:
621 trans = mtransforms.Affine2D().scale(-1, 1).rotate(tick_angle)
622 else:
623 # Don't modify custom tick line markers.
624 trans = self.tick1line._marker._transform
625 self.tick1line._marker._transform = trans
627 if full:
628 self.label2.set_visible(False)
629 self.tick2line.set_visible(False)
630 angle = (thetamax * direction + offset) % 360 - 90
631 if direction > 0:
632 tick_angle = np.deg2rad(angle)
633 else:
634 tick_angle = np.deg2rad(angle + 180)
635 text_angle = (angle + 90) % 180 - 90 # between -90 and +90.
636 mode, user_angle = self._labelrotation
637 if mode == 'auto':
638 text_angle += user_angle
639 else:
640 text_angle = user_angle
642 ha, va = self._determine_anchor(mode, angle, direction < 0)
643 self.label2.set_ha(ha)
644 self.label2.set_va(va)
645 self.label2.set_rotation(text_angle)
647 marker = self.tick2line.get_marker()
648 if marker == mmarkers.TICKLEFT:
649 trans = mtransforms.Affine2D().rotate(tick_angle)
650 elif marker == '_':
651 trans = mtransforms.Affine2D().rotate(tick_angle + np.pi / 2)
652 elif marker == mmarkers.TICKRIGHT:
653 trans = mtransforms.Affine2D().scale(-1, 1).rotate(tick_angle)
654 else:
655 # Don't modify custom tick line markers.
656 trans = self.tick2line._marker._transform
657 self.tick2line._marker._transform = trans
660class RadialAxis(maxis.YAxis):
661 """
662 A radial Axis.
664 This overrides certain properties of a `YAxis` to provide special-casing
665 for a radial axis.
666 """
667 __name__ = 'radialaxis'
668 axis_name = 'radius' #: Read-only name identifying the axis.
670 def __init__(self, *args, **kwargs):
671 super().__init__(*args, **kwargs)
672 self.sticky_edges.y.append(0)
674 def _get_tick(self, major):
675 if major:
676 tick_kw = self._major_tick_kw
677 else:
678 tick_kw = self._minor_tick_kw
679 return RadialTick(self.axes, 0, '', major=major, **tick_kw)
681 def _wrap_locator_formatter(self):
682 self.set_major_locator(RadialLocator(self.get_major_locator(),
683 self.axes))
684 self.isDefault_majloc = True
686 def cla(self):
687 super().cla()
688 self.set_ticks_position('none')
689 self._wrap_locator_formatter()
691 def _set_scale(self, value, **kwargs):
692 super()._set_scale(value, **kwargs)
693 self._wrap_locator_formatter()
696def _is_full_circle_deg(thetamin, thetamax):
697 """
698 Determine if a wedge (in degrees) spans the full circle.
700 The condition is derived from :class:`~matplotlib.patches.Wedge`.
701 """
702 return abs(abs(thetamax - thetamin) - 360.0) < 1e-12
705def _is_full_circle_rad(thetamin, thetamax):
706 """
707 Determine if a wedge (in radians) spans the full circle.
709 The condition is derived from :class:`~matplotlib.patches.Wedge`.
710 """
711 return abs(abs(thetamax - thetamin) - 2 * np.pi) < 1.74e-14
714class _WedgeBbox(mtransforms.Bbox):
715 """
716 Transform (theta, r) wedge Bbox into axes bounding box.
718 Parameters
719 ----------
720 center : (float, float)
721 Center of the wedge
722 viewLim : `~matplotlib.transforms.Bbox`
723 Bbox determining the boundaries of the wedge
724 originLim : `~matplotlib.transforms.Bbox`
725 Bbox determining the origin for the wedge, if different from *viewLim*
726 """
727 def __init__(self, center, viewLim, originLim, **kwargs):
728 mtransforms.Bbox.__init__(self,
729 np.array([[0.0, 0.0], [1.0, 1.0]], np.float),
730 **kwargs)
731 self._center = center
732 self._viewLim = viewLim
733 self._originLim = originLim
734 self.set_children(viewLim, originLim)
736 def __str__(self):
737 return ("{}(\n"
738 "{},\n"
739 "{},\n"
740 "{})"
741 .format(type(self).__name__,
742 mtransforms._indent_str(self._center),
743 mtransforms._indent_str(self._viewLim),
744 mtransforms._indent_str(self._originLim)))
746 def get_points(self):
747 # docstring inherited
748 if self._invalid:
749 points = self._viewLim.get_points().copy()
750 # Scale angular limits to work with Wedge.
751 points[:, 0] *= 180 / np.pi
752 if points[0, 0] > points[1, 0]:
753 points[:, 0] = points[::-1, 0]
755 # Scale radial limits based on origin radius.
756 points[:, 1] -= self._originLim.y0
758 # Scale radial limits to match axes limits.
759 rscale = 0.5 / points[1, 1]
760 points[:, 1] *= rscale
761 width = min(points[1, 1] - points[0, 1], 0.5)
763 # Generate bounding box for wedge.
764 wedge = mpatches.Wedge(self._center, points[1, 1],
765 points[0, 0], points[1, 0],
766 width=width)
767 self.update_from_path(wedge.get_path())
769 # Ensure equal aspect ratio.
770 w, h = self._points[1] - self._points[0]
771 deltah = max(w - h, 0) / 2
772 deltaw = max(h - w, 0) / 2
773 self._points += np.array([[-deltaw, -deltah], [deltaw, deltah]])
775 self._invalid = 0
777 return self._points
780class PolarAxes(Axes):
781 """
782 A polar graph projection, where the input dimensions are *theta*, *r*.
784 Theta starts pointing east and goes anti-clockwise.
785 """
786 name = 'polar'
788 def __init__(self, *args,
789 theta_offset=0, theta_direction=1, rlabel_position=22.5,
790 **kwargs):
791 # docstring inherited
792 self._default_theta_offset = theta_offset
793 self._default_theta_direction = theta_direction
794 self._default_rlabel_position = np.deg2rad(rlabel_position)
795 super().__init__(*args, **kwargs)
796 self.use_sticky_edges = True
797 self.set_aspect('equal', adjustable='box', anchor='C')
798 self.cla()
800 def cla(self):
801 Axes.cla(self)
803 self.title.set_y(1.05)
805 start = self.spines.get('start', None)
806 if start:
807 start.set_visible(False)
808 end = self.spines.get('end', None)
809 if end:
810 end.set_visible(False)
811 self.set_xlim(0.0, 2 * np.pi)
813 self.grid(rcParams['polaraxes.grid'])
814 inner = self.spines.get('inner', None)
815 if inner:
816 inner.set_visible(False)
818 self.set_rorigin(None)
819 self.set_theta_offset(self._default_theta_offset)
820 self.set_theta_direction(self._default_theta_direction)
822 def _init_axis(self):
823 "move this out of __init__ because non-separable axes don't use it"
824 self.xaxis = ThetaAxis(self)
825 self.yaxis = RadialAxis(self)
826 # Calling polar_axes.xaxis.cla() or polar_axes.xaxis.cla()
827 # results in weird artifacts. Therefore we disable this for
828 # now.
829 # self.spines['polar'].register_axis(self.yaxis)
830 self._update_transScale()
832 def _set_lim_and_transforms(self):
833 # A view limit where the minimum radius can be locked if the user
834 # specifies an alternate origin.
835 self._originViewLim = mtransforms.LockableBbox(self.viewLim)
837 # Handle angular offset and direction.
838 self._direction = mtransforms.Affine2D() \
839 .scale(self._default_theta_direction, 1.0)
840 self._theta_offset = mtransforms.Affine2D() \
841 .translate(self._default_theta_offset, 0.0)
842 self.transShift = mtransforms.composite_transform_factory(
843 self._direction,
844 self._theta_offset)
845 # A view limit shifted to the correct location after accounting for
846 # orientation and offset.
847 self._realViewLim = mtransforms.TransformedBbox(self.viewLim,
848 self.transShift)
850 # Transforms the x and y axis separately by a scale factor
851 # It is assumed that this part will have non-linear components
852 self.transScale = mtransforms.TransformWrapper(
853 mtransforms.IdentityTransform())
855 # Scale view limit into a bbox around the selected wedge. This may be
856 # smaller than the usual unit axes rectangle if not plotting the full
857 # circle.
858 self.axesLim = _WedgeBbox((0.5, 0.5),
859 self._realViewLim, self._originViewLim)
861 # Scale the wedge to fill the axes.
862 self.transWedge = mtransforms.BboxTransformFrom(self.axesLim)
864 # Scale the axes to fill the figure.
865 self.transAxes = mtransforms.BboxTransformTo(self.bbox)
867 # A (possibly non-linear) projection on the (already scaled)
868 # data. This one is aware of rmin
869 self.transProjection = self.PolarTransform(
870 self,
871 _apply_theta_transforms=False)
872 # Add dependency on rorigin.
873 self.transProjection.set_children(self._originViewLim)
875 # An affine transformation on the data, generally to limit the
876 # range of the axes
877 self.transProjectionAffine = self.PolarAffine(self.transScale,
878 self._originViewLim)
880 # The complete data transformation stack -- from data all the
881 # way to display coordinates
882 self.transData = (
883 self.transScale + self.transShift + self.transProjection +
884 (self.transProjectionAffine + self.transWedge + self.transAxes))
886 # This is the transform for theta-axis ticks. It is
887 # equivalent to transData, except it always puts r == 0.0 and r == 1.0
888 # at the edge of the axis circles.
889 self._xaxis_transform = (
890 mtransforms.blended_transform_factory(
891 mtransforms.IdentityTransform(),
892 mtransforms.BboxTransformTo(self.viewLim)) +
893 self.transData)
894 # The theta labels are flipped along the radius, so that text 1 is on
895 # the outside by default. This should work the same as before.
896 flipr_transform = mtransforms.Affine2D() \
897 .translate(0.0, -0.5) \
898 .scale(1.0, -1.0) \
899 .translate(0.0, 0.5)
900 self._xaxis_text_transform = flipr_transform + self._xaxis_transform
902 # This is the transform for r-axis ticks. It scales the theta
903 # axis so the gridlines from 0.0 to 1.0, now go from thetamin to
904 # thetamax.
905 self._yaxis_transform = (
906 mtransforms.blended_transform_factory(
907 mtransforms.BboxTransformTo(self.viewLim),
908 mtransforms.IdentityTransform()) +
909 self.transData)
910 # The r-axis labels are put at an angle and padded in the r-direction
911 self._r_label_position = mtransforms.Affine2D() \
912 .translate(self._default_rlabel_position, 0.0)
913 self._yaxis_text_transform = mtransforms.TransformWrapper(
914 self._r_label_position + self.transData)
916 def get_xaxis_transform(self, which='grid'):
917 cbook._check_in_list(['tick1', 'tick2', 'grid'], which=which)
918 return self._xaxis_transform
920 def get_xaxis_text1_transform(self, pad):
921 return self._xaxis_text_transform, 'center', 'center'
923 def get_xaxis_text2_transform(self, pad):
924 return self._xaxis_text_transform, 'center', 'center'
926 def get_yaxis_transform(self, which='grid'):
927 if which in ('tick1', 'tick2'):
928 return self._yaxis_text_transform
929 elif which == 'grid':
930 return self._yaxis_transform
931 else:
932 cbook._check_in_list(['tick1', 'tick2', 'grid'], which=which)
934 def get_yaxis_text1_transform(self, pad):
935 thetamin, thetamax = self._realViewLim.intervalx
936 if _is_full_circle_rad(thetamin, thetamax):
937 return self._yaxis_text_transform, 'bottom', 'left'
938 elif self.get_theta_direction() > 0:
939 halign = 'left'
940 pad_shift = _ThetaShift(self, pad, 'min')
941 else:
942 halign = 'right'
943 pad_shift = _ThetaShift(self, pad, 'max')
944 return self._yaxis_text_transform + pad_shift, 'center', halign
946 def get_yaxis_text2_transform(self, pad):
947 if self.get_theta_direction() > 0:
948 halign = 'right'
949 pad_shift = _ThetaShift(self, pad, 'max')
950 else:
951 halign = 'left'
952 pad_shift = _ThetaShift(self, pad, 'min')
953 return self._yaxis_text_transform + pad_shift, 'center', halign
955 def draw(self, *args, **kwargs):
956 self._unstale_viewLim()
957 thetamin, thetamax = np.rad2deg(self._realViewLim.intervalx)
958 if thetamin > thetamax:
959 thetamin, thetamax = thetamax, thetamin
960 rmin, rmax = ((self._realViewLim.intervaly - self.get_rorigin()) *
961 self.get_rsign())
962 if isinstance(self.patch, mpatches.Wedge):
963 # Backwards-compatibility: Any subclassed Axes might override the
964 # patch to not be the Wedge that PolarAxes uses.
965 center = self.transWedge.transform((0.5, 0.5))
966 self.patch.set_center(center)
967 self.patch.set_theta1(thetamin)
968 self.patch.set_theta2(thetamax)
970 edge, _ = self.transWedge.transform((1, 0))
971 radius = edge - center[0]
972 width = min(radius * (rmax - rmin) / rmax, radius)
973 self.patch.set_radius(radius)
974 self.patch.set_width(width)
976 inner_width = radius - width
977 inner = self.spines.get('inner', None)
978 if inner:
979 inner.set_visible(inner_width != 0.0)
981 visible = not _is_full_circle_deg(thetamin, thetamax)
982 # For backwards compatibility, any subclassed Axes might override the
983 # spines to not include start/end that PolarAxes uses.
984 start = self.spines.get('start', None)
985 end = self.spines.get('end', None)
986 if start:
987 start.set_visible(visible)
988 if end:
989 end.set_visible(visible)
990 if visible:
991 yaxis_text_transform = self._yaxis_transform
992 else:
993 yaxis_text_transform = self._r_label_position + self.transData
994 if self._yaxis_text_transform != yaxis_text_transform:
995 self._yaxis_text_transform.set(yaxis_text_transform)
996 self.yaxis.reset_ticks()
997 self.yaxis.set_clip_path(self.patch)
999 Axes.draw(self, *args, **kwargs)
1001 def _gen_axes_patch(self):
1002 return mpatches.Wedge((0.5, 0.5), 0.5, 0.0, 360.0)
1004 def _gen_axes_spines(self):
1005 spines = OrderedDict([
1006 ('polar', mspines.Spine.arc_spine(self, 'top',
1007 (0.5, 0.5), 0.5, 0.0, 360.0)),
1008 ('start', mspines.Spine.linear_spine(self, 'left')),
1009 ('end', mspines.Spine.linear_spine(self, 'right')),
1010 ('inner', mspines.Spine.arc_spine(self, 'bottom',
1011 (0.5, 0.5), 0.0, 0.0, 360.0))
1012 ])
1013 spines['polar'].set_transform(self.transWedge + self.transAxes)
1014 spines['inner'].set_transform(self.transWedge + self.transAxes)
1015 spines['start'].set_transform(self._yaxis_transform)
1016 spines['end'].set_transform(self._yaxis_transform)
1017 return spines
1019 def set_thetamax(self, thetamax):
1020 """Set the maximum theta limit in degrees."""
1021 self.viewLim.x1 = np.deg2rad(thetamax)
1023 def get_thetamax(self):
1024 """Return the maximum theta limit in degrees."""
1025 return np.rad2deg(self.viewLim.xmax)
1027 def set_thetamin(self, thetamin):
1028 """Set the minimum theta limit in degrees."""
1029 self.viewLim.x0 = np.deg2rad(thetamin)
1031 def get_thetamin(self):
1032 """Get the minimum theta limit in degrees."""
1033 return np.rad2deg(self.viewLim.xmin)
1035 def set_thetalim(self, *args, **kwargs):
1036 """
1037 Set the minimum and maximum theta values.
1039 Parameters
1040 ----------
1041 thetamin : float
1042 Minimum value in degrees.
1043 thetamax : float
1044 Maximum value in degrees.
1045 """
1046 if 'thetamin' in kwargs:
1047 kwargs['xmin'] = np.deg2rad(kwargs.pop('thetamin'))
1048 if 'thetamax' in kwargs:
1049 kwargs['xmax'] = np.deg2rad(kwargs.pop('thetamax'))
1050 return tuple(np.rad2deg(self.set_xlim(*args, **kwargs)))
1052 def set_theta_offset(self, offset):
1053 """
1054 Set the offset for the location of 0 in radians.
1055 """
1056 mtx = self._theta_offset.get_matrix()
1057 mtx[0, 2] = offset
1058 self._theta_offset.invalidate()
1060 def get_theta_offset(self):
1061 """
1062 Get the offset for the location of 0 in radians.
1063 """
1064 return self._theta_offset.get_matrix()[0, 2]
1066 def set_theta_zero_location(self, loc, offset=0.0):
1067 """
1068 Sets the location of theta's zero. (Calls set_theta_offset
1069 with the correct value in radians under the hood.)
1071 loc : str
1072 May be one of "N", "NW", "W", "SW", "S", "SE", "E", or "NE".
1074 offset : float, optional
1075 An offset in degrees to apply from the specified `loc`. **Note:**
1076 this offset is *always* applied counter-clockwise regardless of
1077 the direction setting.
1078 """
1079 mapping = {
1080 'N': np.pi * 0.5,
1081 'NW': np.pi * 0.75,
1082 'W': np.pi,
1083 'SW': np.pi * 1.25,
1084 'S': np.pi * 1.5,
1085 'SE': np.pi * 1.75,
1086 'E': 0,
1087 'NE': np.pi * 0.25}
1088 return self.set_theta_offset(mapping[loc] + np.deg2rad(offset))
1090 def set_theta_direction(self, direction):
1091 """
1092 Set the direction in which theta increases.
1094 clockwise, -1:
1095 Theta increases in the clockwise direction
1097 counterclockwise, anticlockwise, 1:
1098 Theta increases in the counterclockwise direction
1099 """
1100 mtx = self._direction.get_matrix()
1101 if direction in ('clockwise', -1):
1102 mtx[0, 0] = -1
1103 elif direction in ('counterclockwise', 'anticlockwise', 1):
1104 mtx[0, 0] = 1
1105 else:
1106 cbook._check_in_list(
1107 [-1, 1, 'clockwise', 'counterclockwise', 'anticlockwise'],
1108 direction=direction)
1109 self._direction.invalidate()
1111 def get_theta_direction(self):
1112 """
1113 Get the direction in which theta increases.
1115 -1:
1116 Theta increases in the clockwise direction
1118 1:
1119 Theta increases in the counterclockwise direction
1120 """
1121 return self._direction.get_matrix()[0, 0]
1123 def set_rmax(self, rmax):
1124 """
1125 Set the outer radial limit.
1127 Parameters
1128 ----------
1129 rmax : float
1130 """
1131 self.viewLim.y1 = rmax
1133 def get_rmax(self):
1134 """
1135 Returns
1136 -------
1137 float
1138 Outer radial limit.
1139 """
1140 return self.viewLim.ymax
1142 def set_rmin(self, rmin):
1143 """
1144 Set the inner radial limit.
1146 Parameters
1147 ----------
1148 rmin : float
1149 """
1150 self.viewLim.y0 = rmin
1152 def get_rmin(self):
1153 """
1154 Returns
1155 -------
1156 float
1157 The inner radial limit.
1158 """
1159 return self.viewLim.ymin
1161 def set_rorigin(self, rorigin):
1162 """
1163 Update the radial origin.
1165 Parameters
1166 ----------
1167 rorigin : float
1168 """
1169 self._originViewLim.locked_y0 = rorigin
1171 def get_rorigin(self):
1172 """
1173 Returns
1174 -------
1175 float
1176 """
1177 return self._originViewLim.y0
1179 def get_rsign(self):
1180 return np.sign(self._originViewLim.y1 - self._originViewLim.y0)
1182 def set_rlim(self, bottom=None, top=None, emit=True, auto=False, **kwargs):
1183 """
1184 See `~.polar.PolarAxes.set_ylim`.
1185 """
1186 if 'rmin' in kwargs:
1187 if bottom is None:
1188 bottom = kwargs.pop('rmin')
1189 else:
1190 raise ValueError('Cannot supply both positional "bottom"'
1191 'argument and kwarg "rmin"')
1192 if 'rmax' in kwargs:
1193 if top is None:
1194 top = kwargs.pop('rmax')
1195 else:
1196 raise ValueError('Cannot supply both positional "top"'
1197 'argument and kwarg "rmax"')
1198 return self.set_ylim(bottom=bottom, top=top, emit=emit, auto=auto,
1199 **kwargs)
1201 def set_ylim(self, bottom=None, top=None, emit=True, auto=False,
1202 *, ymin=None, ymax=None):
1203 """
1204 Set the data limits for the radial axis.
1206 Parameters
1207 ----------
1208 bottom : scalar, optional
1209 The bottom limit (default: None, which leaves the bottom
1210 limit unchanged).
1211 The bottom and top ylims may be passed as the tuple
1212 (*bottom*, *top*) as the first positional argument (or as
1213 the *bottom* keyword argument).
1215 top : scalar, optional
1216 The top limit (default: None, which leaves the top limit
1217 unchanged).
1219 emit : bool, optional
1220 Whether to notify observers of limit change (default: True).
1222 auto : bool or None, optional
1223 Whether to turn on autoscaling of the y-axis. True turns on,
1224 False turns off (default action), None leaves unchanged.
1226 ymin, ymax : scalar, optional
1227 These arguments are deprecated and will be removed in a future
1228 version. They are equivalent to *bottom* and *top* respectively,
1229 and it is an error to pass both *ymin* and *bottom* or
1230 *ymax* and *top*.
1232 Returns
1233 -------
1234 bottom, top : (float, float)
1235 The new y-axis limits in data coordinates.
1236 """
1237 if ymin is not None:
1238 if bottom is not None:
1239 raise ValueError('Cannot supply both positional "bottom" '
1240 'argument and kwarg "ymin"')
1241 else:
1242 bottom = ymin
1243 if ymax is not None:
1244 if top is not None:
1245 raise ValueError('Cannot supply both positional "top" '
1246 'argument and kwarg "ymax"')
1247 else:
1248 top = ymax
1249 if top is None and np.iterable(bottom):
1250 bottom, top = bottom[0], bottom[1]
1251 return super().set_ylim(bottom=bottom, top=top, emit=emit, auto=auto)
1253 def get_rlabel_position(self):
1254 """
1255 Returns
1256 -------
1257 float
1258 The theta position of the radius labels in degrees.
1259 """
1260 return np.rad2deg(self._r_label_position.get_matrix()[0, 2])
1262 def set_rlabel_position(self, value):
1263 """Updates the theta position of the radius labels.
1265 Parameters
1266 ----------
1267 value : number
1268 The angular position of the radius labels in degrees.
1269 """
1270 self._r_label_position.clear().translate(np.deg2rad(value), 0.0)
1272 def set_yscale(self, *args, **kwargs):
1273 Axes.set_yscale(self, *args, **kwargs)
1274 self.yaxis.set_major_locator(
1275 self.RadialLocator(self.yaxis.get_major_locator(), self))
1277 def set_rscale(self, *args, **kwargs):
1278 return Axes.set_yscale(self, *args, **kwargs)
1280 def set_rticks(self, *args, **kwargs):
1281 return Axes.set_yticks(self, *args, **kwargs)
1283 def set_thetagrids(self, angles, labels=None, fmt=None, **kwargs):
1284 """
1285 Set the theta gridlines in a polar plot.
1287 Parameters
1288 ----------
1289 angles : tuple with floats, degrees
1290 The angles of the theta gridlines.
1292 labels : tuple with strings or None
1293 The labels to use at each theta gridline. The
1294 `.projections.polar.ThetaFormatter` will be used if None.
1296 fmt : str or None
1297 Format string used in `matplotlib.ticker.FormatStrFormatter`.
1298 For example '%f'. Note that the angle that is used is in
1299 radians.
1301 Returns
1302 -------
1303 lines, labels : list of `.lines.Line2D`, list of `.text.Text`
1304 *lines* are the theta gridlines and *labels* are the tick labels.
1306 Other Parameters
1307 ----------------
1308 **kwargs
1309 *kwargs* are optional `~.Text` properties for the labels.
1311 See Also
1312 --------
1313 .PolarAxes.set_rgrids
1314 .Axis.get_gridlines
1315 .Axis.get_ticklabels
1316 """
1318 # Make sure we take into account unitized data
1319 angles = self.convert_yunits(angles)
1320 angles = np.deg2rad(angles)
1321 self.set_xticks(angles)
1322 if labels is not None:
1323 self.set_xticklabels(labels)
1324 elif fmt is not None:
1325 self.xaxis.set_major_formatter(mticker.FormatStrFormatter(fmt))
1326 for t in self.xaxis.get_ticklabels():
1327 t.update(kwargs)
1328 return self.xaxis.get_ticklines(), self.xaxis.get_ticklabels()
1330 def set_rgrids(self, radii, labels=None, angle=None, fmt=None,
1331 **kwargs):
1332 """
1333 Set the radial gridlines on a polar plot.
1335 Parameters
1336 ----------
1337 radii : tuple with floats
1338 The radii for the radial gridlines
1340 labels : tuple with strings or None
1341 The labels to use at each radial gridline. The
1342 `matplotlib.ticker.ScalarFormatter` will be used if None.
1344 angle : float
1345 The angular position of the radius labels in degrees.
1347 fmt : str or None
1348 Format string used in `matplotlib.ticker.FormatStrFormatter`.
1349 For example '%f'.
1351 Returns
1352 -------
1353 lines, labels : list of `.lines.Line2D`, list of `.text.Text`
1354 *lines* are the radial gridlines and *labels* are the tick labels.
1356 Other Parameters
1357 ----------------
1358 **kwargs
1359 *kwargs* are optional `~.Text` properties for the labels.
1361 See Also
1362 --------
1363 .PolarAxes.set_thetagrids
1364 .Axis.get_gridlines
1365 .Axis.get_ticklabels
1366 """
1367 # Make sure we take into account unitized data
1368 radii = self.convert_xunits(radii)
1369 radii = np.asarray(radii)
1371 self.set_yticks(radii)
1372 if labels is not None:
1373 self.set_yticklabels(labels)
1374 elif fmt is not None:
1375 self.yaxis.set_major_formatter(mticker.FormatStrFormatter(fmt))
1376 if angle is None:
1377 angle = self.get_rlabel_position()
1378 self.set_rlabel_position(angle)
1379 for t in self.yaxis.get_ticklabels():
1380 t.update(kwargs)
1381 return self.yaxis.get_gridlines(), self.yaxis.get_ticklabels()
1383 def set_xscale(self, scale, *args, **kwargs):
1384 if scale != 'linear':
1385 raise NotImplementedError(
1386 "You can not set the xscale on a polar plot.")
1388 def format_coord(self, theta, r):
1389 """
1390 Return a format string formatting the coordinate using Unicode
1391 characters.
1392 """
1393 if theta < 0:
1394 theta += 2 * np.pi
1395 theta /= np.pi
1396 return ('\N{GREEK SMALL LETTER THETA}=%0.3f\N{GREEK SMALL LETTER PI} '
1397 '(%0.3f\N{DEGREE SIGN}), r=%0.3f') % (theta, theta * 180.0, r)
1399 def get_data_ratio(self):
1400 '''
1401 Return the aspect ratio of the data itself. For a polar plot,
1402 this should always be 1.0
1403 '''
1404 return 1.0
1406 # # # Interactive panning
1408 def can_zoom(self):
1409 """
1410 Return *True* if this axes supports the zoom box button functionality.
1412 Polar axes do not support zoom boxes.
1413 """
1414 return False
1416 def can_pan(self):
1417 """
1418 Return *True* if this axes supports the pan/zoom button functionality.
1420 For polar axes, this is slightly misleading. Both panning and
1421 zooming are performed by the same button. Panning is performed
1422 in azimuth while zooming is done along the radial.
1423 """
1424 return True
1426 def start_pan(self, x, y, button):
1427 angle = np.deg2rad(self.get_rlabel_position())
1428 mode = ''
1429 if button == 1:
1430 epsilon = np.pi / 45.0
1431 t, r = self.transData.inverted().transform((x, y))
1432 if angle - epsilon <= t <= angle + epsilon:
1433 mode = 'drag_r_labels'
1434 elif button == 3:
1435 mode = 'zoom'
1437 self._pan_start = types.SimpleNamespace(
1438 rmax=self.get_rmax(),
1439 trans=self.transData.frozen(),
1440 trans_inverse=self.transData.inverted().frozen(),
1441 r_label_angle=self.get_rlabel_position(),
1442 x=x,
1443 y=y,
1444 mode=mode)
1446 def end_pan(self):
1447 del self._pan_start
1449 def drag_pan(self, button, key, x, y):
1450 p = self._pan_start
1452 if p.mode == 'drag_r_labels':
1453 (startt, startr), (t, r) = p.trans_inverse.transform(
1454 [(p.x, p.y), (x, y)])
1456 # Deal with theta
1457 dt = np.rad2deg(startt - t)
1458 self.set_rlabel_position(p.r_label_angle - dt)
1460 trans, vert1, horiz1 = self.get_yaxis_text1_transform(0.0)
1461 trans, vert2, horiz2 = self.get_yaxis_text2_transform(0.0)
1462 for t in self.yaxis.majorTicks + self.yaxis.minorTicks:
1463 t.label1.set_va(vert1)
1464 t.label1.set_ha(horiz1)
1465 t.label2.set_va(vert2)
1466 t.label2.set_ha(horiz2)
1468 elif p.mode == 'zoom':
1469 (startt, startr), (t, r) = p.trans_inverse.transform(
1470 [(p.x, p.y), (x, y)])
1472 # Deal with r
1473 scale = r / startr
1474 self.set_rmax(p.rmax / scale)
1477# to keep things all self contained, we can put aliases to the Polar classes
1478# defined above. This isn't strictly necessary, but it makes some of the
1479# code more readable (and provides a backwards compatible Polar API)
1480PolarAxes.PolarTransform = PolarTransform
1481PolarAxes.PolarAffine = PolarAffine
1482PolarAxes.InvertedPolarTransform = InvertedPolarTransform
1483PolarAxes.ThetaFormatter = ThetaFormatter
1484PolarAxes.RadialLocator = RadialLocator
1485PolarAxes.ThetaLocator = ThetaLocator