polar.py 53 KB

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