polar.py 51 KB

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