Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/matplotlib/spines.py: 17%

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

315 statements  

1from collections.abc import MutableMapping 

2import functools 

3 

4import numpy as np 

5 

6import matplotlib as mpl 

7from matplotlib import _api, _docstring 

8from matplotlib.artist import allow_rasterization 

9import matplotlib.transforms as mtransforms 

10import matplotlib.patches as mpatches 

11import matplotlib.path as mpath 

12 

13 

14class Spine(mpatches.Patch): 

15 """ 

16 An axis spine -- the line noting the data area boundaries. 

17 

18 Spines are the lines connecting the axis tick marks and noting the 

19 boundaries of the data area. They can be placed at arbitrary 

20 positions. See `~.Spine.set_position` for more information. 

21 

22 The default position is ``('outward', 0)``. 

23 

24 Spines are subclasses of `.Patch`, and inherit much of their behavior. 

25 

26 Spines draw a line, a circle, or an arc depending on if 

27 `~.Spine.set_patch_line`, `~.Spine.set_patch_circle`, or 

28 `~.Spine.set_patch_arc` has been called. Line-like is the default. 

29 

30 For examples see :ref:`spines_examples`. 

31 """ 

32 def __str__(self): 

33 return "Spine" 

34 

35 @_docstring.dedent_interpd 

36 def __init__(self, axes, spine_type, path, **kwargs): 

37 """ 

38 Parameters 

39 ---------- 

40 axes : `~matplotlib.axes.Axes` 

41 The `~.axes.Axes` instance containing the spine. 

42 spine_type : str 

43 The spine type. 

44 path : `~matplotlib.path.Path` 

45 The `.Path` instance used to draw the spine. 

46 

47 Other Parameters 

48 ---------------- 

49 **kwargs 

50 Valid keyword arguments are: 

51 

52 %(Patch:kwdoc)s 

53 """ 

54 super().__init__(**kwargs) 

55 self.axes = axes 

56 self.set_figure(self.axes.figure) 

57 self.spine_type = spine_type 

58 self.set_facecolor('none') 

59 self.set_edgecolor(mpl.rcParams['axes.edgecolor']) 

60 self.set_linewidth(mpl.rcParams['axes.linewidth']) 

61 self.set_capstyle('projecting') 

62 self.axis = None 

63 

64 self.set_zorder(2.5) 

65 self.set_transform(self.axes.transData) # default transform 

66 

67 self._bounds = None # default bounds 

68 

69 # Defer initial position determination. (Not much support for 

70 # non-rectangular axes is currently implemented, and this lets 

71 # them pass through the spines machinery without errors.) 

72 self._position = None 

73 _api.check_isinstance(mpath.Path, path=path) 

74 self._path = path 

75 

76 # To support drawing both linear and circular spines, this 

77 # class implements Patch behavior three ways. If 

78 # self._patch_type == 'line', behave like a mpatches.PathPatch 

79 # instance. If self._patch_type == 'circle', behave like a 

80 # mpatches.Ellipse instance. If self._patch_type == 'arc', behave like 

81 # a mpatches.Arc instance. 

82 self._patch_type = 'line' 

83 

84 # Behavior copied from mpatches.Ellipse: 

85 # Note: This cannot be calculated until this is added to an Axes 

86 self._patch_transform = mtransforms.IdentityTransform() 

87 

88 def set_patch_arc(self, center, radius, theta1, theta2): 

89 """Set the spine to be arc-like.""" 

90 self._patch_type = 'arc' 

91 self._center = center 

92 self._width = radius * 2 

93 self._height = radius * 2 

94 self._theta1 = theta1 

95 self._theta2 = theta2 

96 self._path = mpath.Path.arc(theta1, theta2) 

97 # arc drawn on axes transform 

98 self.set_transform(self.axes.transAxes) 

99 self.stale = True 

100 

101 def set_patch_circle(self, center, radius): 

102 """Set the spine to be circular.""" 

103 self._patch_type = 'circle' 

104 self._center = center 

105 self._width = radius * 2 

106 self._height = radius * 2 

107 # circle drawn on axes transform 

108 self.set_transform(self.axes.transAxes) 

109 self.stale = True 

110 

111 def set_patch_line(self): 

112 """Set the spine to be linear.""" 

113 self._patch_type = 'line' 

114 self.stale = True 

115 

116 # Behavior copied from mpatches.Ellipse: 

117 def _recompute_transform(self): 

118 """ 

119 Notes 

120 ----- 

121 This cannot be called until after this has been added to an Axes, 

122 otherwise unit conversion will fail. This makes it very important to 

123 call the accessor method and not directly access the transformation 

124 member variable. 

125 """ 

126 assert self._patch_type in ('arc', 'circle') 

127 center = (self.convert_xunits(self._center[0]), 

128 self.convert_yunits(self._center[1])) 

129 width = self.convert_xunits(self._width) 

130 height = self.convert_yunits(self._height) 

131 self._patch_transform = mtransforms.Affine2D() \ 

132 .scale(width * 0.5, height * 0.5) \ 

133 .translate(*center) 

134 

135 def get_patch_transform(self): 

136 if self._patch_type in ('arc', 'circle'): 

137 self._recompute_transform() 

138 return self._patch_transform 

139 else: 

140 return super().get_patch_transform() 

141 

142 def get_window_extent(self, renderer=None): 

143 """ 

144 Return the window extent of the spines in display space, including 

145 padding for ticks (but not their labels) 

146 

147 See Also 

148 -------- 

149 matplotlib.axes.Axes.get_tightbbox 

150 matplotlib.axes.Axes.get_window_extent 

151 """ 

152 # make sure the location is updated so that transforms etc are correct: 

153 self._adjust_location() 

154 bb = super().get_window_extent(renderer=renderer) 

155 if self.axis is None or not self.axis.get_visible(): 

156 return bb 

157 bboxes = [bb] 

158 drawn_ticks = self.axis._update_ticks() 

159 

160 major_tick = next(iter({*drawn_ticks} & {*self.axis.majorTicks}), None) 

161 minor_tick = next(iter({*drawn_ticks} & {*self.axis.minorTicks}), None) 

162 for tick in [major_tick, minor_tick]: 

163 if tick is None: 

164 continue 

165 bb0 = bb.frozen() 

166 tickl = tick._size 

167 tickdir = tick._tickdir 

168 if tickdir == 'out': 

169 padout = 1 

170 padin = 0 

171 elif tickdir == 'in': 

172 padout = 0 

173 padin = 1 

174 else: 

175 padout = 0.5 

176 padin = 0.5 

177 padout = padout * tickl / 72 * self.figure.dpi 

178 padin = padin * tickl / 72 * self.figure.dpi 

179 

180 if tick.tick1line.get_visible(): 

181 if self.spine_type == 'left': 

182 bb0.x0 = bb0.x0 - padout 

183 bb0.x1 = bb0.x1 + padin 

184 elif self.spine_type == 'bottom': 

185 bb0.y0 = bb0.y0 - padout 

186 bb0.y1 = bb0.y1 + padin 

187 

188 if tick.tick2line.get_visible(): 

189 if self.spine_type == 'right': 

190 bb0.x1 = bb0.x1 + padout 

191 bb0.x0 = bb0.x0 - padin 

192 elif self.spine_type == 'top': 

193 bb0.y1 = bb0.y1 + padout 

194 bb0.y0 = bb0.y0 - padout 

195 bboxes.append(bb0) 

196 

197 return mtransforms.Bbox.union(bboxes) 

198 

199 def get_path(self): 

200 return self._path 

201 

202 def _ensure_position_is_set(self): 

203 if self._position is None: 

204 # default position 

205 self._position = ('outward', 0.0) # in points 

206 self.set_position(self._position) 

207 

208 def register_axis(self, axis): 

209 """ 

210 Register an axis. 

211 

212 An axis should be registered with its corresponding spine from 

213 the Axes instance. This allows the spine to clear any axis 

214 properties when needed. 

215 """ 

216 self.axis = axis 

217 self.stale = True 

218 

219 def clear(self): 

220 """Clear the current spine.""" 

221 self._clear() 

222 if self.axis is not None: 

223 self.axis.clear() 

224 

225 def _clear(self): 

226 """ 

227 Clear things directly related to the spine. 

228 

229 In this way it is possible to avoid clearing the Axis as well when calling 

230 from library code where it is known that the Axis is cleared separately. 

231 """ 

232 self._position = None # clear position 

233 

234 def _adjust_location(self): 

235 """Automatically set spine bounds to the view interval.""" 

236 

237 if self.spine_type == 'circle': 

238 return 

239 

240 if self._bounds is not None: 

241 low, high = self._bounds 

242 elif self.spine_type in ('left', 'right'): 

243 low, high = self.axes.viewLim.intervaly 

244 elif self.spine_type in ('top', 'bottom'): 

245 low, high = self.axes.viewLim.intervalx 

246 else: 

247 raise ValueError(f'unknown spine spine_type: {self.spine_type}') 

248 

249 if self._patch_type == 'arc': 

250 if self.spine_type in ('bottom', 'top'): 

251 try: 

252 direction = self.axes.get_theta_direction() 

253 except AttributeError: 

254 direction = 1 

255 try: 

256 offset = self.axes.get_theta_offset() 

257 except AttributeError: 

258 offset = 0 

259 low = low * direction + offset 

260 high = high * direction + offset 

261 if low > high: 

262 low, high = high, low 

263 

264 self._path = mpath.Path.arc(np.rad2deg(low), np.rad2deg(high)) 

265 

266 if self.spine_type == 'bottom': 

267 rmin, rmax = self.axes.viewLim.intervaly 

268 try: 

269 rorigin = self.axes.get_rorigin() 

270 except AttributeError: 

271 rorigin = rmin 

272 scaled_diameter = (rmin - rorigin) / (rmax - rorigin) 

273 self._height = scaled_diameter 

274 self._width = scaled_diameter 

275 

276 else: 

277 raise ValueError('unable to set bounds for spine "%s"' % 

278 self.spine_type) 

279 else: 

280 v1 = self._path.vertices 

281 assert v1.shape == (2, 2), 'unexpected vertices shape' 

282 if self.spine_type in ['left', 'right']: 

283 v1[0, 1] = low 

284 v1[1, 1] = high 

285 elif self.spine_type in ['bottom', 'top']: 

286 v1[0, 0] = low 

287 v1[1, 0] = high 

288 else: 

289 raise ValueError('unable to set bounds for spine "%s"' % 

290 self.spine_type) 

291 

292 @allow_rasterization 

293 def draw(self, renderer): 

294 self._adjust_location() 

295 ret = super().draw(renderer) 

296 self.stale = False 

297 return ret 

298 

299 def set_position(self, position): 

300 """ 

301 Set the position of the spine. 

302 

303 Spine position is specified by a 2 tuple of (position type, 

304 amount). The position types are: 

305 

306 * 'outward': place the spine out from the data area by the specified 

307 number of points. (Negative values place the spine inwards.) 

308 * 'axes': place the spine at the specified Axes coordinate (0 to 1). 

309 * 'data': place the spine at the specified data coordinate. 

310 

311 Additionally, shorthand notations define a special positions: 

312 

313 * 'center' -> ``('axes', 0.5)`` 

314 * 'zero' -> ``('data', 0.0)`` 

315 

316 Examples 

317 -------- 

318 :doc:`/gallery/spines/spine_placement_demo` 

319 """ 

320 if position in ('center', 'zero'): # special positions 

321 pass 

322 else: 

323 if len(position) != 2: 

324 raise ValueError("position should be 'center' or 2-tuple") 

325 if position[0] not in ['outward', 'axes', 'data']: 

326 raise ValueError("position[0] should be one of 'outward', " 

327 "'axes', or 'data' ") 

328 self._position = position 

329 self.set_transform(self.get_spine_transform()) 

330 if self.axis is not None: 

331 self.axis.reset_ticks() 

332 self.stale = True 

333 

334 def get_position(self): 

335 """Return the spine position.""" 

336 self._ensure_position_is_set() 

337 return self._position 

338 

339 def get_spine_transform(self): 

340 """Return the spine transform.""" 

341 self._ensure_position_is_set() 

342 

343 position = self._position 

344 if isinstance(position, str): 

345 if position == 'center': 

346 position = ('axes', 0.5) 

347 elif position == 'zero': 

348 position = ('data', 0) 

349 assert len(position) == 2, 'position should be 2-tuple' 

350 position_type, amount = position 

351 _api.check_in_list(['axes', 'outward', 'data'], 

352 position_type=position_type) 

353 if self.spine_type in ['left', 'right']: 

354 base_transform = self.axes.get_yaxis_transform(which='grid') 

355 elif self.spine_type in ['top', 'bottom']: 

356 base_transform = self.axes.get_xaxis_transform(which='grid') 

357 else: 

358 raise ValueError(f'unknown spine spine_type: {self.spine_type!r}') 

359 

360 if position_type == 'outward': 

361 if amount == 0: # short circuit commonest case 

362 return base_transform 

363 else: 

364 offset_vec = {'left': (-1, 0), 'right': (1, 0), 

365 'bottom': (0, -1), 'top': (0, 1), 

366 }[self.spine_type] 

367 # calculate x and y offset in dots 

368 offset_dots = amount * np.array(offset_vec) / 72 

369 return (base_transform 

370 + mtransforms.ScaledTranslation( 

371 *offset_dots, self.figure.dpi_scale_trans)) 

372 elif position_type == 'axes': 

373 if self.spine_type in ['left', 'right']: 

374 # keep y unchanged, fix x at amount 

375 return (mtransforms.Affine2D.from_values(0, 0, 0, 1, amount, 0) 

376 + base_transform) 

377 elif self.spine_type in ['bottom', 'top']: 

378 # keep x unchanged, fix y at amount 

379 return (mtransforms.Affine2D.from_values(1, 0, 0, 0, 0, amount) 

380 + base_transform) 

381 elif position_type == 'data': 

382 if self.spine_type in ('right', 'top'): 

383 # The right and top spines have a default position of 1 in 

384 # axes coordinates. When specifying the position in data 

385 # coordinates, we need to calculate the position relative to 0. 

386 amount -= 1 

387 if self.spine_type in ('left', 'right'): 

388 return mtransforms.blended_transform_factory( 

389 mtransforms.Affine2D().translate(amount, 0) 

390 + self.axes.transData, 

391 self.axes.transData) 

392 elif self.spine_type in ('bottom', 'top'): 

393 return mtransforms.blended_transform_factory( 

394 self.axes.transData, 

395 mtransforms.Affine2D().translate(0, amount) 

396 + self.axes.transData) 

397 

398 def set_bounds(self, low=None, high=None): 

399 """ 

400 Set the spine bounds. 

401 

402 Parameters 

403 ---------- 

404 low : float or None, optional 

405 The lower spine bound. Passing *None* leaves the limit unchanged. 

406 

407 The bounds may also be passed as the tuple (*low*, *high*) as the 

408 first positional argument. 

409 

410 .. ACCEPTS: (low: float, high: float) 

411 

412 high : float or None, optional 

413 The higher spine bound. Passing *None* leaves the limit unchanged. 

414 """ 

415 if self.spine_type == 'circle': 

416 raise ValueError( 

417 'set_bounds() method incompatible with circular spines') 

418 if high is None and np.iterable(low): 

419 low, high = low 

420 old_low, old_high = self.get_bounds() or (None, None) 

421 if low is None: 

422 low = old_low 

423 if high is None: 

424 high = old_high 

425 self._bounds = (low, high) 

426 self.stale = True 

427 

428 def get_bounds(self): 

429 """Get the bounds of the spine.""" 

430 return self._bounds 

431 

432 @classmethod 

433 def linear_spine(cls, axes, spine_type, **kwargs): 

434 """Create and return a linear `Spine`.""" 

435 # all values of 0.999 get replaced upon call to set_bounds() 

436 if spine_type == 'left': 

437 path = mpath.Path([(0.0, 0.999), (0.0, 0.999)]) 

438 elif spine_type == 'right': 

439 path = mpath.Path([(1.0, 0.999), (1.0, 0.999)]) 

440 elif spine_type == 'bottom': 

441 path = mpath.Path([(0.999, 0.0), (0.999, 0.0)]) 

442 elif spine_type == 'top': 

443 path = mpath.Path([(0.999, 1.0), (0.999, 1.0)]) 

444 else: 

445 raise ValueError('unable to make path for spine "%s"' % spine_type) 

446 result = cls(axes, spine_type, path, **kwargs) 

447 result.set_visible(mpl.rcParams[f'axes.spines.{spine_type}']) 

448 

449 return result 

450 

451 @classmethod 

452 def arc_spine(cls, axes, spine_type, center, radius, theta1, theta2, 

453 **kwargs): 

454 """Create and return an arc `Spine`.""" 

455 path = mpath.Path.arc(theta1, theta2) 

456 result = cls(axes, spine_type, path, **kwargs) 

457 result.set_patch_arc(center, radius, theta1, theta2) 

458 return result 

459 

460 @classmethod 

461 def circular_spine(cls, axes, center, radius, **kwargs): 

462 """Create and return a circular `Spine`.""" 

463 path = mpath.Path.unit_circle() 

464 spine_type = 'circle' 

465 result = cls(axes, spine_type, path, **kwargs) 

466 result.set_patch_circle(center, radius) 

467 return result 

468 

469 def set_color(self, c): 

470 """ 

471 Set the edgecolor. 

472 

473 Parameters 

474 ---------- 

475 c : :mpltype:`color` 

476 

477 Notes 

478 ----- 

479 This method does not modify the facecolor (which defaults to "none"), 

480 unlike the `.Patch.set_color` method defined in the parent class. Use 

481 `.Patch.set_facecolor` to set the facecolor. 

482 """ 

483 self.set_edgecolor(c) 

484 self.stale = True 

485 

486 

487class SpinesProxy: 

488 """ 

489 A proxy to broadcast ``set_*()`` and ``set()`` method calls to contained `.Spines`. 

490 

491 The proxy cannot be used for any other operations on its members. 

492 

493 The supported methods are determined dynamically based on the contained 

494 spines. If not all spines support a given method, it's executed only on 

495 the subset of spines that support it. 

496 """ 

497 def __init__(self, spine_dict): 

498 self._spine_dict = spine_dict 

499 

500 def __getattr__(self, name): 

501 broadcast_targets = [spine for spine in self._spine_dict.values() 

502 if hasattr(spine, name)] 

503 if (name != 'set' and not name.startswith('set_')) or not broadcast_targets: 

504 raise AttributeError( 

505 f"'SpinesProxy' object has no attribute '{name}'") 

506 

507 def x(_targets, _funcname, *args, **kwargs): 

508 for spine in _targets: 

509 getattr(spine, _funcname)(*args, **kwargs) 

510 x = functools.partial(x, broadcast_targets, name) 

511 x.__doc__ = broadcast_targets[0].__doc__ 

512 return x 

513 

514 def __dir__(self): 

515 names = [] 

516 for spine in self._spine_dict.values(): 

517 names.extend(name 

518 for name in dir(spine) if name.startswith('set_')) 

519 return list(sorted(set(names))) 

520 

521 

522class Spines(MutableMapping): 

523 r""" 

524 The container of all `.Spine`\s in an Axes. 

525 

526 The interface is dict-like mapping names (e.g. 'left') to `.Spine` objects. 

527 Additionally, it implements some pandas.Series-like features like accessing 

528 elements by attribute:: 

529 

530 spines['top'].set_visible(False) 

531 spines.top.set_visible(False) 

532 

533 Multiple spines can be addressed simultaneously by passing a list:: 

534 

535 spines[['top', 'right']].set_visible(False) 

536 

537 Use an open slice to address all spines:: 

538 

539 spines[:].set_visible(False) 

540 

541 The latter two indexing methods will return a `SpinesProxy` that broadcasts all 

542 ``set_*()`` and ``set()`` calls to its members, but cannot be used for any other 

543 operation. 

544 """ 

545 def __init__(self, **kwargs): 

546 self._dict = kwargs 

547 

548 @classmethod 

549 def from_dict(cls, d): 

550 return cls(**d) 

551 

552 def __getstate__(self): 

553 return self._dict 

554 

555 def __setstate__(self, state): 

556 self.__init__(**state) 

557 

558 def __getattr__(self, name): 

559 try: 

560 return self._dict[name] 

561 except KeyError: 

562 raise AttributeError( 

563 f"'Spines' object does not contain a '{name}' spine") 

564 

565 def __getitem__(self, key): 

566 if isinstance(key, list): 

567 unknown_keys = [k for k in key if k not in self._dict] 

568 if unknown_keys: 

569 raise KeyError(', '.join(unknown_keys)) 

570 return SpinesProxy({k: v for k, v in self._dict.items() 

571 if k in key}) 

572 if isinstance(key, tuple): 

573 raise ValueError('Multiple spines must be passed as a single list') 

574 if isinstance(key, slice): 

575 if key.start is None and key.stop is None and key.step is None: 

576 return SpinesProxy(self._dict) 

577 else: 

578 raise ValueError( 

579 'Spines does not support slicing except for the fully ' 

580 'open slice [:] to access all spines.') 

581 return self._dict[key] 

582 

583 def __setitem__(self, key, value): 

584 # TODO: Do we want to deprecate adding spines? 

585 self._dict[key] = value 

586 

587 def __delitem__(self, key): 

588 # TODO: Do we want to deprecate deleting spines? 

589 del self._dict[key] 

590 

591 def __iter__(self): 

592 return iter(self._dict) 

593 

594 def __len__(self): 

595 return len(self._dict)