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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

722 statements  

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 

18def _apply_theta_transforms_warn(): 

19 _api.warn_deprecated( 

20 "3.9", 

21 message=( 

22 "Passing `apply_theta_transforms=True` (the default) " 

23 "is deprecated since Matplotlib %(since)s. " 

24 "Support for this will be removed in Matplotlib %(removal)s. " 

25 "To prevent this warning, set `apply_theta_transforms=False`, " 

26 "and make sure to shift theta values before being passed to " 

27 "this transform." 

28 ) 

29 ) 

30 

31 

32class PolarTransform(mtransforms.Transform): 

33 r""" 

34 The base polar transform. 

35 

36 This transform maps polar coordinates :math:`\theta, r` into Cartesian 

37 coordinates :math:`x, y = r \cos(\theta), r \sin(\theta)` 

38 (but does not fully transform into Axes coordinates or 

39 handle positioning in screen space). 

40 

41 This transformation is designed to be applied to data after any scaling 

42 along the radial axis (e.g. log-scaling) has been applied to the input 

43 data. 

44 

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

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

47 """ 

48 

49 input_dims = output_dims = 2 

50 

51 def __init__(self, axis=None, use_rmin=True, *, 

52 apply_theta_transforms=True, scale_transform=None): 

53 """ 

54 Parameters 

55 ---------- 

56 axis : `~matplotlib.axis.Axis`, optional 

57 Axis associated with this transform. This is used to get the 

58 minimum radial limit. 

59 use_rmin : `bool`, optional 

60 If ``True``, subtract the minimum radial axis limit before 

61 transforming to Cartesian coordinates. *axis* must also be 

62 specified for this to take effect. 

63 """ 

64 super().__init__() 

65 self._axis = axis 

66 self._use_rmin = use_rmin 

67 self._apply_theta_transforms = apply_theta_transforms 

68 self._scale_transform = scale_transform 

69 if apply_theta_transforms: 

70 _apply_theta_transforms_warn() 

71 

72 __str__ = mtransforms._make_str_method( 

73 "_axis", 

74 use_rmin="_use_rmin", 

75 apply_theta_transforms="_apply_theta_transforms") 

76 

77 def _get_rorigin(self): 

78 # Get lower r limit after being scaled by the radial scale transform 

79 return self._scale_transform.transform( 

80 (0, self._axis.get_rorigin()))[1] 

81 

82 @_api.rename_parameter("3.8", "tr", "values") 

83 def transform_non_affine(self, values): 

84 # docstring inherited 

85 theta, r = np.transpose(values) 

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

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

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

89 theta *= self._axis.get_theta_direction() 

90 theta += self._axis.get_theta_offset() 

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

92 r = (r - self._get_rorigin()) * self._axis.get_rsign() 

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

94 return np.column_stack([r * np.cos(theta), r * np.sin(theta)]) 

95 

96 def transform_path_non_affine(self, path): 

97 # docstring inherited 

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

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

100 xys = [] 

101 codes = [] 

102 last_t = last_r = None 

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

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

105 if c == Path.LINETO: 

106 (t, r), = trs 

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

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

109 codes.append(Path.LINETO) 

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

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

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

113 # that behavior here. 

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

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

116 r = ((r - self._get_rorigin()) 

117 * self._axis.get_rsign()) 

118 if last_td <= td: 

119 while td - last_td > 360: 

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

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

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

123 last_td += 360 

124 arc = Path.arc(last_td, td) 

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

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

127 else: 

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

129 # codes but the first one are the same. 

130 while last_td - td > 360: 

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

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

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

134 last_td -= 360 

135 arc = Path.arc(td, last_td) 

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

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

138 else: # Interpolate. 

139 trs = cbook.simple_linear_interpolation( 

140 np.vstack([(last_t, last_r), trs]), 

141 path._interpolation_steps)[1:] 

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

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

144 else: # Not a straight line. 

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

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

147 last_t, last_r = trs[-1] 

148 return Path(xys, codes) 

149 

150 def inverted(self): 

151 # docstring inherited 

152 return PolarAxes.InvertedPolarTransform( 

153 self._axis, self._use_rmin, 

154 apply_theta_transforms=self._apply_theta_transforms 

155 ) 

156 

157 

158class PolarAffine(mtransforms.Affine2DBase): 

159 r""" 

160 The affine part of the polar projection. 

161 

162 Scales the output so that maximum radius rests on the edge of the Axes 

163 circle and the origin is mapped to (0.5, 0.5). The transform applied is 

164 the same to x and y components and given by: 

165 

166 .. math:: 

167 

168 x_{1} = 0.5 \left [ \frac{x_{0}}{(r_{\max} - r_{\min})} + 1 \right ] 

169 

170 :math:`r_{\min}, r_{\max}` are the minimum and maximum radial limits after 

171 any scaling (e.g. log scaling) has been removed. 

172 """ 

173 def __init__(self, scale_transform, limits): 

174 """ 

175 Parameters 

176 ---------- 

177 scale_transform : `~matplotlib.transforms.Transform` 

178 Scaling transform for the data. This is used to remove any scaling 

179 from the radial view limits. 

180 limits : `~matplotlib.transforms.BboxBase` 

181 View limits of the data. The only part of its bounds that is used 

182 is the y limits (for the radius limits). 

183 """ 

184 super().__init__() 

185 self._scale_transform = scale_transform 

186 self._limits = limits 

187 self.set_children(scale_transform, limits) 

188 self._mtx = None 

189 

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

191 

192 def get_matrix(self): 

193 # docstring inherited 

194 if self._invalid: 

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

196 yscale = limits_scaled.ymax - limits_scaled.ymin 

197 affine = mtransforms.Affine2D() \ 

198 .scale(0.5 / yscale) \ 

199 .translate(0.5, 0.5) 

200 self._mtx = affine.get_matrix() 

201 self._inverted = None 

202 self._invalid = 0 

203 return self._mtx 

204 

205 

206class InvertedPolarTransform(mtransforms.Transform): 

207 """ 

208 The inverse of the polar transform, mapping Cartesian 

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

210 """ 

211 input_dims = output_dims = 2 

212 

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

214 *, apply_theta_transforms=True): 

215 """ 

216 Parameters 

217 ---------- 

218 axis : `~matplotlib.axis.Axis`, optional 

219 Axis associated with this transform. This is used to get the 

220 minimum radial limit. 

221 use_rmin : `bool`, optional 

222 If ``True``, add the minimum radial axis limit after 

223 transforming from Cartesian coordinates. *axis* must also be 

224 specified for this to take effect. 

225 """ 

226 super().__init__() 

227 self._axis = axis 

228 self._use_rmin = use_rmin 

229 self._apply_theta_transforms = apply_theta_transforms 

230 if apply_theta_transforms: 

231 _apply_theta_transforms_warn() 

232 

233 __str__ = mtransforms._make_str_method( 

234 "_axis", 

235 use_rmin="_use_rmin", 

236 apply_theta_transforms="_apply_theta_transforms") 

237 

238 @_api.rename_parameter("3.8", "xy", "values") 

239 def transform_non_affine(self, values): 

240 # docstring inherited 

241 x, y = values.T 

242 r = np.hypot(x, y) 

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

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

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

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

247 theta -= self._axis.get_theta_offset() 

248 theta *= self._axis.get_theta_direction() 

249 theta %= 2 * np.pi 

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

251 r += self._axis.get_rorigin() 

252 r *= self._axis.get_rsign() 

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

254 

255 def inverted(self): 

256 # docstring inherited 

257 return PolarAxes.PolarTransform( 

258 self._axis, self._use_rmin, 

259 apply_theta_transforms=self._apply_theta_transforms 

260 ) 

261 

262 

263class ThetaFormatter(mticker.Formatter): 

264 """ 

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

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

267 """ 

268 

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

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

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

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

273 return f"{np.rad2deg(x):0.{digits}f}\N{DEGREE SIGN}" 

274 

275 

276class _AxisWrapper: 

277 def __init__(self, axis): 

278 self._axis = axis 

279 

280 def get_view_interval(self): 

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

282 

283 def set_view_interval(self, vmin, vmax): 

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

285 

286 def get_minpos(self): 

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

288 

289 def get_data_interval(self): 

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

291 

292 def set_data_interval(self, vmin, vmax): 

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

294 

295 def get_tick_space(self): 

296 return self._axis.get_tick_space() 

297 

298 

299class ThetaLocator(mticker.Locator): 

300 """ 

301 Used to locate theta ticks. 

302 

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

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

305 locations of every 45 degrees are returned. 

306 """ 

307 

308 def __init__(self, base): 

309 self.base = base 

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

311 

312 def set_axis(self, axis): 

313 self.axis = _AxisWrapper(axis) 

314 self.base.set_axis(self.axis) 

315 

316 def __call__(self): 

317 lim = self.axis.get_view_interval() 

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

319 return np.deg2rad(min(lim)) + np.arange(8) * 2 * np.pi / 8 

320 else: 

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

322 

323 def view_limits(self, vmin, vmax): 

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

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

326 

327 

328class ThetaTick(maxis.XTick): 

329 """ 

330 A theta-axis tick. 

331 

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

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

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

335 the arc spine. 

336 

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

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

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

340 """ 

341 

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

343 self._text1_translate = mtransforms.ScaledTranslation( 

344 0, 0, axes.figure.dpi_scale_trans) 

345 self._text2_translate = mtransforms.ScaledTranslation( 

346 0, 0, axes.figure.dpi_scale_trans) 

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

348 self.label1.set( 

349 rotation_mode='anchor', 

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

351 self.label2.set( 

352 rotation_mode='anchor', 

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

354 

355 def _apply_params(self, **kwargs): 

356 super()._apply_params(**kwargs) 

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

358 trans = self.label1.get_transform() 

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

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

361 trans = self.label2.get_transform() 

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

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

364 

365 def _update_padding(self, pad, angle): 

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

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

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

369 self._text1_translate.invalidate() 

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

371 self._text2_translate.invalidate() 

372 

373 def update_position(self, loc): 

374 super().update_position(loc) 

375 axes = self.axes 

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

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

378 angle -= np.pi / 2 

379 

380 marker = self.tick1line.get_marker() 

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

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

383 elif marker == mmarkers.TICKDOWN: 

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

385 else: 

386 # Don't modify custom tick line markers. 

387 trans = self.tick1line._marker._transform 

388 self.tick1line._marker._transform = trans 

389 

390 marker = self.tick2line.get_marker() 

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

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

393 elif marker == mmarkers.TICKDOWN: 

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

395 else: 

396 # Don't modify custom tick line markers. 

397 trans = self.tick2line._marker._transform 

398 self.tick2line._marker._transform = trans 

399 

400 mode, user_angle = self._labelrotation 

401 if mode == 'default': 

402 text_angle = user_angle 

403 else: 

404 if text_angle > 90: 

405 text_angle -= 180 

406 elif text_angle < -90: 

407 text_angle += 180 

408 text_angle += user_angle 

409 self.label1.set_rotation(text_angle) 

410 self.label2.set_rotation(text_angle) 

411 

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

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

414 pad = self._pad + 7 

415 self._update_padding(pad, 

416 self._loc * axes.get_theta_direction() + 

417 axes.get_theta_offset()) 

418 

419 

420class ThetaAxis(maxis.XAxis): 

421 """ 

422 A theta Axis. 

423 

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

425 for an angular axis. 

426 """ 

427 __name__ = 'thetaaxis' 

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

429 _tick_class = ThetaTick 

430 

431 def _wrap_locator_formatter(self): 

432 self.set_major_locator(ThetaLocator(self.get_major_locator())) 

433 self.set_major_formatter(ThetaFormatter()) 

434 self.isDefault_majloc = True 

435 self.isDefault_majfmt = True 

436 

437 def clear(self): 

438 # docstring inherited 

439 super().clear() 

440 self.set_ticks_position('none') 

441 self._wrap_locator_formatter() 

442 

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

444 if value != 'linear': 

445 raise NotImplementedError( 

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

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

448 # LinearScale.set_default_locators_and_formatters just set the major 

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

450 # at sensible degree multiples. 

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

452 self._wrap_locator_formatter() 

453 

454 def _copy_tick_props(self, src, dest): 

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

456 if src is None or dest is None: 

457 return 

458 super()._copy_tick_props(src, dest) 

459 

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

461 trans = dest._get_text1_transform()[0] 

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

463 trans = dest._get_text2_transform()[0] 

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

465 

466 

467class RadialLocator(mticker.Locator): 

468 """ 

469 Used to locate radius ticks. 

470 

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

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

473 scale of the *r*-axis). 

474 """ 

475 

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

477 self.base = base 

478 self._axes = axes 

479 

480 def set_axis(self, axis): 

481 self.base.set_axis(axis) 

482 

483 def __call__(self): 

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

485 if self._axes: 

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

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

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

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

490 return self.base() 

491 

492 def _zero_in_bounds(self): 

493 """ 

494 Return True if zero is within the valid values for the 

495 scale of the radial axis. 

496 """ 

497 vmin, vmax = self._axes.yaxis._scale.limit_range_for_scale(0, 1, 1e-5) 

498 return vmin == 0 

499 

500 def nonsingular(self, vmin, vmax): 

501 # docstring inherited 

502 if self._zero_in_bounds() and (vmin, vmax) == (-np.inf, np.inf): 

503 # Initial view limits 

504 return (0, 1) 

505 else: 

506 return self.base.nonsingular(vmin, vmax) 

507 

508 def view_limits(self, vmin, vmax): 

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

510 if self._zero_in_bounds() and vmax > vmin: 

511 # this allows inverted r/y-lims 

512 vmin = min(0, vmin) 

513 return mtransforms.nonsingular(vmin, vmax) 

514 

515 

516class _ThetaShift(mtransforms.ScaledTranslation): 

517 """ 

518 Apply a padding shift based on axes theta limits. 

519 

520 This is used to create padding for radial ticks. 

521 

522 Parameters 

523 ---------- 

524 axes : `~matplotlib.axes.Axes` 

525 The owning Axes; used to determine limits. 

526 pad : float 

527 The padding to apply, in points. 

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

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

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

531 """ 

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

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

534 self.set_children(axes._realViewLim) 

535 self.axes = axes 

536 self.mode = mode 

537 self.pad = pad 

538 

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

540 

541 def get_matrix(self): 

542 if self._invalid: 

543 if self.mode == 'rlabel': 

544 angle = ( 

545 np.deg2rad(self.axes.get_rlabel_position() 

546 * self.axes.get_theta_direction()) 

547 + self.axes.get_theta_offset() 

548 - np.pi / 2 

549 ) 

550 elif self.mode == 'min': 

551 angle = self.axes._realViewLim.xmin - np.pi / 2 

552 elif self.mode == 'max': 

553 angle = self.axes._realViewLim.xmax + np.pi / 2 

554 self._t = (self.pad * np.cos(angle) / 72, self.pad * np.sin(angle) / 72) 

555 return super().get_matrix() 

556 

557 

558class RadialTick(maxis.YTick): 

559 """ 

560 A radial-axis tick. 

561 

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

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

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

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

566 'auto' rotation is enabled. 

567 """ 

568 

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

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

571 self.label1.set_rotation_mode('anchor') 

572 self.label2.set_rotation_mode('anchor') 

573 

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

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

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

577 if mode == 'auto': 

578 if start: 

579 if -90 <= angle <= 90: 

580 return 'left', 'center' 

581 else: 

582 return 'right', 'center' 

583 else: 

584 if -90 <= angle <= 90: 

585 return 'right', 'center' 

586 else: 

587 return 'left', 'center' 

588 else: 

589 if start: 

590 if angle < -68.5: 

591 return 'center', 'top' 

592 elif angle < -23.5: 

593 return 'left', 'top' 

594 elif angle < 22.5: 

595 return 'left', 'center' 

596 elif angle < 67.5: 

597 return 'left', 'bottom' 

598 elif angle < 112.5: 

599 return 'center', 'bottom' 

600 elif angle < 157.5: 

601 return 'right', 'bottom' 

602 elif angle < 202.5: 

603 return 'right', 'center' 

604 elif angle < 247.5: 

605 return 'right', 'top' 

606 else: 

607 return 'center', 'top' 

608 else: 

609 if angle < -68.5: 

610 return 'center', 'bottom' 

611 elif angle < -23.5: 

612 return 'right', 'bottom' 

613 elif angle < 22.5: 

614 return 'right', 'center' 

615 elif angle < 67.5: 

616 return 'right', 'top' 

617 elif angle < 112.5: 

618 return 'center', 'top' 

619 elif angle < 157.5: 

620 return 'left', 'top' 

621 elif angle < 202.5: 

622 return 'left', 'center' 

623 elif angle < 247.5: 

624 return 'left', 'bottom' 

625 else: 

626 return 'center', 'bottom' 

627 

628 def update_position(self, loc): 

629 super().update_position(loc) 

630 axes = self.axes 

631 thetamin = axes.get_thetamin() 

632 thetamax = axes.get_thetamax() 

633 direction = axes.get_theta_direction() 

634 offset_rad = axes.get_theta_offset() 

635 offset = np.rad2deg(offset_rad) 

636 full = _is_full_circle_deg(thetamin, thetamax) 

637 

638 if full: 

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

640 offset) % 360 - 90 

641 tick_angle = 0 

642 else: 

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

644 if direction > 0: 

645 tick_angle = np.deg2rad(angle) 

646 else: 

647 tick_angle = np.deg2rad(angle + 180) 

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

649 mode, user_angle = self._labelrotation 

650 if mode == 'auto': 

651 text_angle += user_angle 

652 else: 

653 text_angle = user_angle 

654 

655 if full: 

656 ha = self.label1.get_horizontalalignment() 

657 va = self.label1.get_verticalalignment() 

658 else: 

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

660 self.label1.set_horizontalalignment(ha) 

661 self.label1.set_verticalalignment(va) 

662 self.label1.set_rotation(text_angle) 

663 

664 marker = self.tick1line.get_marker() 

665 if marker == mmarkers.TICKLEFT: 

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

667 elif marker == '_': 

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

669 elif marker == mmarkers.TICKRIGHT: 

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

671 else: 

672 # Don't modify custom tick line markers. 

673 trans = self.tick1line._marker._transform 

674 self.tick1line._marker._transform = trans 

675 

676 if full: 

677 self.label2.set_visible(False) 

678 self.tick2line.set_visible(False) 

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

680 if direction > 0: 

681 tick_angle = np.deg2rad(angle) 

682 else: 

683 tick_angle = np.deg2rad(angle + 180) 

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

685 mode, user_angle = self._labelrotation 

686 if mode == 'auto': 

687 text_angle += user_angle 

688 else: 

689 text_angle = user_angle 

690 

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

692 self.label2.set_ha(ha) 

693 self.label2.set_va(va) 

694 self.label2.set_rotation(text_angle) 

695 

696 marker = self.tick2line.get_marker() 

697 if marker == mmarkers.TICKLEFT: 

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

699 elif marker == '_': 

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

701 elif marker == mmarkers.TICKRIGHT: 

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

703 else: 

704 # Don't modify custom tick line markers. 

705 trans = self.tick2line._marker._transform 

706 self.tick2line._marker._transform = trans 

707 

708 

709class RadialAxis(maxis.YAxis): 

710 """ 

711 A radial Axis. 

712 

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

714 for a radial axis. 

715 """ 

716 __name__ = 'radialaxis' 

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

718 _tick_class = RadialTick 

719 

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

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

722 self.sticky_edges.y.append(0) 

723 

724 def _wrap_locator_formatter(self): 

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

726 self.axes)) 

727 self.isDefault_majloc = True 

728 

729 def clear(self): 

730 # docstring inherited 

731 super().clear() 

732 self.set_ticks_position('none') 

733 self._wrap_locator_formatter() 

734 

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

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

737 self._wrap_locator_formatter() 

738 

739 

740def _is_full_circle_deg(thetamin, thetamax): 

741 """ 

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

743 

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

745 """ 

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

747 

748 

749def _is_full_circle_rad(thetamin, thetamax): 

750 """ 

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

752 

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

754 """ 

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

756 

757 

758class _WedgeBbox(mtransforms.Bbox): 

759 """ 

760 Transform (theta, r) wedge Bbox into Axes bounding box. 

761 

762 Parameters 

763 ---------- 

764 center : (float, float) 

765 Center of the wedge 

766 viewLim : `~matplotlib.transforms.Bbox` 

767 Bbox determining the boundaries of the wedge 

768 originLim : `~matplotlib.transforms.Bbox` 

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

770 """ 

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

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

773 self._center = center 

774 self._viewLim = viewLim 

775 self._originLim = originLim 

776 self.set_children(viewLim, originLim) 

777 

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

779 

780 def get_points(self): 

781 # docstring inherited 

782 if self._invalid: 

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

784 # Scale angular limits to work with Wedge. 

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

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

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

788 

789 # Scale radial limits based on origin radius. 

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

791 

792 # Scale radial limits to match axes limits. 

793 rscale = 0.5 / points[1, 1] 

794 points[:, 1] *= rscale 

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

796 

797 # Generate bounding box for wedge. 

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

799 points[0, 0], points[1, 0], 

800 width=width) 

801 self.update_from_path(wedge.get_path()) 

802 

803 # Ensure equal aspect ratio. 

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

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

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

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

808 

809 self._invalid = 0 

810 

811 return self._points 

812 

813 

814class PolarAxes(Axes): 

815 """ 

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

817 

818 Theta starts pointing east and goes anti-clockwise. 

819 """ 

820 name = 'polar' 

821 

822 def __init__(self, *args, 

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

824 **kwargs): 

825 # docstring inherited 

826 self._default_theta_offset = theta_offset 

827 self._default_theta_direction = theta_direction 

828 self._default_rlabel_position = np.deg2rad(rlabel_position) 

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

830 self.use_sticky_edges = True 

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

832 self.clear() 

833 

834 def clear(self): 

835 # docstring inherited 

836 super().clear() 

837 

838 self.title.set_y(1.05) 

839 

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

841 if start: 

842 start.set_visible(False) 

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

844 if end: 

845 end.set_visible(False) 

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

847 

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

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

850 if inner: 

851 inner.set_visible(False) 

852 

853 self.set_rorigin(None) 

854 self.set_theta_offset(self._default_theta_offset) 

855 self.set_theta_direction(self._default_theta_direction) 

856 

857 def _init_axis(self): 

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

859 self.xaxis = ThetaAxis(self, clear=False) 

860 self.yaxis = RadialAxis(self, clear=False) 

861 self.spines['polar'].register_axis(self.yaxis) 

862 

863 def _set_lim_and_transforms(self): 

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

865 # specifies an alternate origin. 

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

867 

868 # Handle angular offset and direction. 

869 self._direction = mtransforms.Affine2D() \ 

870 .scale(self._default_theta_direction, 1.0) 

871 self._theta_offset = mtransforms.Affine2D() \ 

872 .translate(self._default_theta_offset, 0.0) 

873 self.transShift = self._direction + self._theta_offset 

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

875 # orientation and offset. 

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

877 self.transShift) 

878 

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

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

881 self.transScale = mtransforms.TransformWrapper( 

882 mtransforms.IdentityTransform()) 

883 

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

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

886 # circle. 

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

888 self._realViewLim, self._originViewLim) 

889 

890 # Scale the wedge to fill the axes. 

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

892 

893 # Scale the axes to fill the figure. 

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

895 

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

897 # data. This one is aware of rmin 

898 self.transProjection = self.PolarTransform( 

899 self, 

900 apply_theta_transforms=False, 

901 scale_transform=self.transScale 

902 ) 

903 # Add dependency on rorigin. 

904 self.transProjection.set_children(self._originViewLim) 

905 

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

907 # range of the axes 

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

909 self._originViewLim) 

910 

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

912 # way to display coordinates 

913 # 

914 # 1. Remove any radial axis scaling (e.g. log scaling) 

915 # 2. Shift data in the theta direction 

916 # 3. Project the data from polar to cartesian values 

917 # (with the origin in the same place) 

918 # 4. Scale and translate the cartesian values to Axes coordinates 

919 # (here the origin is moved to the lower left of the Axes) 

920 # 5. Move and scale to fill the Axes 

921 # 6. Convert from Axes coordinates to Figure coordinates 

922 self.transData = ( 

923 self.transScale + 

924 self.transShift + 

925 self.transProjection + 

926 ( 

927 self.transProjectionAffine + 

928 self.transWedge + 

929 self.transAxes 

930 ) 

931 ) 

932 

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

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

935 # at the edge of the axis circles. 

936 self._xaxis_transform = ( 

937 mtransforms.blended_transform_factory( 

938 mtransforms.IdentityTransform(), 

939 mtransforms.BboxTransformTo(self.viewLim)) + 

940 self.transData) 

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

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

943 flipr_transform = mtransforms.Affine2D() \ 

944 .translate(0.0, -0.5) \ 

945 .scale(1.0, -1.0) \ 

946 .translate(0.0, 0.5) 

947 self._xaxis_text_transform = flipr_transform + self._xaxis_transform 

948 

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

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

951 # thetamax. 

952 self._yaxis_transform = ( 

953 mtransforms.blended_transform_factory( 

954 mtransforms.BboxTransformTo(self.viewLim), 

955 mtransforms.IdentityTransform()) + 

956 self.transData) 

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

958 self._r_label_position = mtransforms.Affine2D() \ 

959 .translate(self._default_rlabel_position, 0.0) 

960 self._yaxis_text_transform = mtransforms.TransformWrapper( 

961 self._r_label_position + self.transData) 

962 

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

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

965 return self._xaxis_transform 

966 

967 def get_xaxis_text1_transform(self, pad): 

968 return self._xaxis_text_transform, 'center', 'center' 

969 

970 def get_xaxis_text2_transform(self, pad): 

971 return self._xaxis_text_transform, 'center', 'center' 

972 

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

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

975 return self._yaxis_text_transform 

976 elif which == 'grid': 

977 return self._yaxis_transform 

978 else: 

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

980 

981 def get_yaxis_text1_transform(self, pad): 

982 thetamin, thetamax = self._realViewLim.intervalx 

983 if _is_full_circle_rad(thetamin, thetamax): 

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

985 elif self.get_theta_direction() > 0: 

986 halign = 'left' 

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

988 else: 

989 halign = 'right' 

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

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

992 

993 def get_yaxis_text2_transform(self, pad): 

994 if self.get_theta_direction() > 0: 

995 halign = 'right' 

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

997 else: 

998 halign = 'left' 

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

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

1001 

1002 def draw(self, renderer): 

1003 self._unstale_viewLim() 

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

1005 if thetamin > thetamax: 

1006 thetamin, thetamax = thetamax, thetamin 

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

1008 self.get_rsign()) 

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

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

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

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

1013 self.patch.set_center(center) 

1014 self.patch.set_theta1(thetamin) 

1015 self.patch.set_theta2(thetamax) 

1016 

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

1018 radius = edge - center[0] 

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

1020 self.patch.set_radius(radius) 

1021 self.patch.set_width(width) 

1022 

1023 inner_width = radius - width 

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

1025 if inner: 

1026 inner.set_visible(inner_width != 0.0) 

1027 

1028 visible = not _is_full_circle_deg(thetamin, thetamax) 

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

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

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

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

1033 if start: 

1034 start.set_visible(visible) 

1035 if end: 

1036 end.set_visible(visible) 

1037 if visible: 

1038 yaxis_text_transform = self._yaxis_transform 

1039 else: 

1040 yaxis_text_transform = self._r_label_position + self.transData 

1041 if self._yaxis_text_transform != yaxis_text_transform: 

1042 self._yaxis_text_transform.set(yaxis_text_transform) 

1043 self.yaxis.reset_ticks() 

1044 self.yaxis.set_clip_path(self.patch) 

1045 

1046 super().draw(renderer) 

1047 

1048 def _gen_axes_patch(self): 

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

1050 

1051 def _gen_axes_spines(self): 

1052 spines = { 

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

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

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

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

1057 } 

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

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

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

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

1062 return spines 

1063 

1064 def set_thetamax(self, thetamax): 

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

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

1067 

1068 def get_thetamax(self): 

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

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

1071 

1072 def set_thetamin(self, thetamin): 

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

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

1075 

1076 def get_thetamin(self): 

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

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

1079 

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

1081 r""" 

1082 Set the minimum and maximum theta values. 

1083 

1084 Can take the following signatures: 

1085 

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

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

1088 in degrees. 

1089 

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

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

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

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

1094 angle difference is larger than a full circle. 

1095 """ 

1096 orig_lim = self.get_xlim() # in radians 

1097 if 'thetamin' in kwargs: 

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

1099 if 'thetamax' in kwargs: 

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

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

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

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

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

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

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

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

1108 

1109 def set_theta_offset(self, offset): 

1110 """ 

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

1112 """ 

1113 mtx = self._theta_offset.get_matrix() 

1114 mtx[0, 2] = offset 

1115 self._theta_offset.invalidate() 

1116 

1117 def get_theta_offset(self): 

1118 """ 

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

1120 """ 

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

1122 

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

1124 """ 

1125 Set the location of theta's zero. 

1126 

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

1128 

1129 Parameters 

1130 ---------- 

1131 loc : str 

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

1133 offset : float, default: 0 

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

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

1136 the direction setting. 

1137 """ 

1138 mapping = { 

1139 'N': np.pi * 0.5, 

1140 'NW': np.pi * 0.75, 

1141 'W': np.pi, 

1142 'SW': np.pi * 1.25, 

1143 'S': np.pi * 1.5, 

1144 'SE': np.pi * 1.75, 

1145 'E': 0, 

1146 'NE': np.pi * 0.25} 

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

1148 

1149 def set_theta_direction(self, direction): 

1150 """ 

1151 Set the direction in which theta increases. 

1152 

1153 clockwise, -1: 

1154 Theta increases in the clockwise direction 

1155 

1156 counterclockwise, anticlockwise, 1: 

1157 Theta increases in the counterclockwise direction 

1158 """ 

1159 mtx = self._direction.get_matrix() 

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

1161 mtx[0, 0] = -1 

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

1163 mtx[0, 0] = 1 

1164 else: 

1165 _api.check_in_list( 

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

1167 direction=direction) 

1168 self._direction.invalidate() 

1169 

1170 def get_theta_direction(self): 

1171 """ 

1172 Get the direction in which theta increases. 

1173 

1174 -1: 

1175 Theta increases in the clockwise direction 

1176 

1177 1: 

1178 Theta increases in the counterclockwise direction 

1179 """ 

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

1181 

1182 def set_rmax(self, rmax): 

1183 """ 

1184 Set the outer radial limit. 

1185 

1186 Parameters 

1187 ---------- 

1188 rmax : float 

1189 """ 

1190 self.viewLim.y1 = rmax 

1191 

1192 def get_rmax(self): 

1193 """ 

1194 Returns 

1195 ------- 

1196 float 

1197 Outer radial limit. 

1198 """ 

1199 return self.viewLim.ymax 

1200 

1201 def set_rmin(self, rmin): 

1202 """ 

1203 Set the inner radial limit. 

1204 

1205 Parameters 

1206 ---------- 

1207 rmin : float 

1208 """ 

1209 self.viewLim.y0 = rmin 

1210 

1211 def get_rmin(self): 

1212 """ 

1213 Returns 

1214 ------- 

1215 float 

1216 The inner radial limit. 

1217 """ 

1218 return self.viewLim.ymin 

1219 

1220 def set_rorigin(self, rorigin): 

1221 """ 

1222 Update the radial origin. 

1223 

1224 Parameters 

1225 ---------- 

1226 rorigin : float 

1227 """ 

1228 self._originViewLim.locked_y0 = rorigin 

1229 

1230 def get_rorigin(self): 

1231 """ 

1232 Returns 

1233 ------- 

1234 float 

1235 """ 

1236 return self._originViewLim.y0 

1237 

1238 def get_rsign(self): 

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

1240 

1241 def set_rlim(self, bottom=None, top=None, *, 

1242 emit=True, auto=False, **kwargs): 

1243 """ 

1244 Set the radial axis view limits. 

1245 

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

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

1248 

1249 See Also 

1250 -------- 

1251 .Axes.set_ylim 

1252 """ 

1253 if 'rmin' in kwargs: 

1254 if bottom is None: 

1255 bottom = kwargs.pop('rmin') 

1256 else: 

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

1258 'argument and kwarg "rmin"') 

1259 if 'rmax' in kwargs: 

1260 if top is None: 

1261 top = kwargs.pop('rmax') 

1262 else: 

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

1264 'argument and kwarg "rmax"') 

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

1266 **kwargs) 

1267 

1268 def get_rlabel_position(self): 

1269 """ 

1270 Returns 

1271 ------- 

1272 float 

1273 The theta position of the radius labels in degrees. 

1274 """ 

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

1276 

1277 def set_rlabel_position(self, value): 

1278 """ 

1279 Update the theta position of the radius labels. 

1280 

1281 Parameters 

1282 ---------- 

1283 value : number 

1284 The angular position of the radius labels in degrees. 

1285 """ 

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

1287 

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

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

1290 self.yaxis.set_major_locator( 

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

1292 

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

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

1295 

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

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

1298 

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

1300 """ 

1301 Set the theta gridlines in a polar plot. 

1302 

1303 Parameters 

1304 ---------- 

1305 angles : tuple with floats, degrees 

1306 The angles of the theta gridlines. 

1307 

1308 labels : tuple with strings or None 

1309 The labels to use at each theta gridline. The 

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

1311 

1312 fmt : str or None 

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

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

1315 radians. 

1316 

1317 Returns 

1318 ------- 

1319 lines : list of `.lines.Line2D` 

1320 The theta gridlines. 

1321 

1322 labels : list of `.text.Text` 

1323 The tick labels. 

1324 

1325 Other Parameters 

1326 ---------------- 

1327 **kwargs 

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

1329 

1330 .. warning:: 

1331 

1332 This only sets the properties of the current ticks. 

1333 Ticks are not guaranteed to be persistent. Various operations 

1334 can create, delete and modify the Tick instances. There is an 

1335 imminent risk that these settings can get lost if you work on 

1336 the figure further (including also panning/zooming on a 

1337 displayed figure). 

1338 

1339 Use `.set_tick_params` instead if possible. 

1340 

1341 See Also 

1342 -------- 

1343 .PolarAxes.set_rgrids 

1344 .Axis.get_gridlines 

1345 .Axis.get_ticklabels 

1346 """ 

1347 

1348 # Make sure we take into account unitized data 

1349 angles = self.convert_yunits(angles) 

1350 angles = np.deg2rad(angles) 

1351 self.set_xticks(angles) 

1352 if labels is not None: 

1353 self.set_xticklabels(labels) 

1354 elif fmt is not None: 

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

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

1357 t._internal_update(kwargs) 

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

1359 

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

1361 """ 

1362 Set the radial gridlines on a polar plot. 

1363 

1364 Parameters 

1365 ---------- 

1366 radii : tuple with floats 

1367 The radii for the radial gridlines 

1368 

1369 labels : tuple with strings or None 

1370 The labels to use at each radial gridline. The 

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

1372 

1373 angle : float 

1374 The angular position of the radius labels in degrees. 

1375 

1376 fmt : str or None 

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

1378 For example '%f'. 

1379 

1380 Returns 

1381 ------- 

1382 lines : list of `.lines.Line2D` 

1383 The radial gridlines. 

1384 

1385 labels : list of `.text.Text` 

1386 The tick labels. 

1387 

1388 Other Parameters 

1389 ---------------- 

1390 **kwargs 

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

1392 

1393 .. warning:: 

1394 

1395 This only sets the properties of the current ticks. 

1396 Ticks are not guaranteed to be persistent. Various operations 

1397 can create, delete and modify the Tick instances. There is an 

1398 imminent risk that these settings can get lost if you work on 

1399 the figure further (including also panning/zooming on a 

1400 displayed figure). 

1401 

1402 Use `.set_tick_params` instead if possible. 

1403 

1404 See Also 

1405 -------- 

1406 .PolarAxes.set_thetagrids 

1407 .Axis.get_gridlines 

1408 .Axis.get_ticklabels 

1409 """ 

1410 # Make sure we take into account unitized data 

1411 radii = self.convert_xunits(radii) 

1412 radii = np.asarray(radii) 

1413 

1414 self.set_yticks(radii) 

1415 if labels is not None: 

1416 self.set_yticklabels(labels) 

1417 elif fmt is not None: 

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

1419 if angle is None: 

1420 angle = self.get_rlabel_position() 

1421 self.set_rlabel_position(angle) 

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

1423 t._internal_update(kwargs) 

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

1425 

1426 def format_coord(self, theta, r): 

1427 # docstring inherited 

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

1429 screen_xys = screen_xy + np.stack( 

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

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

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

1433 delta_t_halfturns = delta_t / np.pi 

1434 delta_t_degrees = delta_t_halfturns * 180 

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

1436 if theta < 0: 

1437 theta += 2 * np.pi 

1438 theta_halfturns = theta / np.pi 

1439 theta_degrees = theta_halfturns * 180 

1440 

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

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

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

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

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

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

1447 cbook._g_sig_digits(value, delta)) 

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

1449 

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

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

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

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

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

1455 ) 

1456 

1457 def get_data_ratio(self): 

1458 """ 

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

1460 this should always be 1.0 

1461 """ 

1462 return 1.0 

1463 

1464 # # # Interactive panning 

1465 

1466 def can_zoom(self): 

1467 """ 

1468 Return whether this Axes supports the zoom box button functionality. 

1469 

1470 A polar Axes does not support zoom boxes. 

1471 """ 

1472 return False 

1473 

1474 def can_pan(self): 

1475 """ 

1476 Return whether this Axes supports the pan/zoom button functionality. 

1477 

1478 For a polar Axes, this is slightly misleading. Both panning and 

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

1480 in azimuth while zooming is done along the radial. 

1481 """ 

1482 return True 

1483 

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

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

1486 mode = '' 

1487 if button == 1: 

1488 epsilon = np.pi / 45.0 

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

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

1491 mode = 'drag_r_labels' 

1492 elif button == 3: 

1493 mode = 'zoom' 

1494 

1495 self._pan_start = types.SimpleNamespace( 

1496 rmax=self.get_rmax(), 

1497 trans=self.transData.frozen(), 

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

1499 r_label_angle=self.get_rlabel_position(), 

1500 x=x, 

1501 y=y, 

1502 mode=mode) 

1503 

1504 def end_pan(self): 

1505 del self._pan_start 

1506 

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

1508 p = self._pan_start 

1509 

1510 if p.mode == 'drag_r_labels': 

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

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

1513 

1514 # Deal with theta 

1515 dt = np.rad2deg(startt - t) 

1516 self.set_rlabel_position(p.r_label_angle - dt) 

1517 

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

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

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

1521 t.label1.set_va(vert1) 

1522 t.label1.set_ha(horiz1) 

1523 t.label2.set_va(vert2) 

1524 t.label2.set_ha(horiz2) 

1525 

1526 elif p.mode == 'zoom': 

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

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

1529 

1530 # Deal with r 

1531 scale = r / startr 

1532 self.set_rmax(p.rmax / scale) 

1533 

1534 

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

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

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

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

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

1540# that example is unaffected before changing this. 

1541PolarAxes.PolarTransform = PolarTransform 

1542PolarAxes.PolarAffine = PolarAffine 

1543PolarAxes.InvertedPolarTransform = InvertedPolarTransform 

1544PolarAxes.ThetaFormatter = ThetaFormatter 

1545PolarAxes.RadialLocator = RadialLocator 

1546PolarAxes.ThetaLocator = ThetaLocator