Coverage for /usr/lib/python3/dist-packages/matplotlib/projections/polar.py: 20%

714 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-06-14 15:25 +0200

1import math 

2import types 

3 

4import numpy as np 

5 

6import matplotlib as mpl 

7from matplotlib import _api, cbook 

8from matplotlib.axes import Axes 

9import matplotlib.axis as maxis 

10import matplotlib.markers as mmarkers 

11import matplotlib.patches as mpatches 

12from matplotlib.path import Path 

13import matplotlib.ticker as mticker 

14import matplotlib.transforms as mtransforms 

15from matplotlib.spines import Spine 

16 

17 

18class PolarTransform(mtransforms.Transform): 

19 """ 

20 The base polar transform. 

21 

22 This transform maps polar coordinates ``(theta, r)`` into Cartesian 

23 coordinates ``(x, y) = (r * cos(theta), r * sin(theta))`` (but does not 

24 handle positioning in screen space). 

25 

26 Path segments at a fixed radius are automatically transformed to circular 

27 arcs as long as ``path._interpolation_steps > 1``. 

28 """ 

29 

30 input_dims = output_dims = 2 

31 

32 def __init__(self, axis=None, use_rmin=True, 

33 _apply_theta_transforms=True): 

34 super().__init__() 

35 self._axis = axis 

36 self._use_rmin = use_rmin 

37 self._apply_theta_transforms = _apply_theta_transforms 

38 

39 __str__ = mtransforms._make_str_method( 

40 "_axis", 

41 use_rmin="_use_rmin", 

42 _apply_theta_transforms="_apply_theta_transforms") 

43 

44 def transform_non_affine(self, tr): 

45 # docstring inherited 

46 t, r = np.transpose(tr) 

47 # PolarAxes does not use the theta transforms here, but apply them for 

48 # backwards-compatibility if not being used by it. 

49 if self._apply_theta_transforms and self._axis is not None: 

50 t *= self._axis.get_theta_direction() 

51 t += self._axis.get_theta_offset() 

52 if self._use_rmin and self._axis is not None: 

53 r = (r - self._axis.get_rorigin()) * self._axis.get_rsign() 

54 r = np.where(r >= 0, r, np.nan) 

55 return np.column_stack([r * np.cos(t), r * np.sin(t)]) 

56 

57 def transform_path_non_affine(self, path): 

58 # docstring inherited 

59 if not len(path) or path._interpolation_steps == 1: 

60 return Path(self.transform_non_affine(path.vertices), path.codes) 

61 xys = [] 

62 codes = [] 

63 last_t = last_r = None 

64 for trs, c in path.iter_segments(): 

65 trs = trs.reshape((-1, 2)) 

66 if c == Path.LINETO: 

67 (t, r), = trs 

68 if t == last_t: # Same angle: draw a straight line. 

69 xys.extend(self.transform_non_affine(trs)) 

70 codes.append(Path.LINETO) 

71 elif r == last_r: # Same radius: draw an arc. 

72 # The following is complicated by Path.arc() being 

73 # "helpful" and unwrapping the angles, but we don't want 

74 # that behavior here. 

75 last_td, td = np.rad2deg([last_t, t]) 

76 if self._use_rmin and self._axis is not None: 

77 r = ((r - self._axis.get_rorigin()) 

78 * self._axis.get_rsign()) 

79 if last_td <= td: 

80 while td - last_td > 360: 

81 arc = Path.arc(last_td, last_td + 360) 

82 xys.extend(arc.vertices[1:] * r) 

83 codes.extend(arc.codes[1:]) 

84 last_td += 360 

85 arc = Path.arc(last_td, td) 

86 xys.extend(arc.vertices[1:] * r) 

87 codes.extend(arc.codes[1:]) 

88 else: 

89 # The reverse version also relies on the fact that all 

90 # codes but the first one are the same. 

91 while last_td - td > 360: 

92 arc = Path.arc(last_td - 360, last_td) 

93 xys.extend(arc.vertices[::-1][1:] * r) 

94 codes.extend(arc.codes[1:]) 

95 last_td -= 360 

96 arc = Path.arc(td, last_td) 

97 xys.extend(arc.vertices[::-1][1:] * r) 

98 codes.extend(arc.codes[1:]) 

99 else: # Interpolate. 

100 trs = cbook.simple_linear_interpolation( 

101 np.row_stack([(last_t, last_r), trs]), 

102 path._interpolation_steps)[1:] 

103 xys.extend(self.transform_non_affine(trs)) 

104 codes.extend([Path.LINETO] * len(trs)) 

105 else: # Not a straight line. 

106 xys.extend(self.transform_non_affine(trs)) 

107 codes.extend([c] * len(trs)) 

108 last_t, last_r = trs[-1] 

109 return Path(xys, codes) 

110 

111 def inverted(self): 

112 # docstring inherited 

113 return PolarAxes.InvertedPolarTransform(self._axis, self._use_rmin, 

114 self._apply_theta_transforms) 

115 

116 

117class PolarAffine(mtransforms.Affine2DBase): 

118 """ 

119 The affine part of the polar projection. Scales the output so 

120 that maximum radius rests on the edge of the axes circle. 

121 """ 

122 def __init__(self, scale_transform, limits): 

123 """ 

124 *limits* is the view limit of the data. The only part of 

125 its bounds that is used is the y limits (for the radius limits). 

126 The theta range is handled by the non-affine transform. 

127 """ 

128 super().__init__() 

129 self._scale_transform = scale_transform 

130 self._limits = limits 

131 self.set_children(scale_transform, limits) 

132 self._mtx = None 

133 

134 __str__ = mtransforms._make_str_method("_scale_transform", "_limits") 

135 

136 def get_matrix(self): 

137 # docstring inherited 

138 if self._invalid: 

139 limits_scaled = self._limits.transformed(self._scale_transform) 

140 yscale = limits_scaled.ymax - limits_scaled.ymin 

141 affine = mtransforms.Affine2D() \ 

142 .scale(0.5 / yscale) \ 

143 .translate(0.5, 0.5) 

144 self._mtx = affine.get_matrix() 

145 self._inverted = None 

146 self._invalid = 0 

147 return self._mtx 

148 

149 

150class InvertedPolarTransform(mtransforms.Transform): 

151 """ 

152 The inverse of the polar transform, mapping Cartesian 

153 coordinate space *x* and *y* back to *theta* and *r*. 

154 """ 

155 input_dims = output_dims = 2 

156 

157 def __init__(self, axis=None, use_rmin=True, 

158 _apply_theta_transforms=True): 

159 super().__init__() 

160 self._axis = axis 

161 self._use_rmin = use_rmin 

162 self._apply_theta_transforms = _apply_theta_transforms 

163 

164 __str__ = mtransforms._make_str_method( 

165 "_axis", 

166 use_rmin="_use_rmin", 

167 _apply_theta_transforms="_apply_theta_transforms") 

168 

169 def transform_non_affine(self, xy): 

170 # docstring inherited 

171 x, y = xy.T 

172 r = np.hypot(x, y) 

173 theta = (np.arctan2(y, x) + 2 * np.pi) % (2 * np.pi) 

174 # PolarAxes does not use the theta transforms here, but apply them for 

175 # backwards-compatibility if not being used by it. 

176 if self._apply_theta_transforms and self._axis is not None: 

177 theta -= self._axis.get_theta_offset() 

178 theta *= self._axis.get_theta_direction() 

179 theta %= 2 * np.pi 

180 if self._use_rmin and self._axis is not None: 

181 r += self._axis.get_rorigin() 

182 r *= self._axis.get_rsign() 

183 return np.column_stack([theta, r]) 

184 

185 def inverted(self): 

186 # docstring inherited 

187 return PolarAxes.PolarTransform(self._axis, self._use_rmin, 

188 self._apply_theta_transforms) 

189 

190 

191class ThetaFormatter(mticker.Formatter): 

192 """ 

193 Used to format the *theta* tick labels. Converts the native 

194 unit of radians into degrees and adds a degree symbol. 

195 """ 

196 

197 def __call__(self, x, pos=None): 

198 vmin, vmax = self.axis.get_view_interval() 

199 d = np.rad2deg(abs(vmax - vmin)) 

200 digits = max(-int(np.log10(d) - 1.5), 0) 

201 # Use Unicode rather than mathtext with \circ, so that it will work 

202 # correctly with any arbitrary font (assuming it has a degree sign), 

203 # whereas $5\circ$ will only work correctly with one of the supported 

204 # math fonts (Computer Modern and STIX). 

205 return ("{value:0.{digits:d}f}\N{DEGREE SIGN}" 

206 .format(value=np.rad2deg(x), digits=digits)) 

207 

208 

209class _AxisWrapper: 

210 def __init__(self, axis): 

211 self._axis = axis 

212 

213 def get_view_interval(self): 

214 return np.rad2deg(self._axis.get_view_interval()) 

215 

216 def set_view_interval(self, vmin, vmax): 

217 self._axis.set_view_interval(*np.deg2rad((vmin, vmax))) 

218 

219 def get_minpos(self): 

220 return np.rad2deg(self._axis.get_minpos()) 

221 

222 def get_data_interval(self): 

223 return np.rad2deg(self._axis.get_data_interval()) 

224 

225 def set_data_interval(self, vmin, vmax): 

226 self._axis.set_data_interval(*np.deg2rad((vmin, vmax))) 

227 

228 def get_tick_space(self): 

229 return self._axis.get_tick_space() 

230 

231 

232class ThetaLocator(mticker.Locator): 

233 """ 

234 Used to locate theta ticks. 

235 

236 This will work the same as the base locator except in the case that the 

237 view spans the entire circle. In such cases, the previously used default 

238 locations of every 45 degrees are returned. 

239 """ 

240 

241 def __init__(self, base): 

242 self.base = base 

243 self.axis = self.base.axis = _AxisWrapper(self.base.axis) 

244 

245 def set_axis(self, axis): 

246 self.axis = _AxisWrapper(axis) 

247 self.base.set_axis(self.axis) 

248 

249 def __call__(self): 

250 lim = self.axis.get_view_interval() 

251 if _is_full_circle_deg(lim[0], lim[1]): 

252 return np.arange(8) * 2 * np.pi / 8 

253 else: 

254 return np.deg2rad(self.base()) 

255 

256 def refresh(self): 

257 # docstring inherited 

258 return self.base.refresh() 

259 

260 def view_limits(self, vmin, vmax): 

261 vmin, vmax = np.rad2deg((vmin, vmax)) 

262 return np.deg2rad(self.base.view_limits(vmin, vmax)) 

263 

264 

265class ThetaTick(maxis.XTick): 

266 """ 

267 A theta-axis tick. 

268 

269 This subclass of `.XTick` provides angular ticks with some small 

270 modification to their re-positioning such that ticks are rotated based on 

271 tick location. This results in ticks that are correctly perpendicular to 

272 the arc spine. 

273 

274 When 'auto' rotation is enabled, labels are also rotated to be parallel to 

275 the spine. The label padding is also applied here since it's not possible 

276 to use a generic axes transform to produce tick-specific padding. 

277 """ 

278 

279 def __init__(self, axes, *args, **kwargs): 

280 self._text1_translate = mtransforms.ScaledTranslation( 

281 0, 0, axes.figure.dpi_scale_trans) 

282 self._text2_translate = mtransforms.ScaledTranslation( 

283 0, 0, axes.figure.dpi_scale_trans) 

284 super().__init__(axes, *args, **kwargs) 

285 self.label1.set( 

286 rotation_mode='anchor', 

287 transform=self.label1.get_transform() + self._text1_translate) 

288 self.label2.set( 

289 rotation_mode='anchor', 

290 transform=self.label2.get_transform() + self._text2_translate) 

291 

292 def _apply_params(self, **kwargs): 

293 super()._apply_params(**kwargs) 

294 # Ensure transform is correct; sometimes this gets reset. 

295 trans = self.label1.get_transform() 

296 if not trans.contains_branch(self._text1_translate): 

297 self.label1.set_transform(trans + self._text1_translate) 

298 trans = self.label2.get_transform() 

299 if not trans.contains_branch(self._text2_translate): 

300 self.label2.set_transform(trans + self._text2_translate) 

301 

302 def _update_padding(self, pad, angle): 

303 padx = pad * np.cos(angle) / 72 

304 pady = pad * np.sin(angle) / 72 

305 self._text1_translate._t = (padx, pady) 

306 self._text1_translate.invalidate() 

307 self._text2_translate._t = (-padx, -pady) 

308 self._text2_translate.invalidate() 

309 

310 def update_position(self, loc): 

311 super().update_position(loc) 

312 axes = self.axes 

313 angle = loc * axes.get_theta_direction() + axes.get_theta_offset() 

314 text_angle = np.rad2deg(angle) % 360 - 90 

315 angle -= np.pi / 2 

316 

317 marker = self.tick1line.get_marker() 

318 if marker in (mmarkers.TICKUP, '|'): 

319 trans = mtransforms.Affine2D().scale(1, 1).rotate(angle) 

320 elif marker == mmarkers.TICKDOWN: 

321 trans = mtransforms.Affine2D().scale(1, -1).rotate(angle) 

322 else: 

323 # Don't modify custom tick line markers. 

324 trans = self.tick1line._marker._transform 

325 self.tick1line._marker._transform = trans 

326 

327 marker = self.tick2line.get_marker() 

328 if marker in (mmarkers.TICKUP, '|'): 

329 trans = mtransforms.Affine2D().scale(1, 1).rotate(angle) 

330 elif marker == mmarkers.TICKDOWN: 

331 trans = mtransforms.Affine2D().scale(1, -1).rotate(angle) 

332 else: 

333 # Don't modify custom tick line markers. 

334 trans = self.tick2line._marker._transform 

335 self.tick2line._marker._transform = trans 

336 

337 mode, user_angle = self._labelrotation 

338 if mode == 'default': 

339 text_angle = user_angle 

340 else: 

341 if text_angle > 90: 

342 text_angle -= 180 

343 elif text_angle < -90: 

344 text_angle += 180 

345 text_angle += user_angle 

346 self.label1.set_rotation(text_angle) 

347 self.label2.set_rotation(text_angle) 

348 

349 # This extra padding helps preserve the look from previous releases but 

350 # is also needed because labels are anchored to their center. 

351 pad = self._pad + 7 

352 self._update_padding(pad, 

353 self._loc * axes.get_theta_direction() + 

354 axes.get_theta_offset()) 

355 

356 

357class ThetaAxis(maxis.XAxis): 

358 """ 

359 A theta Axis. 

360 

361 This overrides certain properties of an `.XAxis` to provide special-casing 

362 for an angular axis. 

363 """ 

364 __name__ = 'thetaaxis' 

365 axis_name = 'theta' #: Read-only name identifying the axis. 

366 _tick_class = ThetaTick 

367 

368 def _wrap_locator_formatter(self): 

369 self.set_major_locator(ThetaLocator(self.get_major_locator())) 

370 self.set_major_formatter(ThetaFormatter()) 

371 self.isDefault_majloc = True 

372 self.isDefault_majfmt = True 

373 

374 def clear(self): 

375 # docstring inherited 

376 super().clear() 

377 self.set_ticks_position('none') 

378 self._wrap_locator_formatter() 

379 

380 def _set_scale(self, value, **kwargs): 

381 if value != 'linear': 

382 raise NotImplementedError( 

383 "The xscale cannot be set on a polar plot") 

384 super()._set_scale(value, **kwargs) 

385 # LinearScale.set_default_locators_and_formatters just set the major 

386 # locator to be an AutoLocator, so we customize it here to have ticks 

387 # at sensible degree multiples. 

388 self.get_major_locator().set_params(steps=[1, 1.5, 3, 4.5, 9, 10]) 

389 self._wrap_locator_formatter() 

390 

391 def _copy_tick_props(self, src, dest): 

392 """Copy the props from src tick to dest tick.""" 

393 if src is None or dest is None: 

394 return 

395 super()._copy_tick_props(src, dest) 

396 

397 # Ensure that tick transforms are independent so that padding works. 

398 trans = dest._get_text1_transform()[0] 

399 dest.label1.set_transform(trans + dest._text1_translate) 

400 trans = dest._get_text2_transform()[0] 

401 dest.label2.set_transform(trans + dest._text2_translate) 

402 

403 

404class RadialLocator(mticker.Locator): 

405 """ 

406 Used to locate radius ticks. 

407 

408 Ensures that all ticks are strictly positive. For all other tasks, it 

409 delegates to the base `.Locator` (which may be different depending on the 

410 scale of the *r*-axis). 

411 """ 

412 

413 def __init__(self, base, axes=None): 

414 self.base = base 

415 self._axes = axes 

416 

417 def set_axis(self, axis): 

418 self.base.set_axis(axis) 

419 

420 def __call__(self): 

421 # Ensure previous behaviour with full circle non-annular views. 

422 if self._axes: 

423 if _is_full_circle_rad(*self._axes.viewLim.intervalx): 

424 rorigin = self._axes.get_rorigin() * self._axes.get_rsign() 

425 if self._axes.get_rmin() <= rorigin: 

426 return [tick for tick in self.base() if tick > rorigin] 

427 return self.base() 

428 

429 def nonsingular(self, vmin, vmax): 

430 # docstring inherited 

431 return ((0, 1) if (vmin, vmax) == (-np.inf, np.inf) # Init. limits. 

432 else self.base.nonsingular(vmin, vmax)) 

433 

434 def view_limits(self, vmin, vmax): 

435 vmin, vmax = self.base.view_limits(vmin, vmax) 

436 if vmax > vmin: 

437 # this allows inverted r/y-lims 

438 vmin = min(0, vmin) 

439 return mtransforms.nonsingular(vmin, vmax) 

440 

441 

442class _ThetaShift(mtransforms.ScaledTranslation): 

443 """ 

444 Apply a padding shift based on axes theta limits. 

445 

446 This is used to create padding for radial ticks. 

447 

448 Parameters 

449 ---------- 

450 axes : `~matplotlib.axes.Axes` 

451 The owning axes; used to determine limits. 

452 pad : float 

453 The padding to apply, in points. 

454 mode : {'min', 'max', 'rlabel'} 

455 Whether to shift away from the start (``'min'``) or the end (``'max'``) 

456 of the axes, or using the rlabel position (``'rlabel'``). 

457 """ 

458 def __init__(self, axes, pad, mode): 

459 super().__init__(pad, pad, axes.figure.dpi_scale_trans) 

460 self.set_children(axes._realViewLim) 

461 self.axes = axes 

462 self.mode = mode 

463 self.pad = pad 

464 

465 __str__ = mtransforms._make_str_method("axes", "pad", "mode") 

466 

467 def get_matrix(self): 

468 if self._invalid: 

469 if self.mode == 'rlabel': 

470 angle = ( 

471 np.deg2rad(self.axes.get_rlabel_position()) * 

472 self.axes.get_theta_direction() + 

473 self.axes.get_theta_offset() 

474 ) 

475 else: 

476 if self.mode == 'min': 

477 angle = self.axes._realViewLim.xmin 

478 elif self.mode == 'max': 

479 angle = self.axes._realViewLim.xmax 

480 

481 if self.mode in ('rlabel', 'min'): 

482 padx = np.cos(angle - np.pi / 2) 

483 pady = np.sin(angle - np.pi / 2) 

484 else: 

485 padx = np.cos(angle + np.pi / 2) 

486 pady = np.sin(angle + np.pi / 2) 

487 

488 self._t = (self.pad * padx / 72, self.pad * pady / 72) 

489 return super().get_matrix() 

490 

491 

492class RadialTick(maxis.YTick): 

493 """ 

494 A radial-axis tick. 

495 

496 This subclass of `.YTick` provides radial ticks with some small 

497 modification to their re-positioning such that ticks are rotated based on 

498 axes limits. This results in ticks that are correctly perpendicular to 

499 the spine. Labels are also rotated to be perpendicular to the spine, when 

500 'auto' rotation is enabled. 

501 """ 

502 

503 def __init__(self, *args, **kwargs): 

504 super().__init__(*args, **kwargs) 

505 self.label1.set_rotation_mode('anchor') 

506 self.label2.set_rotation_mode('anchor') 

507 

508 def _determine_anchor(self, mode, angle, start): 

509 # Note: angle is the (spine angle - 90) because it's used for the tick 

510 # & text setup, so all numbers below are -90 from (normed) spine angle. 

511 if mode == 'auto': 

512 if start: 

513 if -90 <= angle <= 90: 

514 return 'left', 'center' 

515 else: 

516 return 'right', 'center' 

517 else: 

518 if -90 <= angle <= 90: 

519 return 'right', 'center' 

520 else: 

521 return 'left', 'center' 

522 else: 

523 if start: 

524 if angle < -68.5: 

525 return 'center', 'top' 

526 elif angle < -23.5: 

527 return 'left', 'top' 

528 elif angle < 22.5: 

529 return 'left', 'center' 

530 elif angle < 67.5: 

531 return 'left', 'bottom' 

532 elif angle < 112.5: 

533 return 'center', 'bottom' 

534 elif angle < 157.5: 

535 return 'right', 'bottom' 

536 elif angle < 202.5: 

537 return 'right', 'center' 

538 elif angle < 247.5: 

539 return 'right', 'top' 

540 else: 

541 return 'center', 'top' 

542 else: 

543 if angle < -68.5: 

544 return 'center', 'bottom' 

545 elif angle < -23.5: 

546 return 'right', 'bottom' 

547 elif angle < 22.5: 

548 return 'right', 'center' 

549 elif angle < 67.5: 

550 return 'right', 'top' 

551 elif angle < 112.5: 

552 return 'center', 'top' 

553 elif angle < 157.5: 

554 return 'left', 'top' 

555 elif angle < 202.5: 

556 return 'left', 'center' 

557 elif angle < 247.5: 

558 return 'left', 'bottom' 

559 else: 

560 return 'center', 'bottom' 

561 

562 def update_position(self, loc): 

563 super().update_position(loc) 

564 axes = self.axes 

565 thetamin = axes.get_thetamin() 

566 thetamax = axes.get_thetamax() 

567 direction = axes.get_theta_direction() 

568 offset_rad = axes.get_theta_offset() 

569 offset = np.rad2deg(offset_rad) 

570 full = _is_full_circle_deg(thetamin, thetamax) 

571 

572 if full: 

573 angle = (axes.get_rlabel_position() * direction + 

574 offset) % 360 - 90 

575 tick_angle = 0 

576 else: 

577 angle = (thetamin * direction + offset) % 360 - 90 

578 if direction > 0: 

579 tick_angle = np.deg2rad(angle) 

580 else: 

581 tick_angle = np.deg2rad(angle + 180) 

582 text_angle = (angle + 90) % 180 - 90 # between -90 and +90. 

583 mode, user_angle = self._labelrotation 

584 if mode == 'auto': 

585 text_angle += user_angle 

586 else: 

587 text_angle = user_angle 

588 

589 if full: 

590 ha = self.label1.get_horizontalalignment() 

591 va = self.label1.get_verticalalignment() 

592 else: 

593 ha, va = self._determine_anchor(mode, angle, direction > 0) 

594 self.label1.set_horizontalalignment(ha) 

595 self.label1.set_verticalalignment(va) 

596 self.label1.set_rotation(text_angle) 

597 

598 marker = self.tick1line.get_marker() 

599 if marker == mmarkers.TICKLEFT: 

600 trans = mtransforms.Affine2D().rotate(tick_angle) 

601 elif marker == '_': 

602 trans = mtransforms.Affine2D().rotate(tick_angle + np.pi / 2) 

603 elif marker == mmarkers.TICKRIGHT: 

604 trans = mtransforms.Affine2D().scale(-1, 1).rotate(tick_angle) 

605 else: 

606 # Don't modify custom tick line markers. 

607 trans = self.tick1line._marker._transform 

608 self.tick1line._marker._transform = trans 

609 

610 if full: 

611 self.label2.set_visible(False) 

612 self.tick2line.set_visible(False) 

613 angle = (thetamax * direction + offset) % 360 - 90 

614 if direction > 0: 

615 tick_angle = np.deg2rad(angle) 

616 else: 

617 tick_angle = np.deg2rad(angle + 180) 

618 text_angle = (angle + 90) % 180 - 90 # between -90 and +90. 

619 mode, user_angle = self._labelrotation 

620 if mode == 'auto': 

621 text_angle += user_angle 

622 else: 

623 text_angle = user_angle 

624 

625 ha, va = self._determine_anchor(mode, angle, direction < 0) 

626 self.label2.set_ha(ha) 

627 self.label2.set_va(va) 

628 self.label2.set_rotation(text_angle) 

629 

630 marker = self.tick2line.get_marker() 

631 if marker == mmarkers.TICKLEFT: 

632 trans = mtransforms.Affine2D().rotate(tick_angle) 

633 elif marker == '_': 

634 trans = mtransforms.Affine2D().rotate(tick_angle + np.pi / 2) 

635 elif marker == mmarkers.TICKRIGHT: 

636 trans = mtransforms.Affine2D().scale(-1, 1).rotate(tick_angle) 

637 else: 

638 # Don't modify custom tick line markers. 

639 trans = self.tick2line._marker._transform 

640 self.tick2line._marker._transform = trans 

641 

642 

643class RadialAxis(maxis.YAxis): 

644 """ 

645 A radial Axis. 

646 

647 This overrides certain properties of a `.YAxis` to provide special-casing 

648 for a radial axis. 

649 """ 

650 __name__ = 'radialaxis' 

651 axis_name = 'radius' #: Read-only name identifying the axis. 

652 _tick_class = RadialTick 

653 

654 def __init__(self, *args, **kwargs): 

655 super().__init__(*args, **kwargs) 

656 self.sticky_edges.y.append(0) 

657 

658 def _wrap_locator_formatter(self): 

659 self.set_major_locator(RadialLocator(self.get_major_locator(), 

660 self.axes)) 

661 self.isDefault_majloc = True 

662 

663 def clear(self): 

664 # docstring inherited 

665 super().clear() 

666 self.set_ticks_position('none') 

667 self._wrap_locator_formatter() 

668 

669 def _set_scale(self, value, **kwargs): 

670 super()._set_scale(value, **kwargs) 

671 self._wrap_locator_formatter() 

672 

673 

674def _is_full_circle_deg(thetamin, thetamax): 

675 """ 

676 Determine if a wedge (in degrees) spans the full circle. 

677 

678 The condition is derived from :class:`~matplotlib.patches.Wedge`. 

679 """ 

680 return abs(abs(thetamax - thetamin) - 360.0) < 1e-12 

681 

682 

683def _is_full_circle_rad(thetamin, thetamax): 

684 """ 

685 Determine if a wedge (in radians) spans the full circle. 

686 

687 The condition is derived from :class:`~matplotlib.patches.Wedge`. 

688 """ 

689 return abs(abs(thetamax - thetamin) - 2 * np.pi) < 1.74e-14 

690 

691 

692class _WedgeBbox(mtransforms.Bbox): 

693 """ 

694 Transform (theta, r) wedge Bbox into axes bounding box. 

695 

696 Parameters 

697 ---------- 

698 center : (float, float) 

699 Center of the wedge 

700 viewLim : `~matplotlib.transforms.Bbox` 

701 Bbox determining the boundaries of the wedge 

702 originLim : `~matplotlib.transforms.Bbox` 

703 Bbox determining the origin for the wedge, if different from *viewLim* 

704 """ 

705 def __init__(self, center, viewLim, originLim, **kwargs): 

706 super().__init__([[0, 0], [1, 1]], **kwargs) 

707 self._center = center 

708 self._viewLim = viewLim 

709 self._originLim = originLim 

710 self.set_children(viewLim, originLim) 

711 

712 __str__ = mtransforms._make_str_method("_center", "_viewLim", "_originLim") 

713 

714 def get_points(self): 

715 # docstring inherited 

716 if self._invalid: 

717 points = self._viewLim.get_points().copy() 

718 # Scale angular limits to work with Wedge. 

719 points[:, 0] *= 180 / np.pi 

720 if points[0, 0] > points[1, 0]: 

721 points[:, 0] = points[::-1, 0] 

722 

723 # Scale radial limits based on origin radius. 

724 points[:, 1] -= self._originLim.y0 

725 

726 # Scale radial limits to match axes limits. 

727 rscale = 0.5 / points[1, 1] 

728 points[:, 1] *= rscale 

729 width = min(points[1, 1] - points[0, 1], 0.5) 

730 

731 # Generate bounding box for wedge. 

732 wedge = mpatches.Wedge(self._center, points[1, 1], 

733 points[0, 0], points[1, 0], 

734 width=width) 

735 self.update_from_path(wedge.get_path()) 

736 

737 # Ensure equal aspect ratio. 

738 w, h = self._points[1] - self._points[0] 

739 deltah = max(w - h, 0) / 2 

740 deltaw = max(h - w, 0) / 2 

741 self._points += np.array([[-deltaw, -deltah], [deltaw, deltah]]) 

742 

743 self._invalid = 0 

744 

745 return self._points 

746 

747 

748class PolarAxes(Axes): 

749 """ 

750 A polar graph projection, where the input dimensions are *theta*, *r*. 

751 

752 Theta starts pointing east and goes anti-clockwise. 

753 """ 

754 name = 'polar' 

755 

756 def __init__(self, *args, 

757 theta_offset=0, theta_direction=1, rlabel_position=22.5, 

758 **kwargs): 

759 # docstring inherited 

760 self._default_theta_offset = theta_offset 

761 self._default_theta_direction = theta_direction 

762 self._default_rlabel_position = np.deg2rad(rlabel_position) 

763 super().__init__(*args, **kwargs) 

764 self.use_sticky_edges = True 

765 self.set_aspect('equal', adjustable='box', anchor='C') 

766 self.clear() 

767 

768 def clear(self): 

769 # docstring inherited 

770 super().clear() 

771 

772 self.title.set_y(1.05) 

773 

774 start = self.spines.get('start', None) 

775 if start: 

776 start.set_visible(False) 

777 end = self.spines.get('end', None) 

778 if end: 

779 end.set_visible(False) 

780 self.set_xlim(0.0, 2 * np.pi) 

781 

782 self.grid(mpl.rcParams['polaraxes.grid']) 

783 inner = self.spines.get('inner', None) 

784 if inner: 

785 inner.set_visible(False) 

786 

787 self.set_rorigin(None) 

788 self.set_theta_offset(self._default_theta_offset) 

789 self.set_theta_direction(self._default_theta_direction) 

790 

791 def _init_axis(self): 

792 # This is moved out of __init__ because non-separable axes don't use it 

793 self.xaxis = ThetaAxis(self) 

794 self.yaxis = RadialAxis(self) 

795 # Calling polar_axes.xaxis.clear() or polar_axes.xaxis.clear() 

796 # results in weird artifacts. Therefore we disable this for 

797 # now. 

798 # self.spines['polar'].register_axis(self.yaxis) 

799 self._update_transScale() 

800 

801 def _set_lim_and_transforms(self): 

802 # A view limit where the minimum radius can be locked if the user 

803 # specifies an alternate origin. 

804 self._originViewLim = mtransforms.LockableBbox(self.viewLim) 

805 

806 # Handle angular offset and direction. 

807 self._direction = mtransforms.Affine2D() \ 

808 .scale(self._default_theta_direction, 1.0) 

809 self._theta_offset = mtransforms.Affine2D() \ 

810 .translate(self._default_theta_offset, 0.0) 

811 self.transShift = self._direction + self._theta_offset 

812 # A view limit shifted to the correct location after accounting for 

813 # orientation and offset. 

814 self._realViewLim = mtransforms.TransformedBbox(self.viewLim, 

815 self.transShift) 

816 

817 # Transforms the x and y axis separately by a scale factor 

818 # It is assumed that this part will have non-linear components 

819 self.transScale = mtransforms.TransformWrapper( 

820 mtransforms.IdentityTransform()) 

821 

822 # Scale view limit into a bbox around the selected wedge. This may be 

823 # smaller than the usual unit axes rectangle if not plotting the full 

824 # circle. 

825 self.axesLim = _WedgeBbox((0.5, 0.5), 

826 self._realViewLim, self._originViewLim) 

827 

828 # Scale the wedge to fill the axes. 

829 self.transWedge = mtransforms.BboxTransformFrom(self.axesLim) 

830 

831 # Scale the axes to fill the figure. 

832 self.transAxes = mtransforms.BboxTransformTo(self.bbox) 

833 

834 # A (possibly non-linear) projection on the (already scaled) 

835 # data. This one is aware of rmin 

836 self.transProjection = self.PolarTransform( 

837 self, 

838 _apply_theta_transforms=False) 

839 # Add dependency on rorigin. 

840 self.transProjection.set_children(self._originViewLim) 

841 

842 # An affine transformation on the data, generally to limit the 

843 # range of the axes 

844 self.transProjectionAffine = self.PolarAffine(self.transScale, 

845 self._originViewLim) 

846 

847 # The complete data transformation stack -- from data all the 

848 # way to display coordinates 

849 self.transData = ( 

850 self.transScale + self.transShift + self.transProjection + 

851 (self.transProjectionAffine + self.transWedge + self.transAxes)) 

852 

853 # This is the transform for theta-axis ticks. It is 

854 # equivalent to transData, except it always puts r == 0.0 and r == 1.0 

855 # at the edge of the axis circles. 

856 self._xaxis_transform = ( 

857 mtransforms.blended_transform_factory( 

858 mtransforms.IdentityTransform(), 

859 mtransforms.BboxTransformTo(self.viewLim)) + 

860 self.transData) 

861 # The theta labels are flipped along the radius, so that text 1 is on 

862 # the outside by default. This should work the same as before. 

863 flipr_transform = mtransforms.Affine2D() \ 

864 .translate(0.0, -0.5) \ 

865 .scale(1.0, -1.0) \ 

866 .translate(0.0, 0.5) 

867 self._xaxis_text_transform = flipr_transform + self._xaxis_transform 

868 

869 # This is the transform for r-axis ticks. It scales the theta 

870 # axis so the gridlines from 0.0 to 1.0, now go from thetamin to 

871 # thetamax. 

872 self._yaxis_transform = ( 

873 mtransforms.blended_transform_factory( 

874 mtransforms.BboxTransformTo(self.viewLim), 

875 mtransforms.IdentityTransform()) + 

876 self.transData) 

877 # The r-axis labels are put at an angle and padded in the r-direction 

878 self._r_label_position = mtransforms.Affine2D() \ 

879 .translate(self._default_rlabel_position, 0.0) 

880 self._yaxis_text_transform = mtransforms.TransformWrapper( 

881 self._r_label_position + self.transData) 

882 

883 def get_xaxis_transform(self, which='grid'): 

884 _api.check_in_list(['tick1', 'tick2', 'grid'], which=which) 

885 return self._xaxis_transform 

886 

887 def get_xaxis_text1_transform(self, pad): 

888 return self._xaxis_text_transform, 'center', 'center' 

889 

890 def get_xaxis_text2_transform(self, pad): 

891 return self._xaxis_text_transform, 'center', 'center' 

892 

893 def get_yaxis_transform(self, which='grid'): 

894 if which in ('tick1', 'tick2'): 

895 return self._yaxis_text_transform 

896 elif which == 'grid': 

897 return self._yaxis_transform 

898 else: 

899 _api.check_in_list(['tick1', 'tick2', 'grid'], which=which) 

900 

901 def get_yaxis_text1_transform(self, pad): 

902 thetamin, thetamax = self._realViewLim.intervalx 

903 if _is_full_circle_rad(thetamin, thetamax): 

904 return self._yaxis_text_transform, 'bottom', 'left' 

905 elif self.get_theta_direction() > 0: 

906 halign = 'left' 

907 pad_shift = _ThetaShift(self, pad, 'min') 

908 else: 

909 halign = 'right' 

910 pad_shift = _ThetaShift(self, pad, 'max') 

911 return self._yaxis_text_transform + pad_shift, 'center', halign 

912 

913 def get_yaxis_text2_transform(self, pad): 

914 if self.get_theta_direction() > 0: 

915 halign = 'right' 

916 pad_shift = _ThetaShift(self, pad, 'max') 

917 else: 

918 halign = 'left' 

919 pad_shift = _ThetaShift(self, pad, 'min') 

920 return self._yaxis_text_transform + pad_shift, 'center', halign 

921 

922 def draw(self, renderer): 

923 self._unstale_viewLim() 

924 thetamin, thetamax = np.rad2deg(self._realViewLim.intervalx) 

925 if thetamin > thetamax: 

926 thetamin, thetamax = thetamax, thetamin 

927 rmin, rmax = ((self._realViewLim.intervaly - self.get_rorigin()) * 

928 self.get_rsign()) 

929 if isinstance(self.patch, mpatches.Wedge): 

930 # Backwards-compatibility: Any subclassed Axes might override the 

931 # patch to not be the Wedge that PolarAxes uses. 

932 center = self.transWedge.transform((0.5, 0.5)) 

933 self.patch.set_center(center) 

934 self.patch.set_theta1(thetamin) 

935 self.patch.set_theta2(thetamax) 

936 

937 edge, _ = self.transWedge.transform((1, 0)) 

938 radius = edge - center[0] 

939 width = min(radius * (rmax - rmin) / rmax, radius) 

940 self.patch.set_radius(radius) 

941 self.patch.set_width(width) 

942 

943 inner_width = radius - width 

944 inner = self.spines.get('inner', None) 

945 if inner: 

946 inner.set_visible(inner_width != 0.0) 

947 

948 visible = not _is_full_circle_deg(thetamin, thetamax) 

949 # For backwards compatibility, any subclassed Axes might override the 

950 # spines to not include start/end that PolarAxes uses. 

951 start = self.spines.get('start', None) 

952 end = self.spines.get('end', None) 

953 if start: 

954 start.set_visible(visible) 

955 if end: 

956 end.set_visible(visible) 

957 if visible: 

958 yaxis_text_transform = self._yaxis_transform 

959 else: 

960 yaxis_text_transform = self._r_label_position + self.transData 

961 if self._yaxis_text_transform != yaxis_text_transform: 

962 self._yaxis_text_transform.set(yaxis_text_transform) 

963 self.yaxis.reset_ticks() 

964 self.yaxis.set_clip_path(self.patch) 

965 

966 super().draw(renderer) 

967 

968 def _gen_axes_patch(self): 

969 return mpatches.Wedge((0.5, 0.5), 0.5, 0.0, 360.0) 

970 

971 def _gen_axes_spines(self): 

972 spines = { 

973 'polar': Spine.arc_spine(self, 'top', (0.5, 0.5), 0.5, 0, 360), 

974 'start': Spine.linear_spine(self, 'left'), 

975 'end': Spine.linear_spine(self, 'right'), 

976 'inner': Spine.arc_spine(self, 'bottom', (0.5, 0.5), 0.0, 0, 360), 

977 } 

978 spines['polar'].set_transform(self.transWedge + self.transAxes) 

979 spines['inner'].set_transform(self.transWedge + self.transAxes) 

980 spines['start'].set_transform(self._yaxis_transform) 

981 spines['end'].set_transform(self._yaxis_transform) 

982 return spines 

983 

984 def set_thetamax(self, thetamax): 

985 """Set the maximum theta limit in degrees.""" 

986 self.viewLim.x1 = np.deg2rad(thetamax) 

987 

988 def get_thetamax(self): 

989 """Return the maximum theta limit in degrees.""" 

990 return np.rad2deg(self.viewLim.xmax) 

991 

992 def set_thetamin(self, thetamin): 

993 """Set the minimum theta limit in degrees.""" 

994 self.viewLim.x0 = np.deg2rad(thetamin) 

995 

996 def get_thetamin(self): 

997 """Get the minimum theta limit in degrees.""" 

998 return np.rad2deg(self.viewLim.xmin) 

999 

1000 def set_thetalim(self, *args, **kwargs): 

1001 r""" 

1002 Set the minimum and maximum theta values. 

1003 

1004 Can take the following signatures: 

1005 

1006 - ``set_thetalim(minval, maxval)``: Set the limits in radians. 

1007 - ``set_thetalim(thetamin=minval, thetamax=maxval)``: Set the limits 

1008 in degrees. 

1009 

1010 where minval and maxval are the minimum and maximum limits. Values are 

1011 wrapped in to the range :math:`[0, 2\pi]` (in radians), so for example 

1012 it is possible to do ``set_thetalim(-np.pi / 2, np.pi / 2)`` to have 

1013 an axis symmetric around 0. A ValueError is raised if the absolute 

1014 angle difference is larger than a full circle. 

1015 """ 

1016 orig_lim = self.get_xlim() # in radians 

1017 if 'thetamin' in kwargs: 

1018 kwargs['xmin'] = np.deg2rad(kwargs.pop('thetamin')) 

1019 if 'thetamax' in kwargs: 

1020 kwargs['xmax'] = np.deg2rad(kwargs.pop('thetamax')) 

1021 new_min, new_max = self.set_xlim(*args, **kwargs) 

1022 # Parsing all permutations of *args, **kwargs is tricky; it is simpler 

1023 # to let set_xlim() do it and then validate the limits. 

1024 if abs(new_max - new_min) > 2 * np.pi: 

1025 self.set_xlim(orig_lim) # un-accept the change 

1026 raise ValueError("The angle range must be less than a full circle") 

1027 return tuple(np.rad2deg((new_min, new_max))) 

1028 

1029 def set_theta_offset(self, offset): 

1030 """ 

1031 Set the offset for the location of 0 in radians. 

1032 """ 

1033 mtx = self._theta_offset.get_matrix() 

1034 mtx[0, 2] = offset 

1035 self._theta_offset.invalidate() 

1036 

1037 def get_theta_offset(self): 

1038 """ 

1039 Get the offset for the location of 0 in radians. 

1040 """ 

1041 return self._theta_offset.get_matrix()[0, 2] 

1042 

1043 def set_theta_zero_location(self, loc, offset=0.0): 

1044 """ 

1045 Set the location of theta's zero. 

1046 

1047 This simply calls `set_theta_offset` with the correct value in radians. 

1048 

1049 Parameters 

1050 ---------- 

1051 loc : str 

1052 May be one of "N", "NW", "W", "SW", "S", "SE", "E", or "NE". 

1053 offset : float, default: 0 

1054 An offset in degrees to apply from the specified *loc*. **Note:** 

1055 this offset is *always* applied counter-clockwise regardless of 

1056 the direction setting. 

1057 """ 

1058 mapping = { 

1059 'N': np.pi * 0.5, 

1060 'NW': np.pi * 0.75, 

1061 'W': np.pi, 

1062 'SW': np.pi * 1.25, 

1063 'S': np.pi * 1.5, 

1064 'SE': np.pi * 1.75, 

1065 'E': 0, 

1066 'NE': np.pi * 0.25} 

1067 return self.set_theta_offset(mapping[loc] + np.deg2rad(offset)) 

1068 

1069 def set_theta_direction(self, direction): 

1070 """ 

1071 Set the direction in which theta increases. 

1072 

1073 clockwise, -1: 

1074 Theta increases in the clockwise direction 

1075 

1076 counterclockwise, anticlockwise, 1: 

1077 Theta increases in the counterclockwise direction 

1078 """ 

1079 mtx = self._direction.get_matrix() 

1080 if direction in ('clockwise', -1): 

1081 mtx[0, 0] = -1 

1082 elif direction in ('counterclockwise', 'anticlockwise', 1): 

1083 mtx[0, 0] = 1 

1084 else: 

1085 _api.check_in_list( 

1086 [-1, 1, 'clockwise', 'counterclockwise', 'anticlockwise'], 

1087 direction=direction) 

1088 self._direction.invalidate() 

1089 

1090 def get_theta_direction(self): 

1091 """ 

1092 Get the direction in which theta increases. 

1093 

1094 -1: 

1095 Theta increases in the clockwise direction 

1096 

1097 1: 

1098 Theta increases in the counterclockwise direction 

1099 """ 

1100 return self._direction.get_matrix()[0, 0] 

1101 

1102 def set_rmax(self, rmax): 

1103 """ 

1104 Set the outer radial limit. 

1105 

1106 Parameters 

1107 ---------- 

1108 rmax : float 

1109 """ 

1110 self.viewLim.y1 = rmax 

1111 

1112 def get_rmax(self): 

1113 """ 

1114 Returns 

1115 ------- 

1116 float 

1117 Outer radial limit. 

1118 """ 

1119 return self.viewLim.ymax 

1120 

1121 def set_rmin(self, rmin): 

1122 """ 

1123 Set the inner radial limit. 

1124 

1125 Parameters 

1126 ---------- 

1127 rmin : float 

1128 """ 

1129 self.viewLim.y0 = rmin 

1130 

1131 def get_rmin(self): 

1132 """ 

1133 Returns 

1134 ------- 

1135 float 

1136 The inner radial limit. 

1137 """ 

1138 return self.viewLim.ymin 

1139 

1140 def set_rorigin(self, rorigin): 

1141 """ 

1142 Update the radial origin. 

1143 

1144 Parameters 

1145 ---------- 

1146 rorigin : float 

1147 """ 

1148 self._originViewLim.locked_y0 = rorigin 

1149 

1150 def get_rorigin(self): 

1151 """ 

1152 Returns 

1153 ------- 

1154 float 

1155 """ 

1156 return self._originViewLim.y0 

1157 

1158 def get_rsign(self): 

1159 return np.sign(self._originViewLim.y1 - self._originViewLim.y0) 

1160 

1161 @_api.make_keyword_only("3.6", "emit") 

1162 def set_rlim(self, bottom=None, top=None, emit=True, auto=False, **kwargs): 

1163 """ 

1164 Set the radial axis view limits. 

1165 

1166 This function behaves like `.Axes.set_ylim`, but additionally supports 

1167 *rmin* and *rmax* as aliases for *bottom* and *top*. 

1168 

1169 See Also 

1170 -------- 

1171 .Axes.set_ylim 

1172 """ 

1173 if 'rmin' in kwargs: 

1174 if bottom is None: 

1175 bottom = kwargs.pop('rmin') 

1176 else: 

1177 raise ValueError('Cannot supply both positional "bottom"' 

1178 'argument and kwarg "rmin"') 

1179 if 'rmax' in kwargs: 

1180 if top is None: 

1181 top = kwargs.pop('rmax') 

1182 else: 

1183 raise ValueError('Cannot supply both positional "top"' 

1184 'argument and kwarg "rmax"') 

1185 return self.set_ylim(bottom=bottom, top=top, emit=emit, auto=auto, 

1186 **kwargs) 

1187 

1188 def get_rlabel_position(self): 

1189 """ 

1190 Returns 

1191 ------- 

1192 float 

1193 The theta position of the radius labels in degrees. 

1194 """ 

1195 return np.rad2deg(self._r_label_position.get_matrix()[0, 2]) 

1196 

1197 def set_rlabel_position(self, value): 

1198 """ 

1199 Update the theta position of the radius labels. 

1200 

1201 Parameters 

1202 ---------- 

1203 value : number 

1204 The angular position of the radius labels in degrees. 

1205 """ 

1206 self._r_label_position.clear().translate(np.deg2rad(value), 0.0) 

1207 

1208 def set_yscale(self, *args, **kwargs): 

1209 super().set_yscale(*args, **kwargs) 

1210 self.yaxis.set_major_locator( 

1211 self.RadialLocator(self.yaxis.get_major_locator(), self)) 

1212 

1213 def set_rscale(self, *args, **kwargs): 

1214 return Axes.set_yscale(self, *args, **kwargs) 

1215 

1216 def set_rticks(self, *args, **kwargs): 

1217 return Axes.set_yticks(self, *args, **kwargs) 

1218 

1219 def set_thetagrids(self, angles, labels=None, fmt=None, **kwargs): 

1220 """ 

1221 Set the theta gridlines in a polar plot. 

1222 

1223 Parameters 

1224 ---------- 

1225 angles : tuple with floats, degrees 

1226 The angles of the theta gridlines. 

1227 

1228 labels : tuple with strings or None 

1229 The labels to use at each theta gridline. The 

1230 `.projections.polar.ThetaFormatter` will be used if None. 

1231 

1232 fmt : str or None 

1233 Format string used in `matplotlib.ticker.FormatStrFormatter`. 

1234 For example '%f'. Note that the angle that is used is in 

1235 radians. 

1236 

1237 Returns 

1238 ------- 

1239 lines : list of `.lines.Line2D` 

1240 The theta gridlines. 

1241 

1242 labels : list of `.text.Text` 

1243 The tick labels. 

1244 

1245 Other Parameters 

1246 ---------------- 

1247 **kwargs 

1248 *kwargs* are optional `.Text` properties for the labels. 

1249 

1250 See Also 

1251 -------- 

1252 .PolarAxes.set_rgrids 

1253 .Axis.get_gridlines 

1254 .Axis.get_ticklabels 

1255 """ 

1256 

1257 # Make sure we take into account unitized data 

1258 angles = self.convert_yunits(angles) 

1259 angles = np.deg2rad(angles) 

1260 self.set_xticks(angles) 

1261 if labels is not None: 

1262 self.set_xticklabels(labels) 

1263 elif fmt is not None: 

1264 self.xaxis.set_major_formatter(mticker.FormatStrFormatter(fmt)) 

1265 for t in self.xaxis.get_ticklabels(): 

1266 t._internal_update(kwargs) 

1267 return self.xaxis.get_ticklines(), self.xaxis.get_ticklabels() 

1268 

1269 def set_rgrids(self, radii, labels=None, angle=None, fmt=None, **kwargs): 

1270 """ 

1271 Set the radial gridlines on a polar plot. 

1272 

1273 Parameters 

1274 ---------- 

1275 radii : tuple with floats 

1276 The radii for the radial gridlines 

1277 

1278 labels : tuple with strings or None 

1279 The labels to use at each radial gridline. The 

1280 `matplotlib.ticker.ScalarFormatter` will be used if None. 

1281 

1282 angle : float 

1283 The angular position of the radius labels in degrees. 

1284 

1285 fmt : str or None 

1286 Format string used in `matplotlib.ticker.FormatStrFormatter`. 

1287 For example '%f'. 

1288 

1289 Returns 

1290 ------- 

1291 lines : list of `.lines.Line2D` 

1292 The radial gridlines. 

1293 

1294 labels : list of `.text.Text` 

1295 The tick labels. 

1296 

1297 Other Parameters 

1298 ---------------- 

1299 **kwargs 

1300 *kwargs* are optional `.Text` properties for the labels. 

1301 

1302 See Also 

1303 -------- 

1304 .PolarAxes.set_thetagrids 

1305 .Axis.get_gridlines 

1306 .Axis.get_ticklabels 

1307 """ 

1308 # Make sure we take into account unitized data 

1309 radii = self.convert_xunits(radii) 

1310 radii = np.asarray(radii) 

1311 

1312 self.set_yticks(radii) 

1313 if labels is not None: 

1314 self.set_yticklabels(labels) 

1315 elif fmt is not None: 

1316 self.yaxis.set_major_formatter(mticker.FormatStrFormatter(fmt)) 

1317 if angle is None: 

1318 angle = self.get_rlabel_position() 

1319 self.set_rlabel_position(angle) 

1320 for t in self.yaxis.get_ticklabels(): 

1321 t._internal_update(kwargs) 

1322 return self.yaxis.get_gridlines(), self.yaxis.get_ticklabels() 

1323 

1324 def format_coord(self, theta, r): 

1325 # docstring inherited 

1326 screen_xy = self.transData.transform((theta, r)) 

1327 screen_xys = screen_xy + np.stack( 

1328 np.meshgrid([-1, 0, 1], [-1, 0, 1])).reshape((2, -1)).T 

1329 ts, rs = self.transData.inverted().transform(screen_xys).T 

1330 delta_t = abs((ts - theta + np.pi) % (2 * np.pi) - np.pi).max() 

1331 delta_t_halfturns = delta_t / np.pi 

1332 delta_t_degrees = delta_t_halfturns * 180 

1333 delta_r = abs(rs - r).max() 

1334 if theta < 0: 

1335 theta += 2 * np.pi 

1336 theta_halfturns = theta / np.pi 

1337 theta_degrees = theta_halfturns * 180 

1338 

1339 # See ScalarFormatter.format_data_short. For r, use #g-formatting 

1340 # (as for linear axes), but for theta, use f-formatting as scientific 

1341 # notation doesn't make sense and the trailing dot is ugly. 

1342 def format_sig(value, delta, opt, fmt): 

1343 # For "f", only count digits after decimal point. 

1344 prec = (max(0, -math.floor(math.log10(delta))) if fmt == "f" else 

1345 cbook._g_sig_digits(value, delta)) 

1346 return f"{value:-{opt}.{prec}{fmt}}" 

1347 

1348 return ('\N{GREEK SMALL LETTER THETA}={}\N{GREEK SMALL LETTER PI} ' 

1349 '({}\N{DEGREE SIGN}), r={}').format( 

1350 format_sig(theta_halfturns, delta_t_halfturns, "", "f"), 

1351 format_sig(theta_degrees, delta_t_degrees, "", "f"), 

1352 format_sig(r, delta_r, "#", "g"), 

1353 ) 

1354 

1355 def get_data_ratio(self): 

1356 """ 

1357 Return the aspect ratio of the data itself. For a polar plot, 

1358 this should always be 1.0 

1359 """ 

1360 return 1.0 

1361 

1362 # # # Interactive panning 

1363 

1364 def can_zoom(self): 

1365 """ 

1366 Return whether this axes supports the zoom box button functionality. 

1367 

1368 Polar axes do not support zoom boxes. 

1369 """ 

1370 return False 

1371 

1372 def can_pan(self): 

1373 """ 

1374 Return whether this axes supports the pan/zoom button functionality. 

1375 

1376 For polar axes, this is slightly misleading. Both panning and 

1377 zooming are performed by the same button. Panning is performed 

1378 in azimuth while zooming is done along the radial. 

1379 """ 

1380 return True 

1381 

1382 def start_pan(self, x, y, button): 

1383 angle = np.deg2rad(self.get_rlabel_position()) 

1384 mode = '' 

1385 if button == 1: 

1386 epsilon = np.pi / 45.0 

1387 t, r = self.transData.inverted().transform((x, y)) 

1388 if angle - epsilon <= t <= angle + epsilon: 

1389 mode = 'drag_r_labels' 

1390 elif button == 3: 

1391 mode = 'zoom' 

1392 

1393 self._pan_start = types.SimpleNamespace( 

1394 rmax=self.get_rmax(), 

1395 trans=self.transData.frozen(), 

1396 trans_inverse=self.transData.inverted().frozen(), 

1397 r_label_angle=self.get_rlabel_position(), 

1398 x=x, 

1399 y=y, 

1400 mode=mode) 

1401 

1402 def end_pan(self): 

1403 del self._pan_start 

1404 

1405 def drag_pan(self, button, key, x, y): 

1406 p = self._pan_start 

1407 

1408 if p.mode == 'drag_r_labels': 

1409 (startt, startr), (t, r) = p.trans_inverse.transform( 

1410 [(p.x, p.y), (x, y)]) 

1411 

1412 # Deal with theta 

1413 dt = np.rad2deg(startt - t) 

1414 self.set_rlabel_position(p.r_label_angle - dt) 

1415 

1416 trans, vert1, horiz1 = self.get_yaxis_text1_transform(0.0) 

1417 trans, vert2, horiz2 = self.get_yaxis_text2_transform(0.0) 

1418 for t in self.yaxis.majorTicks + self.yaxis.minorTicks: 

1419 t.label1.set_va(vert1) 

1420 t.label1.set_ha(horiz1) 

1421 t.label2.set_va(vert2) 

1422 t.label2.set_ha(horiz2) 

1423 

1424 elif p.mode == 'zoom': 

1425 (startt, startr), (t, r) = p.trans_inverse.transform( 

1426 [(p.x, p.y), (x, y)]) 

1427 

1428 # Deal with r 

1429 scale = r / startr 

1430 self.set_rmax(p.rmax / scale) 

1431 

1432 

1433# To keep things all self-contained, we can put aliases to the Polar classes 

1434# defined above. This isn't strictly necessary, but it makes some of the 

1435# code more readable, and provides a backwards compatible Polar API. In 

1436# particular, this is used by the :doc:`/gallery/specialty_plots/radar_chart` 

1437# example to override PolarTransform on a PolarAxes subclass, so make sure that 

1438# that example is unaffected before changing this. 

1439PolarAxes.PolarTransform = PolarTransform 

1440PolarAxes.PolarAffine = PolarAffine 

1441PolarAxes.InvertedPolarTransform = InvertedPolarTransform 

1442PolarAxes.ThetaFormatter = ThetaFormatter 

1443PolarAxes.RadialLocator = RadialLocator 

1444PolarAxes.ThetaLocator = ThetaLocator